diff --git a/README.md b/README.md index 28d3182..dfc19d3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It also parses passed IP addresses so the server is aware of the real player IP ### Compatibility -TCPShield is compatible with Spigot / CraftBukkit, BungeeCord and Velocity. +TCPShield is compatible with Spigot / CraftBukkit, BungeeCord, Velocity and Fabric. When using Spigot / CraftBukkit, [ProtocolLib](https://github.com/aadnk/ProtocolLib) needs to be installed. This does not apply when using Paper 1.16 build #503 or higher. @@ -30,4 +30,5 @@ See [Contact](https://docs.tcpshield.com/about-us) These wonderful contributors have helped TCPShield make this plugin better! * [Paul Zhang](https://github.com/paulzhng) +* [Draylar](https://github.com/Draylar) * [RyanDeLap](https://github.com/RyanDeLap) diff --git a/build.gradle b/build.gradle index 9e7e260..be900c4 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ buildscript { plugins { id 'java' id 'idea' + id 'fabric-loom' version '0.6-SNAPSHOT' } repositories { @@ -40,6 +41,9 @@ repositories { maven { url = 'https://repo.velocitypowered.com/snapshots/' } + maven { + url = 'https://maven.fabricmc.net/' + } flatDir { dirs '/libs' } @@ -59,6 +63,11 @@ dependencies { // Velocity compileOnly group: 'com.velocitypowered', name: 'velocity-api', version: '1.0.0-SNAPSHOT' + // Fabric + minecraft "com.mojang:minecraft:1.16.5" + mappings "net.fabricmc:yarn:1.16.5+build.5:v2" + modImplementation group: 'net.fabricmc', name: 'fabric-loader', version: '0.11.2' + // Testing testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: '5.7.0-M1' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0-M1' diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5b60df3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/TCPShieldFabric.java b/src/main/java/net/tcpshield/tcpshield/fabric/TCPShieldFabric.java new file mode 100644 index 0000000..0d6e4d4 --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/TCPShieldFabric.java @@ -0,0 +1,18 @@ +package net.tcpshield.tcpshield.fabric; + +import net.fabricmc.api.ModInitializer; +import net.tcpshield.tcpshield.HandshakePacketHandler; +import net.tcpshield.tcpshield.fabric.impl.FabricConfigImpl; + +import java.util.logging.Logger; + +public class TCPShieldFabric implements ModInitializer { + + public static final Logger LOGGER = Logger.getLogger("TCPShield"); + public static final HandshakePacketHandler PACKET_HANDLER = new HandshakePacketHandler(LOGGER, new FabricConfigImpl()); + + @Override + public void onInitialize() { + LOGGER.info("TCPShield has been loaded."); + } +} diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricConfigImpl.java b/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricConfigImpl.java new file mode 100644 index 0000000..2f1cefe --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricConfigImpl.java @@ -0,0 +1,105 @@ +package net.tcpshield.tcpshield.fabric.impl; + +import net.fabricmc.loader.api.FabricLoader; +import net.tcpshield.tcpshield.abstraction.TCPShieldConfig; +import net.tcpshield.tcpshield.exception.TCPShieldInitializationException; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class FabricConfigImpl extends TCPShieldConfig { + + private final Path configLocation = new File(FabricLoader.getInstance().getConfigDir().toString(), "tcpshield.yml").toPath(); + + public FabricConfigImpl() { + if(!configExists()) { + createDefaultConfig(); + } + + ConfigData config = loadConfig(); + + this.onlyProxy = config.onlyAllowProxyConnections; + this.ipWhitelistFolder = new File(FabricLoader.getInstance().getGameDirectory(), "ip-whitelist"); + this.geyser = false; + this.debug = config.debugMode; + } + + /** + * @return whether the file specified in {@link FabricConfigImpl#configLocation} exists. + */ + private boolean configExists() { + return Files.exists(configLocation); + } + + /** + * Creates the default configuration file at the location specified in {@link FabricConfigImpl#configLocation}. + * + *

+ * This operation will always override an existing configuration file if it exists. + * Callers should check {@link FabricConfigImpl#configExists()} if they wish to avoid this behavior. + */ + private void createDefaultConfig() { + // Ensure the parent directory of our config file exists. + // If it does not exist, attempt to create it now. + Path parentDirectory = configLocation.getParent(); + if (!Files.exists(parentDirectory) || !Files.isDirectory(parentDirectory)) { + parentDirectory.toFile().mkdirs(); + } + + // Copy the config.yml data from our mod jar to the loader config folder. + try (InputStream in = getClass().getClassLoader().getResourceAsStream("config.yml")) { + Files.copy(in, configLocation); + } catch (Exception e) { + throw new TCPShieldInitializationException(e); + } + } + + private ConfigData loadConfig() { + try { + List strings = Files.readAllLines(configLocation); + + boolean onlyAllowProxyConnections = true; + boolean debugMode = false; + + // Rudimentary config parsing + for (String line : strings) { + String[] keyValue = line.split(": "); + + // A config option will only be valid if it is in the format 'a: b'. + // Ensure that is the case now. + if (keyValue.length == 2) { + String key = keyValue[0]; + String value = keyValue[1]; + + switch (key) { + case "only-allow-proxy-connections": + onlyAllowProxyConnections = Boolean.parseBoolean(value); + break; + case "debug-mode": + debugMode = Boolean.parseBoolean(value); + break; + } + } + } + + return new ConfigData(onlyAllowProxyConnections, debugMode); + } catch (Exception e) { + throw new TCPShieldInitializationException("Couldn't load config in config/tclshield.yml!"); + } + } + + private static class ConfigData { + + protected boolean onlyAllowProxyConnections; + protected boolean debugMode; + + public ConfigData(boolean onlyAllowProxyConnections, boolean debugMode) { + this.onlyAllowProxyConnections = onlyAllowProxyConnections; + this.debugMode = debugMode; + } + } +} diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricPacketImpl.java b/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricPacketImpl.java new file mode 100644 index 0000000..2751ba9 --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricPacketImpl.java @@ -0,0 +1,24 @@ +package net.tcpshield.tcpshield.fabric.impl; + +import net.minecraft.network.packet.c2s.handshake.HandshakeC2SPacket; +import net.tcpshield.tcpshield.abstraction.IPacket; +import net.tcpshield.tcpshield.fabric.mixin.HandshakeC2SPacketAccessor; + +public class FabricPacketImpl implements IPacket { + + private final HandshakeC2SPacket handshake; + + public FabricPacketImpl(HandshakeC2SPacket handshake) { + this.handshake = handshake; + } + + @Override + public String getRawPayload() { + return ((HandshakeC2SPacketAccessor) handshake).getAddress(); + } + + @Override + public void modifyOriginalPacket(String hostname) throws Exception { + // NO OPERATION + } +} \ No newline at end of file diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricPlayerImpl.java b/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricPlayerImpl.java new file mode 100644 index 0000000..1fb5722 --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/impl/FabricPlayerImpl.java @@ -0,0 +1,40 @@ +package net.tcpshield.tcpshield.fabric.impl; + +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.c2s.handshake.HandshakeC2SPacket; +import net.minecraft.network.packet.s2c.login.LoginDisconnectS2CPacket; +import net.minecraft.text.LiteralText; +import net.tcpshield.tcpshield.abstraction.IPlayer; +import net.tcpshield.tcpshield.exception.IPModificationFailureException; +import net.tcpshield.tcpshield.fabric.mixin.ClientConnectionAccessor; + +import java.net.InetSocketAddress; + +public class FabricPlayerImpl implements IPlayer { + + private final ClientConnection connection; + private String ip; + + public FabricPlayerImpl(HandshakeC2SPacket packet, ClientConnection connection) { + this.connection = connection; + this.ip = ((InetSocketAddress) ((ClientConnectionAccessor) connection).getChannel().remoteAddress()).getAddress().getHostAddress(); + } + + @Override + public String getIP() { + return ip; + } + + @Override + public void setIP(InetSocketAddress ip) throws IPModificationFailureException { + // At this point, the IP/connection believe the player has the IP of TCPShield. + // The ip passed into this method contains their CORRECT data, which we have to assign to the player network connection. + ((ClientConnectionAccessor) connection).setAddress(ip); + this.ip = ((InetSocketAddress) ((ClientConnectionAccessor) connection).getChannel().remoteAddress()).getAddress().getHostAddress(); + } + + @Override + public void disconnect() { + connection.disconnect(new LiteralText("Connection failed. Please try again or contact an administrator.")); + } +} \ No newline at end of file diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/mixin/ClientConnectionAccessor.java b/src/main/java/net/tcpshield/tcpshield/fabric/mixin/ClientConnectionAccessor.java new file mode 100644 index 0000000..e725e07 --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/mixin/ClientConnectionAccessor.java @@ -0,0 +1,20 @@ +package net.tcpshield.tcpshield.fabric.mixin; + +import io.netty.channel.Channel; +import net.minecraft.network.ClientConnection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.net.SocketAddress; + +@Mixin(ClientConnection.class) +public interface ClientConnectionAccessor { + @Accessor + void setAddress(SocketAddress address); + + @Accessor + Channel getChannel(); + + @Accessor + SocketAddress getAddress(); +} diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/mixin/HandshakeC2SPacketAccessor.java b/src/main/java/net/tcpshield/tcpshield/fabric/mixin/HandshakeC2SPacketAccessor.java new file mode 100644 index 0000000..fe7e6fa --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/mixin/HandshakeC2SPacketAccessor.java @@ -0,0 +1,11 @@ +package net.tcpshield.tcpshield.fabric.mixin; + +import net.minecraft.network.packet.c2s.handshake.HandshakeC2SPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(HandshakeC2SPacket.class) +public interface HandshakeC2SPacketAccessor { + @Accessor + String getAddress(); +} diff --git a/src/main/java/net/tcpshield/tcpshield/fabric/mixin/ServerHandshakeMixin.java b/src/main/java/net/tcpshield/tcpshield/fabric/mixin/ServerHandshakeMixin.java new file mode 100644 index 0000000..a67c619 --- /dev/null +++ b/src/main/java/net/tcpshield/tcpshield/fabric/mixin/ServerHandshakeMixin.java @@ -0,0 +1,32 @@ +package net.tcpshield.tcpshield.fabric.mixin; + +import net.minecraft.network.ClientConnection; +import net.minecraft.network.packet.c2s.handshake.HandshakeC2SPacket; +import net.minecraft.server.network.ServerHandshakeNetworkHandler; +import net.tcpshield.tcpshield.abstraction.IPacket; +import net.tcpshield.tcpshield.abstraction.IPlayer; +import net.tcpshield.tcpshield.fabric.TCPShieldFabric; +import net.tcpshield.tcpshield.fabric.impl.FabricPacketImpl; +import net.tcpshield.tcpshield.fabric.impl.FabricPlayerImpl; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerHandshakeNetworkHandler.class) +public class ServerHandshakeMixin { + + @Shadow @Final private ClientConnection connection; + + @Inject( + method = "onHandshake", + at = @At("HEAD")) + private void onHandshake(HandshakeC2SPacket handshake, CallbackInfo ci) { + IPacket packet = new FabricPacketImpl(handshake); + IPlayer player = new FabricPlayerImpl(handshake, connection); + + TCPShieldFabric.PACKET_HANDLER.onHandshake(packet, player); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..86bc3d5 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "id": "tcp-shield", + "version": "2.5", + "name": "TCPShield", + "description": "TCPShield IP parsing capabilities for Fabric", + "authors": [ + "TCPShield" + ], + "contact": { + "homepage": "https://tcpshield.com" + }, + "license": "MIT", + "environment": "*", + "entrypoints": { + "main": [ + "net.tcpshield.tcpshield.fabric.TCPShieldFabric" + ] + }, + "mixins": [ + "tcpshield.mixins.json" + ], + "depends": { + "fabricloader": "*", + "minecraft": "*" + } +} diff --git a/src/main/resources/tcpshield.mixins.json b/src/main/resources/tcpshield.mixins.json new file mode 100644 index 0000000..9ad8b4e --- /dev/null +++ b/src/main/resources/tcpshield.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.tcpshield.tcpshield.fabric.mixin", + "compatibilityLevel": "JAVA_8", + "mixins": [ + "ClientConnectionAccessor", + "HandshakeC2SPacketAccessor", + "ServerHandshakeMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}