This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 315e11975b9f [backport camel-4.22.x] CAMEL-26481: camel-syslog -
handle fragmented length fields in Rfc5425FrameDecoder (#28869)
315e11975b9f is described below
commit 315e11975b9ffbd69359e089f9185019ddd0a3c9
Author: Andrea Cosentino <[email protected]>
AuthorDate: Mon Nov 2 15:21:41 2026 +0100
[backport camel-4.22.x] CAMEL-26481: camel-syslog - handle fragmented
length fields in Rfc5425FrameDecoder (#28869)
CAMEL-26481: camel-syslog - handle fragmented length fields in
Rfc5425FrameDecoder (#28846)
(cherry picked from commit 6e68019c2c5a93c3fbd4f1b9ce626a99c8d5549e)
Signed-off-by: Andrea Cosentino <[email protected]>
Co-authored-by: Claude Fable 5.1 <[email protected]>
---
.../syslog/netty/Rfc5425FrameDecoder.java | 74 +++++-------
.../syslog/netty/Rfc5425FrameDecoderTest.java | 130 +++++++++++++++++++++
2 files changed, 158 insertions(+), 46 deletions(-)
diff --git
a/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
b/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
index 36a628c072d7..afe81c44a841 100644
---
a/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
+++
b/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoder.java
@@ -19,7 +19,6 @@ package org.apache.camel.component.syslog.netty;
import java.util.List;
import io.netty.buffer.ByteBuf;
-import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
@@ -33,33 +32,34 @@ public class Rfc5425FrameDecoder extends
ByteToMessageDecoder implements Channel
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object>
out) throws Exception {
if (currentFramelength == null) {
// find index of the first space, it should be after the length
field
- int index = indexOf(in, Unpooled.wrappedBuffer(new byte[] { ' '
}));
-
- // Read part until the first space, if we have found one
- StringBuilder lengthbuffer = new StringBuilder();
- if (index > -1) {
- ByteBuf byteBuf = in.readBytes(index);
- byte[] dest = new byte[byteBuf.readableBytes()];
- byteBuf.readBytes(dest);
- lengthbuffer.append(new String(dest));
+ int spaceIndex = in.indexOf(in.readerIndex(), in.writerIndex(),
(byte) ' ');
+ if (spaceIndex < 0) {
+ // The length delimiter has not arrived yet, wait for more
data without modifying reader index
+ return;
}
- int length;
- try {
- // add one because we have to take in account the space after
- // the length field
- length = Integer.parseInt(lengthbuffer.toString()) + 1;
- } catch (NumberFormatException e) {
- length = -1;
+ int lengthDigits = spaceIndex - in.readerIndex();
+ if (lengthDigits == 0) {
+ throw invalidFrame(in, "empty length field");
}
- // We have not found the length field, reset the buffer so we can
- // retry next time
- if (length < 0) {
- in.resetReaderIndex();
- return;
+ int length = 0;
+ for (int i = 0; i < lengthDigits; i++) {
+ byte b = in.getByte(in.readerIndex() + i);
+ if (b < '0' || b > '9') {
+ throw invalidFrame(in, "non-digit character in length
field");
+ }
+ // the frame is read as length + 1 bytes (the space after the
length field), so the field must leave
+ // room for that in an int; Integer.parseInt used to reject
such a field, this loop must not wrap it
+ if (length > (Integer.MAX_VALUE - 1 - (b - '0')) / 10) {
+ throw invalidFrame(in, "length field does not fit an int");
+ }
+ length = length * 10 + (b - '0');
}
- currentFramelength = length;
+
+ // add one because we have to take in account the space after the
length field
+ currentFramelength = length + 1;
+ in.skipBytes(lengthDigits);
}
// Buffer does not contain enough data yet, wait until it does
@@ -74,30 +74,12 @@ public class Rfc5425FrameDecoder extends
ByteToMessageDecoder implements Channel
}
/**
- * Borrowed from the DelimiterBasedFrameDecoder Returns the number of
bytes between the readerIndex of the haystack
- * and the first needle found in the haystack. -1 is returned if no needle
is found in the haystack.
+ * A malformed length field leaves no way to find the next frame, so the
buffered bytes are dropped before the
+ * exception is raised: the connection is closed on it, and the bytes must
not be decoded again on the way out.
*/
- private static int indexOf(ByteBuf haystack, ByteBuf needle) {
- for (int i = haystack.readerIndex(); i < haystack.writerIndex(); i++) {
- int haystackIndex = i;
- int needleIndex;
- for (needleIndex = 0; needleIndex < needle.capacity();
needleIndex++) {
- if (haystack.getByte(haystackIndex) !=
needle.getByte(needleIndex)) {
- break;
- } else {
- haystackIndex++;
- if (haystackIndex == haystack.writerIndex() && needleIndex
!= needle.capacity() - 1) {
- return -1;
- }
- }
- }
-
- if (needleIndex == needle.capacity()) {
- // Found the needle from the haystack!
- return i - haystack.readerIndex();
- }
- }
- return -1;
+ private static IllegalArgumentException invalidFrame(ByteBuf in, String
reason) {
+ in.skipBytes(in.readableBytes());
+ return new IllegalArgumentException("Invalid RFC 5425 frame: " +
reason);
}
@Override
diff --git
a/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoderTest.java
b/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoderTest.java
new file mode 100644
index 000000000000..1a90ce388c00
--- /dev/null
+++
b/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/netty/Rfc5425FrameDecoderTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.syslog.netty;
+
+import java.nio.charset.StandardCharsets;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.embedded.EmbeddedChannel;
+import io.netty.handler.codec.DecoderException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class Rfc5425FrameDecoderTest {
+
+ @Test
+ void testFragmentedLengthFieldDoesNotDuplicateFrames() {
+ EmbeddedChannel channel = new EmbeddedChannel(new
Rfc5425FrameDecoder());
+
+ // Write first message and partial length of next message: "3 one5"
+ assertTrue(channel.writeInbound(Unpooled.copiedBuffer("3 one5",
StandardCharsets.UTF_8)));
+
+ // Exactly one frame (" one") should be produced
+ ByteBuf frame1 = channel.readInbound();
+ assertNotNull(frame1);
+ assertEquals(" one", frame1.toString(StandardCharsets.UTF_8));
+ frame1.release();
+
+ // No duplicate frame should be produced
+ assertNull(channel.readInbound());
+
+ // Write remainder of second message: " three"
+ assertTrue(channel.writeInbound(Unpooled.copiedBuffer(" three",
StandardCharsets.UTF_8)));
+
+ // Second frame (" three") should be produced
+ ByteBuf frame2 = channel.readInbound();
+ assertNotNull(frame2);
+ assertEquals(" three", frame2.toString(StandardCharsets.UTF_8));
+ frame2.release();
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.finish());
+ }
+
+ @Test
+ void testFragmentedLengthDigitsAcrossPackets() {
+ EmbeddedChannel channel = new EmbeddedChannel(new
Rfc5425FrameDecoder());
+
+ // A complete frame followed by the first digit of the next length
field: "3 one1". Before the fix the
+ // decoder reset the reader index when the space had not arrived,
replaying " one" on the next packet.
+ assertTrue(channel.writeInbound(Unpooled.copiedBuffer("3 one1",
StandardCharsets.UTF_8)));
+ ByteBuf frame1 = channel.readInbound();
+ assertNotNull(frame1);
+ assertEquals(" one", frame1.toString(StandardCharsets.UTF_8));
+ frame1.release();
+ assertNull(channel.readInbound());
+
+ // The second digit, the space and the body arrive in the next packet:
"1 hello world"
+ assertTrue(channel.writeInbound(Unpooled.copiedBuffer("1 hello world",
StandardCharsets.UTF_8)));
+ ByteBuf frame2 = channel.readInbound();
+ assertNotNull(frame2);
+ assertEquals(" hello world", frame2.toString(StandardCharsets.UTF_8));
+ frame2.release();
+
+ assertNull(channel.readInbound());
+ assertFalse(channel.finish());
+ }
+
+ @Test
+ void testEmptyLengthFieldIsRejected() {
+ assertRejected(" hello", "empty length field");
+ }
+
+ @Test
+ void testNonDigitLengthFieldIsRejected() {
+ assertRejected("1a hello", "non-digit character in length field");
+ }
+
+ @Test
+ void testLengthFieldWrappingAnIntIsRejected() {
+ // 4294967296 wraps a 32-bit int to 0, which would make the decoder
emit one-byte frames
+ assertRejected("4294967296 hello", "length field does not fit an int");
+ }
+
+ @Test
+ void testLengthFieldAtIntegerMaxIsRejected() {
+ // the frame is read as length + 1 bytes, so Integer.MAX_VALUE itself
does not fit
+ assertRejected("2147483647 hello", "length field does not fit an int");
+ }
+
+ @Test
+ void testTenDigitLengthFieldThatFitsIsAccepted() {
+ EmbeddedChannel channel = new EmbeddedChannel(new
Rfc5425FrameDecoder());
+
+ // the largest field this decoder accepts; the frame itself never
arrives, so nothing is produced
+ assertFalse(channel.writeInbound(Unpooled.copiedBuffer("2147483646 x",
StandardCharsets.UTF_8)));
+ assertNull(channel.readInbound());
+ channel.finishAndReleaseAll();
+ }
+
+ private static void assertRejected(String input, String reason) {
+ EmbeddedChannel channel = new EmbeddedChannel(new
Rfc5425FrameDecoder());
+
+ // ByteToMessageDecoder wraps the decoder's exception, and
EmbeddedChannel rethrows it from writeInbound
+ DecoderException e = assertThrows(DecoderException.class,
+ () -> channel.writeInbound(Unpooled.copiedBuffer(input,
StandardCharsets.UTF_8)));
+ assertTrue(e.getMessage().contains(reason), e.getMessage());
+ channel.finishAndReleaseAll();
+ }
+}