This is an automated email from the ASF dual-hosted git repository. reta pushed a commit to branch 3.6.x-fixes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 3cb6cfa303abf54609005da37e02c5c8a97144e8 Author: Andriy Redko <[email protected]> AuthorDate: Mon Jul 21 18:49:25 2025 -0400 CXF-9145: Inconcise handling of logging features logMulitpart and logBinary (#2520) (cherry picked from commit afa25c791082e937c9fc5c17d7c7c75ceb39da5a) (cherry picked from commit bb667a2fdc733c26858c634b22c963f03b448c58) --- .../ext/logging/AbstractLoggingInterceptor.java | 50 ++++++++- .../ext/logging/event/DefaultLogEventMapper.java | 15 ++- .../cxf/ext/logging/LoggingInInterceptorTest.java | 120 +++++++++++++++++++++ 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java index 971335854c..67e27633d3 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/AbstractLoggingInterceptor.java @@ -153,16 +153,21 @@ public abstract class AbstractLoggingInterceptor extends AbstractPhaseIntercepto String[] parts = originalLogString.split(Pattern.quote(boundary)); String payload = ""; for (String str : parts) { - if (findContentType(str) != null + final String contentType = findContentType(str); + if (contentType != null && eventMapper.isBinaryContent( - findContentType(str).substring("Content-Type:".length()).trim())) { - payload = payload + "\r\n" + CONTENT_SUPPRESSED + "\r\n"; + contentType.substring("Content-Type:".length()).trim())) { + final String headers = extractHeaders(str); + if (headers == null || headers.isEmpty()) { + payload = payload + "\r\n" + CONTENT_SUPPRESSED + "\r\n"; + } else { + payload = payload + "\r\n" + headers + "\r\n" + CONTENT_SUPPRESSED + "\r\n"; + } } else { payload = payload + str; payload = payload + boundary; } } - originalLogString = payload; } } catch (Exception ex) { @@ -172,6 +177,43 @@ public abstract class AbstractLoggingInterceptor extends AbstractPhaseIntercepto } + private static String extractHeaders(String str) { + // We know that content header is present, let's start from that + final Matcher m = CONTENT_TYPE_PATTERN.matcher(str); + if (m.find()) { + int payloadStart = 0; + + // Look for empty line to find out where the actual payload starts (it should + // follow headers) + int buffer = 0; + for (int i = m.start(0); i < str.length(); ++i) { + final char c = str.charAt(i); + // a linefeed is a terminator, always. + if (c == '\n') { + if (buffer == 0) { + payloadStart = i; + break; + } else { + buffer = 0; + } + } else if (c == '\r') { + //just ignore the CR. The next character SHOULD be an NL. If not, we're + //just going to discard this + continue; + } else { + // just count is as a buffer + ++buffer; + } + } + + if (payloadStart > 0 && payloadStart < str.length()) { + return str.substring(0, payloadStart).trim(); + } + } + + return null; + } + private String findContentType(String payload) { // Use regex to get the Content-Type and return null if it's not found Matcher m = CONTENT_TYPE_PATTERN.matcher(payload); diff --git a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java index 4e8ee740d3..ee3a386bec 100644 --- a/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java +++ b/rt/features/logging/src/main/java/org/apache/cxf/ext/logging/event/DefaultLogEventMapper.java @@ -234,11 +234,22 @@ public class DefaultLogEventMapper { private boolean isBinaryContent(Message message) { String contentType = safeGet(message, Message.CONTENT_TYPE); - return contentType != null && binaryContentMediaTypes.contains(contentType); + return isBinaryContent(contentType); } public boolean isBinaryContent(String contentType) { - return contentType != null && binaryContentMediaTypes.contains(contentType); + if (contentType == null) { + return false; + } else { + // Consider compound header values, like: + // Content-Type: application/xop+xml; charset=UTF-8; type="text/xml" + final int index = contentType.indexOf(';'); + if (index > 0) { + return binaryContentMediaTypes.contains(contentType.substring(0, index).trim()); + } else { + return binaryContentMediaTypes.contains(contentType); + } + } } private boolean isMultipartContent(Message message) { diff --git a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java index fda5b7bf04..d7b7578150 100644 --- a/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java +++ b/rt/features/logging/src/test/java/org/apache/cxf/ext/logging/LoggingInInterceptorTest.java @@ -19,6 +19,9 @@ package org.apache.cxf.ext.logging; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -27,6 +30,7 @@ import java.util.Map; import java.util.Set; import org.apache.cxf.ext.logging.event.LogEvent; +import org.apache.cxf.io.CachedOutputStream; import org.apache.cxf.message.ExchangeImpl; import org.apache.cxf.message.Message; import org.apache.cxf.message.MessageImpl; @@ -36,6 +40,7 @@ import org.junit.Test; import static org.apache.cxf.ext.logging.event.DefaultLogEventMapper.MASKED_HEADER_VALUE; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalToIgnoringCase; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertEquals; @@ -114,4 +119,119 @@ public class LoggingInInterceptorTest { assertEquals(TEST_HEADER_VALUE, event.getHeaders().get(TEST_HEADER_NAME)); } + + @Test + public void shouldLogMultipartPayload() throws IOException { + message.put(Message.ENDPOINT_ADDRESS, "http://localhost:9001/"); + message.put(Message.REQUEST_URI, "/api"); + + StringBuilder buf = new StringBuilder(512); + buf.append("------=_Part_0_2180223.1203118300920\n"); + buf.append("Content-Type: application/xop+xml; charset=UTF-8; type=\"text/xml\"\n"); + buf.append("Content-Transfer-Encoding: 8bit\n"); + buf.append("Content-ID: <[email protected]>\n"); + buf.append('\n'); + buf.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " + + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " + + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" + + "<soap:Body><getNextMessage xmlns=\"http://foo.bar\" /></soap:Body>" + + "</soap:Envelope>\n"); + buf.append("------=_Part_0_2180223.1203118300920--\n"); + + String ct = "multipart/related; type=\"application/xop+xml\"; " + + "boundary=\"----=_Part_0_2180223.1203118300920\""; + + final byte[] bytes = buf.toString().getBytes(StandardCharsets.UTF_8); + final OutputStream os = new CachedOutputStream(); + os.write(bytes, 0, bytes.length); + message.setContent(CachedOutputStream.class, os); + message.put(Message.CONTENT_TYPE, ct); + + interceptor.addBinaryContentMediaTypes("application/xop+xml"); + interceptor.setLogMultipart(true); + interceptor.setLogBinary(true); + interceptor.handleMessage(message); + + assertThat(sender.getEvents(), hasSize(1)); + final LogEvent event = sender.getEvents().get(0); + + assertThat(event.getPayload(), equalToIgnoringCase(buf.toString())); + } + + @Test + public void shouldLogMultipartHeadersOnly() throws IOException { + message.put(Message.ENDPOINT_ADDRESS, "http://localhost:9001/"); + message.put(Message.REQUEST_URI, "/api"); + + final StringBuilder headers = new StringBuilder(512); + headers.append("------=_Part_0_2180223.1203118300920\n"); + headers.append("Content-Type: application/xop+xml; charset=UTF-8; type=\"text/xml\"\n"); + headers.append("Content-Transfer-Encoding: 8bit\n"); + headers.append("Content-ID: <[email protected]>\n"); + + final StringBuilder buf = new StringBuilder(512); + buf.append(headers); + buf.append('\n'); + buf.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " + + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " + + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" + + "<soap:Body><getNextMessage xmlns=\"http://foo.bar\" /></soap:Body>" + + "</soap:Envelope>\n"); + buf.append("------=_Part_0_2180223.1203118300920--\n"); + + String ct = "multipart/related; type=\"application/xop+xml\"; " + + "boundary=\"----=_Part_0_2180223.1203118300920\""; + + final byte[] bytes = buf.toString().getBytes(StandardCharsets.UTF_8); + final OutputStream os = new CachedOutputStream(); + os.write(bytes, 0, bytes.length); + message.setContent(CachedOutputStream.class, os); + message.put(Message.CONTENT_TYPE, ct); + + interceptor.addBinaryContentMediaTypes("application/xop+xml"); + interceptor.setLogMultipart(true); + interceptor.setLogBinary(false); + interceptor.handleMessage(message); + + assertThat(sender.getEvents(), hasSize(1)); + final LogEvent event = sender.getEvents().get(0); + + assertThat(event.getPayload().replaceAll("\r\n", "\n"), equalToIgnoringCase(headers.toString() + + "--- Content suppressed ---\n--\n------=_Part_0_2180223.1203118300920")); + } + + @Test + public void shouldLogMultipartPayloadNoHeaders() throws IOException { + message.put(Message.ENDPOINT_ADDRESS, "http://localhost:9001/"); + message.put(Message.REQUEST_URI, "/api"); + + StringBuilder buf = new StringBuilder(512); + buf.append("------=_Part_0_2180223.1203118300920\n"); + buf.append('\n'); + buf.append("<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " + + "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " + + "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" + + "<soap:Body><getNextMessage xmlns=\"http://foo.bar\" /></soap:Body>" + + "</soap:Envelope>\n"); + buf.append("------=_Part_0_2180223.1203118300920--\n"); + + String ct = "multipart/related; type=\"application/xop+xml\"; " + + "boundary=\"----=_Part_0_2180223.1203118300920\""; + + final byte[] bytes = buf.toString().getBytes(StandardCharsets.UTF_8); + final OutputStream os = new CachedOutputStream(); + os.write(bytes, 0, bytes.length); + message.setContent(CachedOutputStream.class, os); + message.put(Message.CONTENT_TYPE, ct); + + interceptor.addBinaryContentMediaTypes("application/xop+xml"); + interceptor.setLogMultipart(true); + interceptor.setLogBinary(true); + interceptor.handleMessage(message); + + assertThat(sender.getEvents(), hasSize(1)); + final LogEvent event = sender.getEvents().get(0); + + assertThat(event.getPayload(), equalToIgnoringCase(buf.toString())); + } }
