diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java new file mode 100644 index 000000000..81cb4e504 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java @@ -0,0 +1,47 @@ +package com.velocitypowered.proxy.connection; + +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase; +import com.velocitypowered.proxy.connection.client.ClientConnectionPhase; + +/** + * The types of connection that may be selected. + */ +public interface ConnectionType { + + /** + * The initial {@link ClientConnectionPhase} for this connection type. + * + * @return The {@link ClientConnectionPhase} + */ + ClientConnectionPhase getInitialClientPhase(); + + /** + * The initial {@link BackendConnectionPhase} for this connection type. + * + * @return The {@link BackendConnectionPhase} + */ + BackendConnectionPhase getInitialBackendPhase(); + + /** + * Adds properties to the {@link GameProfile} if required. If any properties + * are added, the returned {@link GameProfile} will be a copy. + * + * @param original The original {@link GameProfile} + * @param forwardingType The Velocity {@link PlayerInfoForwarding} + * @return The {@link GameProfile} with the properties added in. + */ + GameProfile addGameProfileTokensIfRequired(GameProfile original, + PlayerInfoForwarding forwardingType); + + /** + * Tests whether the hostname is the handshake packet is valid. + * + * @param address The address to check + * @return true if valid. + */ + default boolean checkServerAddressIsValid(String address) { + return !address.contains("\0"); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionTypes.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionTypes.java new file mode 100644 index 000000000..baf97c90f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionTypes.java @@ -0,0 +1,36 @@ +package com.velocitypowered.proxy.connection; + +import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases; +import com.velocitypowered.proxy.connection.client.ClientConnectionPhases; +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConnectionType; +import com.velocitypowered.proxy.connection.util.ConnectionTypeImpl; + +/** + * The connection types supported by Velocity. + */ +public final class ConnectionTypes { + + /** + * Indicates that the connection has yet to reach the + * point where we have a definitive answer as to what + * type of connection we have. + */ + public static final ConnectionType UNDETERMINED = + new ConnectionTypeImpl(ClientConnectionPhases.VANILLA, BackendConnectionPhases.UNKNOWN); + + /** + * Indicates that the connection is a Vanilla connection. + */ + public static final ConnectionType VANILLA = + new ConnectionTypeImpl(ClientConnectionPhases.VANILLA, BackendConnectionPhases.VANILLA); + + /** + * Indicates that the connection is a 1.8-1.12 Forge + * connection. + */ + public static final ConnectionType LEGACY_FORGE = new LegacyForgeConnectionType(); + + private ConnectionTypes() { + throw new AssertionError(); + } +} 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 ec0e99597..da437dbdb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -56,9 +56,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private ProtocolVersion protocolVersion; private ProtocolVersion nextProtocolVersion; private @Nullable MinecraftConnectionAssociation association; - private boolean isLegacyForge; private final VelocityServer server; - private boolean canSendLegacyFmlResetPacket = false; + private ConnectionType connectionType = ConnectionTypes.UNDETERMINED; public MinecraftConnection(Channel channel, VelocityServer server) { this.channel = channel; @@ -279,22 +278,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.association = association; } - public boolean isLegacyForge() { - return isLegacyForge; - } - - public void setLegacyForge(boolean isForge) { - this.isLegacyForge = isForge; - } - - public boolean canSendLegacyFmlResetPacket() { - return canSendLegacyFmlResetPacket; - } - - public void setCanSendLegacyFmlResetPacket(boolean canSendLegacyFMLResetPacket) { - this.canSendLegacyFmlResetPacket = isLegacyForge && canSendLegacyFMLResetPacket; - } - public ProtocolVersion getNextProtocolVersion() { return this.nextProtocolVersion; } @@ -302,4 +285,20 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void setNextProtocolVersion(ProtocolVersion nextProtocolVersion) { this.nextProtocolVersion = nextProtocolVersion; } + + /** + * Gets the detected {@link ConnectionType} + * @return The {@link ConnectionType} + */ + public ConnectionType getType() { + return connectionType; + } + + /** + * Sets the detected {@link ConnectionType} + * @param connectionType The {@link ConnectionType} + */ + public void setType(ConnectionType connectionType) { + this.connectionType = connectionType; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhase.java new file mode 100644 index 000000000..30c453124 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhase.java @@ -0,0 +1,46 @@ +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHandshakeBackendPhase; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; + +/** + * Provides connection phase specific actions. + * + *

Note that Forge phases are found in the enum + * {@link LegacyForgeHandshakeBackendPhase}.

+ */ +public interface BackendConnectionPhase { + + /** + * Handle a plugin message in the context of + * this phase. + * + * @param message The message to handle + * @return true if handled, false otherwise. + */ + default boolean handle(VelocityServerConnection server, + ConnectedPlayer player, + PluginMessage message) { + return false; + } + + /** + * Indicates whether the connection is considered complete + * @return true if so + */ + default boolean consideredComplete() { + return true; + } + + /** + * Fired when the provided server connection is about to be terminated + * because the provided player is connecting to a new server. + * + * @param serverConnection The server the player is disconnecting from + * @param player The player + */ + default void onDepartForNewServer(VelocityServerConnection serverConnection, + ConnectedPlayer player) { + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhases.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhases.java new file mode 100644 index 000000000..8780f8ef3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhases.java @@ -0,0 +1,42 @@ +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHandshakeBackendPhase; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; + +/** + * Contains Vanilla {@link BackendConnectionPhase}s. + * + *

See {@link LegacyForgeHandshakeBackendPhase} for Legacy Forge + * versions

+ */ +public final class BackendConnectionPhases { + + /** + * The backend connection is vanilla. + */ + public static final BackendConnectionPhase VANILLA = new BackendConnectionPhase() {}; + + /** + * The backend connection is unknown at this time. + */ + public static final BackendConnectionPhase UNKNOWN = new BackendConnectionPhase() { + @Override + public boolean consideredComplete() { + return false; + } + + @Override + public boolean handle(VelocityServerConnection serverConn, + ConnectedPlayer player, + PluginMessage message) { + // The connection may be legacy forge. If so, the Forge handler will deal with this + // for us. Otherwise, we have nothing to do. + return LegacyForgeHandshakeBackendPhase.NOT_STARTED.handle(serverConn, player, message); + } + }; + + private BackendConnectionPhases() { + throw new AssertionError(); + } +} 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 14b1431df..d83f7989d 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 @@ -7,7 +7,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; -import com.velocitypowered.proxy.connection.forge.ForgeConstants; +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.packet.BossBar; @@ -94,18 +94,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } - if (!serverConn.hasCompletedJoin() && packet.getChannel() - .equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { - if (!serverConn.isLegacyForge()) { - serverConn.setLegacyForge(true); - - // We must always reset the handshake before a modded connection is established if - // we haven't done so already. - serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket(); - } - - // Always forward these messages during login. - return false; + if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) { + // Handled. + return true; } ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); @@ -173,7 +164,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { if (mc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_12_2) <= 0) { String channel = message.getChannel(); minecraftOrFmlMessage = channel.startsWith("MC|") || channel - .startsWith(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL); + .startsWith(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL); } else { minecraftOrFmlMessage = message.getChannel().startsWith("minecraft:"); } 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 914578ee0..977d1a95c 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,14 +117,15 @@ public class LoginSessionHandler implements MinecraftSessionHandler { serverConn.getPlayer().getConnection() .setSessionHandler(new ClientPlaySessionHandler(server, serverConn.getPlayer())); } else { - // If the server we are departing is modded, we must always reset the client's handshake. - if (existingConnection.isLegacyForge()) { - serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket(); - } + // For Legacy Forge + existingConnection.getPhase().onDepartForNewServer(serverConn, serverConn.getPlayer()); // Shut down the existing server connection. serverConn.getPlayer().setConnectedServer(null); existingConnection.disconnect(); + + // Send keep alive to try to avoid timeouts + serverConn.getPlayer().sendKeepAlive(); } smc.getChannel().config().setAutoRead(false); 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 21bd12dd0..3720b221f 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 @@ -17,10 +17,12 @@ import com.velocitypowered.api.proxy.server.ServerInfo; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; @@ -44,9 +46,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private final ConnectedPlayer proxyPlayer; private final VelocityServer server; private @Nullable MinecraftConnection connection; - private boolean legacyForge = false; private boolean hasCompletedJoin = false; private boolean gracefulDisconnect = false; + private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private long lastPingId; private long lastPingSent; @@ -93,6 +95,10 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // Kick off the connection process connection.setSessionHandler( new LoginSessionHandler(server, VelocityServerConnection.this, result)); + + // Set the connection phase, which may, for future forge (or whatever), be determined + // at this point already + connectionPhase = connection.getType().getInitialBackendPhase(); startHandshake(); } else { // We need to remember to reset the in-flight connection to allow connect() to work @@ -133,8 +139,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, handshake.setProtocolVersion(proxyPlayer.getConnection().getNextProtocolVersion()); if (forwardingMode == PlayerInfoForwarding.LEGACY) { handshake.setServerAddress(createLegacyForwardingAddress()); - } else if (proxyPlayer.getConnection().isLegacyForge()) { - handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0"); + } else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) { + handshake.setServerAddress(handshake.getServerAddress() + LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN); } else { handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString()); } @@ -198,20 +204,17 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, return true; } - public boolean isLegacyForge() { - return legacyForge; - } - - void setLegacyForge(boolean modded) { - legacyForge = modded; - } - - public boolean hasCompletedJoin() { - return hasCompletedJoin; - } - - public void setHasCompletedJoin(boolean hasCompletedJoin) { - this.hasCompletedJoin = hasCompletedJoin; + public void completeJoin() { + if (!hasCompletedJoin) { + hasCompletedJoin = true; + if (connectionPhase == BackendConnectionPhases.UNKNOWN) { + // Now we know + connectionPhase = BackendConnectionPhases.VANILLA; + if (connection != null) { + connection.setType(ConnectionTypes.VANILLA); + } + } + } } boolean isGracefulDisconnect() { @@ -245,4 +248,35 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, return connection != null && !connection.isClosed() && !gracefulDisconnect && proxyPlayer.isActive(); } + + /** + * Gets the current "phase" of the connection, mostly used for tracking + * modded negotiation for legacy forge servers and provides methods + * for performing phase specific actions. + * + * @return The {@link BackendConnectionPhase} + */ + public BackendConnectionPhase getPhase() { + return connectionPhase; + } + + /** + * Sets the current "phase" of the connection. See {@link #getPhase()} + * + * @param connectionPhase The {@link BackendConnectionPhase} + */ + public void setConnectionPhase(BackendConnectionPhase connectionPhase) { + this.connectionPhase = connectionPhase; + } + + /** + * Gets whether the {@link com.velocitypowered.proxy.protocol.packet.JoinGame} + * packet has been sent by this server. + * + * @return Whether the join has been completed. + */ + public boolean hasCompletedJoin() { + return hasCompletedJoin; + } + } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhase.java new file mode 100644 index 000000000..b5177e73e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhase.java @@ -0,0 +1,55 @@ +package com.velocitypowered.proxy.connection.client; + +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHandshakeClientPhase; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; + +/** + * Provides connection phase specific actions. + * + *

Note that Forge phases are found in the enum + * {@link LegacyForgeHandshakeClientPhase}.

+ */ +public interface ClientConnectionPhase { + + /** + * Handle a plugin message in the context of + * this phase. + * + * @param player The player + * @param handler The {@link ClientPlaySessionHandler} that is handling + * packets + * @param message The message to handle + * @return true if handled, false otherwise. + */ + default boolean handle(ConnectedPlayer player, + ClientPlaySessionHandler handler, + PluginMessage message) { + return false; + } + + /** + * Instruct Velocity to reset the connection phase + * back to its default for the connection type. + * + * @param player The player + */ + default void resetConnectionPhase(ConnectedPlayer player) { + } + + /** + * Perform actions just as the player joins the + * server. + * + * @param player The player + */ + default void onFirstJoin(ConnectedPlayer player) { + } + + /** + * Indicates whether the connection is considered complete. + * @return true if so + */ + default boolean consideredComplete() { + return true; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhases.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhases.java new file mode 100644 index 000000000..874e0f4b5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhases.java @@ -0,0 +1,19 @@ +package com.velocitypowered.proxy.connection.client; + +/** + * The vanilla {@link ClientConnectionPhase}s. + * + *

See {@link com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHandshakeClientPhase} + * for Legacy Forge phases

+ */ +public final class ClientConnectionPhases { + + /** + * The client is connecting with a vanilla client (as far as we can tell). + */ + public static final ClientConnectionPhase VANILLA = new ClientConnectionPhase() {}; + + private ClientConnectionPhases() { + throw new AssertionError(); + } +} 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 7e3e15772..ad3aa128b 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 @@ -3,15 +3,13 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; -import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.connection.forge.ForgeConstants; -import com.velocitypowered.proxy.connection.forge.ForgeUtil; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; @@ -160,7 +158,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { VelocityServerConnection serverConn = player.getConnectedServer(); MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null; if (serverConn != null && backendConn != null) { - if (PluginMessageUtil.isRegister(packet)) { + if (backendConn.getState() != StateRegistry.PLAY) { + logger.warn("Plugin message was sent while the backend was in PLAY. Channel: {}. Packet discarded."); + } else if (PluginMessageUtil.isRegister(packet)) { List actuallyRegistered = new ArrayList<>(); List channels = PluginMessageUtil.getChannels(packet); for (String channel : channels) { @@ -184,33 +184,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } else if (PluginMessageUtil.isMcBrand(packet)) { PluginMessage rewritten = PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion()); backendConn.write(rewritten); - } else if (backendConn.isLegacyForge() && !serverConn.hasCompletedJoin()) { - if (packet.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { - if (!player.getModInfo().isPresent()) { - List mods = ForgeUtil.readModList(packet); - if (!mods.isEmpty()) { - player.setModInfo(new ModInfo("FML", mods)); - } - } + } else if (!player.getPhase().handle(player, this, packet)) { - // Always forward the FML handshake to the remote server. - backendConn.write(packet); + if (!player.getPhase().consideredComplete() + || !serverConn.getPhase().consideredComplete()) { + + // The client is trying to send messages too early. This is primarily caused by mods, but + // it's further aggravated by Velocity. To work around these issues, we will queue any + // non-FML handshake messages to be sent once the FML handshake has completed or the JoinGame + // packet has been received by the proxy, whichever comes first. + loginPluginMessages.add(packet); } else { - // The client is trying to send messages too early. This is primarily caused by mods, but - // it's further aggravated by Velocity. To work around these issues, we will queue any - // non-FML handshake messages to be sent once the JoinGame packet has been received by the - // proxy. - loginPluginMessages.add(packet); - } - } else { - ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); - if (id == null) { - backendConn.write(packet); - } else { - PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, - packet.getData()); - server.getEventManager().fire(event).thenAcceptAsync(pme -> backendConn.write(packet), - backendConn.eventLoop()); + ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); + if (id == null) { + backendConn.write(packet); + } else { + PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, + packet.getData()); + server.getEventManager().fire(event).thenAcceptAsync(pme -> backendConn.write(packet), + backendConn.eventLoop()); + } } } } @@ -227,7 +220,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null && serverConnection.hasCompletedJoin()) { + if (smc != null && serverConnection.getPhase().consideredComplete()) { smc.write(packet); } } @@ -241,7 +234,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } MinecraftConnection smc = serverConnection.getConnection(); - if (smc != null && serverConnection.hasCompletedJoin()) { + if (smc != null && serverConnection.getPhase().consideredComplete()) { smc.write(buf.retain()); } } @@ -289,15 +282,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { spawned = true; player.getConnection().delayedWrite(joinGame); - // We have something special to do for legacy Forge servers - during first connection the FML - // handshake will transition to complete regardless. Thus, we need to ensure that a reset - // packet is ALWAYS sent on first switch. - // - // As we know that calling this branch only happens on first join, we set that if we are a - // Forge client that we must reset on the next switch. - // - // The call will handle if the player is not a Forge player appropriately. - player.getConnection().setCanSendLegacyFmlResetPacket(true); + // Required for Legacy Forge + player.getPhase().onFirstJoin(player); } else { // Clear tab list to avoid duplicate entries player.getTabList().clearAll(); @@ -359,21 +345,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Flush everything player.getConnection().flush(); serverMc.flush(); - serverConn.setHasCompletedJoin(true); - if (serverConn.isLegacyForge()) { - // We only need to indicate we can send a reset packet if we complete a handshake, that is, - // logged onto a Forge server. - // - // The special case is if we log onto a Vanilla server as our first server, FML will treat - // this as complete and **will** need a reset packet sending at some point. We will handle - // this during initial player connection if the player is detected to be forge. - // - // We do not use the result of VelocityServerConnection#isLegacyForge() directly because we - // don't want to set it false if this is a first connection to a Vanilla server. - // - // See LoginSessionHandler#handle for where the counterpart to this method is - player.getConnection().setCanSendLegacyFmlResetPacket(true); - } + serverConn.completeJoin(); } public List getServerBossBars() { @@ -401,4 +373,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getConnection().write(response); } } + + /** + * Immediately send any queued messages to the server. + */ + public void flushQueuedMessages() { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + MinecraftConnection connection = serverConnection.getConnection(); + if (connection != null) { + PluginMessage pm; + while ((pm = loginPluginMessages.poll()) != null) { + connection.write(pm); + } + } + } + } } 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 66c5a3855..ea71285eb 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 @@ -30,12 +30,13 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.connection.forge.ForgeConstants; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.TitlePacket; import com.velocitypowered.proxy.server.VelocityRegisteredServer; @@ -48,6 +49,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; +import java.util.concurrent.ThreadLocalRandom; import net.kyori.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.TranslatableComponent; @@ -79,6 +81,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private @Nullable ModInfo modInfo; private final VelocityTabList tabList; private final VelocityServer server; + private ClientConnectionPhase connectionPhase; @MonotonicNonNull private List serversToTry = null; @@ -91,6 +94,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { this.connection = connection; this.virtualHost = virtualHost; this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; + this.connectionPhase = connection.getType().getInitialClientPhase(); } @Override @@ -139,7 +143,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return Optional.ofNullable(modInfo); } - void setModInfo(ModInfo modInfo) { + public void setModInfo(ModInfo modInfo) { this.modInfo = modInfo; server.getEventManager().fireAndForget(new PlayerModInfoEvent(this, modInfo)); } @@ -409,10 +413,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } public void sendLegacyForgeHandshakeResetPacket() { - if (connection.canSendLegacyFmlResetPacket()) { - connection.write(ForgeConstants.resetPacket()); - connection.setCanSendLegacyFmlResetPacket(false); - } + connectionPhase.resetConnectionPhase(this); } private MinecraftConnection ensureBackendConnection() { @@ -469,6 +470,39 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { ensureBackendConnection().write(Chat.createServerbound(input)); } + /** + * Sends a {@link KeepAlive} packet to the player with a random ID. + * The response will be ignored by Velocity as it will not match the + * ID last sent by the server. + */ + public void sendKeepAlive() { + if (connection.getState() == StateRegistry.PLAY) { + KeepAlive keepAlive = new KeepAlive(); + keepAlive.setRandomId(ThreadLocalRandom.current().nextLong()); + connection.write(keepAlive); + } + } + + /** + * Gets the current "phase" of the connection, mostly used for tracking + * modded negotiation for legacy forge servers and provides methods + * for performing phase specific actions. + * + * @return The {@link ClientConnectionPhase} + */ + public ClientConnectionPhase getPhase() { + return connectionPhase; + } + + /** + * Sets the current "phase" of the connection. See {@link #getPhase()} + * + * @param connectionPhase The {@link ClientConnectionPhase} + */ + public void setPhase(ClientConnectionPhase connectionPhase) { + this.connectionPhase = connectionPhase; + } + private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final RegisteredServer toConnect; 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 2f52e49b0..9a04833e2 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 @@ -10,8 +10,11 @@ import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.VelocityConfiguration; +import com.velocitypowered.proxy.connection.ConnectionType; +import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; @@ -95,13 +98,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { return true; } - // Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13). - boolean isForge = handshake.getServerAddress().endsWith("\0FML\0"); - connection.setLegacyForge(isForge); + ConnectionType type = checkForForge(handshake); + connection.setType(type); - // Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ - // reject Forge. - if (handshake.getServerAddress().contains("\0") && !isForge) { + // Make sure legacy forwarding is not in use on this connection. + if (!type.checkServerAddressIsValid(handshake.getServerAddress())) { connection.closeWith(Disconnect .create(TextComponent.of("Running Velocity behind Velocity is unsupported."))); return true; @@ -124,6 +125,18 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { } } + private ConnectionType checkForForge(Handshake handshake) { + // Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13). + if (handshake.getServerAddress().endsWith(LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN) + && handshake.getProtocolVersion().getProtocol() < ProtocolVersion.MINECRAFT_1_13.getProtocol()) { + return ConnectionTypes.LEGACY_FORGE; + } else { + // For later: See if we can determine Forge 1.13+ here, else this will need to be UNDETERMINED + // until later in the cycle (most likely determinable during the LOGIN phase) + return ConnectionTypes.VANILLA; + } + } + private String cleanVhost(String hostname) { int zeroIdx = hostname.indexOf('\0'); return zeroIdx == -1 ? hostname : hostname.substring(0, zeroIdx); 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 510bbad6a..1771e6b2e 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 @@ -17,7 +17,6 @@ import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; @@ -54,8 +53,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); private static final String MOJANG_HASJOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s"; - private static final GameProfile.Property IS_FORGE_CLIENT_PROPERTY = - new GameProfile.Property("forgeClient", "true", ""); private final VelocityServer server; private final MinecraftConnection inbound; @@ -214,14 +211,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } private void initializePlayer(GameProfile profile, boolean onlineMode) { - if (inbound.isLegacyForge() && server.getConfiguration().getPlayerInfoForwardingMode() - == PlayerInfoForwarding.LEGACY) { - // We can't forward the FML token to the server when we are running in legacy forwarding mode, - // since both use the "hostname" field in the handshake. We add a special property to the - // profile instead, which will be ignored by non-Forge servers and can be intercepted by a - // Forge coremod, such as SpongeForge. - profile = profile.addProperty(IS_FORGE_CLIENT_PROPERTY); - } + // Some connection types may need to alter the game profile. + profile = inbound.getType().addGameProfileTokensIfRequired(profile, + server.getConfiguration().getPlayerInfoForwardingMode()); GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, onlineMode); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/ForgeConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/ForgeConstants.java deleted file mode 100644 index 322741a31..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/ForgeConstants.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.velocitypowered.proxy.connection.forge; - -import com.velocitypowered.proxy.protocol.packet.PluginMessage; - -public class ForgeConstants { - - public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS"; - public static final String FORGE_LEGACY_CHANNEL = "FML"; - public static final String FORGE_MULTIPART_LEGACY_CHANNEL = "FML|MP"; - private static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[]{-2, 0}; - - private ForgeConstants() { - throw new AssertionError(); - } - - public static PluginMessage resetPacket() { - PluginMessage msg = new PluginMessage(); - msg.setChannel(FORGE_LEGACY_HANDSHAKE_CHANNEL); - msg.setData(FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone()); - return msg; - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/ForgeUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/ForgeUtil.java deleted file mode 100644 index cf1774e76..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/ForgeUtil.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.velocitypowered.proxy.connection.forge; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.velocitypowered.api.util.ModInfo; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import java.util.List; - -public class ForgeUtil { - - private ForgeUtil() { - throw new AssertionError(); - } - - public static List readModList(PluginMessage message) { - Preconditions.checkNotNull(message, "message"); - Preconditions - .checkArgument(message.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL), - "message is not a FML HS plugin message"); - - ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData()); - try { - byte discriminator = byteBuf.readByte(); - - if (discriminator == 2) { - ImmutableList.Builder mods = ImmutableList.builder(); - int modCount = ProtocolUtils.readVarInt(byteBuf); - - for (int index = 0; index < modCount; index++) { - String id = ProtocolUtils.readString(byteBuf); - String version = ProtocolUtils.readString(byteBuf); - mods.add(new ModInfo.Mod(id, version)); - } - - return mods.build(); - } - - return ImmutableList.of(); - } finally { - byteBuf.release(); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConnectionType.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConnectionType.java new file mode 100644 index 000000000..4f1cab77a --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConnectionType.java @@ -0,0 +1,38 @@ +package com.velocitypowered.proxy.connection.forge.legacy; + +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.connection.ConnectionTypes; +import com.velocitypowered.proxy.connection.util.ConnectionTypeImpl; + +/** + * Contains extra logic for {@link ConnectionTypes#LEGACY_FORGE} + */ +public class LegacyForgeConnectionType extends ConnectionTypeImpl { + + private static final GameProfile.Property IS_FORGE_CLIENT_PROPERTY = + new GameProfile.Property("forgeClient", "true", ""); + + public LegacyForgeConnectionType() { + super(LegacyForgeHandshakeClientPhase.NOT_STARTED, LegacyForgeHandshakeBackendPhase.NOT_STARTED); + } + + @Override + public GameProfile addGameProfileTokensIfRequired(GameProfile original, PlayerInfoForwarding forwardingType) { + // We can't forward the FML token to the server when we are running in legacy forwarding mode, + // since both use the "hostname" field in the handshake. We add a special property to the + // profile instead, which will be ignored by non-Forge servers and can be intercepted by a + // Forge coremod, such as SpongeForge. + if (forwardingType == PlayerInfoForwarding.LEGACY) { + return original.addProperty(IS_FORGE_CLIENT_PROPERTY); + } + + return original; + } + + @Override + public boolean checkServerAddressIsValid(String address) { + return super.checkServerAddressIsValid( + address.replaceAll(LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN, "")); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java new file mode 100644 index 000000000..6ff0a048f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeConstants.java @@ -0,0 +1,59 @@ +package com.velocitypowered.proxy.connection.forge.legacy; + +/** + * Constants for use with Legacy Forge systems. + */ +public class LegacyForgeConstants { + + /** + * Clients attempting to connect to 1.8+ Forge servers will have + * this token appended to the hostname in the initial handshake + * packet. + */ + public static final String HANDSHAKE_HOSTNAME_TOKEN = "\0FML\0"; + + /** + * The channel for legacy forge handshakes. + */ + public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS"; + + /** + * The reset packet discriminator. + */ + private static final int RESET_DATA_DISCRIMINATOR = -2; + + /** + * The acknowledgement packet discriminator. + */ + static final int ACK_DISCRIMINATOR = -1; + + /** + * The Server -> Client Hello discriminator. + */ + static final int SERVER_HELLO_DISCRIMINATOR = 0; + + /** + * The Client -> Server Hello discriminator. + */ + static final int CLIENT_HELLO_DISCRIMINATOR = 1; + + /** + * The Mod List discriminator. + */ + static final int MOD_LIST_DISCRIMINATOR = 2; + + /** + * The Registry discriminator. + */ + static final int REGISTRY_DISCRIMINATOR = 3; + + /** + * The form of the data for the reset packet + */ + static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[]{RESET_DATA_DISCRIMINATOR, 0}; + + private LegacyForgeConstants() { + throw new AssertionError(); + } + +} 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 new file mode 100644 index 000000000..505e068c8 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeBackendPhase.java @@ -0,0 +1,173 @@ +package com.velocitypowered.proxy.connection.forge.legacy; + +import com.velocitypowered.proxy.connection.ConnectionTypes; +import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase; +import com.velocitypowered.proxy.connection.backend.BackendConnectionPhases; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import javax.annotation.Nullable; + +/** + * Allows for simple tracking of the phase that the Legacy + * Forge handshake is in (server side). + */ +public enum LegacyForgeHandshakeBackendPhase implements BackendConnectionPhase { + + /** + * Dummy phase for use with {@link BackendConnectionPhases#UNKNOWN} + */ + NOT_STARTED(LegacyForgeConstants.SERVER_HELLO_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeBackendPhase nextPhase() { + return HELLO; + } + }, + + /** + * Sent a hello to the client, waiting for a hello back before sending + * the mod list. + */ + HELLO(LegacyForgeConstants.MOD_LIST_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeBackendPhase nextPhase() { + return SENT_MOD_LIST; + } + + @Override + void onTransitionToNewPhase(VelocityServerConnection connection) { + // We must always reset the handshake before a modded connection is established if + // we haven't done so already. + if (connection.getConnection() != null) { + connection.getConnection().setType(ConnectionTypes.LEGACY_FORGE); + } + connection.getPlayer().sendLegacyForgeHandshakeResetPacket(); + } + }, + + /** + * The mod list from the client has been accepted and a server mod list + * has been sent. Waiting for the client to acknowledge. + */ + SENT_MOD_LIST(LegacyForgeConstants.REGISTRY_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeBackendPhase nextPhase() { + return SENT_SERVER_DATA; + } + }, + + /** + * The server data is being sent or has been sent, and is waiting for + * the client to acknowledge it has processed this. + */ + SENT_SERVER_DATA(LegacyForgeConstants.ACK_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeBackendPhase nextPhase() { + return WAITING_ACK; + } + }, + + /** + * Waiting for the client to acknowledge before completing handshake. + */ + WAITING_ACK(LegacyForgeConstants.ACK_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeBackendPhase nextPhase() { + return COMPLETE; + } + }, + + /** + * The server has completed the handshake and will continue after the client ACK. + */ + COMPLETE(null) { + @Override + public boolean consideredComplete() { + return true; + } + }; + + @Nullable private final Integer packetToAdvanceOn; + + /** + * Creates an instance of the {@link LegacyForgeHandshakeClientPhase}. + * + * @param packetToAdvanceOn The ID of the packet discriminator that indicates + * that the server has moved onto a new phase, and + * as such, Velocity should do so too (inspecting + * {@link #nextPhase()}. A null indicates there is no + * further phase to transition to. + */ + LegacyForgeHandshakeBackendPhase(@Nullable Integer packetToAdvanceOn) { + this.packetToAdvanceOn = packetToAdvanceOn; + } + + @Override + public final boolean handle(VelocityServerConnection serverConnection, + ConnectedPlayer player, + PluginMessage message) { + if (message.getChannel().equals(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { + // Get the phase and check if we need to start the next phase. + LegacyForgeHandshakeBackendPhase newPhase = getNewPhase(serverConnection, message); + + // Update phase on server + serverConnection.setConnectionPhase(newPhase); + + // Write the packet to the player, we don't need it now. + player.getConnection().write(message); + return true; + } + + // Not handled, fallback + return false; + } + + @Override + public boolean consideredComplete() { + return false; + } + + @Override + public void onDepartForNewServer(VelocityServerConnection serverConnection, + ConnectedPlayer player) { + // If the server we are departing is modded, we must always reset the client's handshake. + player.getPhase().resetConnectionPhase(player); + } + + /** + * Performs any specific tasks when moving to a new phase. + * + * @param connection The server connection + */ + void onTransitionToNewPhase(VelocityServerConnection connection) { + } + + /** + * Gets the next phase, if any (will return self if we are at the end + * of the handshake). + * + * @return The next phase + */ + LegacyForgeHandshakeBackendPhase nextPhase() { + return this; + } + + /** + * Get the phase to act on, depending on the packet that has been sent. + * + * @param serverConnection The server Velocity is connecting to + * @param packet The packet + * @return The phase to transition to, which may be the same as before. + */ + private LegacyForgeHandshakeBackendPhase getNewPhase(VelocityServerConnection serverConnection, + PluginMessage packet) { + if (packetToAdvanceOn != null + && LegacyForgeUtil.getHandshakePacketDiscriminator(packet) == packetToAdvanceOn) { + LegacyForgeHandshakeBackendPhase phaseToTransitionTo = nextPhase(); + phaseToTransitionTo.onTransitionToNewPhase(serverConnection); + return phaseToTransitionTo; + } + + return this; + } +} 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 new file mode 100644 index 000000000..f8e3f2ef6 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java @@ -0,0 +1,255 @@ +package com.velocitypowered.proxy.connection.forge.legacy; + +import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.connection.client.ClientConnectionPhase; +import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import java.util.List; +import javax.annotation.Nullable; + +/** + * Allows for simple tracking of the phase that the Legacy + * Forge handshake is in + */ +public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { + + /** + * No handshake packets have yet been sent. + * Transition to {@link #HELLO} when the ClientHello is sent. + */ + NOT_STARTED(LegacyForgeConstants.CLIENT_HELLO_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeClientPhase nextPhase() { + return HELLO; + } + + @Override + public void onFirstJoin(ConnectedPlayer player) { + // We have something special to do for legacy Forge servers - during first connection the FML + // handshake will getNewPhase to complete regardless. Thus, we need to ensure that a reset + // packet is ALWAYS sent on first switch. + // + // As we know that calling this branch only happens on first join, we set that if we are a + // Forge client that we must reset on the next switch. + player.setPhase(LegacyForgeHandshakeClientPhase.COMPLETE); + } + + @Override + boolean onHandle(ConnectedPlayer player, + ClientPlaySessionHandler handler, + PluginMessage message, + MinecraftConnection backendConn) { + // If we stay in this phase, we do nothing because it means the packet wasn't handled. + // Returning false indicates this + return false; + } + }, + + /** + * Client and Server exchange pleasantries. + * Transition to {@link #MOD_LIST} when the ModList is sent. + */ + HELLO(LegacyForgeConstants.MOD_LIST_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeClientPhase nextPhase() { + return MOD_LIST; + } + }, + + + + /** + * The Mod list is sent to the server, captured by Velocity. + * Transition to {@link #WAITING_SERVER_DATA} when an ACK is sent, which + * indicates to the server to start sending state data. + */ + MOD_LIST(LegacyForgeConstants.ACK_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeClientPhase nextPhase() { + return WAITING_SERVER_DATA; + } + + @Override + boolean onHandle(ConnectedPlayer player, + ClientPlaySessionHandler handler, + PluginMessage message, + MinecraftConnection backendConn) { + // Read the mod list if we haven't already. + if (!player.getModInfo().isPresent()) { + List mods = LegacyForgeUtil.readModList(message); + if (!mods.isEmpty()) { + player.setModInfo(new ModInfo("FML", mods)); + } + } + + return super.onHandle(player, handler, message, backendConn); + } + }, + + /** + * Waiting for state data to be received. + * Transition to {@link #WAITING_SERVER_COMPLETE} when this is complete + * and the client sends an ACK packet to confirm + */ + WAITING_SERVER_DATA(LegacyForgeConstants.ACK_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeClientPhase nextPhase() { + return WAITING_SERVER_COMPLETE; + } + }, + + /** + * Waiting on the server to send another ACK. + * Transition to {@link #PENDING_COMPLETE} when client sends another + * ACK + */ + WAITING_SERVER_COMPLETE(LegacyForgeConstants.ACK_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeClientPhase nextPhase() { + return PENDING_COMPLETE; + } + }, + + /** + * Waiting on the server to send yet another ACK. + * Transition to {@link #COMPLETE} when client sends another + * ACK + */ + PENDING_COMPLETE(LegacyForgeConstants.ACK_DISCRIMINATOR) { + @Override + LegacyForgeHandshakeClientPhase nextPhase() { + return COMPLETE; + } + }, + + /** + * The handshake is complete. The handshake can be reset. + * + *

Note that a successful connection to a server does not mean that + * we will be in this state. After a handshake reset, if the next server + * is vanilla we will still be in the {@link #NOT_STARTED} phase, + * which means we must NOT send a reset packet. This is handled by + * overriding the {@link #resetConnectionPhase(ConnectedPlayer)} in this + * element (it is usually a no-op).

+ */ + COMPLETE(null) { + @Override + public void resetConnectionPhase(ConnectedPlayer player) { + player.getConnection().write(LegacyForgeUtil.resetPacket()); + player.setPhase(LegacyForgeHandshakeClientPhase.NOT_STARTED); + } + + @Override + public boolean consideredComplete() { + return true; + } + + @Override + boolean onHandle(ConnectedPlayer player, + ClientPlaySessionHandler handler, + PluginMessage message, + MinecraftConnection backendConn) { + super.onHandle(player, handler, message, backendConn); + + // just in case the timing is awful + player.sendKeepAlive(); + handler.flushQueuedMessages(); + + return true; + } + }; + + @Nullable private final Integer packetToAdvanceOn; + + /** + * Creates an instance of the {@link LegacyForgeHandshakeClientPhase}. + * + * @param packetToAdvanceOn The ID of the packet discriminator that indicates + * that the client has moved onto a new phase, and + * as such, Velocity should do so too (inspecting + * {@link #nextPhase()}. A null indicates there is no + * further phase to transition to. + */ + LegacyForgeHandshakeClientPhase(Integer packetToAdvanceOn) { + this.packetToAdvanceOn = packetToAdvanceOn; + } + + @Override + public final boolean handle(ConnectedPlayer player, + ClientPlaySessionHandler handler, + PluginMessage message) { + VelocityServerConnection serverConn = player.getConnectedServer(); + if (serverConn != null) { + MinecraftConnection backendConn = serverConn.getConnection(); + if (backendConn != null + && message.getChannel().equals(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { + // Get the phase and check if we need to start the next phase. + LegacyForgeHandshakeClientPhase newPhase = getNewPhase(message); + + // Update phase on player + player.setPhase(newPhase); + + // Perform phase handling + return newPhase.onHandle(player, handler, message, backendConn); + } + } + + // Not handled, fallback + return false; + } + + /** + * Handles the phase tasks + * + * @param player The player + * @param handler The {@link ClientPlaySessionHandler} that is handling + * packets + * @param message The message to handle + * @param backendConn The backend connection to write to, if required. + * + * @return true if handled, false otherwise. + */ + boolean onHandle(ConnectedPlayer player, + ClientPlaySessionHandler handler, + PluginMessage message, + MinecraftConnection backendConn) { + // Send the packet on to the server. + backendConn.write(message); + + // We handled the packet. No need to continue processing. + return true; + } + + @Override + public boolean consideredComplete() { + return false; + } + + /** + * Gets the next phase, if any (will return self if we are at the end + * of the handshake). + * + * @return The next phase + */ + LegacyForgeHandshakeClientPhase nextPhase() { + return this; + } + + /** + * Get the phase to act on, depending on the packet that has been sent. + * + * @param packet The packet + * @return The phase to transition to, which may be the same as before. + */ + private LegacyForgeHandshakeClientPhase getNewPhase(PluginMessage packet) { + if (packetToAdvanceOn != null + && LegacyForgeUtil.getHandshakePacketDiscriminator(packet) == packetToAdvanceOn) { + return nextPhase(); + } + + return this; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java new file mode 100644 index 000000000..f75067fb5 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java @@ -0,0 +1,81 @@ +package com.velocitypowered.proxy.connection.forge.legacy; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.util.ModInfo; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.List; + +class LegacyForgeUtil { + + private LegacyForgeUtil() { + throw new AssertionError(); + } + + /** + * Gets the discriminator from the FML|HS packet (the first byte in the data) + * + * @param message The message to analyse + * @return The discriminator + */ + static byte getHandshakePacketDiscriminator(PluginMessage message) { + Preconditions.checkArgument( + message.getChannel().equals(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)); + ByteBuf buf = Unpooled.wrappedBuffer(message.getData()); + try { + return buf.readByte(); + } finally { + buf.release(); + } + } + + /** + * Gets the mod list from the mod list packet and parses it. + * + * @param message The message + * @return The list of mods. May be empty. + */ + static List readModList(PluginMessage message) { + Preconditions.checkNotNull(message, "message"); + Preconditions + .checkArgument(message.getChannel().equals(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL), + "message is not a FML HS plugin message"); + + ByteBuf byteBuf = Unpooled.wrappedBuffer(message.getData()); + try { + byte discriminator = byteBuf.readByte(); + + if (discriminator == LegacyForgeConstants.MOD_LIST_DISCRIMINATOR) { + ImmutableList.Builder mods = ImmutableList.builder(); + int modCount = ProtocolUtils.readVarInt(byteBuf); + + for (int index = 0; index < modCount; index++) { + String id = ProtocolUtils.readString(byteBuf); + String version = ProtocolUtils.readString(byteBuf); + mods.add(new ModInfo.Mod(id, version)); + } + + return mods.build(); + } + + return ImmutableList.of(); + } finally { + byteBuf.release(); + } + } + + /** + * Creates a reset packet. + * + * @return A copy of the reset packet + */ + static PluginMessage resetPacket() { + PluginMessage msg = new PluginMessage(); + msg.setChannel(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL); + msg.setData(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_RESET_DATA.clone()); + return msg; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionTypeImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionTypeImpl.java new file mode 100644 index 000000000..cba5efac7 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionTypeImpl.java @@ -0,0 +1,39 @@ +package com.velocitypowered.proxy.connection.util; + +import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.connection.ConnectionType; +import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase; +import com.velocitypowered.proxy.connection.client.ClientConnectionPhase; + +/** + * Indicates the type of connection that has been made + */ +public class ConnectionTypeImpl implements ConnectionType { + + private final ClientConnectionPhase initialClientPhase; + private final BackendConnectionPhase initialBackendPhase; + + public ConnectionTypeImpl(ClientConnectionPhase initialClientPhase, + BackendConnectionPhase initialBackendPhase) { + this.initialClientPhase = initialClientPhase; + this.initialBackendPhase = initialBackendPhase; + } + + @Override + public final ClientConnectionPhase getInitialClientPhase() { + return initialClientPhase; + } + + @Override + public final BackendConnectionPhase getInitialBackendPhase() { + return initialBackendPhase; + } + + @Override + public GameProfile addGameProfileTokensIfRequired(GameProfile original, + PlayerInfoForwarding forwardingType) { + return original; + } +} +