From 683da6ef7d5eea75abcd9cb8cc943c5e89633c86 Mon Sep 17 00:00:00 2001 From: LAX1DUDE Date: Sun, 14 Aug 2022 00:23:10 -0700 Subject: [PATCH] implemented client-side relay query logic --- .../eaglercraft/sp/relay/EaglerSPRelay.java | 5 +- .../adapter/EaglerAdapterImpl2.java | 5 + .../eaglercraft/IntegratedServer.java | 2 + .../net/lax1dude/eaglercraft/RelayQuery.java | 3 + .../eaglercraft/RelayWorldsQuery.java | 17 + .../adapter/EaglerAdapterImpl2.java | 471 +++++++++++++++++- 6 files changed, 492 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/lax1dude/eaglercraft/RelayWorldsQuery.java diff --git a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelay.java b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelay.java index adbf958..cabb04c 100644 --- a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelay.java +++ b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelay.java @@ -321,11 +321,11 @@ public class EaglerSPRelay extends WebSocketServer { cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers)); logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getAttachment()); } - }else if(ipkt.connectionType == 0x02) { + }else if(ipkt.connectionType == 0x03) { logger.debug("[{}]: Pinging the server", arg0.getAttachment()); arg0.send(IPacket.writePacket(new IPacket69Pong(Constants.protocolVersion, config.getComment(), Constants.versionBrand))); arg0.close(); - }else if(ipkt.connectionType == 0x03) { + }else if(ipkt.connectionType == 0x04) { logger.debug("[{}]: Polling the server for other worlds", arg0.getAttachment()); arg0.send(IPacket.writePacket(new IPacket07LocalWorlds(getLocalWorlds(waiting.address)))); arg0.close(); @@ -485,7 +485,6 @@ public class EaglerSPRelay extends WebSocketServer { sock.close(); return false; }else if(l == RateLimit.LOCKOUT) { - sock.send(IPacketFEDisconnectClient.ratelimitPacketLocked); sock.close(); return false; }else { diff --git a/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java b/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java index 10c48b5..3cc4e76 100644 --- a/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java +++ b/src/lwjgl/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java @@ -1742,6 +1742,11 @@ public class EaglerAdapterImpl2 { public VersionMismatch getCompatible() { return VersionMismatch.COMPATIBLE; } + + @Override + public RateLimit isQueryRateLimit() { + return RateLimit.NONE; + } }; diff --git a/src/main/java/net/lax1dude/eaglercraft/IntegratedServer.java b/src/main/java/net/lax1dude/eaglercraft/IntegratedServer.java index 4662f5a..6c4a6ff 100644 --- a/src/main/java/net/lax1dude/eaglercraft/IntegratedServer.java +++ b/src/main/java/net/lax1dude/eaglercraft/IntegratedServer.java @@ -29,6 +29,8 @@ public class IntegratedServer { private static boolean isPaused = false; private static List integratedServerTPS = new LinkedList(); + public static final int preferredRelayVersion = 1; + public static final RelayManager relayManager = new RelayManager(); public static List getTPS() { diff --git a/src/main/java/net/lax1dude/eaglercraft/RelayQuery.java b/src/main/java/net/lax1dude/eaglercraft/RelayQuery.java index 88bda3c..63a9b85 100644 --- a/src/main/java/net/lax1dude/eaglercraft/RelayQuery.java +++ b/src/main/java/net/lax1dude/eaglercraft/RelayQuery.java @@ -1,5 +1,7 @@ package net.lax1dude.eaglercraft; +import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.RateLimit; + public interface RelayQuery { public static enum VersionMismatch { @@ -11,6 +13,7 @@ public interface RelayQuery { public boolean isQueryOpen(); public boolean isQueryFailed(); + public RateLimit isQueryRateLimit(); public void close(); public int getVersion(); diff --git a/src/main/java/net/lax1dude/eaglercraft/RelayWorldsQuery.java b/src/main/java/net/lax1dude/eaglercraft/RelayWorldsQuery.java new file mode 100644 index 0000000..f36eb59 --- /dev/null +++ b/src/main/java/net/lax1dude/eaglercraft/RelayWorldsQuery.java @@ -0,0 +1,17 @@ +package net.lax1dude.eaglercraft; + +import java.util.List; + +import net.lax1dude.eaglercraft.adapter.EaglerAdapterImpl2.RateLimit; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld; + +public interface RelayWorldsQuery { + + public boolean isQueryOpen(); + public boolean isQueryFailed(); + public RateLimit isQueryRateLimit(); + public void close(); + + public List getWorlds(); + +} diff --git a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java index abc34df..05920cf 100644 --- a/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java +++ b/src/teavm/java/net/lax1dude/eaglercraft/adapter/EaglerAdapterImpl2.java @@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -92,6 +93,7 @@ import net.lax1dude.eaglercraft.IntegratedServer; import net.lax1dude.eaglercraft.LocalStorageManager; import net.lax1dude.eaglercraft.PKT; import net.lax1dude.eaglercraft.RelayQuery; +import net.lax1dude.eaglercraft.RelayWorldsQuery; import net.lax1dude.eaglercraft.ServerQuery; import net.lax1dude.eaglercraft.Voice; import net.lax1dude.eaglercraft.adapter.teavm.EaglercraftLANClient; @@ -101,6 +103,12 @@ import net.lax1dude.eaglercraft.adapter.teavm.SelfDefence; import net.lax1dude.eaglercraft.adapter.teavm.WebGL2RenderingContext; import net.lax1dude.eaglercraft.adapter.teavm.WebGLQuery; import net.lax1dude.eaglercraft.adapter.teavm.WebGLVertexArray; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket00Handshake; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket69Pong; +import net.lax1dude.eaglercraft.sp.relay.pkt.IPacketFFErrorCode; import net.minecraft.src.MathHelper; public class EaglerAdapterImpl2 { @@ -2932,8 +2940,222 @@ public class EaglerAdapterImpl2 { } return !isLittleEndian; } + + private static final ArrayBuffer convertToArrayBuffer(byte[] arr) { + Uint8Array buf = Uint8Array.create(arr.length); + buf.set(arr); + return buf.getBuffer(); + } + + private static final Map relayQueryLimited = new HashMap(); + private static final Map relayQueryBlocked = new HashMap(); + + private static class RelayQueryImpl implements RelayQuery { - private static final RelayQuery dummyRelayQuery = new RelayQuery() { + private final WebSocket sock; + private final String uri; + + private boolean open; + private boolean failed; + + private boolean hasRecievedAnyData = false; + + private int vers = -1; + private String comment = ""; + private String brand = ""; + + private long connectionOpenedAt; + private int connectionPing = -1; + + private RateLimit rateLimitStatus = RateLimit.NONE; + + private VersionMismatch versError = VersionMismatch.UNKNOWN; + + private RelayQueryImpl(String uri) { + this.uri = uri; + WebSocket s = null; + try { + connectionOpenedAt = System.currentTimeMillis(); + s = WebSocket.create(uri); + s.setBinaryType("arraybuffer"); + open = true; + failed = false; + }catch(Throwable t) { + connectionOpenedAt = 0l; + sock = null; + open = false; + return; + } + sock = s; + sock.onOpen(new EventListener() { + @Override + public void handleEvent(MessageEvent evt) { + try { + nativeBinarySend(sock, convertToArrayBuffer( + IPacket.writePacket(new IPacket00Handshake(0x03, IntegratedServer.preferredRelayVersion, "")) + )); + } catch (IOException e) { + System.err.println(e.toString()); + sock.close(); + failed = true; + } + } + }); + sock.onMessage(new EventListener() { + @Override + public void handleEvent(MessageEvent evt) { + if(evt.getData() != null && !isString(evt.getData())) { + hasRecievedAnyData = true; + Uint8Array buf = Uint8Array.create(evt.getDataAsArray()); + byte[] arr = new byte[buf.getLength()]; + for(int i = 0; i < arr.length; ++i) { + arr[i] = (byte)buf.get(i); + } + if(arr.length == 2 && arr[0] == (byte)0xFC) { + long millis = System.currentTimeMillis(); + if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) { + rateLimitStatus = RateLimit.BLOCKED; + relayQueryLimited.put(RelayQueryImpl.this.uri, millis); + }else if(arr[1] == (byte)0x02) { + rateLimitStatus = RateLimit.NOW_LOCKED; + relayQueryLimited.put(RelayQueryImpl.this.uri, millis); + relayQueryBlocked.put(RelayQueryImpl.this.uri, millis); + }else { + rateLimitStatus = RateLimit.LOCKED; + relayQueryBlocked.put(RelayQueryImpl.this.uri, millis); + } + failed = true; + open = false; + sock.close(); + }else { + if(open) { + try { + IPacket pkt = IPacket.readPacket(new DataInputStream(new ByteArrayInputStream(arr))); + if(pkt instanceof IPacket69Pong) { + IPacket69Pong ipkt = (IPacket69Pong)pkt; + versError = RelayQuery.VersionMismatch.COMPATIBLE; + connectionPing = (int)(System.currentTimeMillis() - connectionOpenedAt); + vers = ipkt.protcolVersion; + comment = ipkt.comment; + brand = ipkt.brand; + open = false; + failed = false; + sock.close(); + }else if(pkt instanceof IPacketFFErrorCode) { + IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt; + if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) { + String s = ipkt.desc.toLowerCase(); + if(s.contains("outdated client") || s.contains("client outdated")) { + versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED; + }else if(s.contains("outdated server") || s.contains("server outdated") || + s.contains("outdated relay") || s.contains("server relay")) { + versError = RelayQuery.VersionMismatch.RELAY_OUTDATED; + }else { + versError = RelayQuery.VersionMismatch.UNKNOWN; + } + } + System.err.println(uri + ": Recieved query error code " + ipkt.code + ": " + ipkt.desc); + open = false; + failed = true; + sock.close(); + }else { + throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'"); + } + } catch (IOException e) { + System.err.println("Relay Query Error: " + e.toString()); + e.printStackTrace(); + open = false; + failed = true; + sock.close(); + } + } + } + } + } + }); + sock.onClose(new EventListener() { + @Override + public void handleEvent(CloseEvent evt) { + open = false; + if(!hasRecievedAnyData) { + failed = true; + Long l = relayQueryBlocked.get(uri); + if(l != null) { + if(System.currentTimeMillis() - l.longValue() < 400000l) { + rateLimitStatus = RateLimit.LOCKED; + return; + } + } + l = relayQueryLimited.get(uri); + if(l != null) { + if(System.currentTimeMillis() - l.longValue() < 900000l) { + rateLimitStatus = RateLimit.BLOCKED; + return; + } + } + } + } + }); + } + + @Override + public boolean isQueryOpen() { + return open; + } + + @Override + public boolean isQueryFailed() { + return failed; + } + + @Override + public RateLimit isQueryRateLimit() { + return rateLimitStatus; + } + + @Override + public void close() { + if(sock != null && open) { + connectionPing = -1; + sock.close(); + } + open = false; + } + + @Override + public int getVersion() { + return vers; + } + + @Override + public String getComment() { + return comment; + } + + @Override + public String getBrand() { + return brand; + } + + @Override + public long getPing() { + return connectionPing; + } + + @Override + public VersionMismatch getCompatible() { + return versError; + } + + } + + private static class RelayQueryRatelimitDummy implements RelayQuery { + + private final RateLimit type; + + private RelayQueryRatelimitDummy(RateLimit type) { + this.type = type; + } @Override public boolean isQueryOpen() { @@ -2942,7 +3164,12 @@ public class EaglerAdapterImpl2 { @Override public boolean isQueryFailed() { - return false; + return true; + } + + @Override + public RateLimit isQueryRateLimit() { + return type; } @Override @@ -2951,12 +3178,12 @@ public class EaglerAdapterImpl2 { @Override public int getVersion() { - return 1; + return IntegratedServer.preferredRelayVersion; } @Override public String getComment() { - return "this is a dummy"; + return "this query was rate limited"; } @Override @@ -2966,18 +3193,246 @@ public class EaglerAdapterImpl2 { @Override public long getPing() { - return 10l; + return 0l; } @Override public VersionMismatch getCompatible() { return VersionMismatch.COMPATIBLE; } - - }; + + } public static final RelayQuery openRelayQuery(String addr) { - return dummyRelayQuery; + long millis = System.currentTimeMillis(); + + Long l = relayQueryBlocked.get(addr); + if(l != null && millis - l.longValue() < 60000l) { + return new RelayQueryRatelimitDummy(RateLimit.LOCKED); + } + + l = relayQueryLimited.get(addr); + if(l != null && millis - l.longValue() < 10000l) { + return new RelayQueryRatelimitDummy(RateLimit.BLOCKED); + } + + return new RelayQueryImpl(addr); + } + + private static class RelayWorldsQueryImpl implements RelayWorldsQuery { + + private final WebSocket sock; + private final String uri; + + private boolean open; + private boolean failed; + + private boolean hasRecievedAnyData = false; + private RateLimit rateLimitStatus = RateLimit.NONE; + + private RelayQuery.VersionMismatch versError = RelayQuery.VersionMismatch.UNKNOWN; + + private List worlds = null; + + private RelayWorldsQueryImpl(String uri) { + this.uri = uri; + WebSocket s = null; + try { + s = WebSocket.create(uri); + s.setBinaryType("arraybuffer"); + open = true; + failed = false; + }catch(Throwable t) { + sock = null; + open = false; + return; + } + sock = s; + sock.onOpen(new EventListener() { + @Override + public void handleEvent(MessageEvent evt) { + try { + nativeBinarySend(sock, convertToArrayBuffer( + IPacket.writePacket(new IPacket00Handshake(0x04, IntegratedServer.preferredRelayVersion, "")) + )); + } catch (IOException e) { + System.err.println(e.toString()); + sock.close(); + open = false; + failed = true; + } + } + }); + sock.onMessage(new EventListener() { + @Override + public void handleEvent(MessageEvent evt) { + if(evt.getData() != null && !isString(evt.getData())) { + hasRecievedAnyData = true; + Uint8Array buf = Uint8Array.create(evt.getDataAsArray()); + byte[] arr = new byte[buf.getLength()]; + for(int i = 0; i < arr.length; ++i) { + arr[i] = (byte)buf.get(i); + } + if(arr.length == 2 && arr[0] == (byte)0xFC) { + long millis = System.currentTimeMillis(); + if(arr[1] == (byte)0x00 || arr[1] == (byte)0x01) { + rateLimitStatus = RateLimit.BLOCKED; + relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis); + }else if(arr[1] == (byte)0x02) { + rateLimitStatus = RateLimit.NOW_LOCKED; + relayQueryLimited.put(RelayWorldsQueryImpl.this.uri, millis); + relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis); + }else { + rateLimitStatus = RateLimit.LOCKED; + relayQueryBlocked.put(RelayWorldsQueryImpl.this.uri, millis); + } + open = false; + failed = true; + sock.close(); + }else { + if(open) { + try { + IPacket pkt = IPacket.readPacket(new DataInputStream(new ByteArrayInputStream(arr))); + if(pkt instanceof IPacket07LocalWorlds) { + worlds = ((IPacket07LocalWorlds)pkt).worldsList; + sock.close(); + open = false; + failed = false; + }else if(pkt instanceof IPacketFFErrorCode) { + IPacketFFErrorCode ipkt = (IPacketFFErrorCode)pkt; + if(ipkt.code == IPacketFFErrorCode.TYPE_PROTOCOL_VERSION) { + String s = ipkt.desc.toLowerCase(); + if(s.contains("outdated client") || s.contains("client outdated")) { + versError = RelayQuery.VersionMismatch.CLIENT_OUTDATED; + }else if(s.contains("outdated server") || s.contains("server outdated") || + s.contains("outdated relay") || s.contains("server relay")) { + versError = RelayQuery.VersionMismatch.RELAY_OUTDATED; + }else { + versError = RelayQuery.VersionMismatch.UNKNOWN; + } + } + System.err.println(uri + ": Recieved query error code " + ipkt.code + ": " + ipkt.desc); + open = false; + failed = true; + sock.close(); + }else { + throw new IOException("Unexpected packet '" + pkt.getClass().getSimpleName() + "'"); + } + } catch (IOException e) { + System.err.println("Relay World Query Error: " + e.toString()); + e.printStackTrace(); + open = false; + failed = true; + sock.close(); + } + } + } + } + } + }); + sock.onClose(new EventListener() { + @Override + public void handleEvent(CloseEvent evt) { + open = false; + if(!hasRecievedAnyData) { + failed = true; + Long l = relayQueryBlocked.get(uri); + if(l != null) { + if(System.currentTimeMillis() - l.longValue() < 400000l) { + rateLimitStatus = RateLimit.LOCKED; + return; + } + } + l = relayQueryLimited.get(uri); + if(l != null) { + if(System.currentTimeMillis() - l.longValue() < 900000l) { + rateLimitStatus = RateLimit.BLOCKED; + return; + } + } + } + } + }); + } + + @Override + public boolean isQueryOpen() { + return open; + } + + @Override + public boolean isQueryFailed() { + return failed; + } + + @Override + public RateLimit isQueryRateLimit() { + return rateLimitStatus; + } + + @Override + public void close() { + if(open && sock != null) { + sock.close(); + } + open = false; + } + + @Override + public List getWorlds() { + return worlds; + } + + } + + private static class RelayWorldsQueryRatelimitDummy implements RelayWorldsQuery { + + private final RateLimit rateLimit; + + private RelayWorldsQueryRatelimitDummy(RateLimit rateLimit) { + this.rateLimit = rateLimit; + } + + @Override + public boolean isQueryOpen() { + return false; + } + + @Override + public boolean isQueryFailed() { + return true; + } + + @Override + public RateLimit isQueryRateLimit() { + return rateLimit; + } + + @Override + public void close() { + } + + @Override + public List getWorlds() { + return new ArrayList(0); + } + + } + + public static final RelayWorldsQuery openRelayWorldsQuery(String addr) { + long millis = System.currentTimeMillis(); + + Long l = relayQueryBlocked.get(addr); + if(l != null && millis - l.longValue() < 60000l) { + return new RelayWorldsQueryRatelimitDummy(RateLimit.LOCKED); + } + + l = relayQueryLimited.get(addr); + if(l != null && millis - l.longValue() < 10000l) { + return new RelayWorldsQueryRatelimitDummy(RateLimit.BLOCKED); + } + + return new RelayWorldsQueryImpl(addr); } private static EaglercraftLANClient rtcLANClient = null;