You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
fix(rcon): rate limit remote command bursts
This commit is contained in:
@@ -153,6 +153,10 @@ public final class Emulator {
|
||||
Emulator.config.register("camera.price.points.type", "5");
|
||||
Emulator.config.register("camera.render.delay", "5");
|
||||
Emulator.config.register("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||
Emulator.config.register("rcon.rate_limit.enabled", "1");
|
||||
Emulator.config.register("rcon.rate_limit.limit_for_period", "60");
|
||||
Emulator.config.register("rcon.rate_limit.refresh_period_ms", "1000");
|
||||
Emulator.config.register("rcon.rate_limit.timeout_ms", "0");
|
||||
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||
System.out.println();
|
||||
LOGGER.info("https://github.com/duckietm/Arcturus-Morningstar-Extended, ");
|
||||
|
||||
@@ -3,15 +3,21 @@ package com.eu.habbo.networking.rconserver;
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.messages.rcon.*;
|
||||
import com.eu.habbo.networking.Server;
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import gnu.trove.map.hash.THashMap;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiter;
|
||||
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.ChannelInitializer;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.SocketAddress;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -23,6 +29,8 @@ public class RCONServer extends Server {
|
||||
|
||||
private final THashMap<String, Class<? extends RCONMessage>> messages;
|
||||
private final GsonBuilder gsonBuilder;
|
||||
private final boolean rateLimitEnabled;
|
||||
private final LoadingCache<String, RateLimiter> rateLimiters;
|
||||
List<String> allowedAdresses = new ArrayList<>();
|
||||
|
||||
public RCONServer(String host, int port) throws Exception {
|
||||
@@ -32,6 +40,16 @@ public class RCONServer extends Server {
|
||||
|
||||
this.gsonBuilder = new GsonBuilder();
|
||||
this.gsonBuilder.registerTypeAdapter(RCONMessage.class, new RCONMessage.RCONMessageSerializer());
|
||||
this.rateLimitEnabled = Emulator.getConfig().getBoolean("rcon.rate_limit.enabled", true);
|
||||
RateLimiterConfig rateLimiterConfig = RateLimiterConfig.custom()
|
||||
.limitForPeriod(Math.max(1, Emulator.getConfig().getInt("rcon.rate_limit.limit_for_period", 60)))
|
||||
.limitRefreshPeriod(Duration.ofMillis(Math.max(100, Emulator.getConfig().getInt("rcon.rate_limit.refresh_period_ms", 1000))))
|
||||
.timeoutDuration(Duration.ofMillis(Math.max(0, Emulator.getConfig().getInt("rcon.rate_limit.timeout_ms", 0))))
|
||||
.build();
|
||||
this.rateLimiters = Caffeine.newBuilder()
|
||||
.maximumSize(512)
|
||||
.expireAfterAccess(Duration.ofMinutes(10))
|
||||
.build(address -> RateLimiter.of("rcon-" + address, rateLimiterConfig));
|
||||
|
||||
this.addRCONMessage("alertuser", AlertUser.class);
|
||||
this.addRCONMessage("disconnect", DisconnectUser.class);
|
||||
@@ -89,6 +107,11 @@ public class RCONServer extends Server {
|
||||
}
|
||||
|
||||
public String handle(ChannelHandlerContext ctx, String key, String body) throws Exception {
|
||||
if (!this.acquirePermit(ctx)) {
|
||||
LOGGER.warn("RCON rate limit exceeded for {}", remoteAddress(ctx));
|
||||
return "RATE_LIMITED";
|
||||
}
|
||||
|
||||
Class<? extends RCONMessage> message = this.messages.get(key.replace("_", "").toLowerCase());
|
||||
|
||||
String result;
|
||||
@@ -118,4 +141,17 @@ public class RCONServer extends Server {
|
||||
public List<String> getCommands() {
|
||||
return new ArrayList<>(this.messages.keySet());
|
||||
}
|
||||
|
||||
private boolean acquirePermit(ChannelHandlerContext ctx) {
|
||||
return !this.rateLimitEnabled || this.rateLimiters.get(remoteAddress(ctx)).acquirePermission();
|
||||
}
|
||||
|
||||
private static String remoteAddress(ChannelHandlerContext ctx) {
|
||||
if (ctx == null || ctx.channel() == null) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
SocketAddress address = ctx.channel().remoteAddress();
|
||||
return address == null ? "unknown" : address.toString();
|
||||
}
|
||||
}
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.eu.habbo.networking.rconserver;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RCONServerHandlerContractTest {
|
||||
private static String serverSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/networking/rconserver/RCONServer.java"));
|
||||
}
|
||||
|
||||
private static String emulatorSource() throws Exception {
|
||||
return Files.readString(Path.of("src/main/java/com/eu/habbo/Emulator.java"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rconRequestsAreRateLimitedPerRemoteAddress() throws Exception {
|
||||
String source = serverSource();
|
||||
|
||||
assertTrue(source.contains("LoadingCache<String, RateLimiter>"),
|
||||
"RCON server must keep per-remote-address rate limiters");
|
||||
assertTrue(source.contains("Caffeine.newBuilder()"),
|
||||
"RCON rate limiters must expire instead of growing forever");
|
||||
assertTrue(source.contains(".acquirePermission()"),
|
||||
"RCON handler must consume a Resilience4j permit before dispatching commands");
|
||||
assertTrue(source.contains("RATE_LIMITED"),
|
||||
"RCON callers need a deterministic response when the rate limit rejects a request");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rconRateLimitDefaultsAreRegisteredBeforeServerStarts() throws Exception {
|
||||
String source = emulatorSource();
|
||||
int registerIndex = source.indexOf("register(\"rcon.rate_limit.enabled\", \"1\")");
|
||||
int serverIndex = source.indexOf("new RCONServer");
|
||||
|
||||
assertTrue(registerIndex >= 0, "RCON rate limiting must have a registered default toggle");
|
||||
assertTrue(source.contains("register(\"rcon.rate_limit.limit_for_period\", \"60\")"),
|
||||
"RCON rate limit must have a registered default limit");
|
||||
assertTrue(source.contains("register(\"rcon.rate_limit.refresh_period_ms\", \"1000\")"),
|
||||
"RCON rate limit must have a registered default refresh period");
|
||||
assertTrue(source.contains("register(\"rcon.rate_limit.timeout_ms\", \"0\")"),
|
||||
"RCON rate limit must reject immediately by default instead of blocking event loops");
|
||||
assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user