From b7697b5fe4618f4014ccc729abee12cf29d4a8fb Mon Sep 17 00:00:00 2001 From: Ikhun Um Date: Tue, 30 Jan 2024 00:39:28 +0900 Subject: [PATCH] Introduce `HeaderMaskingFunction` - A header name is necessary to dynamically mask header values. - Handle a null value returned by masking functions. - Replace Function logging}/HeadersSanitizer.java | 17 ++++--- .../{ => logging}/JsonHeadersSanitizer.java | 12 +++-- .../JsonHeadersSanitizerBuilder.java | 6 +-- .../common/logging/JsonLogFormatter.java | 1 - .../logging/JsonLogFormatterBuilder.java | 1 - .../{ => logging}/TextHeadersSanitizer.java | 33 ++++++++---- .../TextHeadersSanitizerBuilder.java | 6 +-- .../common/logging/TextLogFormatter.java | 1 - .../logging/TextLogFormatterBuilder.java | 1 - .../common/logging/JsonLogFormatterTest.java | 44 +++++++++++++--- .../common/logging/TextLogFormatterTest.java | 51 +++++++++++++++---- 14 files changed, 185 insertions(+), 56 deletions(-) rename core/src/main/java/com/linecorp/armeria/common/{ => logging}/AbstractHeadersSanitizerBuilder.java (77%) create mode 100644 core/src/main/java/com/linecorp/armeria/common/logging/HeaderMaskingFunction.java rename core/src/main/java/com/linecorp/armeria/common/{ => logging}/HeadersSanitizer.java (78%) rename core/src/main/java/com/linecorp/armeria/common/{ => logging}/JsonHeadersSanitizer.java (82%) rename core/src/main/java/com/linecorp/armeria/common/{ => logging}/JsonHeadersSanitizerBuilder.java (93%) rename core/src/main/java/com/linecorp/armeria/common/{ => logging}/TextHeadersSanitizer.java (68%) rename core/src/main/java/com/linecorp/armeria/common/{ => logging}/TextHeadersSanitizerBuilder.java (89%) diff --git a/core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/AbstractHeadersSanitizerBuilder.java similarity index 77% rename from core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java rename to core/src/main/java/com/linecorp/armeria/common/logging/AbstractHeadersSanitizerBuilder.java index 89a3e83b030..db7367928e6 100644 --- a/core/src/main/java/com/linecorp/armeria/common/AbstractHeadersSanitizerBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/AbstractHeadersSanitizerBuilder.java @@ -14,7 +14,7 @@ * under the License. */ -package com.linecorp.armeria.common; +package com.linecorp.armeria.common.logging; import static java.util.Objects.requireNonNull; @@ -24,6 +24,8 @@ import com.google.common.collect.ImmutableSet; +import com.linecorp.armeria.common.HttpHeaderNames; + import io.netty.util.AsciiString; /** @@ -41,7 +43,9 @@ public abstract class AbstractHeadersSanitizerBuilder { private final Set maskingHeaders = new HashSet<>(); - private Function maskingFunction = header -> "****"; + private HeaderMaskingFunction maskingFunction = HeaderMaskingFunction.of(); + + AbstractHeadersSanitizerBuilder() {} /** * Sets the headers to mask before logging. @@ -69,8 +73,21 @@ final Set maskingHeaders() { /** * Sets the {@link Function} to use to maskFunction headers before logging. + * The default maskingFunction is {@link HeaderMaskingFunction#of()} + * + *
{@code
+     * builder.maskingFunction((name, value) -> {
+     *   if (name.equals(HttpHeaderNames.AUTHORIZATION)) {
+     *      return "****";
+     *   } else if (name.equals(HttpHeaderNames.COOKIE)) {
+     *     return name.substring(0, 4) + "****";
+     *   } else {
+     *     return value;
+     *   }
+     * }
+     * }
*/ - public AbstractHeadersSanitizerBuilder maskingFunction(Function maskingFunction) { + public AbstractHeadersSanitizerBuilder maskingFunction(HeaderMaskingFunction maskingFunction) { this.maskingFunction = requireNonNull(maskingFunction, "maskingFunction"); return this; } @@ -78,7 +95,7 @@ public AbstractHeadersSanitizerBuilder maskingFunction(Function maskingFunction() { + final HeaderMaskingFunction maskingFunction() { return maskingFunction; } } diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/AbstractLogFormatterBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/AbstractLogFormatterBuilder.java index 08bf5bf3c39..77eba904ddd 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/AbstractLogFormatterBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/AbstractLogFormatterBuilder.java @@ -21,7 +21,6 @@ import java.util.function.BiFunction; import java.util.function.Function; -import com.linecorp.armeria.common.HeadersSanitizer; import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.annotation.Nullable; diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/HeaderMaskingFunction.java b/core/src/main/java/com/linecorp/armeria/common/logging/HeaderMaskingFunction.java new file mode 100644 index 00000000000..c665f2446cb --- /dev/null +++ b/core/src/main/java/com/linecorp/armeria/common/logging/HeaderMaskingFunction.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 LINE Corporation + * + * LINE Corporation 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: + * + * https://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 com.linecorp.armeria.common.logging; + +import com.linecorp.armeria.common.annotation.Nullable; + +import io.netty.util.AsciiString; + +/** + * A function that masks the specified header value. + */ +@FunctionalInterface +public interface HeaderMaskingFunction { + + /** + * Returns the default {@link HeaderMaskingFunction} that masks the given value with {@code ****}. + */ + static HeaderMaskingFunction of() { + return (name, value) -> "****"; + } + + /** + * Masks the specified {@code value} of the specified {@code name}. + * If {@code null} is returned, the specified {@code value} will be removed from the log. + */ + @Nullable + String mask(AsciiString name, String value); +} diff --git a/core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java b/core/src/main/java/com/linecorp/armeria/common/logging/HeadersSanitizer.java similarity index 78% rename from core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java rename to core/src/main/java/com/linecorp/armeria/common/logging/HeadersSanitizer.java index bfd3b3a9e35..4811b7fa78d 100644 --- a/core/src/main/java/com/linecorp/armeria/common/HeadersSanitizer.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/HeadersSanitizer.java @@ -14,12 +14,15 @@ * under the License. */ -package com.linecorp.armeria.common; +package com.linecorp.armeria.common.logging; import java.util.function.BiFunction; import com.fasterxml.jackson.databind.JsonNode; +import com.linecorp.armeria.common.HttpHeaderNames; +import com.linecorp.armeria.common.HttpHeaders; +import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.annotation.Nullable; /** @@ -28,9 +31,9 @@ @FunctionalInterface public interface HeadersSanitizer extends BiFunction { /** - * Returns the default text {@link HeadersSanitizer}. - * {@link HttpHeaderNames#AUTHORIZATION}, {@link HttpHeaderNames#COOKIE}, {@link HttpHeaderNames#SET_COOKIE}, - * and {@link HttpHeaderNames#PROXY_AUTHORIZATION} are masked. + * Returns the default text {@link HeadersSanitizer} that masks + * {@link HttpHeaderNames#AUTHORIZATION}, {@link HttpHeaderNames#COOKIE}, + * {@link HttpHeaderNames#SET_COOKIE}, and {@link HttpHeaderNames#PROXY_AUTHORIZATION} with {@code ****}. */ static HeadersSanitizer ofText() { return TextHeadersSanitizer.INSTANCE; @@ -44,9 +47,9 @@ static TextHeadersSanitizerBuilder builderForText() { } /** - * Returns the default json {@link HeadersSanitizer}. - * {@link HttpHeaderNames#AUTHORIZATION}, {@link HttpHeaderNames#COOKIE}, {@link HttpHeaderNames#SET_COOKIE}, - * and {@link HttpHeaderNames#PROXY_AUTHORIZATION} are masked. + * Returns the default JSON {@link HeadersSanitizer} that masks + * {@link HttpHeaderNames#AUTHORIZATION}, {@link HttpHeaderNames#COOKIE}, + * {@link HttpHeaderNames#SET_COOKIE}, and {@link HttpHeaderNames#PROXY_AUTHORIZATION} with {@code ****}. */ static HeadersSanitizer ofJson() { return JsonHeadersSanitizer.INSTANCE; diff --git a/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizer.java b/core/src/main/java/com/linecorp/armeria/common/logging/JsonHeadersSanitizer.java similarity index 82% rename from core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizer.java rename to core/src/main/java/com/linecorp/armeria/common/logging/JsonHeadersSanitizer.java index 9ec5f1a6b2b..f9d7dab41da 100644 --- a/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizer.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/JsonHeadersSanitizer.java @@ -14,17 +14,19 @@ * under the License. */ -package com.linecorp.armeria.common; +package com.linecorp.armeria.common.logging; -import static com.linecorp.armeria.common.TextHeadersSanitizer.maskHeaders; +import static com.linecorp.armeria.common.logging.TextHeadersSanitizer.maskHeaders; import java.util.Set; -import java.util.function.Function; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.linecorp.armeria.common.HttpHeaders; +import com.linecorp.armeria.common.RequestContext; + import io.netty.util.AsciiString; /** @@ -34,10 +36,10 @@ final class JsonHeadersSanitizer implements HeadersSanitizer { static final HeadersSanitizer INSTANCE = new JsonHeadersSanitizerBuilder().build(); private final Set maskingHeaders; - private final Function maskingFunction; + private final HeaderMaskingFunction maskingFunction; private final ObjectMapper objectMapper; - JsonHeadersSanitizer(Set maskingHeaders, Function maskingFunction, + JsonHeadersSanitizer(Set maskingHeaders, HeaderMaskingFunction maskingFunction, ObjectMapper objectMapper) { this.maskingHeaders = maskingHeaders; this.maskingFunction = maskingFunction; diff --git a/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/JsonHeadersSanitizerBuilder.java similarity index 93% rename from core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java rename to core/src/main/java/com/linecorp/armeria/common/logging/JsonHeadersSanitizerBuilder.java index bdee7fc66e7..cd8440af1cc 100644 --- a/core/src/main/java/com/linecorp/armeria/common/JsonHeadersSanitizerBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/JsonHeadersSanitizerBuilder.java @@ -14,12 +14,10 @@ * under the License. */ -package com.linecorp.armeria.common; +package com.linecorp.armeria.common.logging; import static java.util.Objects.requireNonNull; -import java.util.function.Function; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -45,7 +43,7 @@ public JsonHeadersSanitizerBuilder maskingHeaders(Iterable maskingFunction) { + public JsonHeadersSanitizerBuilder maskingFunction(HeaderMaskingFunction maskingFunction) { return (JsonHeadersSanitizerBuilder) super.maskingFunction(maskingFunction); } diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java b/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java index ca2c821a537..e6a25507999 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatter.java @@ -29,7 +29,6 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.MoreObjects; -import com.linecorp.armeria.common.HeadersSanitizer; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatterBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatterBuilder.java index 8ee9e9e14dc..d3bfc634976 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatterBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/JsonLogFormatterBuilder.java @@ -24,7 +24,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.linecorp.armeria.common.HeadersSanitizer; import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.annotation.Nullable; diff --git a/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java b/core/src/main/java/com/linecorp/armeria/common/logging/TextHeadersSanitizer.java similarity index 68% rename from core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java rename to core/src/main/java/com/linecorp/armeria/common/logging/TextHeadersSanitizer.java index 415b2172073..c089dffacd1 100644 --- a/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizer.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/TextHeadersSanitizer.java @@ -14,17 +14,20 @@ * under the License. */ -package com.linecorp.armeria.common; +package com.linecorp.armeria.common.logging; import static com.google.common.collect.ImmutableList.toImmutableList; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.function.BiConsumer; -import java.util.function.Function; import com.google.common.collect.ImmutableList; +import com.linecorp.armeria.common.HttpHeaders; +import com.linecorp.armeria.common.RequestContext; + import io.netty.util.AsciiString; /** @@ -36,9 +39,10 @@ final class TextHeadersSanitizer implements HeadersSanitizer { private final Set maskingHeaders; - private final Function maskingFunction; + private final HeaderMaskingFunction maskingFunction; - TextHeadersSanitizer(Set maskingHeaders, Function maskingFunction) { + TextHeadersSanitizer(Set maskingHeaders, + HeaderMaskingFunction maskingFunction) { this.maskingHeaders = maskingHeaders; this.maskingFunction = maskingFunction; } @@ -66,19 +70,30 @@ public String sanitize(RequestContext ctx, HttpHeaders headers) { } static void maskHeaders( - HttpHeaders headers, Set maskingHeaders, Function maskingFunction, - final BiConsumer> consumer) { + HttpHeaders headers, Set maskingHeaders, + HeaderMaskingFunction maskingFunction, + BiConsumer> consumer) { for (AsciiString headerName : headers.names()) { List values = headers.getAll(headerName); if (maskingHeaders.contains(headerName)) { // Mask the header values. if (values.size() == 1) { - values = ImmutableList.of(maskingFunction.apply(values.get(0))); + final String masked = maskingFunction.mask(headerName, values.get(0)); + if (masked == null) { + values = ImmutableList.of(); + } else { + values = ImmutableList.of(masked); + } } else { - values = values.stream().map(maskingFunction).collect(toImmutableList()); + values = values.stream() + .map(value -> maskingFunction.mask(headerName, value)) + .filter(Objects::nonNull) + .collect(toImmutableList()); } } - consumer.accept(headerName, values); + if (!values.isEmpty()) { + consumer.accept(headerName, values); + } } } } diff --git a/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/TextHeadersSanitizerBuilder.java similarity index 89% rename from core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java rename to core/src/main/java/com/linecorp/armeria/common/logging/TextHeadersSanitizerBuilder.java index d648577db50..c84353bb189 100644 --- a/core/src/main/java/com/linecorp/armeria/common/TextHeadersSanitizerBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/TextHeadersSanitizerBuilder.java @@ -14,9 +14,7 @@ * under the License. */ -package com.linecorp.armeria.common; - -import java.util.function.Function; +package com.linecorp.armeria.common.logging; /** * A builder implementation for Text {@link HeadersSanitizer}. @@ -34,7 +32,7 @@ public TextHeadersSanitizerBuilder maskingHeaders(Iterable maskingFunction) { + public TextHeadersSanitizerBuilder maskingFunction(HeaderMaskingFunction maskingFunction) { return (TextHeadersSanitizerBuilder) super.maskingFunction(maskingFunction); } diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java b/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java index 6c419f813a9..ce39d9a7f95 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatter.java @@ -23,7 +23,6 @@ import com.google.common.base.MoreObjects; -import com.linecorp.armeria.common.HeadersSanitizer; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.annotation.Nullable; diff --git a/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatterBuilder.java b/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatterBuilder.java index 8f7e67b45c9..e79f23fd63a 100644 --- a/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatterBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/common/logging/TextLogFormatterBuilder.java @@ -20,7 +20,6 @@ import java.util.function.BiFunction; -import com.linecorp.armeria.common.HeadersSanitizer; import com.linecorp.armeria.common.HttpHeaders; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.annotation.Nullable; diff --git a/core/src/test/java/com/linecorp/armeria/common/logging/JsonLogFormatterTest.java b/core/src/test/java/com/linecorp/armeria/common/logging/JsonLogFormatterTest.java index 4d757b483b2..a7f14aa1a70 100644 --- a/core/src/test/java/com/linecorp/armeria/common/logging/JsonLogFormatterTest.java +++ b/core/src/test/java/com/linecorp/armeria/common/logging/JsonLogFormatterTest.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,7 +25,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import com.linecorp.armeria.common.HeadersSanitizer; +import com.linecorp.armeria.common.HttpHeaderNames; import com.linecorp.armeria.common.HttpMethod; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpStatus; @@ -105,7 +104,7 @@ void defaultMaskingHeadersShouldBeOverridable() { @Test void maskRequestHeaders() { - final Function maskingFunction = (header) -> "****armeria****"; + final HeaderMaskingFunction maskingFunction = (name, value) -> "****armeria****"; final LogFormatter logFormatter = LogFormatter.builderForJson() .requestHeadersSanitizer( HeadersSanitizer.builderForJson() @@ -124,7 +123,7 @@ void maskRequestHeaders() { final Matcher matcher1 = Pattern.compile("\"accept\":\"(.*?)\"").matcher(requestLog); assertThat(matcher1.find()).isTrue(); - assertThat(matcher1.group(1)).isEqualTo(maskingFunction.apply("text/html")); + assertThat(matcher1.group(1)).isEqualTo(maskingFunction.mask(HttpHeaderNames.ACCEPT, "text/html")); final Matcher matcher2 = Pattern.compile("\"cache-control\":\"(.*?)\"").matcher(requestLog); assertThat(matcher2.find()).isTrue(); @@ -133,7 +132,7 @@ void maskRequestHeaders() { @Test void maskResponseHeaders() { - final Function maskingFunction = (header) -> "****armeria****"; + final HeaderMaskingFunction maskingFunction = (name, value) -> "****armeria****"; final LogFormatter logFormatter = LogFormatter.builderForJson() .responseHeadersSanitizer( HeadersSanitizer.builderForJson() @@ -150,7 +149,8 @@ void maskResponseHeaders() { final String responseLog = logFormatter.formatResponse(log); final Matcher matcher1 = Pattern.compile("\"content-type\":\"(.*?)\"").matcher(responseLog); assertThat(matcher1.find()).isTrue(); - assertThat(matcher1.group(1)).isEqualTo(maskingFunction.apply("text/html")); + assertThat(matcher1.group(1)).isEqualTo( + maskingFunction.mask(HttpHeaderNames.CONTENT_TYPE, "text/html")); final Matcher matcher2 = Pattern.compile("\"cache-control\":\"(.*?)\"").matcher(responseLog); assertThat(matcher2.find()).isTrue(); @@ -159,7 +159,7 @@ void maskResponseHeaders() { @Test void maskRequestHeadersWithDuplicateHeaderName() { - final Function maskingFunction = (header) -> "****armeria****"; + final HeaderMaskingFunction maskingFunction = (name, value) -> "****armeria****"; final LogFormatter logFormatter = LogFormatter.builderForJson() .requestHeadersSanitizer( HeadersSanitizer.builderForJson() @@ -180,6 +180,34 @@ void maskRequestHeadersWithDuplicateHeaderName() { final Matcher matcher1 = Pattern.compile("\"accept-encoding\":\"(.*?)\"").matcher(requestLog); assertThat(matcher1.find()).isTrue(); assertThat(matcher1.group(1)).isEqualTo( - "[" + maskingFunction.apply("gzip") + ", " + maskingFunction.apply("deflate") + "]"); + "[" + maskingFunction.mask(HttpHeaderNames.ACCEPT_ENCODING, "gzip") + ", " + + maskingFunction.mask(HttpHeaderNames.ACCEPT_ENCODING, "deflate") + "]"); + } + + @Test + void removeSensitiveHeaders() { + final LogFormatter logFormatter = + LogFormatter.builderForJson() + .responseHeadersSanitizer( + HeadersSanitizer.builderForJson() + .maskingHeaders("set-cookie", "multiple-header") + .maskingFunction((name, value) -> null) + .build()) + .build(); + final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/hello")); + final DefaultRequestLog log = (DefaultRequestLog) ctx.log(); + log.responseHeaders(ResponseHeaders.of(HttpStatus.OK, HttpHeaderNames.SET_COOKIE, "armeria=fun", + "multiple-header", "armeria1", "multiple-header", "armeria2", + HttpHeaderNames.CACHE_CONTROL, "no-cache")); + log.endResponse(); + + final String responseLog = logFormatter.formatResponse(log); + final Matcher matcher1 = Pattern.compile("\"set-cookie\":\"(.*?)\"").matcher(responseLog); + assertThat(matcher1.find()).isFalse(); + final Matcher matcher2 = Pattern.compile("\"multiple-header\":\"(.*?)\"").matcher(responseLog); + assertThat(matcher2.find()).isFalse(); + final Matcher matcher3 = Pattern.compile("\"cache-control\":\"(.*?)\"").matcher(responseLog); + assertThat(matcher3.find()).isTrue(); + assertThat(matcher3.group(1)).isEqualTo("no-cache"); } } diff --git a/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java b/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java index 215f3697d19..43e9b78ad23 100644 --- a/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java +++ b/core/src/test/java/com/linecorp/armeria/common/logging/TextLogFormatterTest.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,7 +25,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import com.linecorp.armeria.common.HeadersSanitizer; +import com.linecorp.armeria.common.HttpHeaderNames; import com.linecorp.armeria.common.HttpMethod; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpStatus; @@ -114,7 +113,7 @@ void defaultMaskingHeadersShouldBeOverridable() { @Test void maskRequestHeaders() { - final Function maskingFunction = (header) -> "****armeria****"; + final HeaderMaskingFunction maskingFunction = (name, value) -> "****armeria****"; final LogFormatter logFormatter = LogFormatter.builderForText() .requestHeadersSanitizer( HeadersSanitizer.builderForText() @@ -135,11 +134,13 @@ void maskRequestHeaders() { System.out.println(requestLog); final Matcher matcher1 = Pattern.compile("cookie=(.*?)[,\\]]").matcher(requestLog); assertThat(matcher1.find()).isTrue(); - assertThat(matcher1.group(1)).isEqualTo(maskingFunction.apply("Armeria=awesome")); + assertThat(matcher1.group(1)).isEqualTo( + maskingFunction.mask(HttpHeaderNames.COOKIE, "Armeria=awesome")); final Matcher matcher2 = Pattern.compile("authorization=(.*?)[,\\]]").matcher(requestLog); assertThat(matcher2.find()).isTrue(); - assertThat(matcher2.group(1)).isEqualTo(maskingFunction.apply("Basic XXX==")); + assertThat(matcher2.group(1)).isEqualTo( + maskingFunction.mask(HttpHeaderNames.AUTHORIZATION, "Basic XXX==")); final Matcher matcher3 = Pattern.compile("cache-control=(.*?)[,\\]]").matcher(requestLog); assertThat(matcher3.find()).isTrue(); @@ -148,7 +149,7 @@ void maskRequestHeaders() { @Test void maskResponseHeaders() { - final Function maskingFunction = (header) -> "****armeria****"; + final HeaderMaskingFunction maskingFunction = (name, value) -> "****armeria****"; final LogFormatter logFormatter = LogFormatter.builderForText() .responseHeadersSanitizer( HeadersSanitizer.builderForText() @@ -167,11 +168,13 @@ void maskResponseHeaders() { final String responseLog = logFormatter.formatResponse(log); final Matcher matcher1 = Pattern.compile("content-type=(.*?)[,\\]]").matcher(responseLog); assertThat(matcher1.find()).isTrue(); - assertThat(matcher1.group(1)).isEqualTo(maskingFunction.apply("text/html")); + assertThat(matcher1.group(1)).isEqualTo( + maskingFunction.mask(HttpHeaderNames.CONTENT_TYPE, "text/html")); final Matcher matcher2 = Pattern.compile("set-cookie=(.*?)[,\\]]").matcher(responseLog); assertThat(matcher2.find()).isTrue(); - assertThat(matcher2.group(1)).isEqualTo(maskingFunction.apply("Armeria=awesome")); + assertThat(matcher2.group(1)).isEqualTo( + maskingFunction.mask(HttpHeaderNames.SET_COOKIE, "Armeria=awesome")); final Matcher matcher3 = Pattern.compile("cache-control=(.*?)[,\\]]").matcher(responseLog); assertThat(matcher3.find()).isTrue(); @@ -180,7 +183,7 @@ void maskResponseHeaders() { @Test void maskRequestHeadersWithDuplicateHeaderName() { - final Function maskingFunction = (header) -> "****armeria****"; + final HeaderMaskingFunction maskingFunction = (name, value) -> "****armeria****"; final LogFormatter logFormatter = LogFormatter.builderForText() .requestHeadersSanitizer( HeadersSanitizer.builderForText() @@ -201,6 +204,34 @@ void maskRequestHeadersWithDuplicateHeaderName() { final Matcher matcher1 = Pattern.compile("accept-encoding=\\[(.*?)]").matcher(requestLog); assertThat(matcher1.find()).isTrue(); assertThat(matcher1.group(1)).isEqualTo( - maskingFunction.apply("gzip") + ", " + maskingFunction.apply("deflate")); + maskingFunction.mask(HttpHeaderNames.ACCEPT_ENCODING, "gzip") + ", " + + maskingFunction.mask(HttpHeaderNames.ACCEPT_ENCODING, "deflate")); + } + + @Test + void removeSensitiveHeaders() { + final LogFormatter logFormatter = + LogFormatter.builderForText() + .responseHeadersSanitizer( + HeadersSanitizer.builderForText() + .maskingHeaders("set-cookie", "multiple-header") + .maskingFunction((name, value) -> null) + .build()) + .build(); + final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/hello")); + final DefaultRequestLog log = (DefaultRequestLog) ctx.log(); + log.responseHeaders(ResponseHeaders.of(HttpStatus.OK, HttpHeaderNames.SET_COOKIE, "armeria=fun", + "multiple-header", "armeria1", "multiple-header", "armeria2", + HttpHeaderNames.CACHE_CONTROL, "no-cache")); + log.endResponse(); + + final String responseLog = logFormatter.formatResponse(log); + final Matcher matcher1 = Pattern.compile("\"set-cookie\"=\"(.*?)\"").matcher(responseLog); + assertThat(matcher1.find()).isFalse(); + final Matcher matcher2 = Pattern.compile("\"multiple-header\"=\"(.*?)\"").matcher(responseLog); + assertThat(matcher2.find()).isFalse(); + final Matcher matcher3 = Pattern.compile("\"cache-control\"=\"(.*?)\"").matcher(responseLog); + assertThat(matcher3.find()).isTrue(); + assertThat(matcher3.group(1)).isEqualTo("no-cache"); } }