Skip to content

Commit

Permalink
Redact HTTP headers on LoggingFeature (#5025)
Browse files Browse the repository at this point in the history
  • Loading branch information
nunomsantos authored May 2, 2022
1 parent 79f5547 commit 43a9e18
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 20 deletions.
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.
*/
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. The headers are semicolon-separated.
*/
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. The headers are semicolon-separated.
*/
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. The headers are semicolon-separated.
*/
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);
}
}
}
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

0 comments on commit 43a9e18

Please sign in to comment.