diff --git a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPClient.java b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPClient.java index c186395..f31b8e7 100644 --- a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPClient.java +++ b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPClient.java @@ -20,12 +20,14 @@ public class EaglerSPClient { public final long createdOn; public boolean serverNotifiedOfClose = false; public LoginState state = LoginState.INIT; + public final String address; - EaglerSPClient(WebSocket sock, EaglerSPServer srv, String id) { + EaglerSPClient(WebSocket sock, EaglerSPServer srv, String id, String addr) { this.socket = sock; this.server = srv; this.id = id; this.createdOn = System.currentTimeMillis(); + this.address = addr; } public void send(IPacket packet) { 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 96dc50c..adbf958 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 @@ -18,6 +18,7 @@ import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; +import net.lax1dude.eaglercraft.sp.relay.RateLimiter.RateLimit; import net.lax1dude.eaglercraft.sp.relay.pkt.*; import net.lax1dude.eaglercraft.sp.relay.pkt.IPacket07LocalWorlds.LocalWorld; @@ -26,6 +27,9 @@ public class EaglerSPRelay extends WebSocketServer { public static EaglerSPRelay instance; public static final EaglerSPRelayConfig config = new EaglerSPRelayConfig(); + private static RateLimiter pingRateLimiter = null; + private static RateLimiter worldRateLimiter = null; + public static final DebugLogger logger = DebugLogger.getLogger("EaglerSPRelay"); public static void main(String[] args) throws IOException, InterruptedException { @@ -35,9 +39,24 @@ public class EaglerSPRelay extends WebSocketServer { logger.debug("Debug logging enabled"); } } + logger.info("Starting EaglerSPRelay version {}...", Constants.versionName); config.load(new File("relayConfig.ini")); + + if(config.isPingRateLimitEnable()) { + pingRateLimiter = new RateLimiter(config.getPingRateLimitPeriod() * 1000, + config.getPingRateLimitLimit(), config.getPingRateLimitLockoutLimit(), + config.getPingRateLimitLockoutDuration() * 1000); + } + + if(config.isWorldRateLimitEnable()) { + worldRateLimiter = new RateLimiter(config.getWorldRateLimitPeriod() * 1000, + config.getWorldRateLimitLimit(), config.getWorldRateLimitLockoutLimit(), + config.getWorldRateLimitLockoutDuration() * 1000); + } + EaglerSPRelayConfigRelayList.loadRelays(new File("relays.txt")); + logger.info("Starting WebSocket Server..."); instance = new EaglerSPRelay(new InetSocketAddress(config.getAddress(), config.getPort())); instance.setConnectionLostTimeout(20); @@ -45,6 +64,7 @@ public class EaglerSPRelay extends WebSocketServer { instance.start(); Thread tickThread = new Thread((() -> { + int rateLimitUpdateCounter = 0; while(true) { try { long millis = System.currentTimeMillis(); @@ -52,7 +72,7 @@ public class EaglerSPRelay extends WebSocketServer { Iterator> itr = pendingConnections.entrySet().iterator(); while(itr.hasNext()) { Entry etr = itr.next(); - if(millis - etr.getValue().openTime > 1000l) { + if(millis - etr.getValue().openTime > 500l) { etr.getKey().close(); itr.remove(); } @@ -62,16 +82,25 @@ public class EaglerSPRelay extends WebSocketServer { Iterator itr = clientConnections.values().iterator(); while(itr.hasNext()) { EaglerSPClient cl = itr.next(); - if(millis - cl.createdOn > 500l) { + if(millis - cl.createdOn > 5000l) { cl.disconnect(IPacketFEDisconnectClient.TYPE_TIMEOUT, "Took too long to connect!"); } } } + if(++rateLimitUpdateCounter > 300) { + if(pingRateLimiter != null) { + pingRateLimiter.update(); + } + if(worldRateLimiter != null) { + worldRateLimiter.update(); + } + rateLimitUpdateCounter = 0; + } }catch(Throwable t) { logger.error("Error in update loop!"); logger.error(t); } - Util.sleep(50l); + Util.sleep(100l); } }), "Relay Tick"); tickThread.setDaemon(true); @@ -114,6 +143,7 @@ public class EaglerSPRelay extends WebSocketServer { private static final Map clientConnections = new HashMap(); private static final Map serverCodes = new HashMap(); private static final Map serverConnections = new HashMap(); + private static final Map> clientAddressSets = new HashMap(); private static final Map> serverAddressSets = new HashMap(); @Override @@ -131,7 +161,32 @@ public class EaglerSPRelay extends WebSocketServer { }else { addr = arg0.getRemoteSocketAddress().getAddress().getHostAddress().toLowerCase(); } + + int totalCons = 0; + synchronized(pendingConnections) { + Iterator pendingItr = pendingConnections.values().iterator(); + while(pendingItr.hasNext()) { + if(pendingItr.next().address.equals(addr)) { + ++totalCons; + } + } + } + synchronized(clientAddressSets) { + List lst = clientAddressSets.get(addr); + if(lst != null) { + totalCons += lst.size(); + } + } + + if(totalCons >= config.getConnectionsPerIP()) { + logger.debug("[{}]: Too many connections are open on this address", arg0.getAttachment()); + arg0.send(IPacketFEDisconnectClient.ratelimitPacketTooMany); + arg0.close(); + return; + } + arg0.setAttachment(addr); + PendingConnection waiting = new PendingConnection(millis, addr); logger.debug("[{}]: Connection opened", arg0.getRemoteSocketAddress()); synchronized(pendingConnections) { @@ -165,6 +220,21 @@ public class EaglerSPRelay extends WebSocketServer { return; } if(ipkt.connectionType == 0x01) { + if(!rateLimit(worldRateLimiter, arg0, waiting.address)) { + logger.debug("[{}]: Got world ratelimited", arg0.getAttachment()); + return; + } + synchronized(serverAddressSets) { + List lst = serverAddressSets.get(waiting.address); + if(lst != null) { + if(lst.size() >= config.getWorldsPerIP()) { + logger.debug("[{}]: Too many worlds are open on this address", arg0.getAttachment()); + arg0.send(IPacketFEDisconnectClient.ratelimitPacketTooMany); + arg0.close(); + return; + } + } + } logger.debug("[{}]: Connected as a server", arg0.getAttachment()); EaglerSPServer srv; synchronized(serverCodes) { @@ -200,57 +270,71 @@ public class EaglerSPRelay extends WebSocketServer { } srv.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers)); logger.debug("[{}][Relay -> Server] PKT 0x01: Send ICE server list to server", arg0.getAttachment()); - }else if(ipkt.connectionType == 0x02) { - String code = ipkt.connectionCode; - logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getAttachment(), code); - if(code.length() != config.getCodeLength()) { - logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting"); - arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH, - "The join code is the wrong length, it should be " + config.getCodeLength() + " chars long"))); + }else { + if(!rateLimit(pingRateLimiter, arg0, waiting.address)) { + logger.debug("[{}]: Got ping ratelimited", arg0.getAttachment()); + return; + } + if(ipkt.connectionType == 0x02) { + String code = ipkt.connectionCode; + logger.debug("[{}]: Connected as a client, requested server code: {}", arg0.getAttachment(), code); + if(code.length() != config.getCodeLength()) { + logger.debug("The code '{}' is invalid because it's the wrong length, disconnecting"); + arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_CODE_LENGTH, + "The join code is the wrong length, it should be " + config.getCodeLength() + " chars long"))); + arg0.close(); + }else { + EaglerSPServer srv; + synchronized(serverCodes) { + srv = serverCodes.get(code); + } + if(srv == null) { + arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INCORRECT_CODE, + "Invalid code, no LAN world found!"))); + arg0.close(); + return; + } + String id; + EaglerSPClient cl; + synchronized(clientIds) { + int j = 0; + do { + id = EaglerSPClient.generateClientId(); + }while(clientIds.containsKey(id)); + cl = new EaglerSPClient(arg0, srv, id, waiting.address); + clientIds.put(id, cl); + ipkt.connectionCode = id; + arg0.send(IPacket.writePacket(ipkt)); + srv.handleNewClient(cl); + } + synchronized(clientConnections) { + clientConnections.put(arg0, cl); + } + synchronized(clientAddressSets) { + List lst = clientAddressSets.get(cl.address); + if(lst == null) { + lst = new ArrayList(); + clientAddressSets.put(cl.address, lst); + } + lst.add(cl); + } + 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) { + 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) { + logger.debug("[{}]: Polling the server for other worlds", arg0.getAttachment()); + arg0.send(IPacket.writePacket(new IPacket07LocalWorlds(getLocalWorlds(waiting.address)))); arg0.close(); }else { - EaglerSPServer srv; - synchronized(serverCodes) { - srv = serverCodes.get(code); - } - if(srv == null) { - arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_INCORRECT_CODE, - "Invalid code, no LAN world found!"))); - arg0.close(); - return; - } - String id; - EaglerSPClient cl; - synchronized(clientIds) { - int j = 0; - do { - id = EaglerSPClient.generateClientId(); - }while(clientIds.containsKey(id)); - cl = new EaglerSPClient(arg0, srv, id); - clientIds.put(id, cl); - ipkt.connectionCode = id; - arg0.send(IPacket.writePacket(ipkt)); - srv.handleNewClient(cl); - } - synchronized(clientConnections) { - clientConnections.put(arg0, cl); - } - cl.send(new IPacket01ICEServers(EaglerSPRelayConfigRelayList.relayServers)); - logger.debug("[{}][Relay -> Client] PKT 0x01: Send ICE server list to client", arg0.getAttachment()); + logger.debug("[{}]: Unknown connection type: {}", arg0.getAttachment(), ipkt.connectionType); + arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION, + "Unexpected Init Packet"))); + arg0.close(); } - }else if(ipkt.connectionType == 0x02) { - 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) { - logger.debug("[{}]: Polling the server for other worlds", arg0.getAttachment()); - arg0.send(IPacket.writePacket(new IPacket07LocalWorlds(getLocalWorlds(waiting.address)))); - arg0.close(); - }else { - logger.debug("[{}]: Unknown connection type: {}", arg0.getAttachment(), ipkt.connectionType); - arg0.send(IPacket.writePacket(new IPacketFFErrorCode(IPacketFFErrorCode.TYPE_ILLEGAL_OPERATION, - "Unexpected Init Packet"))); - arg0.close(); } }else { logger.debug("[{}]: Pending connection did not send a 0x00 packet to identify " @@ -314,6 +398,15 @@ public class EaglerSPRelay extends WebSocketServer { synchronized(serverCodes) { serverCodes.remove(srv.code); } + synchronized(serverAddressSets) { + List lst = serverAddressSets.get(srv.serverAddress); + if(lst != null) { + lst.remove(srv); + if(lst.size() == 0) { + serverAddressSets.remove(srv.serverAddress); + } + } + } ArrayList clientList; synchronized(clientConnections) { clientList = new ArrayList(clientConnections.values()); @@ -326,18 +419,20 @@ public class EaglerSPRelay extends WebSocketServer { cl.socket.close(); } } - synchronized(serverAddressSets) { - List lst = serverAddressSets.get(srv.serverAddress); - lst.remove(srv); - if(lst.size() == 0) { - serverAddressSets.remove(srv.serverAddress); - } - } }else { EaglerSPClient cl; synchronized(clientConnections) { cl = clientConnections.remove(arg0); } + synchronized(clientAddressSets) { + List lst = clientAddressSets.get(cl.address); + if(lst != null) { + lst.remove(cl); + if(lst.size() == 0) { + clientAddressSets.remove(cl.address); + } + } + } if(cl != null) { logger.debug("[{}]: Client closed, id: {}", arg0.getAttachment(), cl.id); synchronized(clientIds) { @@ -375,5 +470,30 @@ public class EaglerSPRelay extends WebSocketServer { } return lst; } + + private boolean rateLimit(RateLimiter limiter, WebSocket sock, String addr) { + if(limiter != null) { + RateLimit l = limiter.limit(addr); + if(l == RateLimit.NONE) { + return true; + }else if(l == RateLimit.LIMIT) { + sock.send(IPacketFEDisconnectClient.ratelimitPacketBlock); + sock.close(); + return false; + }else if(l == RateLimit.LIMIT_NOW_LOCKOUT) { + sock.send(IPacketFEDisconnectClient.ratelimitPacketBlockLock); + sock.close(); + return false; + }else if(l == RateLimit.LOCKOUT) { + sock.send(IPacketFEDisconnectClient.ratelimitPacketLocked); + sock.close(); + return false; + }else { + return true; // ? + } + }else { + return true; + } + } } diff --git a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelayConfig.java b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelayConfig.java index 417b6e5..f317ab8 100644 --- a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelayConfig.java +++ b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/EaglerSPRelayConfig.java @@ -17,12 +17,22 @@ public class EaglerSPRelayConfig { private int codeLength = 5; private String codeChars = "abcdefghijklmnopqrstuvwxyz0123456789$%&*+?!"; private boolean codeMixCase = false; - private int connectionsPerIP = 256; - private boolean rateLimitEnable = false; - private int rateLimitPeriod = 128; - private int rateLimitLimit = 48; - private int rateLimitLockoutLimit = 64; - private int rateLimitLockoutDuration = 600; + + private int connectionsPerIP = 128; + private int worldsPerIP = 32; + + private boolean openRateLimitEnable = true; + private int openRateLimitPeriod = 192; + private int openRateLimitLimit = 32; + private int openRateLimitLockoutLimit = 48; + private int openRateLimitLockoutDuration = 600; + + private boolean pingRateLimitEnable = true; + private int pingRateLimitPeriod = 256; + private int pingRateLimitLimit = 128; + private int pingRateLimitLockoutLimit = 192; + private int pingRateLimitLockoutDuration = 300; + private String originWhitelist = ""; private String[] originWhitelistArray = new String[0]; private boolean enableRealIpHeader = false; @@ -35,11 +45,17 @@ public class EaglerSPRelayConfig { }else { EaglerSPRelay.logger.info("Loading config file: {}", conf.getAbsoluteFile()); boolean gotPort = false, gotCodeLength = false, gotCodeChars = false; - boolean gotCodeMixCase = false, gotConnectionsPerIP = false; - boolean gotRateLimitEnable = false, gotRateLimitPeriod = false; - boolean gotRateLimitLimit = false, gotRateLimitLockoutLimit = false; - boolean gotRateLimitLockoutDuration = false, gotOriginWhitelist = false; - boolean gotEnableRealIpHeader = false, gotAddress = false, gotComment = false; + boolean gotCodeMixCase = false; + boolean gotConnectionsPerIP = false, gotWorldsPerIP = false, + gotOpenRateLimitEnable = false, gotOpenRateLimitPeriod = false, + gotOpenRateLimitLimit = false, gotOpenRateLimitLockoutLimit = false, + gotOpenRateLimitLockoutDuration = false; + boolean gotPingRateLimitEnable = false, gotPingRateLimitPeriod = false, + gotPingRateLimitLimit = false, gotPingRateLimitLockoutLimit = false, + gotPingRateLimitLockoutDuration = false; + boolean gotOriginWhitelist = false, gotEnableRealIpHeader = false, + gotAddress = false, gotComment = false; + Throwable t2 = null; try(BufferedReader reader = new BufferedReader(new FileReader(conf))) { String s; @@ -86,6 +102,66 @@ public class EaglerSPRelayConfig { t2 = t; break; } + }else if(ss[0].equalsIgnoreCase("worlds-per-ip")) { + try { + worldsPerIP = Integer.parseInt(ss[1]); + gotWorldsPerIP = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid worlds-per-ip {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-enable")) { + try { + openRateLimitEnable = getBooleanValue(ss[1]); + gotOpenRateLimitEnable = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-enable {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-period")) { + try { + openRateLimitPeriod = Integer.parseInt(ss[1]); + gotOpenRateLimitPeriod = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-limit")) { + try { + openRateLimitLimit = Integer.parseInt(ss[1]); + gotOpenRateLimitLimit = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-lockout-limit")) { + try { + openRateLimitLockoutLimit = Integer.parseInt(ss[1]); + gotOpenRateLimitLockoutLimit = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } + }else if(ss[0].equalsIgnoreCase("world-ratelimit-lockout-duration")) { + try { + openRateLimitLockoutDuration = Integer.parseInt(ss[1]); + gotOpenRateLimitLockoutDuration = true; + }catch(Throwable t) { + EaglerSPRelay.logger.warn("Invalid world-ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn(t); + t2 = t; + break; + } }else if(ss[0].equalsIgnoreCase("connections-per-ip")) { try { connectionsPerIP = Integer.parseInt(ss[1]); @@ -96,52 +172,52 @@ public class EaglerSPRelayConfig { t2 = t; break; } - }else if(ss[0].equalsIgnoreCase("ratelimit-enable")) { + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-enable")) { try { - rateLimitEnable = getBooleanValue(ss[1]); - gotRateLimitEnable = true; + pingRateLimitEnable = getBooleanValue(ss[1]); + gotPingRateLimitEnable = true; }catch(Throwable t) { - EaglerSPRelay.logger.warn("Invalid rate-limit-enable {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-enable {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn(t); t2 = t; break; } - }else if(ss[0].equalsIgnoreCase("ratelimit-period")) { + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-period")) { try { - rateLimitPeriod = Integer.parseInt(ss[1]); - gotRateLimitPeriod = true; + pingRateLimitPeriod = Integer.parseInt(ss[1]); + gotPingRateLimitPeriod = true; }catch(Throwable t) { - EaglerSPRelay.logger.warn("Invalid ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-period {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn(t); t2 = t; break; } - }else if(ss[0].equalsIgnoreCase("ratelimit-limit")) { + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-limit")) { try { - rateLimitLimit = Integer.parseInt(ss[1]); - gotRateLimitLimit = true; + pingRateLimitLimit = Integer.parseInt(ss[1]); + gotPingRateLimitLimit = true; }catch(Throwable t) { - EaglerSPRelay.logger.warn("Invalid ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn(t); t2 = t; break; } - }else if(ss[0].equalsIgnoreCase("ratelimit-lockout-limit")) { + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-lockout-limit")) { try { - rateLimitLockoutLimit = Integer.parseInt(ss[1]); - gotRateLimitLockoutLimit = true; + pingRateLimitLockoutLimit = Integer.parseInt(ss[1]); + gotPingRateLimitLockoutLimit = true; }catch(Throwable t) { - EaglerSPRelay.logger.warn("Invalid ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-lockout-limit {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn(t); t2 = t; break; } - }else if(ss[0].equalsIgnoreCase("ratelimit-lockout-duration")) { + }else if(ss[0].equalsIgnoreCase("ping-ratelimit-lockout-duration")) { try { - rateLimitLockoutDuration = Integer.parseInt(ss[1]); - gotRateLimitLockoutDuration = true; + pingRateLimitLockoutDuration = Integer.parseInt(ss[1]); + gotPingRateLimitLockoutDuration = true; }catch(Throwable t) { - EaglerSPRelay.logger.warn("Invalid ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile()); + EaglerSPRelay.logger.warn("Invalid ping-ratelimit-lockout-duration {} in conf {}", ss[1], conf.getAbsoluteFile()); EaglerSPRelay.logger.warn(t); t2 = t; break; @@ -174,10 +250,14 @@ public class EaglerSPRelayConfig { t2 = t; } if(t2 != null || !gotPort || !gotCodeLength || !gotCodeChars || - !gotCodeMixCase || !gotConnectionsPerIP || !gotRateLimitEnable || - !gotRateLimitPeriod || !gotRateLimitLimit || !gotRateLimitLockoutLimit || - !gotRateLimitLockoutDuration || !gotOriginWhitelist || - !gotEnableRealIpHeader || !gotAddress || !gotComment) { + !gotCodeMixCase || !gotWorldsPerIP || !gotOpenRateLimitEnable || + !gotOpenRateLimitPeriod || !gotOpenRateLimitLimit || + !gotOpenRateLimitLockoutLimit || !gotOpenRateLimitLockoutDuration || + !gotConnectionsPerIP || !gotPingRateLimitEnable || + !gotPingRateLimitPeriod || !gotPingRateLimitLimit || + !gotPingRateLimitLockoutLimit || !gotPingRateLimitLockoutDuration || + !gotOriginWhitelist || !gotEnableRealIpHeader || !gotAddress || + !gotComment) { EaglerSPRelay.logger.warn("Updating config file: {}", conf.getAbsoluteFile()); save(conf); } @@ -204,11 +284,17 @@ public class EaglerSPRelayConfig { w.println("code-chars=" + codeChars); w.println("code-mix-case=" + codeMixCase); w.println("connections-per-ip=" + connectionsPerIP); - w.println("ratelimit-enable=" + rateLimitEnable); - w.println("ratelimit-period=" + rateLimitPeriod); - w.println("ratelimit-limit=" + rateLimitLimit); - w.println("ratelimit-lockout-limit=" + rateLimitLockoutLimit); - w.println("ratelimit-lockout-duration=" + rateLimitLockoutDuration); + w.println("ping-ratelimit-enable=" + pingRateLimitEnable); + w.println("ping-ratelimit-period=" + pingRateLimitPeriod); + w.println("ping-ratelimit-limit=" + pingRateLimitLimit); + w.println("ping-ratelimit-lockout-limit=" + pingRateLimitLockoutLimit); + w.println("ping-ratelimit-lockout-duration=" + pingRateLimitLockoutDuration); + w.println("worlds-per-ip=" + worldsPerIP); + w.println("world-ratelimit-enable=" + openRateLimitEnable); + w.println("world-ratelimit-period=" + openRateLimitPeriod); + w.println("world-ratelimit-limit=" + openRateLimitLimit); + w.println("world-ratelimit-lockout-limit=" + openRateLimitLockoutLimit); + w.println("world-ratelimit-lockout-duration=" + openRateLimitLockoutDuration); w.println("origin-whitelist=" + originWhitelist); w.println("enable-real-ip-header=" + enableRealIpHeader); w.print("server-comment=" + serverComment); @@ -252,24 +338,48 @@ public class EaglerSPRelayConfig { return connectionsPerIP; } - public boolean isRateLimitEnable() { - return rateLimitEnable; + public boolean isPingRateLimitEnable() { + return pingRateLimitEnable; } - public int getRateLimitPeriod() { - return rateLimitPeriod; + public int getPingRateLimitPeriod() { + return pingRateLimitPeriod; } - public int getRateLimitLimit() { - return rateLimitLimit; + public int getPingRateLimitLimit() { + return pingRateLimitLimit; } - public int getRateLimitLockoutLimit() { - return rateLimitLockoutLimit; + public int getPingRateLimitLockoutLimit() { + return pingRateLimitLockoutLimit; } - public int getRateLimitLockoutDuration() { - return rateLimitLockoutDuration; + public int getPingRateLimitLockoutDuration() { + return pingRateLimitLockoutDuration; + } + + public int getWorldsPerIP() { + return worldsPerIP; + } + + public boolean isWorldRateLimitEnable() { + return openRateLimitEnable; + } + + public int getWorldRateLimitPeriod() { + return openRateLimitPeriod; + } + + public int getWorldRateLimitLimit() { + return openRateLimitLimit; + } + + public int getWorldRateLimitLockoutLimit() { + return openRateLimitLockoutLimit; + } + + public int getWorldRateLimitLockoutDuration() { + return openRateLimitLockoutDuration; } public String getOriginWhitelist() { @@ -279,6 +389,23 @@ public class EaglerSPRelayConfig { public String[] getOriginWhitelistArray() { return originWhitelistArray; } + + public boolean getIsWhitelisted(String domain) { + domain = domain.toLowerCase(); + for(int i = 0; i < originWhitelistArray.length; ++i) { + String etr = originWhitelistArray[i].toLowerCase(); + if(etr.startsWith("*")) { + if(domain.endsWith(etr.substring(1))) { + return true; + } + }else { + if(domain.equals(etr)) { + return true; + } + } + } + return false; + } public boolean isEnableRealIpHeader() { return enableRealIpHeader; diff --git a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/RateLimiter.java b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/RateLimiter.java new file mode 100644 index 0000000..dc5ebe7 --- /dev/null +++ b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/RateLimiter.java @@ -0,0 +1,113 @@ +package net.lax1dude.eaglercraft.sp.relay; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class RateLimiter { + + private final int period; + private final int limit; + private final int lockoutLimit; + private final int lockoutDuration; + + private class RateLimitEntry { + + protected long timer; + protected int count; + protected long lockedTimer; + protected boolean locked; + + protected RateLimitEntry() { + timer = System.currentTimeMillis(); + count = 0; + lockedTimer = 0l; + locked = false; + } + + protected void update() { + long millis = System.currentTimeMillis(); + if(locked) { + if(millis - lockedTimer > RateLimiter.this.lockoutDuration) { + timer = millis; + count = 0; + lockedTimer = 0l; + locked = false; + } + }else { + long p = RateLimiter.this.period; + int breaker = 0; + while(millis - timer > p) { + timer += p; + --count; + if(count < 0 || ++breaker > 100) { + timer = millis; + count = 0; + break; + } + } + } + } + + } + + public static enum RateLimit { + NONE, LIMIT, LIMIT_NOW_LOCKOUT, LOCKOUT; + } + + private final Map limiters = new HashMap(); + + public RateLimiter(int period, int limit, int lockoutLimit, int lockoutDuration) { + this.period = period; + this.limit = limit; + this.lockoutLimit = lockoutLimit; + this.lockoutDuration = lockoutDuration; + } + + public RateLimit limit(String addr) { + synchronized(this) { + RateLimitEntry etr = limiters.get(addr); + + if(etr == null) { + etr = new RateLimitEntry(); + limiters.put(addr, etr); + }else { + etr.update(); + } + + if(etr.locked) { + return RateLimit.LOCKOUT; + } + + ++etr.count; + if(etr.count >= lockoutLimit) { + etr.count = 0; + etr.locked = true; + etr.lockedTimer = System.currentTimeMillis(); + return RateLimit.LIMIT_NOW_LOCKOUT; + }else if(etr.count > limit) { + return RateLimit.LIMIT; + }else { + return RateLimit.NONE; + } + } + } + + public void update() { + synchronized(this) { + Iterator itr = limiters.values().iterator(); + while(itr.hasNext()) { + if(itr.next().count == 0) { + itr.remove(); + } + } + } + } + + public void reset() { + synchronized(this) { + limiters.clear(); + } + } + +} diff --git a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/pkt/IPacketFEDisconnectClient.java b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/pkt/IPacketFEDisconnectClient.java index b39288d..34d6163 100644 --- a/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/pkt/IPacketFEDisconnectClient.java +++ b/sp-relay/src/main/java/net/lax1dude/eaglercraft/sp/relay/pkt/IPacketFEDisconnectClient.java @@ -3,6 +3,7 @@ package net.lax1dude.eaglercraft.sp.relay.pkt; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.nio.ByteBuffer; public class IPacketFEDisconnectClient extends IPacket { @@ -41,5 +42,10 @@ public class IPacketFEDisconnectClient extends IPacket { public int packetLength() { return -1; } + + public static final ByteBuffer ratelimitPacketTooMany = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x00 }); + public static final ByteBuffer ratelimitPacketBlock = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x01 }); + public static final ByteBuffer ratelimitPacketBlockLock = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x02 }); + public static final ByteBuffer ratelimitPacketLocked = ByteBuffer.wrap(new byte[] { (byte)0xFC, (byte)0x03 }); }