geforkt von Mirrors/Velocity
Merge pull request #136 from dualspiral/forge-handshake
Refactor Forge support to track the handshake & include keepalives
Dieser Commit ist enthalten in:
Commit
ed4994a792
@ -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");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -56,9 +56,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
private ProtocolVersion protocolVersion;
|
||||
private ProtocolVersion nextProtocolVersion;
|
||||
private @Nullable MinecraftConnectionAssociation association;
|
||||
private boolean isLegacyForge;
|
||||
private final VelocityServer server;
|
||||
private boolean canSendLegacyFmlResetPacket = false;
|
||||
private ConnectionType connectionType = ConnectionTypes.UNDETERMINED;
|
||||
|
||||
public MinecraftConnection(Channel channel, VelocityServer server) {
|
||||
this.channel = channel;
|
||||
@ -279,22 +278,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
this.association = association;
|
||||
}
|
||||
|
||||
public boolean isLegacyForge() {
|
||||
return isLegacyForge;
|
||||
}
|
||||
|
||||
public void setLegacyForge(boolean isForge) {
|
||||
this.isLegacyForge = isForge;
|
||||
}
|
||||
|
||||
public boolean canSendLegacyFmlResetPacket() {
|
||||
return canSendLegacyFmlResetPacket;
|
||||
}
|
||||
|
||||
public void setCanSendLegacyFmlResetPacket(boolean canSendLegacyFMLResetPacket) {
|
||||
this.canSendLegacyFmlResetPacket = isLegacyForge && canSendLegacyFMLResetPacket;
|
||||
}
|
||||
|
||||
public ProtocolVersion getNextProtocolVersion() {
|
||||
return this.nextProtocolVersion;
|
||||
}
|
||||
@ -302,4 +285,20 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
public void setNextProtocolVersion(ProtocolVersion nextProtocolVersion) {
|
||||
this.nextProtocolVersion = nextProtocolVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the detected {@link ConnectionType}
|
||||
* @return The {@link ConnectionType}
|
||||
*/
|
||||
public ConnectionType getType() {
|
||||
return connectionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the detected {@link ConnectionType}
|
||||
* @param connectionType The {@link ConnectionType}
|
||||
*/
|
||||
public void setType(ConnectionType connectionType) {
|
||||
this.connectionType = connectionType;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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,10 +17,12 @@ import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||
@ -44,9 +46,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
private final ConnectedPlayer proxyPlayer;
|
||||
private final VelocityServer server;
|
||||
private @Nullable MinecraftConnection connection;
|
||||
private boolean legacyForge = false;
|
||||
private boolean hasCompletedJoin = false;
|
||||
private boolean gracefulDisconnect = false;
|
||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||
private long lastPingId;
|
||||
private long lastPingSent;
|
||||
|
||||
@ -93,6 +95,10 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
// Kick off the connection process
|
||||
connection.setSessionHandler(
|
||||
new LoginSessionHandler(server, VelocityServerConnection.this, result));
|
||||
|
||||
// Set the connection phase, which may, for future forge (or whatever), be determined
|
||||
// at this point already
|
||||
connectionPhase = connection.getType().getInitialBackendPhase();
|
||||
startHandshake();
|
||||
} else {
|
||||
// We need to remember to reset the in-flight connection to allow connect() to work
|
||||
@ -133,8 +139,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
handshake.setProtocolVersion(proxyPlayer.getConnection().getNextProtocolVersion());
|
||||
if (forwardingMode == PlayerInfoForwarding.LEGACY) {
|
||||
handshake.setServerAddress(createLegacyForwardingAddress());
|
||||
} else if (proxyPlayer.getConnection().isLegacyForge()) {
|
||||
handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0");
|
||||
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||
handshake.setServerAddress(handshake.getServerAddress() + LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN);
|
||||
} else {
|
||||
handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString());
|
||||
}
|
||||
@ -198,20 +204,17 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isLegacyForge() {
|
||||
return legacyForge;
|
||||
}
|
||||
|
||||
void setLegacyForge(boolean modded) {
|
||||
legacyForge = modded;
|
||||
}
|
||||
|
||||
public boolean hasCompletedJoin() {
|
||||
return hasCompletedJoin;
|
||||
}
|
||||
|
||||
public void setHasCompletedJoin(boolean hasCompletedJoin) {
|
||||
this.hasCompletedJoin = hasCompletedJoin;
|
||||
public void completeJoin() {
|
||||
if (!hasCompletedJoin) {
|
||||
hasCompletedJoin = true;
|
||||
if (connectionPhase == BackendConnectionPhases.UNKNOWN) {
|
||||
// Now we know
|
||||
connectionPhase = BackendConnectionPhases.VANILLA;
|
||||
if (connection != null) {
|
||||
connection.setType(ConnectionTypes.VANILLA);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isGracefulDisconnect() {
|
||||
@ -245,4 +248,35 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
return connection != null && !connection.isClosed() && !gracefulDisconnect
|
||||
&& proxyPlayer.isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current "phase" of the connection, mostly used for tracking
|
||||
* modded negotiation for legacy forge servers and provides methods
|
||||
* for performing phase specific actions.
|
||||
*
|
||||
* @return The {@link BackendConnectionPhase}
|
||||
*/
|
||||
public BackendConnectionPhase getPhase() {
|
||||
return connectionPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current "phase" of the connection. See {@link #getPhase()}
|
||||
*
|
||||
* @param connectionPhase The {@link BackendConnectionPhase}
|
||||
*/
|
||||
public void setConnectionPhase(BackendConnectionPhase connectionPhase) {
|
||||
this.connectionPhase = connectionPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the {@link com.velocitypowered.proxy.protocol.packet.JoinGame}
|
||||
* packet has been sent by this server.
|
||||
*
|
||||
* @return Whether the join has been completed.
|
||||
*/
|
||||
public boolean hasCompletedJoin() {
|
||||
return hasCompletedJoin;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -3,15 +3,13 @@ package com.velocitypowered.proxy.connection.client;
|
||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||
import com.velocitypowered.api.event.player.PlayerChatEvent;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.util.ModInfo;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||
import com.velocitypowered.proxy.connection.forge.ForgeConstants;
|
||||
import com.velocitypowered.proxy.connection.forge.ForgeUtil;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.BossBar;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
@ -160,7 +158,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
VelocityServerConnection serverConn = player.getConnectedServer();
|
||||
MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null;
|
||||
if (serverConn != null && backendConn != null) {
|
||||
if (PluginMessageUtil.isRegister(packet)) {
|
||||
if (backendConn.getState() != StateRegistry.PLAY) {
|
||||
logger.warn("Plugin message was sent while the backend was in PLAY. Channel: {}. Packet discarded.");
|
||||
} else if (PluginMessageUtil.isRegister(packet)) {
|
||||
List<String> actuallyRegistered = new ArrayList<>();
|
||||
List<String> channels = PluginMessageUtil.getChannels(packet);
|
||||
for (String channel : channels) {
|
||||
@ -184,33 +184,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
||||
PluginMessage rewritten = PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion());
|
||||
backendConn.write(rewritten);
|
||||
} else if (backendConn.isLegacyForge() && !serverConn.hasCompletedJoin()) {
|
||||
if (packet.getChannel().equals(ForgeConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) {
|
||||
if (!player.getModInfo().isPresent()) {
|
||||
List<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 +220,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.hasCompletedJoin()) {
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
smc.write(packet);
|
||||
}
|
||||
}
|
||||
@ -241,7 +234,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.hasCompletedJoin()) {
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
smc.write(buf.retain());
|
||||
}
|
||||
}
|
||||
@ -289,15 +282,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
spawned = true;
|
||||
player.getConnection().delayedWrite(joinGame);
|
||||
|
||||
// We have something special to do for legacy Forge servers - during first connection the FML
|
||||
// handshake will transition to complete regardless. Thus, we need to ensure that a reset
|
||||
// packet is ALWAYS sent on first switch.
|
||||
//
|
||||
// As we know that calling this branch only happens on first join, we set that if we are a
|
||||
// Forge client that we must reset on the next switch.
|
||||
//
|
||||
// The call will handle if the player is not a Forge player appropriately.
|
||||
player.getConnection().setCanSendLegacyFmlResetPacket(true);
|
||||
// Required for Legacy Forge
|
||||
player.getPhase().onFirstJoin(player);
|
||||
} else {
|
||||
// Clear tab list to avoid duplicate entries
|
||||
player.getTabList().clearAll();
|
||||
@ -359,21 +345,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
// Flush everything
|
||||
player.getConnection().flush();
|
||||
serverMc.flush();
|
||||
serverConn.setHasCompletedJoin(true);
|
||||
if (serverConn.isLegacyForge()) {
|
||||
// We only need to indicate we can send a reset packet if we complete a handshake, that is,
|
||||
// logged onto a Forge server.
|
||||
//
|
||||
// The special case is if we log onto a Vanilla server as our first server, FML will treat
|
||||
// this as complete and **will** need a reset packet sending at some point. We will handle
|
||||
// this during initial player connection if the player is detected to be forge.
|
||||
//
|
||||
// We do not use the result of VelocityServerConnection#isLegacyForge() directly because we
|
||||
// don't want to set it false if this is a first connection to a Vanilla server.
|
||||
//
|
||||
// See LoginSessionHandler#handle for where the counterpart to this method is
|
||||
player.getConnection().setCanSendLegacyFmlResetPacket(true);
|
||||
}
|
||||
serverConn.completeJoin();
|
||||
}
|
||||
|
||||
public List<UUID> getServerBossBars() {
|
||||
@ -401,4 +373,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
player.getConnection().write(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately send any queued messages to the server.
|
||||
*/
|
||||
public void flushQueuedMessages() {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
if (serverConnection != null) {
|
||||
MinecraftConnection connection = serverConnection.getConnection();
|
||||
if (connection != null) {
|
||||
PluginMessage pm;
|
||||
while ((pm = loginPluginMessages.poll()) != null) {
|
||||
connection.write(pm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,12 +30,13 @@ import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||
import com.velocitypowered.proxy.connection.forge.ForgeConstants;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.Chat;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettings;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
import com.velocitypowered.proxy.protocol.packet.KeepAlive;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
|
||||
import com.velocitypowered.proxy.protocol.packet.TitlePacket;
|
||||
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
|
||||
@ -48,6 +49,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import net.kyori.text.Component;
|
||||
import net.kyori.text.TextComponent;
|
||||
import net.kyori.text.TranslatableComponent;
|
||||
@ -79,6 +81,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
private @Nullable ModInfo modInfo;
|
||||
private final VelocityTabList tabList;
|
||||
private final VelocityServer server;
|
||||
private ClientConnectionPhase connectionPhase;
|
||||
|
||||
@MonotonicNonNull
|
||||
private List<String> serversToTry = null;
|
||||
@ -91,6 +94,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
this.connection = connection;
|
||||
this.virtualHost = virtualHost;
|
||||
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
|
||||
this.connectionPhase = connection.getType().getInitialClientPhase();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -139,7 +143,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
return Optional.ofNullable(modInfo);
|
||||
}
|
||||
|
||||
void setModInfo(ModInfo modInfo) {
|
||||
public void setModInfo(ModInfo modInfo) {
|
||||
this.modInfo = modInfo;
|
||||
server.getEventManager().fireAndForget(new PlayerModInfoEvent(this, modInfo));
|
||||
}
|
||||
@ -409,10 +413,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
}
|
||||
|
||||
public void sendLegacyForgeHandshakeResetPacket() {
|
||||
if (connection.canSendLegacyFmlResetPacket()) {
|
||||
connection.write(ForgeConstants.resetPacket());
|
||||
connection.setCanSendLegacyFmlResetPacket(false);
|
||||
}
|
||||
connectionPhase.resetConnectionPhase(this);
|
||||
}
|
||||
|
||||
private MinecraftConnection ensureBackendConnection() {
|
||||
@ -469,6 +470,39 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
|
||||
ensureBackendConnection().write(Chat.createServerbound(input));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a {@link KeepAlive} packet to the player with a random ID.
|
||||
* The response will be ignored by Velocity as it will not match the
|
||||
* ID last sent by the server.
|
||||
*/
|
||||
public void sendKeepAlive() {
|
||||
if (connection.getState() == StateRegistry.PLAY) {
|
||||
KeepAlive keepAlive = new KeepAlive();
|
||||
keepAlive.setRandomId(ThreadLocalRandom.current().nextLong());
|
||||
connection.write(keepAlive);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current "phase" of the connection, mostly used for tracking
|
||||
* modded negotiation for legacy forge servers and provides methods
|
||||
* for performing phase specific actions.
|
||||
*
|
||||
* @return The {@link ClientConnectionPhase}
|
||||
*/
|
||||
public ClientConnectionPhase getPhase() {
|
||||
return connectionPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current "phase" of the connection. See {@link #getPhase()}
|
||||
*
|
||||
* @param connectionPhase The {@link ClientConnectionPhase}
|
||||
*/
|
||||
public void setPhase(ClientConnectionPhase connectionPhase) {
|
||||
this.connectionPhase = connectionPhase;
|
||||
}
|
||||
|
||||
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
||||
|
||||
private final RegisteredServer toConnect;
|
||||
|
@ -10,8 +10,11 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.ConnectionType;
|
||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.packet.Disconnect;
|
||||
@ -95,13 +98,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13).
|
||||
boolean isForge = handshake.getServerAddress().endsWith("\0FML\0");
|
||||
connection.setLegacyForge(isForge);
|
||||
ConnectionType type = checkForForge(handshake);
|
||||
connection.setType(type);
|
||||
|
||||
// Make sure legacy forwarding is not in use on this connection. Make sure that we do _not_
|
||||
// reject Forge.
|
||||
if (handshake.getServerAddress().contains("\0") && !isForge) {
|
||||
// Make sure legacy forwarding is not in use on this connection.
|
||||
if (!type.checkServerAddressIsValid(handshake.getServerAddress())) {
|
||||
connection.closeWith(Disconnect
|
||||
.create(TextComponent.of("Running Velocity behind Velocity is unsupported.")));
|
||||
return true;
|
||||
@ -124,6 +125,18 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private ConnectionType checkForForge(Handshake handshake) {
|
||||
// Determine if we're using Forge (1.8 to 1.12, may not be the case in 1.13).
|
||||
if (handshake.getServerAddress().endsWith(LegacyForgeConstants.HANDSHAKE_HOSTNAME_TOKEN)
|
||||
&& handshake.getProtocolVersion().getProtocol() < ProtocolVersion.MINECRAFT_1_13.getProtocol()) {
|
||||
return ConnectionTypes.LEGACY_FORGE;
|
||||
} else {
|
||||
// For later: See if we can determine Forge 1.13+ here, else this will need to be UNDETERMINED
|
||||
// until later in the cycle (most likely determinable during the LOGIN phase)
|
||||
return ConnectionTypes.VANILLA;
|
||||
}
|
||||
}
|
||||
|
||||
private String cleanVhost(String hostname) {
|
||||
int zeroIdx = hostname.indexOf('\0');
|
||||
return zeroIdx == -1 ? hostname : hostname.substring(0, zeroIdx);
|
||||
|
@ -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.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, ""));
|
||||
}
|
||||
}
|
@ -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,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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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