1
0
mirror of https://github.com/GeyserMC/Floodgate.git synced 2025-12-28 02:59:16 +00:00

Adds experimental Velocity support, fixed bugs and changed some class names

This commit is contained in:
Tim203
2020-02-22 16:32:20 +01:00
parent 580ae0dcf0
commit 7edff139cc
26 changed files with 702 additions and 214 deletions

View File

@@ -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">
<parent>
<artifactId>floodgate-parent</artifactId>
<groupId>org.geysermc</groupId>
<artifactId>floodgate-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@@ -22,7 +22,7 @@
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>bukkit</artifactId>
<version>1.8.8-R0.1-SNAPSHOT</version>
<version>${bukkit-version}</version>
<scope>provided</scope>
</dependency>
<dependency>

View File

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

View File

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

View File

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

View File

@@ -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<Object> {
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<Object> {
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<Object> 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> {
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<Object> {
// 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<Object> {
}
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<Object> {
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<Object> {
}
}
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));

View File

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

View File

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

View File

@@ -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">
<parent>
<artifactId>floodgate-parent</artifactId>
<groupId>org.geysermc</groupId>
<artifactId>floodgate-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>floodgate-bungee</artifactId>
<repositories>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.md-5</groupId>

View File

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

View File

@@ -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<UUID, String> 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());

View File

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

View File

@@ -1,4 +1,5 @@
name: ${project.name}
description: ${project.description}
version: ${project.version}
author: ${project.organization.name}
main: org.geysermc.floodgate.BungeePlugin

View File

@@ -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<UUID, FloodgatePlayer> 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];
}
}
}

View File

@@ -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(

View File

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

View File

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

View File

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

View File

@@ -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<br>
* Example net.minecraft.server.v1_8R3.PacketHandhakingInSetProtocol will become:<br>
* 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> T invokeCasted(Object instance, Method method, Class<T> 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 extends AccessibleObject> T makeAccessible(T accessibleObject) {

View File

@@ -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:

41
pom.xml
View File

@@ -11,15 +11,19 @@
<module>bungee</module>
<module>common</module>
<module>bukkit</module>
<module>velocity</module>
</modules>
<packaging>pom</packaging>
<name>floodgate</name>
<description>Allows Bedrock players to join Java edition servers while keeping online mode</description>
<url>https://geysermc.org</url>
<url>https://github.com/GeyserMC/Floodgate/</url>
<properties>
<geyser-version>1.0-SNAPSHOT</geyser-version>
<bukkit-version>1.8.8-R0.1-SNAPSHOT</bukkit-version>
<bungee-version>1.15-SNAPSHOT</bungee-version>
<velocity-version>1.1.0-SNAPSHOT</velocity-version>
<outputName>${project.name}</outputName>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
@@ -29,46 +33,15 @@
<organization>
<name>GeyserMC</name>
<url>https://github.com/GeyserMC/Floodgate/blob/master/pom.xml</url>
<url>https://geysermc.org/</url>
</organization>
<scm>
<connection>scm:git:https://github.com/GeyserMC/Floodgate.git</connection>
<developerConnection>scm:git:git@github.com:GeyserMC/Floodgate.git</developerConnection>
<url>https://github.com/GeyserMC/Floodgate</url>
<url>https://github.com/GeyserMC/Floodgate/</url>
</scm>
<repositories>
<repository>
<id>bungeecord-repo</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</repository>
<repository>
<id>nukkitx-release-repo</id>
<url>https://repo.nukkitx.com/maven-releases/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>nukkitx-snapshot-repo</id>
<url>https://repo.nukkitx.com/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>Lucko.me</id>
<url>https://nexus.lucko.me/repository/maven-snapshots/</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>releases</id>

87
velocity/pom.xml Normal file
View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
<parent>
<groupId>org.geysermc</groupId>
<artifactId>floodgate-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<url>${parent.url}</url>
<artifactId>floodgate-velocity</artifactId>
<repositories>
<repository>
<id>velocity-repo</id>
<url>https://repo.velocitypowered.com/snapshots/</url>
</repository>
<repository>
<id>sponge-repo</id>
<url>https://repo.spongepowered.org/maven/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.26.0-GA</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.openhft</groupId>
<artifactId>compiler</artifactId>
<version>2.3.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.45.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-api</artifactId>
<version>${velocity-version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.geysermc</groupId>
<artifactId>floodgate-common</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>${outputName}</finalName>
<shadedArtifactAttached>true</shadedArtifactAttached>
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

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

View File

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

View File

@@ -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<InboundConnection> workingSet;
private Cache<InboundConnection, FloodgatePlayer> playerCache;
private Cache<InboundConnection, String> 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");
}
}

View File

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

View File

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