From fdf5f27da60312d3944a4c8bd43a73af22b767e7 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Tue, 7 Aug 2018 01:02:39 -0400 Subject: [PATCH] Improve server list ping, especially for legacy MC versions. --- .../api/proxy/ProxyServer.java | 6 ++ .../velocitypowered/proxy/VelocityServer.java | 5 ++ .../proxy/connection/MinecraftConnection.java | 11 +++- .../connection/MinecraftSessionHandler.java | 2 +- .../client/HandshakeSessionHandler.java | 29 ++++++++- .../client/LoginSessionHandler.java | 64 +++++++++++-------- .../client/StatusSessionHandler.java | 6 +- .../proxy/protocol/ProtocolConstants.java | 2 + .../protocol/netty/LegacyPingDecoder.java | 12 +++- .../protocol/netty/LegacyPingEncoder.java | 26 ++------ .../protocol/packet/LegacyDisconnect.java | 32 ++++++++++ .../protocol/packet/LegacyHandshake.java | 17 +++++ .../proxy/protocol/packet/LegacyPing.java | 15 ++++- 13 files changed, 169 insertions(+), 58 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyHandshake.java diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index 7e23a0a07..1973482cf 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -32,6 +32,12 @@ public interface ProxyServer { */ Collection getAllPlayers(); + /** + * Returns the number of players currently connected to this proxy. + * @return the players on this proxy + */ + int getPlayerCount(); + /** * Retrieves a registered {@link ServerInfo} instance by its name. * @param name the name of the server diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 4a5385e50..a19213ad6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -149,6 +149,11 @@ public class VelocityServer implements ProxyServer { return ImmutableList.copyOf(connectionsByUuid.values()); } + @Override + public int getPlayerCount() { + return connectionsByUuid.size(); + } + @Override public Optional getServerInfo(@Nonnull String name) { Preconditions.checkNotNull(name, "name"); 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 a5375bf25..e829c008e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -6,6 +6,7 @@ import com.velocitypowered.natives.encryption.VelocityCipherFactory; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.protocol.PacketWrapper; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.natives.encryption.JavaVelocityCipher; import com.velocitypowered.natives.encryption.VelocityCipher; @@ -167,8 +168,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void setProtocolVersion(int protocolVersion) { this.protocolVersion = protocolVersion; - this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion); - this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion); + if (protocolVersion != ProtocolConstants.LEGACY) { + this.channel.pipeline().get(MinecraftEncoder.class).setProtocolVersion(protocolVersion); + this.channel.pipeline().get(MinecraftDecoder.class).setProtocolVersion(protocolVersion); + } else { + // Legacy handshake handling + this.channel.pipeline().remove(MINECRAFT_ENCODER); + this.channel.pipeline().remove(MINECRAFT_DECODER); + } } public MinecraftSessionHandler getSessionHandler() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index d445fb51c..4f13190de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -4,7 +4,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import io.netty.buffer.ByteBuf; public interface MinecraftSessionHandler { - void handle(MinecraftPacket packet) throws Exception; + void handle(MinecraftPacket packet); default void handleUnknown(ByteBuf buf) { // No-op: we'll release the buffer later. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index 1b9ca2ba7..e16915f6d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -1,14 +1,18 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.data.ServerPing; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.packet.Disconnect; -import com.velocitypowered.proxy.protocol.packet.Handshake; +import com.velocitypowered.proxy.protocol.packet.*; +import net.kyori.text.TextComponent; import net.kyori.text.TranslatableComponent; +import net.kyori.text.format.TextColor; public class HandshakeSessionHandler implements MinecraftSessionHandler { private final MinecraftConnection connection; @@ -19,6 +23,12 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public void handle(MinecraftPacket packet) { + if (packet instanceof LegacyPing || packet instanceof LegacyHandshake) { + connection.setProtocolVersion(ProtocolConstants.LEGACY); + handleLegacy(packet); + return; + } + if (!(packet instanceof Handshake)) { throw new IllegalArgumentException("Did not expect packet " + packet.getClass().getName()); } @@ -43,6 +53,21 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { default: throw new IllegalArgumentException("Invalid state " + handshake.getNextStatus()); } + } + private void handleLegacy(MinecraftPacket packet) { + if (packet instanceof LegacyPing) { + VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration(); + ServerPing ping = new ServerPing( + new ServerPing.Version(ProtocolConstants.MAXIMUM_GENERIC_VERSION, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING), + new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()), + configuration.getMotdComponent(), + null + ); + // The disconnect packet is the same as the server response one. + connection.closeWith(LegacyDisconnect.fromPingResponse(LegacyPingResponse.from(ping))); + } else if (packet instanceof LegacyHandshake) { + connection.closeWith(LegacyDisconnect.from(TextComponent.of("Your client is old, please upgrade!", TextColor.RED))); + } } } 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 d97bdb11f..cc28c91fa 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 @@ -19,6 +19,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.net.InetSocketAddress; +import java.net.MalformedURLException; import java.net.URL; import java.security.GeneralSecurityException; import java.security.KeyPair; @@ -53,7 +54,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } @Override - public void handle(MinecraftPacket packet) throws Exception { + public void handle(MinecraftPacket packet) { if (packet instanceof LoginPluginResponse) { LoginPluginResponse lpr = (LoginPluginResponse) packet; if (lpr.getId() == playerInfoId && lpr.isSuccess()) { @@ -75,34 +76,41 @@ public class LoginSessionHandler implements MinecraftSessionHandler { handleSuccessfulLogin(GameProfile.forOfflinePlayer(login.getUsername())); } } else if (packet instanceof EncryptionResponse) { - KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair(); - EncryptionResponse response = (EncryptionResponse) packet; - byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken()); - if (!Arrays.equals(verify, decryptedVerifyToken)) { - throw new IllegalStateException("Unable to successfully decrypt the verification token."); + try { + KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair(); + EncryptionResponse response = (EncryptionResponse) packet; + byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken()); + if (!Arrays.equals(verify, decryptedVerifyToken)) { + throw new IllegalStateException("Unable to successfully decrypt the verification token."); + } + + byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret()); + String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); + + String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString(); + VelocityServer.getServer().getHttpClient() + .get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp))) + .thenAcceptAsync(profileResponse -> { + try { + inbound.enableEncryption(decryptedSharedSecret); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + + GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class); + handleSuccessfulLogin(profile); + }, inbound.getChannel().eventLoop()) + .exceptionally(exception -> { + logger.error("Unable to enable encryption", exception); + inbound.close(); + return null; + }); + } catch (GeneralSecurityException e) { + logger.error("Unable to enable encryption", e); + inbound.close(); + } catch (MalformedURLException e) { + throw new AssertionError(e); } - - byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret()); - String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); - - String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString(); - VelocityServer.getServer().getHttpClient() - .get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp))) - .thenAcceptAsync(profileResponse -> { - try { - inbound.enableEncryption(decryptedSharedSecret); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } - - GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class); - handleSuccessfulLogin(profile); - }, inbound.getChannel().eventLoop()) - .exceptionally(exception -> { - logger.error("Unable to enable encryption", exception); - inbound.close(); - return null; - }); } } 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 375185973..e75eafebd 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 @@ -35,9 +35,11 @@ public class StatusSessionHandler implements MinecraftSessionHandler { VelocityConfiguration configuration = VelocityServer.getServer().getConfiguration(); // Status request + int shownVersion = ProtocolConstants.isSupported(connection.getProtocolVersion()) ? connection.getProtocolVersion() : + ProtocolConstants.MAXIMUM_GENERIC_VERSION; ServerPing ping = new ServerPing( - new ServerPing.Version(connection.getProtocolVersion(), "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING), - new ServerPing.Players(0, configuration.getShowMaxPlayers()), + new ServerPing.Version(shownVersion, "Velocity " + ProtocolConstants.SUPPORTED_GENERIC_VERSION_STRING), + new ServerPing.Players(VelocityServer.getServer().getPlayerCount(), configuration.getShowMaxPlayers()), configuration.getMotdComponent(), null ); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolConstants.java index 96c98f3d6..4b61a1256 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolConstants.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolConstants.java @@ -3,6 +3,8 @@ package com.velocitypowered.proxy.protocol; import java.util.Arrays; public enum ProtocolConstants { ; + public static final int LEGACY = -1; + public static final int MINECRAFT_1_8 = 47; public static final int MINECRAFT_1_9 = 107; public static final int MINECRAFT_1_9_1 = 108; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingDecoder.java index 46e41a519..153421386 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingDecoder.java @@ -1,7 +1,10 @@ package com.velocitypowered.proxy.protocol.netty; +import com.velocitypowered.proxy.protocol.PacketWrapper; +import com.velocitypowered.proxy.protocol.packet.LegacyHandshake; import com.velocitypowered.proxy.protocol.packet.LegacyPing; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.ByteToMessageDecoder; @@ -18,9 +21,12 @@ public class LegacyPingDecoder extends ByteToMessageDecoder { short second = in.getUnsignedByte(in.readerIndex() + 1); if (first == 0xfe && second == 0x01) { in.skipBytes(in.readableBytes()); - out.add(new LegacyPing()); + out.add(new PacketWrapper(new LegacyPing(), Unpooled.EMPTY_BUFFER)); + } else if (first == 0x02) { + in.skipBytes(in.readableBytes()); + out.add(new PacketWrapper(new LegacyHandshake(), Unpooled.EMPTY_BUFFER)); + } else { + ctx.pipeline().remove(this); } - - ctx.pipeline().remove(this); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java index 6ef41fe51..3b31305e1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/LegacyPingEncoder.java @@ -1,39 +1,27 @@ package com.velocitypowered.proxy.protocol.netty; -import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; -import com.velocitypowered.proxy.protocol.packet.LegacyPingResponse; +import com.velocitypowered.proxy.protocol.packet.LegacyDisconnect; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; import java.nio.charset.StandardCharsets; -import java.util.List; @ChannelHandler.Sharable -public class LegacyPingEncoder extends MessageToByteEncoder { +public class LegacyPingEncoder extends MessageToByteEncoder { public static final LegacyPingEncoder INSTANCE = new LegacyPingEncoder(); private LegacyPingEncoder() {} @Override - protected void encode(ChannelHandlerContext ctx, LegacyPingResponse msg, ByteBuf out) throws Exception { + protected void encode(ChannelHandlerContext ctx, LegacyDisconnect msg, ByteBuf out) throws Exception { out.writeByte(0xff); - String serializedResponse = serialize(msg); - out.writeShort(serializedResponse.length()); - out.writeBytes(serializedResponse.getBytes(StandardCharsets.UTF_16BE)); + writeLegacyString(out, msg.getReason()); } - private String serialize(LegacyPingResponse response) { - List parts = ImmutableList.of( - "§1", - Integer.toString(response.getProtocolVersion()), - response.getServerVersion(), - response.getMotd(), - Integer.toString(response.getPlayersOnline()), - Integer.toString(response.getPlayersMax()) - ); - return Joiner.on('\0').join(parts); + private static void writeLegacyString(ByteBuf out, String string) { + out.writeShort(string.length()); + out.writeBytes(string.getBytes(StandardCharsets.UTF_16BE)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java new file mode 100644 index 000000000..7a4ef0f0f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyDisconnect.java @@ -0,0 +1,32 @@ +package com.velocitypowered.proxy.protocol.packet; + +import net.kyori.text.TextComponent; +import net.kyori.text.serializer.ComponentSerializers; + +public class LegacyDisconnect { + private final String reason; + + public LegacyDisconnect(String reason) { + this.reason = reason; + } + + public static LegacyDisconnect fromPingResponse(LegacyPingResponse response) { + String kickMessage = String.join("\0", + "§1", + Integer.toString(response.getProtocolVersion()), + response.getServerVersion(), + response.getMotd(), + Integer.toString(response.getPlayersOnline()), + Integer.toString(response.getPlayersMax()) + ); + return new LegacyDisconnect(kickMessage); + } + + public static LegacyDisconnect from(TextComponent component) { + return new LegacyDisconnect(ComponentSerializers.LEGACY.serialize(component)); + } + + public String getReason() { + return reason; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyHandshake.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyHandshake.java new file mode 100644 index 000000000..f6588674f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyHandshake.java @@ -0,0 +1,17 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import io.netty.buffer.ByteBuf; + +public class LegacyHandshake implements MinecraftPacket { + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + throw new UnsupportedOperationException(); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPing.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPing.java index ea75cd214..847160869 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPing.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LegacyPing.java @@ -1,4 +1,17 @@ package com.velocitypowered.proxy.protocol.packet; -public class LegacyPing { +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import io.netty.buffer.ByteBuf; + +public class LegacyPing implements MinecraftPacket { + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + throw new UnsupportedOperationException(); + } }