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