Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redact HTTP headers on LoggingFeature #5025

Merged
Merged
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 collection of HTTP headers to be redacted when logging.
nunomsantos marked this conversation as resolved.
Show resolved Hide resolved
*/
public ClientLoggingFilter(LoggingFeature.LoggingFeatureBuilder builder) {
super(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@

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;

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;

Expand All @@ -41,6 +44,7 @@
* <li>{@link #LOGGING_FEATURE_VERBOSITY}</li>
* <li>{@link #LOGGING_FEATURE_MAX_ENTITY_SIZE}</li>
* <li>{@link #LOGGING_FEATURE_SEPARATOR}</li>
* <li>{@link #LOGGING_FEATURE_REDACT_HEADERS}</li>
* </ul>
* <p>
* If any of the configuration value is not set, following default values are applied:
Expand All @@ -50,6 +54,7 @@
* <li>verbosity: {@link Verbosity#PAYLOAD_TEXT}</li>
* <li>maximum entity size: {@value #DEFAULT_MAX_ENTITY_SIZE}</li>
* <li>line separator: {@link #DEFAULT_SEPARATOR}</li>
* <li>redact headers: {@value #DEFAULT_REDACT_HEADERS}</li>
* </ul>
* <p>
* Server configurable properties:
Expand All @@ -59,6 +64,7 @@
* <li>{@link #LOGGING_FEATURE_VERBOSITY_SERVER}</li>
* <li>{@link #LOGGING_FEATURE_MAX_ENTITY_SIZE_SERVER}</li>
* <li>{@link #LOGGING_FEATURE_SEPARATOR_SERVER}</li>
* <li>{@link #LOGGING_FEATURE_REDACT_HEADERS_SERVER}</li>
* </ul>
* Client configurable properties:
* <ul>
Expand All @@ -67,6 +73,7 @@
* <li>{@link #LOGGING_FEATURE_VERBOSITY_CLIENT}</li>
* <li>{@link #LOGGING_FEATURE_MAX_ENTITY_SIZE_CLIENT}</li>
* <li>{@link #LOGGING_FEATURE_SEPARATOR_CLIENT}</li>
* <li>{@link #LOGGING_FEATURE_REDACT_HEADERS_CLIENT}</li>
* </ul>
*
* @author Ondrej Kosatka
Expand Down Expand Up @@ -94,12 +101,17 @@ 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 semicolon.
*/
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";
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.
Expand All @@ -121,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";
/**
Expand All @@ -143,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";
/**
Expand All @@ -165,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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add The headers are semicolon-separated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. Please give me some time.

Copy link
Contributor Author

@nunomsantos nunomsantos Apr 7, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed changes to the Javadocs on the 3 variations of the property.

I checked if we could set a property multiple times and then get a collection, but it looked like that's not possible.
Can you confirm that? Please let me know if you think of a better way to do this.

*/
public static final String LOGGING_FEATURE_REDACT_HEADERS_CLIENT =
LOGGING_FEATURE_CLIENT_PREFIX + REDACT_HEADERS_POSTFIX;

private final LoggingFeatureBuilder builder;

Expand Down Expand Up @@ -269,7 +295,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<String, ?> properties = context.getConfiguration().getProperties();
//get values from properties (if any)
final String filterLoggerName = CommonProperties.getValue(
properties,
Expand All @@ -283,14 +309,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(
Expand All @@ -310,6 +336,14 @@ private static LoggingFeatureBuilder configureBuilderParameters(LoggingFeatureBu
LOGGING_FEATURE_MAX_ENTITY_SIZE,
DEFAULT_MAX_ENTITY_SIZE
));
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);

Expand All @@ -319,6 +353,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;
}
Expand Down Expand Up @@ -376,6 +412,7 @@ public static class LoggingFeatureBuilder {
Integer maxEntitySize;
Level level;
String separator;
Collection<String> redactHeaders;

public LoggingFeatureBuilder() {

Expand All @@ -400,9 +437,13 @@ public LoggingFeatureBuilder separator(String separator) {
this.separator = separator;
return this;
}
public LoggingFeatureBuilder redactHeaders(Collection<String> redactHeaders) {
this.redactHeaders = redactHeaders;
return this;
}

public LoggingFeature build() {
return new LoggingFeature(this);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -75,22 +78,25 @@ 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) {
return properties.containsKey(LOGGING_FEATURE_LOGGER_NAME_CLIENT)
|| 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) {
return properties.containsKey(LOGGING_FEATURE_LOGGER_NAME_SERVER)
|| 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,29 @@
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;
import javax.ws.rs.core.MultivaluedMap;
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;

Expand Down Expand Up @@ -104,6 +111,7 @@ public int compare(final Map.Entry<String, List<String>> o1, final Map.Entry<Str
final Verbosity verbosity;
final int maxEntitySize;
final String separator;
final Predicate<String> redactHeaderPredicate;

/**
* Creates a logging filter using builder instance with custom logger and entity logging turned on,
Expand All @@ -117,6 +125,7 @@ public int compare(final Map.Entry<String, List<String>> o1, final Map.Entry<Str
* 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 collection of HTTP headers to be redacted when logging.
*/

LoggingInterceptor(LoggingFeature.LoggingFeatureBuilder builder) {
Expand All @@ -125,6 +134,9 @@ public int compare(final Map.Entry<String, List<String>> o1, final Map.Entry<Str
this.verbosity = builder.verbosity;
this.maxEntitySize = Math.max(0, builder.maxEntitySize);
this.separator = builder.separator;
this.redactHeaderPredicate = builder.redactHeaders != null && !builder.redactHeaders.isEmpty()
? new RedactHeaderPredicate(builder.redactHeaders)
: header -> false;
}

/**
Expand Down Expand Up @@ -169,20 +181,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<StringBuilder, List<?>> 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);
}
};
}
}

Expand Down Expand Up @@ -312,4 +332,24 @@ public void write(byte[] ba, int off, int len) throws IOException {
}
}

private static final class RedactHeaderPredicate implements Predicate<String> {
private final Set<String> headersToRedact;

RedactHeaderPredicate(Collection<String> headersToRedact) {
this.headersToRedact = headersToRedact.stream()
.filter(Objects::nonNull)
.filter(Predicates.not(String::isEmpty))
.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);
Copy link
Contributor Author

@nunomsantos nunomsantos Apr 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just crossed my mind that maybe we should do a null check here. Not sure if it's possible for a null header ever to reach this point but could do it just to be on the safe side.
The problem would not be in the constructor usage, but when test(header) is called.

Thoughts, @jansupol @senivam?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's OK. This code final String header = headerEntry.getKey(); is NPE safe and this code

this.headersToRedact = headersToRedact.stream()
                   .filter(Objects::nonNull)
                   .filter(Predicates.not(String::isEmpty))
                   .map(RedactHeaderPredicate::normalize)
                   .collect(Collectors.toSet());

is NPE safe. Nothing else calls that predicate.

}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -64,6 +64,7 @@ final class ServerLoggingFilter extends LoggingInterceptor implements ContainerR
* 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 collection of HTTP headers to be redacted when logging.
*/
public ServerLoggingFilter(final LoggingFeature.LoggingFeatureBuilder builder) {
super(builder);
Expand Down
Loading