Skip to content

Commit 1fb9e55

Browse files
committed
fix: Fixed java.lang.IndexOutOfBoundsException on large payloads
1 parent dd1b026 commit 1fb9e55

7 files changed

Lines changed: 106 additions & 6 deletions

File tree

src/main/java/com/gportal/a2s/QueryClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public QueryClient() {
3232
Bootstrap bootstrap = new Bootstrap()
3333
.group(worker)
3434
.channel(NioDatagramChannel.class)
35+
.option(io.netty.channel.ChannelOption.SO_RCVBUF, 65535)
36+
.option(io.netty.channel.ChannelOption.RCVBUF_ALLOCATOR, new io.netty.channel.FixedRecvByteBufAllocator(65535))
3537
.handler(new ChannelInitializer<NioDatagramChannel>() {
3638
protected void initChannel(NioDatagramChannel ch) throws Exception {
3739
ch.pipeline().addLast(

src/main/java/com/gportal/a2s/QueryServer.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public QueryServer(InetSocketAddress address, ServerInfo info) {
3636
Bootstrap bootstrap = new Bootstrap()
3737
.group(worker)
3838
.channel(NioDatagramChannel.class)
39+
.option(io.netty.channel.ChannelOption.SO_RCVBUF, 65535)
40+
.option(io.netty.channel.ChannelOption.RCVBUF_ALLOCATOR, new io.netty.channel.FixedRecvByteBufAllocator(65535))
3941
.handler(new ChannelInitializer<NioDatagramChannel>() {
4042
protected void initChannel(NioDatagramChannel ch) throws Exception {
4143
ch.pipeline().addLast(

src/main/java/com/gportal/a2s/messages/PlayerQuery.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public record PlayerQuery(InetSocketAddress remoteAddress, Integer challenge) im
1212
public PlayerQuery withChallenge(int challenge) { return new PlayerQuery(remoteAddress(), challenge); }
1313

1414
public static PlayerQuery read(InetSocketAddress remoteAddress, ByteBuf buffer) {
15-
Integer challenge = buffer.readIntLE();
16-
if(challenge==0) challenge = null;
15+
Integer challenge = buffer.readableBytes()>=4?buffer.readIntLE():null;
16+
if(challenge!=null && challenge==0) challenge = null;
1717
return new PlayerQuery(remoteAddress, challenge);
1818
}
1919
public PlayerQuery write(ByteBuf buffer) {

src/main/java/com/gportal/a2s/messages/PlayerReply.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public record PlayerReply(InetSocketAddress remoteAddress, List<PlayerInfo> payl
1313
public static final byte OP = 0x44;
1414

1515
public static PlayerReply read(InetSocketAddress remoteAddress, ByteBuf buffer) {
16-
byte count = buffer.readByte();
16+
int count = buffer.readUnsignedByte();
1717
List<PlayerInfo> payload = new ArrayList<PlayerInfo>();
1818
for(int i = 0; i < count; i++) {
1919
payload.add(PlayerInfo.read(buffer));

src/main/java/com/gportal/a2s/messages/RulesQuery.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ public record RulesQuery(InetSocketAddress remoteAddress, Integer challenge) imp
1212
public RulesQuery withChallenge(int challenge) { return new RulesQuery(remoteAddress(), challenge); }
1313

1414
public static RulesQuery read(InetSocketAddress remoteAddress, ByteBuf buffer) {
15-
Integer challenge = buffer.readIntLE();
16-
if(challenge==0) challenge = null;
15+
Integer challenge = buffer.readableBytes()>=4?buffer.readIntLE():null;
16+
if(challenge!=null && challenge==0) challenge = null;
1717
return new RulesQuery(remoteAddress, challenge);
1818
}
1919
public RulesQuery write(ByteBuf buffer) {

src/main/java/com/gportal/a2s/messages/RulesReply.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public record RulesReply(InetSocketAddress remoteAddress, Map<String, String> pa
1515
public static final byte OP = 0x45;
1616

1717
public static RulesReply read(InetSocketAddress remoteAddress, ByteBuf buffer) {
18-
short count = buffer.readShortLE();
18+
int count = buffer.readUnsignedShortLE();
1919
Map<String, String> payload = new HashMap<String, String>();
2020
for(int i = 0; i < count; i++) {
2121
payload.put(readString(buffer), readString(buffer));
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import java.net.InetAddress;
2+
import java.net.InetSocketAddress;
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.concurrent.TimeUnit;
7+
8+
import com.gportal.a2s.PlayerInfo;
9+
import com.gportal.a2s.QueryClient;
10+
import com.gportal.a2s.QueryServer;
11+
import com.gportal.a2s.ServerInfo;
12+
13+
import org.junit.jupiter.api.AfterAll;
14+
import org.junit.jupiter.api.BeforeAll;
15+
import org.junit.jupiter.api.Test;
16+
import static org.junit.jupiter.api.Assertions.*;
17+
18+
public class LargePayloadETETest {
19+
private static QueryClient client;
20+
private static QueryServer server;
21+
private static final int PORT = 27016;
22+
23+
@BeforeAll
24+
public static void init() throws Exception {
25+
client = new QueryClient();
26+
27+
// Create a very long server name (2000 chars)
28+
StringBuilder longName = new StringBuilder();
29+
for (int i = 0; i < 2000; i++) {
30+
longName.append("A");
31+
}
32+
33+
ServerInfo info = new ServerInfo(
34+
new InetSocketAddress(InetAddress.getLocalHost(), PORT),
35+
(byte) 17,
36+
longName.toString(),
37+
"Map",
38+
"Folder",
39+
"Game",
40+
(short) 0,
41+
(byte) 0, // players - will be updated by server.players.size() if it was dynamic, but here it's static in ServerInfo
42+
(byte) 255,
43+
(byte) 0,
44+
'd',
45+
'l',
46+
false,
47+
true,
48+
"1.0.0.0",
49+
null, null, null, null, null, null
50+
);
51+
52+
server = new QueryServer(new InetSocketAddress(InetAddress.getLocalHost(), PORT), info);
53+
54+
// Add 200 players to exceed typical MTU/buffer limits
55+
for (int i = 0; i < 200; i++) {
56+
server.players.add(new PlayerInfo((byte) i, "Player " + i, i * 10, 100.0f));
57+
}
58+
59+
// Update player count in info to reflect actual players
60+
server.info.setPlayers((byte) 200);
61+
62+
// Add many rules
63+
for (int i = 0; i < 200; i++) {
64+
server.rules.put("RuleKey" + i, "RuleValue" + i);
65+
}
66+
}
67+
68+
@Test
69+
public void testLargeInfoReply() throws Exception {
70+
ServerInfo info = client.queryServer(new InetSocketAddress(InetAddress.getLocalHost(), PORT)).get(5, TimeUnit.SECONDS);
71+
assertNotNull(info);
72+
assertEquals(2000, info.name().length());
73+
}
74+
75+
@Test
76+
public void testLargePlayerReply() throws Exception {
77+
List<PlayerInfo> players = client.queryPlayers(new InetSocketAddress(InetAddress.getLocalHost(), PORT)).get(5, TimeUnit.SECONDS);
78+
assertNotNull(players);
79+
assertEquals(200, players.size());
80+
assertEquals("Player 199", players.get(199).name());
81+
}
82+
83+
@Test
84+
public void testLargeRulesReply() throws Exception {
85+
Map<String, String> rules = client.queryRules(new InetSocketAddress(InetAddress.getLocalHost(), PORT)).get(5, TimeUnit.SECONDS);
86+
assertNotNull(rules);
87+
assertEquals(200, rules.size());
88+
assertEquals("RuleValue199", rules.get("RuleKey199"));
89+
}
90+
91+
@AfterAll
92+
public static void cleanup() {
93+
if (client != null) client.shutdown();
94+
if (server != null) server.shutdown();
95+
}
96+
}

0 commit comments

Comments
 (0)