Mirror von
https://github.com/PaperMC/Velocity.git
synchronisiert 2024-12-23 23:00:35 +01:00
Track the forge handshake and re-engineer Forge support
The system is now organised into connection types and phases. Most of the core Velocity logic is now isolated from the Forge logic to allow for simpler maintainance and to try to support future loaders (Forge 1.13?) in a cleaner fashion. There are a couple of points where Forge specific calls are in the main body, but these have been minimised. Also send keepalives from Velocity to during backend server switch and after Forge handshake, to try to help Forge clients
Dieser Commit ist enthalten in:
Ursprung
8f859b980c
Commit
ccd37e5b9c
@ -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");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>Note that Forge phases are found in the enum
|
||||
* {@link LegacyForgeHandshakeBackendPhase}.</p>
|
||||
*/
|
||||
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) {
|
||||
}
|
||||
}
|
@ -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:");
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>Note that Forge phases are found in the enum
|
||||
* {@link LegacyForgeHandshakeClientPhase}.</p>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<ModInfo.Mod> 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<UUID> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String> 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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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, ""));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<ModInfo.Mod> 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.
|
||||
*
|
||||
* <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
|
||||
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren