3
0
Mirror von https://github.com/PaperMC/Velocity.git synchronisiert 2024-11-17 05:20:14 +01:00

Merge pull request #136 from dualspiral/forge-handshake

Refactor Forge support to track the handshake & include keepalives
Dieser Commit ist enthalten in:
Andrew Steinborn 2018-12-01 14:03:16 -05:00 committet von GitHub
Commit ed4994a792
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
22 geänderte Dateien mit 1073 neuen und 199 gelöschten Zeilen

Datei anzeigen

@ -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");
}
}

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -56,9 +56,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private ProtocolVersion protocolVersion; private ProtocolVersion protocolVersion;
private ProtocolVersion nextProtocolVersion; private ProtocolVersion nextProtocolVersion;
private @Nullable MinecraftConnectionAssociation association; private @Nullable MinecraftConnectionAssociation association;
private boolean isLegacyForge;
private final VelocityServer server; private final VelocityServer server;
private boolean canSendLegacyFmlResetPacket = false; private ConnectionType connectionType = ConnectionTypes.UNDETERMINED;
public MinecraftConnection(Channel channel, VelocityServer server) { public MinecraftConnection(Channel channel, VelocityServer server) {
this.channel = channel; this.channel = channel;
@ -279,22 +278,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
this.association = association; 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() { public ProtocolVersion getNextProtocolVersion() {
return this.nextProtocolVersion; return this.nextProtocolVersion;
} }
@ -302,4 +285,20 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setNextProtocolVersion(ProtocolVersion nextProtocolVersion) { public void setNextProtocolVersion(ProtocolVersion nextProtocolVersion) {
this.nextProtocolVersion = 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;
}
} }

Datei anzeigen

@ -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.
*
* <p>Note that Forge phases are found in the enum
* {@link LegacyForgeHandshakeBackendPhase}.</p>
*/
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) {
}
}

Datei anzeigen

@ -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.
*
* <p>See {@link LegacyForgeHandshakeBackendPhase} for Legacy Forge
* versions</p>
*/
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();
}
}

Datei anzeigen

@ -7,7 +7,7 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; 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.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.BossBar;
@ -94,18 +94,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
if (!serverConn.hasCompletedJoin() && packet.getChannel() if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
.equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { // Handled.
if (!serverConn.isLegacyForge()) { return true;
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;
} }
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); 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) { if (mc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_12_2) <= 0) {
String channel = message.getChannel(); String channel = message.getChannel();
minecraftOrFmlMessage = channel.startsWith("MC|") || channel minecraftOrFmlMessage = channel.startsWith("MC|") || channel
.startsWith(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL); .startsWith(LegacyForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL);
} else { } else {
minecraftOrFmlMessage = message.getChannel().startsWith("minecraft:"); minecraftOrFmlMessage = message.getChannel().startsWith("minecraft:");
} }

Datei anzeigen

@ -117,14 +117,15 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
serverConn.getPlayer().getConnection() serverConn.getPlayer().getConnection()
.setSessionHandler(new ClientPlaySessionHandler(server, serverConn.getPlayer())); .setSessionHandler(new ClientPlaySessionHandler(server, serverConn.getPlayer()));
} else { } else {
// If the server we are departing is modded, we must always reset the client's handshake. // For Legacy Forge
if (existingConnection.isLegacyForge()) { existingConnection.getPhase().onDepartForNewServer(serverConn, serverConn.getPlayer());
serverConn.getPlayer().sendLegacyForgeHandshakeResetPacket();
}
// Shut down the existing server connection. // Shut down the existing server connection.
serverConn.getPlayer().setConnectedServer(null); serverConn.getPlayer().setConnectedServer(null);
existingConnection.disconnect(); existingConnection.disconnect();
// Send keep alive to try to avoid timeouts
serverConn.getPlayer().sendKeepAlive();
} }
smc.getChannel().config().setAutoRead(false); smc.getChannel().config().setAutoRead(false);

Datei anzeigen

@ -17,10 +17,12 @@ import com.velocitypowered.api.proxy.server.ServerInfo;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.ConnectionTypes;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils; 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.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
@ -44,9 +46,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private final ConnectedPlayer proxyPlayer; private final ConnectedPlayer proxyPlayer;
private final VelocityServer server; private final VelocityServer server;
private @Nullable MinecraftConnection connection; private @Nullable MinecraftConnection connection;
private boolean legacyForge = false;
private boolean hasCompletedJoin = false; private boolean hasCompletedJoin = false;
private boolean gracefulDisconnect = false; private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private long lastPingId; private long lastPingId;
private long lastPingSent; private long lastPingSent;
@ -93,6 +95,10 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
// Kick off the connection process // Kick off the connection process
connection.setSessionHandler( connection.setSessionHandler(
new LoginSessionHandler(server, VelocityServerConnection.this, result)); 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(); startHandshake();
} else { } else {
// We need to remember to reset the in-flight connection to allow connect() to work // 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()); handshake.setProtocolVersion(proxyPlayer.getConnection().getNextProtocolVersion());
if (forwardingMode == PlayerInfoForwarding.LEGACY) { if (forwardingMode == PlayerInfoForwarding.LEGACY) {
handshake.setServerAddress(createLegacyForwardingAddress()); handshake.setServerAddress(createLegacyForwardingAddress());
} else if (proxyPlayer.getConnection().isLegacyForge()) { } else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0"); handshake.setServerAddress(handshake.getServerAddress() + LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN);
} else { } else {
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString()); handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
} }
@ -198,20 +204,17 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
return true; return true;
} }
public boolean isLegacyForge() { public void completeJoin() {
return legacyForge; if (!hasCompletedJoin) {
hasCompletedJoin = true;
if (connectionPhase == BackendConnectionPhases.UNKNOWN) {
// Now we know
connectionPhase = BackendConnectionPhases.VANILLA;
if (connection != null) {
connection.setType(ConnectionTypes.VANILLA);
} }
void setLegacyForge(boolean modded) {
legacyForge = modded;
} }
public boolean hasCompletedJoin() {
return hasCompletedJoin;
} }
public void setHasCompletedJoin(boolean hasCompletedJoin) {
this.hasCompletedJoin = hasCompletedJoin;
} }
boolean isGracefulDisconnect() { boolean isGracefulDisconnect() {
@ -245,4 +248,35 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
return connection != null && !connection.isClosed() && !gracefulDisconnect return connection != null && !connection.isClosed() && !gracefulDisconnect
&& proxyPlayer.isActive(); && 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;
}
} }

Datei anzeigen

@ -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.
*
* <p>Note that Forge phases are found in the enum
* {@link LegacyForgeHandshakeClientPhase}.</p>
*/
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;
}
}

Datei anzeigen

@ -0,0 +1,19 @@
package com.velocitypowered.proxy.connection.client;
/**
* The vanilla {@link ClientConnectionPhase}s.
*
* <p>See {@link com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHandshakeClientPhase}
* for Legacy Forge phases</p>
*/
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();
}
}

Datei anzeigen

@ -3,15 +3,13 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.PlayerChatEvent; import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.util.ModInfo;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; 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.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.BossBar; import com.velocitypowered.proxy.protocol.packet.BossBar;
import com.velocitypowered.proxy.protocol.packet.Chat; import com.velocitypowered.proxy.protocol.packet.Chat;
import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.ClientSettings;
@ -160,7 +158,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
VelocityServerConnection serverConn = player.getConnectedServer(); VelocityServerConnection serverConn = player.getConnectedServer();
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null; MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
if (serverConn != null && backendConn != 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<String> actuallyRegistered = new ArrayList<>(); List<String> actuallyRegistered = new ArrayList<>();
List<String> channels = PluginMessageUtil.getChannels(packet); List<String> channels = PluginMessageUtil.getChannels(packet);
for (String channel : channels) { for (String channel : channels) {
@ -184,24 +184,16 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} else if (PluginMessageUtil.isMcBrand(packet)) { } else if (PluginMessageUtil.isMcBrand(packet)) {
PluginMessage rewritten = PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion()); PluginMessage rewritten = PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion());
backendConn.write(rewritten); backendConn.write(rewritten);
} else if (backendConn.isLegacyForge() && !serverConn.hasCompletedJoin()) { } else if (!player.getPhase().handle(player, this, packet)) {
if (packet.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
if (!player.getModInfo().isPresent()) { if (!player.getPhase().consideredComplete()
List<ModInfo.Mod> mods = ForgeUtil.readModList(packet); || !serverConn.getPhase().consideredComplete()) {
if (!mods.isEmpty()) {
player.setModInfo(new ModInfo("FML", mods));
}
}
// Always forward the FML handshake to the remote server.
backendConn.write(packet);
} else {
// The client is trying to send messages too early. This is primarily caused by mods, but // 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 // 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 // non-FML handshake messages to be sent once the FML handshake has completed or the JoinGame
// proxy. // packet has been received by the proxy, whichever comes first.
loginPluginMessages.add(packet); loginPluginMessages.add(packet);
}
} else { } else {
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) { if (id == null) {
@ -214,6 +206,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
} }
} }
}
return true; return true;
} }
@ -227,7 +220,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.hasCompletedJoin()) { if (smc != null && serverConnection.getPhase().consideredComplete()) {
smc.write(packet); smc.write(packet);
} }
} }
@ -241,7 +234,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.hasCompletedJoin()) { if (smc != null && serverConnection.getPhase().consideredComplete()) {
smc.write(buf.retain()); smc.write(buf.retain());
} }
} }
@ -289,15 +282,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
spawned = true; spawned = true;
player.getConnection().delayedWrite(joinGame); player.getConnection().delayedWrite(joinGame);
// We have something special to do for legacy Forge servers - during first connection the FML // Required for Legacy Forge
// handshake will transition to complete regardless. Thus, we need to ensure that a reset player.getPhase().onFirstJoin(player);
// 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);
} else { } else {
// Clear tab list to avoid duplicate entries // Clear tab list to avoid duplicate entries
player.getTabList().clearAll(); player.getTabList().clearAll();
@ -359,21 +345,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Flush everything // Flush everything
player.getConnection().flush(); player.getConnection().flush();
serverMc.flush(); serverMc.flush();
serverConn.setHasCompletedJoin(true); serverConn.completeJoin();
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);
}
} }
public List<UUID> getServerBossBars() { public List<UUID> getServerBossBars() {
@ -401,4 +373,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
player.getConnection().write(response); 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);
}
}
}
}
} }

Datei anzeigen

@ -30,12 +30,13 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; 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.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; 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.Chat;
import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.ClientSettings;
import com.velocitypowered.proxy.protocol.packet.Disconnect; 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.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.TitlePacket; import com.velocitypowered.proxy.protocol.packet.TitlePacket;
import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.server.VelocityRegisteredServer;
@ -48,6 +49,7 @@ import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
import net.kyori.text.Component; import net.kyori.text.Component;
import net.kyori.text.TextComponent; import net.kyori.text.TextComponent;
import net.kyori.text.TranslatableComponent; import net.kyori.text.TranslatableComponent;
@ -79,6 +81,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private @Nullable ModInfo modInfo; private @Nullable ModInfo modInfo;
private final VelocityTabList tabList; private final VelocityTabList tabList;
private final VelocityServer server; private final VelocityServer server;
private ClientConnectionPhase connectionPhase;
@MonotonicNonNull @MonotonicNonNull
private List<String> serversToTry = null; private List<String> serversToTry = null;
@ -91,6 +94,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
this.connection = connection; this.connection = connection;
this.virtualHost = virtualHost; this.virtualHost = virtualHost;
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
this.connectionPhase = connection.getType().getInitialClientPhase();
} }
@Override @Override
@ -139,7 +143,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return Optional.ofNullable(modInfo); return Optional.ofNullable(modInfo);
} }
void setModInfo(ModInfo modInfo) { public void setModInfo(ModInfo modInfo) {
this.modInfo = modInfo; this.modInfo = modInfo;
server.getEventManager().fireAndForget(new PlayerModInfoEvent(this, modInfo)); server.getEventManager().fireAndForget(new PlayerModInfoEvent(this, modInfo));
} }
@ -409,10 +413,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
} }
public void sendLegacyForgeHandshakeResetPacket() { public void sendLegacyForgeHandshakeResetPacket() {
if (connection.canSendLegacyFmlResetPacket()) { connectionPhase.resetConnectionPhase(this);
connection.write(ForgeConstants.resetPacket());
connection.setCanSendLegacyFmlResetPacket(false);
}
} }
private MinecraftConnection ensureBackendConnection() { private MinecraftConnection ensureBackendConnection() {
@ -469,6 +470,39 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
ensureBackendConnection().write(Chat.createServerbound(input)); 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 class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect; private final RegisteredServer toConnect;

Datei anzeigen

@ -10,8 +10,11 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.config.VelocityConfiguration; 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.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; 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.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.Disconnect;
@ -95,13 +98,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13). ConnectionType type = checkForForge(handshake);
boolean isForge = handshake.getServerAddress().endsWith("\0FML\0"); connection.setType(type);
connection.setLegacyForge(isForge);
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_ // Make sure legacy forwarding is not in use on this connection.
// reject Forge. if (!type.checkServerAddressIsValid(handshake.getServerAddress())) {
if (handshake.getServerAddress().contains("\0") && !isForge) {
connection.closeWith(Disconnect connection.closeWith(Disconnect
.create(TextComponent.of("Running Velocity behind Velocity is unsupported."))); .create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
return true; 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) { private String cleanVhost(String hostname) {
int zeroIdx = hostname.indexOf('\0'); int zeroIdx = hostname.indexOf('\0');
return zeroIdx == -1 ? hostname : hostname.substring(0, zeroIdx); return zeroIdx == -1 ? hostname : hostname.substring(0, zeroIdx);

Datei anzeigen

@ -17,7 +17,6 @@ import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; 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 Logger logger = LogManager.getLogger(LoginSessionHandler.class);
private static final String MOJANG_HASJOINED_URL = private static final String MOJANG_HASJOINED_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s"; "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 VelocityServer server;
private final MinecraftConnection inbound; private final MinecraftConnection inbound;
@ -214,14 +211,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
} }
private void initializePlayer(GameProfile profile, boolean onlineMode) { private void initializePlayer(GameProfile profile, boolean onlineMode) {
if (inbound.isLegacyForge() && server.getConfiguration().getPlayerInfoForwardingMode() // Some connection types may need to alter the game profile.
== PlayerInfoForwarding.LEGACY) { profile = inbound.getType().addGameProfileTokensIfRequired(profile,
// We can't forward the FML token to the server when we are running in legacy forwarding mode, server.getConfiguration().getPlayerInfoForwardingMode());
// 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);
}
GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile, GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(apiInbound, profile,
onlineMode); onlineMode);

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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<ModInfo.Mod> 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<ModInfo.Mod> 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();
}
}
}

Datei anzeigen

@ -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, ""));
}
}

Datei anzeigen

@ -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();
}
}

Datei anzeigen

@ -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;
}
}

Datei anzeigen

@ -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<ModInfo.Mod> 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.
*
* <p>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).</p>
*/
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;
}
}

Datei anzeigen

@ -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<ModInfo.Mod> 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<ModInfo.Mod> 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;
}
}

Datei anzeigen

@ -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;
}
}