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..aa21e91f2
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/ConnectionType.java
@@ -0,0 +1,69 @@
+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;
+import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConnectionType;
+import com.velocitypowered.proxy.connection.util.ConnectionTypeImpl;
+
+/**
+ * The types of connection that may be selected.
+ */
+public interface ConnectionType {
+
+ /**
+ * Indicates that the connection has yet to reach the
+ * point where we have a definitive answer as to what
+ * type of connection we have.
+ */
+ ConnectionType UNDETERMINED =
+ new ConnectionTypeImpl(ClientConnectionPhase.VANILLA, BackendConnectionPhase.UNKNOWN);
+
+ /**
+ * Indicates that the connection is a Vanilla connection.
+ */
+ ConnectionType VANILLA =
+ new ConnectionTypeImpl(ClientConnectionPhase.VANILLA, BackendConnectionPhase.VANILLA);
+
+ /**
+ * Indicates that the connection is a 1.8-1.12 Forge
+ * connection.
+ */
+ ConnectionType LEGACY_FORGE = new LegacyForgeConnectionType();
+
+ /**
+ * 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/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java
index ec0e99597..f8361121d 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 = ConnectionType.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..f9e6d25d5
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendConnectionPhase.java
@@ -0,0 +1,70 @@
+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 {
+
+ /**
+ * The backend connection is vanilla.
+ */
+ BackendConnectionPhase VANILLA = new BackendConnectionPhase() {};
+
+ /**
+ * The backend connection is unknown at this time.
+ */
+ 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);
+ }
+ };
+
+ /**
+ * 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/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..13ff6a43e 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,6 +17,7 @@ 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.ConnectionType;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
@@ -44,9 +45,8 @@ 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 = BackendConnectionPhase.UNKNOWN;
private long lastPingId;
private long lastPingSent;
@@ -93,6 +93,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,7 +137,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setProtocolVersion(proxyPlayer.getConnection().getNextProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createLegacyForwardingAddress());
- } else if (proxyPlayer.getConnection().isLegacyForge()) {
+ } else if (proxyPlayer.getConnection().getType() == ConnectionType.LEGACY_FORGE) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
} else {
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
@@ -198,20 +202,14 @@ 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 (connectionPhase == BackendConnectionPhase.UNKNOWN) {
+ // Now we know
+ connectionPhase = BackendConnectionPhase.VANILLA;
+ if (connection != null) {
+ connection.setType(ConnectionType.VANILLA);
+ }
+ }
}
boolean isGracefulDisconnect() {
@@ -245,4 +243,24 @@ 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;
+ }
}
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..d5ff78ed3
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConnectionPhase.java
@@ -0,0 +1,60 @@
+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 {
+
+ /**
+ * The client is connecting with a vanilla client (as far as we can tell).
+ */
+ ClientConnectionPhase VANILLA = new 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/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java
index 582458417..625e237aa 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
@@ -9,8 +9,6 @@ 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.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Chat;
@@ -184,33 +182,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 +218,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 +232,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 +280,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 +343,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() {
@@ -400,4 +370,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..6bc50e707 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;
@@ -45,6 +46,7 @@ import java.net.InetSocketAddress;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
+import java.util.Random;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
@@ -64,6 +66,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private static final PlainComponentSerializer PASS_THRU_TRANSLATE = new PlainComponentSerializer(
c -> "", TranslatableComponent::key);
static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
+ private static final Random RANDOM = new Random();
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
@@ -79,6 +82,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 +95,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 +144,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 +414,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 +471,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(RANDOM.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..79dd45b83 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,10 @@ 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.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 +97,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 +124,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() < ProtocolConstants.MINECRAFT_1_13) {
+ return ConnectionType.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 ConnectionType.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..386c3fe19
--- /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.ConnectionType;
+import com.velocitypowered.proxy.connection.util.ConnectionTypeImpl;
+
+/**
+ * Contains extra logic for {@link ConnectionType#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..5ff277a8a
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeBackendPhase.java
@@ -0,0 +1,172 @@
+package com.velocitypowered.proxy.connection.forge.legacy;
+
+import com.velocitypowered.proxy.connection.ConnectionType;
+import com.velocitypowered.proxy.connection.backend.BackendConnectionPhase;
+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 BackendConnectionPhase#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(ConnectionType.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..14b82367a
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java
@@ -0,0 +1,242 @@
+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 javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * 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);
+ }
+ },
+
+ /**
+ * 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
+ void preWrite(ConnectedPlayer player, PluginMessage packet) {
+ // Read the mod list if we haven't already.
+ if (!player.getModInfo().isPresent()) {
+ List mods = LegacyForgeUtil.readModList(packet);
+ if (!mods.isEmpty()) {
+ player.setModInfo(new ModInfo("FML", mods));
+ }
+ }
+ }
+ },
+
+ /**
+ * 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
+ void postWrite(ConnectedPlayer player, ClientPlaySessionHandler handler) {
+ // just in case the timing is awful
+ player.sendKeepAlive();
+ handler.flushQueuedMessages();
+ }
+ };
+
+ @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 tasks before sending the packet on to the server.
+ newPhase.preWrite(player, message);
+
+ // Send the packet on to the server.
+ backendConn.write(message);
+
+ // Perform tasks after sending the packet on, such as keep alives.
+ newPhase.postWrite(player, handler);
+
+ // We handled the packet, nothing else needs to.
+ return true;
+ }
+ }
+
+ // Not handled, fallback
+ return false;
+ }
+
+ @Override
+ public boolean consideredComplete() {
+ return false;
+ }
+
+ /**
+ * Actions to occur before the handled packet is sent on to
+ * the server.
+ *
+ * @param player The player to act on
+ * @param packet The packet that was sent
+ */
+ void preWrite(ConnectedPlayer player, PluginMessage packet) {
+ // usually nothing to do.
+ }
+
+ /**
+ * Actions to occur after the handled packet is sent on to the
+ * server.
+ *
+ * @param player The player
+ * @param handler The {@link ClientPlaySessionHandler} to act with
+ */
+ void postWrite(ConnectedPlayer player, ClientPlaySessionHandler handler) {
+ // usually nothing to do
+ }
+
+ /**
+ * 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;
+ }
+}
+