diff --git a/src/main/java/me/ayunami2000/ayungee/ChatHandler.java b/src/main/java/me/ayunami2000/ayungee/ChatHandler.java new file mode 100644 index 0000000..9da05c0 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayungee/ChatHandler.java @@ -0,0 +1,81 @@ +package me.ayunami2000.ayungee; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class ChatHandler { + // return true to cancel being sent to server/client + + public static boolean fromServer(Client client, String message) { + return false; + } + + public static boolean fromClient(Client client, String message) { + 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) : ""; + switch (commandBase) { + case "server": + 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 }); + } else { + try { + int destServer = Integer.parseInt(args); + client.server = Math.max(0, Math.min(Main.servers.size() - 1, destServer)); + try { + client.socket.close(); + } catch (IOException ignored) {} + } catch (NumberFormatException e) { + //not a number + client.conn.send(new byte[] { 3, 0, 29, 0, (byte) 167, 0, 57, 0, 84, 0, 104, 0, 97, 0, 116, 0, 32, 0, 105, 0, 115, 0, 32, 0, 110, 0, 111, 0, 116, 0, 32, 0, 97, 0, 32, 0, 118, 0, 97, 0, 108, 0, 105, 0, 100, 0, 32, 0, 110, 0, 117, 0, 109, 0, 98, 0, 101, 0, 114, 0, 33 }); + } + } + break; + /* + case "register": + + break; + case "login": + + break; + */ + default: + return false; + } + return true; + } + + public static boolean serverChatMessage(Client client, byte[] packet) { + return fireChatMessage(client, true, packet); + } + + public static boolean clientChatMessage(Client client, byte[] packet) { + return fireChatMessage(client, false, packet); + } + + private static boolean fireChatMessage(Client client, boolean fromServer, byte[] packet) { + if (packet.length >= 3 && packet[0] == 3) { + ByteBuffer bb = ByteBuffer.wrap(packet); + bb.get(); + int msgLen = bb.getShort(); //(short) ((msg[1] << 8) + msg[2] & 0xff); + if (msgLen >= 0) { + if (packet.length >= 3 + msgLen * 2) { + //byte[] chatBytes = new byte[msgLen]; + //for (int i = 0; i < chatBytes.length; i++) chatBytes[i] = msg[4 + i * 2]; + //String chatMsg = new String(chatBytes); + StringBuilder chatBuilder = new StringBuilder(); + for (int i = 0; i < msgLen; i++) chatBuilder.append(bb.getChar()); + String chatMsg = chatBuilder.toString(); + if (fromServer) { + return fromServer(client, chatMsg); + } else { + return fromClient(client, chatMsg); + } + } + } + } + return false; + } +} diff --git a/src/main/java/me/ayunami2000/ayungee/Client.java b/src/main/java/me/ayunami2000/ayungee/Client.java index 6ebf653..76d1562 100644 --- a/src/main/java/me/ayunami2000/ayungee/Client.java +++ b/src/main/java/me/ayunami2000/ayungee/Client.java @@ -1,5 +1,7 @@ package me.ayunami2000.ayungee; +import org.java_websocket.WebSocket; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -14,6 +16,8 @@ public class Client { public List msgCache =new ArrayList<>(); + public WebSocket conn; + public String username; public int server = 0; @@ -33,7 +37,12 @@ public class Client { socketIn = sock.getInputStream(); } - public Client(String uname) { + public Client(WebSocket c, String uname) { + conn = c; username = uname; } + + public String toString() { + return username + " (" + Main.getIp(conn) + ")"; + } } diff --git a/src/main/java/me/ayunami2000/ayungee/Main.java b/src/main/java/me/ayunami2000/ayungee/Main.java index 9349782..b0d014c 100644 --- a/src/main/java/me/ayunami2000/ayungee/Main.java +++ b/src/main/java/me/ayunami2000/ayungee/Main.java @@ -14,7 +14,6 @@ import java.nio.file.StandardCopyOption; import java.time.Instant; import java.util.*; import java.util.regex.Pattern; -import java.util.stream.Collectors; public class Main { public static List servers = new ArrayList<>(); @@ -92,7 +91,7 @@ public class Main { Files.copy(cc.getInputStream(), Paths.get("origin_blacklist.txt"), StandardCopyOption.REPLACE_EXISTING); readUrlBlacklist(); } catch (IOException e) { - System.out.println("An error occurred attempting to update the origin blacklist!"); + printMsg("An error occurred attempting to update the origin blacklist!"); } try { Thread.sleep(300000); @@ -161,16 +160,49 @@ public class Main { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); boolean running = true; - System.out.println("ayungee started!"); + printMsg("ayungee started!"); while (running) { - //System.out.print("> "); String cmd = reader.readLine(); String[] pieces = cmd.split(" "); pieces[0] = pieces[0].toLowerCase(); switch (pieces[0]) { case "help": case "?": - System.out.println("help ; unban ; banip ; ban ; send ; stop"); + printMsg("help ; unban ; banip ; ban ; kickip ; kick ; send ; stop"); + break; + case "kick": + if (pieces.length == 1) { + printMsg("Usage: " + pieces[0] + " "); + break; + } + //there should NEVER be duplicate usernames... + Client[] targetClientss = clients.values().stream().filter(client -> client.username.equals(pieces[1])).toArray(Client[]::new); + if (targetClientss.length == 0) targetClientss = clients.values().stream().filter(client -> client.username.equalsIgnoreCase(pieces[1])).toArray(Client[]::new); + if (targetClientss.length == 0) { + printMsg("Unable to find any user with that username!"); + break; + } + for (Client targetClient : targetClientss) { + targetClient.conn.close(); + printMsg("Successfully kicked user " + targetClient); + } + break; + case "kickip": + case "kick-ip": + if (pieces.length == 1) { + printMsg("Usage: " + pieces[0] + " "); + break; + } + //there should NEVER be duplicate usernames... + Client[] targetClientsss = clients.values().stream().filter(client -> getIp(client.conn).equals(pieces[1])).toArray(Client[]::new); + if (targetClientsss.length == 0) { + printMsg("Unable to find any user with that IP!"); + break; + } + for (Client targetClient : targetClientsss) { + targetClient.conn.close(); + printMsg("Successfully kicked user " + targetClient); + } break; case "unban": case "pardon": @@ -179,103 +211,105 @@ public class Main { case "pardon-ip": case "pardonip": if (pieces.length == 1) { - System.out.println("Usage: " + pieces[0] + " "); + printMsg("Usage: " + pieces[0] + " "); break; } if (bans.remove(pieces[1])) { - System.out.println("Successfully unbanned IP " + pieces[1]); + printMsg("Successfully unbanned IP " + pieces[1]); saveBans(); } else { - System.out.println("IP " + pieces[1] + " is not banned!"); + printMsg("IP " + pieces[1] + " is not banned!"); } break; case "ban": if (pieces.length == 1) { - System.out.println("Usage: " + pieces[0] + " "); + printMsg("Usage: " + pieces[0] + " "); break; } //there should NEVER be duplicate usernames... Client[] targetClients = clients.values().stream().filter(client -> client.username.equals(pieces[1])).toArray(Client[]::new); if (targetClients.length == 0) targetClients = clients.values().stream().filter(client -> client.username.equalsIgnoreCase(pieces[1])).toArray(Client[]::new); if (targetClients.length == 0) { - System.out.println("Unable to find any user with that username! (note: they must be online)"); + printMsg("Unable to find any user with that username! (note: they must be online)"); break; } for (Client targetClient : targetClients) { - WebSocket targetWebSocket = getKeysByValue(clients, targetClient).stream().findFirst().orElse(null); - if (targetWebSocket == null) { - System.out.println("An internal error occurred which should never happen! Oops..."); - return; - } + WebSocket targetWebSocket = targetClient.conn; String ipToBan = getIp(targetWebSocket); if (bans.add(ipToBan)) { - System.out.println("Successfully banned user " + targetClient.username + " with IP " + ipToBan); + printMsg("Successfully banned user " + targetClient.username + " with IP " + ipToBan); try { saveBans(); } catch (IOException ignored) {} } else { - System.out.println("IP " + ipToBan + " is already banned!"); + printMsg("IP " + ipToBan + " is already banned!"); } } break; case "ban-ip": case "banip": if (pieces.length == 1) { - System.out.println("Usage: " + pieces[0] + " "); + printMsg("Usage: " + pieces[0] + " "); break; } if (bans.add(pieces[1])) { - System.out.println("Successfully banned IP " + pieces[1]); + Client[] targetClientssss = clients.values().stream().filter(client -> getIp(client.conn).equals(pieces[1])).toArray(Client[]::new); + for (Client client : targetClientssss) client.conn.close(); + printMsg("Successfully banned IP " + pieces[1]); saveBans(); } else { - System.out.println("IP " + pieces[1] + " is already banned!"); + printMsg("IP " + pieces[1] + " is already banned!"); } break; case "send": case "server": if (pieces.length == 1 || pieces.length == 2) { - System.out.println("Usage: " + pieces[0] + " "); + printMsg("Usage: " + pieces[0] + " "); break; } Client targetUser = clients.values().stream().filter(client -> client.username.equals(pieces[1])).findFirst().orElse(clients.values().stream().filter(client -> client.username.equalsIgnoreCase(pieces[1])).findFirst().orElse(null)); if (targetUser == null) { - System.out.println("Unable to find any user with that username!"); + printMsg("Unable to find any user with that username!"); break; } + // 99% sure don't need to worry about this + /* + if (!targetUser.hasLoginHappened) { + printMsg("This user is still logging in to a server; please wait until they have logged in to change their server!"); + break; + } + */ try { int destServer = Integer.parseInt(pieces[2]); targetUser.server = Math.max(0, Math.min(servers.size() - 1, destServer)); + targetUser.socket.close(); + printMsg("Successfully send user " + targetUser + " to server " + destServer + "!"); } catch (NumberFormatException e) { - System.out.println("That is not a valid number!"); + printMsg("That is not a valid number!"); } break; case "stop": case "end": case "exit": case "quit": - System.out.println("Stopping!"); + printMsg("Stopping!"); running = false; webSocketServer.stop(10); System.exit(0); break; default: - System.out.println("Command not found!"); + printMsg("Command not found!"); } } } - public static String getIp(WebSocket conn) { - return conn.getAttachment(); + public static void printMsg(String msg) { + System.out.println(msg); + //System.out.print("> "); // todo: copy current input to after the > } - // https://stackoverflow.com/a/2904266/6917520 - - public static Set getKeysByValue(Map map, E value) { - return map.entrySet() - .stream() - .filter(entry -> Objects.equals(entry.getValue(), value)) - .map(Map.Entry::getKey) - .collect(Collectors.toSet()); + public static String getIp(WebSocket conn) { + return conn.getAttachment(); } private static void saveBans() throws IOException { diff --git a/src/main/java/me/ayunami2000/ayungee/PluginMessages.java b/src/main/java/me/ayunami2000/ayungee/PluginMessages.java new file mode 100644 index 0000000..f89b1b4 --- /dev/null +++ b/src/main/java/me/ayunami2000/ayungee/PluginMessages.java @@ -0,0 +1,65 @@ +package me.ayunami2000.ayungee; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +public class PluginMessages { + // return true to cancel being sent to server/client + + public static boolean fromServer(Client client, String name, byte[] data) { + if (name.equals("BungeeCord")) { // eat all bungeecord messages + DataInputStream dataIn = new DataInputStream(new ByteArrayInputStream(data)); + try { + String bungeeTag = dataIn.readUTF(); + if (bungeeTag.equals("Connect")) { // actually send current player to server :D + String destServer = dataIn.readUTF(); + try { + int destServerInt = Integer.parseInt(destServer); + client.server = Math.max(0, Math.min(Main.servers.size() - 1, destServerInt)); + } catch (NumberFormatException ignored) {} + } + } catch (IOException e) { + // broken packet + } + return true; + } + return false; + } + + public static boolean fromClient(Client client, String name, byte[] data) { + return Skins.setSkin(client.username, client.conn, name, data); + } + + public static boolean serverPluginMessage(Client client, byte[] packet) { + return firePluginMessage(client, true, packet); + } + + public static boolean clientPluginMessage(Client client, byte[] packet) { + return firePluginMessage(client, false, packet); + } + + private static boolean firePluginMessage(Client client, boolean fromServer, byte[] packet) { + if(packet.length >= 3 && packet[0] == (byte) 250) { + ByteBuffer bb = ByteBuffer.wrap(packet); + bb.get(); + int tagLen = bb.getShort(); + if (tagLen < 0 || tagLen * 2 > packet.length - 3) return false; + StringBuilder tagBuilder = new StringBuilder(); + for (int i = 0; i < tagLen; i++) tagBuilder.append(bb.getChar()); + //int dataLen = bb.getShort(); + int dataLen = bb.remaining() - 2; // - 2 for the 2 bytes specifying data length + String tag = tagBuilder.toString(); + int offset = 3 + tagLen * 2 + 2; // - 2 for the 2 bytes specifying data length + byte[] msg = new byte[dataLen]; + System.arraycopy(packet, offset, msg, 0, dataLen); + if (fromServer) { + return fromServer(client, tag, msg); + } else { + return fromClient(client, tag, msg); + } + } + return false; + } +} diff --git a/src/main/java/me/ayunami2000/ayungee/Skins.java b/src/main/java/me/ayunami2000/ayungee/Skins.java index 872bed3..fa5e473 100644 --- a/src/main/java/me/ayunami2000/ayungee/Skins.java +++ b/src/main/java/me/ayunami2000/ayungee/Skins.java @@ -4,7 +4,6 @@ import org.java_websocket.WebSocket; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; -import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -16,21 +15,9 @@ public class Skins { private static final int[] SKIN_DATA_SIZE = new int[] { 64*32*4, 64*64*4, -9, -9, 1, 64*64*4, -9 }; // 128 pixel skins crash clients private static final int[] CAPE_DATA_SIZE = new int[] { 32*32*4, -9, 1 }; - public static boolean setSkin(String user, WebSocket conn, byte[] initMsg) { - if(initMsg.length >= 3) { + public static boolean setSkin(String user, WebSocket conn, String tag, byte[] msg) { + if(msg.length >= 3) { try { - ByteBuffer bb = ByteBuffer.wrap(initMsg); - bb.get(); - int tagLen = bb.getShort(); - if (!(tagLen >= 0 && tagLen < initMsg.length - 1)) return false; - StringBuilder tagBuilder = new StringBuilder(); - for (int i = 0; i < tagLen; i++) tagBuilder.append(bb.getChar()); - //int dataLen = bb.getShort(); - int dataLen = bb.remaining() - 2; - String tag = tagBuilder.toString(); - int offset = 3 + tagLen * 2 + 2; - byte[] msg = new byte[dataLen]; - System.arraycopy(initMsg, offset, msg, 0, dataLen); if("EAG|MySkin".equals(tag)) { if(!skinCollection.containsKey(user)) { int t = (int)msg[0] & 0xFF; diff --git a/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java b/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java index b4ca237..0349339 100644 --- a/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java +++ b/src/main/java/me/ayunami2000/ayungee/WebSocketProxy.java @@ -62,7 +62,7 @@ public class WebSocketProxy extends WebSocketServer { public void onClose(WebSocket conn, int code, String reason, boolean remote) { Client selfClient = Main.clients.remove(conn); if (selfClient != null) { - System.out.println("Player " + selfClient.username + " (" + Main.getIp(conn) + ") left!"); + Main.printMsg("Player " + selfClient + " left!"); Skins.removeSkin(selfClient.username); if (selfClient.socket.isClosed()) { try { @@ -97,31 +97,33 @@ public class WebSocketProxy extends WebSocketServer { if (!conn.isOpen()) return; byte[] msg = message.array(); if (!Main.clients.containsKey(conn)) { - if (msg.length > 3 && msg[1] == (byte) 69) { - // todo: it uses shorts dumbass, get with the system - short unameLen = (short) ((msg[2] << 8) + msg[3] & 0xff); + if (msg.length > 3 && msg[0] == 2 && msg[1] == 69) { + message.get(); + message.get(); + short unameLen = message.getShort(); if (unameLen < 3 || unameLen > 16) { conn.close(); return; } - if (msg.length < 5 + msg[3] * 2) { + if (msg.length < 5 + unameLen * 2) { conn.close(); return; } - byte[] uname = new byte[unameLen]; - for (int i = 0; i < uname.length; i++) uname[i] = msg[5 + i * 2]; - String username = new String(uname); - if (Main.clients.values().stream().anyMatch(client -> client.username.equals(username))) { + StringBuilder unameBuilder = new StringBuilder(); + for (int i = 0; i < unameLen; i++) unameBuilder.append(message.getChar()); + String username = unameBuilder.toString(); + if (Main.clients.values().stream().anyMatch(client -> client.username.equals(username) || client.conn == conn)) { conn.close(); return; } - Client selfClient = new Client(username); + Client selfClient = new Client(conn, username); Main.clients.put(conn, selfClient); new Thread(() -> { try { while (conn.isOpen()) { int currServer = selfClient.server; selfClient.hasLoginHappened = false; + if (!selfClient.firstTime) Main.printMsg("Player " + selfClient + " joined server " + selfClient.server + "!"); ServerItem chosenServer = Main.servers.get(currServer); Socket selfSocket = new Socket(chosenServer.host, chosenServer.port); selfClient.setSocket(selfSocket); @@ -142,12 +144,16 @@ public class WebSocketProxy extends WebSocketServer { } else { continue; } - if (data[0] == 1 && !selfClient.hasLoginHappened) selfClient.hasLoginHappened = true; - if (selfClient.firstTime && data[0] == 1) selfClient.clientEntityId = selfClient.serverEntityId = EntityMap.readInt(data, 1); - if (!selfClient.firstTime && data[0] == 1) { + if (ChatHandler.serverChatMessage(selfClient, data)) continue; + if (PluginMessages.serverPluginMessage(selfClient, data)) continue; + boolean loginPacket = data[0] == 1; + if (loginPacket && !selfClient.hasLoginHappened) selfClient.hasLoginHappened = true; + if (selfClient.firstTime && loginPacket) selfClient.clientEntityId = selfClient.serverEntityId = EntityMap.readInt(data, 1); + if (!selfClient.firstTime && loginPacket) { selfClient.serverEntityId = EntityMap.readInt(data, 1); // assume server is giving valid data; we don't have to validate it because it isn't a potentially malicious client - byte[] worldByte = new byte[data[6] * 2 + 2]; + short worldByteLen = (short) ((data[5] << 8) + data[6] & 0xff); + byte[] worldByte = new byte[worldByteLen * 2 + 2]; System.arraycopy(data, 5, worldByte, 0, worldByte.length); byte gamemode = data[worldByte.length + 5]; byte dimension = data[worldByte.length + 6]; @@ -169,6 +175,7 @@ public class WebSocketProxy extends WebSocketServer { } EntityMap.rewrite(data, selfClient.serverEntityId, selfClient.clientEntityId); if (conn.isOpen()) conn.send(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(); if (!selfSocket.isClosed()) selfSocket.close(); @@ -181,42 +188,15 @@ public class WebSocketProxy extends WebSocketServer { }).start(); msg[1] = (byte) 61; selfClient.handshake = msg; - System.out.println("Player " + username + " (" + Main.getIp(conn) + ") joined!"); + Main.printMsg("Player " + selfClient + " joined!"); } else { conn.close(); return; } } Client currClient = Main.clients.get(conn); - if (msg.length >= 3 && msg[0] == 3) { - int msgLen = (short) ((msg[1] << 8) + msg[2] & 0xff); - if (msgLen != 0) { - if (msg.length >= 3 + msgLen * 2) { - byte[] chatBytes = new byte[msgLen]; - for (int i = 0; i < chatBytes.length; i++) chatBytes[i] = msg[4 + i * 2]; - String chatMsg = new String(chatBytes); - if (chatMsg.toLowerCase().startsWith("/server")) { - String msgArgs = chatMsg.substring(7 + (chatMsg.contains(" ") ? 1 : 0)); - if (msgArgs.isEmpty()) { - //usage msg - 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 }); - } else { - try { - int destServer = Integer.parseInt(msgArgs); - currClient.server = Math.max(0, Math.min(Main.servers.size() - 1, destServer)); - } catch (NumberFormatException e) { - //not a number - conn.send(new byte[] { 3, 0, 29, 0, (byte) 167, 0, 57, 0, 84, 0, 104, 0, 97, 0, 116, 0, 32, 0, 105, 0, 115, 0, 32, 0, 110, 0, 111, 0, 116, 0, 32, 0, 97, 0, 32, 0, 118, 0, 97, 0, 108, 0, 105, 0, 100, 0, 32, 0, 110, 0, 117, 0, 109, 0, 98, 0, 101, 0, 114, 0, 33 }); - } - } - return; // don't send to underlying server - } - } - } - } - if (msg.length > 0 && msg[0] == (byte) 250) { - if (Skins.setSkin(currClient.username, conn, msg)) return; - } + if (ChatHandler.clientChatMessage(currClient, msg)) return; + if (PluginMessages.clientPluginMessage(currClient, msg)) return; if (!currClient.firstTime && !currClient.hasLoginHappened && !(msg[0] == 1 || msg[0] == 2)) return; if (currClient.socketOut == null || currClient.socket.isOutputShutdown()) { currClient.msgCache.add(msg); diff --git a/target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar b/target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar index 3fb828b..879581d 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