This is an automated email from the ASF dual-hosted git repository. apupier pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit 47c90e8dc91fe1e203b772a7b7310c23b49aa71d Author: smjain <[email protected]> AuthorDate: Tue Sep 29 09:24:49 2026 +0530 CAMEL-25127: camel-syslog - accept valid RFC 5424 messages that the parser rejected or misread SyslogConverter.parseMessage failed on, or misread, valid RFC 5424 messages: - MSG is optional (SYSLOG-MSG = HEADER SP STRUCTURED-DATA [SP MSG]), but every field loop read until a space, so a message that ends at its structured data (RFC 5424 example 4) threw BufferUnderflowException. The end of the input now also ends a field, and MSG is then empty. - The structured data loop only tracked '[' and ']', so an escaped ']' inside a PARAM-VALUE (RFC 5424 6.3.3) ended the structured data and moved its tail into the log message. The loop now follows the grammar: a '"' after '=' starts a PARAM-VALUE, in which a backslash escapes the next char, and only a ']' outside a value closes the element. - The NILVALUE timestamp "-" (RFC 5424 6.2.3) threw IllegalArgumentException; the timestamp is now left null. A field that is followed by a space is read exactly as before. Co-Authored-By: Claude Opus 5.5 <[email protected]> --- .../camel/component/syslog/SyslogConverter.java | 42 ++++-- .../component/syslog/SyslogRfc5424ParseTest.java | 166 +++++++++++++++++++++ 2 files changed, 196 insertions(+), 12 deletions(-) diff --git a/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/SyslogConverter.java b/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/SyslogConverter.java index b22551e8b471..9320a6ec9f4c 100644 --- a/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/SyslogConverter.java +++ b/components/camel-syslog/src/main/java/org/apache/camel/component/syslog/SyslogConverter.java @@ -224,19 +224,22 @@ public final class SyslogConverter { LOG.error("Invalid syslog message, missing a mandatory space after version"); } - // This should be the timestamp + // This should be the timestamp, or the NILVALUE when the sender has no time (RFC 5424 6.2.3) StringBuilder date = new StringBuilder(); - while ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { + while (byteBuffer.hasRemaining() && (charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { date.append(charFound); } - syslogMessage.setTimestamp(DatatypeConverter.parseDateTime(date.toString())); + if (!"-".contentEquals(date)) { + syslogMessage.setTimestamp(DatatypeConverter.parseDateTime(date.toString())); + } } - // The host is the char sequence until the next ' ' + // The host is the char sequence until the next ' ', or the end of the message: MSG is optional (RFC 5424 6), + // so any field can be the last one StringBuilder host = new StringBuilder(); - while ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { + while (byteBuffer.hasRemaining() && (charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { host.append(charFound); } @@ -245,32 +248,47 @@ public final class SyslogConverter { if (isRfc5424) { Rfc5424SyslogMessage rfc5424SyslogMessage = (Rfc5424SyslogMessage) syslogMessage; StringBuilder appName = new StringBuilder(); - while ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { + while (byteBuffer.hasRemaining() && (charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { appName.append(charFound); } rfc5424SyslogMessage.setAppName(decode(appName, charset)); StringBuilder procId = new StringBuilder(); - while ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { + while (byteBuffer.hasRemaining() && (charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { procId.append(charFound); } rfc5424SyslogMessage.setProcId(decode(procId, charset)); StringBuilder msgId = new StringBuilder(); - while ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { + while (byteBuffer.hasRemaining() && (charFound = (char) (byteBuffer.get() & 0xff)) != ' ') { msgId.append(charFound); } rfc5424SyslogMessage.setMsgId(decode(msgId, charset)); + // STRUCTURED-DATA is the NILVALUE or SD-ELEMENTs (RFC 5424 6.3). A PARAM-VALUE is quoted, and escapes + // '"', '\' and ']' with a backslash, so a ']' only closes the element outside a PARAM-VALUE StringBuilder structuredData = new StringBuilder(); boolean inblock = false; - while ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ' || inblock) { - if (charFound == '[') { + boolean inValue = false; + boolean escaped = false; + char previous = 0; + while (byteBuffer.hasRemaining() && ((charFound = (char) (byteBuffer.get() & 0xff)) != ' ' || inblock)) { + if (inValue) { + if (escaped) { + escaped = false; + } else if (charFound == '\\') { + escaped = true; + } else if (charFound == '"') { + inValue = false; + } + } else if (charFound == '[') { inblock = true; - } - if (charFound == ']') { + } else if (charFound == ']') { inblock = false; + } else if (charFound == '"' && inblock && previous == '=') { + inValue = true; } + previous = charFound; structuredData.append(charFound); } rfc5424SyslogMessage.setStructuredData(decode(structuredData, charset)); diff --git a/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/SyslogRfc5424ParseTest.java b/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/SyslogRfc5424ParseTest.java new file mode 100644 index 000000000000..251b73b2e0fa --- /dev/null +++ b/components/camel-syslog/src/test/java/org/apache/camel/component/syslog/SyslogRfc5424ParseTest.java @@ -0,0 +1,166 @@ +/* + * 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; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * Valid RFC 5424 messages: without MSG, with escaped characters in a PARAM-VALUE, and with a NILVALUE timestamp. + */ +public class SyslogRfc5424ParseTest extends CamelTestSupport { + + private static final String HEADER = "<165>1 2003-10-11T22:14:15.003Z mymachine.example.com evntslog - ID47 "; + private static final String EXAMPLE_SD = "[exampleSDID@32473 iut=\"3\" eventSource=\"Application\" eventID=\"1011\"]"; + + @Test + public void testStructuredDataOnly() { + // RFC 5424 6.5, example 4: "This is a valid message" + String sd = EXAMPLE_SD + "[examplePriority@32473 class=\"high\"]"; + + Rfc5424SyslogMessage message = parse(HEADER + sd); + + assertEquals("mymachine.example.com", message.getHostname()); + assertEquals("evntslog", message.getAppName()); + assertEquals("-", message.getProcId()); + assertEquals("ID47", message.getMsgId()); + assertEquals(sd, message.getStructuredData()); + assertEquals("", message.getLogMessage()); + } + + @Test + public void testNilStructuredDataWithoutMsg() { + Rfc5424SyslogMessage message = parse("<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 -"); + + assertEquals("ID47", message.getMsgId()); + assertEquals("-", message.getStructuredData()); + assertEquals("", message.getLogMessage()); + } + + @Test + public void testEscapedBracketInParamValue() { + String sd = "[exampleSDID@32473 note=\"a\\] b\"]"; + + Rfc5424SyslogMessage message = parse(HEADER + sd + " hello"); + + assertEquals(sd, message.getStructuredData()); + assertEquals("hello", message.getLogMessage()); + } + + @Test + public void testEscapedQuoteAndBackslashInParamValue() { + String sd = "[exampleSDID@32473 quote=\"say \\\"x\\] y\\\"\" path=\"c:\\\\\" note=\"b\\] c\"][other@32473 n=\"2\"]"; + + Rfc5424SyslogMessage message = parse(HEADER + sd + " hello world"); + + assertEquals(sd, message.getStructuredData()); + assertEquals("hello world", message.getLogMessage()); + } + + @Test + public void testNilTimestamp() { + // RFC 5424 6.2.3: a sender that cannot obtain the time MUST send the NILVALUE + Rfc5424SyslogMessage message = parse("<34>1 - mymachine.example.com su - ID47 - hello"); + + assertNull(message.getTimestamp()); + assertEquals("mymachine.example.com", message.getHostname()); + assertEquals("su", message.getAppName()); + assertEquals("hello", message.getLogMessage()); + } + + @Test + public void testRfc3164WithoutMsg() { + SyslogMessage message = SyslogConverter.toSyslogMessage("<34>Oct 11 22:14:15 mymachine"); + + assertEquals("mymachine", message.getHostname()); + assertEquals("", message.getLogMessage()); + } + + @Test + public void testRfc5424ExamplesUnchanged() { + Rfc5424SyslogMessage example1 = parse( + "<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - 'su root' failed for lonvick on /dev/pts/8"); + assertNotNull(example1.getTimestamp()); + assertEquals("-", example1.getStructuredData()); + assertEquals("'su root' failed for lonvick on /dev/pts/8", example1.getLogMessage()); + + Rfc5424SyslogMessage example2 + = parse("<165>1 2003-08-24T05:14:15.000003-07:00 192.0.2.1 myproc 8710 - - %% It's time to make the do-nuts."); + assertEquals("192.0.2.1", example2.getHostname()); + assertEquals("8710", example2.getProcId()); + assertEquals("-", example2.getMsgId()); + assertEquals("-", example2.getStructuredData()); + assertEquals("%% It's time to make the do-nuts.", example2.getLogMessage()); + + Rfc5424SyslogMessage example3 = parse(HEADER + EXAMPLE_SD + " An application event log entry..."); + assertEquals(EXAMPLE_SD, example3.getStructuredData()); + assertEquals("An application event log entry...", example3.getLogMessage()); + + // a quote that does not start a PARAM-VALUE, as parsed before + Rfc5424SyslogMessage stray = parse(HEADER + "[id@1 a=\"1\"b\"] hello"); + assertEquals("[id@1 a=\"1\"b\"]", stray.getStructuredData()); + assertEquals("hello", stray.getLogMessage()); + } + + @Test + public void testUnmarshalStructuredDataOnly() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + mock.expectedHeaderReceived(SyslogConstants.SYSLOG_HOSTNAME, "mymachine.example.com"); + + String sd = EXAMPLE_SD + "[examplePriority@32473 class=\"high\"]"; + template.sendBody("direct:unmarshal", HEADER + sd); + + MockEndpoint.assertIsSatisfied(context); + Rfc5424SyslogMessage message + = assertInstanceOf(Rfc5424SyslogMessage.class, mock.getReceivedExchanges().get(0).getIn().getBody()); + assertEquals(sd, message.getStructuredData()); + } + + @Test + public void testUnmarshalNilTimestamp() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(1); + + template.sendBody("direct:unmarshal", "<34>1 - mymachine.example.com su - ID47 - hello"); + + MockEndpoint.assertIsSatisfied(context); + assertNull(mock.getReceivedExchanges().get(0).getIn().getHeader(SyslogConstants.SYSLOG_TIMESTAMP)); + assertEquals("hello", mock.getReceivedExchanges().get(0).getIn().getBody(SyslogMessage.class).getLogMessage()); + } + + private static Rfc5424SyslogMessage parse(String text) { + return assertInstanceOf(Rfc5424SyslogMessage.class, SyslogConverter.toSyslogMessage(text)); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:unmarshal").unmarshal().syslog().to("mock:result"); + } + }; + } +}
