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
+ }
+}