From 44912952e28e65bda1d51c6473ca3ee26845bd4b Mon Sep 17 00:00:00 2001 From: "Nuno M. Santos" Date: Fri, 1 Apr 2022 10:16:37 +0100 Subject: [PATCH 1/9] Add tests showing desired behavior when logging headers --- .../tests/e2e/common/LoggingFeatureTest.java | 273 ++++++++++++++++++ 1 file changed, 273 insertions(+) diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java index 45ed7343ca..f8d423afc1 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java @@ -18,10 +18,13 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -37,6 +40,8 @@ import javax.ws.rs.container.ContainerResponseContext; import javax.ws.rs.container.ContainerResponseFilter; import javax.ws.rs.core.Application; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -48,9 +53,13 @@ import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; +import org.hamcrest.Matcher; +import org.hamcrest.core.SubstringMatcher; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Suite; + +import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; @@ -117,6 +126,16 @@ public Response post(String text) { .build(); } + @Path("/echo-headers") + @GET + @Produces(TEXT_MEDIA_TYPE) + public Response getSameHeadersAsRequest(@Context HttpHeaders httpHeaders) { + Response.ResponseBuilder responseBuilder = Response.ok(ENTITY); + httpHeaders.getRequestHeaders().forEach( + (key, values) -> values.forEach( + value -> responseBuilder.header(key, value))); + return responseBuilder.build(); + } } /** @@ -299,6 +318,218 @@ public void testLoggingFeatureMaxEntitySize() { assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage(), containsString(trimmedEntity)); assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage(), containsString(trimmedEntity)); } + + @Test + public void testSingleValuedHeader() { + String headerName = "X-Single-Valued-Header"; + String headerValue = "test-value"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .request() + .header(headerName, headerValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = new ContainsHeaderMatcher(headerName, headerValue); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test + public void testMultivaluedHeader() { + String headerName = "X-Multi-Valued-Header"; + String firstHeaderValue = "first-value"; + String secondHeaderValue = "second-value"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .request() + .header(headerName, firstHeaderValue) + .header(headerName, secondHeaderValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = new ContainsHeaderMatcher(headerName, firstHeaderValue, secondHeaderValue); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test + public void testMultipleHeaders() { + String firstHeaderName = "X-First-Header"; + String firstHeaderValue = "first-value"; + String secondHeaderName = "X-Second-Header"; + String secondHeaderValue = "second-value"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .request() + .header(firstHeaderName, firstHeaderValue) + .header(secondHeaderName, secondHeaderValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(firstHeaderName, firstHeaderValue), + new ContainsHeaderMatcher(secondHeaderName, secondHeaderValue)); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test(expected = AssertionError.class) + public void testAuthorizationHeaderRedactedByDefault() { + String headerName = HttpHeaders.AUTHORIZATION; + String headerValue = "username:password"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .request() + .header(headerName, headerValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(headerName, "[redacted]"), + not(containsString(headerValue))); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test(expected = AssertionError.class) + public void testLoggingFeatureRedactOneHeader() { + String headerName = "X-Redact-This-Header"; + String headerValue = "sensitive-info"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .property("LOGGING_FEATURE_REDACT_HEADERS", headerName) + .request() + .header(headerName, headerValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(headerName, "[redacted]"), + not(containsString(headerValue))); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test(expected = AssertionError.class) + public void testLoggingFeatureRedactOneHeaderNormalizing() { + String headerName = "X-Redact-This-Header"; + String headerValue = "sensitive-info"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .property("LOGGING_FEATURE_REDACT_HEADERS", " " + headerName.toUpperCase(Locale.ROOT) + " ") + .request() + .header(headerName, headerValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(headerName, "[redacted]"), + not(containsString(headerValue))); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test(expected = AssertionError.class) + public void testLoggingFeatureRedactMultivaluedHeader() { + String headerName = "X-Redact-This-Header"; + String firstHeaderValue = "sensitive-info"; + String secondHeaderValue = "additional-info"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .property("LOGGING_FEATURE_REDACT_HEADERS", headerName) + .request() + .header(headerName, firstHeaderValue) + .header(headerName, secondHeaderValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(headerName, "[redacted]"), + not(containsString(firstHeaderValue)), + not(containsString(secondHeaderValue))); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test(expected = AssertionError.class) + public void testLoggingFeatureRedactMultipleHeaders() { + String firstHeaderName = "X-Redact-This-Header"; + String firstHeaderValue = "sensitive-info"; + String secondHeaderName = "X-Also-Redact-This-Header"; + String secondHeaderValue = "additional-info"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .property("LOGGING_FEATURE_REDACT_HEADERS", firstHeaderName + "," + secondHeaderName) + .request() + .header(firstHeaderName, firstHeaderValue) + .header(secondHeaderName, secondHeaderValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(firstHeaderName, "[redacted]"), + not(containsString(firstHeaderValue)), + new ContainsHeaderMatcher(secondHeaderName, "[redacted]"), + not(containsString(secondHeaderValue))); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } + + @Test + public void testLoggingFeatureRedactZeroHeaders() { + String headerName = HttpHeaders.AUTHORIZATION; + String headerValue = "username:password"; + final Response response = target("/echo-headers") + .register(LoggingFeature.class) + .property(LoggingFeature.LOGGING_FEATURE_LOGGER_NAME, LOGGER_NAME) + .property("LOGGING_FEATURE_REDACT_HEADERS", "") + .request() + .header(headerName, headerValue) + .get(); + + // Correct response status. + assertThat(response.getStatus(), is(Response.Status.OK.getStatusCode())); + // Check logs for header + List logRecords = getLoggedRecords(); + Matcher matcher = allOf( + new ContainsHeaderMatcher(headerName, headerValue), + not(containsString("[redacted]"))); + assertThat(getLoggingFilterRequestLogRecord(logRecords).getMessage().toLowerCase(), matcher); + assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); + } } /** @@ -503,4 +734,46 @@ public void testFilterAsContainerFilter() throws Exception { } } + + private static final class ContainsHeaderMatcher extends SubstringMatcher { + + private static final boolean ORDER_OF_HEADER_VALUES_IS_GUARANTEED = true; + + ContainsHeaderMatcher(String headerName, String... headerValues) { + super(makeRegex(headerName, Arrays.asList(headerValues))); + } + + private static String makeRegex(String headerName, List headerValues) { + StringBuilder stringBuilder = new StringBuilder("^[\\s\\S]*") + // Header name is case insensitive + .append("(?i)").append(quote(headerName)).append("(?-i): "); + + if (headerValues.size() == 1) { + stringBuilder.append(quote(headerValues.get(0))); + } else if (headerValues.size() > 1) { + if (ORDER_OF_HEADER_VALUES_IS_GUARANTEED) { + stringBuilder.append(String.join(",", headerValues)); + } else { + headerValues.forEach(headerValue -> stringBuilder + .append("(?=.*").append(quote(headerValue)).append(",?)")); + } + } + + return stringBuilder.append("[\\s\\S]*$").toString(); + } + + private static String quote(String input) { + return Pattern.quote(input); + } + + @Override + protected boolean evalSubstringOf(String string) { + return string.matches(substring); + } + + @Override + protected String relationship() { + return "matching regex"; + } + } } From b362de6c00b1228c65af0cddc599c48aa103e1b9 Mon Sep 17 00:00:00 2001 From: "Nuno M. Santos" Date: Fri, 1 Apr 2022 17:33:34 +0100 Subject: [PATCH 2/9] Redact HTTP Authorization header by default --- .../jersey/logging/LoggingFeature.java | 24 ++++++-- .../jersey/logging/LoggingInterceptor.java | 55 ++++++++++++++++--- .../tests/e2e/common/LoggingFeatureTest.java | 4 +- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java index f9b4d51a5d..0be0682d86 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java @@ -16,6 +16,8 @@ package org.glassfish.jersey.logging; +import java.util.Arrays; +import java.util.Collection; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -23,6 +25,7 @@ import javax.ws.rs.RuntimeType; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.HttpHeaders; import org.glassfish.jersey.CommonProperties; @@ -50,6 +53,7 @@ *
  • verbosity: {@link Verbosity#PAYLOAD_TEXT}
  • *
  • maximum entity size: {@value #DEFAULT_MAX_ENTITY_SIZE}
  • *
  • line separator: {@link #DEFAULT_SEPARATOR}
  • + *
  • redact headers: {@value #DEFAULT_REDACT_HEADERS}
  • * *

    * Server configurable properties: @@ -94,6 +98,10 @@ public class LoggingFeature implements Feature { * Default separator for entity logging. */ public static final String DEFAULT_SEPARATOR = "\n"; + /** + * Default headers to be redacted. If multiple, separate each header with a comma. + */ + public static final String DEFAULT_REDACT_HEADERS = HttpHeaders.AUTHORIZATION; private static final String LOGGER_NAME_POSTFIX = ".logger.name"; private static final String LOGGER_LEVEL_POSTFIX = ".logger.level"; @@ -269,7 +277,7 @@ private LoggingInterceptor createLoggingFilter(FeatureContext context, RuntimeTy private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBuilder builder, FeatureContext context, RuntimeType runtimeType) { - final Map properties = context.getConfiguration().getProperties(); + final Map properties = context.getConfiguration().getProperties(); //get values from properties (if any) final String filterLoggerName = CommonProperties.getValue( properties, @@ -283,14 +291,14 @@ private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBu properties, runtimeType == RuntimeType.SERVER ? LOGGING_FEATURE_LOGGER_LEVEL_SERVER : LOGGING_FEATURE_LOGGER_LEVEL_CLIENT, CommonProperties.getValue( - context.getConfiguration().getProperties(), + properties, LOGGING_FEATURE_LOGGER_LEVEL, DEFAULT_LOGGER_LEVEL)); final String filterSeparator = CommonProperties.getValue( properties, runtimeType == RuntimeType.SERVER ? LOGGING_FEATURE_SEPARATOR_SERVER : LOGGING_FEATURE_SEPARATOR_CLIENT, CommonProperties.getValue( - context.getConfiguration().getProperties(), + properties, LOGGING_FEATURE_SEPARATOR, DEFAULT_SEPARATOR)); final Verbosity filterVerbosity = CommonProperties.getValue( @@ -310,6 +318,7 @@ private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBu LOGGING_FEATURE_MAX_ENTITY_SIZE, DEFAULT_MAX_ENTITY_SIZE )); + String redactHeaders = DEFAULT_REDACT_HEADERS; final Level loggerLevel = Level.parse(filterLevel); @@ -319,6 +328,8 @@ private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBu builder.maxEntitySize = builder.maxEntitySize == null ? filterMaxEntitySize : builder.maxEntitySize; builder.level = builder.level == null ? loggerLevel : builder.level; builder.separator = builder.separator == null ? filterSeparator : builder.separator; + builder.redactHeaders = builder.redactHeaders == null + ? Arrays.asList(redactHeaders.split(",")) : builder.redactHeaders; return builder; } @@ -376,6 +387,7 @@ public static class LoggingFeatureBuilder { Integer maxEntitySize; Level level; String separator; + Collection redactHeaders; public LoggingFeatureBuilder() { @@ -400,9 +412,13 @@ public LoggingFeatureBuilder separator(String separator) { this.separator = separator; return this; } + public LoggingFeatureBuilder redactHeaders(Collection redactHeaders) { + this.redactHeaders = redactHeaders; + return this; + } public LoggingFeature build() { return new LoggingFeature(this); } } -} \ No newline at end of file +} diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java index fd1791b7d8..6cf08c9ea9 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java @@ -24,15 +24,21 @@ import java.io.OutputStream; import java.net.URI; import java.nio.charset.Charset; +import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; +import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; @@ -104,6 +110,7 @@ public int compare(final Map.Entry> o1, final Map.Entry redactHeaderPredicate; /** * Creates a logging filter using builder instance with custom logger and entity logging turned on, @@ -125,6 +132,9 @@ public int compare(final Map.Entry> o1, final Map.Entry false; } /** @@ -169,20 +179,28 @@ void printPrefixedHeaders(final StringBuilder b, final List val = headerEntry.getValue(); final String header = headerEntry.getKey(); - if (val.size() == 1) { - prefixId(b, id).append(prefix).append(header).append(": ").append(val.get(0)).append(separator); - } else { - final StringBuilder sb = new StringBuilder(); + prefixId(b, id).append(prefix).append(header).append(": "); + getValuesAppender(header, val).accept(b, val); + b.append(separator); + } + } + + private BiConsumer> getValuesAppender(String header, List values) { + if (redactHeaderPredicate.test(header)) { + return (b, v) -> b.append("[redacted]"); + } else if (values.size() == 1) { + return (b, v) -> b.append(v.get(0)); + } else { + return (b, v) -> { boolean add = false; - for (final Object s : val) { + for (final Object s : v) { if (add) { - sb.append(','); + b.append(','); } add = true; - sb.append(s); + b.append(s); } - prefixId(b, id).append(prefix).append(header).append(": ").append(sb.toString()).append(separator); - } + }; } } @@ -312,4 +330,23 @@ public void write(byte[] ba, int off, int len) throws IOException { } } + private static final class RedactHeaderPredicate implements Predicate { + private final Set headersToRedact; + + RedactHeaderPredicate(Collection headersToRedact) { + this.headersToRedact = headersToRedact.stream() + .filter(Objects::nonNull) + .map(RedactHeaderPredicate::normalize) + .collect(Collectors.toSet()); + } + + @Override + public boolean test(String header) { + return headersToRedact.contains(normalize(header)); + } + + private static String normalize(String input) { + return input.trim().toLowerCase(Locale.ROOT); + } + } } diff --git a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java index f8d423afc1..b4db47bb55 100644 --- a/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java +++ b/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java @@ -386,7 +386,7 @@ public void testMultipleHeaders() { assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); } - @Test(expected = AssertionError.class) + @Test public void testAuthorizationHeaderRedactedByDefault() { String headerName = HttpHeaders.AUTHORIZATION; String headerValue = "username:password"; @@ -508,7 +508,7 @@ public void testLoggingFeatureRedactMultipleHeaders() { assertThat(getLoggingFilterResponseLogRecord(logRecords).getMessage().toLowerCase(), matcher); } - @Test + @Test(expected = AssertionError.class) public void testLoggingFeatureRedactZeroHeaders() { String headerName = HttpHeaders.AUTHORIZATION; String headerValue = "username:password"; From 9c04a77a6facb01625227a03b381f8da761653a5 Mon Sep 17 00:00:00 2001 From: "Nuno M. Santos" Date: Fri, 1 Apr 2022 18:09:24 +0100 Subject: [PATCH 3/9] Allow configuration of HTTP headers to be redacted when logging requests and responses --- .../jersey/logging/ClientLoggingFilter.java | 3 +- .../jersey/logging/LoggingFeature.java | 27 +++++++++++++- .../LoggingFeatureAutoDiscoverable.java | 14 +++++--- .../jersey/logging/LoggingInterceptor.java | 1 + .../jersey/logging/ServerLoggingFilter.java | 3 +- .../tests/e2e/common/LoggingFeatureTest.java | 36 ++++++++----------- 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java b/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java index 07c3dbeca6..2228bf59cc 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -64,6 +64,7 @@ final class ClientLoggingFilter extends LoggingInterceptor implements ClientRequ * logging filter will print (and buffer in memory) only the specified number of bytes * and print "...more..." string at the end. Negative values are interpreted as zero. * separator delimiter for particular log lines. Default is Linux new line delimiter + * redactHeaders a string of comma separated HTTP headers to be redacted when logging. */ public ClientLoggingFilter(LoggingFeature.LoggingFeatureBuilder builder) { super(builder); diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java index 0be0682d86..ce7097ee31 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java @@ -44,6 +44,7 @@ *

  • {@link #LOGGING_FEATURE_VERBOSITY}
  • *
  • {@link #LOGGING_FEATURE_MAX_ENTITY_SIZE}
  • *
  • {@link #LOGGING_FEATURE_SEPARATOR}
  • + *
  • {@link #LOGGING_FEATURE_REDACT_HEADERS}
  • * *

    * If any of the configuration value is not set, following default values are applied: @@ -63,6 +64,7 @@ *

  • {@link #LOGGING_FEATURE_VERBOSITY_SERVER}
  • *
  • {@link #LOGGING_FEATURE_MAX_ENTITY_SIZE_SERVER}
  • *
  • {@link #LOGGING_FEATURE_SEPARATOR_SERVER}
  • + *
  • {@link #LOGGING_FEATURE_REDACT_HEADERS_SERVER}
  • * * Client configurable properties: *
      @@ -71,6 +73,7 @@ *
    • {@link #LOGGING_FEATURE_VERBOSITY_CLIENT}
    • *
    • {@link #LOGGING_FEATURE_MAX_ENTITY_SIZE_CLIENT}
    • *
    • {@link #LOGGING_FEATURE_SEPARATOR_CLIENT}
    • + *
    • {@link #LOGGING_FEATURE_REDACT_HEADERS_CLIENT}
    • *
    * * @author Ondrej Kosatka @@ -108,6 +111,7 @@ public class LoggingFeature implements Feature { private static final String VERBOSITY_POSTFIX = ".verbosity"; private static final String MAX_ENTITY_POSTFIX = ".entity.maxSize"; private static final String SEPARATOR_POSTFIX = ".separator"; + private static final String REDACT_HEADERS_POSTFIX = ".headers.redact"; private static final String LOGGING_FEATURE_COMMON_PREFIX = "jersey.config.logging"; /** * Common logger name property. @@ -129,6 +133,10 @@ public class LoggingFeature implements Feature { * Common property for configuring logging separator. */ public static final String LOGGING_FEATURE_SEPARATOR = LOGGING_FEATURE_COMMON_PREFIX + SEPARATOR_POSTFIX; + /** + * Common property for configuring headers to be redacted. + */ + public static final String LOGGING_FEATURE_REDACT_HEADERS = LOGGING_FEATURE_COMMON_PREFIX + REDACT_HEADERS_POSTFIX; private static final String LOGGING_FEATURE_SERVER_PREFIX = "jersey.config.server.logging"; /** @@ -151,6 +159,11 @@ public class LoggingFeature implements Feature { * Server property for configuring separator. */ public static final String LOGGING_FEATURE_SEPARATOR_SERVER = LOGGING_FEATURE_SERVER_PREFIX + SEPARATOR_POSTFIX; + /** + * Server property for configuring headers to be redacted. + */ + public static final String LOGGING_FEATURE_REDACT_HEADERS_SERVER = + LOGGING_FEATURE_SERVER_PREFIX + REDACT_HEADERS_POSTFIX; private static final String LOGGING_FEATURE_CLIENT_PREFIX = "jersey.config.client.logging"; /** @@ -173,6 +186,11 @@ public class LoggingFeature implements Feature { * Client property for logging separator. */ public static final String LOGGING_FEATURE_SEPARATOR_CLIENT = LOGGING_FEATURE_CLIENT_PREFIX + SEPARATOR_POSTFIX; + /** + * Client property for configuring headers to be redacted. + */ + public static final String LOGGING_FEATURE_REDACT_HEADERS_CLIENT = + LOGGING_FEATURE_CLIENT_PREFIX + REDACT_HEADERS_POSTFIX; private final LoggingFeatureBuilder builder; @@ -318,7 +336,14 @@ private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBu LOGGING_FEATURE_MAX_ENTITY_SIZE, DEFAULT_MAX_ENTITY_SIZE )); - String redactHeaders = DEFAULT_REDACT_HEADERS; + final String redactHeaders = CommonProperties.getValue( + properties, + runtimeType == RuntimeType.SERVER + ? LOGGING_FEATURE_REDACT_HEADERS_SERVER : LOGGING_FEATURE_REDACT_HEADERS_CLIENT, + CommonProperties.getValue( + properties, + LOGGING_FEATURE_REDACT_HEADERS, + DEFAULT_REDACT_HEADERS)); final Level loggerLevel = Level.parse(filterLevel); diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeatureAutoDiscoverable.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeatureAutoDiscoverable.java index 01e61a5e9d..84ca349bbd 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeatureAutoDiscoverable.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeatureAutoDiscoverable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2021 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2022 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -34,6 +34,9 @@ import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_MAX_ENTITY_SIZE; import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_MAX_ENTITY_SIZE_CLIENT; import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_MAX_ENTITY_SIZE_SERVER; +import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_REDACT_HEADERS; +import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_REDACT_HEADERS_CLIENT; +import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_REDACT_HEADERS_SERVER; import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_SEPARATOR; import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_SEPARATOR_CLIENT; import static org.glassfish.jersey.logging.LoggingFeature.LOGGING_FEATURE_SEPARATOR_SERVER; @@ -75,7 +78,8 @@ private boolean commonPropertyConfigured(Map properties) { || properties.containsKey(LOGGING_FEATURE_LOGGER_LEVEL) || properties.containsKey(LOGGING_FEATURE_VERBOSITY) || properties.containsKey(LOGGING_FEATURE_MAX_ENTITY_SIZE) - || properties.containsKey(LOGGING_FEATURE_SEPARATOR); + || properties.containsKey(LOGGING_FEATURE_SEPARATOR) + || properties.containsKey(LOGGING_FEATURE_REDACT_HEADERS); } private boolean clientConfigured(Map properties) { @@ -83,7 +87,8 @@ private boolean clientConfigured(Map properties) { || properties.containsKey(LOGGING_FEATURE_LOGGER_LEVEL_CLIENT) || properties.containsKey(LOGGING_FEATURE_VERBOSITY_CLIENT) || properties.containsKey(LOGGING_FEATURE_MAX_ENTITY_SIZE_CLIENT) - || properties.containsKey(LOGGING_FEATURE_SEPARATOR_CLIENT); + || properties.containsKey(LOGGING_FEATURE_SEPARATOR_CLIENT) + || properties.containsKey(LOGGING_FEATURE_REDACT_HEADERS_CLIENT); } private boolean serverConfigured(Map properties) { @@ -91,6 +96,7 @@ private boolean serverConfigured(Map properties) { || properties.containsKey(LOGGING_FEATURE_LOGGER_LEVEL_SERVER) || properties.containsKey(LOGGING_FEATURE_VERBOSITY_SERVER) || properties.containsKey(LOGGING_FEATURE_MAX_ENTITY_SIZE_SERVER) - || properties.containsKey(LOGGING_FEATURE_SEPARATOR_SERVER); + || properties.containsKey(LOGGING_FEATURE_SEPARATOR_SERVER) + || properties.containsKey(LOGGING_FEATURE_REDACT_HEADERS_SERVER); } } diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java index 6cf08c9ea9..344479966a 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java @@ -124,6 +124,7 @@ public int compare(final Map.Entry> o1, final Map.Entry headerValues) { // Header name is case insensitive .append("(?i)").append(quote(headerName)).append("(?-i): "); - if (headerValues.size() == 1) { - stringBuilder.append(quote(headerValues.get(0))); - } else if (headerValues.size() > 1) { - if (ORDER_OF_HEADER_VALUES_IS_GUARANTEED) { - stringBuilder.append(String.join(",", headerValues)); - } else { - headerValues.forEach(headerValue -> stringBuilder - .append("(?=.*").append(quote(headerValue)).append(",?)")); - } - } + // Not assuming order of header values is guaranteed to be consistent + headerValues.forEach(headerValue -> stringBuilder + .append("(?=.*").append(quote(headerValue)).append(",?)")); return stringBuilder.append("[\\s\\S]*$").toString(); } From 8069ee40fd1fac4dd8d34a1c690cd7d07274b555 Mon Sep 17 00:00:00 2001 From: "Nuno M. Santos" Date: Fri, 1 Apr 2022 19:02:33 +0100 Subject: [PATCH 4/9] Separate multiple HTTP headers to redact with a semicolon --- .../org/glassfish/jersey/logging/ClientLoggingFilter.java | 2 +- .../java/org/glassfish/jersey/logging/LoggingFeature.java | 4 ++-- .../java/org/glassfish/jersey/logging/LoggingInterceptor.java | 2 +- .../org/glassfish/jersey/logging/ServerLoggingFilter.java | 2 +- .../glassfish/jersey/tests/e2e/common/LoggingFeatureTest.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java b/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java index 2228bf59cc..b250915db6 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/ClientLoggingFilter.java @@ -64,7 +64,7 @@ final class ClientLoggingFilter extends LoggingInterceptor implements ClientRequ * logging filter will print (and buffer in memory) only the specified number of bytes * and print "...more..." string at the end. Negative values are interpreted as zero. * separator delimiter for particular log lines. Default is Linux new line delimiter - * redactHeaders a string of comma separated HTTP headers to be redacted when logging. + * redactHeaders a collection of HTTP headers to be redacted when logging. */ public ClientLoggingFilter(LoggingFeature.LoggingFeatureBuilder builder) { super(builder); diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java index ce7097ee31..abe2d47826 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingFeature.java @@ -102,7 +102,7 @@ public class LoggingFeature implements Feature { */ public static final String DEFAULT_SEPARATOR = "\n"; /** - * Default headers to be redacted. If multiple, separate each header with a comma. + * Default headers to be redacted. If multiple, separate each header with a semicolon. */ public static final String DEFAULT_REDACT_HEADERS = HttpHeaders.AUTHORIZATION; @@ -354,7 +354,7 @@ private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBu builder.level = builder.level == null ? loggerLevel : builder.level; builder.separator = builder.separator == null ? filterSeparator : builder.separator; builder.redactHeaders = builder.redactHeaders == null - ? Arrays.asList(redactHeaders.split(",")) : builder.redactHeaders; + ? Arrays.asList(redactHeaders.split(";")) : builder.redactHeaders; return builder; } diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java index 344479966a..c306c312f0 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java @@ -124,7 +124,7 @@ public int compare(final Map.Entry> o1, final Map.Entry Date: Fri, 1 Apr 2022 19:31:36 +0100 Subject: [PATCH 5/9] Filter out empty strings --- .../java/org/glassfish/jersey/logging/LoggingInterceptor.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java index c306c312f0..2d2e926bcc 100644 --- a/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java +++ b/core-common/src/main/java/org/glassfish/jersey/logging/LoggingInterceptor.java @@ -46,6 +46,7 @@ import javax.ws.rs.ext.WriterInterceptor; import javax.ws.rs.ext.WriterInterceptorContext; +import org.glassfish.jersey.internal.guava.Predicates; import org.glassfish.jersey.logging.LoggingFeature.Verbosity; import org.glassfish.jersey.message.MessageUtils; @@ -337,6 +338,7 @@ private static final class RedactHeaderPredicate implements Predicate { RedactHeaderPredicate(Collection headersToRedact) { this.headersToRedact = headersToRedact.stream() .filter(Objects::nonNull) + .filter(Predicates.not(String::isEmpty)) .map(RedactHeaderPredicate::normalize) .collect(Collectors.toSet()); } From 2c65e1a8f435379d2c793159ba64aeee974169d1 Mon Sep 17 00:00:00 2001 From: "Nuno M. Santos" Date: Fri, 1 Apr 2022 19:45:12 +0100 Subject: [PATCH 6/9] Update documentation --- docs/src/main/docbook/appendix-properties.xml | 35 ++++++++++++++++++- docs/src/main/docbook/jersey.ent | 8 +++++ docs/src/main/docbook/logging.xml | 15 +++++++- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/docs/src/main/docbook/appendix-properties.xml b/docs/src/main/docbook/appendix-properties.xml index 5c3455ed12..c154cd29f2 100644 --- a/docs/src/main/docbook/appendix-properties.xml +++ b/docs/src/main/docbook/appendix-properties.xml @@ -1,7 +1,7 @@