diff --git a/.travis.yml b/.travis.yml index 62f109821..3d0c36340 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,4 +7,4 @@ cache: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ jdk: - - oraclejdk8 \ No newline at end of file + - openjdk8 \ No newline at end of file diff --git a/README.md b/README.md index a1358c3fd..4b35a9a02 100644 --- a/README.md +++ b/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. \ No newline at end of file diff --git a/api/build.gradle b/api/build.gradle index 7322e4718..a9e466865 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -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}" } diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index 69d1eb791..81a7b8297 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -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. * diff --git a/api/src/main/java/com/velocitypowered/api/command/RawCommand.java b/api/src/main/java/com/velocitypowered/api/command/RawCommand.java new file mode 100644 index 000000000..c1d1c7499 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/command/RawCommand.java @@ -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 suggest(CommandSource source, String currentLine) { + return ImmutableList.of(); + } + + @Override + default List 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}. + * + *

If this method returns false, the handling will be forwarded onto + * the players current server.

+ * + * @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; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java new file mode 100644 index 000000000..f033d954e --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/player/TabCompleteEvent.java @@ -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 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 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 getSuggestions() { + return suggestions; + } + + @Override + public String toString() { + return "TabCompleteEvent{" + + "player=" + player + + ", partialMessage='" + partialMessage + '\'' + + ", suggestions=" + suggestions + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 268e3a99c..d1764a5c6 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -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; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java index 364b188b3..71bb47ba7 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -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 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. * diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java index f571c4517..db3ef1ed5 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java @@ -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; diff --git a/build.gradle b/build.gradle index e50be5c2f..8d9b45507 100644 --- a/build.gradle +++ b/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 { diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java index b6a27a7c8..b65501ff0 100644 --- a/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java +++ b/native/src/main/java/com/velocitypowered/natives/encryption/NativeVelocityCipher.java @@ -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 diff --git a/proxy/build.gradle b/proxy/build.gradle index 64b858350..9c5bf26fb 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -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}" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java index 6f2dfa535..402746171 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java @@ -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 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(); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index a8ee585b5..2f260cce6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -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 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 playersTeardownFuture = CompletableFuture.allOf(players.stream() + .map(ConnectedPlayer::getTeardownFuture) + .toArray((IntFunction[]>) 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); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index dcd47339c..0195645ea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -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 commands = new HashMap<>(); + private final Map 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 availableCommands = ImmutableList.builder(); - for (Map.Entry entry : commands.entrySet()) { - if (entry.getKey().regionMatches(true, 0, alias, 0, alias.length()) + for (Map.Entry 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 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); + } + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java new file mode 100644 index 000000000..89ed3ad8e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java @@ -0,0 +1,7 @@ +package com.velocitypowered.proxy.config; + +public enum PingPassthroughMode { + DISABLED, + MODS, + ALL +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 6fd13239e..6d8c24f4d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -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 + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index beefa6b05..c1f15cc19 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 5128efe9e..b43fc0a2a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -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); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 14b7ad22c..d18951369 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -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(); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 6f32e7ad1..1c6a0b05b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index a8b97c028..42d06fd89 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -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 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() { @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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 9593108a8..2ae0d08a1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -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 serverBossBars = new ArrayList<>(); private final Queue 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 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 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 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 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 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()); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 7589300fa..83c6c86a6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -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 knownChannels; + private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List 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 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 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 nextServer = getNextServerToTry(rs); - if (nextServer.isPresent()) { - // There can't be any connection in flight now. - resetInFlightConnection(); - createConnectionRequest(nextServer.get()).fireAndForget(); - } else { - disconnect(friendlyReason); - } - }); + Optional 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 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); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java index 59c2e9255..1d1485ef8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java @@ -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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index d0707588c..6e38f0518 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -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 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 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()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index 0916d4aea..613271733 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -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 attemptPingPassthrough(PingPassthroughMode mode, + List servers, ProtocolVersion pingingVersion) { + ServerPing fallback = constructLocalPing(pingingVersion); + List> pings = new ArrayList<>(); + for (String s : servers) { + Optional 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> 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 = 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 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 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)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeBackendPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeBackendPhase.java index ad3610231..bf44814aa 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeBackendPhase.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeBackendPhase.java @@ -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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java index fd4577181..b394cb137 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java @@ -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; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java index ca01f86d7..8d2c9d410 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java @@ -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; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 97b0341c6..765af2261 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -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 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 FilterContext filter(FilterContext 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; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java index beb42d92f..b5829f375 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -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 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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java b/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java deleted file mode 100644 index ec2436b72..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java +++ /dev/null @@ -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() { - @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 get(URL url, EventLoop loop) { - CompletableFuture 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 post(URL url, ByteBuf body, - Consumer 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 post(URL url, EventLoop loop, ByteBuf body, - Consumer decorator) { - CompletableFuture 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; - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/http/SimpleHttpResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/network/http/SimpleHttpResponse.java deleted file mode 100644 index 3bcfdc40a..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/http/SimpleHttpResponse.java +++ /dev/null @@ -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 + '\'' - + '}'; - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/http/SimpleHttpResponseCollector.java b/proxy/src/main/java/com/velocitypowered/proxy/network/http/SimpleHttpResponseCollector.java deleted file mode 100644 index 533e8170f..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/http/SimpleHttpResponseCollector.java +++ /dev/null @@ -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 reply; - private int httpCode; - - SimpleHttpResponseCollector(CompletableFuture 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); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/netty/DnsAddressResolverGroupNameResolverAdapter.java b/proxy/src/main/java/com/velocitypowered/proxy/network/netty/DnsAddressResolverGroupNameResolverAdapter.java new file mode 100644 index 000000000..f76a160c9 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/netty/DnsAddressResolverGroupNameResolverAdapter.java @@ -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 promise) throws Exception { + EventExecutor executor = this.findExecutor(); + resolverGroup.getResolver(executor).resolve(InetSocketAddress.createUnresolved(inetHost, 17)) + .addListener((FutureListener) future -> { + if (future.isSuccess()) { + promise.trySuccess(future.getNow().getAddress()); + } else { + promise.tryFailure(future.cause()); + } + }); + } + + @Override + protected void doResolveAll(String inetHost, Promise> promise) + throws Exception { + EventExecutor executor = this.findExecutor(); + resolverGroup.getResolver(executor).resolveAll(InetSocketAddress.createUnresolved(inetHost, 17)) + .addListener((FutureListener>) future -> { + if (future.isSuccess()) { + List 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(); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java index 0f5a61cf1..897e37130 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java @@ -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) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java index 358d8154e..9a040dec4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java @@ -35,10 +35,9 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder { 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); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessage.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessage.java index 26fb8cb70..a3dae879d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessage.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginMessage.java @@ -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 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginResponse.java index 159482cd8..5321c0eea 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginPluginResponse.java @@ -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 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java index 7c5045568..e0269f77a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PluginMessage.java @@ -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=" + + ", 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); + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java index c6db498d2..3975b3235 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java @@ -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; + } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/DeferredByteBufHolder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/DeferredByteBufHolder.java new file mode 100644 index 000000000..6a35a4e5e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/DeferredByteBufHolder.java @@ -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 + "]"; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 9fef82a1f..c659826b7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -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(""); } } - } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 63e7630c8..93094cc36 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -18,13 +18,15 @@ public class PingSessionHandler implements MinecraftSessionHandler { private final CompletableFuture result; private final RegisteredServer server; private final MinecraftConnection connection; + private final ProtocolVersion version; private boolean completed = false; PingSessionHandler(CompletableFuture 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 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index c0109467e..cf6694cb4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -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 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 ping(@Nullable EventLoop loop, ProtocolVersion version) { if (server == null) { throw new IllegalStateException("No Velocity proxy instance available"); } CompletableFuture pingFuture = new CompletableFuture<>(); - server.initializeGenericBootstrap() + server.createBootstrap(loop) .handler(new ChannelInitializer() { @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()); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index 5a0d5db2a..aacf26012 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -21,7 +21,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class VelocityTabList implements TabList { private final MinecraftConnection connection; - private final Map entries = new ConcurrentHashMap<>(); + private final Map 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 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; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java index 1f2d774ec..647ce3d7a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListEntry.java @@ -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; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/AddressUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/util/AddressUtil.java index 8874509aa..033babcb9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/AddressUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/AddressUtil.java @@ -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()); + } } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityMessages.java index f88defb22..4acf5889f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityMessages.java @@ -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(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java b/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java index 98dc81d50..6b4d9b01a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/bossbar/VelocityBossBar.java @@ -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); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java new file mode 100644 index 000000000..87fb83524 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrent/VelocityNettyThreadFactory.java @@ -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); + } +} diff --git a/proxy/src/main/resources/log4j2.component.properties b/proxy/src/main/resources/log4j2.component.properties index 6ed08f31f..e43f5dacc 100644 --- a/proxy/src/main/resources/log4j2.component.properties +++ b/proxy/src/main/resources/log4j2.component.properties @@ -1 +1,2 @@ log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector +log4j.skipJansi=true