Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-12-23 23:00:35 +01:00
Merge branch 'dev/1.1.0' into natives-java-improvements
# Conflicts: # native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java
Dieser Commit ist enthalten in:
Commit
35856fec04
@ -7,4 +7,4 @@ cache:
|
||||
- $HOME/.gradle/caches/
|
||||
- $HOME/.gradle/wrapper/
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
- openjdk8
|
13
README.md
13
README.md
@ -34,15 +34,4 @@ Once you've built Velocity, you can copy and run the `-all` JAR from
|
||||
and you can configure it from there.
|
||||
|
||||
Alternatively, you can get the proxy JAR from the [downloads](https://www.velocitypowered.com/downloads)
|
||||
page.
|
||||
|
||||
## Status
|
||||
|
||||
Velocity is currently in beta. Production networks are successfully running
|
||||
Velocity with many hundreds of concurrent players online, but your mileage
|
||||
may vary.
|
||||
|
||||
Velocity supports Minecraft 1.8-1.14.2. Velocity is best supported with Paper
|
||||
and SpongeVanilla. Minecraft Forge is fully supported but mod compatibility
|
||||
may vary. Generally, Velocity will support many mods better than BungeeCord
|
||||
or Waterfall do but compatibility can not always be ensured.
|
||||
page.
|
@ -24,9 +24,13 @@ dependencies {
|
||||
compile "net.kyori:text-serializer-plain:${textVersion}"
|
||||
compile 'com.moandjiezana.toml:toml4j:0.7.2'
|
||||
compile "org.slf4j:slf4j-api:${slf4jVersion}"
|
||||
compile 'com.google.inject:guice:4.2.0'
|
||||
compile 'com.google.inject:guice:4.2.2'
|
||||
compile "org.checkerframework:checker-qual:${checkerFrameworkVersion}"
|
||||
|
||||
compile "org.spongepowered:configurate-hocon:${configurateVersion}"
|
||||
compile "org.spongepowered:configurate-yaml:${configurateVersion}"
|
||||
compile "org.spongepowered:configurate-gson:${configurateVersion}"
|
||||
|
||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
}
|
||||
|
@ -10,9 +10,22 @@ public interface CommandManager {
|
||||
*
|
||||
* @param command the command to register
|
||||
* @param aliases the alias to use
|
||||
*
|
||||
* @deprecated This method requires at least one alias, but this is only enforced at runtime.
|
||||
* Prefer {@link #register(String, Command, String...)} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
void register(Command command, String... aliases);
|
||||
|
||||
/**
|
||||
* Registers the specified command with the manager with the specified aliases.
|
||||
*
|
||||
* @param alias the first alias to register
|
||||
* @param command the command to register
|
||||
* @param otherAliases the other aliases to use
|
||||
*/
|
||||
void register(String alias, Command command, String... otherAliases);
|
||||
|
||||
/**
|
||||
* Unregisters a command.
|
||||
*
|
||||
|
60
api/src/main/java/com/velocitypowered/api/command/RawCommand.java
Normale Datei
60
api/src/main/java/com/velocitypowered/api/command/RawCommand.java
Normale Datei
@ -0,0 +1,60 @@
|
||||
package com.velocitypowered.api.command;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* A specialized sub-interface of {@code Command} which indicates that the proxy should pass a
|
||||
* raw command to the command. This is useful for bolting on external command frameworks to
|
||||
* Velocity.
|
||||
*/
|
||||
public interface RawCommand extends Command {
|
||||
/**
|
||||
* Executes the command for the specified {@link CommandSource}.
|
||||
*
|
||||
* @param source the source of this command
|
||||
* @param commandLine the full command line after the command name
|
||||
*/
|
||||
void execute(CommandSource source, String commandLine);
|
||||
|
||||
default void execute(CommandSource source, String @NonNull [] args) {
|
||||
execute(source, String.join(" ", args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides tab complete suggestions for a command for a specified {@link CommandSource}.
|
||||
*
|
||||
* @param source the source to run the command for
|
||||
* @param currentLine the current, partial command line for this command
|
||||
* @return tab complete suggestions
|
||||
*/
|
||||
default List<String> suggest(CommandSource source, String currentLine) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
default List<String> suggest(CommandSource source, String @NonNull [] currentArgs) {
|
||||
return suggest(source, String.join(" ", currentArgs));
|
||||
}
|
||||
|
||||
@Override
|
||||
default boolean hasPermission(CommandSource source, String @NonNull [] args) {
|
||||
return hasPermission(source, String.join(" ", args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to check if the {@code source} has permission to use this command with the provided
|
||||
* {@code args}.
|
||||
*
|
||||
* <p>If this method returns false, the handling will be forwarded onto
|
||||
* the players current server.</p>
|
||||
*
|
||||
* @param source the source of the command
|
||||
* @param commandLine the arguments for this command
|
||||
* @return whether the source has permission
|
||||
*/
|
||||
default boolean hasPermission(CommandSource source, String commandLine) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is fired after a tab complete response is sent by the remote server, for clients on
|
||||
* 1.12.2 and below. You have the opportunity to modify the response sent to the remote player.
|
||||
*/
|
||||
public class TabCompleteEvent {
|
||||
private final Player player;
|
||||
private final String partialMessage;
|
||||
private final List<String> suggestions;
|
||||
|
||||
/**
|
||||
* Constructs a new TabCompleteEvent instance.
|
||||
* @param player the player
|
||||
* @param partialMessage the partial message
|
||||
* @param suggestions the initial list of suggestions
|
||||
*/
|
||||
public TabCompleteEvent(Player player, String partialMessage, List<String> suggestions) {
|
||||
this.player = checkNotNull(player, "player");
|
||||
this.partialMessage = checkNotNull(partialMessage, "partialMessage");
|
||||
this.suggestions = new ArrayList<>(checkNotNull(suggestions, "suggestions"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player requesting the tab completion.
|
||||
* @return the requesting player
|
||||
*/
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message being partially completed.
|
||||
* @return the partial message
|
||||
*/
|
||||
public String getPartialMessage() {
|
||||
return partialMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the suggestions provided to the user, as a mutable list.
|
||||
* @return the suggestions
|
||||
*/
|
||||
public List<String> getSuggestions() {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TabCompleteEvent{"
|
||||
+ "player=" + player
|
||||
+ ", partialMessage='" + partialMessage + '\''
|
||||
+ ", suggestions=" + suggestions
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -29,7 +29,9 @@ public enum ProtocolVersion {
|
||||
MINECRAFT_1_13_2(404, "1.13.2"),
|
||||
MINECRAFT_1_14(477, "1.14"),
|
||||
MINECRAFT_1_14_1(480, "1.14.1"),
|
||||
MINECRAFT_1_14_2(485, "1.14.2");
|
||||
MINECRAFT_1_14_2(485, "1.14.2"),
|
||||
MINECRAFT_1_14_3(490, "1.14.3"),
|
||||
MINECRAFT_1_14_4(498, "1.14.4");
|
||||
|
||||
private final int protocol;
|
||||
private final String name;
|
||||
|
@ -37,12 +37,20 @@ public interface TabList {
|
||||
* Removes the {@link TabListEntry} from the tab list with the {@link GameProfile} identified with
|
||||
* the specified {@link UUID}.
|
||||
*
|
||||
* @param uuid of the
|
||||
* @param uuid of the entry
|
||||
* @return {@link Optional} containing the removed {@link TabListEntry} if present, otherwise
|
||||
* {@link Optional#empty()}
|
||||
*/
|
||||
Optional<TabListEntry> removeEntry(UUID uuid);
|
||||
|
||||
/**
|
||||
* Determines if the specified entry exists in the tab list.
|
||||
*
|
||||
* @param uuid the UUID of the entry
|
||||
* @return {@code true} if it exists, {@code false} if it does not
|
||||
*/
|
||||
boolean containsEntry(UUID uuid);
|
||||
|
||||
/**
|
||||
* Returns an immutable {@link Collection} of the {@link TabListEntry}s in the tab list.
|
||||
*
|
||||
|
@ -182,6 +182,19 @@ public final class ServerPing {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the modified {@code mods} list in the response.
|
||||
* @param mods the mods list to use
|
||||
* @return this build, for chaining
|
||||
*/
|
||||
public Builder mods(ModInfo mods) {
|
||||
Preconditions.checkNotNull(mods, "mods");
|
||||
this.modType = mods.getType();
|
||||
this.mods.clear();
|
||||
this.mods.addAll(mods.getMods());
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clearMods() {
|
||||
this.mods.clear();
|
||||
return this;
|
||||
|
13
build.gradle
13
build.gradle
@ -13,20 +13,21 @@ plugins {
|
||||
|
||||
allprojects {
|
||||
group 'com.velocitypowered'
|
||||
version '1.0.0-SNAPSHOT'
|
||||
version '1.1.0-SNAPSHOT'
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
|
||||
ext {
|
||||
// dependency versions
|
||||
textVersion = '3.0.1'
|
||||
textVersion = '3.0.2'
|
||||
junitVersion = '5.3.0-M1'
|
||||
slf4jVersion = '1.7.25'
|
||||
log4jVersion = '2.11.2'
|
||||
nettyVersion = '4.1.35.Final'
|
||||
nettyVersion = '4.1.38.Final'
|
||||
guavaVersion = '25.1-jre'
|
||||
checkerFrameworkVersion = '2.7.0'
|
||||
configurateVersion = '3.6'
|
||||
|
||||
getCurrentShortRevision = {
|
||||
new ByteArrayOutputStream().withStream { os ->
|
||||
@ -53,6 +54,12 @@ allprojects {
|
||||
maven {
|
||||
url "https://libraries.minecraft.net"
|
||||
}
|
||||
|
||||
// Configurate
|
||||
maven {
|
||||
name = 'sponge'
|
||||
url = 'https://repo.spongepowered.org/maven'
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
|
@ -56,11 +56,16 @@ public class NativeVelocityCipher implements VelocityCipher {
|
||||
int len = source.readableBytes();
|
||||
ByteBuf out = ctx.alloc().directBuffer(len);
|
||||
|
||||
impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len,
|
||||
out.memoryAddress(), encrypt);
|
||||
source.skipBytes(len);
|
||||
out.writerIndex(len);
|
||||
return out;
|
||||
try {
|
||||
impl.process(this.ctx, source.memoryAddress() + source.readerIndex(), len,
|
||||
out.memoryAddress(), encrypt);
|
||||
source.skipBytes(len);
|
||||
out.writerIndex(len);
|
||||
return out;
|
||||
} catch (Exception e) {
|
||||
out.release();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -13,7 +13,12 @@ apply plugin: 'com.github.johnrengelman.shadow'
|
||||
jar {
|
||||
manifest {
|
||||
def buildNumber = System.getenv("BUILD_NUMBER") ?: "unknown"
|
||||
def version = "${project.version} (git-${project.ext.getCurrentShortRevision()}-b${buildNumber})"
|
||||
def version
|
||||
if (project.version.endsWith("-SNAPSHOT")) {
|
||||
version = "${project.version} (git-${project.ext.getCurrentShortRevision()}-b${buildNumber})"
|
||||
} else {
|
||||
version = "${project.version}"
|
||||
}
|
||||
|
||||
attributes 'Main-Class': 'com.velocitypowered.proxy.Velocity'
|
||||
attributes 'Implementation-Title': "Velocity"
|
||||
@ -43,7 +48,6 @@ dependencies {
|
||||
compile "io.netty:netty-handler:${nettyVersion}"
|
||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}"
|
||||
compile "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
||||
compile "io.netty:netty-transport-native-kqueue:${nettyVersion}:osx-x86_64"
|
||||
compile "io.netty:netty-resolver-dns:${nettyVersion}"
|
||||
|
||||
compile "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||
@ -52,14 +56,18 @@ dependencies {
|
||||
compile "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
||||
|
||||
compile 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options
|
||||
compile 'net.minecrell:terminalconsoleappender:1.1.1'
|
||||
runtime 'net.java.dev.jna:jna:4.5.2' // Needed for JLine
|
||||
compile 'net.minecrell:terminalconsoleappender:1.2.0'
|
||||
runtime 'org.jline:jline-terminal-jansi:3.12.1' // Needed for JLine
|
||||
runtime 'com.lmax:disruptor:3.4.2' // Async loggers
|
||||
|
||||
compile 'it.unimi.dsi:fastutil:8.2.2'
|
||||
compile 'it.unimi.dsi:fastutil:8.2.3'
|
||||
compile 'net.kyori:event-method-asm:3.0.0'
|
||||
|
||||
compile 'com.mojang:brigadier:1.0.15'
|
||||
|
||||
compile 'org.asynchttpclient:async-http-client:2.10.1'
|
||||
|
||||
compile 'com.spotify:completable-futures:0.3.2'
|
||||
|
||||
testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
|
@ -3,12 +3,9 @@ package com.velocitypowered.proxy;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufOutputStream;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
@ -21,11 +18,14 @@ import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.asynchttpclient.ListenableFuture;
|
||||
import org.asynchttpclient.Response;
|
||||
|
||||
/**
|
||||
* bStats collects some data for plugin authors.
|
||||
@ -185,40 +185,44 @@ public class Metrics {
|
||||
}
|
||||
|
||||
// Compress the data to save bandwidth
|
||||
ByteBuf reqBody = createResponseBody(data);
|
||||
|
||||
server.getHttpClient().post(new URL(URL), reqBody, request -> {
|
||||
request.headers().add(HttpHeaderNames.CONTENT_ENCODING, "gzip");
|
||||
request.headers().add(HttpHeaderNames.ACCEPT, "application/json");
|
||||
request.headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json");
|
||||
})
|
||||
.whenCompleteAsync((resp, exc) -> {
|
||||
if (logFailedRequests) {
|
||||
if (exc != null) {
|
||||
logger.error("Unable to send metrics to bStats", exc);
|
||||
} else if (resp.getCode() != 429) {
|
||||
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
||||
resp.getCode());
|
||||
}
|
||||
ListenableFuture<Response> future = server.getAsyncHttpClient()
|
||||
.preparePost(URL)
|
||||
.addHeader(HttpHeaderNames.CONTENT_ENCODING, "gzip")
|
||||
.addHeader(HttpHeaderNames.ACCEPT, "application/json")
|
||||
.addHeader(HttpHeaderNames.CONTENT_TYPE, "application/json")
|
||||
.setBody(createResponseBody(data))
|
||||
.execute();
|
||||
future.addListener(() -> {
|
||||
if (logFailedRequests) {
|
||||
try {
|
||||
Response r = future.get();
|
||||
if (r.getStatusCode() != 429) {
|
||||
logger.error("Got HTTP status code {} when sending metrics to bStats",
|
||||
r.getStatusCode());
|
||||
}
|
||||
});
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (ExecutionException e) {
|
||||
logger.error("Unable to send metrics to bStats", e);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
private static ByteBuf createResponseBody(JsonObject object) throws IOException {
|
||||
ByteBuf buf = Unpooled.buffer();
|
||||
private static byte[] createResponseBody(JsonObject object) throws IOException {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try (Writer writer =
|
||||
new BufferedWriter(
|
||||
new OutputStreamWriter(
|
||||
new GZIPOutputStream(new ByteBufOutputStream(buf)), StandardCharsets.UTF_8
|
||||
new GZIPOutputStream(os), StandardCharsets.UTF_8
|
||||
)
|
||||
)
|
||||
) {
|
||||
VelocityServer.GSON.toJson(object, writer);
|
||||
} catch (IOException e) {
|
||||
buf.release();
|
||||
throw e;
|
||||
}
|
||||
return buf;
|
||||
return os.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -30,7 +30,6 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||
import com.velocitypowered.proxy.network.ConnectionManager;
|
||||
import com.velocitypowered.proxy.network.http.NettyHttpClient;
|
||||
import com.velocitypowered.proxy.plugin.VelocityEventManager;
|
||||
import com.velocitypowered.proxy.plugin.VelocityPluginManager;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
@ -59,18 +58,26 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.IntFunction;
|
||||
import java.util.stream.Collectors;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.TranslatableComponent;
|
||||
import net.kyori.text.serializer.gson.GsonComponentSerializer;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.asynchttpclient.AsyncHttpClient;
|
||||
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
|
||||
public class VelocityServer implements ProxyServer {
|
||||
@ -84,7 +91,6 @@ public class VelocityServer implements ProxyServer {
|
||||
private final ConnectionManager cm;
|
||||
private final ProxyOptions options;
|
||||
private @MonotonicNonNull VelocityConfiguration configuration;
|
||||
private @MonotonicNonNull NettyHttpClient httpClient;
|
||||
private @MonotonicNonNull KeyPair serverKeyPair;
|
||||
private final ServerMap servers;
|
||||
private final VelocityCommandManager commandManager = new VelocityCommandManager();
|
||||
@ -167,10 +173,10 @@ public class VelocityServer implements ProxyServer {
|
||||
cm.logChannelInformation();
|
||||
|
||||
// Initialize commands first
|
||||
commandManager.register(new VelocityCommand(this), "velocity");
|
||||
commandManager.register(new ServerCommand(this), "server");
|
||||
commandManager.register(new ShutdownCommand(this), "shutdown", "end");
|
||||
commandManager.register(new GlistCommand(this), "glist");
|
||||
commandManager.register("velocity", new VelocityCommand(this));
|
||||
commandManager.register("server", new ServerCommand(this));
|
||||
commandManager.register("shutdown", new ShutdownCommand(this),"end");
|
||||
commandManager.register("glist", new GlistCommand(this));
|
||||
|
||||
try {
|
||||
Path configPath = Paths.get("velocity.toml");
|
||||
@ -196,7 +202,6 @@ public class VelocityServer implements ProxyServer {
|
||||
}
|
||||
|
||||
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
|
||||
httpClient = new NettyHttpClient(this);
|
||||
loadPlugins();
|
||||
|
||||
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
|
||||
@ -259,15 +264,7 @@ public class VelocityServer implements ProxyServer {
|
||||
logger.info("Loaded {} plugins", pluginManager.getPlugins().size());
|
||||
}
|
||||
|
||||
public EventLoopGroup getWorkerGroup() {
|
||||
return this.cm.getWorkerGroup();
|
||||
}
|
||||
|
||||
public Bootstrap initializeGenericBootstrap() {
|
||||
return this.cm.createWorker();
|
||||
}
|
||||
|
||||
public Bootstrap initializeGenericBootstrap(EventLoopGroup group) {
|
||||
public Bootstrap createBootstrap(@Nullable EventLoopGroup group) {
|
||||
return this.cm.createWorker(group);
|
||||
}
|
||||
|
||||
@ -379,14 +376,35 @@ public class VelocityServer implements ProxyServer {
|
||||
Runnable shutdownProcess = () -> {
|
||||
logger.info("Shutting down the proxy...");
|
||||
|
||||
for (ConnectedPlayer player : ImmutableList.copyOf(connectionsByUuid.values())) {
|
||||
// Shutdown the connection manager, this should be
|
||||
// done first to refuse new connections
|
||||
cm.shutdown();
|
||||
|
||||
ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
|
||||
for (ConnectedPlayer player : players) {
|
||||
player.disconnect(TextComponent.of("Proxy shutting down."));
|
||||
}
|
||||
|
||||
this.cm.shutdown();
|
||||
|
||||
try {
|
||||
if (!eventManager.shutdown() || !scheduler.shutdown()) {
|
||||
boolean timedOut = false;
|
||||
|
||||
try {
|
||||
// Wait for the connections finish tearing down, this
|
||||
// makes sure that all the disconnect events are being fired
|
||||
|
||||
CompletableFuture<Void> playersTeardownFuture = CompletableFuture.allOf(players.stream()
|
||||
.map(ConnectedPlayer::getTeardownFuture)
|
||||
.toArray((IntFunction<CompletableFuture<Void>[]>) CompletableFuture[]::new));
|
||||
|
||||
playersTeardownFuture.get(10, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException | ExecutionException e) {
|
||||
timedOut = true;
|
||||
}
|
||||
|
||||
timedOut = !eventManager.shutdown() || timedOut;
|
||||
timedOut = !scheduler.shutdown() || timedOut;
|
||||
|
||||
if (timedOut) {
|
||||
logger.error("Your plugins took over 10 seconds to shut down.");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
@ -407,8 +425,8 @@ public class VelocityServer implements ProxyServer {
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public NettyHttpClient getHttpClient() {
|
||||
return ensureInitialized(httpClient);
|
||||
public AsyncHttpClient getAsyncHttpClient() {
|
||||
return ensureInitialized(cm).getHttpClient();
|
||||
}
|
||||
|
||||
public Ratelimiter getIpAttemptLimiter() {
|
||||
@ -428,6 +446,9 @@ public class VelocityServer implements ProxyServer {
|
||||
* @return {@code true} if we can register the connection, {@code false} if not
|
||||
*/
|
||||
public boolean canRegisterConnection(ConnectedPlayer connection) {
|
||||
if (configuration.isOnlineMode() && configuration.isOnlineModeKickExistingPlayers()) {
|
||||
return true;
|
||||
}
|
||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||
return !(connectionsByName.containsKey(lowerName)
|
||||
|| connectionsByUuid.containsKey(connection.getUniqueId()));
|
||||
@ -440,12 +461,24 @@ public class VelocityServer implements ProxyServer {
|
||||
*/
|
||||
public boolean registerConnection(ConnectedPlayer connection) {
|
||||
String lowerName = connection.getUsername().toLowerCase(Locale.US);
|
||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||
return false;
|
||||
}
|
||||
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
|
||||
connectionsByName.remove(lowerName, connection);
|
||||
return false;
|
||||
|
||||
if (!this.configuration.isOnlineModeKickExistingPlayers()) {
|
||||
if (connectionsByName.putIfAbsent(lowerName, connection) != null) {
|
||||
return false;
|
||||
}
|
||||
if (connectionsByUuid.putIfAbsent(connection.getUniqueId(), connection) != null) {
|
||||
connectionsByName.remove(lowerName, connection);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
ConnectedPlayer existing = connectionsByUuid.get(connection.getUniqueId());
|
||||
if (existing != null) {
|
||||
existing.disconnect(TranslatableComponent.of("multiplayer.disconnect.duplicate_login"));
|
||||
}
|
||||
|
||||
// We can now replace the entries as needed.
|
||||
connectionsByName.put(lowerName, connection);
|
||||
connectionsByUuid.put(connection.getUniqueId(), connection);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -472,7 +505,7 @@ public class VelocityServer implements ProxyServer {
|
||||
Preconditions.checkNotNull(component, "component");
|
||||
Chat chat = Chat.createClientbound(component);
|
||||
for (ConnectedPlayer player : connectionsByUuid.values()) {
|
||||
player.getMinecraftConnection().write(chat);
|
||||
player.getConnection().write(chat);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,27 +6,38 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import java.util.ArrayList;
|
||||
import com.velocitypowered.api.command.RawCommand;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
public class VelocityCommandManager implements CommandManager {
|
||||
|
||||
private final Map<String, Command> commands = new HashMap<>();
|
||||
private final Map<String, RawCommand> commands = new HashMap<>();
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
public void register(final Command command, final String... aliases) {
|
||||
Preconditions.checkNotNull(aliases, "aliases");
|
||||
Preconditions.checkArgument(aliases.length > 0, "no aliases provided");
|
||||
register(aliases[0], command, Arrays.copyOfRange(aliases, 1, aliases.length));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(String alias, Command command, String... otherAliases) {
|
||||
Preconditions.checkNotNull(alias, "alias");
|
||||
Preconditions.checkNotNull(otherAliases, "otherAliases");
|
||||
Preconditions.checkNotNull(command, "executor");
|
||||
for (int i = 0, length = aliases.length; i < length; i++) {
|
||||
final String alias = aliases[i];
|
||||
Preconditions.checkNotNull(alias, "alias at index %s", i);
|
||||
this.commands.put(alias.toLowerCase(Locale.ENGLISH), command);
|
||||
|
||||
RawCommand rawCmd = RegularCommandWrapper.wrap(command);
|
||||
this.commands.put(alias.toLowerCase(Locale.ENGLISH), rawCmd);
|
||||
|
||||
for (int i = 0, length = otherAliases.length; i < length; i++) {
|
||||
final String alias1 = otherAliases[i];
|
||||
Preconditions.checkNotNull(alias1, "alias at index %s", i + 1);
|
||||
this.commands.put(alias1.toLowerCase(Locale.ENGLISH), rawCmd);
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,25 +52,23 @@ public class VelocityCommandManager implements CommandManager {
|
||||
Preconditions.checkNotNull(source, "invoker");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
return false;
|
||||
String alias = cmdLine;
|
||||
String args = "";
|
||||
int firstSpace = cmdLine.indexOf(' ');
|
||||
if (firstSpace != -1) {
|
||||
alias = cmdLine.substring(0, firstSpace);
|
||||
args = cmdLine.substring(firstSpace).trim();
|
||||
}
|
||||
|
||||
String alias = split[0];
|
||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
if (command == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("nullness")
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
try {
|
||||
if (!command.hasPermission(source, actualArgs)) {
|
||||
if (!command.hasPermission(source, args)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
command.execute(source, actualArgs);
|
||||
command.execute(source, args);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
|
||||
@ -84,18 +93,12 @@ public class VelocityCommandManager implements CommandManager {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
// No command available.
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
String alias = split[0];
|
||||
if (split.length == 1) {
|
||||
int firstSpace = cmdLine.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
// Offer to fill in commands.
|
||||
ImmutableList.Builder<String> availableCommands = ImmutableList.builder();
|
||||
for (Map.Entry<String, Command> entry : commands.entrySet()) {
|
||||
if (entry.getKey().regionMatches(true, 0, alias, 0, alias.length())
|
||||
for (Map.Entry<String, RawCommand> entry : commands.entrySet()) {
|
||||
if (entry.getKey().regionMatches(true, 0, cmdLine, 0, cmdLine.length())
|
||||
&& entry.getValue().hasPermission(source, new String[0])) {
|
||||
availableCommands.add("/" + entry.getKey());
|
||||
}
|
||||
@ -103,23 +106,22 @@ public class VelocityCommandManager implements CommandManager {
|
||||
return availableCommands.build();
|
||||
}
|
||||
|
||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
String alias = cmdLine.substring(0, firstSpace);
|
||||
String args = cmdLine.substring(firstSpace).trim();
|
||||
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
if (command == null) {
|
||||
// No such command, so we can't offer any tab complete suggestions.
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("nullness")
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
try {
|
||||
if (!command.hasPermission(source, actualArgs)) {
|
||||
if (!command.hasPermission(source, args)) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
return ImmutableList.copyOf(command.suggest(source, actualArgs));
|
||||
return ImmutableList.copyOf(command.suggest(source, args));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Unable to invoke suggestions for command " + alias + " for " + source, e);
|
||||
"Unable to invoke suggestions for command " + cmdLine + " for " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,26 +135,61 @@ public class VelocityCommandManager implements CommandManager {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
String[] split = cmdLine.split(" ", -1);
|
||||
if (split.length == 0) {
|
||||
// No command available.
|
||||
return false;
|
||||
String alias = cmdLine;
|
||||
String args = "";
|
||||
int firstSpace = cmdLine.indexOf(' ');
|
||||
if (firstSpace != -1) {
|
||||
alias = cmdLine.substring(0, firstSpace);
|
||||
args = cmdLine.substring(firstSpace).trim();
|
||||
}
|
||||
|
||||
String alias = split[0];
|
||||
Command command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
RawCommand command = commands.get(alias.toLowerCase(Locale.ENGLISH));
|
||||
if (command == null) {
|
||||
// No such command.
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("nullness")
|
||||
String[] actualArgs = Arrays.copyOfRange(split, 1, split.length);
|
||||
try {
|
||||
return command.hasPermission(source, actualArgs);
|
||||
return command.hasPermission(source, args);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Unable to invoke suggestions for command " + alias + " for " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class RegularCommandWrapper implements RawCommand {
|
||||
|
||||
private final Command delegate;
|
||||
|
||||
private RegularCommandWrapper(Command delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
private static String[] split(String line) {
|
||||
if (line.isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
return line.split(" ", -1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSource source, String commandLine) {
|
||||
delegate.execute(source, split(commandLine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(CommandSource source, String currentLine) {
|
||||
return delegate.suggest(source, split(currentLine));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(CommandSource source, String commandLine) {
|
||||
return delegate.hasPermission(source, split(commandLine));
|
||||
}
|
||||
|
||||
static RawCommand wrap(Command command) {
|
||||
if (command instanceof RawCommand) {
|
||||
return (RawCommand) command;
|
||||
}
|
||||
return new RegularCommandWrapper(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
package com.velocitypowered.proxy.config;
|
||||
|
||||
public enum PingPassthroughMode {
|
||||
DISABLED,
|
||||
MODS,
|
||||
ALL
|
||||
}
|
@ -72,11 +72,36 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
@ConfigKey("forwarding-secret")
|
||||
private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
@Comment({"Announce whether or not your server supports Forge. If you run a modded server, we",
|
||||
"suggest turning this on."})
|
||||
@Comment({
|
||||
"Announce whether or not your server supports Forge. If you run a modded server, we",
|
||||
"suggest turning this on.",
|
||||
"",
|
||||
"If your network runs one modpack consistently, consider using ping-passthrough = \"mods\"",
|
||||
"instead for a nicer display in the server list."
|
||||
})
|
||||
@ConfigKey("announce-forge")
|
||||
private boolean announceForge = false;
|
||||
|
||||
@Comment({"If enabled (default is false) and the proxy is in online mode, Velocity will kick",
|
||||
"any existing player who is online if a duplicate connection attempt is made."})
|
||||
@ConfigKey("kick-existing-players")
|
||||
private boolean onlineModeKickExistingPlayers = false;
|
||||
|
||||
@Comment({
|
||||
"Should Velocity pass server list ping requests to a backend server?",
|
||||
"Available options:",
|
||||
"- \"disabled\": No pass-through will be done. The velocity.toml and server-icon.png",
|
||||
" will determine the initial server list ping response.",
|
||||
"- \"mods\": Passes only the mod list from your backend server into the response.",
|
||||
" The first server in your try list (or forced host) with a mod list will be",
|
||||
" used. If no backend servers can be contacted, Velocity will not display any",
|
||||
" mod information.",
|
||||
"- \"all\": Passes everything from the backend server into the response. The Velocity",
|
||||
" configuration is used if no servers could be contacted."
|
||||
})
|
||||
@ConfigKey("ping-passthrough")
|
||||
private PingPassthroughMode pingPassthrough;
|
||||
|
||||
@Table("[servers]")
|
||||
private final Servers servers;
|
||||
|
||||
@ -109,7 +134,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
|
||||
private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode,
|
||||
boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
|
||||
Servers servers, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) {
|
||||
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, Servers servers,
|
||||
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) {
|
||||
this.bind = bind;
|
||||
this.motd = motd;
|
||||
this.showMaxPlayers = showMaxPlayers;
|
||||
@ -117,6 +143,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
this.announceForge = announceForge;
|
||||
this.playerInfoForwardingMode = playerInfoForwardingMode;
|
||||
this.forwardingSecret = forwardingSecret;
|
||||
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
|
||||
this.pingPassthrough = pingPassthrough;
|
||||
this.servers = servers;
|
||||
this.forcedHosts = forcedHosts;
|
||||
this.advanced = advanced;
|
||||
@ -365,10 +393,18 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
return advanced.isProxyProtocol();
|
||||
}
|
||||
|
||||
public boolean useTcpFastOpen() {
|
||||
return advanced.tcpFastOpen;
|
||||
}
|
||||
|
||||
public Metrics getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
public PingPassthroughMode getPingPassthrough() {
|
||||
return pingPassthrough;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
@ -416,6 +452,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
|
||||
String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN")
|
||||
.toUpperCase(Locale.US);
|
||||
String passThroughName = toml.getString("ping-passthrough", "DISABLED")
|
||||
.toUpperCase(Locale.US);
|
||||
|
||||
return new VelocityConfiguration(
|
||||
toml.getString("bind", "0.0.0.0:25577"),
|
||||
@ -425,6 +463,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
toml.getBoolean("announce-forge", false),
|
||||
PlayerInfoForwarding.valueOf(forwardingModeName),
|
||||
forwardingSecret,
|
||||
toml.getBoolean("kick-existing-players", false),
|
||||
PingPassthroughMode.valueOf(passThroughName),
|
||||
servers,
|
||||
forcedHosts,
|
||||
advanced,
|
||||
@ -443,6 +483,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public boolean isOnlineModeKickExistingPlayers() {
|
||||
return onlineModeKickExistingPlayers;
|
||||
}
|
||||
|
||||
private static class Servers {
|
||||
|
||||
@IsMap
|
||||
@ -596,6 +640,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
@ConfigKey("proxy-protocol")
|
||||
private boolean proxyProtocol = false;
|
||||
|
||||
@Comment("Enables TCP fast open support on the proxy. Requires the proxy to run on Linux.")
|
||||
@ConfigKey("tcp-fast-open")
|
||||
private boolean tcpFastOpen = false;
|
||||
|
||||
private Advanced() {
|
||||
}
|
||||
|
||||
@ -607,6 +655,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue();
|
||||
this.readTimeout = toml.getLong("read-timeout", 30000L).intValue();
|
||||
this.proxyProtocol = toml.getBoolean("proxy-protocol", false);
|
||||
this.tcpFastOpen = toml.getBoolean("tcp-fast-open", false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -634,6 +683,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
return proxyProtocol;
|
||||
}
|
||||
|
||||
public boolean isTcpFastOpen() {
|
||||
return tcpFastOpen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Advanced{"
|
||||
@ -643,6 +696,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi
|
||||
+ ", connectionTimeout=" + connectionTimeout
|
||||
+ ", readTimeout=" + readTimeout
|
||||
+ ", proxyProtocol=" + proxyProtocol
|
||||
+ ", tcpFastOpen=" + tcpFastOpen
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
|
@ -131,8 +131,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
try {
|
||||
sessionHandler.exception(cause);
|
||||
} catch (Exception ex) {
|
||||
logger.error("{}: exception handling exception", (association != null ? association :
|
||||
channel.remoteAddress()), cause);
|
||||
logger.error("{}: exception handling exception in {}",
|
||||
(association != null ? association : channel.remoteAddress()), sessionHandler, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,7 +140,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
if (cause instanceof ReadTimeoutException) {
|
||||
logger.error("{}: read timed out", association);
|
||||
} else {
|
||||
logger.error("{}: exception encountered", association, cause);
|
||||
logger.error("{}: exception encountered in {}", association, sessionHandler, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,6 +155,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureInEventLoop() {
|
||||
Preconditions.checkState(this.channel.eventLoop().inEventLoop(), "Not in event loop");
|
||||
}
|
||||
|
||||
public EventLoop eventLoop() {
|
||||
return channel.eventLoop();
|
||||
}
|
||||
@ -233,6 +237,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
* @param autoReading whether or not we should read data automatically
|
||||
*/
|
||||
public void setAutoReading(boolean autoReading) {
|
||||
ensureInEventLoop();
|
||||
|
||||
channel.config().setAutoRead(autoReading);
|
||||
if (autoReading) {
|
||||
// For some reason, the channel may not completely read its queued contents once autoread
|
||||
@ -249,6 +255,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
* @param state the new state
|
||||
*/
|
||||
public void setState(StateRegistry state) {
|
||||
ensureInEventLoop();
|
||||
|
||||
this.state = state;
|
||||
this.channel.pipeline().get(MinecraftEncoder.class).setState(state);
|
||||
this.channel.pipeline().get(MinecraftDecoder.class).setState(state);
|
||||
@ -263,6 +271,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
* @param protocolVersion the protocol version to use
|
||||
*/
|
||||
public void setProtocolVersion(ProtocolVersion protocolVersion) {
|
||||
ensureInEventLoop();
|
||||
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.nextProtocolVersion = protocolVersion;
|
||||
if (protocolVersion != ProtocolVersion.LEGACY) {
|
||||
@ -284,6 +294,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
* @param sessionHandler the handler to use
|
||||
*/
|
||||
public void setSessionHandler(MinecraftSessionHandler sessionHandler) {
|
||||
ensureInEventLoop();
|
||||
|
||||
if (this.sessionHandler != null) {
|
||||
this.sessionHandler.deactivated();
|
||||
}
|
||||
@ -302,6 +314,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
*/
|
||||
public void setCompressionThreshold(int threshold) {
|
||||
ensureOpen();
|
||||
ensureInEventLoop();
|
||||
|
||||
if (threshold == -1) {
|
||||
channel.pipeline().remove(COMPRESSION_DECODER);
|
||||
@ -325,6 +338,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
*/
|
||||
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
|
||||
ensureOpen();
|
||||
ensureInEventLoop();
|
||||
|
||||
SecretKey key = new SecretKeySpec(secret, "AES");
|
||||
|
||||
@ -342,6 +356,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
|
||||
public void setAssociation(MinecraftConnectionAssociation association) {
|
||||
ensureInEventLoop();
|
||||
this.association = association;
|
||||
}
|
||||
|
||||
|
@ -5,26 +5,25 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||
import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommands;
|
||||
import com.velocitypowered.proxy.protocol.packet.AvailableCommands.ProtocolSuggestionProvider;
|
||||
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.JoinGame;
|
||||
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
|
||||
import com.velocitypowered.proxy.protocol.packet.PlayerListItem;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
|
||||
public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@ -37,7 +36,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
BackendPlaySessionHandler(VelocityServer server, VelocityServerConnection serverConn) {
|
||||
this.server = server;
|
||||
this.serverConn = serverConn;
|
||||
this.playerConnection = serverConn.getPlayer().getMinecraftConnection();
|
||||
this.playerConnection = serverConn.getPlayer().getConnection();
|
||||
|
||||
MinecraftSessionHandler psh = playerConnection.getSessionHandler();
|
||||
if (!(psh instanceof ClientPlaySessionHandler)) {
|
||||
@ -119,12 +118,15 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
|
||||
packet.getData());
|
||||
copy);
|
||||
server.getEventManager().fire(event)
|
||||
.thenAcceptAsync(pme -> {
|
||||
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
|
||||
playerConnection.write(packet);
|
||||
PluginMessage copied = new PluginMessage(packet.getChannel(),
|
||||
Unpooled.wrappedBuffer(copy));
|
||||
playerConnection.write(copied);
|
||||
}
|
||||
}, playerConnection.eventLoop());
|
||||
return true;
|
||||
@ -163,6 +165,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void handleGeneric(MinecraftPacket packet) {
|
||||
if (packet instanceof PluginMessage) {
|
||||
((PluginMessage) packet).retain();
|
||||
}
|
||||
playerConnection.write(packet);
|
||||
}
|
||||
|
||||
@ -185,7 +190,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
public void disconnected() {
|
||||
serverConn.getServer().removePlayer(serverConn.getPlayer());
|
||||
if (!serverConn.isGracefulDisconnect() && !exceptionTriggered) {
|
||||
serverConn.getPlayer().disconnect(ConnectionMessages.UNEXPECTED_DISCONNECT);
|
||||
serverConn.getPlayer().handleConnectionException(serverConn.getServer(),
|
||||
Disconnect.create(ConnectionMessages.UNEXPECTED_DISCONNECT), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.velocitypowered.proxy.connection.backend;
|
||||
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.api.util.GameProfile;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||
@ -56,23 +55,16 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
MinecraftConnection mc = serverConn.ensureConnected();
|
||||
VelocityConfiguration configuration = server.getConfiguration();
|
||||
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet
|
||||
.getChannel()
|
||||
.equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
||||
LoginPluginResponse response = new LoginPluginResponse();
|
||||
response.setSuccess(true);
|
||||
response.setId(packet.getId());
|
||||
response.setData(createForwardingData(configuration.getForwardingSecret(),
|
||||
.getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) {
|
||||
ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(),
|
||||
serverConn.getPlayer().getRemoteAddress().getHostString(),
|
||||
serverConn.getPlayer().getGameProfile()));
|
||||
serverConn.getPlayer().getGameProfile());
|
||||
LoginPluginResponse response = new LoginPluginResponse(packet.getId(), true, forwardingData);
|
||||
mc.write(response);
|
||||
informationForwarded = true;
|
||||
} else {
|
||||
// Don't understand
|
||||
LoginPluginResponse response = new LoginPluginResponse();
|
||||
response.setSuccess(false);
|
||||
response.setId(packet.getId());
|
||||
response.setData(Unpooled.EMPTY_BUFFER);
|
||||
mc.write(response);
|
||||
mc.write(new LoginPluginResponse(packet.getId(), false, Unpooled.EMPTY_BUFFER));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -125,32 +117,28 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
private static ByteBuf createForwardingData(byte[] hmacSecret, String address,
|
||||
GameProfile profile) {
|
||||
ByteBuf dataToForward = Unpooled.buffer();
|
||||
ByteBuf finalData = Unpooled.buffer();
|
||||
ByteBuf forwarded = Unpooled.buffer(2048);
|
||||
try {
|
||||
ProtocolUtils.writeVarInt(dataToForward, VelocityConstants.FORWARDING_VERSION);
|
||||
ProtocolUtils.writeString(dataToForward, address);
|
||||
ProtocolUtils.writeUuid(dataToForward, profile.getId());
|
||||
ProtocolUtils.writeString(dataToForward, profile.getName());
|
||||
ProtocolUtils.writeProperties(dataToForward, profile.getProperties());
|
||||
ProtocolUtils.writeVarInt(forwarded, VelocityConstants.FORWARDING_VERSION);
|
||||
ProtocolUtils.writeString(forwarded, address);
|
||||
ProtocolUtils.writeUuid(forwarded, profile.getId());
|
||||
ProtocolUtils.writeString(forwarded, profile.getName());
|
||||
ProtocolUtils.writeProperties(forwarded, profile.getProperties());
|
||||
|
||||
SecretKey key = new SecretKeySpec(hmacSecret, "HmacSHA256");
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(key);
|
||||
mac.update(dataToForward.array(), dataToForward.arrayOffset(), dataToForward.readableBytes());
|
||||
mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes());
|
||||
byte[] sig = mac.doFinal();
|
||||
finalData.writeBytes(sig);
|
||||
finalData.writeBytes(dataToForward);
|
||||
return finalData;
|
||||
|
||||
return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded);
|
||||
} catch (InvalidKeyException e) {
|
||||
finalData.release();
|
||||
forwarded.release();
|
||||
throw new RuntimeException("Unable to authenticate data", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
// Should never happen
|
||||
finalData.release();
|
||||
forwarded.release();
|
||||
throw new AssertionError(e);
|
||||
} finally {
|
||||
dataToForward.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,13 +84,13 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
.whenCompleteAsync((x, error) -> {
|
||||
// Strap on the ClientPlaySessionHandler if required.
|
||||
ClientPlaySessionHandler playHandler;
|
||||
if (serverConn.getPlayer().getMinecraftConnection().getSessionHandler()
|
||||
if (serverConn.getPlayer().getConnection().getSessionHandler()
|
||||
instanceof ClientPlaySessionHandler) {
|
||||
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getMinecraftConnection()
|
||||
playHandler = (ClientPlaySessionHandler) serverConn.getPlayer().getConnection()
|
||||
.getSessionHandler();
|
||||
} else {
|
||||
playHandler = new ClientPlaySessionHandler(server, serverConn.getPlayer());
|
||||
serverConn.getPlayer().getMinecraftConnection().setSessionHandler(playHandler);
|
||||
serverConn.getPlayer().getConnection().setSessionHandler(playHandler);
|
||||
}
|
||||
playHandler.handleBackendJoinGame(packet, serverConn);
|
||||
|
||||
@ -167,7 +167,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
serverConn.getPlayer().getMinecraftConnection().write(packet);
|
||||
serverConn.getPlayer().getConnection().write(packet.retain());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -12,7 +12,6 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.ConnectionRequestBuilder;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
@ -33,6 +32,7 @@ import com.velocitypowered.proxy.protocol.packet.Handshake;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLogin;
|
||||
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
@ -76,7 +76,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
CompletableFuture<Impl> result = new CompletableFuture<>();
|
||||
// Note: we use the event loop for the connection the player is on. This reduces context
|
||||
// switches.
|
||||
server.initializeGenericBootstrap(proxyPlayer.getMinecraftConnection().eventLoop())
|
||||
server.createBootstrap(proxyPlayer.getConnection().eventLoop())
|
||||
.handler(new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
protected void initChannel(Channel ch) throws Exception {
|
||||
@ -138,23 +138,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
PlayerInfoForwarding forwardingMode = server.getConfiguration().getPlayerInfoForwardingMode();
|
||||
|
||||
// Initiate the handshake.
|
||||
ProtocolVersion protocolVersion = proxyPlayer.getMinecraftConnection().getNextProtocolVersion();
|
||||
ProtocolVersion protocolVersion = proxyPlayer.getConnection().getNextProtocolVersion();
|
||||
Handshake handshake = new Handshake();
|
||||
handshake.setNextStatus(StateRegistry.LOGIN_ID);
|
||||
handshake.setProtocolVersion(protocolVersion);
|
||||
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
||||
handshake.setServerAddress(createLegacyForwardingAddress());
|
||||
} else if (proxyPlayer.getMinecraftConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||
handshake.setServerAddress(handshake.getServerAddress() + HANDSHAKE_HOSTNAME_TOKEN);
|
||||
} else {
|
||||
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
|
||||
}
|
||||
handshake.setPort(registeredServer.getServerInfo().getAddress().getPort());
|
||||
mc.write(handshake);
|
||||
mc.delayedWrite(handshake);
|
||||
|
||||
mc.setProtocolVersion(protocolVersion);
|
||||
mc.setState(StateRegistry.LOGIN);
|
||||
mc.write(new ServerLogin(proxyPlayer.getUsername()));
|
||||
mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername()));
|
||||
mc.flush();
|
||||
}
|
||||
|
||||
public @Nullable MinecraftConnection getConnection() {
|
||||
@ -212,9 +213,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
|
||||
MinecraftConnection mc = ensureConnected();
|
||||
|
||||
PluginMessage message = new PluginMessage();
|
||||
message.setChannel(identifier.getId());
|
||||
message.setData(data);
|
||||
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
|
||||
mc.write(message);
|
||||
return true;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
|
||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
@ -29,14 +30,15 @@ import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse.Offer;
|
||||
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.format.TextColor;
|
||||
@ -57,7 +59,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
private final List<UUID> serverBossBars = new ArrayList<>();
|
||||
private final Queue<PluginMessage> loginPluginMessages = new ArrayDeque<>();
|
||||
private final VelocityServer server;
|
||||
private @Nullable TabCompleteRequest legacyCommandTabComplete;
|
||||
private @Nullable TabCompleteRequest outstandingTabComplete;
|
||||
|
||||
/**
|
||||
* Constructs a client play session handler.
|
||||
@ -75,11 +77,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
.getProtocolVersion());
|
||||
if (!channels.isEmpty()) {
|
||||
PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels);
|
||||
player.getMinecraftConnection().write(register);
|
||||
player.getConnection().write(register);
|
||||
player.getKnownChannels().addAll(channels);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivated() {
|
||||
for (PluginMessage message : loginPluginMessages) {
|
||||
ReferenceCountUtil.release(message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(KeepAlive packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
@ -146,25 +155,226 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
public boolean handle(TabCompleteRequest packet) {
|
||||
boolean isCommand = !packet.isAssumeCommand() && packet.getCommand().startsWith("/");
|
||||
|
||||
if (!isCommand) {
|
||||
// We can't deal with anything else.
|
||||
return false;
|
||||
if (isCommand) {
|
||||
return this.handleCommandTabComplete(packet);
|
||||
} else {
|
||||
return this.handleRegularTabComplete(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(PluginMessage packet) {
|
||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
||||
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
|
||||
if (serverConn != null && backendConn != null) {
|
||||
if (backendConn.getState() != StateRegistry.PLAY) {
|
||||
logger.warn("A plugin message was received while the backend server was not "
|
||||
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
||||
} else if (PluginMessageUtil.isRegister(packet)) {
|
||||
player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet));
|
||||
backendConn.write(packet.retain());
|
||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||
backendConn.write(packet.retain());
|
||||
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
||||
backendConn.write(PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion()));
|
||||
} else {
|
||||
if (serverConn.getPhase() == BackendConnectionPhases.IN_TRANSITION) {
|
||||
// We must bypass the currently-connected server when forwarding Forge packets.
|
||||
VelocityServerConnection inFlight = player.getConnectionInFlight();
|
||||
if (inFlight != null) {
|
||||
player.getPhase().handle(player, packet, inFlight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!player.getPhase().handle(player, packet, serverConn)) {
|
||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||
.consideredComplete()) {
|
||||
// The client is trying to send messages too early. This is primarily caused by mods,
|
||||
// but further aggravated by Velocity. To work around these issues, we will queue any
|
||||
// non-FML handshake messages to be sent once the FML handshake has completed or the
|
||||
// JoinGame packet has been received by the proxy, whichever comes first.
|
||||
//
|
||||
// We also need to make sure to retain these packets so they can be flushed
|
||||
// appropriately.
|
||||
loginPluginMessages.add(packet.retain());
|
||||
} else {
|
||||
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||
if (id == null) {
|
||||
backendConn.write(packet.retain());
|
||||
} else {
|
||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id,
|
||||
ByteBufUtil.getBytes(packet.content()));
|
||||
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
|
||||
PluginMessage message = new PluginMessage(packet.getChannel(),
|
||||
Unpooled.wrappedBuffer(copy));
|
||||
backendConn.write(message);
|
||||
}, backendConn.eventLoop());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(ResourcePackResponse packet) {
|
||||
server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player,
|
||||
packet.getStatus()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleGeneric(MinecraftPacket packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
// No server connection yet, probably transitioning.
|
||||
return;
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
if (packet instanceof PluginMessage) {
|
||||
((PluginMessage) packet).retain();
|
||||
}
|
||||
smc.write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUnknown(ByteBuf buf) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
// No server connection yet, probably transitioning.
|
||||
return;
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
smc.write(buf.retain());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected() {
|
||||
player.teardown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exception(Throwable throwable) {
|
||||
player.disconnect(TextComponent.of("Your connection has encountered an error. Try again later.",
|
||||
TextColor.RED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writabilityChanged() {
|
||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
||||
if (serverConn != null) {
|
||||
boolean writable = player.getConnection().getChannel().isWritable();
|
||||
MinecraftConnection smc = serverConn.getConnection();
|
||||
if (smc != null) {
|
||||
smc.setAutoReading(writable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the {@code JoinGame} packet. This function is responsible for handling the client-side
|
||||
* switching servers in Velocity.
|
||||
* @param joinGame the join game packet
|
||||
* @param destination the new server we are connecting to
|
||||
*/
|
||||
public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection destination) {
|
||||
final MinecraftConnection serverMc = destination.ensureConnected();
|
||||
|
||||
if (!spawned) {
|
||||
// Nothing special to do with regards to spawning the player
|
||||
spawned = true;
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
|
||||
// Required for Legacy Forge
|
||||
player.getPhase().onFirstJoin(player);
|
||||
} else {
|
||||
// Clear tab list to avoid duplicate entries
|
||||
player.getTabList().clearAll();
|
||||
|
||||
// In order to handle switching to another server, you will need to send three packets:
|
||||
//
|
||||
// - The join game packet from the backend server
|
||||
// - A respawn packet with a different dimension
|
||||
// - Another respawn with the correct dimension
|
||||
//
|
||||
// The two respawns with different dimensions are required, otherwise the client gets
|
||||
// confused.
|
||||
//
|
||||
// Most notably, by having the client accept the join game packet, we can work around the need
|
||||
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
|
||||
// improving compatibility with mods.
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
|
||||
joinGame.getLevelType()));
|
||||
player.getConnection().delayedWrite(
|
||||
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
|
||||
joinGame.getLevelType()));
|
||||
}
|
||||
|
||||
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
|
||||
// track them.
|
||||
for (UUID serverBossBar : serverBossBars) {
|
||||
BossBar deletePacket = new BossBar();
|
||||
deletePacket.setUuid(serverBossBar);
|
||||
deletePacket.setAction(BossBar.REMOVE);
|
||||
player.getConnection().delayedWrite(deletePacket);
|
||||
}
|
||||
serverBossBars.clear();
|
||||
|
||||
// Tell the server about this client's plugin message channels.
|
||||
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
||||
if (!player.getKnownChannels().isEmpty()) {
|
||||
serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels()));
|
||||
}
|
||||
|
||||
// If we had plugin messages queued during login/FML handshake, send them now.
|
||||
PluginMessage pm;
|
||||
while ((pm = loginPluginMessages.poll()) != null) {
|
||||
serverMc.delayedWrite(pm);
|
||||
}
|
||||
|
||||
// Clear any title from the previous server.
|
||||
player.getConnection()
|
||||
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
|
||||
|
||||
// Flush everything
|
||||
player.getConnection().flush();
|
||||
serverMc.flush();
|
||||
destination.completeJoin();
|
||||
}
|
||||
|
||||
public List<UUID> getServerBossBars() {
|
||||
return serverBossBars;
|
||||
}
|
||||
|
||||
|
||||
private boolean handleCommandTabComplete(TabCompleteRequest packet) {
|
||||
// In 1.13+, we need to do additional work for the richer suggestions available.
|
||||
String command = packet.getCommand().substring(1);
|
||||
int spacePos = command.indexOf(' ');
|
||||
if (spacePos == -1) {
|
||||
return false;
|
||||
spacePos = command.length();
|
||||
}
|
||||
|
||||
String commandLabel = command.substring(0, spacePos);
|
||||
if (!server.getCommandManager().hasCommand(commandLabel)) {
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
|
||||
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
|
||||
// tab list completion support for command names. In 1.13, Brigadier handles everything for
|
||||
// us.
|
||||
legacyCommandTabComplete = packet;
|
||||
// additional tab completion support.
|
||||
outstandingTabComplete = packet;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -199,221 +409,67 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
resp.setLength(length);
|
||||
resp.getOffers().addAll(offers);
|
||||
|
||||
player.getMinecraftConnection().write(resp);
|
||||
player.getConnection().write(resp);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(PluginMessage packet) {
|
||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
||||
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
|
||||
if (serverConn != null && backendConn != null) {
|
||||
if (backendConn.getState() != StateRegistry.PLAY) {
|
||||
logger.warn("A plugin message was received while the backend server was not "
|
||||
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
|
||||
} else if (PluginMessageUtil.isRegister(packet)) {
|
||||
player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet));
|
||||
backendConn.write(packet);
|
||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||
backendConn.write(packet);
|
||||
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
||||
backendConn.write(PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion()));
|
||||
} else {
|
||||
if (serverConn.getPhase() == BackendConnectionPhases.IN_TRANSITION) {
|
||||
// We must bypass the currently-connected server when forwarding Forge packets.
|
||||
VelocityServerConnection inFlight = player.getConnectionInFlight();
|
||||
if (inFlight != null) {
|
||||
player.getPhase().handle(player, packet, inFlight);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!player.getPhase().handle(player, packet, serverConn)) {
|
||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||
.consideredComplete()) {
|
||||
// The client is trying to send messages too early. This is primarily caused by mods,
|
||||
// but further aggravated by Velocity. To work around these issues, we will queue any
|
||||
// non-FML handshake messages to be sent once the FML handshake has completed or the
|
||||
// JoinGame packet has been received by the proxy, whichever comes first.
|
||||
loginPluginMessages.add(packet);
|
||||
} else {
|
||||
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||
if (id == null) {
|
||||
backendConn.write(packet);
|
||||
} else {
|
||||
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id,
|
||||
packet.getData());
|
||||
server.getEventManager().fire(event).thenAcceptAsync(pme -> backendConn.write(packet),
|
||||
backendConn.eventLoop());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean handleRegularTabComplete(TabCompleteRequest packet) {
|
||||
if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) {
|
||||
// Outstanding tab completes are recorded for use with 1.12 clients and below to provide
|
||||
// additional tab completion support.
|
||||
outstandingTabComplete = packet;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(ResourcePackResponse packet) {
|
||||
server.getEventManager().fireAndForget(new PlayerResourcePackStatusEvent(player,
|
||||
packet.getStatus()));
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleGeneric(MinecraftPacket packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
// No server connection yet, probably transitioning.
|
||||
return;
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
smc.write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleUnknown(ByteBuf buf) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection == null) {
|
||||
// No server connection yet, probably transitioning.
|
||||
return;
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
smc.write(buf.retain());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected() {
|
||||
player.teardown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exception(Throwable throwable) {
|
||||
player.disconnect(TextComponent.of("Your connection has encountered an error. Try again later.",
|
||||
TextColor.RED));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writabilityChanged() {
|
||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
||||
if (serverConn != null) {
|
||||
boolean writable = player.getMinecraftConnection().getChannel().isWritable();
|
||||
MinecraftConnection smc = serverConn.getConnection();
|
||||
if (smc != null) {
|
||||
smc.setAutoReading(writable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the {@code JoinGame} packet. This function is responsible for handling the client-side
|
||||
* switching servers in Velocity.
|
||||
* @param joinGame the join game packet
|
||||
* @param destination the new server we are connecting to
|
||||
*/
|
||||
public void handleBackendJoinGame(JoinGame joinGame, VelocityServerConnection destination) {
|
||||
final MinecraftConnection serverMc = destination.ensureConnected();
|
||||
|
||||
if (!spawned) {
|
||||
// Nothing special to do with regards to spawning the player
|
||||
spawned = true;
|
||||
player.getMinecraftConnection().delayedWrite(joinGame);
|
||||
|
||||
// Required for Legacy Forge
|
||||
player.getPhase().onFirstJoin(player);
|
||||
} else {
|
||||
// Clear tab list to avoid duplicate entries
|
||||
player.getTabList().clearAll();
|
||||
|
||||
// In order to handle switching to another server, you will need to send three packets:
|
||||
//
|
||||
// - The join game packet from the backend server
|
||||
// - A respawn packet with a different dimension
|
||||
// - Another respawn with the correct dimension
|
||||
//
|
||||
// The two respawns with different dimensions are required, otherwise the client gets
|
||||
// confused.
|
||||
//
|
||||
// Most notably, by having the client accept the join game packet, we can work around the need
|
||||
// to perform entity ID rewrites, eliminating potential issues from rewriting packets and
|
||||
// improving compatibility with mods.
|
||||
player.getMinecraftConnection().delayedWrite(joinGame);
|
||||
int tempDim = joinGame.getDimension() == 0 ? -1 : 0;
|
||||
player.getMinecraftConnection().delayedWrite(
|
||||
new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(),
|
||||
joinGame.getLevelType()));
|
||||
player.getMinecraftConnection().delayedWrite(
|
||||
new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(),
|
||||
joinGame.getLevelType()));
|
||||
}
|
||||
|
||||
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
|
||||
// track them.
|
||||
for (UUID serverBossBar : serverBossBars) {
|
||||
BossBar deletePacket = new BossBar();
|
||||
deletePacket.setUuid(serverBossBar);
|
||||
deletePacket.setAction(BossBar.REMOVE);
|
||||
player.getMinecraftConnection().delayedWrite(deletePacket);
|
||||
}
|
||||
serverBossBars.clear();
|
||||
|
||||
// Tell the server about this client's plugin message channels.
|
||||
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
||||
if (!player.getKnownChannels().isEmpty()) {
|
||||
serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels()));
|
||||
}
|
||||
|
||||
// If we had plugin messages queued during login/FML handshake, send them now.
|
||||
PluginMessage pm;
|
||||
while ((pm = loginPluginMessages.poll()) != null) {
|
||||
serverMc.delayedWrite(pm);
|
||||
}
|
||||
|
||||
// Clear any title from the previous server.
|
||||
player.getMinecraftConnection()
|
||||
.delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion()));
|
||||
|
||||
// Flush everything
|
||||
player.getMinecraftConnection().flush();
|
||||
serverMc.flush();
|
||||
destination.completeJoin();
|
||||
}
|
||||
|
||||
public List<UUID> getServerBossBars() {
|
||||
return serverBossBars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles additional tab complete for 1.12 and lower clients.
|
||||
* Handles additional tab complete.
|
||||
*
|
||||
* @param response the tab complete response from the backend
|
||||
*/
|
||||
public void handleTabCompleteResponse(TabCompleteResponse response) {
|
||||
if (legacyCommandTabComplete != null) {
|
||||
String command = legacyCommandTabComplete.getCommand().substring(1);
|
||||
try {
|
||||
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
|
||||
for (String offer : offers) {
|
||||
response.getOffers().add(new Offer(offer, null));
|
||||
}
|
||||
response.getOffers().sort(null);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to provide tab list completions for {} for command '{}'",
|
||||
player.getUsername(),
|
||||
command, e);
|
||||
if (outstandingTabComplete != null) {
|
||||
if (outstandingTabComplete.isAssumeCommand()) {
|
||||
return; // used for command blocks which can't run Velocity commands anyway
|
||||
}
|
||||
legacyCommandTabComplete = null;
|
||||
if (outstandingTabComplete.getCommand().startsWith("/")) {
|
||||
this.finishCommandTabComplete(outstandingTabComplete, response);
|
||||
} else {
|
||||
this.finishRegularTabComplete(outstandingTabComplete, response);
|
||||
}
|
||||
outstandingTabComplete = null;
|
||||
}
|
||||
}
|
||||
|
||||
player.getMinecraftConnection().write(response);
|
||||
private void finishCommandTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
|
||||
String command = request.getCommand().substring(1);
|
||||
try {
|
||||
List<String> offers = server.getCommandManager().offerSuggestions(player, command);
|
||||
for (String offer : offers) {
|
||||
response.getOffers().add(new Offer(offer, null));
|
||||
}
|
||||
response.getOffers().sort(null);
|
||||
player.getConnection().write(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to provide tab list completions for {} for command '{}'",
|
||||
player.getUsername(),
|
||||
command, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) {
|
||||
List<String> offers = new ArrayList<>();
|
||||
for (Offer offer : response.getOffers()) {
|
||||
offers.add(offer.getText());
|
||||
}
|
||||
server.getEventManager().fire(new TabCompleteEvent(player, request.getCommand(), offers))
|
||||
.thenAcceptAsync(e -> {
|
||||
response.getOffers().clear();
|
||||
for (String s : e.getSuggestions()) {
|
||||
response.getOffers().add(new Offer(s));
|
||||
}
|
||||
player.getConnection().write(response);
|
||||
}, player.getConnection().eventLoop());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -49,6 +49,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabList;
|
||||
import com.velocitypowered.proxy.util.VelocityMessages;
|
||||
import com.velocitypowered.proxy.util.collect.CappedSet;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@ -82,7 +83,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
/**
|
||||
* The actual Minecraft connection. This is actually a wrapper object around the Netty channel.
|
||||
*/
|
||||
private final MinecraftConnection minecraftConnection;
|
||||
private final MinecraftConnection connection;
|
||||
private final @Nullable InetSocketAddress virtualHost;
|
||||
private GameProfile profile;
|
||||
private PermissionFunction permissionFunction;
|
||||
@ -96,18 +97,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
private final VelocityServer server;
|
||||
private ClientConnectionPhase connectionPhase;
|
||||
private final Collection<String> knownChannels;
|
||||
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
|
||||
|
||||
private @MonotonicNonNull List<String> serversToTry = null;
|
||||
|
||||
ConnectedPlayer(VelocityServer server, GameProfile profile,
|
||||
MinecraftConnection minecraftConnection, @Nullable InetSocketAddress virtualHost) {
|
||||
MinecraftConnection connection, @Nullable InetSocketAddress virtualHost) {
|
||||
this.server = server;
|
||||
this.tabList = new VelocityTabList(minecraftConnection);
|
||||
this.tabList = new VelocityTabList(connection);
|
||||
this.profile = profile;
|
||||
this.minecraftConnection = minecraftConnection;
|
||||
this.connection = connection;
|
||||
this.virtualHost = virtualHost;
|
||||
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
||||
this.connectionPhase = minecraftConnection.getType().getInitialClientPhase();
|
||||
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||
this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS);
|
||||
}
|
||||
|
||||
@ -131,8 +133,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public MinecraftConnection getMinecraftConnection() {
|
||||
return minecraftConnection;
|
||||
public MinecraftConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -167,7 +169,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
|
||||
@Override
|
||||
public InetSocketAddress getRemoteAddress() {
|
||||
return (InetSocketAddress) minecraftConnection.getRemoteAddress();
|
||||
return (InetSocketAddress) connection.getRemoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -181,12 +183,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
|
||||
@Override
|
||||
public boolean isActive() {
|
||||
return minecraftConnection.getChannel().isActive();
|
||||
return connection.getChannel().isActive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolVersion getProtocolVersion() {
|
||||
return minecraftConnection.getProtocolVersion();
|
||||
return connection.getProtocolVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -202,7 +204,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
TitlePacket pkt = new TitlePacket();
|
||||
pkt.setAction(TitlePacket.SET_ACTION_BAR);
|
||||
pkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(component));
|
||||
minecraftConnection.write(pkt);
|
||||
connection.write(pkt);
|
||||
return;
|
||||
} else {
|
||||
// Due to issues with action bar packets, we'll need to convert the text message into a
|
||||
@ -218,7 +220,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
Chat chat = new Chat();
|
||||
chat.setType(pos);
|
||||
chat.setMessage(json);
|
||||
minecraftConnection.write(chat);
|
||||
connection.write(chat);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -255,23 +257,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
public void disconnect(Component reason) {
|
||||
logger.info("{} has disconnected: {}", this,
|
||||
LegacyComponentSerializer.legacy().serialize(reason));
|
||||
minecraftConnection.closeWith(Disconnect.create(reason));
|
||||
connection.closeWith(Disconnect.create(reason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendTitle(Title title) {
|
||||
Preconditions.checkNotNull(title, "title");
|
||||
|
||||
ProtocolVersion protocolVersion = minecraftConnection.getProtocolVersion();
|
||||
ProtocolVersion protocolVersion = connection.getProtocolVersion();
|
||||
if (title.equals(Titles.reset())) {
|
||||
minecraftConnection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||
connection.write(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||
} else if (title.equals(Titles.hide())) {
|
||||
minecraftConnection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
|
||||
connection.write(TitlePacket.hideForProtocolVersion(protocolVersion));
|
||||
} else if (title instanceof TextTitle) {
|
||||
TextTitle tt = (TextTitle) title;
|
||||
|
||||
if (tt.isResetBeforeSend()) {
|
||||
minecraftConnection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||
connection.delayedWrite(TitlePacket.resetForProtocolVersion(protocolVersion));
|
||||
}
|
||||
|
||||
Optional<Component> titleText = tt.getTitle();
|
||||
@ -279,7 +281,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
TitlePacket titlePkt = new TitlePacket();
|
||||
titlePkt.setAction(TitlePacket.SET_TITLE);
|
||||
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(titleText.get()));
|
||||
minecraftConnection.delayedWrite(titlePkt);
|
||||
connection.delayedWrite(titlePkt);
|
||||
}
|
||||
|
||||
Optional<Component> subtitleText = tt.getSubtitle();
|
||||
@ -287,7 +289,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
TitlePacket titlePkt = new TitlePacket();
|
||||
titlePkt.setAction(TitlePacket.SET_SUBTITLE);
|
||||
titlePkt.setComponent(GsonComponentSerializer.INSTANCE.serialize(subtitleText.get()));
|
||||
minecraftConnection.delayedWrite(titlePkt);
|
||||
connection.delayedWrite(titlePkt);
|
||||
}
|
||||
|
||||
if (tt.areTimesSet()) {
|
||||
@ -295,9 +297,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
timesPkt.setFadeIn(tt.getFadeIn());
|
||||
timesPkt.setStay(tt.getStay());
|
||||
timesPkt.setFadeOut(tt.getFadeOut());
|
||||
minecraftConnection.delayedWrite(timesPkt);
|
||||
connection.delayedWrite(timesPkt);
|
||||
}
|
||||
minecraftConnection.flush();
|
||||
connection.flush();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown title class " + title.getClass().getName());
|
||||
}
|
||||
@ -404,18 +406,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
if (connectedServer == null) {
|
||||
// The player isn't yet connected to a server. Note that we need to do this in a future run
|
||||
// of the event loop due to an issue with the Netty kqueue transport.
|
||||
minecraftConnection.eventLoop().execute(() -> {
|
||||
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
|
||||
if (nextServer.isPresent()) {
|
||||
// There can't be any connection in flight now.
|
||||
resetInFlightConnection();
|
||||
createConnectionRequest(nextServer.get()).fireAndForget();
|
||||
} else {
|
||||
disconnect(friendlyReason);
|
||||
}
|
||||
});
|
||||
Optional<RegisteredServer> nextServer = getNextServerToTry(rs);
|
||||
if (nextServer.isPresent()) {
|
||||
// There can't be any connection in flight now.
|
||||
resetInFlightConnection();
|
||||
createConnectionRequest(nextServer.get()).fireAndForget();
|
||||
} else {
|
||||
disconnect(friendlyReason);
|
||||
}
|
||||
} else {
|
||||
boolean kickedFromCurrent = connectedServer.getServer().equals(rs);
|
||||
ServerKickResult result;
|
||||
@ -453,9 +451,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
if (newResult == null || !newResult) {
|
||||
disconnect(friendlyReason);
|
||||
} else {
|
||||
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER);
|
||||
sendMessage(VelocityMessages.MOVED_TO_NEW_SERVER.append(friendlyReason));
|
||||
}
|
||||
}, minecraftConnection.eventLoop());
|
||||
}, connection.eventLoop());
|
||||
} else if (event.getResult() instanceof Notify) {
|
||||
Notify res = (Notify) event.getResult();
|
||||
if (event.kickedDuringServerConnect()) {
|
||||
@ -467,7 +465,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
// In case someone gets creative, assume we want to disconnect the player.
|
||||
disconnect(friendlyReason);
|
||||
}
|
||||
}, minecraftConnection.eventLoop());
|
||||
}, connection.eventLoop());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -557,7 +555,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
connectedServer.disconnect();
|
||||
}
|
||||
server.unregisterConnection(this);
|
||||
server.getEventManager().fireAndForget(new DisconnectEvent(this));
|
||||
server.getEventManager().fire(new DisconnectEvent(this))
|
||||
.thenRun(() -> this.teardownFuture.complete(null));
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> getTeardownFuture() {
|
||||
return teardownFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -574,10 +577,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) {
|
||||
Preconditions.checkNotNull(identifier, "identifier");
|
||||
Preconditions.checkNotNull(data, "data");
|
||||
PluginMessage message = new PluginMessage();
|
||||
message.setChannel(identifier.getId());
|
||||
message.setData(data);
|
||||
minecraftConnection.write(message);
|
||||
PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data));
|
||||
connection.write(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -596,7 +597,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
ResourcePackRequest request = new ResourcePackRequest();
|
||||
request.setUrl(url);
|
||||
request.setHash("");
|
||||
minecraftConnection.write(request);
|
||||
connection.write(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -608,7 +609,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
ResourcePackRequest request = new ResourcePackRequest();
|
||||
request.setUrl(url);
|
||||
request.setHash(ByteBufUtil.hexDump(hash));
|
||||
minecraftConnection.write(request);
|
||||
connection.write(request);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -617,10 +618,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
* ID last sent by the server.
|
||||
*/
|
||||
public void sendKeepAlive() {
|
||||
if (minecraftConnection.getState() == StateRegistry.PLAY) {
|
||||
if (connection.getState() == StateRegistry.PLAY) {
|
||||
KeepAlive keepAlive = new KeepAlive();
|
||||
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
|
||||
minecraftConnection.write(keepAlive);
|
||||
connection.write(keepAlive);
|
||||
}
|
||||
}
|
||||
|
||||
@ -744,8 +745,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
if (status != null && !status.isSafe()) {
|
||||
// If it's not safe to continue the connection we need to shut it down.
|
||||
handleConnectionException(status.getAttemptedConnection(), throwable, true);
|
||||
} else if ((status != null && !status.isSuccessful())) {
|
||||
resetInFlightConnection();
|
||||
}
|
||||
})
|
||||
}, connection.eventLoop())
|
||||
.thenApply(x -> x);
|
||||
}
|
||||
|
||||
@ -778,7 +781,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
// The only remaining value is successful (no need to do anything!)
|
||||
break;
|
||||
}
|
||||
}, minecraftConnection.eventLoop())
|
||||
}, connection.eventLoop())
|
||||
.thenApply(Result::isSuccessful);
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
|
||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||
player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet));
|
||||
}
|
||||
serverConn.ensureConnected().write(packet);
|
||||
serverConn.ensureConnected().write(packet.retain());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_
|
||||
import static com.velocitypowered.proxy.connection.VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL;
|
||||
import static com.velocitypowered.proxy.util.EncryptionUtils.decryptRsa;
|
||||
import static com.velocitypowered.proxy.util.EncryptionUtils.generateServerId;
|
||||
import static org.asynchttpclient.Dsl.asyncHttpClient;
|
||||
import static org.asynchttpclient.Dsl.config;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.net.UrlEscapers;
|
||||
@ -42,10 +44,14 @@ import java.security.KeyPair;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import net.kyori.text.Component;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.asynchttpclient.Dsl;
|
||||
import org.asynchttpclient.ListenableFuture;
|
||||
import org.asynchttpclient.Response;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
@ -123,46 +129,50 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
String url = String.format(MOJANG_HASJOINED_URL,
|
||||
urlFormParameterEscaper().escape(login.getUsername()), serverId,
|
||||
urlFormParameterEscaper().escape(playerIp));
|
||||
server.getHttpClient()
|
||||
.get(new URL(url), mcConnection.eventLoop())
|
||||
.thenAcceptAsync(profileResponse -> {
|
||||
if (mcConnection.isClosed()) {
|
||||
// The player disconnected after we authenticated them.
|
||||
return;
|
||||
}
|
||||
|
||||
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
||||
// is enabled.
|
||||
try {
|
||||
mcConnection.enableEncryption(decryptedSharedSecret);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
ListenableFuture<Response> hasJoinedResponse = server.getAsyncHttpClient().prepareGet(url)
|
||||
.execute();
|
||||
hasJoinedResponse.addListener(() -> {
|
||||
if (mcConnection.isClosed()) {
|
||||
// The player disconnected after we authenticated them.
|
||||
return;
|
||||
}
|
||||
|
||||
if (profileResponse.getCode() == 200) {
|
||||
// All went well, initialize the session.
|
||||
initializePlayer(GSON.fromJson(profileResponse.getBody(), GameProfile.class), true);
|
||||
} else if (profileResponse.getCode() == 204) {
|
||||
// Apparently an offline-mode user logged onto this online-mode proxy.
|
||||
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
|
||||
} else {
|
||||
// Something else went wrong
|
||||
logger.error(
|
||||
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
|
||||
profileResponse.getCode(), login.getUsername(), playerIp);
|
||||
mcConnection.close();
|
||||
}
|
||||
}, mcConnection.eventLoop())
|
||||
.exceptionally(exception -> {
|
||||
logger.error("Unable to enable encryption", exception);
|
||||
// Go ahead and enable encryption. Once the client sends EncryptionResponse, encryption
|
||||
// is enabled.
|
||||
try {
|
||||
mcConnection.enableEncryption(decryptedSharedSecret);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
Response profileResponse = hasJoinedResponse.get();
|
||||
if (profileResponse.getStatusCode() == 200) {
|
||||
// All went well, initialize the session.
|
||||
initializePlayer(GSON.fromJson(profileResponse.getResponseBody(), GameProfile.class),
|
||||
true);
|
||||
} else if (profileResponse.getStatusCode() == 204) {
|
||||
// Apparently an offline-mode user logged onto this online-mode proxy.
|
||||
inbound.disconnect(VelocityMessages.ONLINE_MODE_ONLY);
|
||||
} else {
|
||||
// Something else went wrong
|
||||
logger.error(
|
||||
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
|
||||
profileResponse.getStatusCode(), login.getUsername(), playerIp);
|
||||
mcConnection.close();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} catch (ExecutionException e) {
|
||||
logger.error("Unable to authenticate with Mojang", e);
|
||||
mcConnection.close();
|
||||
} catch (InterruptedException e) {
|
||||
// not much we can do usefully
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}, mcConnection.eventLoop());
|
||||
} catch (GeneralSecurityException e) {
|
||||
logger.error("Unable to enable encryption", e);
|
||||
mcConnection.close();
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -179,6 +189,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
// The player was disconnected
|
||||
return;
|
||||
}
|
||||
|
||||
PreLoginComponentResult result = event.getResult();
|
||||
Optional<Component> disconnectReason = result.getReason();
|
||||
if (disconnectReason.isPresent()) {
|
||||
@ -277,7 +288,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
player.disconnect(VelocityMessages.ALREADY_CONNECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
mcConnection.setSessionHandler(new InitialConnectSessionHandler(player));
|
||||
server.getEventManager().fire(new PostLoginEvent(player))
|
||||
.thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget());
|
||||
|
@ -1,12 +1,15 @@
|
||||
package com.velocitypowered.proxy.connection.client;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.spotify.futures.CompletableFutures;
|
||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.InboundConnection;
|
||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||
import com.velocitypowered.api.proxy.server.ServerPing;
|
||||
import com.velocitypowered.api.util.ModInfo;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.PingPassthroughMode;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
@ -15,27 +18,32 @@ import com.velocitypowered.proxy.protocol.packet.LegacyPing;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusPing;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequest;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusResponse;
|
||||
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
private final VelocityServer server;
|
||||
private final MinecraftConnection connection;
|
||||
private final InboundConnection inboundWrapper;
|
||||
private final InboundConnection inbound;
|
||||
|
||||
StatusSessionHandler(VelocityServer server, MinecraftConnection connection,
|
||||
InboundConnection inboundWrapper) {
|
||||
InboundConnection inbound) {
|
||||
this.server = server;
|
||||
this.connection = connection;
|
||||
this.inboundWrapper = inboundWrapper;
|
||||
this.inbound = inbound;
|
||||
}
|
||||
|
||||
private ServerPing createInitialPing() {
|
||||
private ServerPing constructLocalPing(ProtocolVersion version) {
|
||||
VelocityConfiguration configuration = server.getConfiguration();
|
||||
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
|
||||
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
||||
return new ServerPing(
|
||||
new ServerPing.Version(shownVersion.getProtocol(),
|
||||
new ServerPing.Version(version.getProtocol(),
|
||||
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
|
||||
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
|
||||
ImmutableList.of()),
|
||||
@ -45,12 +53,78 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
);
|
||||
}
|
||||
|
||||
private CompletableFuture<ServerPing> attemptPingPassthrough(PingPassthroughMode mode,
|
||||
List<String> servers, ProtocolVersion pingingVersion) {
|
||||
ServerPing fallback = constructLocalPing(pingingVersion);
|
||||
List<CompletableFuture<ServerPing>> pings = new ArrayList<>();
|
||||
for (String s : servers) {
|
||||
Optional<RegisteredServer> rs = server.getServer(s);
|
||||
if (!rs.isPresent()) {
|
||||
continue;
|
||||
}
|
||||
VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get();
|
||||
pings.add(vrs.ping(connection.eventLoop(), pingingVersion));
|
||||
}
|
||||
if (pings.isEmpty()) {
|
||||
return CompletableFuture.completedFuture(fallback);
|
||||
}
|
||||
|
||||
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
|
||||
(ex) -> fallback);
|
||||
switch (mode) {
|
||||
case ALL:
|
||||
return pingResponses.thenApply(responses -> {
|
||||
// Find the first non-fallback
|
||||
for (ServerPing response : responses) {
|
||||
if (response == fallback) {
|
||||
continue;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
return fallback;
|
||||
});
|
||||
case MODS:
|
||||
return pingResponses.thenApply(responses -> {
|
||||
// Find the first non-fallback that contains a mod list
|
||||
for (ServerPing response : responses) {
|
||||
if (response == fallback) {
|
||||
continue;
|
||||
}
|
||||
Optional<ModInfo> modInfo = response.getModinfo();
|
||||
if (modInfo.isPresent()) {
|
||||
return fallback.asBuilder().mods(modInfo.get()).build();
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
});
|
||||
default:
|
||||
// Not possible, but covered for completeness.
|
||||
return CompletableFuture.completedFuture(fallback);
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<ServerPing> getInitialPing() {
|
||||
VelocityConfiguration configuration = server.getConfiguration();
|
||||
ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion())
|
||||
? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION;
|
||||
PingPassthroughMode passthrough = configuration.getPingPassthrough();
|
||||
|
||||
if (passthrough == PingPassthroughMode.DISABLED) {
|
||||
return CompletableFuture.completedFuture(constructLocalPing(shownVersion));
|
||||
} else {
|
||||
String virtualHostStr = inbound.getVirtualHost().map(InetSocketAddress::getHostString)
|
||||
.orElse("");
|
||||
List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
|
||||
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
|
||||
return attemptPingPassthrough(configuration.getPingPassthrough(), serversToTry, shownVersion);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(LegacyPing packet) {
|
||||
ServerPing initialPing = createInitialPing();
|
||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
||||
server.getEventManager().fire(event)
|
||||
.thenRunAsync(() -> {
|
||||
getInitialPing()
|
||||
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
|
||||
.thenAcceptAsync(event -> {
|
||||
connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(),
|
||||
packet.getVersion()));
|
||||
}, connection.eventLoop());
|
||||
@ -65,11 +139,10 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public boolean handle(StatusRequest packet) {
|
||||
ServerPing initialPing = createInitialPing();
|
||||
ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing);
|
||||
server.getEventManager().fire(event)
|
||||
.thenRunAsync(
|
||||
() -> {
|
||||
getInitialPing()
|
||||
.thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping)))
|
||||
.thenAcceptAsync(
|
||||
(event) -> {
|
||||
StringBuilder json = new StringBuilder();
|
||||
VelocityServer.GSON.toJson(event.getPing(), json);
|
||||
connection.write(new StatusResponse(json));
|
||||
|
@ -114,7 +114,7 @@ public enum LegacyForgeHandshakeBackendPhase implements BackendConnectionPhase {
|
||||
serverConnection.setConnectionPhase(newPhase);
|
||||
|
||||
// Write the packet to the player, we don't need it now.
|
||||
player.getMinecraftConnection().write(message);
|
||||
player.getConnection().write(message.retain());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
|
||||
COMPLETE(null) {
|
||||
@Override
|
||||
public void resetConnectionPhase(ConnectedPlayer player) {
|
||||
player.getMinecraftConnection().write(LegacyForgeUtil.resetPacket());
|
||||
player.getConnection().write(LegacyForgeUtil.resetPacket());
|
||||
player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED);
|
||||
}
|
||||
|
||||
@ -212,7 +212,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase {
|
||||
PluginMessage message,
|
||||
MinecraftConnection backendConn) {
|
||||
// Send the packet on to the server.
|
||||
backendConn.write(message);
|
||||
backendConn.write(message.retain());
|
||||
|
||||
// We handled the packet. No need to continue processing.
|
||||
return true;
|
||||
|
@ -27,9 +27,8 @@ class LegacyForgeUtil {
|
||||
*/
|
||||
static byte getHandshakePacketDiscriminator(PluginMessage message) {
|
||||
Preconditions.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL));
|
||||
byte[] data = message.getData();
|
||||
Preconditions.checkArgument(data.length >= 1);
|
||||
return data[0];
|
||||
Preconditions.checkArgument(message.content().isReadable());
|
||||
return message.content().getByte(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,7 +43,7 @@ class LegacyForgeUtil {
|
||||
.checkArgument(message.getChannel().equals(FORGE_LEGACY_HANDSHAKE_CHANNEL),
|
||||
"message is not a FML HS plugin message");
|
||||
|
||||
ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData());
|
||||
ByteBuf byteBuf = message.content().retainedSlice();
|
||||
try {
|
||||
byte discriminator = byteBuf.readByte();
|
||||
|
||||
@ -75,7 +74,7 @@ class LegacyForgeUtil {
|
||||
static PluginMessage resetPacket() {
|
||||
PluginMessage msg = new PluginMessage();
|
||||
msg.setChannel(FORGE_LEGACY_HANDSHAKE_CHANNEL);
|
||||
msg.setData(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone());
|
||||
msg.replace(Unpooled.wrappedBuffer(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone()));
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
package com.velocitypowered.proxy.network;
|
||||
|
||||
import static org.asynchttpclient.Dsl.asyncHttpClient;
|
||||
import static org.asynchttpclient.Dsl.config;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.network.netty.DnsAddressResolverGroupNameResolverAdapter;
|
||||
import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler;
|
||||
import io.netty.bootstrap.Bootstrap;
|
||||
import io.netty.bootstrap.ServerBootstrap;
|
||||
@ -11,17 +15,26 @@ import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelOption;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.channel.epoll.EpollChannelOption;
|
||||
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
||||
import io.netty.resolver.dns.DnsNameResolverBuilder;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.asynchttpclient.AsyncHttpClient;
|
||||
import org.asynchttpclient.RequestBuilder;
|
||||
import org.asynchttpclient.filter.FilterContext;
|
||||
import org.asynchttpclient.filter.FilterContext.FilterContextBuilder;
|
||||
import org.asynchttpclient.filter.FilterException;
|
||||
import org.asynchttpclient.filter.RequestFilter;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public final class ConnectionManager {
|
||||
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 21,
|
||||
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
|
||||
1 << 21);
|
||||
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
|
||||
private final Map<InetSocketAddress, Channel> endpoints = new HashMap<>();
|
||||
@ -35,6 +48,7 @@ public final class ConnectionManager {
|
||||
public final ServerChannelInitializerHolder serverChannelInitializer;
|
||||
|
||||
private final DnsAddressResolverGroup resolverGroup;
|
||||
private final AsyncHttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Initalizes the {@code ConnectionManager}.
|
||||
@ -48,12 +62,26 @@ public final class ConnectionManager {
|
||||
this.workerGroup = this.transportType.createEventLoopGroup(TransportType.Type.WORKER);
|
||||
this.serverChannelInitializer = new ServerChannelInitializerHolder(
|
||||
new ServerChannelInitializer(this.server));
|
||||
this.resolverGroup = new DnsAddressResolverGroup(
|
||||
new DnsNameResolverBuilder()
|
||||
.channelType(this.transportType.datagramChannelClass)
|
||||
.negativeTtl(15)
|
||||
.ndots(1)
|
||||
);
|
||||
this.resolverGroup = new DnsAddressResolverGroup(new DnsNameResolverBuilder()
|
||||
.channelType(this.transportType.datagramChannelClass)
|
||||
.negativeTtl(15)
|
||||
.ndots(1));
|
||||
this.httpClient = asyncHttpClient(config()
|
||||
.setEventLoopGroup(this.workerGroup)
|
||||
.setUserAgent(server.getVersion().getName() + "/" + server.getVersion().getVersion())
|
||||
.addRequestFilter(new RequestFilter() {
|
||||
@Override
|
||||
public <T> FilterContext<T> filter(FilterContext<T> ctx) throws FilterException {
|
||||
return new FilterContextBuilder<>(ctx)
|
||||
.request(new RequestBuilder(ctx.getRequest())
|
||||
.setNameResolver(
|
||||
new DnsAddressResolverGroupNameResolverAdapter(resolverGroup, workerGroup)
|
||||
)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
})
|
||||
.build());
|
||||
}
|
||||
|
||||
public void logChannelInformation() {
|
||||
@ -75,6 +103,11 @@ public final class ConnectionManager {
|
||||
.childOption(ChannelOption.TCP_NODELAY, true)
|
||||
.childOption(ChannelOption.IP_TOS, 0x18)
|
||||
.localAddress(address);
|
||||
|
||||
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
|
||||
bootstrap.option(EpollChannelOption.TCP_FASTOPEN, 3);
|
||||
}
|
||||
|
||||
bootstrap.bind()
|
||||
.addListener((ChannelFutureListener) future -> {
|
||||
final Channel channel = future.channel();
|
||||
@ -112,25 +145,25 @@ public final class ConnectionManager {
|
||||
});
|
||||
}
|
||||
|
||||
public Bootstrap createWorker() {
|
||||
return this.createWorker(this.workerGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TCP {@link Bootstrap} using Velocity's event loops.
|
||||
*
|
||||
* @param group the event loop group to use
|
||||
* @param group the event loop group to use. Use {@code null} for the default worker group.
|
||||
*
|
||||
* @return a new {@link Bootstrap}
|
||||
*/
|
||||
public Bootstrap createWorker(EventLoopGroup group) {
|
||||
return new Bootstrap()
|
||||
public Bootstrap createWorker(@Nullable EventLoopGroup group) {
|
||||
Bootstrap bootstrap = new Bootstrap()
|
||||
.channel(this.transportType.socketChannelClass)
|
||||
.group(group)
|
||||
.option(ChannelOption.TCP_NODELAY, true)
|
||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
||||
this.server.getConfiguration().getConnectTimeout())
|
||||
.group(group == null ? this.workerGroup : group)
|
||||
.resolver(this.resolverGroup);
|
||||
if (transportType == TransportType.EPOLL && server.getConfiguration().useTcpFastOpen()) {
|
||||
bootstrap.option(EpollChannelOption.TCP_FASTOPEN_CONNECT, true);
|
||||
}
|
||||
return bootstrap;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,11 +197,11 @@ public final class ConnectionManager {
|
||||
return bossGroup;
|
||||
}
|
||||
|
||||
public EventLoopGroup getWorkerGroup() {
|
||||
return workerGroup;
|
||||
}
|
||||
|
||||
public ServerChannelInitializerHolder getServerChannelInitializer() {
|
||||
return this.serverChannelInitializer;
|
||||
}
|
||||
|
||||
public AsyncHttpClient getHttpClient() {
|
||||
return httpClient;
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,12 @@
|
||||
package com.velocitypowered.proxy.network;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory;
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.epoll.Epoll;
|
||||
import io.netty.channel.epoll.EpollDatagramChannel;
|
||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
||||
import io.netty.channel.epoll.EpollServerSocketChannel;
|
||||
import io.netty.channel.epoll.EpollSocketChannel;
|
||||
import io.netty.channel.kqueue.KQueue;
|
||||
import io.netty.channel.kqueue.KQueueDatagramChannel;
|
||||
import io.netty.channel.kqueue.KQueueEventLoopGroup;
|
||||
import io.netty.channel.kqueue.KQueueServerSocketChannel;
|
||||
import io.netty.channel.kqueue.KQueueSocketChannel;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
@ -27,10 +22,7 @@ enum TransportType {
|
||||
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))),
|
||||
EPOLL("epoll", EpollServerSocketChannel.class, EpollSocketChannel.class,
|
||||
EpollDatagramChannel.class,
|
||||
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))),
|
||||
KQUEUE("Kqueue", KQueueServerSocketChannel.class, KQueueSocketChannel.class,
|
||||
KQueueDatagramChannel.class,
|
||||
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)));
|
||||
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)));
|
||||
|
||||
final String name;
|
||||
final Class<? extends ServerSocketChannel> serverSocketChannelClass;
|
||||
@ -60,17 +52,16 @@ enum TransportType {
|
||||
}
|
||||
|
||||
private static ThreadFactory createThreadFactory(final String name, final Type type) {
|
||||
return new ThreadFactoryBuilder()
|
||||
.setNameFormat("Netty " + name + ' ' + type.toString() + " #%d")
|
||||
.setDaemon(true)
|
||||
.build();
|
||||
return new VelocityNettyThreadFactory("Netty " + name + ' ' + type.toString() + " #%d");
|
||||
}
|
||||
|
||||
public static TransportType bestType() {
|
||||
if (Boolean.getBoolean("velocity.disable-native-transport")) {
|
||||
return NIO;
|
||||
}
|
||||
|
||||
if (Epoll.isAvailable()) {
|
||||
return EPOLL;
|
||||
} else if (KQueue.isAvailable()) {
|
||||
return KQUEUE;
|
||||
} else {
|
||||
return NIO;
|
||||
}
|
||||
|
@ -1,153 +0,0 @@
|
||||
package com.velocitypowered.proxy.network.http;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.handler.codec.http.DefaultFullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpClientCodec;
|
||||
import io.netty.handler.codec.http.HttpContentCompressor;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import io.netty.handler.codec.http.HttpRequest;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.ssl.SslContext;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
|
||||
public class NettyHttpClient {
|
||||
|
||||
private final String userAgent;
|
||||
private final VelocityServer server;
|
||||
|
||||
/**
|
||||
* Initializes the HTTP client.
|
||||
*
|
||||
* @param server the Velocity server
|
||||
*/
|
||||
public NettyHttpClient(VelocityServer server) {
|
||||
this.userAgent = server.getVersion().getName() + "/" + server.getVersion().getVersion();
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
private ChannelFuture establishConnection(URL url, EventLoop loop) {
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
boolean ssl = url.getProtocol().equals("https");
|
||||
if (port == -1) {
|
||||
port = ssl ? 443 : 80;
|
||||
}
|
||||
|
||||
InetSocketAddress address = InetSocketAddress.createUnresolved(host, port);
|
||||
return server.initializeGenericBootstrap(loop)
|
||||
.handler(new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
protected void initChannel(Channel ch) throws Exception {
|
||||
if (ssl) {
|
||||
SslContext context = SslContextBuilder.forClient().protocols("TLSv1.2").build();
|
||||
// Unbelievably, Java doesn't automatically check the CN to make sure we're talking
|
||||
// to the right host! Therefore, we provide the intended host name and port, along
|
||||
// with asking Java very nicely if it could check the hostname in the certificate
|
||||
// for us.
|
||||
SSLEngine engine = context.newEngine(ch.alloc(), address.getHostString(),
|
||||
address.getPort());
|
||||
engine.getSSLParameters().setEndpointIdentificationAlgorithm("HTTPS");
|
||||
ch.pipeline().addLast("ssl", new SslHandler(engine));
|
||||
}
|
||||
ch.pipeline().addLast("http", new HttpClientCodec());
|
||||
}
|
||||
})
|
||||
.connect(address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts an HTTP GET request to the specified URL.
|
||||
* @param url the URL to fetch
|
||||
* @param loop the event loop to use
|
||||
* @return a future representing the response
|
||||
*/
|
||||
public CompletableFuture<SimpleHttpResponse> get(URL url, EventLoop loop) {
|
||||
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
|
||||
establishConnection(url, loop)
|
||||
.addListener((ChannelFutureListener) future -> {
|
||||
if (future.isSuccess()) {
|
||||
Channel channel = future.channel();
|
||||
|
||||
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
|
||||
|
||||
String pathAndQuery = url.getPath();
|
||||
if (url.getQuery() != null && url.getQuery().length() > 0) {
|
||||
pathAndQuery += "?" + url.getQuery();
|
||||
}
|
||||
|
||||
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
|
||||
HttpMethod.GET, pathAndQuery);
|
||||
request.headers().add(HttpHeaderNames.HOST, url.getHost());
|
||||
request.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
|
||||
channel.writeAndFlush(request, channel.voidPromise());
|
||||
} else {
|
||||
reply.completeExceptionally(future.cause());
|
||||
}
|
||||
});
|
||||
return reply;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts an HTTP POST request to the specified URL.
|
||||
* @param url the URL to fetch
|
||||
* @param body the body to post
|
||||
* @param decorator a consumer that can modify the request as required
|
||||
* @return a future representing the response
|
||||
*/
|
||||
public CompletableFuture<SimpleHttpResponse> post(URL url, ByteBuf body,
|
||||
Consumer<HttpRequest> decorator) {
|
||||
return post(url, server.getWorkerGroup().next(), body, decorator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts an HTTP POST request to the specified URL.
|
||||
* @param url the URL to fetch
|
||||
* @param loop the event loop to use
|
||||
* @param body the body to post
|
||||
* @param decorator a consumer that can modify the request as required
|
||||
* @return a future representing the response
|
||||
*/
|
||||
public CompletableFuture<SimpleHttpResponse> post(URL url, EventLoop loop, ByteBuf body,
|
||||
Consumer<HttpRequest> decorator) {
|
||||
CompletableFuture<SimpleHttpResponse> reply = new CompletableFuture<>();
|
||||
establishConnection(url, loop)
|
||||
.addListener((ChannelFutureListener) future -> {
|
||||
if (future.isSuccess()) {
|
||||
Channel channel = future.channel();
|
||||
|
||||
channel.pipeline().addLast("collector", new SimpleHttpResponseCollector(reply));
|
||||
|
||||
String pathAndQuery = url.getPath();
|
||||
if (url.getQuery() != null && url.getQuery().length() > 0) {
|
||||
pathAndQuery += "?" + url.getQuery();
|
||||
}
|
||||
|
||||
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
|
||||
HttpMethod.POST, pathAndQuery, body);
|
||||
request.headers().add(HttpHeaderNames.HOST, url.getHost());
|
||||
request.headers().add(HttpHeaderNames.USER_AGENT, userAgent);
|
||||
request.headers().add(HttpHeaderNames.CONTENT_LENGTH, body.readableBytes());
|
||||
decorator.accept(request);
|
||||
|
||||
channel.writeAndFlush(request, channel.voidPromise());
|
||||
} else {
|
||||
body.release();
|
||||
reply.completeExceptionally(future.cause());
|
||||
}
|
||||
});
|
||||
return reply;
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package com.velocitypowered.proxy.network.http;
|
||||
|
||||
public class SimpleHttpResponse {
|
||||
|
||||
private final int code;
|
||||
private final String body;
|
||||
|
||||
SimpleHttpResponse(int code, String body) {
|
||||
this.code = code;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleHttpResponse{"
|
||||
+ "code=" + code
|
||||
+ ", body='" + body + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
package com.velocitypowered.proxy.network.http;
|
||||
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||
import io.netty.handler.codec.http.HttpContent;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import io.netty.handler.codec.http.HttpResponseStatus;
|
||||
import io.netty.handler.codec.http.HttpUtil;
|
||||
import io.netty.handler.codec.http.LastHttpContent;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private final StringBuilder buffer = new StringBuilder();
|
||||
private final CompletableFuture<SimpleHttpResponse> reply;
|
||||
private int httpCode;
|
||||
|
||||
SimpleHttpResponseCollector(CompletableFuture<SimpleHttpResponse> reply) {
|
||||
this.reply = reply;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
try {
|
||||
if (msg instanceof HttpResponse) {
|
||||
HttpResponse response = (HttpResponse) msg;
|
||||
HttpResponseStatus status = response.status();
|
||||
this.httpCode = status.code();
|
||||
}
|
||||
|
||||
if (msg instanceof HttpContent) {
|
||||
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
|
||||
|
||||
if (msg instanceof LastHttpContent) {
|
||||
ctx.close();
|
||||
reply.complete(new SimpleHttpResponse(httpCode, buffer.toString()));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
|
||||
ctx.close();
|
||||
reply.completeExceptionally(cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package com.velocitypowered.proxy.network.netty;
|
||||
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.resolver.InetNameResolver;
|
||||
import io.netty.resolver.dns.DnsAddressResolverGroup;
|
||||
import io.netty.util.concurrent.EventExecutor;
|
||||
import io.netty.util.concurrent.FutureListener;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DnsAddressResolverGroupNameResolverAdapter extends InetNameResolver {
|
||||
|
||||
private final DnsAddressResolverGroup resolverGroup;
|
||||
private final EventLoopGroup group;
|
||||
|
||||
/**
|
||||
* Creates a DnsAddressResolverGroupNameResolverAdapter.
|
||||
* @param resolverGroup the resolver group to use
|
||||
* @param group the event loop group
|
||||
*/
|
||||
public DnsAddressResolverGroupNameResolverAdapter(
|
||||
DnsAddressResolverGroup resolverGroup, EventLoopGroup group) {
|
||||
super(ImmediateEventExecutor.INSTANCE);
|
||||
this.resolverGroup = resolverGroup;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
|
||||
EventExecutor executor = this.findExecutor();
|
||||
resolverGroup.getResolver(executor).resolve(InetSocketAddress.createUnresolved(inetHost, 17))
|
||||
.addListener((FutureListener<InetSocketAddress>) future -> {
|
||||
if (future.isSuccess()) {
|
||||
promise.trySuccess(future.getNow().getAddress());
|
||||
} else {
|
||||
promise.tryFailure(future.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doResolveAll(String inetHost, Promise<List<InetAddress>> promise)
|
||||
throws Exception {
|
||||
EventExecutor executor = this.findExecutor();
|
||||
resolverGroup.getResolver(executor).resolveAll(InetSocketAddress.createUnresolved(inetHost, 17))
|
||||
.addListener((FutureListener<List<InetSocketAddress>>) future -> {
|
||||
if (future.isSuccess()) {
|
||||
List<InetAddress> addresses = new ArrayList<>(future.getNow().size());
|
||||
for (InetSocketAddress address : future.getNow()) {
|
||||
addresses.add(address.getAddress());
|
||||
}
|
||||
promise.trySuccess(addresses);
|
||||
} else {
|
||||
promise.tryFailure(future.cause());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private EventExecutor findExecutor() {
|
||||
for (EventExecutor executor : group) {
|
||||
if (executor.inEventLoop()) {
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
|
||||
// otherwise, pick one
|
||||
return group.next();
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package com.velocitypowered.proxy.plugin.loader.java;
|
||||
|
||||
import com.google.inject.Binder;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.Scopes;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
@ -27,6 +28,8 @@ class VelocityPluginModule implements Module {
|
||||
|
||||
@Override
|
||||
public void configure(Binder binder) {
|
||||
binder.bind(description.getMainClass()).in(Scopes.SINGLETON);
|
||||
|
||||
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId()));
|
||||
binder.bind(ProxyServer.class).toInstance(server);
|
||||
binder.bind(Path.class).annotatedWith(DataDirectory.class)
|
||||
|
@ -35,10 +35,9 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
|
||||
checkFrame(expectedSize >= threshold, "Uncompressed size %s is less than threshold %s",
|
||||
expectedSize, threshold);
|
||||
checkFrame(expectedSize <= MAXIMUM_UNCOMPRESSED_SIZE, "Expected uncompressed size"
|
||||
+ "%s is larger than protocol maximum of %s", expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
|
||||
int initialCapacity = Math.min(expectedSize, MAXIMUM_UNCOMPRESSED_SIZE);
|
||||
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, expectedSize);
|
||||
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, initialCapacity);
|
||||
try {
|
||||
compressor.inflate(compatibleIn, uncompressed, expectedSize);
|
||||
out.add(uncompressed);
|
||||
|
@ -4,24 +4,24 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class LoginPluginMessage implements MinecraftPacket {
|
||||
public class LoginPluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
|
||||
|
||||
private int id;
|
||||
private @Nullable String channel;
|
||||
private ByteBuf data = Unpooled.EMPTY_BUFFER;
|
||||
|
||||
public LoginPluginMessage() {
|
||||
|
||||
super(null);
|
||||
}
|
||||
|
||||
public LoginPluginMessage(int id, @Nullable String channel, ByteBuf data) {
|
||||
super(data);
|
||||
this.id = id;
|
||||
this.channel = channel;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
@ -35,16 +35,12 @@ public class LoginPluginMessage implements MinecraftPacket {
|
||||
return channel;
|
||||
}
|
||||
|
||||
public ByteBuf getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoginPluginMessage{"
|
||||
+ "id=" + id
|
||||
+ ", channel='" + channel + '\''
|
||||
+ ", data=" + data
|
||||
+ ", data=" + super.toString()
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@ -53,9 +49,9 @@ public class LoginPluginMessage implements MinecraftPacket {
|
||||
this.id = ProtocolUtils.readVarInt(buf);
|
||||
this.channel = ProtocolUtils.readString(buf);
|
||||
if (buf.isReadable()) {
|
||||
this.data = buf.readSlice(buf.readableBytes());
|
||||
this.replace(buf.readSlice(buf.readableBytes()));
|
||||
} else {
|
||||
this.data = Unpooled.EMPTY_BUFFER;
|
||||
this.replace(Unpooled.EMPTY_BUFFER);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,7 +62,7 @@ public class LoginPluginMessage implements MinecraftPacket {
|
||||
throw new IllegalStateException("Channel is not specified!");
|
||||
}
|
||||
ProtocolUtils.writeString(buf, channel);
|
||||
buf.writeBytes(data);
|
||||
buf.writeBytes(content());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -4,14 +4,25 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
public class LoginPluginResponse implements MinecraftPacket {
|
||||
public class LoginPluginResponse extends DeferredByteBufHolder implements MinecraftPacket {
|
||||
|
||||
private int id;
|
||||
private boolean success;
|
||||
private ByteBuf data = Unpooled.EMPTY_BUFFER;
|
||||
|
||||
public LoginPluginResponse() {
|
||||
super(Unpooled.EMPTY_BUFFER);
|
||||
}
|
||||
|
||||
public LoginPluginResponse(int id, boolean success, @MonotonicNonNull ByteBuf buf) {
|
||||
super(buf);
|
||||
this.id = id;
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
@ -29,20 +40,12 @@ public class LoginPluginResponse implements MinecraftPacket {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public ByteBuf getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(ByteBuf data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LoginPluginResponse{"
|
||||
+ "id=" + id
|
||||
+ ", success=" + success
|
||||
+ ", data=" + data
|
||||
+ ", data=" + super.toString()
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@ -51,9 +54,9 @@ public class LoginPluginResponse implements MinecraftPacket {
|
||||
this.id = ProtocolUtils.readVarInt(buf);
|
||||
this.success = buf.readBoolean();
|
||||
if (buf.isReadable()) {
|
||||
this.data = buf.readSlice(buf.readableBytes());
|
||||
this.replace(buf.readSlice(buf.readableBytes()));
|
||||
} else {
|
||||
this.data = Unpooled.EMPTY_BUFFER;
|
||||
this.replace(Unpooled.EMPTY_BUFFER);
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,7 +64,7 @@ public class LoginPluginResponse implements MinecraftPacket {
|
||||
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
ProtocolUtils.writeVarInt(buf, id);
|
||||
buf.writeBoolean(success);
|
||||
buf.writeBytes(data);
|
||||
buf.writeBytes(content());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,21 +1,29 @@
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
|
||||
import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.transformLegacyToModernChannel;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class PluginMessage implements MinecraftPacket {
|
||||
public class PluginMessage extends DeferredByteBufHolder implements MinecraftPacket {
|
||||
|
||||
private @Nullable String channel;
|
||||
private byte[] data = EMPTY_BYTE_ARRAY;
|
||||
|
||||
public PluginMessage() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
public PluginMessage(String channel,
|
||||
@MonotonicNonNull ByteBuf backing) {
|
||||
super(backing);
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public String getChannel() {
|
||||
if (channel == null) {
|
||||
@ -28,19 +36,11 @@ public class PluginMessage implements MinecraftPacket {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PluginMessage{"
|
||||
+ "channel='" + channel + '\''
|
||||
+ ", data=<removed>"
|
||||
+ ", data=" + super.toString()
|
||||
+ '}';
|
||||
}
|
||||
|
||||
@ -50,8 +50,7 @@ public class PluginMessage implements MinecraftPacket {
|
||||
if (version.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0) {
|
||||
this.channel = transformLegacyToModernChannel(this.channel);
|
||||
}
|
||||
this.data = new byte[buf.readableBytes()];
|
||||
buf.readBytes(data);
|
||||
this.replace(buf.readRetainedSlice(buf.readableBytes()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -64,11 +63,51 @@ public class PluginMessage implements MinecraftPacket {
|
||||
} else {
|
||||
ProtocolUtils.writeString(buf, this.channel);
|
||||
}
|
||||
buf.writeBytes(data);
|
||||
buf.writeBytes(content());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage copy() {
|
||||
return (PluginMessage) super.copy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage duplicate() {
|
||||
return (PluginMessage) super.duplicate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage retainedDuplicate() {
|
||||
return (PluginMessage) super.retainedDuplicate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage replace(ByteBuf content) {
|
||||
return (PluginMessage) super.replace(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage retain() {
|
||||
return (PluginMessage) super.retain();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage retain(int increment) {
|
||||
return (PluginMessage) super.retain(increment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage touch() {
|
||||
return (PluginMessage) super.touch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginMessage touch(Object hint) {
|
||||
return (PluginMessage) super.touch(hint);
|
||||
}
|
||||
}
|
||||
|
@ -133,5 +133,9 @@ public class TabCompleteResponse implements MinecraftPacket {
|
||||
public int compareTo(Offer o) {
|
||||
return this.text.compareTo(o.text);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
package com.velocitypowered.proxy.protocol.util;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufHolder;
|
||||
import io.netty.util.IllegalReferenceCountException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A special-purpose implementation of {@code ByteBufHolder} that can defer accepting its buffer.
|
||||
* This is required because Velocity packets are, for better or worse, mutable.
|
||||
*/
|
||||
public class DeferredByteBufHolder implements ByteBufHolder {
|
||||
|
||||
@MonotonicNonNull
|
||||
private ByteBuf backing;
|
||||
|
||||
public DeferredByteBufHolder(
|
||||
@MonotonicNonNull ByteBuf backing) {
|
||||
this.backing = backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuf content() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
if (backing.refCnt() <= 0) {
|
||||
throw new IllegalReferenceCountException(backing.refCnt());
|
||||
}
|
||||
return backing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder copy() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return new DeferredByteBufHolder(backing.copy());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder duplicate() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return new DeferredByteBufHolder(backing.duplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder retainedDuplicate() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return new DeferredByteBufHolder(backing.retainedDuplicate());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder replace(ByteBuf content) {
|
||||
if (content == null) {
|
||||
throw new NullPointerException("content");
|
||||
}
|
||||
this.backing = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int refCnt() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return backing.refCnt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder retain() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.retain();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder retain(int increment) {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.retain(increment);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder touch() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.touch();
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBufHolder touch(Object hint) {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
backing.touch(hint);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release() {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return backing.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean release(int decrement) {
|
||||
if (backing == null) {
|
||||
throw new IllegalStateException("Trying to obtain contents of holder with a null buffer");
|
||||
}
|
||||
return backing.release(decrement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String str = "DeferredByteBufHolder[";
|
||||
if (backing == null) {
|
||||
str += "null";
|
||||
} else {
|
||||
str += backing.toString();
|
||||
}
|
||||
return str + "]";
|
||||
}
|
||||
}
|
@ -11,7 +11,9 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.util.ByteProcessor;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@ -93,12 +95,12 @@ public class PluginMessageUtil {
|
||||
checkNotNull(message, "message");
|
||||
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
|
||||
message.getChannel());
|
||||
if (message.getData().length == 0) {
|
||||
if (!message.content().isReadable()) {
|
||||
// If we try to split this, we will get an one-element array with the empty string, which
|
||||
// has caused issues with 1.13+ compatibility. Just return an empty list.
|
||||
return ImmutableList.of();
|
||||
}
|
||||
String channels = new String(message.getData(), StandardCharsets.UTF_8);
|
||||
String channels = message.content().toString(StandardCharsets.UTF_8);
|
||||
return ImmutableList.copyOf(channels.split("\0"));
|
||||
}
|
||||
|
||||
@ -114,10 +116,9 @@ public class PluginMessageUtil {
|
||||
Preconditions.checkArgument(channels.size() > 0, "no channels specified");
|
||||
String channelName = protocolVersion.compareTo(ProtocolVersion.MINECRAFT_1_13) >= 0
|
||||
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
|
||||
PluginMessage message = new PluginMessage();
|
||||
message.setChannel(channelName);
|
||||
message.setData(String.join("\0", channels).getBytes(StandardCharsets.UTF_8));
|
||||
return message;
|
||||
ByteBuf contents = Unpooled.buffer();
|
||||
contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8);
|
||||
return new PluginMessage(channelName, contents);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -133,21 +134,11 @@ public class PluginMessageUtil {
|
||||
|
||||
String toAppend = " (" + version.getName() + ")";
|
||||
|
||||
byte[] rewrittenData;
|
||||
ByteBuf rewrittenBuf = Unpooled.buffer();
|
||||
try {
|
||||
String currentBrand = ProtocolUtils.readString(Unpooled.wrappedBuffer(message.getData()));
|
||||
ProtocolUtils.writeString(rewrittenBuf, currentBrand + toAppend);
|
||||
rewrittenData = new byte[rewrittenBuf.readableBytes()];
|
||||
rewrittenBuf.readBytes(rewrittenData);
|
||||
} finally {
|
||||
rewrittenBuf.release();
|
||||
}
|
||||
String currentBrand = ProtocolUtils.readString(message.content().slice());
|
||||
ProtocolUtils.writeString(rewrittenBuf, currentBrand + toAppend);
|
||||
|
||||
PluginMessage newMsg = new PluginMessage();
|
||||
newMsg.setChannel(message.getChannel());
|
||||
newMsg.setData(rewrittenData);
|
||||
return newMsg;
|
||||
return new PluginMessage(message.getChannel(), rewrittenBuf);
|
||||
}
|
||||
|
||||
private static final Pattern INVALID_IDENTIFIER_REGEX = Pattern.compile("[^a-z0-9\\-_]*");
|
||||
@ -185,5 +176,4 @@ public class PluginMessageUtil {
|
||||
return "legacy:" + INVALID_IDENTIFIER_REGEX.matcher(lower).replaceAll("");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -18,13 +18,15 @@ public class PingSessionHandler implements MinecraftSessionHandler {
|
||||
private final CompletableFuture<ServerPing> result;
|
||||
private final RegisteredServer server;
|
||||
private final MinecraftConnection connection;
|
||||
private final ProtocolVersion version;
|
||||
private boolean completed = false;
|
||||
|
||||
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
|
||||
MinecraftConnection connection) {
|
||||
MinecraftConnection connection, ProtocolVersion version) {
|
||||
this.result = result;
|
||||
this.server = server;
|
||||
this.connection = connection;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -33,11 +35,13 @@ public class PingSessionHandler implements MinecraftSessionHandler {
|
||||
handshake.setNextStatus(StateRegistry.STATUS_ID);
|
||||
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString());
|
||||
handshake.setPort(server.getServerInfo().getAddress().getPort());
|
||||
handshake.setProtocolVersion(ProtocolVersion.MINIMUM_VERSION);
|
||||
connection.write(handshake);
|
||||
handshake.setProtocolVersion(version);
|
||||
connection.delayedWrite(handshake);
|
||||
|
||||
connection.setState(StateRegistry.STATUS);
|
||||
connection.write(StatusRequest.INSTANCE);
|
||||
connection.delayedWrite(StatusRequest.INSTANCE);
|
||||
|
||||
connection.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -9,6 +9,7 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.ServerConnection;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
@ -19,7 +20,6 @@ import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
|
||||
@ -28,6 +28,7 @@ import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.EventLoop;
|
||||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import java.util.Collection;
|
||||
import java.util.Set;
|
||||
@ -59,11 +60,22 @@ public class VelocityRegisteredServer implements RegisteredServer {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ServerPing> ping() {
|
||||
return ping(null, ProtocolVersion.UNKNOWN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pings the specified server using the specified event {@code loop}, claiming to be
|
||||
* {@code version}.
|
||||
* @param loop the event loop to use
|
||||
* @param version the version to report
|
||||
* @return the server list ping response
|
||||
*/
|
||||
public CompletableFuture<ServerPing> ping(@Nullable EventLoop loop, ProtocolVersion version) {
|
||||
if (server == null) {
|
||||
throw new IllegalStateException("No Velocity proxy instance available");
|
||||
}
|
||||
CompletableFuture<ServerPing> pingFuture = new CompletableFuture<>();
|
||||
server.initializeGenericBootstrap()
|
||||
server.createBootstrap(loop)
|
||||
.handler(new ChannelInitializer<Channel>() {
|
||||
@Override
|
||||
protected void initChannel(Channel ch) throws Exception {
|
||||
@ -87,8 +99,8 @@ public class VelocityRegisteredServer implements RegisteredServer {
|
||||
public void operationComplete(ChannelFuture future) throws Exception {
|
||||
if (future.isSuccess()) {
|
||||
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
|
||||
conn.setSessionHandler(
|
||||
new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn));
|
||||
conn.setSessionHandler(new PingSessionHandler(
|
||||
pingFuture, VelocityRegisteredServer.this, conn, version));
|
||||
} else {
|
||||
pingFuture.completeExceptionally(future.cause());
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
public class VelocityTabList implements TabList {
|
||||
|
||||
private final MinecraftConnection connection;
|
||||
private final Map<UUID, TabListEntry> entries = new ConcurrentHashMap<>();
|
||||
private final Map<UUID, VelocityTabListEntry> entries = new ConcurrentHashMap<>();
|
||||
|
||||
public VelocityTabList(MinecraftConnection connection) {
|
||||
this.connection = connection;
|
||||
@ -46,15 +46,19 @@ public class VelocityTabList implements TabList {
|
||||
"The provided entry was not created by this tab list");
|
||||
Preconditions.checkArgument(!entries.containsKey(entry.getProfile().getId()),
|
||||
"this TabList already contains an entry with the same uuid");
|
||||
Preconditions.checkArgument(entry instanceof VelocityTabListEntry,
|
||||
"Not a Velocity tab list entry");
|
||||
|
||||
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
|
||||
connection.write(
|
||||
new PlayerListItem(PlayerListItem.ADD_PLAYER, Collections.singletonList(packetItem)));
|
||||
entries.put(entry.getProfile().getId(), entry);
|
||||
entries.put(entry.getProfile().getId(), (VelocityTabListEntry) entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<TabListEntry> removeEntry(UUID uuid) {
|
||||
Preconditions.checkNotNull(uuid, "uuid");
|
||||
|
||||
TabListEntry entry = entries.remove(uuid);
|
||||
if (entry != null) {
|
||||
PlayerListItem.Item packetItem = PlayerListItem.Item.from(entry);
|
||||
@ -65,6 +69,12 @@ public class VelocityTabList implements TabList {
|
||||
return Optional.ofNullable(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsEntry(UUID uuid) {
|
||||
Preconditions.checkNotNull(uuid, "uuid");
|
||||
return entries.containsKey(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all entries from the tab list. Note that the entries are written with
|
||||
* {@link MinecraftConnection#delayedWrite(Object)}, so make sure to do an explicit
|
||||
@ -111,7 +121,7 @@ public class VelocityTabList implements TabList {
|
||||
if (name == null || properties == null) {
|
||||
throw new IllegalStateException("Got null game profile for ADD_PLAYER");
|
||||
}
|
||||
entries.put(item.getUuid(), TabListEntry.builder()
|
||||
entries.put(item.getUuid(), (VelocityTabListEntry) TabListEntry.builder()
|
||||
.tabList(this)
|
||||
.profile(new GameProfile(uuid, name, properties))
|
||||
.displayName(item.getDisplayName())
|
||||
@ -124,23 +134,23 @@ public class VelocityTabList implements TabList {
|
||||
entries.remove(uuid);
|
||||
break;
|
||||
case PlayerListItem.UPDATE_DISPLAY_NAME: {
|
||||
TabListEntry entry = entries.get(uuid);
|
||||
VelocityTabListEntry entry = entries.get(uuid);
|
||||
if (entry != null) {
|
||||
entry.setDisplayName(item.getDisplayName());
|
||||
entry.setDisplayNameInternal(item.getDisplayName());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PlayerListItem.UPDATE_LATENCY: {
|
||||
TabListEntry entry = entries.get(uuid);
|
||||
VelocityTabListEntry entry = entries.get(uuid);
|
||||
if (entry != null) {
|
||||
entry.setLatency(item.getLatency());
|
||||
entry.setLatencyInternal(item.getLatency());
|
||||
}
|
||||
break;
|
||||
}
|
||||
case PlayerListItem.UPDATE_GAMEMODE: {
|
||||
TabListEntry entry = entries.get(uuid);
|
||||
VelocityTabListEntry entry = entries.get(uuid);
|
||||
if (entry != null) {
|
||||
entry.setLatency(item.getGameMode());
|
||||
entry.setGameModeInternal(item.getGameMode());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -47,6 +47,10 @@ public class VelocityTabListEntry implements TabListEntry {
|
||||
return this;
|
||||
}
|
||||
|
||||
void setDisplayNameInternal(@Nullable Component displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLatency() {
|
||||
return latency;
|
||||
@ -59,6 +63,10 @@ public class VelocityTabListEntry implements TabListEntry {
|
||||
return this;
|
||||
}
|
||||
|
||||
void setLatencyInternal(int latency) {
|
||||
this.latency = latency;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getGameMode() {
|
||||
return gameMode;
|
||||
@ -70,4 +78,8 @@ public class VelocityTabListEntry implements TabListEntry {
|
||||
tabList.updateEntry(PlayerListItem.UPDATE_GAMEMODE, this);
|
||||
return this;
|
||||
}
|
||||
|
||||
void setGameModeInternal(int gameMode) {
|
||||
this.gameMode = gameMode;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URI;
|
||||
|
||||
@ -19,7 +21,12 @@ public class AddressUtil {
|
||||
public static InetSocketAddress parseAddress(String ip) {
|
||||
Preconditions.checkNotNull(ip, "ip");
|
||||
URI uri = URI.create("tcp://" + ip);
|
||||
return InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
|
||||
try {
|
||||
InetAddress ia = InetAddresses.forUriString(uri.getHost());
|
||||
return new InetSocketAddress(ia, uri.getPort());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return InetSocketAddress.createUnresolved(uri.getHost(), uri.getPort());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,7 +7,13 @@ import net.kyori.text.format.TextColor;
|
||||
public class VelocityMessages {
|
||||
|
||||
public static final Component ONLINE_MODE_ONLY = TextComponent
|
||||
.of("This server only accepts connections from online-mode clients.", TextColor.RED);
|
||||
.builder("This server only accepts connections from online-mode clients.")
|
||||
.color(TextColor.RED)
|
||||
.append(
|
||||
TextComponent.of("\n\nDid you change your username? Sign out of Minecraft, sign back in, "
|
||||
+ "and try again.", TextColor.GRAY)
|
||||
)
|
||||
.build();
|
||||
public static final Component NO_PROXY_BEHIND_PROXY = TextComponent
|
||||
.of("Running Velocity behind Velocity isn't supported.", TextColor.RED);
|
||||
public static final Component NO_AVAILABLE_SERVERS = TextComponent
|
||||
@ -15,7 +21,7 @@ public class VelocityMessages {
|
||||
public static final Component ALREADY_CONNECTED = TextComponent
|
||||
.of("You are already connected to this proxy!", TextColor.RED);
|
||||
public static final Component MOVED_TO_NEW_SERVER = TextComponent
|
||||
.of("You were moved from the server you were on because you were kicked", TextColor.RED);
|
||||
.of("The server you were on kicked you: ", TextColor.RED);
|
||||
|
||||
private VelocityMessages() {
|
||||
throw new AssertionError();
|
||||
|
@ -271,6 +271,6 @@ public class VelocityBossBar implements com.velocitypowered.api.util.bossbar.Bos
|
||||
|
||||
private void sendPacket(Player player, MinecraftPacket packet) {
|
||||
ConnectedPlayer connected = (ConnectedPlayer) player;
|
||||
connected.getMinecraftConnection().write(packet);
|
||||
connected.getConnection().write(packet);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
package com.velocitypowered.proxy.util.concurrent;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import io.netty.util.concurrent.FastThreadLocalThread;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class VelocityNettyThreadFactory implements ThreadFactory {
|
||||
|
||||
private final AtomicInteger threadNumber = new AtomicInteger();
|
||||
private final String nameFormat;
|
||||
|
||||
public VelocityNettyThreadFactory(String nameFormat) {
|
||||
this.nameFormat = checkNotNull(nameFormat, "nameFormat");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Thread newThread(Runnable r) {
|
||||
String name = String.format(nameFormat, threadNumber.incrementAndGet());
|
||||
return new FastThreadLocalThread(r, name);
|
||||
}
|
||||
}
|
@ -1 +1,2 @@
|
||||
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
|
||||
log4j.skipJansi=true
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren