diff --git a/.travis.yml b/.travis.yml index 62f10982..3d0c3634 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 c518f653..4b35a9a0 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.3. 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 2a43ac04..a9e46686 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -24,7 +24,7 @@ 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}" 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 00000000..f033d954 --- /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 941728e2..93b1a372 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -32,7 +32,8 @@ public enum ProtocolVersion { 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_3(490, "1.14.3"); + 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/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java index f571c451..db3ef1ed 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 3341ce3a..8d9b4550 100644 --- a/build.gradle +++ b/build.gradle @@ -20,11 +20,11 @@ allprojects { 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.37.Final' + nettyVersion = '4.1.38.Final' guavaVersion = '25.1-jre' checkerFrameworkVersion = '2.7.0' configurateVersion = '3.6' diff --git a/proxy/build.gradle b/proxy/build.gradle index 54ddc7b8..9c5bf26f 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -48,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}" @@ -57,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 6f2dfa53..40274617 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 095fd5e8..2f260cce 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; @@ -70,12 +69,15 @@ 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 { @@ -89,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(); @@ -201,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 @@ -264,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); } @@ -433,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() { @@ -454,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())); @@ -466,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; } @@ -498,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/config/PingPassthroughMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java new file mode 100644 index 00000000..89ed3ad8 --- /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 6fd13239..6d8c24f4 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 f4c56ae7..c1f15cc1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -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 fcfa53fb..ba6678b7 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 @@ -36,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)) { @@ -190,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 b2535931..d1895136 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 @@ -117,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 b8d7ed75..1c6a0b05 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.retain()); + 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 bb414290..42d06fd8 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; @@ -77,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 { @@ -139,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() { 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 03530ef9..e0bf9dc3 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 @@ -7,6 +7,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; @@ -59,7 +60,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. @@ -77,7 +78,7 @@ 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); } } @@ -155,61 +156,11 @@ 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; - } - - // 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; - } - - 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; - } - return false; - } - - List suggestions = server.getCommandManager().offerSuggestions(player, command); - if (suggestions.isEmpty()) { - return false; - } - - List offers = new ArrayList<>(); - int longestLength = 0; - for (String suggestion : suggestions) { - offers.add(new Offer(suggestion)); - if (suggestion.length() > longestLength) { - longestLength = suggestion.length(); - } - } - - TabCompleteResponse resp = new TabCompleteResponse(); - resp.setTransactionId(packet.getTransactionId()); - - int startPos = packet.getCommand().lastIndexOf(' ') + 1; - int length; - if (startPos == 0) { - startPos = packet.getCommand().length() + 1; - length = longestLength; + if (isCommand) { + return this.handleCommandTabComplete(packet); } else { - length = packet.getCommand().length() - startPos; + return this.handleRegularTabComplete(packet); } - - resp.setStart(startPos); - resp.setLength(length); - resp.getOffers().addAll(offers); - - player.getMinecraftConnection().write(resp); - return true; } @Override @@ -325,7 +276,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { public void writabilityChanged() { VelocityServerConnection serverConn = player.getConnectedServer(); if (serverConn != null) { - boolean writable = player.getMinecraftConnection().getChannel().isWritable(); + boolean writable = player.getConnection().getChannel().isWritable(); MinecraftConnection smc = serverConn.getConnection(); if (smc != null) { smc.setAutoReading(writable); @@ -345,7 +296,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { if (!spawned) { // Nothing special to do with regards to spawning the player spawned = true; - player.getMinecraftConnection().delayedWrite(joinGame); + player.getConnection().delayedWrite(joinGame); // Required for Legacy Forge player.getPhase().onFirstJoin(player); @@ -365,12 +316,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // 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); + player.getConnection().delayedWrite(joinGame); int tempDim = joinGame.getDimension() == 0 ? -1 : 0; - player.getMinecraftConnection().delayedWrite( + player.getConnection().delayedWrite( new Respawn(tempDim, joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType())); - player.getMinecraftConnection().delayedWrite( + player.getConnection().delayedWrite( new Respawn(joinGame.getDimension(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType())); } @@ -381,7 +332,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { BossBar deletePacket = new BossBar(); deletePacket.setUuid(serverBossBar); deletePacket.setAction(BossBar.REMOVE); - player.getMinecraftConnection().delayedWrite(deletePacket); + player.getConnection().delayedWrite(deletePacket); } serverBossBars.clear(); @@ -399,12 +350,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Clear any title from the previous server. if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { - player.getMinecraftConnection() + player.getConnection() .delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion())); } // Flush everything - player.getMinecraftConnection().flush(); + player.getConnection().flush(); serverMc.flush(); destination.completeJoin(); } @@ -413,29 +364,116 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { 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) { + 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 + // additional tab completion support. + outstandingTabComplete = packet; + } + return false; + } + + List suggestions = server.getCommandManager().offerSuggestions(player, command); + if (suggestions.isEmpty()) { + return false; + } + + List offers = new ArrayList<>(); + int longestLength = 0; + for (String suggestion : suggestions) { + offers.add(new Offer(suggestion)); + if (suggestion.length() > longestLength) { + longestLength = suggestion.length(); + } + } + + TabCompleteResponse resp = new TabCompleteResponse(); + resp.setTransactionId(packet.getTransactionId()); + + int startPos = packet.getCommand().lastIndexOf(' ') + 1; + int length; + if (startPos == 0) { + startPos = packet.getCommand().length() + 1; + length = longestLength; + } else { + length = packet.getCommand().length() - startPos; + } + + resp.setStart(startPos); + resp.setLength(length); + resp.getOffers().addAll(offers); + + player.getConnection().write(resp); + return true; + } + + 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 false; + } + /** - * 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 9ad15bf6..d1a60902 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 @@ -59,7 +59,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ThreadLocalRandom; import net.kyori.text.Component; import net.kyori.text.TextComponent; @@ -85,7 +84,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; @@ -104,18 +103,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private @MonotonicNonNull List serversToTry = null; ConnectedPlayer(VelocityServer server, GameProfile profile, - MinecraftConnection minecraftConnection, @Nullable InetSocketAddress virtualHost) { + MinecraftConnection connection, @Nullable InetSocketAddress virtualHost) { this.server = server; - if (minecraftConnection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { - this.tabList = new VelocityTabList(minecraftConnection); + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.tabList = new VelocityTabList(connection); } else { - this.tabList = new VelocityTabListLegacy(minecraftConnection); + this.tabList = new VelocityTabListLegacy(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); } @@ -139,8 +138,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return profile; } - public MinecraftConnection getMinecraftConnection() { - return minecraftConnection; + public MinecraftConnection getConnection() { + return connection; } @Override @@ -175,7 +174,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { @Override public InetSocketAddress getRemoteAddress() { - return (InetSocketAddress) minecraftConnection.getRemoteAddress(); + return (InetSocketAddress) connection.getRemoteAddress(); } @Override @@ -189,12 +188,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 @@ -210,7 +209,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 @@ -226,7 +225,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { Chat chat = new Chat(); chat.setType(pos); chat.setMessage(json); - minecraftConnection.write(chat); + connection.write(chat); } @Override @@ -263,23 +262,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(); @@ -287,7 +286,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(); @@ -295,7 +294,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()) { @@ -303,9 +302,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()); } @@ -457,9 +456,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()) { @@ -471,7 +470,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()); } /** @@ -584,7 +583,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { Preconditions.checkNotNull(identifier, "identifier"); Preconditions.checkNotNull(data, "data"); PluginMessage message = new PluginMessage(identifier.getId(), Unpooled.wrappedBuffer(data)); - minecraftConnection.write(message); + connection.write(message); return true; } @@ -603,7 +602,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { ResourcePackRequest request = new ResourcePackRequest(); request.setUrl(url); request.setHash(""); - minecraftConnection.write(request); + connection.write(request); } @Override @@ -615,7 +614,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); } /** @@ -624,10 +623,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); } } @@ -751,8 +750,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); } @@ -785,7 +786,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/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index 7f85107d..44423286 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 @@ -8,6 +8,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; @@ -43,10 +45,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 { @@ -124,46 +130,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; } @@ -180,6 +190,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // The player was disconnected return; } + PreLoginComponentResult result = event.getResult(); Optional disconnectReason = result.getReason(); if (disconnectReason.isPresent()) { @@ -278,7 +289,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 0916d4ae..61327173 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 08884b0e..c0da5ba9 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.retain()); + 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 f75dc04b..b394cb13 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); } 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 97b0341c..765af226 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 e382a485..b5829f37 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java @@ -7,11 +7,6 @@ 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; @@ -64,10 +56,12 @@ enum TransportType { } 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 ec2436b7..00000000 --- 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 3bcfdc40..00000000 --- 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 533e8170..00000000 --- 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 00000000..f76a160c --- /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/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java index 56af37e6..a57485f0 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 greater 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/TabCompleteResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java index c6db498d..3975b323 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/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 63e7630c..93094cc3 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 c0109467..cf6694cb 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/util/AddressUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/util/AddressUtil.java index 8874509a..033babcb 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 1a4d2933..4acf5889 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityMessages.java @@ -21,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 98dc81d5..6b4d9b01 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/resources/log4j2.component.properties b/proxy/src/main/resources/log4j2.component.properties index 6ed08f31..e43f5dac 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