diff --git a/README.md b/README.md index b62865b..87db49b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # ayungee -lightweight bungeecord alternative for eaglercraft servers running protocolsupport +~~lightweight~~ bungeecord alternative for eaglercraft servers running protocolsupport + +now contains an optional **login system**!! :D Thanks to LAX1DUDE and md-5 for very small snippets of EaglerBungee used in this project (specifically for the server icon, skins, & entity remapping) -**TODO: built-in auth system & more bungee backwards compatibility** +**TODO: more bungee backwards compatibility, a few more kick messages/reasons, scoreboard & tab clearing, & automatic java player skins** -**if you have questions about the license, please, reach out to me. i just put the license i put on most of my projects on this, but if you have a problem with it, let me know. +**if you have questions about the license, please, reach out to me. i just put the license i put on most of my projects on this, but if you have a problem with it, **let me know**. \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9397094..7b9f81a 100644 --- a/pom.xml +++ b/pom.xml @@ -29,6 +29,11 @@ json-simple 1.1.1 + + de.mkammerer + argon2-jvm + 2.11 + diff --git a/src/main/java/me/ayunami2000/ayungee/Auth.java b/src/main/java/me/ayunami2000/ayungee/Auth.java new file mode 100644 index 0000000..fcfceec --- /dev/null +++ b/src/main/java/me/ayunami2000/ayungee/Auth.java @@ -0,0 +1,118 @@ +package me.ayunami2000.ayungee; + +import de.mkammerer.argon2.Argon2; +import de.mkammerer.argon2.Argon2Factory; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +public class Auth { + private static class AuthData { + public String passHash; + public Set ips; + + public AuthData(String p, Set i) { + passHash = p; + ips = i; + } + } + + private static final Argon2 argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id); + + private static Map database = new HashMap<>(); + + public static boolean register(String username, char[] password, String ip) { + AuthData authData = database.get(username); + if (authData != null) return false; + if (isIpAtTheLimit(ip)) return false; + String hash = argon2.hash(10, 65536, 1, password); + Set initIps = new HashSet<>(); + initIps.add(ip); + database.put(username, new AuthData(hash, initIps)); + writeDatabase(); + return true; + // todo: registering & packet cancellation + } + + public static boolean isRegistered(String username) { + return database.containsKey(username); + } + + public static boolean changePass(String username, char[] password) { + AuthData authData = database.get(username); + authData.passHash = argon2.hash(10, 65536, 1, password); + writeDatabase(); + return true; + } + + public static boolean login(String username, char[] password) { + AuthData authData = database.get(username); + if (authData == null) return false; + return argon2.verify(authData.passHash, password); + } + + private static boolean isIpAtTheLimit(String ip) { + if (Main.authIpLimit <= 0) return false; + Map cache = new HashMap<>(database); + int num = 0; + for (AuthData authData : cache.values()) { + if (authData.ips.contains(ip)) num++; + if (num >= Main.authIpLimit) { + cache.clear(); + return true; + } + } + cache.clear(); + return false; + } + + // only use once, on load + public static void readDatabase() { + try { + File authFile = new File("auth.uwu"); + if (!authFile.exists()) authFile.createNewFile(); + + Map cache = new HashMap<>(); + + String[] lines = new String(Files.readAllBytes(authFile.toPath())).trim().split("\n"); + if (lines.length == 1 && lines[0].isEmpty()) return; + for (String line : lines) { + String[] pieces = line.split("\u0000"); + cache.put(pieces[0], new AuthData(pieces[2], new HashSet<>(Arrays.asList(pieces[1].split("§"))))); + } + + database.clear(); + database.putAll(cache); + cache.clear(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void writeDatabase() { + StringBuilder out = new StringBuilder(); + + Map cache = new HashMap<>(database); + + for (String username : cache.keySet()) { + AuthData entry = cache.get(username); + out.append(username); + out.append("\u0000"); + out.append(String.join("§", entry.ips)); + out.append("\u0000"); + out.append(entry.passHash); + out.append("\n"); + } + + cache.clear(); + + try { + Files.write(Paths.get("auth.uwu"), out.toString().getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/me/ayunami2000/ayungee/ChatHandler.java b/src/main/java/me/ayunami2000/ayungee/ChatHandler.java index 6b4a9f6..2689cfd 100644 --- a/src/main/java/me/ayunami2000/ayungee/ChatHandler.java +++ b/src/main/java/me/ayunami2000/ayungee/ChatHandler.java @@ -1,6 +1,5 @@ package me.ayunami2000.ayungee; -import java.io.IOException; import java.nio.ByteBuffer; public class ChatHandler { @@ -14,9 +13,15 @@ public class ChatHandler { if (!message.startsWith("/")) return false; int ind = message.indexOf(' '); String commandBase = message.substring(1, ind != -1 ? ind : message.length()).toLowerCase(); - String args = ind != -1 ? message.substring(ind + 1) : ""; + String args = ind == -1 ? "" : message.substring(ind + 1); + int ind2 = args.indexOf(' '); + // todo: make it an array at this point dumbass + String firstArg = ind2 == -1 ? args : args.substring(0, ind); + String secondArg = args.substring(firstArg.length()); switch (commandBase) { case "server": + if (!client.authed) return false; + if (args.isEmpty()) { //usage msg client.conn.send(new byte[] { 3, 0, 25, 0, (byte) 167, 0, 57, 0, 85, 0, 115, 0, 97, 0, 103, 0, 101, 0, 58, 0, 32, 0, 47, 0, 115, 0, 101, 0, 114, 0, 118, 0, 101, 0, 114, 0, 32, 0, 60, 0, 110, 0, 117, 0, 109, 0, 98, 0, 101, 0, 114, 0, 62 }); @@ -30,14 +35,70 @@ public class ChatHandler { } } break; - /* case "register": - + case "reg": + if (!Main.useAuth) return false; + if (client.authed) return false; + if (firstArg.isEmpty()) { + client.conn.send(new byte[] { 3, 0, 30, 0, (byte) 167, 0, 57, 0, 89, 0, 111, 0, 117, 0, 32, 0, 109, 0, 117, 0, 115, 0, 116, 0, 32, 0, 115, 0, 112, 0, 101, 0, 99, 0, 105, 0, 102, 0, 121, 0, 32, 0, 97, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 46 }); + return true; + } + if (!firstArg.equals(secondArg)) { + client.conn.send(new byte[] { 3, 0, 31, 0, (byte) 167, 0, 57, 0, 84, 0, 104, 0, 111, 0, 115, 0, 101, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 115, 0, 32, 0, 100, 0, 111, 0, 32, 0, 110, 0, 111, 0, 116, 0, 32, 0, 109, 0, 97, 0, 116, 0, 99, 0, 104, 0, 33 }); + return true; + } + if (Auth.register(client.username, firstArg.toCharArray(), Main.getIp(client.conn))) { + Main.printMsg("Player " + client + " registered successfully!"); + client.authed = true; + client.server = -1; + } else { + client.conn.send(new byte[] { (byte) 255, 0, 38, 0, (byte) 167, 0, 57, 0, 84, 0, 104, 0, 105, 0, 115, 0, 32, 0, 117, 0, 115, 0, 101, 0, 114, 0, 110, 0, 97, 0, 109, 0, 101, 0, 32, 0, 105, 0, 115, 0, 32, 0, 97, 0, 108, 0, 114, 0, 101, 0, 97, 0, 100, 0, 121, 0, 32, 0, 114, 0, 101, 0, 103, 0, 105, 0, 115, 0, 116, 0, 101, 0, 114, 0, 101, 0, 100, 0, 33 }); + client.conn.close(); + } break; case "login": - + case "l": + if (!Main.useAuth) return false; + if (client.authed) return false; + if (firstArg.isEmpty()) { + client.conn.send(new byte[] { 3, 0, 30, 0, (byte) 167, 0, 57, 0, 89, 0, 111, 0, 117, 0, 32, 0, 109, 0, 117, 0, 115, 0, 116, 0, 32, 0, 115, 0, 112, 0, 101, 0, 99, 0, 105, 0, 102, 0, 121, 0, 32, 0, 97, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 46 }); + return true; + } + if (Auth.login(client.username, firstArg.toCharArray())) { + Main.printMsg("Player " + client + " logged in!"); + client.authed = true; + } else { + client.conn.send(new byte[] { (byte) 255, 0, 29, 0, (byte) 167, 0, 57, 0, 84, 0, 104, 0, 97, 0, 116, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 32, 0, 105, 0, 115, 0, 32, 0, 105, 0, 110, 0, 99, 0, 111, 0, 114, 0, 114, 0, 101, 0, 99, 0, 116, 0, 33 }); + client.conn.close(); + } + break; + case "changepass": + case "changepassword": + case "changeepasswd": + case "changepwd": + case "changepw": + if (!Main.useAuth) return false; + if (!client.authed) return false; + if (firstArg.isEmpty() || secondArg.isEmpty()) { + client.conn.send(new byte[] { 3, 0, 63, 0, (byte) 167, 0, 57, 0, 80, 0, 108, 0, 101, 0, 97, 0, 115, 0, 101, 0, 32, 0, 115, 0, 112, 0, 101, 0, 99, 0, 105, 0, 102, 0, 121, 0, 32, 0, 116, 0, 104, 0, 101, 0, 32, 0, 111, 0, 108, 0, 100, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 32, 0, 102, 0, 111, 0, 108, 0, 108, 0, 111, 0, 119, 0, 101, 0, 100, 0, 32, 0, 98, 0, 121, 0, 32, 0, 116, 0, 104, 0, 101, 0, 32, 0, 110, 0, 101, 0, 119, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 33 }); + return true; + } + if (!Auth.isRegistered(client.username)) { + // register first... + client.conn.send(new byte[] { 3, 0, 60, 0, (byte) 167, 0, 57, 0, 89, 0, 111, 0, 117, 0, 32, 0, 109, 0, 117, 0, 115, 0, 116, 0, 32, 0, 114, 0, 101, 0, 103, 0, 105, 0, 115, 0, 116, 0, 101, 0, 114, 0, 32, 0, 97, 0, 110, 0, 32, 0, 97, 0, 99, 0, 99, 0, 111, 0, 117, 0, 110, 0, 116, 0, 32, 0, 102, 0, 105, 0, 114, 0, 115, 0, 116, 0, 32, 0, 116, 0, 111, 0, 32, 0, 99, 0, 104, 0, 97, 0, 110, 0, 103, 0, 101, 0, 32, 0, 105, 0, 116, 0, 115, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 33 }); + return true; + } + if (!Auth.login(client.username, firstArg.toCharArray())) { + // invalid old password + client.conn.send(new byte[] { 3, 0, 29, 0, (byte) 167, 0, 57, 0, 84, 0, 104, 0, 97, 0, 116, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 32, 0, 105, 0, 115, 0, 32, 0, 105, 0, 110, 0, 99, 0, 111, 0, 114, 0, 114, 0, 101, 0, 99, 0, 116, 0, 33 }); + return true; + } + if (Auth.changePass(client.username, secondArg.toCharArray())) { + // changed password! + Main.printMsg("Player " + client + " changed their password!"); + client.conn.send(new byte[] { 3, 0, 33, 0, (byte) 167, 0, 57, 0, 89, 0, 111, 0, 117, 0, 114, 0, 32, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 32, 0, 104, 0, 97, 0, 115, 0, 32, 0, 98, 0, 101, 0, 101, 0, 110, 0, 32, 0, 99, 0, 104, 0, 97, 0, 110, 0, 103, 0, 101, 0, 100, 0, 33 }); + } break; - */ default: return false; } diff --git a/src/main/java/me/ayunami2000/ayungee/Client.java b/src/main/java/me/ayunami2000/ayungee/Client.java index 76d1562..355240e 100644 --- a/src/main/java/me/ayunami2000/ayungee/Client.java +++ b/src/main/java/me/ayunami2000/ayungee/Client.java @@ -28,9 +28,14 @@ public class Client { public boolean hasLoginHappened = false; + public boolean authed = !Main.useAuth; + public int clientEntityId; public int serverEntityId; + public List packetCache = new ArrayList<>(); + public byte[] positionPacket = null; + public void setSocket(Socket sock) throws IOException { socket = sock; socketOut = sock.getOutputStream(); diff --git a/src/main/java/me/ayunami2000/ayungee/Main.java b/src/main/java/me/ayunami2000/ayungee/Main.java index c7b3c31..6b53f61 100644 --- a/src/main/java/me/ayunami2000/ayungee/Main.java +++ b/src/main/java/me/ayunami2000/ayungee/Main.java @@ -24,6 +24,11 @@ public class Main { public static boolean forwarded = false; + public static boolean filterUsernames = true; + + public static boolean useAuth = false; + public static int authIpLimit = -1; + public static WebSocketServer webSocketServer = null; public static Map clients = new HashMap<>(); @@ -112,6 +117,19 @@ public class Main { } } + Map configAuth = new HashMap<>(); + configAuth.put("enabled", false); + configAuth.put("ip_limit", -1); + + configAuth = (LinkedHashMap) config.getOrDefault("auth", configAuth); + + useAuth = (boolean) configAuth.getOrDefault("enabled", false); + + if (useAuth) { + authIpLimit = (int) configAuth.getOrDefault("ip_limit", 0); + Auth.readDatabase(); + } + webPort = (int) config.getOrDefault("web_port", 25565); forwarded = (boolean) config.getOrDefault("forwarded", false); @@ -279,6 +297,10 @@ public class Main { break; } */ + if (!targetUser.authed) { + printMsg("That user is not yet authenticated!"); + break; + } try { int destServer = Integer.parseInt(pieces[2]); targetUser.server = Math.max(0, Math.min(servers.size() - 1, destServer)); diff --git a/src/main/java/me/ayunami2000/ayungee/PluginMessages.java b/src/main/java/me/ayunami2000/ayungee/PluginMessages.java index f89b1b4..1554586 100644 --- a/src/main/java/me/ayunami2000/ayungee/PluginMessages.java +++ b/src/main/java/me/ayunami2000/ayungee/PluginMessages.java @@ -14,6 +14,7 @@ public class PluginMessages { try { String bungeeTag = dataIn.readUTF(); if (bungeeTag.equals("Connect")) { // actually send current player to server :D + if (!client.authed) return true; String destServer = dataIn.readUTF(); try { int destServerInt = Integer.parseInt(destServer); diff --git a/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java b/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java index 3ce1e3c..5b74212 100644 --- a/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java +++ b/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java @@ -53,9 +53,9 @@ public class WebSocketProxy extends WebSocketServer { } new Thread(() -> { try { - Thread.sleep(5000); + Thread.sleep(10000); } catch (InterruptedException ignored) {} - if (conn.isOpen() && (Main.bans.contains(Main.getIp(conn)) || !Main.clients.containsKey(conn))) conn.close(); + if (conn.isOpen() && (Main.bans.contains(Main.getIp(conn)) || !Main.clients.containsKey(conn) || !Main.clients.get(conn).authed)) conn.close(); }).start(); } @@ -113,6 +113,10 @@ public class WebSocketProxy extends WebSocketServer { StringBuilder unameBuilder = new StringBuilder(); for (int i = 0; i < unameLen; i++) unameBuilder.append(message.getChar()); String username = unameBuilder.toString(); + if (!username.equals(username.replaceAll("[^A-Za-z0-9_-]", "_"))) { + conn.close(); + return; + } if (Main.clients.values().stream().anyMatch(client -> client.username.equals(username) || client.conn == conn)) { conn.close(); return; @@ -123,9 +127,16 @@ public class WebSocketProxy extends WebSocketServer { try { while (conn.isOpen()) { int currServer = selfClient.server; + if (currServer == -1 && selfClient.authed) currServer = selfClient.server = 0; selfClient.hasLoginHappened = false; - if (!selfClient.firstTime) Main.printMsg("Player " + selfClient + " joined server " + selfClient.server + "!"); + if (!selfClient.firstTime) Main.printMsg("Player " + selfClient + " joined server " + currServer + "!"); ServerItem chosenServer = Main.servers.get(currServer); + /* + if (chosenServer.host.equals(Main.authKey)) { + // todo: custom server here + return; + } + */ Socket selfSocket = new Socket(); try { // todo: pregenerate InetSocketAddresses @@ -154,6 +165,7 @@ public class WebSocketProxy extends WebSocketServer { } if (ChatHandler.serverChatMessage(selfClient, data)) continue; if (PluginMessages.serverPluginMessage(selfClient, data)) continue; + if (!selfClient.authed && data[0] == 13) selfClient.positionPacket = data; boolean loginPacket = data[0] == 1; if (loginPacket && !selfClient.hasLoginHappened) selfClient.hasLoginHappened = true; if (selfClient.firstTime && loginPacket) selfClient.clientEntityId = selfClient.serverEntityId = EntityMap.readInt(data, 1); @@ -182,7 +194,17 @@ public class WebSocketProxy extends WebSocketServer { if (conn.isOpen()) conn.send(new byte[] { 9, 0, 0, 0, -1, 0, 0, 1, 0, 0, 7, 0, 100, 0, 101, 0, 102, 0, 97, 0, 117, 0, 108, 0, 116 }); } EntityMap.rewrite(data, selfClient.serverEntityId, selfClient.clientEntityId); - if (conn.isOpen()) conn.send(data); + if (selfClient.authed || ((loginPacket || data[0] == 51 || data[0] == 13 || data[0] == 6) || !selfClient.hasLoginHappened)) { + if (selfClient.authed) { + while (selfClient.packetCache.size() > 0) { + if (conn.isOpen()) conn.send(selfClient.packetCache.remove(0)); + } + } + if (conn.isOpen()) conn.send(data); + } else { + // cache data + selfClient.packetCache.add(data); + } if (loginPacket) sendToServer(new byte[] { (byte) 250, 0, 8, 0, 82, 0, 69, 0, 71, 0, 73, 0, 83, 0, 84, 0, 69, 0, 82, 0, 10, 66, 117, 110, 103, 101, 101, 67, 111, 114, 100 }, selfClient); } if (conn.isOpen() && selfClient.server == currServer) conn.close(); @@ -216,6 +238,13 @@ public class WebSocketProxy extends WebSocketServer { } public void sendToServer(byte[] orig, Client client) throws IOException { + if (!client.authed) { + if (orig.length > 0) { + if (orig[0] == 2 || orig[0] == 3) client.conn.send(new byte[] { 3, 0, 114, 0, (byte) 167, 0, 57, 0, 80, 0, 108, 0, 101, 0, 97, 0, 115, 0, 101, 0, 32, 0, 114, 0, 101, 0, 103, 0, 105, 0, 115, 0, 116, 0, 101, 0, 114, 0, 32, 0, 111, 0, 114, 0, 32, 0, 108, 0, 111, 0, 103, 0, 105, 0, 110, 0, 32, 0, 116, 0, 111, 0, 32, 0, 99, 0, 111, 0, 110, 0, 116, 0, 105, 0, 110, 0, 117, 0, 101, 0, 32, 0, 116, 0, 111, 0, 32, 0, 116, 0, 104, 0, 105, 0, 115, 0, 32, 0, 115, 0, 101, 0, 114, 0, 118, 0, 101, 0, 114, 0, 33, 0, 32, 0, 47, 0, 114, 0, 101, 0, 103, 0, 105, 0, 115, 0, 116, 0, 101, 0, 114, 0, 32, 0, 60, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 62, 0, 32, 0, 60, 0, 99, 0, 111, 0, 110, 0, 102, 0, 105, 0, 114, 0, 109, 0, 80, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 62, 0, 32, 0, 111, 0, 114, 0, 32, 0, 47, 0, 108, 0, 111, 0, 103, 0, 105, 0, 110, 0, 32, 0, 60, 0, 112, 0, 97, 0, 115, 0, 115, 0, 119, 0, 111, 0, 114, 0, 100, 0, 62 }); + if (orig[0] == 10 || orig[0] == 11 || orig[0] == 12 || orig[0] == 13 || orig[0] == 14 || orig[0] == 15) client.conn.send(client.positionPacket); + } + if (client.hasLoginHappened) return; // drop client packets :trol: + } byte[] data = orig.clone(); EntityMap.rewrite(data, client.clientEntityId, client.serverEntityId); client.socketOut.write(data); diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 971bcc4..ca50028 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -11,6 +11,12 @@ origin_blacklist: "https://g.eags.us/eaglercraft/origin_blacklist.txt" # for example, # - "https://g.eags.us" origins: [] +# authentication info +auth: + # use auth + enabled: false + # max registers per ip (set to 0 for unlimited, not recommended, but necessary for servers that cannot see user IPs) + ip_limit: 0 # motd info motd: # the motd itself diff --git a/target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar b/target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar index 0b71fe9..8b98881 100644 Binary files a/target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar and b/target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar differ