Skip to content

Commit 3987e5d

Browse files
Merge pull request #2064 from rabbitmq/mk-item-697fx
Be more defensive when parsing shortstr values in tables
2 parents 640c00f + 7bdaea7 commit 3987e5d

5 files changed

Lines changed: 275 additions & 3 deletions

File tree

src/main/java/com/rabbitmq/client/RpcServer.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.rabbitmq.client;
1818

1919
import com.rabbitmq.utility.Utility;
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
2022

2123
import java.io.IOException;
2224
import java.util.concurrent.BlockingQueue;
@@ -27,6 +29,9 @@
2729
* The class is agnostic about the format of RPC arguments / return values.
2830
*/
2931
public class RpcServer {
32+
33+
private static final Logger LOGGER = LoggerFactory.getLogger(RpcServer.class);
34+
3035
/** Channel we are communicating on */
3136
private final Channel _channel;
3237
/** Queue to receive requests from */
@@ -119,8 +124,15 @@ public ShutdownSignalException mainloop()
119124
_mainloopRunning = false;
120125
continue;
121126
}
122-
processRequest(request);
123-
_channel.basicAck(request.getEnvelope().getDeliveryTag(), false);
127+
try {
128+
processRequest(request);
129+
_channel.basicAck(request.getEnvelope().getDeliveryTag(), false);
130+
} catch (ShutdownSignalException sse) {
131+
throw sse;
132+
} catch (RuntimeException e) {
133+
LOGGER.warn("Discarding request that could not be processed", e);
134+
_channel.basicReject(request.getEnvelope().getDeliveryTag(), false);
135+
}
124136
}
125137
return null;
126138
} catch (ShutdownSignalException sse) {

src/main/java/com/rabbitmq/client/impl/ValueReader.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ private static long unsignedExtend(int value)
5252
return extended & INT_MASK;
5353
}
5454

55+
/** Maximum length, in bytes, of a shortstr on the wire. */
56+
private static final int MAX_SHORTSTR_LENGTH = 255;
57+
5558
/** The stream we are reading from. */
5659
private final DataInputStream in;
5760

@@ -71,7 +74,25 @@ private static String readShortstr(DataInputStream in)
7174
{
7275
byte [] b = new byte[in.readUnsignedByte()];
7376
in.readFully(b);
74-
return new String(b, StandardCharsets.UTF_8);
77+
return truncateToMaxUtf8Length(new String(b, StandardCharsets.UTF_8), MAX_SHORTSTR_LENGTH);
78+
}
79+
80+
private static String truncateToMaxUtf8Length(String s, int maxBytes) {
81+
if (s.length() <= maxBytes / 3 || s.indexOf('\uFFFD') < 0) {
82+
return s;
83+
}
84+
int bytes = 0;
85+
int i = 0;
86+
while (i < s.length()) {
87+
int codePoint = s.codePointAt(i);
88+
int width = codePoint < 0x80 ? 1 : codePoint < 0x800 ? 2 : codePoint < 0x10000 ? 3 : 4;
89+
if (bytes + width > maxBytes) {
90+
return s.substring(0, i);
91+
}
92+
bytes += width;
93+
i += Character.charCount(codePoint);
94+
}
95+
return s;
7596
}
7697

7798
/** Public API - reads a short string. */

src/test/java/com/rabbitmq/client/test/ClientTestSuite.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
@Suite
2525
@SelectClasses({
2626
TableTest.class,
27+
ShortstrRoundTripTest.class,
2728
LongStringTest.class,
2829
BlockingCellTest.class,
2930
TruncatedInputStreamTest.class,

src/test/java/com/rabbitmq/client/test/RpcTest.java

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,100 @@ public void handleRecoveryStarted(Recoverable recoverable) {
330330
client.close();
331331
}
332332

333+
@Test
334+
public void serverKeepsRunningWhenHandlerFails() throws Exception {
335+
rpcServer = new FailingRpcServer(serverChannel, queue);
336+
Thread serverThread = new Thread(() -> {
337+
try {
338+
rpcServer.mainloop();
339+
} catch (Exception e) {
340+
}
341+
});
342+
serverThread.start();
343+
RpcClient client = new RpcClient(new RpcClientParams()
344+
.channel(clientChannel).exchange("").routingKey(queue).timeout(1000));
345+
346+
try {
347+
client.doCall(null, "boom".getBytes());
348+
fail("The handler failed, the call should have timed out");
349+
} catch (TimeoutException e) {
350+
}
351+
352+
RpcClient.Response response = client.doCall(null, "hello".getBytes());
353+
assertEquals("*** hello ***", new String(response.getBody()));
354+
assertTrue(serverThread.isAlive());
355+
356+
client.close();
357+
}
358+
359+
@Test
360+
public void requestThatCannotBeProcessedIsNotRequeued() throws Exception {
361+
rpcServer = new FailingRpcServer(serverChannel, queue);
362+
Thread serverThread = new Thread(() -> {
363+
try {
364+
rpcServer.mainloop();
365+
} catch (Exception e) {
366+
}
367+
});
368+
serverThread.start();
369+
RpcClient client = new RpcClient(new RpcClientParams()
370+
.channel(clientChannel).exchange("").routingKey(queue).timeout(1000));
371+
372+
try {
373+
client.doCall(null, "boom".getBytes());
374+
fail("The handler failed, the call should have timed out");
375+
} catch (TimeoutException e) {
376+
}
377+
378+
waitAtMost(Duration.ofSeconds(5), () -> clientChannel.messageCount(queue) == 0);
379+
assertTrue(serverThread.isAlive());
380+
381+
client.close();
382+
}
383+
384+
@Test
385+
public void serverKeepsRunningWhenSeveralHandlerCallsFail() throws Exception {
386+
rpcServer = new FailingRpcServer(serverChannel, queue);
387+
Thread serverThread = new Thread(() -> {
388+
try {
389+
rpcServer.mainloop();
390+
} catch (Exception e) {
391+
}
392+
});
393+
serverThread.start();
394+
RpcClient client = new RpcClient(new RpcClientParams()
395+
.channel(clientChannel).exchange("").routingKey(queue).timeout(1000));
396+
397+
for (int i = 0; i < 5; i++) {
398+
try {
399+
client.doCall(null, "boom".getBytes());
400+
fail("The handler failed, the call should have timed out");
401+
} catch (TimeoutException e) {
402+
}
403+
}
404+
405+
RpcClient.Response response = client.doCall(null, "hello".getBytes());
406+
assertEquals("*** hello ***", new String(response.getBody()));
407+
assertTrue(serverThread.isAlive());
408+
409+
client.close();
410+
}
411+
412+
private static class FailingRpcServer extends TestRpcServer {
413+
414+
public FailingRpcServer(Channel channel, String queueName) throws IOException {
415+
super(channel, queueName);
416+
}
417+
418+
@Override
419+
public byte[] handleCall(Delivery request, AMQP.BasicProperties replyProperties) {
420+
if ("boom".equals(new String(request.getBody()))) {
421+
throw new IllegalArgumentException("cannot handle this request");
422+
}
423+
return super.handleCall(request, replyProperties);
424+
}
425+
}
426+
333427
private static class TestRpcServer extends RpcServer {
334428

335429
public TestRpcServer(Channel channel, String queueName) throws IOException {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) 2007-2026 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries.
2+
//
3+
// This software, the RabbitMQ Java client library, is triple-licensed under the
4+
// Mozilla Public License 2.0 ("MPL"), the GNU General Public License version 2
5+
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
6+
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL,
7+
// please see LICENSE-APACHE2.
8+
//
9+
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
10+
// either express or implied. See the LICENSE file for specific language governing
11+
// rights and limitations of this software.
12+
//
13+
// If you have any questions regarding licensing, please contact us at
14+
// info@rabbitmq.com.
15+
16+
package com.rabbitmq.client.test;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatCode;
20+
21+
import com.rabbitmq.client.AMQP;
22+
import com.rabbitmq.client.impl.ValueReader;
23+
import com.rabbitmq.client.impl.ValueWriter;
24+
import java.io.ByteArrayInputStream;
25+
import java.io.ByteArrayOutputStream;
26+
import java.io.DataInputStream;
27+
import java.io.DataOutputStream;
28+
import java.io.IOException;
29+
import java.nio.charset.StandardCharsets;
30+
import java.util.Arrays;
31+
import org.junit.jupiter.api.Test;
32+
33+
public class ShortstrRoundTripTest {
34+
35+
private static String read(byte[] payload) throws IOException {
36+
ByteArrayOutputStream frame = new ByteArrayOutputStream();
37+
frame.write(payload.length);
38+
frame.write(payload);
39+
return new ValueReader(new DataInputStream(new ByteArrayInputStream(frame.toByteArray())))
40+
.readShortstr();
41+
}
42+
43+
private static byte[] write(String s) throws IOException {
44+
ByteArrayOutputStream out = new ByteArrayOutputStream();
45+
new ValueWriter(new DataOutputStream(out)).writeShortstr(s);
46+
return out.toByteArray();
47+
}
48+
49+
@Test
50+
public void valueReadFromWireCanBeWrittenBack() throws IOException {
51+
byte[] payload = new byte[255];
52+
Arrays.fill(payload, (byte) 0xFF);
53+
String decoded = read(payload);
54+
assertThat(decoded.getBytes(StandardCharsets.UTF_8).length).isLessThanOrEqualTo(255);
55+
assertThatCode(() -> write(decoded)).doesNotThrowAnyException();
56+
}
57+
58+
@Test
59+
public void wellFormedValuesArePreserved() throws IOException {
60+
for (String value : new String[] {"", "hello", "santé", "你好", "😀"}) {
61+
assertThat(read(value.getBytes(StandardCharsets.UTF_8))).isEqualTo(value);
62+
}
63+
StringBuilder sb = new StringBuilder();
64+
for (int i = 0; i < 85; i++) {
65+
sb.append("你");
66+
}
67+
String maxLength = sb.toString();
68+
assertThat(maxLength.getBytes(StandardCharsets.UTF_8).length).isEqualTo(255);
69+
assertThat(read(maxLength.getBytes(StandardCharsets.UTF_8))).isEqualTo(maxLength);
70+
}
71+
72+
@Test
73+
public void partiallyMalformedValueStaysWithinLimit() throws IOException {
74+
byte[] payload = new byte[255];
75+
Arrays.fill(payload, (byte) 'a');
76+
for (int i = 100; i < 255; i++) {
77+
payload[i] = (byte) 0xFF;
78+
}
79+
String decoded = read(payload);
80+
assertThat(decoded.getBytes(StandardCharsets.UTF_8).length).isLessThanOrEqualTo(255);
81+
assertThatCode(() -> write(decoded)).doesNotThrowAnyException();
82+
assertThat(decoded).startsWith("aaaa");
83+
}
84+
85+
@Test
86+
public void messagePropertiesReadFromWireCanBeWrittenBack() throws IOException {
87+
byte[] malformed = new byte[255];
88+
Arrays.fill(malformed, (byte) 0xFF);
89+
90+
ByteArrayOutputStream header = new ByteArrayOutputStream();
91+
DataOutputStream out = new DataOutputStream(header);
92+
out.writeShort(0);
93+
out.writeLong(6);
94+
out.writeShort((1 << 10) | (1 << 9) | (1 << 7) | (1 << 5) | (1 << 4));
95+
out.writeByte(malformed.length);
96+
out.write(malformed);
97+
out.writeByte(malformed.length);
98+
out.write(malformed);
99+
out.writeByte(malformed.length);
100+
out.write(malformed);
101+
out.writeByte(malformed.length);
102+
out.write(malformed);
103+
out.writeByte(malformed.length);
104+
out.write(malformed);
105+
out.flush();
106+
107+
AMQP.BasicProperties properties =
108+
new AMQP.BasicProperties(
109+
new DataInputStream(new ByteArrayInputStream(header.toByteArray())));
110+
111+
assertThat(properties.getCorrelationId()).isNotNull();
112+
assertThat(properties.getReplyTo()).isNotNull();
113+
assertThat(properties.getMessageId()).isNotNull();
114+
assertThat(properties.getType()).isNotNull();
115+
assertThat(properties.getUserId()).isNotNull();
116+
117+
AMQP.BasicProperties echoed =
118+
new AMQP.BasicProperties.Builder()
119+
.correlationId(properties.getCorrelationId())
120+
.replyTo(properties.getReplyTo())
121+
.messageId(properties.getMessageId())
122+
.type(properties.getType())
123+
.userId(properties.getUserId())
124+
.build();
125+
126+
assertThatCode(
127+
() -> {
128+
ByteArrayOutputStream sink = new ByteArrayOutputStream();
129+
echoed.writePropertiesTo(
130+
new com.rabbitmq.client.impl.ContentHeaderPropertyWriter(
131+
new DataOutputStream(sink)));
132+
})
133+
.doesNotThrowAnyException();
134+
}
135+
136+
@Test
137+
public void oversizedValuesAreStillRejected() {
138+
StringBuilder sb = new StringBuilder();
139+
for (int i = 0; i < 256; i++) {
140+
sb.append('a');
141+
}
142+
assertThatCode(() -> write(sb.toString())).isInstanceOf(IllegalArgumentException.class);
143+
}
144+
}

0 commit comments

Comments
 (0)