This commit is contained in:
Adrian Bergqvist 2022-09-29 23:59:32 +02:00
parent 007ff8511a
commit 2a4aba72e9
No known key found for this signature in database
GPG Key ID: FAE7D8EDE225E686
10 changed files with 17 additions and 835 deletions

View File

@ -1,11 +1,8 @@
package org.adde0109.ambassador;
import com.google.inject.Inject;
import com.velocitypowered.api.event.Continuation;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
@ -16,10 +13,6 @@ import java.util.concurrent.Callable;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.network.ConnectionManager;
import io.netty.channel.ChannelInitializer;
import org.adde0109.ambassador.forge.ForgeConnection;
import org.adde0109.ambassador.forge.ForgeHandshakeHandler;
import org.adde0109.ambassador.forge.ForgeHandshakeUtils;
import org.adde0109.ambassador.forge.ForgeServerSwitchHandler;
import org.adde0109.ambassador.velocity.VelocityBackendChannelInitializer;
import org.adde0109.ambassador.velocity.VelocityServerChannelInitializer;
import org.adde0109.ambassador.velocity.VelocityEventHandler;
@ -34,12 +27,9 @@ public class Ambassador {
public ProxyServer server;
public final Logger logger;
public AmbassadorConfig config;
private final Metrics.Factory metricsFactory;
private final Path dataDirectory;
public ForgeHandshakeHandler forgeHandshakeHandler;
public ForgeServerSwitchHandler forgeServerSwitchHandler;
@ -55,18 +45,8 @@ public class Ambassador {
public void onProxyInitialization(ProxyInitializeEvent event) throws ReflectiveOperationException {
initMetrics();
config = AmbassadorConfig.readOrCreateConfig(dataDirectory,server,logger);
if(config != null) {
forgeHandshakeHandler = new ForgeHandshakeHandler(this);
forgeServerSwitchHandler = new ForgeServerSwitchHandler(this);
server.getEventManager().register(this, new VelocityEventHandler(this));
server.getEventManager().register(this,forgeServerSwitchHandler);
}
else {
logger.warn("Ambassador will be disabled because of errors");
}
server.getEventManager().register(this, new VelocityEventHandler(this));
ForgeHandshakeUtils.HandshakeReceiver.logger = logger;
inject();
}
@ -81,39 +61,7 @@ public class Ambassador {
((ConnectionManager) cmField.get(server)).backendChannelInitializer.set(new VelocityBackendChannelInitializer(originalBackend,(VelocityServer) server));
}
@Subscribe
public void onProxyReloadEvent(ProxyReloadEvent event) {
AmbassadorConfig c = AmbassadorConfig.readOrCreateConfig(dataDirectory,server,logger);
if (config != null) {
config = c;
logger.info("Successfully reloaded the config");
} else {
logger.warn("Using the old config");
}
}
@Subscribe
public void onPlayerChooseInitialServerEvent(PlayerChooseInitialServerEvent event, Continuation continuation) {
//Only handle Forge connections
if((event.getInitialServer().isPresent()) && (forgeHandshakeHandler.getForgeConnection(event.getPlayer()).isPresent())) {
//Forge client
ForgeConnection forgeConnection = forgeHandshakeHandler.getForgeConnection(event.getPlayer()).get();
if (forgeConnection.isForced()) {
event.setInitialServer(forgeConnection.getSyncedServer().get());
}
forgeConnection.setForced(config.getForced(forgeConnection.getConnection().getProtocolVersion().getProtocol()));
}
continuation.resume();
}
private void initMetrics() {
Metrics metrics = metricsFactory.make(this, 15655);
metrics.addCustomChart(new SingleLineChart("modern_forge_players", new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return (forgeHandshakeHandler != null) ? forgeHandshakeHandler.getAmountOfForgeConnections() : 0;
}
}));
}
}

View File

@ -1,164 +0,0 @@
package org.adde0109.ambassador;
import com.electronwill.nightconfig.core.CommentedConfig;
import com.electronwill.nightconfig.core.UnmodifiableConfig;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import com.google.common.collect.ImmutableMap;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import org.slf4j.Logger;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class AmbassadorConfig {
private static final int CONFIG_VERSION = 1;
private final ProxyServer server;
private final Logger logger;
private Differentiators differentiatorsSettings;
private ReSync reSyncSettings;
private AmbassadorConfig(ProxyServer server,Logger logger) {
this.server = server;
this.logger = logger;
}
public RegisteredServer getServer(int protocolVersion) {
return differentiatorsSettings.differentiators.get(protocolVersion).handshakeServer;
}
public boolean getForced (int protocolVersion) {
return differentiatorsSettings.differentiators.get(protocolVersion).forced;
}
public boolean shouldHandle(int protocolVersion) {
return differentiatorsSettings.differentiators.containsKey(protocolVersion);
}
public int getReSyncTimeout() {
return reSyncSettings.reSyncTimeout;
}
public reSyncOption reSyncOptionForge() {
return reSyncSettings.reSyncForgeForge;
}
public reSyncOption reSyncOptionVanilla() {
return reSyncSettings.reSyncForgeVanilla;
}
public static AmbassadorConfig readOrCreateConfig(Path dataDirectory,ProxyServer server, Logger logger) {
AmbassadorConfig ambassadorConfig = new AmbassadorConfig(server,logger);
try {
Files.createDirectories(dataDirectory);
}
catch (FileAlreadyExistsException ignored) {
}
catch (IOException e) {
logger.error("Config related error: " + e);
return null;
}
try {
CommentedFileConfig config = CommentedFileConfig.builder(dataDirectory.resolve("ambassador.toml"))
.defaultData(Ambassador.class.getClassLoader().getResource("default-ambassador.toml"))
.autosave()
.preserveInsertionOrder()
.sync()
.build();
config.load();
if (config.getOrElse("config-version",0) != CONFIG_VERSION) {
throw new Exception("Incompatible config-version detected! Please delete 'ambassador.toml' and reload.");
}
CommentedConfig differentiatorsSettingsConfig = config.get("Differentiators");
CommentedConfig reSyncSettingsConfig = config.get("ReSync");
ambassadorConfig.differentiatorsSettings = ambassadorConfig.new Differentiators(differentiatorsSettingsConfig);
ambassadorConfig.reSyncSettings = ambassadorConfig.new ReSync(reSyncSettingsConfig);
config.save();
return ambassadorConfig;
}
catch (Exception e) {
logger.error("Config related error: " + e);
return null;
}
}
private class ReSync {
private int reSyncTimeout = 30;
private reSyncOption reSyncForgeForge = reSyncOption.ALWAYS;
private reSyncOption reSyncForgeVanilla = reSyncOption.NEVER;
private ReSync(CommentedConfig config) {
if (config != null) {
reSyncTimeout = config.getOrElse("resync-timeout",reSyncTimeout);
reSyncForgeForge = reSyncOption.valueOf(
config.getOrElse("resync-forge-to-forge",reSyncForgeForge.name()).toUpperCase());
}
reSyncForgeVanilla = reSyncOption.valueOf(
config.getOrElse("unsync-forge-to-vanilla",reSyncForgeVanilla.name()).toUpperCase());
}
}
public enum reSyncOption {
NEVER,
ASK,
ALWAYS
}
private class Differentiators {
private Map<Integer,DifferentiatorSettings> differentiators = ImmutableMap.of(
758, new DifferentiatorSettings(),
754, new DifferentiatorSettings()
);
private Differentiators(){
}
private Differentiators(CommentedConfig config) throws Exception {
if (config != null) {
Map<Integer,DifferentiatorSettings> differentiators = new HashMap<>();
for (UnmodifiableConfig.Entry entry : config.entrySet()) {
if (entry.getValue() instanceof CommentedConfig) {
differentiators.put(Integer.decode(entry.getKey()),new DifferentiatorSettings(entry.getValue()));
}
}
this.differentiators = ImmutableMap.copyOf(differentiators);
}
}
}
private class DifferentiatorSettings {
private RegisteredServer handshakeServer = null;
private boolean forced = false;
private DifferentiatorSettings(){
}
private DifferentiatorSettings(CommentedConfig config) throws Exception {
if (config != null) {
String serverName = config.getOrElse("default-forge-server", "");
if (!Objects.equals(serverName, ""))
handshakeServer = server.getServer(serverName)
.orElseThrow(() -> new Exception(serverName + "is not a registered server!"));
this.forced = config.getOrElse("forced",forced);
}
}
}
}

View File

@ -1,168 +0,0 @@
package org.adde0109.ambassador.forge;
import com.google.common.io.ByteArrayDataInput;
import com.velocitypowered.api.event.Continuation;
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
import com.velocitypowered.api.proxy.LoginPhaseConnection;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.slf4j.Logger;
import java.io.EOFException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
public class ForgeConnection {
private final Logger logger;
private final LoginPhaseConnection connection;
private Optional<byte[]> recivedClientModlist = Optional.empty();
private static byte[] recivedClientACK;
private boolean ignoreSyncExepction = false;
private Optional<ForgeHandshakeUtils.CachedServerHandshake> transmittedHandshake = Optional.empty();
private boolean forced = false;
private Optional<RegisteredServer> syncedTo = Optional.empty();
private boolean resettable;
public ForgeConnection(LoginPhaseConnection connection, Logger logger) {
this.connection = connection;
this.logger = logger;
}
public CompletableFuture<Boolean> testIfForge(LoginPhaseConnection connection) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
byte[] testPacket = ForgeHandshakeUtils.generateTestPacket();
//This gets also sent to vanilla
connection.sendLoginPluginMessage(MinecraftChannelIdentifier.create("fml", "loginwrapper"), testPacket,
responseBody -> {
future.complete(responseBody != null);
ignoreSyncExepction = responseBody == null;
});
return future;
}
public CompletableFuture<Boolean> sync(ForgeServerConnection forgeServerConnection) {
CompletableFuture<Boolean> future = new CompletableFuture<>();
forgeServerConnection.getHandshake().whenComplete((msg,ex) -> {
if (ex != null) {
future.complete(false);
logger.warn("Sync Exception: " + ex);
} else {
//This gets also sent to vanilla
sendModlist(msg.modListPacket).thenAccept((response) -> {
if (!ignoreSyncExepction && response == null) {
logger.warn("Sync Exception: Client responded with an empty body.");
}
if (response != null) {
recivedClientModlist = Optional.of(response);
//If the client is resettable.
ByteBuf clientModListPacket = Unpooled.wrappedBuffer(response);
clientModListPacket.readBytes(14); //Channel Identifier
ProtocolUtils.readVarInt(clientModListPacket); //Length
int packetID = ProtocolUtils.readVarInt(clientModListPacket);
String[] mods = ProtocolUtils.readStringArray(clientModListPacket);
resettable = Arrays.stream(mods).anyMatch((s) -> s.equals("clientresetpacket"));
clientModListPacket.release();
}
});
//This gets also sent to vanilla
sendOther(msg.otherPackets).thenAccept((response) -> {
if (!ignoreSyncExepction && response == null) {
logger.warn("Sync Exception: Client responded with an empty body.");
}
//TODO: Generate the ACK packet ourself.
ForgeConnection.recivedClientACK = (response == null) ? ForgeConnection.recivedClientACK : response;
transmittedHandshake = Optional.of(msg);
syncedTo = Optional.of(forgeServerConnection.getServer());
});
future.complete(true);
}
});
return future;
}
private CompletableFuture<byte[]> sendModlist(byte[] modListPacket) {
CompletableFuture<byte[]> future = new CompletableFuture<>();
connection.sendLoginPluginMessage(MinecraftChannelIdentifier.create("fml", "loginwrapper"), modListPacket,
future::complete);
return future;
}
private CompletableFuture<byte[]> sendOther(List<byte[]> otherPackets) {
CompletableFuture<byte[]> future = new CompletableFuture<>();
for (int i = 0; i < otherPackets.size(); i++) {
connection.sendLoginPluginMessage(MinecraftChannelIdentifier.create("fml", "loginwrapper"), otherPackets.get(i),
(i < (otherPackets.size() - 1)) ? responseBody -> {
} : future::complete);
}
return future;
}
public void handleServerHandshakePacket(ServerLoginPluginMessageEvent event, Continuation continuation) {
ByteArrayDataInput data = event.contentsAsDataStream();
if (data.skipBytes(14) != 14) { //Channel Identifier
continuation.resumeWithException(new EOFException());
return;
}
ForgeHandshakeUtils.readVarInt(data); //Length
int packetID = ForgeHandshakeUtils.readVarInt(data);
if (packetID == 1) {
if (getRecivedClientModlist().isPresent()) {
event.setResult(ServerLoginPluginMessageEvent.ResponseResult.reply(getRecivedClientModlist().get()));
} else {
continuation.resumeWithException(new Exception("Client isn't synced. This should have been caught" +
" during serverPreConnect"));
return;
}
} else {
if (getRecivedClientACK() != null) {
event.setResult(ServerLoginPluginMessageEvent.ResponseResult.reply(getRecivedClientACK()));
} else {
continuation.resumeWithException(new Exception("No response available."));
return;
}
}
continuation.resume();
}
public LoginPhaseConnection getConnection() {
return connection;
}
public boolean isResettable() {
return resettable;
}
public Optional<ForgeHandshakeUtils.CachedServerHandshake> getTransmittedHandshake() {
return transmittedHandshake;
}
public Optional<byte[]> getRecivedClientModlist() {
return recivedClientModlist;
}
public static byte[] getRecivedClientACK() {
return recivedClientACK;
}
public Optional<RegisteredServer> getSyncedServer() {
return syncedTo;
}
public void setForced(boolean forced) {
this.forced = forced;
}
public boolean isForced() {
return forced;
}
}

View File

@ -19,6 +19,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import net.kyori.adventure.text.Component;
import org.adde0109.ambassador.velocity.VelocityForgeClientConnectionPhase;
import org.adde0109.ambassador.velocity.VelocityForgeHandshakeSessionHandler;
import org.checkerframework.checker.units.qual.A;
@ -45,13 +46,10 @@ public class ForgeFML2ClientConnectionPhase implements VelocityForgeClientConnec
public void handleLogin(ConnectedPlayer player,ForgeHandshakeUtils.CachedServerHandshake handshake, Continuation continuation) {
final MinecraftConnection connection = player.getConnection();
VelocityForgeHandshakeSessionHandler sessionHandler = new VelocityForgeHandshakeSessionHandler(connection.getSessionHandler(), player);
if (handshake == null) {
connection.delayedWrite(new LoginPluginMessage(98, "fml:loginwrapper", Unpooled.wrappedBuffer(ForgeHandshakeUtils.generateResetPacket())));
listenerList.add(98);
this.whenComplete = () -> {
this.clientPhase = ClientPhase.VANILLA;
continuation.resume();
};
//Without initial modlist for now
if (true) {
connection.delayedWrite(new LoginPluginMessage(0, "fml:loginwrapper", Unpooled.wrappedBuffer(ForgeHandshakeUtils.emptyModlist)));
listenerList.add(0);
} else {
connection.delayedWrite(new LoginPluginMessage(0, "fml:loginwrapper", Unpooled.wrappedBuffer(handshake.modListPacket)));
listenerList.add(0);
@ -59,11 +57,11 @@ public class ForgeFML2ClientConnectionPhase implements VelocityForgeClientConnec
connection.delayedWrite(new LoginPluginMessage(i + 1, "fml:loginwrapper", Unpooled.wrappedBuffer(handshake.otherPackets.get(i))));
listenerList.add(i + 1);
}
this.whenComplete = () -> {
this.clientPhase = ClientPhase.MODDED;
continuation.resume();
};
}
this.whenComplete = () -> {
this.clientPhase = ClientPhase.MODDED;
continuation.resume();
};
connection.setSessionHandler(sessionHandler);
connection.flush();
}
@ -72,9 +70,11 @@ public class ForgeFML2ClientConnectionPhase implements VelocityForgeClientConnec
public boolean handle(ConnectedPlayer player, LoginPluginResponse packet) {
if (packet.getId() == 98) {
this.clientPhase = ClientPhase.HANDSHAKE;
this.isResettable = packet.isSuccess();
} else if (packet.getId() == 0) {
this.clientPhase = ClientPhase.MODLIST;
if (modListData == null) {
modListData = ByteBufUtil.getBytes(packet.content());
}
}
if (!listenerList.removeIf(id -> id.equals(packet.getId()))) {
player.getConnectionInFlight().getConnection().write(packet.retain());

View File

@ -1,144 +0,0 @@
package org.adde0109.ambassador.forge;
import com.velocitypowered.api.event.Continuation;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.player.KickedFromServerEvent;
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import org.adde0109.ambassador.Ambassador;
import org.adde0109.ambassador.velocity.VelocityForgeClientConnectionPhase;
public class ForgeHandshakeHandler {
private final Ambassador ambassador;
private final Map<RegisteredServer, ForgeServerConnection>
forgeServerConnectionMap = new HashMap<>();
private final Map<InetSocketAddress,ForgeConnection> incomingForgeConnections = new HashMap<>();
private static final ChannelIdentifier LOGIN_WRAPPER_ID = MinecraftChannelIdentifier.create("fml","loginwrapper");
public ForgeHandshakeHandler(Ambassador ambassador) {
this.ambassador = ambassador;
}
public void handleLogin(ConnectedPlayer player, Continuation continuation) {
getInitialHandshake(player).whenCompleteAsync((msg,ex) -> {
if (ex != null) {
ambassador.logger.warn("Forge player, " + player.getUsername() + ", is entering vanilla-mode because of: " + ex.getMessage());
}
((VelocityForgeClientConnectionPhase) player.getPhase()).handleLogin(player,msg,continuation);
}, player.getConnection().eventLoop());
}
private CompletableFuture<ForgeHandshakeUtils.CachedServerHandshake> getInitialHandshake(ConnectedPlayer player) {
CompletableFuture<ForgeHandshakeUtils.CachedServerHandshake> future;
RegisteredServer initialServer;
if((initialServer = ambassador.config.getServer(player.getConnection().getProtocolVersion().getProtocol())) != null) {
future = ForgeHandshakeUtils.HandshakeReceiver.downloadHandshake(initialServer);
} else {
future = CompletableFuture.failedFuture(new Exception("No initial server is specified"));
}
return future;
}
private void registerForgeConnection(ForgeConnection forgeConnection) {
if (forgeConnection != null) {
incomingForgeConnections.values().removeIf((c) -> !c.getConnection().isActive());
incomingForgeConnections.put(forgeConnection.getConnection().getRemoteAddress(), forgeConnection);
}
}
public Optional<ForgeConnection> getForgeConnection(Player player) {
return getForgeConnection(player.getRemoteAddress());
}
private Optional<ForgeConnection> getForgeConnection(InetSocketAddress socketAddress) {
incomingForgeConnections.values().removeIf((c) -> !c.getConnection().isActive());
return Optional.ofNullable(incomingForgeConnections.get(socketAddress));
}
public int getAmountOfForgeConnections() {
return incomingForgeConnections.size();
}
public Optional<ForgeServerConnection> getForgeServerConnection(RegisteredServer registeredServer) {
return Optional.ofNullable(forgeServerConnectionMap.get(registeredServer));
}
public void registerForgeServer(RegisteredServer server, ForgeServerConnection forgeServerConnection) {
forgeServerConnectionMap.put(server,forgeServerConnection);
}
public void unRegisterForgeServer(RegisteredServer server) {
forgeServerConnectionMap.remove(server);
}
//@Subscribe
public void onServerLoginPluginMessageEvent(ServerLoginPluginMessageEvent event, Continuation continuation) {
if (!event.getIdentifier().equals(LOGIN_WRAPPER_ID)) {
continuation.resume();
return;
}
//Check 2
if (getForgeServerConnection(event.getConnection().getServer()).isEmpty()) {
registerForgeServer(event.getConnection().getServer(),
new ForgeServerConnection(event.getConnection().getServer()));
}
if (incomingForgeConnections.containsKey(event.getConnection().getPlayer().getRemoteAddress())) {
incomingForgeConnections.get(event.getConnection().getPlayer().getRemoteAddress())
.handleServerHandshakePacket(event,continuation);
} else {
//This will lead to "multiplayer.disconnect.unexpected_query_response"
//and will be handled during KickFromServerEvent.
continuation.resume();
}
}
//@Subscribe
public void onKickedFromServerEvent(KickedFromServerEvent event, Continuation continuation) {
Optional<ForgeConnection> forgeConnectionOptional = getForgeConnection(event.getPlayer());
if (forgeConnectionOptional.isPresent()) {
if (forgeConnectionOptional.get().isForced() && event.getResult() instanceof KickedFromServerEvent.RedirectPlayer) {
event.setResult(KickedFromServerEvent.DisconnectPlayer.create(event.getServerKickReason().get()));
}
} else if (event.getServerKickReason().isPresent()) {
Component reason = event.getServerKickReason().get();
if (reason instanceof TranslatableComponent)
if (((TranslatableComponent) reason).key().equals("multiplayer.disconnect.unexpected_query_response")) {
if (getForgeServerConnection(event.getServer()).isPresent()) {
//Turns out the server the vanilla client is connecting to is forge. Let's handle the connection error.
ambassador.logger.info("Vanilla player {} tried to connect to forge server {}. The connection error can be ignored.",
event.getPlayer(),event.getServer().getServerInfo().getName());
KickedFromServerEvent.ServerKickResult result = event.getResult();
Component component = Component.text("The server you were trying to connect to requires Forge to be installed.", NamedTextColor.RED);
if (result instanceof KickedFromServerEvent.DisconnectPlayer) {
event.setResult(KickedFromServerEvent.DisconnectPlayer.create(component));
} else if (result instanceof KickedFromServerEvent.RedirectPlayer) {
RegisteredServer redirectServer = ((KickedFromServerEvent.RedirectPlayer)event.getResult()).getServer();
event.setResult(KickedFromServerEvent.RedirectPlayer.create(redirectServer,component));
} else if (result instanceof KickedFromServerEvent.Notify) {
event.setResult(KickedFromServerEvent.Notify.create(component));
}
}
}
}
continuation.resume();
}
}

View File

@ -1,36 +0,0 @@
package org.adde0109.ambassador.forge;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo;
import java.util.concurrent.CompletableFuture;
public class ForgeServerConnection {
private static final int PACKET_LENGTH_INDEX = 14; //length of "fml:handshake"+1
private final RegisteredServer handshakeServer;
private ForgeHandshakeUtils.CachedServerHandshake handshake;
public RegisteredServer getServer() {
return handshakeServer;
}
public ForgeServerConnection(RegisteredServer handshakeServer) {
this.handshakeServer = handshakeServer;
}
public CompletableFuture<ForgeHandshakeUtils.CachedServerHandshake> getHandshake() {
CompletableFuture<ForgeHandshakeUtils.CachedServerHandshake> future;
if (handshake == null) {
future = ForgeHandshakeUtils.HandshakeReceiver.downloadHandshake(handshakeServer);
} else {
future = ForgeHandshakeUtils.HandshakeReceiver.downloadHandshake(handshakeServer,handshake);
}
future.thenAccept(p -> handshake = p);
return future;
}
public ServerInfo getServerInfo() {
return handshakeServer.getServerInfo();
}
}

View File

@ -1,217 +0,0 @@
package org.adde0109.ambassador.forge;
import com.velocitypowered.api.event.Continuation;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.UuidUtils;
import com.velocitypowered.proxy.config.PlayerInfoForwarding;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.concurrent.*;
import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage;
import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse;
import com.velocitypowered.proxy.protocol.packet.PluginMessage;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.EventLoop;
import io.netty.util.concurrent.GenericFutureListener;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.adde0109.ambassador.Ambassador;
import org.adde0109.ambassador.AmbassadorConfig;
import org.apache.commons.collections4.map.PassiveExpiringMap;
public class ForgeServerSwitchHandler {
private final Ambassador ambassador;
public final PassiveExpiringMap<String,ForgeServerConnection> reSyncMap;
public ForgeServerSwitchHandler(Ambassador ambassador) {
this.ambassador = ambassador;
this.reSyncMap = new PassiveExpiringMap<>(ambassador.config.getReSyncTimeout(),TimeUnit.SECONDS);
}
//@Subscribe(order = PostOrder.LAST)
public void onServerPreConnectEvent(ServerPreConnectEvent event, Continuation continuation) {
if (!event.getResult().isAllowed()) {
continuation.resume();
return;
}
Optional<ForgeServerConnection> forgeServerConnectionOptional = ambassador.forgeHandshakeHandler.getForgeServerConnection(event.getOriginalServer());
Optional<ForgeConnection> forgeConnectionOptional = ambassador.forgeHandshakeHandler.getForgeConnection(event.getPlayer());
if (forgeConnectionOptional.isPresent()) {
ForgeConnection forgeConnection = forgeConnectionOptional.get();
if (forgeConnection.isResettable()) {
continuation.resume();
return;
}
ForgeServerConnection forgeServerConnection = forgeServerConnectionOptional.orElseGet(() -> new ForgeServerConnection(event.getOriginalServer()));
forgeServerConnection.getHandshake().whenComplete((msg, ex) -> {
if (ex != null) {
//The server was forge but aren't right now. Or it's just offline.
if (ex instanceof ForgeHandshakeUtils.HandshakeReceiver.HandshakeNotAvailableException) {
//It's not running ambassador, so it should be unregistered.
if (forgeServerConnectionOptional.isPresent())
ambassador.forgeHandshakeHandler.unRegisterForgeServer(forgeServerConnection.getServer());
}
} else {
//If the server just got discovered, register it.
if (forgeServerConnectionOptional.isEmpty())
ambassador.forgeHandshakeHandler.registerForgeServer(event.getOriginalServer(),forgeServerConnection);
//To make legacy forwarding work
List<GameProfile.Property> properties = new ArrayList<>(event.getPlayer().getGameProfileProperties());
properties.add(new GameProfile.Property("extraData", "\1FML2\1",""));
event.getPlayer().setGameProfileProperties(properties);
if (ambassador.config.reSyncOptionForge() != AmbassadorConfig.reSyncOption.NEVER) {
if (forgeConnection.getTransmittedHandshake().isEmpty() || !msg.equals(forgeConnection.getTransmittedHandshake().get())) {
event.setResult(ServerPreConnectEvent.ServerResult.denied());
kickReSync(event.getPlayer(), forgeServerConnection);
}
}
}
continuation.resume();
});
} else if (forgeServerConnectionOptional.isPresent()) {
//If vanilla tries to connect to a server we know is forge
event.setResult(ServerPreConnectEvent.ServerResult.denied());
event.getPlayer().sendMessage(Component.text("This server requires Forge!", NamedTextColor.RED));
continuation.resume();
} else {
//The server is not known to us.
continuation.resume();
}
}
private void kickReSync(Player player, ForgeServerConnection forgeServerConnection){
ambassador.logger.info("Kicking {} because of re-sync needed", player);
player.disconnect(Component.text("Please reconnect"));
reSyncMap.put(player.getUsername(),forgeServerConnection);
}
//@Subscribe
public void onServerConnectedEvent(ServerConnectedEvent event, Continuation continuation) {
ConnectedPlayer player = ((ConnectedPlayer) event.getPlayer());
Optional<ForgeConnection> forgeConnection = ambassador.forgeHandshakeHandler.getForgeConnection(player);
if (forgeConnection.isEmpty() || !forgeConnection.get().isResettable()) {
//Don't bother unless the client can be reset.
continuation.resume();
return;
}
Optional<ForgeServerConnection> forgeServerConnection = ambassador.forgeHandshakeHandler.getForgeServerConnection(event.getServer());
if (forgeServerConnection.isPresent() && event.getPreviousServer().isPresent()) {
Future<ForgeHandshakeUtils.CachedServerHandshake> handshakeFuture = forgeServerConnection.get().getHandshake();
player.getConnection().eventLoop().submit(() -> {
reSync(player,handshakeFuture,continuation);
});
} else {
continuation.resume();
}
}
private void reSync(ConnectedPlayer player, Future<ForgeHandshakeUtils.CachedServerHandshake> handshakeFuture, Continuation continuation) {
MinecraftConnection connection = player.getConnection();
connection.setSessionHandler(new ReSyncHandler(player,handshakeFuture,continuation));
connection.write(new PluginMessage("fml:handshake", Unpooled.wrappedBuffer(ForgeHandshakeUtils.generateResetPacket())));
connection.setState(StateRegistry.LOGIN);
}
private class ReSyncHandler implements MinecraftSessionHandler {
private final ConnectedPlayer player;
private final MinecraftConnection connection;
private final Future<ForgeHandshakeUtils.CachedServerHandshake> handshakeFuture;
private final MinecraftSessionHandler originalHandler;
private final Continuation continuation;
private List<Integer> inTransit = new ArrayList<>();
ReSyncHandler(ConnectedPlayer player, Future<ForgeHandshakeUtils.CachedServerHandshake> handshakeFuture, Continuation continuation) {
this.player = player;
this.connection = player.getConnection();
this.handshakeFuture = handshakeFuture;
this.originalHandler = this.connection.getSessionHandler();
this.continuation = continuation;
}
@Override
public boolean handle(LoginPluginResponse packet) {
if (!inTransit.removeIf((s) -> s == packet.getId())) {
if (packet.getId() == 98) {
ForgeHandshakeUtils.CachedServerHandshake handshake;
try {
handshake = handshakeFuture.get();
} catch (Exception e) {
return true;
}
sendHandshake(connection, handshake);
return true;
}
return false;
} else if (inTransit.isEmpty()) {
complete();
}
return true;
}
private void sendHandshake(MinecraftConnection connection, ForgeHandshakeUtils.CachedServerHandshake handshake) {
int transactionId = 1;
connection.delayedWrite(new LoginPluginMessage(transactionId, "fml:loginwrapper", Unpooled.wrappedBuffer(handshake.modListPacket)));
inTransit.add(transactionId);
for (byte[] data : handshake.otherPackets) {
transactionId++;
connection.delayedWrite(new LoginPluginMessage(transactionId, "fml:loginwrapper", Unpooled.wrappedBuffer(data)));
inTransit.add(transactionId);
}
connection.flush();
}
private void complete() {
VelocityConfiguration configuration = (VelocityConfiguration) ambassador.server.getConfiguration();
UUID playerUniqueId = player.getUniqueId();
if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.NONE) {
playerUniqueId = UuidUtils.generateOfflinePlayerUuid(player.getUsername());
}
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(player.getUsername());
success.setUuid(playerUniqueId);
connection.write(success);
connection.setState(StateRegistry.PLAY);
connection.setSessionHandler(originalHandler);
continuation.resume();
}
@Override
public void handleUnknown(ByteBuf buf) {
originalHandler.handleUnknown(buf);
}
@Override
public void disconnected() {
originalHandler.disconnected();
}
}
}

View File

@ -36,7 +36,7 @@ public class VelocityEventHandler {
continuation.resume();
return;
}
ambassador.forgeHandshakeHandler.handleLogin(player,continuation);
phase.handleLogin(player,null,continuation);
}
/*@Subscribe

View File

@ -13,7 +13,7 @@ import java.util.List;
public class VelocityForgeBackendConnectionPhase implements BackendConnectionPhase {
private final List<LoginPluginMessage> queuedHandshakePackets = new ArrayList<>();
private List<LoginPluginMessage> queuedHandshakePackets = new ArrayList<>();
public VelocityForgeBackendConnectionPhase() {
}
@ -34,7 +34,9 @@ public class VelocityForgeBackendConnectionPhase implements BackendConnectionPha
player.getConnection().delayedWrite(msg);
}
player.getConnection().flush();
queuedHandshakePackets = null;
});
queuedHandshakePackets = new ArrayList<>();
queuedHandshakePackets.add(message);
} else if (clientPhase.clientPhase != null) {
player.getConnection().write(message);

View File

@ -1,39 +0,0 @@
config-version = 1
# The player can only be synced to ONE forge server at a time.
# Resync will always happen if the player isn't synced to any server and is switching to a forge server.
[ReSync]
# In seconds. How much time the player has to reconnect before the resync cancels.
resync-timeout = 30
# Possible values: "Always", "Never".
# Always: Always kicks the player while switching servers. The safest.
# Never: Switches server without kicking/resyncing the player. Can cause client crashes depending on the mods.
# When the player is connecting to a forge server while being synced to another.
# Default: "always"
resync-forge-to-forge = "always"
# When the player is connecting to a vanilla server and is synced to a forge server.
# Default "never". "always" not recommended for servers with vanilla hubs/lobbies.
unsync-forge-to-vanilla = "never"
# Maybe you want to have one 1.16.5 modpack-server and one 1.18.2 modpack-server behind Velocity, in order for Ambassador to tell the
# diffrence between modpacks on the connecting client, the plugin looks at the client's protocol version.
# You may add more diffrentiators, just make sure they have diffrent protocol versions.
[Differentiators]
# Protocol version - 1.18.2
[Differentiators.758]
# The server that players will initially sync to when connecting to the proxy.
# When left empty; Players will be able to join but will need to resync when joining connecting to a forge server.
# The name should be the same as specified in the "velocity.toml" config, e.g. "lobby".
default-forge-server = ""
# Some modpacks are not compatible with vanilla servers.
# To make sure they don't get connected to one upon login (like a vanilla lobby), change this to true.
forced = false
# Protocol version - 1.16.5
[Differentiators.754]
default-forge-server = ""
forced = false