You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 15:36:17 +00:00
fix(rcon): bound inbound payload handling
This commit is contained in:
@@ -166,6 +166,7 @@ public final class Emulator {
|
|||||||
Emulator.config.register("rcon.rate_limit.timeout_ms", "0");
|
Emulator.config.register("rcon.rate_limit.timeout_ms", "0");
|
||||||
Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank");
|
Emulator.config.register("rcon.execute_command.denied_permissions", "cmd_shutdown;cmd_give_rank");
|
||||||
Emulator.config.register("rcon.execute_command.allowed_permissions", "");
|
Emulator.config.register("rcon.execute_command.allowed_permissions", "");
|
||||||
|
Emulator.config.register("rcon.max_payload_bytes", "65536");
|
||||||
registerEarningsSettings();
|
registerEarningsSettings();
|
||||||
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
String hotelTimezoneId = Emulator.getConfig().getValue("hotel.timezone", java.time.ZoneId.systemDefault().getId());
|
||||||
System.out.println(startupCard(hotelTimezoneId));
|
System.out.println(startupCard(hotelTimezoneId));
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import io.netty.channel.ChannelInboundHandlerAdapter;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.SocketAddress;
|
||||||
|
|
||||||
public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(RCONServerHandler.class);
|
||||||
@@ -19,20 +22,21 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
// Gson is thread-safe and immutable once built — share one instance instead
|
// Gson is thread-safe and immutable once built — share one instance instead
|
||||||
// of allocating a parser per RCON request.
|
// of allocating a parser per RCON request.
|
||||||
private static final Gson GSON = new Gson();
|
private static final Gson GSON = new Gson();
|
||||||
|
private static final int DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
||||||
String adress = ctx.channel().remoteAddress().toString().split(":")[0].replace("/", "");
|
String address = remoteAddress(ctx);
|
||||||
|
|
||||||
for (String s : Emulator.getRconServer().allowedAdresses) {
|
for (String s : Emulator.getRconServer().allowedAdresses) {
|
||||||
if (s.equalsIgnoreCase(adress)) {
|
if (s.equalsIgnoreCase(address)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.channel().close();
|
ctx.channel().close();
|
||||||
|
|
||||||
LOGGER.warn("RCON Remote connection closed: {}. IP not allowed!", adress);
|
LOGGER.warn("RCON Remote connection closed: {}. IP not allowed!", address);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -43,7 +47,15 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] d = new byte[data.readableBytes()];
|
int readableBytes = data.readableBytes();
|
||||||
|
int maxPayloadBytes = maxPayloadBytes();
|
||||||
|
if (readableBytes > maxPayloadBytes) {
|
||||||
|
writeAndClose(ctx, "PAYLOAD_TOO_LARGE");
|
||||||
|
LOGGER.warn("Rejected oversized RCON payload: {} bytes (max {})", readableBytes, maxPayloadBytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] d = new byte[readableBytes];
|
||||||
data.getBytes(0, d);
|
data.getBytes(0, d);
|
||||||
String message = new String(d, java.nio.charset.StandardCharsets.UTF_8);
|
String message = new String(d, java.nio.charset.StandardCharsets.UTF_8);
|
||||||
Gson gson = GSON;
|
Gson gson = GSON;
|
||||||
@@ -60,12 +72,34 @@ public class RCONServerHandler extends ChannelInboundHandlerAdapter {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise());
|
writeAndClose(ctx, response);
|
||||||
ctx.channel().flush();
|
|
||||||
ctx.flush();
|
|
||||||
f.channel().close();
|
|
||||||
} finally {
|
} finally {
|
||||||
data.release();
|
data.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int maxPayloadBytes() {
|
||||||
|
if (Emulator.getConfig() == null) {
|
||||||
|
return DEFAULT_MAX_PAYLOAD_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
int configured = Emulator.getConfig().getInt("rcon.max_payload_bytes", DEFAULT_MAX_PAYLOAD_BYTES);
|
||||||
|
return configured > 0 ? configured : DEFAULT_MAX_PAYLOAD_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String remoteAddress(ChannelHandlerContext ctx) {
|
||||||
|
SocketAddress socketAddress = ctx.channel().remoteAddress();
|
||||||
|
if (socketAddress instanceof InetSocketAddress inetSocketAddress && inetSocketAddress.getAddress() != null) {
|
||||||
|
return inetSocketAddress.getAddress().getHostAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
return socketAddress == null ? "" : socketAddress.toString().replace("/", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAndClose(ChannelHandlerContext ctx, String response) {
|
||||||
|
ChannelFuture f = ctx.channel().write(Unpooled.copiedBuffer(response.getBytes(java.nio.charset.StandardCharsets.UTF_8)), ctx.channel().voidPromise());
|
||||||
|
ctx.channel().flush();
|
||||||
|
ctx.flush();
|
||||||
|
f.channel().close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -50,6 +50,30 @@ class RCONServerHandlerContractTest {
|
|||||||
assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed");
|
assertTrue(registerIndex < serverIndex, "RCON rate limit defaults must be registered before RCONServer is constructed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rconPayloadSizeIsBoundedBeforeBufferCopy() throws Exception {
|
||||||
|
String handler = handlerSource();
|
||||||
|
String emulator = emulatorSource();
|
||||||
|
|
||||||
|
int readableBytes = handler.indexOf("int readableBytes = data.readableBytes()");
|
||||||
|
int maxPayload = handler.indexOf("int maxPayloadBytes = maxPayloadBytes()", readableBytes);
|
||||||
|
int oversizedGuard = handler.indexOf("readableBytes > maxPayloadBytes", maxPayload);
|
||||||
|
int byteArray = handler.indexOf("new byte[readableBytes]", readableBytes);
|
||||||
|
|
||||||
|
assertTrue(handler.contains("DEFAULT_MAX_PAYLOAD_BYTES = 64 * 1024"),
|
||||||
|
"RCON handler should have a conservative default payload cap");
|
||||||
|
assertTrue(handler.contains("rcon.max_payload_bytes"),
|
||||||
|
"RCON max payload should be configurable");
|
||||||
|
assertTrue(readableBytes > -1, "RCON handler must read ByteBuf size before allocation");
|
||||||
|
assertTrue(maxPayload > readableBytes, "RCON handler must resolve max payload before allocation");
|
||||||
|
assertTrue(oversizedGuard > maxPayload, "RCON handler must reject oversized payloads");
|
||||||
|
assertTrue(oversizedGuard < byteArray, "Oversized RCON payloads must be rejected before byte array allocation");
|
||||||
|
assertTrue(handler.contains("PAYLOAD_TOO_LARGE"),
|
||||||
|
"RCON callers need a deterministic response for oversized payloads");
|
||||||
|
assertTrue(emulator.contains("register(\"rcon.max_payload_bytes\", \"65536\")"),
|
||||||
|
"RCON max payload default must be registered before startup");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void inboundByteBufIsReleasedFromFinallyBlock() throws Exception {
|
void inboundByteBufIsReleasedFromFinallyBlock() throws Exception {
|
||||||
String source = handlerSource();
|
String source = handlerSource();
|
||||||
@@ -59,4 +83,16 @@ class RCONServerHandlerContractTest {
|
|||||||
assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block");
|
assertTrue(finallyIndex >= 0, "RCON channelRead must release inbound ByteBufs from a finally block");
|
||||||
assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts");
|
assertTrue(releaseIndex > finallyIndex, "RCON channelRead must release the inbound ByteBuf after finally starts");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rconWhitelistUsesSocketAddressInsteadOfStringSplitting() throws Exception {
|
||||||
|
String source = handlerSource();
|
||||||
|
|
||||||
|
assertTrue(source.contains("InetSocketAddress"),
|
||||||
|
"RCON whitelist should resolve socket addresses instead of parsing remoteAddress.toString()");
|
||||||
|
assertTrue(source.contains("getHostAddress()"),
|
||||||
|
"RCON whitelist should compare the resolved host address");
|
||||||
|
assertTrue(!source.contains(".toString().split(\":\")"),
|
||||||
|
"RCON whitelist must not split host:port strings because that breaks IPv6 addresses");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user