From 7edff139cc10364a3f106fd1dd7677cdfd2ea42c Mon Sep 17 00:00:00 2001 From: Tim203 Date: Sat, 22 Feb 2020 16:32:20 +0100 Subject: [PATCH] Adds experimental Velocity support, fixed bugs and changed some class names --- bukkit/pom.xml | 4 +- .../floodgate/BukkitFloodgateAPI.java | 14 -- .../org/geysermc/floodgate/BukkitPlugin.java | 2 +- .../org/geysermc/floodgate/FloodgateAPI.java | 21 +++ .../org/geysermc/floodgate/PacketHandler.java | 89 ++++----- .../floodgate/injector/BukkitInjector.java | 3 +- bukkit/src/main/resources/plugin.yml | 2 + bungee/pom.xml | 9 +- .../org/geysermc/floodgate/BungeePlugin.java | 114 +++++------- ...geeFloodgateAPI.java => FloodgateAPI.java} | 6 +- bungee/src/main/resources/config.yml | 6 +- bungee/src/main/resources/plugin.yml | 1 + ...gateAPI.java => AbstractFloodgateAPI.java} | 27 +-- .../geysermc/floodgate/FloodgateConfig.java | 5 +- .../geysermc/floodgate/FloodgatePlayer.java | 7 +- .../geysermc/floodgate/HandshakeHandler.java | 76 ++++++++ .../org/geysermc/floodgate/util/DeviceOS.java | 27 +++ .../floodgate/util/ReflectionUtil.java | 43 +++-- common/src/main/resources/config.yml | 4 +- pom.xml | 41 +--- velocity/pom.xml | 87 +++++++++ .../org/geysermc/floodgate/FloodgateAPI.java | 37 ++++ .../floodgate/VelocityFloodgateConfig.java | 10 + .../geysermc/floodgate/VelocityPlugin.java | 175 ++++++++++++++++++ .../floodgate/injector/VelocityInjector.java | 105 +++++++++++ .../src/main/resources/velocity-plugin.json | 1 + 26 files changed, 702 insertions(+), 214 deletions(-) delete mode 100644 bukkit/src/main/java/org/geysermc/floodgate/BukkitFloodgateAPI.java create mode 100644 bukkit/src/main/java/org/geysermc/floodgate/FloodgateAPI.java rename bungee/src/main/java/org/geysermc/floodgate/{BungeeFloodgateAPI.java => FloodgateAPI.java} (80%) rename common/src/main/java/org/geysermc/floodgate/{FloodgateAPI.java => AbstractFloodgateAPI.java} (64%) create mode 100644 common/src/main/java/org/geysermc/floodgate/HandshakeHandler.java create mode 100644 common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java create mode 100644 velocity/pom.xml create mode 100644 velocity/src/main/java/org/geysermc/floodgate/FloodgateAPI.java create mode 100644 velocity/src/main/java/org/geysermc/floodgate/VelocityFloodgateConfig.java create mode 100644 velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java create mode 100644 velocity/src/main/java/org/geysermc/floodgate/injector/VelocityInjector.java create mode 100644 velocity/src/main/resources/velocity-plugin.json diff --git a/bukkit/pom.xml b/bukkit/pom.xml index aa5cc27e..da5934f6 100644 --- a/bukkit/pom.xml +++ b/bukkit/pom.xml @@ -3,8 +3,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - floodgate-parent org.geysermc + floodgate-parent 1.0-SNAPSHOT 4.0.0 @@ -22,7 +22,7 @@ org.bukkit bukkit - 1.8.8-R0.1-SNAPSHOT + ${bukkit-version} provided diff --git a/bukkit/src/main/java/org/geysermc/floodgate/BukkitFloodgateAPI.java b/bukkit/src/main/java/org/geysermc/floodgate/BukkitFloodgateAPI.java deleted file mode 100644 index 6ed791f6..00000000 --- a/bukkit/src/main/java/org/geysermc/floodgate/BukkitFloodgateAPI.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.geysermc.floodgate; - -import org.bukkit.entity.Player; - -import java.util.UUID; - -public class BukkitFloodgateAPI extends FloodgateAPI { - /** - * See {@link FloodgateAPI#getPlayer(UUID)} - */ - public static FloodgatePlayer getPlayer(Player player) { - return getPlayer(player.getUniqueId()); - } -} diff --git a/bukkit/src/main/java/org/geysermc/floodgate/BukkitPlugin.java b/bukkit/src/main/java/org/geysermc/floodgate/BukkitPlugin.java index 85bbcc05..7fcfa934 100644 --- a/bukkit/src/main/java/org/geysermc/floodgate/BukkitPlugin.java +++ b/bukkit/src/main/java/org/geysermc/floodgate/BukkitPlugin.java @@ -17,7 +17,7 @@ public class BukkitPlugin extends JavaPlugin { if (!getDataFolder().exists()) { getDataFolder().mkdir(); } - ReflectionUtil.setServerVersion(getServer().getClass().getPackage().getName().split("\\.")[3]); + ReflectionUtil.setPrefix("net.minecraft.server." + getServer().getClass().getPackage().getName().split("\\.")[3]); configuration = FloodgateConfig.load(getLogger(), getDataFolder().toPath().resolve("config.yml")); } diff --git a/bukkit/src/main/java/org/geysermc/floodgate/FloodgateAPI.java b/bukkit/src/main/java/org/geysermc/floodgate/FloodgateAPI.java new file mode 100644 index 00000000..28386a99 --- /dev/null +++ b/bukkit/src/main/java/org/geysermc/floodgate/FloodgateAPI.java @@ -0,0 +1,21 @@ +package org.geysermc.floodgate; + +import org.bukkit.entity.Player; + +import java.util.UUID; + +public class FloodgateAPI extends AbstractFloodgateAPI { + /** + * See {@link AbstractFloodgateAPI#getPlayer(UUID)} + */ + public static FloodgatePlayer getPlayer(Player player) { + return getPlayer(player.getUniqueId()); + } + + /** + * See {@link AbstractFloodgateAPI#isBedrockPlayer(UUID)} + */ + public static boolean isBedrockPlayer(Player player) { + return isBedrockPlayer(player.getUniqueId()); + } +} diff --git a/bukkit/src/main/java/org/geysermc/floodgate/PacketHandler.java b/bukkit/src/main/java/org/geysermc/floodgate/PacketHandler.java index 84017ea4..e0f0b4fa 100644 --- a/bukkit/src/main/java/org/geysermc/floodgate/PacketHandler.java +++ b/bukkit/src/main/java/org/geysermc/floodgate/PacketHandler.java @@ -4,9 +4,9 @@ import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; import lombok.RequiredArgsConstructor; +import org.geysermc.floodgate.HandshakeHandler.HandshakeResult; import org.geysermc.floodgate.injector.BukkitInjector; import org.geysermc.floodgate.util.BedrockData; -import org.geysermc.floodgate.util.EncryptionUtil; import org.geysermc.floodgate.util.ReflectionUtil; import java.lang.reflect.Constructor; @@ -17,12 +17,13 @@ import java.net.SocketAddress; import java.util.List; import java.util.UUID; -import static org.geysermc.floodgate.util.BedrockData.FLOODGATE_IDENTIFIER; import static org.geysermc.floodgate.util.ReflectionUtil.*; @RequiredArgsConstructor public class PacketHandler extends MessageToMessageDecoder { private static BukkitPlugin plugin = BukkitPlugin.getInstance(); + private static HandshakeHandler handshakeHandler; + private static Class networkManagerClass; private static Class handshakePacketClass; private static Field hostField; @@ -47,54 +48,52 @@ public class PacketHandler extends MessageToMessageDecoder { private final ChannelFuture future; private Object networkManager; - private FloodgatePlayer floodgatePlayer; + private FloodgatePlayer fPlayer; private boolean bungee; @Override protected void decode(ChannelHandlerContext ctx, Object packet, List out) throws Exception { boolean isHandhake = handshakePacketClass.isInstance(packet); boolean isLogin = loginStartPacketClass.isInstance(packet); + try { if (isHandhake) { networkManager = ctx.channel().pipeline().get("packet_handler"); - String[] host = getCastedValue(packet, hostField, String.class).split("\0"); - bungee = host.length == 6 || host.length == 7; // 4 = normal | 6, 7 = bungee - - if ((host.length == 4 || bungee) && host[1].equals(FLOODGATE_IDENTIFIER)) { - BedrockData bedrockData = EncryptionUtil.decryptBedrockData( - plugin.getConfiguration().getPrivateKey(), - host[2] + '\0' + host[3] - ); - - if (bedrockData.getDataLength() != BedrockData.EXPECTED_LENGTH) { + HandshakeResult result = handshakeHandler.handle(getCastedValue(packet, hostField, String.class)); + switch (result.getResultType()) { + case SUCCESS: + break; + case INVALID_DATA_LENGTH: plugin.getLogger().info(String.format( plugin.getConfiguration().getMessages().getInvalidArgumentsLength(), - BedrockData.EXPECTED_LENGTH, bedrockData.getDataLength() + BedrockData.EXPECTED_LENGTH, result.getBedrockData().getDataLength() )); - ctx.close(); //todo add option to see disconnect message? - } - - FloodgatePlayer player = floodgatePlayer = new FloodgatePlayer(bedrockData); - FloodgateAPI.players.put(player.getJavaUniqueId(), player); - - if (bungee) { - setValue(packet, hostField, host[0] + '\0' + - bedrockData.getIp() + '\0' + player.getJavaUniqueId() + - (host.length == 7 ? '\0' + host[6] : "") - ); - } else { - // Use a spoofedUUID for initUUID (just like Bungeecord) - setValue(networkManager, "spoofedUUID", player.getJavaUniqueId()); - // Use the player his IP for stuff instead of Geyser his IP - SocketAddress newAddress = new InetSocketAddress( - bedrockData.getIp(), - ((InetSocketAddress) ctx.channel().remoteAddress()).getPort() - ); - setValue(networkManager, getFieldOfType(networkManagerClass, SocketAddress.class, false), newAddress); - } - plugin.getLogger().info("Added " + player.getJavaUsername() + " " + player.getJavaUniqueId()); + ctx.close(); + default: // only continue when SUCCESS + return; } + + fPlayer = result.getFloodgatePlayer(); + BedrockData bedrockData = result.getBedrockData(); + String[] data = result.getHandshakeData(); + + if (bungee = (data.length == 6 || data.length == 7)) { + setValue(packet, hostField, data[0] + '\0' + + bedrockData.getIp() + '\0' + fPlayer.getJavaUniqueId() + + (data.length == 7 ? '\0' + data[6] : "") + ); + } else { + // Use a spoofedUUID for initUUID (just like Bungeecord) + setValue(networkManager, "spoofedUUID", fPlayer.getJavaUniqueId()); + // Use the player his IP for stuff instead of Geyser his IP + SocketAddress newAddress = new InetSocketAddress( + bedrockData.getIp(), + ((InetSocketAddress) ctx.channel().remoteAddress()).getPort() + ); + setValue(networkManager, getFieldOfType(networkManagerClass, SocketAddress.class, false), newAddress); + } + plugin.getLogger().info("Added " + fPlayer.getJavaUsername() + " " + fPlayer.getJavaUniqueId()); out.add(packet); } else if (isLogin) { if (!bungee) { @@ -102,7 +101,7 @@ public class PacketHandler extends MessageToMessageDecoder { Object loginListener = packetListenerField.get(networkManager); // Set the player his GameProfile - Object gameProfile = gameProfileConstructor.newInstance(floodgatePlayer.getJavaUniqueId(), floodgatePlayer.getJavaUsername()); + Object gameProfile = gameProfileConstructor.newInstance(fPlayer.getJavaUniqueId(), fPlayer.getJavaUsername()); setValue(loginListener, gameProfileField, gameProfile); initUUIDMethod.invoke(loginListener); // LoginListener#initUUID @@ -113,7 +112,7 @@ public class PacketHandler extends MessageToMessageDecoder { // out.add(packet); don't let this packet through as we want to skip the login cycle } } finally { - if (isHandhake && bungee || isLogin && !bungee || floodgatePlayer == null) { + if (isHandhake && bungee || isLogin && !bungee || fPlayer == null) { // remove the injection of the client because we're finished BukkitInjector.removeInjectedClient(future, ctx.channel()); } @@ -127,8 +126,10 @@ public class PacketHandler extends MessageToMessageDecoder { } static { - networkManagerClass = getNMSClass("NetworkManager"); - loginStartPacketClass = getNMSClass("PacketLoginInStart"); + handshakeHandler = new HandshakeHandler(plugin.getConfiguration().getPrivateKey(), false); + + networkManagerClass = getPrefixedClass("NetworkManager"); + loginStartPacketClass = getPrefixedClass("PacketLoginInStart"); gameProfileClass = ReflectionUtil.getClass("com.mojang.authlib.GameProfile"); assert gameProfileClass != null; @@ -138,11 +139,11 @@ public class PacketHandler extends MessageToMessageDecoder { e.printStackTrace(); } - handshakePacketClass = getNMSClass("PacketHandshakingInSetProtocol"); + handshakePacketClass = getPrefixedClass("PacketHandshakingInSetProtocol"); assert handshakePacketClass != null; hostField = getFieldOfType(handshakePacketClass, String.class, true); - loginListenerClass = getNMSClass("LoginListener"); + loginListenerClass = getPrefixedClass("LoginListener"); assert loginListenerClass != null; gameProfileField = getFieldOfType(loginListenerClass, gameProfileClass, true); initUUIDMethod = getMethod(loginListenerClass, "initUUID"); @@ -161,10 +162,10 @@ public class PacketHandler extends MessageToMessageDecoder { } } - Class packetListenerClass = getNMSClass("PacketListener"); + Class packetListenerClass = getPrefixedClass("PacketListener"); packetListenerField = getFieldOfType(networkManagerClass, packetListenerClass, true); - loginHandlerClass = getNMSClass("LoginListener$LoginHandler"); + loginHandlerClass = getPrefixedClass("LoginListener$LoginHandler"); assert loginHandlerClass != null; try { loginHandlerConstructor = makeAccessible(loginHandlerClass.getDeclaredConstructor(loginListenerClass)); diff --git a/bukkit/src/main/java/org/geysermc/floodgate/injector/BukkitInjector.java b/bukkit/src/main/java/org/geysermc/floodgate/injector/BukkitInjector.java index a12b27e3..d99d6ccc 100644 --- a/bukkit/src/main/java/org/geysermc/floodgate/injector/BukkitInjector.java +++ b/bukkit/src/main/java/org/geysermc/floodgate/injector/BukkitInjector.java @@ -20,6 +20,7 @@ public class BukkitInjector { private static String injectedFieldName; public static boolean inject() throws Exception { + if (isInjected()) return true; if (getServerConnection() != null) { for (Field f : serverConnection.getClass().getDeclaredFields()) { if (f.getType() == List.class) { @@ -102,7 +103,7 @@ public class BukkitInjector { public static Object getServerConnection() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { if (serverConnection != null) return serverConnection; - Class minecraftServer = ReflectionUtil.getNMSClass("MinecraftServer"); + Class minecraftServer = ReflectionUtil.getPrefixedClass("MinecraftServer"); assert minecraftServer != null; Object minecraftServerInstance = ReflectionUtil.invokeStatic(minecraftServer, "getServer"); diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index 8bfb448c..65626848 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -1,4 +1,6 @@ name: ${project.name} +description: ${project.description} version: ${project.version} author: ${project.organization.name} +website: ${project.url} main: org.geysermc.floodgate.BukkitPlugin \ No newline at end of file diff --git a/bungee/pom.xml b/bungee/pom.xml index e20db2d7..243e4e2a 100644 --- a/bungee/pom.xml +++ b/bungee/pom.xml @@ -3,14 +3,21 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - floodgate-parent org.geysermc + floodgate-parent 1.0-SNAPSHOT 4.0.0 floodgate-bungee + + + bungeecord-repo + https://oss.sonatype.org/content/repositories/snapshots + + + net.md-5 diff --git a/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java b/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java index 905eb10d..ab37295a 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java +++ b/bungee/src/main/java/org/geysermc/floodgate/BungeePlugin.java @@ -1,7 +1,6 @@ package org.geysermc.floodgate; import lombok.Getter; -import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.event.PlayerDisconnectEvent; import net.md_5.bungee.api.event.PreLoginEvent; import net.md_5.bungee.api.event.ServerConnectEvent; @@ -10,34 +9,31 @@ import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventPriority; import net.md_5.bungee.protocol.packet.Handshake; +import org.geysermc.floodgate.HandshakeHandler.HandshakeResult; import org.geysermc.floodgate.util.BedrockData; -import org.geysermc.floodgate.util.EncryptionUtil; import org.geysermc.floodgate.util.ReflectionUtil; -import javax.crypto.BadPaddingException; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; import java.lang.reflect.Field; import java.net.InetSocketAddress; import java.net.SocketAddress; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; import static org.geysermc.floodgate.util.BedrockData.FLOODGATE_IDENTIFIER; public class BungeePlugin extends Plugin implements Listener { @Getter private static BungeePlugin instance; - @Getter private BungeeFloodgateConfig config = null; private static Field extraHandshakeData; + @Getter private BungeeFloodgateConfig config; + private HandshakeHandler handshakeHandler; + @Override public void onLoad() { instance = this; if (!getDataFolder().exists()) { getDataFolder().mkdir(); } - config = FloodgateConfig.load(getLogger(), getDataFolder().toPath().resolve("config.yml"), BungeeFloodgateConfig.class); + handshakeHandler = new HandshakeHandler(config.getPrivateKey(), true); } @Override @@ -48,87 +44,73 @@ public class BungeePlugin extends Plugin implements Listener { @EventHandler(priority = EventPriority.LOW) public void onServerConnect(ServerConnectEvent e) { // Passes the information through to the connecting server if enabled - if (config.isSendFloodgateData() && BungeeFloodgateAPI.isBedrockPlayer(e.getPlayer())) { + if (config.isSendFloodgateData() && FloodgateAPI.isBedrockPlayer(e.getPlayer())) { Handshake handshake = ReflectionUtil.getCastedValue(e.getPlayer().getPendingConnection(), "handshake", Handshake.class); handshake.setHost( handshake.getHost().split("\0")[0] + '\0' + // Ensures that only the hostname remains! - FLOODGATE_IDENTIFIER + '\0' + BungeeFloodgateAPI.getEncryptedData(e.getPlayer().getUniqueId()) + FLOODGATE_IDENTIFIER + '\0' + FloodgateAPI.getEncryptedData(e.getPlayer().getUniqueId()) ); + // Bungeecord will add his data after our data } } @EventHandler(priority = EventPriority.LOW) public void onPreLogin(PreLoginEvent event) { - // only need to check when server is in online mode :D - if (ProxyServer.getInstance().getConfig().isOnlineMode()) { - event.registerIntent(this); + event.registerIntent(this); + getProxy().getScheduler().runAsync(this, () -> { + String extraData = ReflectionUtil.getCastedValue(event.getConnection(), extraHandshakeData, String.class); - getProxy().getScheduler().runAsync(this, () -> { - String extraData = ReflectionUtil.getCastedValue(event.getConnection(), extraHandshakeData, String.class); - String[] data = extraData.split("\0"); + HandshakeResult result = handshakeHandler.handle(extraData); + switch (result.getResultType()) { + case SUCCESS: + break; + case EXCEPTION: + event.setCancelReason(config.getMessages().getInvalidKey()); + case INVALID_DATA_LENGTH: + event.setCancelReason(String.format( + config.getMessages().getInvalidArgumentsLength(), + BedrockData.EXPECTED_LENGTH, result.getBedrockData().getDataLength() + )); + default: // only continue when SUCCESS + return; + } - if (data.length == 4 && data[1].equals(FLOODGATE_IDENTIFIER)) { - try { - BedrockData bedrockData = EncryptionUtil.decryptBedrockData( - config.getPrivateKey(), data[2] + '\0' + data[3] - ); + FloodgatePlayer player = result.getFloodgatePlayer(); + FloodgateAPI.addEncryptedData(player.getJavaUniqueId(), result.getHandshakeData()[2] + '\0' + result.getHandshakeData()[3]); - if (bedrockData.getDataLength() != BedrockData.EXPECTED_LENGTH) { - event.setCancelReason(String.format( - config.getMessages().getInvalidArgumentsLength(), - BedrockData.EXPECTED_LENGTH, bedrockData.getDataLength() - )); - event.setCancelled(true); - return; - } + event.getConnection().setOnlineMode(false); + event.getConnection().setUniqueId(player.getJavaUniqueId()); - FloodgatePlayer player = new FloodgatePlayer(bedrockData); - FloodgateAPI.players.put(player.getJavaUniqueId(), player); - BungeeFloodgateAPI.addEncryptedData(player.getJavaUniqueId(), data[2] + '\0' + data[3]); - - event.getConnection().setOnlineMode(false); - event.getConnection().setUniqueId(player.getJavaUniqueId()); - - ReflectionUtil.setValue(event.getConnection(), "name", player.getJavaUsername()); - Object channelWrapper = ReflectionUtil.getValue(event.getConnection(), "ch"); - SocketAddress remoteAddress = ReflectionUtil.getCastedValue(channelWrapper, "remoteAddress", SocketAddress.class); - if (!(remoteAddress instanceof InetSocketAddress)) { - getLogger().info( - "Player " + player.getUsername() + " doesn't use a InetSocketAddress. " + - "It uses " + remoteAddress.getClass().getSimpleName() + ". Ignoring the player, I guess." - ); - return; - } - ReflectionUtil.setValue( - channelWrapper, "remoteAddress", - new InetSocketAddress(bedrockData.getIp(), ((InetSocketAddress) remoteAddress).getPort()) - ); - - System.out.println("Added " + player.getUsername() + " " + player.getJavaUniqueId()); - } catch (NullPointerException | NoSuchPaddingException | NoSuchAlgorithmException | - InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) { - event.setCancelReason(config.getMessages().getInvalidKey()); - event.setCancelled(true); - e.printStackTrace(); - } - } - event.completeIntent(this); - }); - } + ReflectionUtil.setValue(event.getConnection(), "name", player.getJavaUsername()); + Object channelWrapper = ReflectionUtil.getValue(event.getConnection(), "ch"); + SocketAddress remoteAddress = ReflectionUtil.getCastedValue(channelWrapper, "remoteAddress", SocketAddress.class); + if (!(remoteAddress instanceof InetSocketAddress)) { + getLogger().info( + "Player " + player.getUsername() + " doesn't use a InetSocketAddress. " + + "It uses " + remoteAddress.getClass().getSimpleName() + ". Ignoring the player, I guess." + ); + return; + } + ReflectionUtil.setValue( + channelWrapper, "remoteAddress", + new InetSocketAddress(result.getBedrockData().getIp(), ((InetSocketAddress) remoteAddress).getPort()) + ); + event.completeIntent(this); + }); } @EventHandler public void onPlayerDisconnect(PlayerDisconnectEvent event) { - FloodgatePlayer player = BungeeFloodgateAPI.getPlayerByConnection(event.getPlayer().getPendingConnection()); + FloodgatePlayer player = FloodgateAPI.getPlayerByConnection(event.getPlayer().getPendingConnection()); if (player != null) { FloodgateAPI.players.remove(player.getJavaUniqueId()); - BungeeFloodgateAPI.removeEncryptedData(player.getJavaUniqueId()); + FloodgateAPI.removeEncryptedData(player.getJavaUniqueId()); System.out.println("Removed " + player.getUsername() + " " + event.getPlayer().getUniqueId()); } } static { Class initial_handler = ReflectionUtil.getClass("net.md_5.bungee.connection.InitialHandler"); - extraHandshakeData = ReflectionUtil.getField(initial_handler, "getExtraDataInHandshake"); + extraHandshakeData = ReflectionUtil.getField(initial_handler, "extraDataInHandshake"); } } diff --git a/bungee/src/main/java/org/geysermc/floodgate/BungeeFloodgateAPI.java b/bungee/src/main/java/org/geysermc/floodgate/FloodgateAPI.java similarity index 80% rename from bungee/src/main/java/org/geysermc/floodgate/BungeeFloodgateAPI.java rename to bungee/src/main/java/org/geysermc/floodgate/FloodgateAPI.java index 7e6861c3..bca6f56a 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/BungeeFloodgateAPI.java +++ b/bungee/src/main/java/org/geysermc/floodgate/FloodgateAPI.java @@ -7,11 +7,11 @@ import java.util.HashMap; import java.util.Map; import java.util.UUID; -public class BungeeFloodgateAPI extends FloodgateAPI { +public class FloodgateAPI extends AbstractFloodgateAPI { private static Map encryptedData = new HashMap<>(); static void addEncryptedData(UUID uuid, String encryptedData) { - BungeeFloodgateAPI.encryptedData.put(uuid, encryptedData); // just override it I guess + FloodgateAPI.encryptedData.put(uuid, encryptedData); // just override it I guess } static void removeEncryptedData(UUID uuid) { @@ -23,7 +23,7 @@ public class BungeeFloodgateAPI extends FloodgateAPI { } /** - * See {@link FloodgateAPI#getPlayer(UUID)} + * See {@link AbstractFloodgateAPI#getPlayer(UUID)} */ public static FloodgatePlayer getPlayerByConnection(PendingConnection connection) { return getPlayer(connection.getUniqueId()); diff --git a/bungee/src/main/resources/config.yml b/bungee/src/main/resources/config.yml index 81bf9490..32637cc4 100644 --- a/bungee/src/main/resources/config.yml +++ b/bungee/src/main/resources/config.yml @@ -1,11 +1,9 @@ # In Floodgate bedrock player data is send encrypted # The following value should point to the key Floodgate generated. -# The public key should be used for the Geyser(s) and the private key for -# - bungeecord -# - bukkit (craftbukkit, spigot etc.) +# The public key should be used for the Geyser(s) and the private key for the Floodgate(s) key-file-name: key.pem -# Should Bungeecord send the bedrock player data to the servers it is connecting to? +# Should Velocity send the bedrock player data to the servers it is connecting to? # This requires Floodgate to be installed on the servers. # You'll get kicked if you don't use the plugin. The default value is false because of it send-floodgate-data: false diff --git a/bungee/src/main/resources/plugin.yml b/bungee/src/main/resources/plugin.yml index 0c069f24..d6d2d490 100644 --- a/bungee/src/main/resources/plugin.yml +++ b/bungee/src/main/resources/plugin.yml @@ -1,4 +1,5 @@ name: ${project.name} +description: ${project.description} version: ${project.version} author: ${project.organization.name} main: org.geysermc.floodgate.BungeePlugin \ No newline at end of file diff --git a/common/src/main/java/org/geysermc/floodgate/FloodgateAPI.java b/common/src/main/java/org/geysermc/floodgate/AbstractFloodgateAPI.java similarity index 64% rename from common/src/main/java/org/geysermc/floodgate/FloodgateAPI.java rename to common/src/main/java/org/geysermc/floodgate/AbstractFloodgateAPI.java index 88f4a532..8add34f1 100644 --- a/common/src/main/java/org/geysermc/floodgate/FloodgateAPI.java +++ b/common/src/main/java/org/geysermc/floodgate/AbstractFloodgateAPI.java @@ -1,12 +1,10 @@ package org.geysermc.floodgate; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; - import java.util.HashMap; import java.util.Map; import java.util.UUID; -class FloodgateAPI { +abstract class AbstractFloodgateAPI { static final Map players = new HashMap<>(); /** @@ -33,27 +31,4 @@ class FloodgateAPI { public static UUID createJavaPlayerId(long xuid) { return new UUID(0, xuid); } - - public enum DeviceOS { - @JsonEnumDefaultValue - UNKOWN, - ANDROID, - IOS, - OSX, - FIREOS, - GEARVR, - HOLOLENS, - WIN10, - WIN32, - DEDICATED, - ORBIS, - NX, - SWITCH; - - private static final DeviceOS[] VALUES = values(); - - public static DeviceOS getById(int id) { - return id < VALUES.length ? VALUES[id] : VALUES[0]; - } - } } diff --git a/common/src/main/java/org/geysermc/floodgate/FloodgateConfig.java b/common/src/main/java/org/geysermc/floodgate/FloodgateConfig.java index 3fb9086b..00d67995 100644 --- a/common/src/main/java/org/geysermc/floodgate/FloodgateConfig.java +++ b/common/src/main/java/org/geysermc/floodgate/FloodgateConfig.java @@ -80,7 +80,10 @@ public class FloodgateConfig { } catch (Exception e) { logger.log(Level.SEVERE, "Error while loading config", e); } - if (config == null) return null; + + if (config == null) { + throw new RuntimeException("Failed to load config file! Try to delete the data folder of Floodgate"); + } try { config.privateKey = EncryptionUtil.getKeyFromFile( diff --git a/common/src/main/java/org/geysermc/floodgate/FloodgatePlayer.java b/common/src/main/java/org/geysermc/floodgate/FloodgatePlayer.java index 0fe92c92..2273b9f0 100644 --- a/common/src/main/java/org/geysermc/floodgate/FloodgatePlayer.java +++ b/common/src/main/java/org/geysermc/floodgate/FloodgatePlayer.java @@ -2,6 +2,7 @@ package org.geysermc.floodgate; import lombok.Getter; import org.geysermc.floodgate.util.BedrockData; +import org.geysermc.floodgate.util.DeviceOS; import java.util.UUID; @@ -26,7 +27,7 @@ public class FloodgatePlayer { /** * The operation system of the bedrock client */ - private FloodgateAPI.DeviceOS deviceOS; + private DeviceOS deviceOS; /** * The language code of the bedrock client */ @@ -41,8 +42,8 @@ public class FloodgatePlayer { username = data.getUsername(); javaUsername = "*" + data.getUsername().substring(0, Math.min(data.getUsername().length(), 15)); xuid = data.getXuid(); - deviceOS = FloodgateAPI.DeviceOS.getById(data.getDeviceId()); + deviceOS = DeviceOS.getById(data.getDeviceId()); languageCode = data.getLanguageCode(); - javaUniqueId = FloodgateAPI.createJavaPlayerId(Long.parseLong(data.getXuid())); + javaUniqueId = AbstractFloodgateAPI.createJavaPlayerId(Long.parseLong(data.getXuid())); } } diff --git a/common/src/main/java/org/geysermc/floodgate/HandshakeHandler.java b/common/src/main/java/org/geysermc/floodgate/HandshakeHandler.java new file mode 100644 index 00000000..f1e13661 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/HandshakeHandler.java @@ -0,0 +1,76 @@ +package org.geysermc.floodgate; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NonNull; +import org.geysermc.floodgate.util.BedrockData; +import org.geysermc.floodgate.util.EncryptionUtil; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; + +import static org.geysermc.floodgate.util.BedrockData.EXPECTED_LENGTH; +import static org.geysermc.floodgate.util.BedrockData.FLOODGATE_IDENTIFIER; + +public class HandshakeHandler { + private PrivateKey privateKey; + private boolean bungee; + + public HandshakeHandler(@NonNull PrivateKey privateKey, boolean bungee) { + this.privateKey = privateKey; + this.bungee = bungee; + } + + public HandshakeResult handle(@NonNull String handshakeData) { + try { + String[] data = handshakeData.split("\0"); + boolean isBungeeData = data.length == 6 || data.length == 7; + + if (bungee && isBungeeData || !isBungeeData && data.length != 4 || !data[1].equals(FLOODGATE_IDENTIFIER)) { + return ResultType.NOT_FLOODGATE_DATA.getCachedResult(); + } + + BedrockData bedrockData = EncryptionUtil.decryptBedrockData( + privateKey, data[2] + '\0' + data[3] + ); + + if (bedrockData.getDataLength() != EXPECTED_LENGTH) { + return ResultType.INVALID_DATA_LENGTH.getCachedResult(); + } + + FloodgatePlayer player = new FloodgatePlayer(bedrockData); + AbstractFloodgateAPI.players.put(player.getJavaUniqueId(), player); + return new HandshakeResult(ResultType.SUCCESS, data, bedrockData, player); + } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) { + e.printStackTrace(); + return ResultType.EXCEPTION.getCachedResult(); + } + } + + @AllArgsConstructor(access = AccessLevel.PROTECTED) + @Getter + public static class HandshakeResult { + private ResultType resultType; + private String[] handshakeData; + private BedrockData bedrockData; + private FloodgatePlayer floodgatePlayer; + } + + public enum ResultType { + EXCEPTION, + NOT_FLOODGATE_DATA, + INVALID_DATA_LENGTH, + SUCCESS; + + @Getter private HandshakeResult cachedResult; + + ResultType() { + cachedResult = new HandshakeResult(this, null, null, null); + } + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java b/common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java new file mode 100644 index 00000000..cb658e82 --- /dev/null +++ b/common/src/main/java/org/geysermc/floodgate/util/DeviceOS.java @@ -0,0 +1,27 @@ +package org.geysermc.floodgate.util; + + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; + +public enum DeviceOS { + @JsonEnumDefaultValue + UNKNOWN, + ANDROID, + IOS, + OSX, + FIREOS, + GEARVR, + HOLOLENS, + WIN10, + WIN32, + DEDICATED, + ORBIS, + NX, + SWITCH; + + private static final DeviceOS[] VALUES = values(); + + public static DeviceOS getById(int id) { + return id < VALUES.length ? VALUES[id] : VALUES[0]; + } +} diff --git a/common/src/main/java/org/geysermc/floodgate/util/ReflectionUtil.java b/common/src/main/java/org/geysermc/floodgate/util/ReflectionUtil.java index ba61819e..2fe36927 100644 --- a/common/src/main/java/org/geysermc/floodgate/util/ReflectionUtil.java +++ b/common/src/main/java/org/geysermc/floodgate/util/ReflectionUtil.java @@ -1,22 +1,24 @@ package org.geysermc.floodgate.util; +import lombok.Getter; +import lombok.Setter; + import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class ReflectionUtil { - /* - * Used in the Bukkit version + /** + * Prefix without dot
+ * Example net.minecraft.server.v1_8R3.PacketHandhakingInSetProtocol will become:
+ * net.minecraft.server.v1_8R3 */ - private static String nmsPackage = null; + @Getter @Setter + private static String prefix = null; - public static Class getNMSClass(String className) { - return getClass(nmsPackage + className); - } - - public static void setServerVersion(String serverVersion) { - nmsPackage = "net.minecraft.server." + serverVersion + "."; + public static Class getPrefixedClass(String className) { + return getClass(prefix +"."+ className); } public static Class getClass(String className) { @@ -101,8 +103,27 @@ public class ReflectionUtil { } } - public static Object invokeStatic(Class clazz, String method) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - return clazz.getDeclaredMethod(method).invoke(null); + public static Object invoke(Object instance, Method method) { + try { + return method.invoke(instance); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } + } + + @SuppressWarnings("unchecked") + public static T invokeCasted(Object instance, Method method, Class cast) { + return (T) invoke(instance, method); + } + + public static Object invokeStatic(Class clazz, String method) { + try { + return getMethod(clazz, method).invoke(null); + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + return null; + } } public static T makeAccessible(T accessibleObject) { diff --git a/common/src/main/resources/config.yml b/common/src/main/resources/config.yml index cc3e2ebf..b4fcde06 100644 --- a/common/src/main/resources/config.yml +++ b/common/src/main/resources/config.yml @@ -1,8 +1,6 @@ # In Floodgate bedrock player data is send encrypted # The following value should point to the key Floodgate generated. -# The public key should be used for the Geyser(s) and the private key for -# - bungeecord -# - bukkit (craftbukkit, spigot etc.) +# The public key should be used for the Geyser(s) and the private key for the Floodgate(s) key-file-name: key.pem disconnect: diff --git a/pom.xml b/pom.xml index 61ddef2d..5671356d 100644 --- a/pom.xml +++ b/pom.xml @@ -11,15 +11,19 @@ bungee common bukkit + velocity pom floodgate Allows Bedrock players to join Java edition servers while keeping online mode - https://geysermc.org + https://github.com/GeyserMC/Floodgate/ 1.0-SNAPSHOT + 1.8.8-R0.1-SNAPSHOT 1.15-SNAPSHOT + 1.1.0-SNAPSHOT + ${project.name} UTF-8 UTF-8 @@ -29,46 +33,15 @@ GeyserMC - https://github.com/GeyserMC/Floodgate/blob/master/pom.xml + https://geysermc.org/ scm:git:https://github.com/GeyserMC/Floodgate.git scm:git:git@github.com:GeyserMC/Floodgate.git - https://github.com/GeyserMC/Floodgate + https://github.com/GeyserMC/Floodgate/ - - - bungeecord-repo - https://oss.sonatype.org/content/repositories/snapshots - - - nukkitx-release-repo - https://repo.nukkitx.com/maven-releases/ - - true - - - false - - - - nukkitx-snapshot-repo - https://repo.nukkitx.com/maven-snapshots/ - - false - - - true - - - - Lucko.me - https://nexus.lucko.me/repository/maven-snapshots/ - - - releases diff --git a/velocity/pom.xml b/velocity/pom.xml new file mode 100644 index 00000000..bd894d46 --- /dev/null +++ b/velocity/pom.xml @@ -0,0 +1,87 @@ + + + + org.geysermc + floodgate-parent + 1.0-SNAPSHOT + + 4.0.0 + ${parent.url} + + floodgate-velocity + + + + velocity-repo + https://repo.velocitypowered.com/snapshots/ + + + sponge-repo + https://repo.spongepowered.org/maven/ + + + + + + org.javassist + javassist + 3.26.0-GA + compile + + + net.openhft + compiler + 2.3.5 + compile + + + io.netty + netty-all + 4.1.45.Final + provided + + + com.velocitypowered + velocity-api + ${velocity-version} + provided + + + org.geysermc + floodgate-common + ${project.version} + compile + + + + + + + src/main/resources + true + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + ${outputName} + true + true + + + + + \ No newline at end of file diff --git a/velocity/src/main/java/org/geysermc/floodgate/FloodgateAPI.java b/velocity/src/main/java/org/geysermc/floodgate/FloodgateAPI.java new file mode 100644 index 00000000..bb7cca9a --- /dev/null +++ b/velocity/src/main/java/org/geysermc/floodgate/FloodgateAPI.java @@ -0,0 +1,37 @@ +package org.geysermc.floodgate; + +import com.velocitypowered.api.proxy.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class FloodgateAPI extends AbstractFloodgateAPI { + private static Map encryptedData = new HashMap<>(); + + static void addEncryptedData(UUID uuid, String encryptedData) { + FloodgateAPI.encryptedData.put(uuid, encryptedData); // just override it I guess + } + + static void removeEncryptedData(UUID uuid) { + encryptedData.remove(uuid); + } + + public static String getEncryptedData(UUID uuid) { + return encryptedData.get(uuid); + } + + /** + * See {@link AbstractFloodgateAPI#getPlayer(UUID)} + */ + public static FloodgatePlayer getPlayer(Player player) { + return getPlayer(player.getUniqueId()); + } + + /** + * See {@link AbstractFloodgateAPI#isBedrockPlayer(UUID)} + */ + public static boolean isBedrockPlayer(Player player) { + return isBedrockPlayer(player.getUniqueId()); + } +} diff --git a/velocity/src/main/java/org/geysermc/floodgate/VelocityFloodgateConfig.java b/velocity/src/main/java/org/geysermc/floodgate/VelocityFloodgateConfig.java new file mode 100644 index 00000000..3e49c0de --- /dev/null +++ b/velocity/src/main/java/org/geysermc/floodgate/VelocityFloodgateConfig.java @@ -0,0 +1,10 @@ +package org.geysermc.floodgate; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; + +public class VelocityFloodgateConfig extends FloodgateConfig { + @JsonProperty(value = "send-floodgate-data") + @Getter + private boolean sendFloodgateData; +} diff --git a/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java b/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java new file mode 100644 index 00000000..f897e33b --- /dev/null +++ b/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java @@ -0,0 +1,175 @@ +package org.geysermc.floodgate; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.inject.Inject; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent; +import com.velocitypowered.api.event.connection.PreLoginEvent; +import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; +import com.velocitypowered.api.event.player.GameProfileRequestEvent; +import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.util.GameProfile; +import lombok.Getter; +import net.kyori.text.TextComponent; +import org.geysermc.floodgate.HandshakeHandler.HandshakeResult; +import org.geysermc.floodgate.injector.VelocityInjector; +import org.geysermc.floodgate.util.ReflectionUtil; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +import static org.geysermc.floodgate.util.ReflectionUtil.getField; +import static org.geysermc.floodgate.util.ReflectionUtil.getPrefixedClass; + +public class VelocityPlugin { + @Getter private VelocityFloodgateConfig config; + private HandshakeHandler handshakeHandler; + + private Set workingSet; + private Cache playerCache; + private Cache playersToKick; + + private Logger logger; + private boolean injectSucceed; + + @Inject + public VelocityPlugin(ProxyServer server, Logger logger) { + // we're too late if we would do this in the init event + injectSucceed = false; + try { + injectSucceed = VelocityInjector.inject(this); + } catch (Exception e) { + e.printStackTrace(); + } + this.logger = logger; + if (!injectSucceed) return; + + this.workingSet = new HashSet<>(); + this.playersToKick = CacheBuilder.newBuilder() + .maximumSize(3000) + .expireAfterWrite(50, TimeUnit.SECONDS) + .build(); + this.playerCache = CacheBuilder.newBuilder() + .maximumSize(500) + .expireAfterAccess(50, TimeUnit.SECONDS) + .build(); + } + + @Subscribe + public void onProxyInitialization(ProxyInitializeEvent event) { + if (injectSucceed) { + logger.info("Floodgate injection process succeeded!"); + } else { + logger.severe("Failed to inject! Floodgate won't do anything."); + return; + } + + File dataFolder = new File("plugins/floodgate/"); + if (!dataFolder.exists()) { + dataFolder.mkdir(); + } + + config = FloodgateConfig.load(logger, dataFolder.toPath().resolve("config.yml"), VelocityFloodgateConfig.class); + handshakeHandler = new HandshakeHandler(config.getPrivateKey(), true); + } + + @Subscribe + public void onConnectionHandshake(ConnectionHandshakeEvent event) { + if (!injectSucceed) return; + workingSet.add(event.getConnection()); + + try { + Object cachedHandshake = ReflectionUtil.getValue(event.getConnection(), handshakeField); + String handshakeData = ReflectionUtil.getCastedValue(cachedHandshake, handshakeAddressField, String.class); + + HandshakeResult result = handshakeHandler.handle(handshakeData); + switch (result.getResultType()) { + case SUCCESS: + break; + case EXCEPTION: + playersToKick.put(event.getConnection(), config.getMessages().getInvalidKey()); + case INVALID_DATA_LENGTH: + playersToKick.put(event.getConnection(), config.getMessages().getInvalidArgumentsLength()); + default: + return; + } + + FloodgatePlayer player = result.getFloodgatePlayer(); + FloodgateAPI.addEncryptedData(player.getJavaUniqueId(), result.getHandshakeData()[2] + '\0' + result.getHandshakeData()[3]); + playerCache.put(event.getConnection(), player); + logger.info("Added " + player.getJavaUsername() + " " + player.getJavaUniqueId()); + } catch (Exception e) { + e.printStackTrace(); + } finally { + workingSet.remove(event.getConnection()); + } + } + + @Subscribe + public void onPreLogin(PreLoginEvent event) { + if (!injectSucceed) return; + + if (workingSet.contains(event.getConnection())) { + int count = 70; + // 70 * 70 / 1000 = 4.9 seconds to get removed from working cache + while (count-- != 0 && workingSet.contains(event.getConnection())) { + // should be 'just fine' because the event system is multithreaded + try { + Thread.sleep(70); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + if (workingSet.contains(event.getConnection())) { + String message = "Took more then 4.9 seconds (after PreLoginEvent) to finish Handshake data for "+event.getUsername(); + logger.warning(message); + event.setResult(PreLoginComponentResult.denied(TextComponent.of(message))); + return; + } + } + + FloodgatePlayer player = playerCache.getIfPresent(event.getConnection()); + if (player != null) { + System.out.println(event.getUsername()); + event.setResult(PreLoginComponentResult.forceOfflineMode()); + return; + } + + String message = playersToKick.getIfPresent(event.getConnection()); + if (message != null) { + playersToKick.invalidate(event.getConnection()); + event.setResult(PreLoginComponentResult.denied(TextComponent.of(message))); + } + } + + @Subscribe + public void onGameProfileRequest(GameProfileRequestEvent event) { + if (!injectSucceed) return; + + FloodgatePlayer player = playerCache.getIfPresent(event.getConnection()); + if (player != null) { + playerCache.invalidate(event.getConnection()); + event.setGameProfile(new GameProfile(player.getJavaUniqueId(), player.getJavaUsername(), new ArrayList<>())); + } + } + + private Field handshakeField; + private Field handshakeAddressField; + + public void initReflection() { + ReflectionUtil.setPrefix("com.velocitypowered.proxy"); + Class IIC = getPrefixedClass("connection.client.InitialInboundConnection"); + Class HandshakePacket = getPrefixedClass("protocol.packet.Handshake"); + + handshakeField = getField(IIC, "handshake"); + handshakeAddressField = getField(HandshakePacket, "serverAddress"); + } +} diff --git a/velocity/src/main/java/org/geysermc/floodgate/injector/VelocityInjector.java b/velocity/src/main/java/org/geysermc/floodgate/injector/VelocityInjector.java new file mode 100644 index 00000000..d46f5a02 --- /dev/null +++ b/velocity/src/main/java/org/geysermc/floodgate/injector/VelocityInjector.java @@ -0,0 +1,105 @@ +package org.geysermc.floodgate.injector; + +import com.velocitypowered.api.proxy.Player; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtField; +import javassist.Modifier; +import javassist.bytecode.Descriptor; +import lombok.Getter; +import org.geysermc.floodgate.FloodgateAPI; +import org.geysermc.floodgate.VelocityPlugin; +import org.geysermc.floodgate.util.ReflectionUtil; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.BiConsumer; + +import static org.geysermc.floodgate.util.BedrockData.FLOODGATE_IDENTIFIER; + +public class VelocityInjector { + @Getter private static boolean injected = false; + + public static boolean inject(VelocityPlugin plugin) throws Exception { + if (isInjected()) return true; + // Tnx Velocity for making it difficult x) + // But I made my way in >:) + // Please note that doing this is not recommended + + // This method will edit the source code of MinecraftConnection to allow us to edit any outgoing packet. + + ClassPool classPool = ClassPool.getDefault(); + CtClass ctClass = classPool.get("com.velocitypowered.proxy.connection.MinecraftConnection"); + ctClass.defrost(); + + // create dataConsumer field + CtClass biConsumerClass = classPool.get("java.util.function.BiConsumer"); + CtField dataConsumer = new CtField(biConsumerClass, "dataConsumer", ctClass); + dataConsumer.setModifiers(Modifier.PUBLIC | Modifier.STATIC); + ctClass.addField(dataConsumer); + + // override write and delayedWrite in MinecraftConnection + String voidObjectDescriptor = Descriptor.ofMethod(CtClass.voidType, new CtClass[] { classPool.get("java.lang.Object") }); + String codeToInsert = + "if (msg != null && dataConsumer != null && msg instanceof com.velocitypowered.proxy.protocol.MinecraftPacket) {\n" + + " dataConsumer.accept(this, (com.velocitypowered.proxy.protocol.MinecraftPacket)msg);\n" + + "}\n"; + + ctClass.getMethod("write", voidObjectDescriptor).insertAt(1, codeToInsert); + ctClass.getMethod("delayedWrite", voidObjectDescriptor).insertAt(1, codeToInsert); + + Class clazz = ctClass.toClass(); + + // The most important part is done, + // So now we call initReflection to let us use getPrefixedClass + plugin.initReflection(); + + // handshake handle stuff + Class handshakePacket = ReflectionUtil.getPrefixedClass("protocol.packet.Handshake"); + Field serverAddress = ReflectionUtil.getField(handshakePacket, "serverAddress"); + + Field sessionHandlerField = ReflectionUtil.getField(clazz, "sessionHandler"); + Class loginHandler = ReflectionUtil.getPrefixedClass("connection.backend.LoginSessionHandler"); + + Field serverConnField = ReflectionUtil.getField(loginHandler, "serverConn"); + + Class serverConnection = ReflectionUtil.getPrefixedClass("connection.backend.VelocityServerConnection"); + Method playerGetMethod = ReflectionUtil.getMethod(serverConnection, "getPlayer"); + assert playerGetMethod != null; + + // create and set our custom made Consumer + + BiConsumer biConsumer = (minecraftConnection, packet) -> { + // This consumer is only called when the server is sending data (outgoing). + // So when we get a handshake packet it'll be Velocity trying to connect to a server. + // And that is exactly what we need, to edit the outgoing handshake packet to include Floodgate data + if (plugin.getConfig().isSendFloodgateData()) { + try { + if (handshakePacket.isInstance(packet)) { + String address = ReflectionUtil.getCastedValue(packet, serverAddress, String.class); + + Object sessionHandler = ReflectionUtil.getValue(minecraftConnection, sessionHandlerField); + if (loginHandler.isInstance(sessionHandler)) { + Object serverConn = ReflectionUtil.getValue(sessionHandler, serverConnField); + Player connectedPlayer = ReflectionUtil.invokeCasted(serverConn, playerGetMethod, Player.class); + + String encryptedData = FloodgateAPI.getEncryptedData(connectedPlayer.getUniqueId()); + if (encryptedData == null) return; // we know enough, this is not a Floodgate player + + String[] splitted = address.split("\0"); + String remaining = address.substring(splitted[0].length()); + ReflectionUtil.setValue(packet, serverAddress, splitted[0] + '\0' + FLOODGATE_IDENTIFIER + '\0' + encryptedData + remaining); + // keep the same system as we have on Bungeecord. Our data is before the Bungee data + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + Class minecraftConnection = ReflectionUtil.getPrefixedClass("connection.MinecraftConnection"); + ReflectionUtil.setValue(null, ReflectionUtil.getField(minecraftConnection, "dataConsumer"), biConsumer); + return true; + } +} diff --git a/velocity/src/main/resources/velocity-plugin.json b/velocity/src/main/resources/velocity-plugin.json new file mode 100644 index 00000000..6bdc8e29 --- /dev/null +++ b/velocity/src/main/resources/velocity-plugin.json @@ -0,0 +1 @@ +{"id": "floodgate", "name": "${project.artifactId}", "version": "${project.version}", "description": "${project.description}", "url": "${project.url}", "authors": ["${project.organization.name}"], "main": "org.geysermc.floodgate.VelocityPlugin"} \ No newline at end of file