make it actually connect servers now :D

crazy right??!?!?!???!
This commit is contained in:
ayunami2000 2022-07-06 16:38:13 -04:00
parent 75423d4ea5
commit 9c9d3c28eb
10 changed files with 359 additions and 44 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
web
target/*
!target/ayungee-1.0-SNAPSHOT-jar-with-dependencies.jar
test

View File

@ -2,6 +2,10 @@
lightweight bungeecord alternative for eaglercraft servers running protocolsupport
Thanks to LAX1DUDE for very small snippets of EaglerBungee used in this project (specifically for the server icon)
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: skins & capes**
**TODO: ability for plugins to change a player's server & built-in auth system**
**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.

View File

@ -16,10 +16,17 @@ public class Client {
public String username;
public int server = 0;
public byte[] handshake = null;
public int clientEntityId;
public int serverEntityId;
public void setSocket(Socket sock) throws IOException {
socket = sock;
socketOut = sock.getOutputStream();
socketIn = socket.getInputStream();
socketIn = sock.getInputStream();
}
public Client(String uname) {

View File

@ -0,0 +1,81 @@
package me.ayunami2000.ayungee;
// https://github.com/LAX1DUDE/eaglercraft/blob/a8d5c856de28ba2a263abc055d7b26d50dc2bf7e/eaglercraftbungee/src/main/java/net/md_5/bungee/EntityMap.java
public class EntityMap {
public static final int[][] entityIds;
public static void rewrite(final byte[] packet, final int oldId, final int newId) {
final int packetId = packet[0] & 0xFF;
if (packetId == 29) {
for (int pos = 2; pos < (short) ((packet[1] << 8) + packet[2] & 0xff); pos += 4) {
final int readId = readInt(packet, pos);
if (readId == oldId) {
setInt(packet, pos, newId);
} else if (readId == newId) {
setInt(packet, pos, oldId);
}
}
} else {
final int[] idArray = EntityMap.entityIds[packetId];
if (idArray != null) {
for (final int pos2 : idArray) {
final int readId2 = readInt(packet, pos2);
if (readId2 == oldId) {
setInt(packet, pos2, newId);
} else if (readId2 == newId) {
setInt(packet, pos2, oldId);
}
}
}
}
if (packetId == 23) {
final int type = packet[5] & 0xFF;
if (type == 60 || type == 90) {
final int index20 = readInt(packet, 20);
if (packet.length > 24 && index20 == oldId) {
setInt(packet, 20, newId);
}
}
}
}
public static void setInt(final byte[] buf, final int pos, final int i) {
buf[pos] = (byte) (i >> 24);
buf[pos + 1] = (byte) (i >> 16);
buf[pos + 2] = (byte) (i >> 8);
buf[pos + 3] = (byte) i;
}
public static int readInt(final byte[] buf, final int pos) {
return (buf[pos] & 0xFF) << 24 | (buf[pos + 1] & 0xFF) << 16 | (buf[pos + 2] & 0xFF) << 8 | (buf[pos + 3] & 0xFF);
}
static {
(entityIds = new int[256][])[5] = new int[] { 1 };
EntityMap.entityIds[7] = new int[] { 1, 5 };
EntityMap.entityIds[17] = new int[] { 1 };
EntityMap.entityIds[18] = new int[] { 1 };
EntityMap.entityIds[19] = new int[] { 1 };
EntityMap.entityIds[20] = new int[] { 1 };
EntityMap.entityIds[22] = new int[] { 1, 5 };
EntityMap.entityIds[23] = new int[] { 1 };
EntityMap.entityIds[24] = new int[] { 1 };
EntityMap.entityIds[25] = new int[] { 1 };
EntityMap.entityIds[26] = new int[] { 1 };
EntityMap.entityIds[28] = new int[] { 1 };
EntityMap.entityIds[30] = new int[] { 1 };
EntityMap.entityIds[31] = new int[] { 1 };
EntityMap.entityIds[32] = new int[] { 1 };
EntityMap.entityIds[33] = new int[] { 1 };
EntityMap.entityIds[34] = new int[] { 1 };
EntityMap.entityIds[35] = new int[] { 1 };
EntityMap.entityIds[38] = new int[] { 1 };
EntityMap.entityIds[39] = new int[] { 1, 5 };
EntityMap.entityIds[40] = new int[] { 1 };
EntityMap.entityIds[41] = new int[] { 1 };
EntityMap.entityIds[42] = new int[] { 1 };
EntityMap.entityIds[55] = new int[] { 1 };
EntityMap.entityIds[71] = new int[] { 1 };
}
}

View File

@ -7,9 +7,7 @@ import org.yaml.snakeyaml.Yaml;
import javax.imageio.ImageIO;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
@ -19,8 +17,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class Main {
public static String hostname = "localhost";
public static int port = 25569;
public static List<ServerItem> servers = new ArrayList<>();
public static int webPort = 25565;
public static String motdJson = "";
@ -28,8 +25,6 @@ public class Main {
public static boolean forwarded = false;
public static boolean eaglerPackets = false;
public static WebSocketServer webSocketServer = null;
public static Map<WebSocket, Client> clients = new HashMap<>();
@ -107,12 +102,19 @@ public class Main {
}).start();
}
servers.add(new ServerItem("localhost", 25569));
List<String> stringServers = (List<String>) config.getOrDefault("servers", new ArrayList<>());
if (!stringServers.isEmpty()) {
servers.clear();
for (String serverEntry : stringServers) {
servers.add(new ServerItem(serverEntry));
}
}
hostname = (String) config.getOrDefault("hostname", "localhost");
port = (int) config.getOrDefault("port", 25569);
webPort = (int) config.getOrDefault("web_port", 25565);
forwarded = (boolean) config.getOrDefault("forwarded", false);
eaglerPackets = (boolean) config.getOrDefault("eag_packets", false);
List<String> defaultMotd = new ArrayList<>();
@ -168,7 +170,7 @@ public class Main {
switch (pieces[0]) {
case "help":
case "?":
System.out.println("help ; unban <ip> ; banip <ip> ; ban <username> ; stop");
System.out.println("help ; unban <ip> ; banip <ip> ; ban <username> ; send <username> <serverid> ; stop");
break;
case "unban":
case "pardon":
@ -229,6 +231,24 @@ public class Main {
System.out.println("IP " + pieces[1] + " is already banned!");
}
break;
case "send":
case "server":
if (pieces.length == 1 || pieces.length == 2) {
System.out.println("Usage: " + pieces[0] + " <username> <serverindex>");
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!");
break;
}
try {
int destServer = Integer.parseInt(pieces[2]);
targetUser.server = Math.max(0, Math.min(servers.size() - 1, destServer));
} catch (NumberFormatException e) {
System.out.println("That is not a valid number!");
}
break;
case "stop":
case "end":
case "exit":

View File

@ -0,0 +1,17 @@
package me.ayunami2000.ayungee;
public class ServerItem {
public String host;
public int port = 25565;
public ServerItem(String h, int p) {
host = h;
port = p;
}
public ServerItem(String hp) {
String[] pieces = hp.split(":");
if (pieces.length > 1) port = Integer.parseInt(pieces[1]);
host = pieces[0];
}
}

View File

@ -0,0 +1,115 @@
package me.ayunami2000.ayungee;
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;
public class Skins {
private static final HashMap<String,byte[]> skinCollection = new HashMap();
private static final HashMap<String,byte[]> capeCollection = new HashMap();
private static final HashMap<String,Long> lastSkinLayerUpdate = new HashMap();
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) {
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;
if(t >= 0 && t < SKIN_DATA_SIZE.length && msg.length == (SKIN_DATA_SIZE[t] + 1)) {
skinCollection.put(user, msg);
}
}
}else if("EAG|MyCape".equals(tag)) {
if(!capeCollection.containsKey(user)) {
int t = (int)msg[0] & 0xFF;
if(t >= 0 && t < CAPE_DATA_SIZE.length && msg.length == (CAPE_DATA_SIZE[t] + 2)) {
capeCollection.put(user, msg);
}
}
}else if("EAG|FetchSkin".equals(tag)) {
if(msg.length > 2) {
String fetch = new String(msg, 2, msg.length - 2, StandardCharsets.UTF_8);
byte[] data;
if((data = skinCollection.get(fetch)) != null) {
byte[] conc = new byte[data.length + 2];
conc[0] = msg[0]; conc[1] = msg[1]; //synchronization cookie
System.arraycopy(data, 0, conc, 2, data.length);
if((data = capeCollection.get(fetch)) != null) {
byte[] conc2 = new byte[conc.length + data.length];
System.arraycopy(conc, 0, conc2, 0, conc.length);
System.arraycopy(data, 0, conc2, conc.length, data.length);
conc = conc2;
}
byte[] packetPrefix = new byte[] { (byte) 250, 0, 12, 0, 69, 0, 65, 0, 71, 0, 124, 0, 85, 0, 115, 0, 101, 0, 114, 0, 83, 0, 107, 0, 105, 0, 110, (byte) ((conc.length >>> 8) & 0xFF), (byte) (conc.length & 0xFF) };
byte[] fullPacket = new byte[packetPrefix.length + conc.length];
System.arraycopy(packetPrefix, 0, fullPacket, 0, packetPrefix.length);
System.arraycopy(conc, 0, fullPacket, packetPrefix.length, conc.length);
conn.send(fullPacket);
}
}
}else if("EAG|SkinLayers".equals(tag)) {
long millis = System.currentTimeMillis();
Long lsu = lastSkinLayerUpdate.get(user);
if(lsu != null && millis - lsu.longValue() < 700l) { // DoS protection
return false;
}
lastSkinLayerUpdate.put(user, millis);
byte[] data;
if((data = capeCollection.get(user)) != null) {
data[1] = msg[0];
}else {
data = new byte[] { (byte)2, msg[0], (byte)0 };
capeCollection.put(user, data);
}
ByteArrayOutputStream bao = new ByteArrayOutputStream();
DataOutputStream dd = new DataOutputStream(bao);
dd.write(msg[0]);
dd.writeUTF(user);
byte[] bpacket = bao.toByteArray();
byte[] packetPrefix = new byte[] { (byte) 250, 0, 14, 0, 69, 0, 65, 0, 71, 0, 124, 0, 83, 0, 107, 0, 105, 0, 110, 0, 76, 0, 97, 0, 121, 0, 101, 0, 114, 0, 115, (byte) ((bpacket.length >>> 8) & 0xFF), (byte) (bpacket.length & 0xFF) };
int off = bpacket.length == 0 ? 2 : 0;
byte[] fullPacket = new byte[(packetPrefix.length - off) + bpacket.length];
System.arraycopy(packetPrefix, 0, fullPacket, 0, packetPrefix.length - off);
if (bpacket.length != 0) System.arraycopy(bpacket, 0, fullPacket, packetPrefix.length, bpacket.length);
for (WebSocket pl : Main.clients.keySet()) {
if (pl.equals(conn)) continue;
if (pl.isOpen()) pl.send(fullPacket);
}
} else {
return false;
}
}catch(Throwable t) {
// hacker
}
} else {
return false;
}
return true;
}
public static void removeSkin(String username) {
skinCollection.remove(username);
capeCollection.remove(username);
lastSkinLayerUpdate.remove(username);
}
}

View File

@ -63,6 +63,7 @@ public class WebSocketProxy extends WebSocketServer {
Client selfClient = Main.clients.remove(conn);
if (selfClient != null) {
System.out.println("Player " + selfClient.username + " (" + Main.getIp(conn) + ") left!");
Skins.removeSkin(selfClient.username);
if (selfClient.socket.isClosed()) {
try {
selfClient.socket.close();
@ -88,7 +89,7 @@ public class WebSocketProxy extends WebSocketServer {
}
@Override
public void onMessage(WebSocket conn, ByteBuffer message) {
public void onMessage(WebSocket conn, ByteBuffer message) { // todo: make use of the fact that it's already a bytebuffer dumbass
if (Main.bans.contains(Main.getIp(conn))) {
conn.close();
return;
@ -97,60 +98,132 @@ public class WebSocketProxy extends WebSocketServer {
byte[] msg = message.array();
if (!Main.clients.containsKey(conn)) {
if (msg.length > 3 && msg[1] == (byte) 69) {
if (msg[3] < 3 || msg[3] > 16) {
// todo: it uses shorts dumbass, get with the system
short unameLen = (short) ((msg[2] << 8) + msg[3] & 0xff);
if (unameLen < 3 || unameLen > 16) {
conn.close();
return;
}
byte[] uname = new byte[msg[3]];
if (msg.length < 5 + msg[3] * 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);
Main.clients.put(conn, new Client(username));
Client selfClient = new Client(username);
Main.clients.put(conn, selfClient);
new Thread(() -> {
try {
Socket selfSocket = new Socket(Main.hostname, Main.port);
Client selfClient = Main.clients.get(conn);
selfClient.setSocket(selfSocket);
while (selfClient.msgCache.size() > 0) selfClient.socketOut.write(selfClient.msgCache.remove(0));
while (conn.isOpen() && !selfSocket.isInputShutdown()) {
byte[] data = new byte[maxBuffSize];
int read = selfClient.socketIn.read(data, 0, maxBuffSize);
if (read == maxBuffSize) {
boolean firstTime = true;
while (conn.isOpen()) {
int currServer = selfClient.server;
ServerItem chosenServer = Main.servers.get(currServer);
Socket selfSocket = new Socket(chosenServer.host, chosenServer.port);
selfClient.setSocket(selfSocket);
if (!firstTime) sendToServer(selfClient.handshake, selfClient);
while (selfClient.msgCache.size() > 0) sendToServer(selfClient.msgCache.remove(0), selfClient);
while (conn.isOpen() && !selfSocket.isInputShutdown() && selfClient.server == currServer) {
byte[] dataa = new byte[maxBuffSize];
int read = selfClient.socketIn.read(dataa, 0, maxBuffSize);
byte[] data;
if (read == maxBuffSize) {
data = dataa;
} else if (read > 0) {
data = new byte[read];
System.arraycopy(dataa, 0, data, 0, read);
} else {
continue;
}
if (firstTime && data[0] == 1) selfClient.clientEntityId = selfClient.serverEntityId = EntityMap.readInt(data, 1);
if (!firstTime && data[0] == 1) {
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];
System.arraycopy(data, 5, worldByte, 0, worldByte.length);
byte gamemode = data[worldByte.length + 5];
byte dimension = data[worldByte.length + 6];
byte difficulty = data[worldByte.length + 7];
Arrays.fill(data, (byte) 0);
data[0] = 9;
EntityMap.setInt(data, 1, dimension);
data[5] = difficulty;
data[6] = gamemode;
data[7] = (byte)(256 & 0xff);
data[8] = (byte)((256 >> 8) & 0xff);
System.arraycopy(worldByte, 0, data, 9, worldByte.length);
read = 9 + worldByte.length;
byte[] trimData = new byte[read];
System.arraycopy(data, 0, trimData, 0, read);
data = trimData;
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 });
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);
} else if (read > 0) {
byte[] trueData = new byte[read];
System.arraycopy(data, 0, trueData, 0, read);
if (conn.isOpen()) conn.send(trueData);
}
if (conn.isOpen() && selfClient.server == currServer) conn.close();
if (!selfSocket.isClosed()) selfSocket.close();
selfClient.socketOut = null;
firstTime = false;
}
if (conn.isOpen()) conn.close();
if (!selfSocket.isClosed()) selfSocket.close();
} catch (IOException ex) {
conn.close();
}
}).start();
msg[1] = (byte) 61;
selfClient.handshake = msg;
System.out.println("Player " + username + " (" + Main.getIp(conn) + ") joined!");
} else {
conn.close();
return;
}
}
byte[] packet = message.array();
if (!Main.eaglerPackets && packet.length >= 11 && packet[0] == -6 && packet[2] >= 4 && packet[4] == 69 && packet[6] == 65 && packet[8] == 71 && packet[10] == 124) return; // EAG|
Client currClient = Main.clients.get(conn);
if (currClient.socketOut == null) {
currClient.msgCache.add(packet);
} else if (!currClient.socket.isOutputShutdown()) {
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 (currClient.socketOut == null || currClient.socket.isOutputShutdown()) {
currClient.msgCache.add(msg);
} else {
try {
currClient.socketOut.write(packet);
sendToServer(msg, currClient);
} catch (IOException ignored) {}
}
}
public void sendToServer(byte[] orig, Client client) throws IOException {
byte[] data = orig.clone();
EntityMap.rewrite(data, client.clientEntityId, client.serverEntityId);
client.socketOut.write(data);
}
@Override
public void onError(WebSocket conn, Exception ex) {
//

View File

@ -1,13 +1,10 @@
# mc server ip
hostname: localhost
# mc server port
port: 25569
# mc servers in network. first one is default/hub
servers:
- localhost:25569
# ayungee port
web_port: 25565
# if this is behind a reverse proxy, such as caddy or nginx. uses X-Real-IP header.
forwarded: false
# forward eagler-specific packets?
eag_packets: false
# origin blacklist URL (leave empty to disable syncing)
origin_blacklist: "https://g.eags.us/eaglercraft/origin_blacklist.txt"
# whitelisted origins -- if specified, only allows the listed origins to connect