From f65fc532598d43004019df1a67924dfdc06c1415 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Fri, 10 Jan 2020 09:07:42 +0000 Subject: [PATCH 01/11] Initial fix --- .../format/DefaultHttpMessageFormatter.java | 9 ++ .../common/format/HttpMessageFormatter.java | 3 + .../format/SanitisedHttpMessageFormatter.java | 34 +++++ .../hotels/styx/ProxyConnectorFactory.java | 14 +- .../main/java/com/hotels/styx/StyxServer.java | 3 +- .../codec/NettyToStyxRequestDecoder.java | 14 +- .../codec/NettyToStyxRequestDecoderTest.java | 34 ++++- .../support/matchers/LoggingTestSupport.java | 4 +- .../styx/logging/HttpMessageLoggingSpec.kt | 135 +++++++++++++++++- 9 files changed, 236 insertions(+), 14 deletions(-) diff --git a/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java index 0ba282a535..9fe9274e66 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java @@ -19,26 +19,35 @@ import com.hotels.styx.api.HttpResponse; import com.hotels.styx.api.LiveHttpRequest; import com.hotels.styx.api.LiveHttpResponse; +import io.netty.handler.codec.http.HttpObject; /** * Provides default formatting for requests and responses. */ public class DefaultHttpMessageFormatter implements HttpMessageFormatter { + @Override public String formatRequest(HttpRequest request) { return request == null ? null : request.toString(); } + @Override public String formatRequest(LiveHttpRequest request) { return request == null ? null : request.toString(); } + @Override public String formatResponse(HttpResponse response) { return response == null ? null : response.toString(); } + @Override public String formatResponse(LiveHttpResponse response) { return response == null ? null : response.toString(); } + @Override + public String formatNettyMessage(HttpObject message) { + return message == null ? null : message.toString(); + } } diff --git a/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java index 3afeec6ad3..f468a9e4e7 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java @@ -19,6 +19,7 @@ import com.hotels.styx.api.HttpResponse; import com.hotels.styx.api.LiveHttpRequest; import com.hotels.styx.api.LiveHttpResponse; +import io.netty.handler.codec.http.HttpObject; /** * A common interface for formatting requests and responses. @@ -33,4 +34,6 @@ public interface HttpMessageFormatter { String formatResponse(LiveHttpResponse response); + String formatNettyMessage(HttpObject message); + } diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java index eb37e45fa4..2cd7c8a1d7 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java @@ -24,6 +24,8 @@ import com.hotels.styx.api.LiveHttpRequest; import com.hotels.styx.api.LiveHttpResponse; import com.hotels.styx.api.Url; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.LastHttpContent; import static java.util.Objects.requireNonNull; @@ -79,6 +81,31 @@ public String formatResponse(LiveHttpResponse response) { response.headers()); } + @Override + public String formatNettyMessage(HttpObject message) { + if (message == null) { + return NULL; + } else if (message instanceof io.netty.handler.codec.http.HttpRequest) { + return formatNettyRequest((io.netty.handler.codec.http.HttpRequest) message); + } else if (message instanceof LastHttpContent) { + return formatNettyContent((LastHttpContent) message); + } else { + return message.toString(); + } + } + + private String formatNettyRequest(io.netty.handler.codec.http.HttpRequest request) { + return "{version=" + request.protocolVersion() + + ", method=" + request.method() + + ", uri=" + request.uri() + + ", headers=[" + sanitisedHttpHeaderFormatter.format(convertToStyxHeaders(request.headers())) + "]}"; + } + + private String formatNettyContent(LastHttpContent content) { + return "{data=" + content.content() + + ", trailingHeaders=[" + sanitisedHttpHeaderFormatter.format(convertToStyxHeaders(content.trailingHeaders())) + "]}"; + } + private String formatRequest(HttpVersion version, HttpMethod method, Url url, Object id, HttpHeaders headers) { return "{version=" + version + ", method=" + method @@ -93,4 +120,11 @@ private String formatResponse(HttpVersion version, HttpResponseStatus status, Ht + ", headers=[" + sanitisedHttpHeaderFormatter.format(headers) + "]}"; } + private HttpHeaders convertToStyxHeaders(io.netty.handler.codec.http.HttpHeaders nettyHeaders) { + HttpHeaders.Builder styxHeaders = new HttpHeaders.Builder(); + nettyHeaders.names().forEach(name -> { + styxHeaders.add(name, nettyHeaders.getAll(name)); + }); + return styxHeaders.build(); + } } diff --git a/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java b/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java index f37efa9414..b886028e07 100644 --- a/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java +++ b/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java @@ -18,6 +18,7 @@ import com.codahale.metrics.Histogram; import com.hotels.styx.api.HttpHandler; import com.hotels.styx.api.MetricRegistry; +import com.hotels.styx.common.format.HttpMessageFormatter; import com.hotels.styx.proxy.HttpCompressor; import com.hotels.styx.proxy.ServerProtocolDistributionRecorder; import com.hotels.styx.proxy.encoders.ConfigurableUnwiseCharsEncoder; @@ -69,24 +70,27 @@ class ProxyConnectorFactory implements ServerConnectorFactory { private final String unwiseCharacters; private final ResponseEnhancer responseEnhancer; private final boolean requestTracking; + private final HttpMessageFormatter httpMessageFormatter; ProxyConnectorFactory(NettyServerConfig serverConfig, MetricRegistry metrics, HttpErrorStatusListener errorStatusListener, String unwiseCharacters, ResponseEnhancer responseEnhancer, - boolean requestTracking) { + boolean requestTracking, + HttpMessageFormatter httpMessageFormatter) { this.serverConfig = requireNonNull(serverConfig); this.metrics = requireNonNull(metrics); this.errorStatusListener = requireNonNull(errorStatusListener); this.unwiseCharacters = requireNonNull(unwiseCharacters); this.responseEnhancer = requireNonNull(responseEnhancer); this.requestTracking = requestTracking; + this.httpMessageFormatter = httpMessageFormatter; } @Override public ServerConnector create(ConnectorConfig config) { - return new ProxyConnector(config, serverConfig, metrics, errorStatusListener, unwiseCharacters, responseEnhancer, requestTracking); + return new ProxyConnector(config, serverConfig, metrics, errorStatusListener, unwiseCharacters, responseEnhancer, requestTracking, httpMessageFormatter); } private static final class ProxyConnector implements ServerConnector { @@ -101,6 +105,7 @@ private static final class ProxyConnector implements ServerConnector { private final Optional sslContext; private final ResponseEnhancer responseEnhancer; private final RequestTracker requestTracker; + private final HttpMessageFormatter httpMessageFormatter; private ProxyConnector(ConnectorConfig config, NettyServerConfig serverConfig, @@ -108,7 +113,8 @@ private ProxyConnector(ConnectorConfig config, HttpErrorStatusListener errorStatusListener, String unwiseCharacters, ResponseEnhancer responseEnhancer, - boolean requestTracking) { + boolean requestTracking, + HttpMessageFormatter httpMessageFormatter) { this.responseEnhancer = requireNonNull(responseEnhancer); this.config = requireNonNull(config); this.serverConfig = requireNonNull(serverConfig); @@ -124,6 +130,7 @@ private ProxyConnector(ConnectorConfig config, this.sslContext = Optional.empty(); } this.requestTracker = requestTracking ? CurrentRequestTracker.INSTANCE : RequestTracker.NO_OP; + this.httpMessageFormatter = httpMessageFormatter; } @Override @@ -181,6 +188,7 @@ private NettyToStyxRequestDecoder requestTranslator() { return new NettyToStyxRequestDecoder.Builder() .flowControlEnabled(true) .unwiseCharEncoder(unwiseCharEncoder) + .httpMessageFormatter(httpMessageFormatter) .build(); } diff --git a/components/proxy/src/main/java/com/hotels/styx/StyxServer.java b/components/proxy/src/main/java/com/hotels/styx/StyxServer.java index 807961310e..9c98b0ba26 100644 --- a/components/proxy/src/main/java/com/hotels/styx/StyxServer.java +++ b/components/proxy/src/main/java/com/hotels/styx/StyxServer.java @@ -245,7 +245,8 @@ private static HttpServer httpServer(Environment environment, ConnectorConfig co environment.errorListener(), environment.configuration().get(ENCODE_UNWISECHARS).orElse(""), (builder, request) -> builder.header(styxInfoHeaderName, responseInfoFormat.format(request)), - environment.configuration().get("requestTracking", Boolean.class).orElse(false)) + environment.configuration().get("requestTracking", Boolean.class).orElse(false), + environment.httpMessageFormatter()) .create(connectorConfig); return NettyServerBuilder.newBuilder() diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java index 1ab86b1f3c..35d7087853 100644 --- a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java +++ b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java @@ -21,6 +21,8 @@ import com.hotels.styx.api.HttpVersion; import com.hotels.styx.api.LiveHttpRequest; import com.hotels.styx.api.Url; +import com.hotels.styx.common.format.DefaultHttpMessageFormatter; +import com.hotels.styx.common.format.HttpMessageFormatter; import com.hotels.styx.server.BadRequestException; import com.hotels.styx.server.UniqueIdSupplier; import io.netty.buffer.ByteBuf; @@ -64,18 +66,22 @@ public final class NettyToStyxRequestDecoder extends MessageToMessageDecoder out) throws Exception { if (httpObject.getDecoderResult().isFailure()) { - throw new BadRequestException("Error while decoding request: " + httpObject, httpObject.getDecoderResult().cause()); + String formattedHttpObject = httpMessageFormatter.formatNettyMessage(httpObject); + throw new BadRequestException("Error while decoding request: " + formattedHttpObject, httpObject.getDecoderResult().cause()); } try { @@ -251,6 +257,7 @@ public static final class Builder { private boolean flowControlEnabled; private UniqueIdSupplier uniqueIdSupplier = UUID_VERSION_ONE_SUPPLIER; private UnwiseCharsEncoder unwiseCharEncoder = IGNORE; + private HttpMessageFormatter httpMessageFormatter = new DefaultHttpMessageFormatter(); public Builder uniqueIdSupplier(UniqueIdSupplier uniqueIdSupplier) { this.uniqueIdSupplier = requireNonNull(uniqueIdSupplier); @@ -267,6 +274,11 @@ public Builder unwiseCharEncoder(UnwiseCharsEncoder unwiseCharEncoder) { return this; } + public Builder httpMessageFormatter(HttpMessageFormatter httpMessageFormatter) { + this.httpMessageFormatter = httpMessageFormatter; + return this; + } + public NettyToStyxRequestDecoder build() { return new NettyToStyxRequestDecoder(this); } diff --git a/components/server/src/test/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoderTest.java b/components/server/src/test/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoderTest.java index ac030d28f9..da8b3e2eff 100644 --- a/components/server/src/test/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoderTest.java +++ b/components/server/src/test/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoderTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,6 +21,9 @@ import com.hotels.styx.api.HttpHeader; import com.hotels.styx.api.HttpMethod; import com.hotels.styx.api.LiveHttpRequest; +import com.hotels.styx.common.format.HttpMessageFormatter; +import com.hotels.styx.common.format.SanitisedHttpHeaderFormatter; +import com.hotels.styx.common.format.SanitisedHttpMessageFormatter; import com.hotels.styx.server.BadRequestException; import com.hotels.styx.server.UniqueIdSupplier; import io.netty.buffer.ByteBuf; @@ -28,6 +31,7 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.DecoderException; +import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpRequest; @@ -46,6 +50,7 @@ import rx.observers.TestSubscriber; import java.net.URI; +import java.util.Arrays; import java.util.concurrent.CountDownLatch; import static com.google.common.base.Charsets.US_ASCII; @@ -68,7 +73,9 @@ import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static io.netty.handler.codec.http.LastHttpContent.EMPTY_LAST_CONTENT; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.matchesPattern; +import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -110,6 +117,27 @@ public void rejectsRequestsWithBadURL() throws Throwable { assertEquals(BadRequestException.class, e.getCause().getClass()); } + @Test + public void sanitisesHeadersInBadRequestException() throws Throwable { + FullHttpRequest originalRequest = newHttpRequest("/uri"); + HttpHeaders originalRequestHeaders = originalRequest.headers(); + originalRequestHeaders.add("Foo", "foo"); + originalRequestHeaders.add("secret-header", "secret"); + originalRequestHeaders.add("Cookie", "Bar=bar;secret-cookie=secret"); + originalRequest.setDecoderResult(DecoderResult.failure(new Exception("It just didn't work"))); + + Exception e = assertThrows(DecoderException.class, () -> decode(originalRequest)); + + assertEquals(BadRequestException.class, e.getCause().getClass()); + String breMsg = e.getCause().getMessage(); + assertThat(breMsg, containsString("Foo=foo")); + assertThat(breMsg, containsString("secret-header=****")); + assertThat(breMsg, containsString("Bar=bar")); + assertThat(breMsg, containsString("secret-cookie=****")); + assertThat(breMsg, not(containsString("secret-header=secret"))); + assertThat(breMsg, not(containsString("secret-cookie=secret"))); + } + @Test public void decodesNettyInternalRequestToStyxRequest() throws Exception { FullHttpRequest originalRequest = newHttpRequest("/uri"); @@ -391,8 +419,12 @@ private static FullHttpRequest newHttpRequest(String uri) { private LiveHttpRequest decode(HttpRequest request) { HttpRequestRecorder requestRecorder = new HttpRequestRecorder(); + HttpMessageFormatter formatter = new SanitisedHttpMessageFormatter(new SanitisedHttpHeaderFormatter( + Arrays.asList("secret-header"), Arrays.asList("secret-cookie") + )); EmbeddedChannel channel = new EmbeddedChannel(new NettyToStyxRequestDecoder.Builder() .uniqueIdSupplier(uniqueIdSupplier) + .httpMessageFormatter(formatter) .build(), requestRecorder); channel.writeInbound(request); return requestRecorder.styxRequest; diff --git a/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java b/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java index 2f76bd8403..70fcf0bae9 100644 --- a/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java +++ b/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java @@ -28,8 +28,8 @@ import static org.slf4j.LoggerFactory.getLogger; public class LoggingTestSupport { - private final Logger logger; - private final ListAppender appender; + public final Logger logger; + public final ListAppender appender; public LoggingTestSupport(Class classUnderTest) { this(logger(classUnderTest)); diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt index 2c6128c49e..3f00d36ed6 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt @@ -15,17 +15,21 @@ */ package com.hotels.styx.logging -import ch.qos.logback.classic.Level.INFO +import ch.qos.logback.classic.Level.* import com.hotels.styx.api.HttpHeaderNames.HOST import com.hotels.styx.api.HttpRequest +import com.hotels.styx.api.HttpResponseStatus.BAD_REQUEST +import com.hotels.styx.client.BadHttpResponseException import com.hotels.styx.client.StyxHttpClient -import com.hotels.styx.support.StyxServerProvider +import com.hotels.styx.proxy.HttpErrorStatusCauseLogger +import com.hotels.styx.support.* import com.hotels.styx.support.matchers.LoggingTestSupport -import com.hotels.styx.support.proxyHttpHostHeader -import com.hotels.styx.support.shouldContain -import com.hotels.styx.support.wait import io.kotlintest.Spec +import io.kotlintest.matchers.string.shouldContain +import io.kotlintest.matchers.string.shouldNotContain +import io.kotlintest.shouldBe import io.kotlintest.specs.FeatureSpec +import kotlin.test.assertFailsWith class HttpMessageLoggingSpec : FeatureSpec() { @@ -52,10 +56,105 @@ class HttpMessageLoggingSpec : FeatureSpec() { logger.log().shouldContain(INFO, expectedRequest) logger.log().shouldContain(INFO, expectedResponse) } + + scenario("Requests with badly-formed headers should hide sensitive cookies and headers when logged") { + + httpErrorLogger.logger.level = ERROR + httpErrorLogger.appender.list.clear() + + val response = client.send(HttpRequest.get("/a/path") + .header(HOST, styxServer().proxyHttpHostHeader()) + .header("header1", "h1") + .header("header2", "h2") + .header("cookie", "cookie1=c1;cookie2=c2") + .header("badheader", "with\u0000nullchar") + .build()) + .wait()!! + + response.status() shouldBe BAD_REQUEST + + val event = httpErrorLogger.log().first() + event.level shouldBe ERROR + + var t = event.throwableProxy + while (t != null) { + anySensitiveHeadersAreHidden(t.message) + t = t.cause + } + } + + scenario("Requests with badly-formed cookies should hide sensitive cookies and headers when logged") { + + httpErrorLogger.logger.level = ERROR + httpErrorLogger.appender.list.clear() + + val response = client.send(HttpRequest.get("/a/path") + .header(HOST, styxServer().proxyHttpHostHeader()) + .header("header1", "h1") + .header("header2", "h2") + .header("cookie", "cookie1=c1;cookie2=c2;badcookie=bad\u0000bad") + .build()) + .wait()!! + + response.status() shouldBe BAD_REQUEST + + val event = httpErrorLogger.log().first() + event.level shouldBe ERROR + + var t = event.throwableProxy + while (t != null) { + anySensitiveHeadersAreHidden(t.message) + t = t.cause + } + } + + scenario("Responses with badly-formed headers should hide sensitive cookies and headers when logged") { + + rootLogger.appender.list.clear() + rootLogger.logger.level = DEBUG + + var exception: Throwable? = assertFailsWith { + client.send(HttpRequest.get("/bad/path") + .header(HOST, styxServer().proxyHttpHostHeader()) + .header("header1", "h1") + .header("header2", "h2") + .header("cookie", "cookie1=c1;cookie2=c2") + .build()) + .wait() + } + + while (exception != null) { + anySensitiveHeadersAreHidden(exception.message ?: "") + exception = exception.cause + } + + rootLogger.log().forEach { + anySensitiveHeadersAreHidden(it.message) + var t = it.throwableProxy + while (t != null) { + anySensitiveHeadersAreHidden(t.message) + t = t.cause + } + } + } } } - val logger = LoggingTestSupport("com.hotels.styx.http-messages.inbound") + private fun anySensitiveHeadersAreHidden(msg: String) { + msg shouldNotContain "header1=h1" + msg shouldNotContain "cookie1=c1" + if (msg.contains("header2=h2")) { + msg shouldContain "header1=****" + } + if (msg.contains("cookie2=c2")) { + msg shouldContain "cookie1=****" + } + } + + private val logger = LoggingTestSupport("com.hotels.styx.http-messages.inbound") + private val httpErrorLogger = LoggingTestSupport(HttpErrorStatusCauseLogger::class.java) + private val rootLogger = LoggingTestSupport("ROOT") + val client: StyxHttpClient = StyxHttpClient.Builder().build() @@ -87,6 +186,28 @@ class HttpMessageLoggingSpec : FeatureSpec() { routingObjects: root: + type: PathPrefixRouter + config: + routes: + - prefix: / + destination: default + - prefix: /bad + destination: bad + + default: + type: StaticResponseHandler + config: + status: 200 + content: "" + headers: + - name: "header1" + value: "h1" + - name: "header2" + value: "h2" + - name: "cookie" + value: "cookie1=c1;cookie2=c2" + + bad: type: StaticResponseHandler config: status: 200 @@ -98,6 +219,8 @@ class HttpMessageLoggingSpec : FeatureSpec() { value: "h2" - name: "cookie" value: "cookie1=c1;cookie2=c2" + - name: "badheader" + value: "bad\u0000bad" httpPipeline: root """.trimIndent()) From 16d39b9805546e2e02925018bc95314c1ff71c96 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Mon, 13 Jan 2020 16:15:56 +0000 Subject: [PATCH 02/11] Sanitising proxy for cookies in throwables. --- .../format/DefaultHttpMessageFormatter.java | 5 + .../common/format/HttpMessageFormatter.java | 2 + .../format/SanitisedHttpHeaderFormatter.java | 18 ++- .../format/SanitisedHttpMessageFormatter.java | 5 + .../format/SanitisingThrowableProxy.java | 114 ++++++++++++++++++ .../format/SanitisingThrowableProxyTest.java | 91 ++++++++++++++ .../codec/NettyToStyxRequestDecoder.java | 3 +- 7 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java create mode 100644 components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java diff --git a/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java index 9fe9274e66..31a11d069e 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java @@ -50,4 +50,9 @@ public String formatResponse(LiveHttpResponse response) { public String formatNettyMessage(HttpObject message) { return message == null ? null : message.toString(); } + + @Override + public Throwable wrap(Throwable t) { + return t; + } } diff --git a/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java index f468a9e4e7..430064d7ae 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java @@ -36,4 +36,6 @@ public interface HttpMessageFormatter { String formatNettyMessage(HttpObject message); + Throwable wrap(Throwable t); + } diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java index d8b3799585..e246ddd9a5 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java @@ -20,6 +20,7 @@ import com.hotels.styx.api.RequestCookie; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -37,8 +38,12 @@ public class SanitisedHttpHeaderFormatter { private final List cookiesToHide; public SanitisedHttpHeaderFormatter(List headersToHide, List cookiesToHide) { - this.headersToHide = requireNonNull(headersToHide); - this.cookiesToHide = requireNonNull(cookiesToHide); + this.headersToHide = Collections.unmodifiableList(requireNonNull(headersToHide)); + this.cookiesToHide = Collections.unmodifiableList(requireNonNull(cookiesToHide)); + } + + public List cookiesToHide() { + return cookiesToHide; } public String format(HttpHeaders headers) { @@ -69,13 +74,16 @@ private boolean isHeaderACookie(HttpHeader header) { } private String formatCookieHeader(HttpHeader header) { - String cookies = RequestCookie.decode(header.value()).stream() + return header.name() + "=" + formatCookieHeaderValue(header.value()); + } + + String formatCookieHeaderValue(String value) { + return RequestCookie.decode(value).stream() .map(this::hideOrFormatCookie) .collect(Collectors.joining(";")); - - return header.name() + "=" + cookies; } + private String hideOrFormatCookie(RequestCookie cookie) { return shouldHideCookie(cookie) ? cookie.name() + "=****" diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java index 2cd7c8a1d7..a2925364be 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java @@ -94,6 +94,11 @@ public String formatNettyMessage(HttpObject message) { } } + @Override + public Throwable wrap(Throwable t) { + return t == null ? null : new SanitisingThrowableProxy(t, sanitisedHttpHeaderFormatter); + } + private String formatNettyRequest(io.netty.handler.codec.http.HttpRequest request) { return "{version=" + request.protocolVersion() + ", method=" + request.method() diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java new file mode 100644 index 0000000000..da65f22060 --- /dev/null +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java @@ -0,0 +1,114 @@ +package com.hotels.styx.common.format; + +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Wraps a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. + * Any cause is similarly wrapped before being returned. + */ +public class SanitisingThrowableProxy extends Throwable { + + private static final ConcurrentHashMap COOKIE_NAME_PATTERN_CACHE = new ConcurrentHashMap<>(); + private static Pattern cookieNamePattern(String cookieName) { + return COOKIE_NAME_PATTERN_CACHE.computeIfAbsent(cookieName, name -> Pattern.compile(name + "\\s*=")); + } + + private final Throwable throwable; + private final SanitisedHttpHeaderFormatter formatter; + + /** + * Wrap a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. + * @param throwable the throwable to wrap + * @param formatter provides the sanitising logic + */ + public SanitisingThrowableProxy(Throwable throwable, SanitisedHttpHeaderFormatter formatter) { + this.throwable = throwable; + this.formatter = formatter; + } + + private String sanitiseCookies(String message) { + if (message == null) { + return null; + } + return formatter.cookiesToHide().stream() + // Find earliest 'cookiename=' in message + .map(cookie -> cookieNamePattern(cookie).matcher(message)) + .filter(Matcher::find) + .map(Matcher::start) + .min(Integer::compareTo) + .map(cookiesStart -> { + // Assume the cookies run to the end of the message + String cookies = message.substring(cookiesStart); + String sanitizedCookies = formatter.formatCookieHeaderValue(cookies); + return message.substring(0, cookiesStart) + sanitizedCookies; + }) + .orElse(message); + } + + /** + * Returns the class of the wrapped Throwable. + * @return the class of the wrapped Throwable. + */ + public Class delegateClass() { + return throwable.getClass(); + } + + @Override + public String getMessage() { + return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getMessage()); + } + + @Override + public String getLocalizedMessage() { + return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getLocalizedMessage()); + } + + @Override + public synchronized Throwable getCause() { + Throwable cause = throwable.getCause(); + return cause == null ? null : new SanitisingThrowableProxy(cause, formatter); + } + + @Override + public synchronized Throwable initCause(Throwable cause) { + return throwable.initCause(cause); + } + + @Override + public String toString() { + return "Sanitized: " + throwable.toString(); + } + + @Override + public void printStackTrace() { + printStackTrace(System.err); + } + + @Override + public void printStackTrace(PrintStream s) { + throwable.printStackTrace(s); + } + + @Override + public void printStackTrace(PrintWriter s) { + throwable.printStackTrace(s); + } + + @Override + public synchronized Throwable fillInStackTrace() { + if (throwable != null) { + throwable.fillInStackTrace(); + } + return this; + } + + @Override + public StackTraceElement[] getStackTrace() { + return throwable.getStackTrace(); + } +} + diff --git a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java new file mode 100644 index 0000000000..4113a7f181 --- /dev/null +++ b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java @@ -0,0 +1,91 @@ +package com.hotels.styx.common.format; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.StringEndsWith.endsWith; + +class SanitisingThrowableProxyTest { + + SanitisedHttpHeaderFormatter formatter = new SanitisedHttpHeaderFormatter( + emptyList(), asList("secret-cookie", "private-cookie") + ); + + Exception exception(String msg) { + try { + throw new Exception(msg); + } catch (Exception e) { + return e; + } + } + + Exception exception(String msg, Exception cause) { + try { + throw new Exception(msg, cause); + } catch (Exception e) { + return e; + } + } + + @Test + public void messagesWithNoCookiesAreNotChanged() { + Exception e = exception("This does not contain any cookies."); + SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + + String prefix = "java.lang.Exception: "; + assertThat(proxy.getMessage(), equalTo(prefix + e.getMessage())); + assertThat(proxy.getLocalizedMessage(), equalTo(prefix + e.getLocalizedMessage())); + assertThat(proxy.toString(), equalTo("Sanitized: " + e.toString())); + } + + @Test + public void messagesWithUnrecognizedCookiesAreNotChanged() { + Exception e = exception("Some cookies: cookie1=c1;cookie2=c2"); + SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + assertThat(proxy.getMessage(), endsWith(e.getMessage())); + assertThat(proxy.getLocalizedMessage(), endsWith(e.getLocalizedMessage())); + assertThat(proxy.toString(), endsWith(e.toString())); + } + + @Test + public void messagesWithRecognizedCookiesAreSanitized() { + Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;cookie2=c2;private-cookie=private"); + SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + assertThat(proxy.getMessage(), containsString("cookie1=c1")); + assertThat(proxy.getMessage(), containsString("cookie2=c2")); + assertThat(proxy.getMessage(), containsString("secret-cookie=****")); + assertThat(proxy.getMessage(), containsString("private-cookie=****")); + } + + @Test + public void exceptionCausesAreSanitized() { + Exception inner = exception("Inner: cookie1=c1;secret-cookie=secret"); + Exception outer = exception("Outer: cookie2=c2;private-cookie=private", inner); + SanitisingThrowableProxy outerProxy = new SanitisingThrowableProxy(outer, formatter); + assertThat(outerProxy.getMessage(), containsString("cookie2=c2")); + assertThat(outerProxy.getMessage(), containsString("private-cookie=****")); + assertThat(outerProxy.getCause().getMessage(), containsString("cookie1=c1")); + assertThat(outerProxy.getCause().getMessage(), containsString("secret-cookie=****")); + } + + @Test + public void nullMessagesAreAllowed() { + Exception e = exception(null); + SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + assertThat(proxy.getMessage(), equalTo("java.lang.Exception: null")); + } + + @Test + public void messagesWithInvalidCookiesAreSanitized() { + Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;bad-cookie=bad\u0000bad;private-cookie=private"); + SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + assertThat(proxy.getMessage(), containsString("cookie1=c1")); + assertThat(proxy.getMessage(), containsString("bad-cookie=bad\u0000bad")); + assertThat(proxy.getMessage(), containsString("secret-cookie=****")); + assertThat(proxy.getMessage(), containsString("private-cookie=****")); + } +} \ No newline at end of file diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java index 35d7087853..88b27c378a 100644 --- a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java +++ b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java @@ -81,7 +81,8 @@ private NettyToStyxRequestDecoder(Builder builder) { protected void decode(ChannelHandlerContext ctx, HttpObject httpObject, List out) throws Exception { if (httpObject.getDecoderResult().isFailure()) { String formattedHttpObject = httpMessageFormatter.formatNettyMessage(httpObject); - throw new BadRequestException("Error while decoding request: " + formattedHttpObject, httpObject.getDecoderResult().cause()); + throw new BadRequestException("Error while decoding request: " + formattedHttpObject, + httpMessageFormatter.wrap(httpObject.getDecoderResult().cause())); } try { From be3de1bbbbf979fe56b33d5ef214b420703a52c5 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Tue, 14 Jan 2020 11:59:32 +0000 Subject: [PATCH 03/11] Use a CGLIB proxy --- components/common/pom.xml | 7 ++ .../format/SanitisedHttpMessageFormatter.java | 5 +- ...SanitisingThrowableMethodInterceptor.java} | 86 +++++++------------ ...tisingThrowableMethodInterceptorTest.java} | 15 ++-- .../codec/NettyToStyxRequestDecoder.java | 2 + 5 files changed, 52 insertions(+), 63 deletions(-) rename components/common/src/main/java/com/hotels/styx/common/format/{SanitisingThrowableProxy.java => SanitisingThrowableMethodInterceptor.java} (57%) rename components/common/src/test/java/com/hotels/styx/common/format/{SanitisingThrowableProxyTest.java => SanitisingThrowableMethodInterceptorTest.java} (80%) diff --git a/components/common/pom.xml b/components/common/pom.xml index 3ac4314bb3..f153448a14 100644 --- a/components/common/pom.xml +++ b/components/common/pom.xml @@ -15,6 +15,13 @@ + + + cglib + cglib + 3.2.4 + + com.hotels.styx styx-api diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java index a2925364be..ad3740e002 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java @@ -26,6 +26,9 @@ import com.hotels.styx.api.Url; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.LastHttpContent; +import net.sf.cglib.proxy.Enhancer; + +import java.lang.reflect.Proxy; import static java.util.Objects.requireNonNull; @@ -96,7 +99,7 @@ public String formatNettyMessage(HttpObject message) { @Override public Throwable wrap(Throwable t) { - return t == null ? null : new SanitisingThrowableProxy(t, sanitisedHttpHeaderFormatter); + return t == null ? null : (Throwable) Enhancer.create(t.getClass(), new SanitisingThrowableMethodInterceptor(t, sanitisedHttpHeaderFormatter)); } private String formatNettyRequest(io.netty.handler.codec.http.HttpRequest request) { diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java similarity index 57% rename from components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java rename to components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java index da65f22060..d3a8aec8fb 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableProxy.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java @@ -1,7 +1,12 @@ package com.hotels.styx.common.format; -import java.io.PrintStream; -import java.io.PrintWriter; +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -10,7 +15,7 @@ * Wraps a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. * Any cause is similarly wrapped before being returned. */ -public class SanitisingThrowableProxy extends Throwable { +public class SanitisingThrowableMethodInterceptor implements MethodInterceptor { private static final ConcurrentHashMap COOKIE_NAME_PATTERN_CACHE = new ConcurrentHashMap<>(); private static Pattern cookieNamePattern(String cookieName) { @@ -21,15 +26,31 @@ private static Pattern cookieNamePattern(String cookieName) { private final SanitisedHttpHeaderFormatter formatter; /** - * Wrap a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. - * @param throwable the throwable to wrap + * Enhance a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. * @param formatter provides the sanitising logic */ - public SanitisingThrowableProxy(Throwable throwable, SanitisedHttpHeaderFormatter formatter) { + public SanitisingThrowableMethodInterceptor(Throwable throwable, SanitisedHttpHeaderFormatter formatter) { this.throwable = throwable; this.formatter = formatter; } + @Override + public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, + MethodProxy proxy) throws Throwable { + switch (method.getName()) { + case "getMessage": + return getMessage(); + case "getLocalizedMessage": + return getLocalizedMessage(); + case "toString": + return toString(); + case "getCause": + return getCause(); + default: + return proxy.invoke(throwable, args); + } + } + private String sanitiseCookies(String message) { if (message == null) { return null; @@ -49,66 +70,21 @@ private String sanitiseCookies(String message) { .orElse(message); } - /** - * Returns the class of the wrapped Throwable. - * @return the class of the wrapped Throwable. - */ - public Class delegateClass() { - return throwable.getClass(); - } - - @Override - public String getMessage() { + public String getMessage() throws Throwable { return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getMessage()); } - @Override - public String getLocalizedMessage() { + public String getLocalizedMessage() throws Throwable { return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getLocalizedMessage()); } - @Override - public synchronized Throwable getCause() { + public synchronized Throwable getCause() throws Throwable { Throwable cause = throwable.getCause(); - return cause == null ? null : new SanitisingThrowableProxy(cause, formatter); + return cause == null ? null : (Throwable) Enhancer.create(cause.getClass(), new SanitisingThrowableMethodInterceptor(cause, formatter)); } - @Override - public synchronized Throwable initCause(Throwable cause) { - return throwable.initCause(cause); - } - - @Override public String toString() { return "Sanitized: " + throwable.toString(); } - - @Override - public void printStackTrace() { - printStackTrace(System.err); - } - - @Override - public void printStackTrace(PrintStream s) { - throwable.printStackTrace(s); - } - - @Override - public void printStackTrace(PrintWriter s) { - throwable.printStackTrace(s); - } - - @Override - public synchronized Throwable fillInStackTrace() { - if (throwable != null) { - throwable.fillInStackTrace(); - } - return this; - } - - @Override - public StackTraceElement[] getStackTrace() { - return throwable.getStackTrace(); - } } diff --git a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java similarity index 80% rename from components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java rename to components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java index 4113a7f181..cc15fd8d9b 100644 --- a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableProxyTest.java +++ b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java @@ -1,5 +1,6 @@ package com.hotels.styx.common.format; +import net.sf.cglib.proxy.Enhancer; import org.junit.jupiter.api.Test; import static java.util.Arrays.asList; @@ -9,7 +10,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.StringEndsWith.endsWith; -class SanitisingThrowableProxyTest { +class SanitisingThrowableMethodInterceptorTest { SanitisedHttpHeaderFormatter formatter = new SanitisedHttpHeaderFormatter( emptyList(), asList("secret-cookie", "private-cookie") @@ -34,7 +35,7 @@ Exception exception(String msg, Exception cause) { @Test public void messagesWithNoCookiesAreNotChanged() { Exception e = exception("This does not contain any cookies."); - SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); String prefix = "java.lang.Exception: "; assertThat(proxy.getMessage(), equalTo(prefix + e.getMessage())); @@ -45,7 +46,7 @@ public void messagesWithNoCookiesAreNotChanged() { @Test public void messagesWithUnrecognizedCookiesAreNotChanged() { Exception e = exception("Some cookies: cookie1=c1;cookie2=c2"); - SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); assertThat(proxy.getMessage(), endsWith(e.getMessage())); assertThat(proxy.getLocalizedMessage(), endsWith(e.getLocalizedMessage())); assertThat(proxy.toString(), endsWith(e.toString())); @@ -54,7 +55,7 @@ public void messagesWithUnrecognizedCookiesAreNotChanged() { @Test public void messagesWithRecognizedCookiesAreSanitized() { Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;cookie2=c2;private-cookie=private"); - SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("cookie2=c2")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); @@ -65,7 +66,7 @@ public void messagesWithRecognizedCookiesAreSanitized() { public void exceptionCausesAreSanitized() { Exception inner = exception("Inner: cookie1=c1;secret-cookie=secret"); Exception outer = exception("Outer: cookie2=c2;private-cookie=private", inner); - SanitisingThrowableProxy outerProxy = new SanitisingThrowableProxy(outer, formatter); + Throwable outerProxy = (Throwable) Enhancer.create(outer.getClass(), new SanitisingThrowableMethodInterceptor(outer, formatter)); assertThat(outerProxy.getMessage(), containsString("cookie2=c2")); assertThat(outerProxy.getMessage(), containsString("private-cookie=****")); assertThat(outerProxy.getCause().getMessage(), containsString("cookie1=c1")); @@ -75,14 +76,14 @@ public void exceptionCausesAreSanitized() { @Test public void nullMessagesAreAllowed() { Exception e = exception(null); - SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); assertThat(proxy.getMessage(), equalTo("java.lang.Exception: null")); } @Test public void messagesWithInvalidCookiesAreSanitized() { Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;bad-cookie=bad\u0000bad;private-cookie=private"); - SanitisingThrowableProxy proxy = new SanitisingThrowableProxy(e, formatter); + Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("bad-cookie=bad\u0000bad")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java index 88b27c378a..22f1a0c07f 100644 --- a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java +++ b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java @@ -83,6 +83,8 @@ protected void decode(ChannelHandlerContext ctx, HttpObject httpObject, List Date: Tue, 14 Jan 2020 12:23:04 +0000 Subject: [PATCH 04/11] Update copyright dates --- .../java/com/hotels/styx/api/ResponseCookie.java | 2 +- .../format/DefaultHttpMessageFormatter.java | 2 +- .../styx/common/format/HttpMessageFormatter.java | 2 +- .../format/SanitisedHttpHeaderFormatter.java | 2 +- .../format/SanitisedHttpMessageFormatter.java | 2 +- .../SanitisingThrowableMethodInterceptor.java | 15 +++++++++++++++ .../SanitisingThrowableMethodInterceptorTest.java | 15 +++++++++++++++ .../com/hotels/styx/ProxyConnectorFactory.java | 2 +- .../java/com/hotels/styx/ResponseInfoFormat.java | 2 +- .../src/main/java/com/hotels/styx/StyxServer.java | 2 +- .../styx/admin/handlers/PluginListHandler.java | 2 +- .../styx/admin/handlers/UrlPatternRouter.java | 2 +- .../com/hotels/styx/ResponseInfoFormatTest.java | 2 +- .../admin/handlers/PluginListHandlerTest.java | 2 +- .../java/com/hotels/styx/proxy/StyxProxyTest.java | 2 +- .../java/com/hotels/styx/server/HttpServer.java | 2 +- .../com/hotels/styx/server/netty/NettyServer.java | 2 +- .../netty/codec/NettyToStyxRequestDecoder.java | 2 +- .../styx/support/matchers/LoggingTestSupport.java | 2 +- .../styx/http/StaticFileOnRealServerIT.java | 2 +- .../test/scala/com/hotels/styx/MockServer.scala | 2 +- .../hotels/styx/plugins/PluginToggleSpec.scala | 2 +- .../styx/support/configuration/StyxBackend.scala | 2 +- .../com/hotels/styx/servers/MockOriginServer.java | 2 +- .../FileBasedOriginsFileChangeMonitorSpec.kt | 2 +- .../kotlin/com/hotels/styx/admin/MetricsSpec.kt | 2 +- .../styx/admin/OriginsFileCompatibilitySpec.kt | 2 +- .../styx/admin/ProviderAdminInterfaceSpec.kt | 2 +- .../hotels/styx/logging/HttpMessageLoggingSpec.kt | 2 +- .../hotels/styx/resiliency/OriginResourcesSpec.kt | 2 +- .../hotels/styx/routing/PluginsPipelineSpec.kt | 2 +- 31 files changed, 59 insertions(+), 29 deletions(-) diff --git a/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java b/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java index 5e2b33d64e..6579a66518 100644 --- a/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java +++ b/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2019 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java index 31a11d069e..fe555ac82b 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/DefaultHttpMessageFormatter.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java index 430064d7ae..a62eadcd1e 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/HttpMessageFormatter.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java index e246ddd9a5..b655e0c0ea 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java index ad3740e002..149920a893 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java index d3a8aec8fb..1dc82162e4 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java @@ -1,3 +1,18 @@ +/* + Copyright (C) 2013-2020 Expedia Inc. + + Licensed 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 + + http://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.hotels.styx.common.format; import net.sf.cglib.proxy.Enhancer; diff --git a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java index cc15fd8d9b..aed572ba7e 100644 --- a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java +++ b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java @@ -1,3 +1,18 @@ +/* + Copyright (C) 2013-2020 Expedia Inc. + + Licensed 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 + + http://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.hotels.styx.common.format; import net.sf.cglib.proxy.Enhancer; diff --git a/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java b/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java index b886028e07..1189b6d849 100644 --- a/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java +++ b/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/main/java/com/hotels/styx/ResponseInfoFormat.java b/components/proxy/src/main/java/com/hotels/styx/ResponseInfoFormat.java index a7c986a64a..42e7649c21 100644 --- a/components/proxy/src/main/java/com/hotels/styx/ResponseInfoFormat.java +++ b/components/proxy/src/main/java/com/hotels/styx/ResponseInfoFormat.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/main/java/com/hotels/styx/StyxServer.java b/components/proxy/src/main/java/com/hotels/styx/StyxServer.java index 9c98b0ba26..6b562ac3b6 100644 --- a/components/proxy/src/main/java/com/hotels/styx/StyxServer.java +++ b/components/proxy/src/main/java/com/hotels/styx/StyxServer.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/PluginListHandler.java b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/PluginListHandler.java index 94b4ed8c08..b9d7622a8d 100644 --- a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/PluginListHandler.java +++ b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/PluginListHandler.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/UrlPatternRouter.java b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/UrlPatternRouter.java index cca057a2bc..9221f46a6e 100644 --- a/components/proxy/src/main/java/com/hotels/styx/admin/handlers/UrlPatternRouter.java +++ b/components/proxy/src/main/java/com/hotels/styx/admin/handlers/UrlPatternRouter.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/test/java/com/hotels/styx/ResponseInfoFormatTest.java b/components/proxy/src/test/java/com/hotels/styx/ResponseInfoFormatTest.java index 33fdd75709..decd24f8d3 100644 --- a/components/proxy/src/test/java/com/hotels/styx/ResponseInfoFormatTest.java +++ b/components/proxy/src/test/java/com/hotels/styx/ResponseInfoFormatTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/test/java/com/hotels/styx/admin/handlers/PluginListHandlerTest.java b/components/proxy/src/test/java/com/hotels/styx/admin/handlers/PluginListHandlerTest.java index 3e93a0206a..57165a5de8 100644 --- a/components/proxy/src/test/java/com/hotels/styx/admin/handlers/PluginListHandlerTest.java +++ b/components/proxy/src/test/java/com/hotels/styx/admin/handlers/PluginListHandlerTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/proxy/src/test/java/com/hotels/styx/proxy/StyxProxyTest.java b/components/proxy/src/test/java/com/hotels/styx/proxy/StyxProxyTest.java index efa23de66e..f2182cbdde 100644 --- a/components/proxy/src/test/java/com/hotels/styx/proxy/StyxProxyTest.java +++ b/components/proxy/src/test/java/com/hotels/styx/proxy/StyxProxyTest.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/server/src/main/java/com/hotels/styx/server/HttpServer.java b/components/server/src/main/java/com/hotels/styx/server/HttpServer.java index 2da964ec75..ad400406b1 100644 --- a/components/server/src/main/java/com/hotels/styx/server/HttpServer.java +++ b/components/server/src/main/java/com/hotels/styx/server/HttpServer.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/NettyServer.java b/components/server/src/main/java/com/hotels/styx/server/netty/NettyServer.java index ceab8cba02..320f05db86 100644 --- a/components/server/src/main/java/com/hotels/styx/server/netty/NettyServer.java +++ b/components/server/src/main/java/com/hotels/styx/server/netty/NettyServer.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java index 22f1a0c07f..bfae5f22eb 100644 --- a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java +++ b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java b/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java index 70fcf0bae9..081f29de93 100644 --- a/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java +++ b/support/testsupport/src/main/java/com/hotels/styx/support/matchers/LoggingTestSupport.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2018 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/e2e-suite/src/test/java/com/hotels/styx/http/StaticFileOnRealServerIT.java b/system-tests/e2e-suite/src/test/java/com/hotels/styx/http/StaticFileOnRealServerIT.java index 2df92cb8d9..2e617f371b 100644 --- a/system-tests/e2e-suite/src/test/java/com/hotels/styx/http/StaticFileOnRealServerIT.java +++ b/system-tests/e2e-suite/src/test/java/com/hotels/styx/http/StaticFileOnRealServerIT.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/MockServer.scala b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/MockServer.scala index 8740191bbb..ce1ceef8d5 100644 --- a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/MockServer.scala +++ b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/MockServer.scala @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/plugins/PluginToggleSpec.scala b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/plugins/PluginToggleSpec.scala index c99e90ddc6..be8b092173 100644 --- a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/plugins/PluginToggleSpec.scala +++ b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/plugins/PluginToggleSpec.scala @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/support/configuration/StyxBackend.scala b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/support/configuration/StyxBackend.scala index fea25ab3b3..b6097efd80 100644 --- a/system-tests/e2e-suite/src/test/scala/com/hotels/styx/support/configuration/StyxBackend.scala +++ b/system-tests/e2e-suite/src/test/scala/com/hotels/styx/support/configuration/StyxBackend.scala @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/e2e-testsupport/src/main/java/com/hotels/styx/servers/MockOriginServer.java b/system-tests/e2e-testsupport/src/main/java/com/hotels/styx/servers/MockOriginServer.java index 7800cfa18f..bf2625714c 100644 --- a/system-tests/e2e-testsupport/src/main/java/com/hotels/styx/servers/MockOriginServer.java +++ b/system-tests/e2e-testsupport/src/main/java/com/hotels/styx/servers/MockOriginServer.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt index 815be4d6a4..e1559c30c1 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/FileBasedOriginsFileChangeMonitorSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt index 19ba3bef80..75743fa7d8 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/MetricsSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/OriginsFileCompatibilitySpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/OriginsFileCompatibilitySpec.kt index 5cf0e27915..8cc6e5a591 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/OriginsFileCompatibilitySpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/OriginsFileCompatibilitySpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt index 4824a2eab6..8389cd1cea 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/admin/ProviderAdminInterfaceSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt index 3f00d36ed6..17873f3aae 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt index 9b85b49af9..5fe329ab38 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/resiliency/OriginResourcesSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/PluginsPipelineSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/PluginsPipelineSpec.kt index 861477be4e..f401237b69 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/PluginsPipelineSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/routing/PluginsPipelineSpec.kt @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From c3a74afd8af3b61e40c4a6c7935d40cfba65354b Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Tue, 14 Jan 2020 13:11:32 +0000 Subject: [PATCH 05/11] Provide factory to avoid creating unlimited CGLIB enhanced classes --- components/common/pom.xml | 11 +++--- .../format/SanitisedHttpMessageFormatter.java | 5 +-- .../format/SanitisingThrowableFactory.java | 36 +++++++++++++++++++ .../SanitisingThrowableMethodInterceptor.java | 16 ++++----- ...itisingThrowableMethodInterceptorTest.java | 13 ++++--- .../codec/NettyToStyxRequestDecoder.java | 2 -- pom.xml | 7 ++++ 7 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java diff --git a/components/common/pom.xml b/components/common/pom.xml index f153448a14..db8d81f35d 100644 --- a/components/common/pom.xml +++ b/components/common/pom.xml @@ -16,12 +16,6 @@ - - cglib - cglib - 3.2.4 - - com.hotels.styx styx-api @@ -72,6 +66,11 @@ guava + + cglib + cglib + + org.mockito mockito-core diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java index 149920a893..77f12cc4b2 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java @@ -26,9 +26,6 @@ import com.hotels.styx.api.Url; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.LastHttpContent; -import net.sf.cglib.proxy.Enhancer; - -import java.lang.reflect.Proxy; import static java.util.Objects.requireNonNull; @@ -99,7 +96,7 @@ public String formatNettyMessage(HttpObject message) { @Override public Throwable wrap(Throwable t) { - return t == null ? null : (Throwable) Enhancer.create(t.getClass(), new SanitisingThrowableMethodInterceptor(t, sanitisedHttpHeaderFormatter)); + return t == null ? null : SanitisingThrowableFactory.instance().create(t, sanitisedHttpHeaderFormatter); } private String formatNettyRequest(io.netty.handler.codec.http.HttpRequest request) { diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java new file mode 100644 index 0000000000..653cdf12df --- /dev/null +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java @@ -0,0 +1,36 @@ +package com.hotels.styx.common.format; + +import net.sf.cglib.proxy.Enhancer; +import net.sf.cglib.proxy.Factory; +import net.sf.cglib.proxy.MethodInterceptor; +import net.sf.cglib.proxy.MethodProxy; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; + +public class SanitisingThrowableFactory { + + private static final SanitisingThrowableFactory INSTANCE = new SanitisingThrowableFactory(); + + public static SanitisingThrowableFactory instance() { + return INSTANCE; + } + + private ConcurrentHashMap, Factory> cache = new ConcurrentHashMap<>(); + + private SanitisingThrowableFactory() { /* Just making this private */ } + + public Throwable create(Throwable target, SanitisedHttpHeaderFormatter formatter) { + Factory factory = cache.computeIfAbsent(target.getClass(), clazz -> (Factory) Enhancer.create(clazz, new Passthrough())); + SanitisingThrowableMethodInterceptor interceptor = new SanitisingThrowableMethodInterceptor(target, formatter); + return (Throwable) factory.newInstance(interceptor); + } + + static class Passthrough implements MethodInterceptor { + + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return proxy.invokeSuper(obj, args); + } + } +} diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java index 1dc82162e4..2ebd4c1fda 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java @@ -15,13 +15,9 @@ */ package com.hotels.styx.common.format; -import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -42,6 +38,7 @@ private static Pattern cookieNamePattern(String cookieName) { /** * Enhance a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. + * @param throwable the target throwable to enhance * @param formatter provides the sanitising logic */ public SanitisingThrowableMethodInterceptor(Throwable throwable, SanitisedHttpHeaderFormatter formatter) { @@ -61,6 +58,9 @@ public Object intercept(Object obj, java.lang.reflect.Method method, Object[] ar return toString(); case "getCause": return getCause(); + case "fillInStackTrace": + return obj; // don't replace the stack trace in the original exception. + default: return proxy.invoke(throwable, args); } @@ -85,17 +85,17 @@ private String sanitiseCookies(String message) { .orElse(message); } - public String getMessage() throws Throwable { + public String getMessage() { return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getMessage()); } - public String getLocalizedMessage() throws Throwable { + public String getLocalizedMessage() { return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getLocalizedMessage()); } - public synchronized Throwable getCause() throws Throwable { + public Throwable getCause() { Throwable cause = throwable.getCause(); - return cause == null ? null : (Throwable) Enhancer.create(cause.getClass(), new SanitisingThrowableMethodInterceptor(cause, formatter)); + return cause == null ? null : SanitisingThrowableFactory.instance().create(cause, formatter); } public String toString() { diff --git a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java index aed572ba7e..e68f6381c4 100644 --- a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java +++ b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java @@ -15,7 +15,6 @@ */ package com.hotels.styx.common.format; -import net.sf.cglib.proxy.Enhancer; import org.junit.jupiter.api.Test; import static java.util.Arrays.asList; @@ -50,7 +49,7 @@ Exception exception(String msg, Exception cause) { @Test public void messagesWithNoCookiesAreNotChanged() { Exception e = exception("This does not contain any cookies."); - Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); + Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); String prefix = "java.lang.Exception: "; assertThat(proxy.getMessage(), equalTo(prefix + e.getMessage())); @@ -61,7 +60,7 @@ public void messagesWithNoCookiesAreNotChanged() { @Test public void messagesWithUnrecognizedCookiesAreNotChanged() { Exception e = exception("Some cookies: cookie1=c1;cookie2=c2"); - Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); + Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), endsWith(e.getMessage())); assertThat(proxy.getLocalizedMessage(), endsWith(e.getLocalizedMessage())); assertThat(proxy.toString(), endsWith(e.toString())); @@ -70,7 +69,7 @@ public void messagesWithUnrecognizedCookiesAreNotChanged() { @Test public void messagesWithRecognizedCookiesAreSanitized() { Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;cookie2=c2;private-cookie=private"); - Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); + Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("cookie2=c2")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); @@ -81,7 +80,7 @@ public void messagesWithRecognizedCookiesAreSanitized() { public void exceptionCausesAreSanitized() { Exception inner = exception("Inner: cookie1=c1;secret-cookie=secret"); Exception outer = exception("Outer: cookie2=c2;private-cookie=private", inner); - Throwable outerProxy = (Throwable) Enhancer.create(outer.getClass(), new SanitisingThrowableMethodInterceptor(outer, formatter)); + Throwable outerProxy = SanitisingThrowableFactory.instance().create(outer, formatter); assertThat(outerProxy.getMessage(), containsString("cookie2=c2")); assertThat(outerProxy.getMessage(), containsString("private-cookie=****")); assertThat(outerProxy.getCause().getMessage(), containsString("cookie1=c1")); @@ -91,14 +90,14 @@ public void exceptionCausesAreSanitized() { @Test public void nullMessagesAreAllowed() { Exception e = exception(null); - Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); + Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), equalTo("java.lang.Exception: null")); } @Test public void messagesWithInvalidCookiesAreSanitized() { Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;bad-cookie=bad\u0000bad;private-cookie=private"); - Throwable proxy = (Throwable) Enhancer.create(e.getClass(), new SanitisingThrowableMethodInterceptor(e, formatter)); + Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("bad-cookie=bad\u0000bad")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); diff --git a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java index bfae5f22eb..bd9a4bc59e 100644 --- a/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java +++ b/components/server/src/main/java/com/hotels/styx/server/netty/codec/NettyToStyxRequestDecoder.java @@ -83,8 +83,6 @@ protected void decode(ChannelHandlerContext ctx, HttpObject httpObject, List1.0.2 3.3.0.RELEASE 3.0.3 + 3.3.0 1.3.21 @@ -333,6 +334,12 @@ ${jackson-module-kotlin.version} + + cglib + cglib + ${cglib.version} + + io.dropwizard.metrics From 7837a8b949cec51bcde37d73c3f4b89cc8e81d61 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Tue, 14 Jan 2020 13:17:59 +0000 Subject: [PATCH 06/11] Copyright header --- .../java/com/hotels/styx/api/ResponseCookie.java | 2 +- .../common/format/SanitisingThrowableFactory.java | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java b/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java index 6579a66518..a2601a0c47 100644 --- a/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java +++ b/components/api/src/main/java/com/hotels/styx/api/ResponseCookie.java @@ -1,5 +1,5 @@ /* - Copyright (C) 2013-2019 Expedia Inc. + Copyright (C) 2013-2020 Expedia Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java index 653cdf12df..9e221b1ebe 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java @@ -1,3 +1,18 @@ +/* + Copyright (C) 2013-2020 Expedia Inc. + + Licensed 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 + + http://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.hotels.styx.common.format; import net.sf.cglib.proxy.Enhancer; From 8a8c48c6dd7baa4af7e54691bdb251eaa97acf1e Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Fri, 17 Jan 2020 11:08:46 +0000 Subject: [PATCH 07/11] Reimplement with ByteBuddy, and address review comments --- components/common/pom.xml | 4 +- .../format/SanitisedHttpHeaderFormatter.java | 7 ++- .../format/SanitisingThrowableFactory.java | 54 ++++++++++++----- ...va => SanitisingThrowableInterceptor.java} | 56 +++++++++--------- ...> SanitisingThrowableInterceptorTest.java} | 58 +++++++++++++------ pom.xml | 8 +-- 6 files changed, 116 insertions(+), 71 deletions(-) rename components/common/src/main/java/com/hotels/styx/common/format/{SanitisingThrowableMethodInterceptor.java => SanitisingThrowableInterceptor.java} (65%) rename components/common/src/test/java/com/hotels/styx/common/format/{SanitisingThrowableMethodInterceptorTest.java => SanitisingThrowableInterceptorTest.java} (63%) diff --git a/components/common/pom.xml b/components/common/pom.xml index db8d81f35d..5d535ee0c6 100644 --- a/components/common/pom.xml +++ b/components/common/pom.xml @@ -67,8 +67,8 @@ - cglib - cglib + net.bytebuddy + byte-buddy diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java index b655e0c0ea..516666c315 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java @@ -19,6 +19,7 @@ import com.hotels.styx.api.HttpHeaders; import com.hotels.styx.api.RequestCookie; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -38,12 +39,12 @@ public class SanitisedHttpHeaderFormatter { private final List cookiesToHide; public SanitisedHttpHeaderFormatter(List headersToHide, List cookiesToHide) { - this.headersToHide = Collections.unmodifiableList(requireNonNull(headersToHide)); - this.cookiesToHide = Collections.unmodifiableList(requireNonNull(cookiesToHide)); + this.headersToHide = new ArrayList<>(requireNonNull(headersToHide)); + this.cookiesToHide = new ArrayList<>(requireNonNull(cookiesToHide)); } public List cookiesToHide() { - return cookiesToHide; + return new ArrayList<>(cookiesToHide); } public String format(HttpHeaders headers) { diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java index 9e221b1ebe..3fbc98ced1 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java @@ -15,37 +15,61 @@ */ package com.hotels.styx.common.format; -import net.sf.cglib.proxy.Enhancer; -import net.sf.cglib.proxy.Factory; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.TypeCache; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.matcher.ElementMatchers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.lang.reflect.Method; -import java.util.concurrent.ConcurrentHashMap; +import java.lang.reflect.Constructor; + +import static net.bytebuddy.TypeCache.Sort.SOFT; public class SanitisingThrowableFactory { + private static final Logger LOG = LoggerFactory.getLogger(SanitisingThrowableFactory.class); + private static final SanitisingThrowableFactory INSTANCE = new SanitisingThrowableFactory(); public static SanitisingThrowableFactory instance() { return INSTANCE; } - private ConcurrentHashMap, Factory> cache = new ConcurrentHashMap<>(); + private TypeCache typeCache = new TypeCache<>(SOFT); private SanitisingThrowableFactory() { /* Just making this private */ } public Throwable create(Throwable target, SanitisedHttpHeaderFormatter formatter) { - Factory factory = cache.computeIfAbsent(target.getClass(), clazz -> (Factory) Enhancer.create(clazz, new Passthrough())); - SanitisingThrowableMethodInterceptor interceptor = new SanitisingThrowableMethodInterceptor(target, formatter); - return (Throwable) factory.newInstance(interceptor); - } - static class Passthrough implements MethodInterceptor { + Class clazz = target.getClass(); + try { + Constructor defaultConstructor = clazz.getConstructor(); + + Class proxyClass = typeCache.findOrInsert(getClass().getClassLoader(), clazz.getName(), () -> + new ByteBuddy() + .subclass(clazz) + .defineField("methodInterceptor", SanitisingThrowableInterceptor.class, Visibility.PRIVATE) + .defineConstructor(Visibility.PUBLIC) + .withParameters(SanitisingThrowableInterceptor.class) + .intercept(FieldAccessor.ofField("methodInterceptor").setsArgumentAt(0) + .andThen(MethodCall.invoke(defaultConstructor))) + .method(ElementMatchers.any()) + .intercept(MethodDelegation.toField("methodInterceptor")) + .make() + .load(getClass().getClassLoader()) + .getLoaded()); - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - return proxy.invokeSuper(obj, args); + Throwable proxy = (Throwable) proxyClass + .getConstructor(SanitisingThrowableInterceptor.class) + .newInstance(new SanitisingThrowableInterceptor(target, formatter)); + return proxy; + } catch (Exception e) { + LOG.warn("Unable to proxy throwable class {} - {}", clazz, e.toString()); // No need to log stack trace here } + return target; } } diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java similarity index 65% rename from components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java rename to components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java index 2ebd4c1fda..3c288f3da2 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptor.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java @@ -15,9 +15,11 @@ */ package com.hotels.styx.common.format; -import net.sf.cglib.proxy.MethodInterceptor; -import net.sf.cglib.proxy.MethodProxy; +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import java.lang.reflect.Method; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,44 +28,36 @@ * Wraps a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. * Any cause is similarly wrapped before being returned. */ -public class SanitisingThrowableMethodInterceptor implements MethodInterceptor { +public class SanitisingThrowableInterceptor { private static final ConcurrentHashMap COOKIE_NAME_PATTERN_CACHE = new ConcurrentHashMap<>(); private static Pattern cookieNamePattern(String cookieName) { return COOKIE_NAME_PATTERN_CACHE.computeIfAbsent(cookieName, name -> Pattern.compile(name + "\\s*=")); } - private final Throwable throwable; + private final Throwable target; private final SanitisedHttpHeaderFormatter formatter; /** * Enhance a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. - * @param throwable the target throwable to enhance + * @param target the target throwable to enhance * @param formatter provides the sanitising logic */ - public SanitisingThrowableMethodInterceptor(Throwable throwable, SanitisedHttpHeaderFormatter formatter) { - this.throwable = throwable; + public SanitisingThrowableInterceptor(Throwable target, SanitisedHttpHeaderFormatter formatter) { + this.target = target; this.formatter = formatter; } - @Override - public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, - MethodProxy proxy) throws Throwable { - switch (method.getName()) { - case "getMessage": - return getMessage(); - case "getLocalizedMessage": - return getLocalizedMessage(); - case "toString": - return toString(); - case "getCause": - return getCause(); - case "fillInStackTrace": - return obj; // don't replace the stack trace in the original exception. - - default: - return proxy.invoke(throwable, args); - } + /** + * Default method interceptor, to delegate all other method calls to the target. + * @param method the method being proxied + * @param args the arguments of the method being proxied + * @return the response from the target object + * @throws Exception if the target method throws an exception + */ + @RuntimeType + public Object intercept(@Origin Method method, @AllArguments Object[] args) throws Exception { + return method.invoke(target, args); } private String sanitiseCookies(String message) { @@ -86,20 +80,24 @@ private String sanitiseCookies(String message) { } public String getMessage() { - return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getMessage()); + return target.getClass().getName() + ": " + sanitiseCookies(target.getMessage()); } public String getLocalizedMessage() { - return throwable.getClass().getName() + ": " + sanitiseCookies(throwable.getLocalizedMessage()); + return target.getClass().getName() + ": " + sanitiseCookies(target.getLocalizedMessage()); } public Throwable getCause() { - Throwable cause = throwable.getCause(); + Throwable cause = target.getCause(); return cause == null ? null : SanitisingThrowableFactory.instance().create(cause, formatter); } + public Throwable fillInStackTrace() { + return target; + } + public String toString() { - return "Sanitized: " + throwable.toString(); + return "Sanitized: " + target.toString(); } } diff --git a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableInterceptorTest.java similarity index 63% rename from components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java rename to components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableInterceptorTest.java index e68f6381c4..df62d19e64 100644 --- a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableMethodInterceptorTest.java +++ b/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableInterceptorTest.java @@ -22,25 +22,19 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; import static org.hamcrest.core.StringEndsWith.endsWith; -class SanitisingThrowableMethodInterceptorTest { +class SanitisingThrowableInterceptorTest { SanitisedHttpHeaderFormatter formatter = new SanitisedHttpHeaderFormatter( emptyList(), asList("secret-cookie", "private-cookie") ); - Exception exception(String msg) { + static Exception throwAndReturn(Exception ex) { try { - throw new Exception(msg); - } catch (Exception e) { - return e; - } - } - - Exception exception(String msg, Exception cause) { - try { - throw new Exception(msg, cause); + throw ex; } catch (Exception e) { return e; } @@ -48,7 +42,7 @@ Exception exception(String msg, Exception cause) { @Test public void messagesWithNoCookiesAreNotChanged() { - Exception e = exception("This does not contain any cookies."); + Exception e = throwAndReturn(new Exception("This does not contain any cookies.")); Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); String prefix = "java.lang.Exception: "; @@ -59,7 +53,7 @@ public void messagesWithNoCookiesAreNotChanged() { @Test public void messagesWithUnrecognizedCookiesAreNotChanged() { - Exception e = exception("Some cookies: cookie1=c1;cookie2=c2"); + Exception e = throwAndReturn(new Exception("Some cookies: cookie1=c1;cookie2=c2")); Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), endsWith(e.getMessage())); assertThat(proxy.getLocalizedMessage(), endsWith(e.getLocalizedMessage())); @@ -68,7 +62,7 @@ public void messagesWithUnrecognizedCookiesAreNotChanged() { @Test public void messagesWithRecognizedCookiesAreSanitized() { - Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;cookie2=c2;private-cookie=private"); + Exception e = throwAndReturn(new Exception("Some cookies: cookie1=c1;secret-cookie=secret;cookie2=c2;private-cookie=private")); Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("cookie2=c2")); @@ -78,8 +72,8 @@ public void messagesWithRecognizedCookiesAreSanitized() { @Test public void exceptionCausesAreSanitized() { - Exception inner = exception("Inner: cookie1=c1;secret-cookie=secret"); - Exception outer = exception("Outer: cookie2=c2;private-cookie=private", inner); + Exception inner = throwAndReturn(new Exception("Inner: cookie1=c1;secret-cookie=secret")); + Exception outer = throwAndReturn(new Exception("Outer: cookie2=c2;private-cookie=private", inner)); Throwable outerProxy = SanitisingThrowableFactory.instance().create(outer, formatter); assertThat(outerProxy.getMessage(), containsString("cookie2=c2")); assertThat(outerProxy.getMessage(), containsString("private-cookie=****")); @@ -89,18 +83,46 @@ public void exceptionCausesAreSanitized() { @Test public void nullMessagesAreAllowed() { - Exception e = exception(null); + Exception e = throwAndReturn(new Exception((String) null)); Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), equalTo("java.lang.Exception: null")); } + @Test + public void exceptionsWithNoDefaultConstructorAreNotProxied() { + Exception e = throwAndReturn(new NoDefaultConstructorException("Ooops, no default constructor")); + Throwable notProxy = SanitisingThrowableFactory.instance().create(e, formatter); + assertThat(notProxy.getMessage(), equalTo("Ooops, no default constructor")); + assertThat(notProxy.getClass().getSuperclass(), not(instanceOf(NoDefaultConstructorException.class))); // i.e. not proxied. + } + + @Test + public void exceptionsOfFinalClassAreNotProxied() { + Exception e = throwAndReturn(new FinalClassException()); + Throwable notProxy = SanitisingThrowableFactory.instance().create(e, formatter); + assertThat(notProxy.getMessage(), equalTo("This is a final class.")); + assertThat(notProxy.getClass().getSuperclass(), not(instanceOf(FinalClassException.class))); // i.e. not proxied. + } + @Test public void messagesWithInvalidCookiesAreSanitized() { - Exception e = exception("Some cookies: cookie1=c1;secret-cookie=secret;bad-cookie=bad\u0000bad;private-cookie=private"); + Exception e = throwAndReturn(new Exception("Some cookies: cookie1=c1;secret-cookie=secret;bad-cookie=bad\u0000bad;private-cookie=private")); Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("bad-cookie=bad\u0000bad")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); assertThat(proxy.getMessage(), containsString("private-cookie=****")); } + + static class NoDefaultConstructorException extends Exception { + NoDefaultConstructorException(String msg) { + super(msg); + } + } + + static final class FinalClassException extends Exception { + FinalClassException() { + super("This is a final class."); + } + } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2137d6e959..d5c6ad5286 100644 --- a/pom.xml +++ b/pom.xml @@ -335,11 +335,11 @@ - cglib - cglib - ${cglib.version} + net.bytebuddy + byte-buddy + 1.10.6 - + io.dropwizard.metrics From 443c742fb85179e80e247241fabf523982ca2306 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Fri, 17 Jan 2020 11:25:18 +0000 Subject: [PATCH 08/11] Fix after merge --- .../src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt b/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt index dc6869adc8..122c1e0dc4 100644 --- a/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt +++ b/components/proxy/src/main/kotlin/com/hotels/styx/servers/StyxHttpServer.kt @@ -124,7 +124,8 @@ internal class StyxHttpServerFactory : StyxServerFactory { environment.configuration().styxHeaderConfig().styxInfoHeaderName(), ResponseInfoFormat(environment).format(request)) }, - false) + false, + environment.httpMessageFormatter()) .create( if (config.tlsSettings == null) { HttpConnectorConfig(config.port) From 82bc5dd4654a4ee411d6b0d3b306b53ea37de214 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Fri, 17 Jan 2020 11:43:21 +0000 Subject: [PATCH 09/11] Checkstyle --- .../format/SanitisedHttpHeaderFormatter.java | 8 +++--- .../format/SanitisingThrowableFactory.java | 25 ++++++++++++++++--- .../SanitisingThrowableInterceptor.java | 2 +- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java index 516666c315..3c13d0f502 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpHeaderFormatter.java @@ -21,11 +21,11 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.StreamSupport; +import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; /** @@ -39,12 +39,12 @@ public class SanitisedHttpHeaderFormatter { private final List cookiesToHide; public SanitisedHttpHeaderFormatter(List headersToHide, List cookiesToHide) { - this.headersToHide = new ArrayList<>(requireNonNull(headersToHide)); - this.cookiesToHide = new ArrayList<>(requireNonNull(cookiesToHide)); + this.headersToHide = unmodifiableList(new ArrayList<>(requireNonNull(headersToHide))); + this.cookiesToHide = unmodifiableList(new ArrayList<>(requireNonNull(cookiesToHide))); } public List cookiesToHide() { - return new ArrayList<>(cookiesToHide); + return cookiesToHide; } public String format(HttpHeaders headers) { diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java index 3fbc98ced1..becfc5d240 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java @@ -29,25 +29,43 @@ import static net.bytebuddy.TypeCache.Sort.SOFT; +/** + * Factory for wrapping a {@link Throwable} in a dynamic proxy that sanitizes its message to hide sensitive cookie + * information. The supplied Throwable must be non-final, and must have a no-args constructor. The proxy-intercepted + * methods are handled by an instance of {@link SanitisingThrowableInterceptor} + */ public class SanitisingThrowableFactory { private static final Logger LOG = LoggerFactory.getLogger(SanitisingThrowableFactory.class); private static final SanitisingThrowableFactory INSTANCE = new SanitisingThrowableFactory(); + /** + * Return the singleton instance of this factory. + * @return the singleton instance. + */ public static SanitisingThrowableFactory instance() { return INSTANCE; } - private TypeCache typeCache = new TypeCache<>(SOFT); + private final TypeCache typeCache = new TypeCache<>(SOFT); private SanitisingThrowableFactory() { /* Just making this private */ } + /** + * Wrap a {@link Throwable} in a dynamic proxy that sanitizes its message to hide sensitive cookie + * information. The supplied Throwable must be non-final, and must have a no-args constructor. If the proxy + * cannot be created for any reason (including that it is proxying a final class, or one without a no-args constructor), + * then a warning is logged and the unproxied Throwable is returned back to the caller. + * @param target the Throwable to be proxied + * @param formatter hides the sensitive cookies. + * @return the proxied Throwable, or the supplied Target if it cannot be proxied. + */ public Throwable create(Throwable target, SanitisedHttpHeaderFormatter formatter) { Class clazz = target.getClass(); try { - Constructor defaultConstructor = clazz.getConstructor(); + Constructor defaultConstructor = clazz.getConstructor(); Class proxyClass = typeCache.findOrInsert(getClass().getClassLoader(), clazz.getName(), () -> new ByteBuddy() @@ -63,10 +81,9 @@ public Throwable create(Throwable target, SanitisedHttpHeaderFormatter formatter .load(getClass().getClassLoader()) .getLoaded()); - Throwable proxy = (Throwable) proxyClass + return (Throwable) proxyClass .getConstructor(SanitisingThrowableInterceptor.class) .newInstance(new SanitisingThrowableInterceptor(target, formatter)); - return proxy; } catch (Exception e) { LOG.warn("Unable to proxy throwable class {} - {}", clazz, e.toString()); // No need to log stack trace here } diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java index 3c288f3da2..8444529acd 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java @@ -25,7 +25,7 @@ import java.util.regex.Pattern; /** - * Wraps a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. + * Intercepts methods of a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. * Any cause is similarly wrapped before being returned. */ public class SanitisingThrowableInterceptor { From c1bf233cf761087db16fe2d1a6b4e9e6c5b6de78 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Fri, 17 Jan 2020 11:59:45 +0000 Subject: [PATCH 10/11] Refactor ProxyConnector creation --- .../hotels/styx/ProxyConnectorFactory.java | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java b/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java index 90ab81cc7b..102bb0df6c 100644 --- a/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java +++ b/components/proxy/src/main/java/com/hotels/styx/ProxyConnectorFactory.java @@ -90,7 +90,7 @@ public ProxyConnectorFactory(NettyServerConfig serverConfig, @Override public ServerConnector create(ConnectorConfig config) { - return new ProxyConnector(config, serverConfig, metrics, errorStatusListener, unwiseCharacters, responseEnhancer, requestTracking, httpMessageFormatter); + return new ProxyConnector(config, this); } private static final class ProxyConnector implements ServerConnector { @@ -107,30 +107,23 @@ private static final class ProxyConnector implements ServerConnector { private final RequestTracker requestTracker; private final HttpMessageFormatter httpMessageFormatter; - private ProxyConnector(ConnectorConfig config, - NettyServerConfig serverConfig, - MetricRegistry metrics, - HttpErrorStatusListener errorStatusListener, - String unwiseCharacters, - ResponseEnhancer responseEnhancer, - boolean requestTracking, - HttpMessageFormatter httpMessageFormatter) { - this.responseEnhancer = requireNonNull(responseEnhancer); + private ProxyConnector(ConnectorConfig config, ProxyConnectorFactory factory) { this.config = requireNonNull(config); - this.serverConfig = requireNonNull(serverConfig); - this.metrics = requireNonNull(metrics); - this.httpErrorStatusListener = requireNonNull(errorStatusListener); + this.responseEnhancer = requireNonNull(factory.responseEnhancer); + this.serverConfig = requireNonNull(factory.serverConfig); + this.metrics = requireNonNull(factory.metrics); + this.httpErrorStatusListener = requireNonNull(factory.errorStatusListener); this.channelStatsHandler = new ChannelStatisticsHandler(metrics); this.requestStatsCollector = new RequestStatsCollector(metrics.scope("requests")); this.excessConnectionRejector = new ExcessConnectionRejector(new DefaultChannelGroup(GlobalEventExecutor.INSTANCE), serverConfig.maxConnectionsCount()); - this.unwiseCharEncoder = new ConfigurableUnwiseCharsEncoder(unwiseCharacters); + this.unwiseCharEncoder = new ConfigurableUnwiseCharsEncoder(factory.unwiseCharacters); if (isHttps()) { this.sslContext = Optional.of(newSSLContext((HttpsConnectorConfig) config, metrics)); } else { this.sslContext = Optional.empty(); } - this.requestTracker = requestTracking ? CurrentRequestTracker.INSTANCE : RequestTracker.NO_OP; - this.httpMessageFormatter = httpMessageFormatter; + this.requestTracker = factory.requestTracking ? CurrentRequestTracker.INSTANCE : RequestTracker.NO_OP; + this.httpMessageFormatter = factory.httpMessageFormatter; } @Override From b556859c216c77fae450f55202d39ba4e3431202 Mon Sep 17 00:00:00 2001 From: Chris Gresty Date: Fri, 17 Jan 2020 16:33:01 +0000 Subject: [PATCH 11/11] Review comments --- .../format/SanitisedHttpMessageFormatter.java | 2 +- .../format/SanitisingThrowableFactory.java | 92 --------- .../SanitisingThrowableInterceptor.java | 103 ---------- .../common/format/ThrowableSanitiser.java | 176 ++++++++++++++++++ ...rTest.java => ThrowableSanitiserTest.java} | 18 +- pom.xml | 4 +- .../styx/logging/HttpMessageLoggingSpec.kt | 4 +- 7 files changed, 191 insertions(+), 208 deletions(-) delete mode 100644 components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java delete mode 100644 components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java create mode 100644 components/common/src/main/java/com/hotels/styx/common/format/ThrowableSanitiser.java rename components/common/src/test/java/com/hotels/styx/common/format/{SanitisingThrowableInterceptorTest.java => ThrowableSanitiserTest.java} (87%) diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java index 77f12cc4b2..f757d29dee 100644 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java +++ b/components/common/src/main/java/com/hotels/styx/common/format/SanitisedHttpMessageFormatter.java @@ -96,7 +96,7 @@ public String formatNettyMessage(HttpObject message) { @Override public Throwable wrap(Throwable t) { - return t == null ? null : SanitisingThrowableFactory.instance().create(t, sanitisedHttpHeaderFormatter); + return t == null ? null : ThrowableSanitiser.instance().sanitise(t, sanitisedHttpHeaderFormatter); } private String formatNettyRequest(io.netty.handler.codec.http.HttpRequest request) { diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java deleted file mode 100644 index becfc5d240..0000000000 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableFactory.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright (C) 2013-2020 Expedia Inc. - - Licensed 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 - - http://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.hotels.styx.common.format; - -import net.bytebuddy.ByteBuddy; -import net.bytebuddy.TypeCache; -import net.bytebuddy.description.modifier.Visibility; -import net.bytebuddy.implementation.FieldAccessor; -import net.bytebuddy.implementation.MethodCall; -import net.bytebuddy.implementation.MethodDelegation; -import net.bytebuddy.matcher.ElementMatchers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.lang.reflect.Constructor; - -import static net.bytebuddy.TypeCache.Sort.SOFT; - -/** - * Factory for wrapping a {@link Throwable} in a dynamic proxy that sanitizes its message to hide sensitive cookie - * information. The supplied Throwable must be non-final, and must have a no-args constructor. The proxy-intercepted - * methods are handled by an instance of {@link SanitisingThrowableInterceptor} - */ -public class SanitisingThrowableFactory { - - private static final Logger LOG = LoggerFactory.getLogger(SanitisingThrowableFactory.class); - - private static final SanitisingThrowableFactory INSTANCE = new SanitisingThrowableFactory(); - - /** - * Return the singleton instance of this factory. - * @return the singleton instance. - */ - public static SanitisingThrowableFactory instance() { - return INSTANCE; - } - - private final TypeCache typeCache = new TypeCache<>(SOFT); - - private SanitisingThrowableFactory() { /* Just making this private */ } - - /** - * Wrap a {@link Throwable} in a dynamic proxy that sanitizes its message to hide sensitive cookie - * information. The supplied Throwable must be non-final, and must have a no-args constructor. If the proxy - * cannot be created for any reason (including that it is proxying a final class, or one without a no-args constructor), - * then a warning is logged and the unproxied Throwable is returned back to the caller. - * @param target the Throwable to be proxied - * @param formatter hides the sensitive cookies. - * @return the proxied Throwable, or the supplied Target if it cannot be proxied. - */ - public Throwable create(Throwable target, SanitisedHttpHeaderFormatter formatter) { - - Class clazz = target.getClass(); - try { - Constructor defaultConstructor = clazz.getConstructor(); - - Class proxyClass = typeCache.findOrInsert(getClass().getClassLoader(), clazz.getName(), () -> - new ByteBuddy() - .subclass(clazz) - .defineField("methodInterceptor", SanitisingThrowableInterceptor.class, Visibility.PRIVATE) - .defineConstructor(Visibility.PUBLIC) - .withParameters(SanitisingThrowableInterceptor.class) - .intercept(FieldAccessor.ofField("methodInterceptor").setsArgumentAt(0) - .andThen(MethodCall.invoke(defaultConstructor))) - .method(ElementMatchers.any()) - .intercept(MethodDelegation.toField("methodInterceptor")) - .make() - .load(getClass().getClassLoader()) - .getLoaded()); - - return (Throwable) proxyClass - .getConstructor(SanitisingThrowableInterceptor.class) - .newInstance(new SanitisingThrowableInterceptor(target, formatter)); - } catch (Exception e) { - LOG.warn("Unable to proxy throwable class {} - {}", clazz, e.toString()); // No need to log stack trace here - } - return target; - } -} diff --git a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java b/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java deleted file mode 100644 index 8444529acd..0000000000 --- a/components/common/src/main/java/com/hotels/styx/common/format/SanitisingThrowableInterceptor.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright (C) 2013-2020 Expedia Inc. - - Licensed 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 - - http://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.hotels.styx.common.format; - -import net.bytebuddy.implementation.bind.annotation.AllArguments; -import net.bytebuddy.implementation.bind.annotation.Origin; -import net.bytebuddy.implementation.bind.annotation.RuntimeType; - -import java.lang.reflect.Method; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Intercepts methods of a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. - * Any cause is similarly wrapped before being returned. - */ -public class SanitisingThrowableInterceptor { - - private static final ConcurrentHashMap COOKIE_NAME_PATTERN_CACHE = new ConcurrentHashMap<>(); - private static Pattern cookieNamePattern(String cookieName) { - return COOKIE_NAME_PATTERN_CACHE.computeIfAbsent(cookieName, name -> Pattern.compile(name + "\\s*=")); - } - - private final Throwable target; - private final SanitisedHttpHeaderFormatter formatter; - - /** - * Enhance a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. - * @param target the target throwable to enhance - * @param formatter provides the sanitising logic - */ - public SanitisingThrowableInterceptor(Throwable target, SanitisedHttpHeaderFormatter formatter) { - this.target = target; - this.formatter = formatter; - } - - /** - * Default method interceptor, to delegate all other method calls to the target. - * @param method the method being proxied - * @param args the arguments of the method being proxied - * @return the response from the target object - * @throws Exception if the target method throws an exception - */ - @RuntimeType - public Object intercept(@Origin Method method, @AllArguments Object[] args) throws Exception { - return method.invoke(target, args); - } - - private String sanitiseCookies(String message) { - if (message == null) { - return null; - } - return formatter.cookiesToHide().stream() - // Find earliest 'cookiename=' in message - .map(cookie -> cookieNamePattern(cookie).matcher(message)) - .filter(Matcher::find) - .map(Matcher::start) - .min(Integer::compareTo) - .map(cookiesStart -> { - // Assume the cookies run to the end of the message - String cookies = message.substring(cookiesStart); - String sanitizedCookies = formatter.formatCookieHeaderValue(cookies); - return message.substring(0, cookiesStart) + sanitizedCookies; - }) - .orElse(message); - } - - public String getMessage() { - return target.getClass().getName() + ": " + sanitiseCookies(target.getMessage()); - } - - public String getLocalizedMessage() { - return target.getClass().getName() + ": " + sanitiseCookies(target.getLocalizedMessage()); - } - - public Throwable getCause() { - Throwable cause = target.getCause(); - return cause == null ? null : SanitisingThrowableFactory.instance().create(cause, formatter); - } - - public Throwable fillInStackTrace() { - return target; - } - - public String toString() { - return "Sanitized: " + target.toString(); - } -} - diff --git a/components/common/src/main/java/com/hotels/styx/common/format/ThrowableSanitiser.java b/components/common/src/main/java/com/hotels/styx/common/format/ThrowableSanitiser.java new file mode 100644 index 0000000000..b81e8dc30b --- /dev/null +++ b/components/common/src/main/java/com/hotels/styx/common/format/ThrowableSanitiser.java @@ -0,0 +1,176 @@ +/* + Copyright (C) 2013-2020 Expedia Inc. + + Licensed 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 + + http://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.hotels.styx.common.format; + +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.TypeCache; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.MethodCall; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.matcher.ElementMatchers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static net.bytebuddy.TypeCache.Sort.SOFT; + +/** + * Wraps a {@link Throwable} in a dynamic proxy that sanitizes its message to hide sensitive cookie + * information. The supplied Throwable must be non-final, and must have a no-args constructor. The proxy-intercepted + * methods are handled by an instance of {@link Interceptor} + */ +public class ThrowableSanitiser { + + private static final Logger LOG = LoggerFactory.getLogger(ThrowableSanitiser.class); + + private static final ThrowableSanitiser INSTANCE = new ThrowableSanitiser(); + + /** + * Return the singleton instance of this factory. + * @return the singleton instance. + */ + public static ThrowableSanitiser instance() { + return INSTANCE; + } + + private final TypeCache typeCache = new TypeCache<>(SOFT); + + private ThrowableSanitiser() { /* Just making this private */ } + + /** + * Wrap a {@link Throwable} in a dynamic proxy that sanitizes its message to hide sensitive cookie + * information. The supplied Throwable must be non-final, and must have a no-args constructor. If the proxy + * cannot be created for any reason (including that it is proxying a final class, or one without a no-args constructor), + * then a warning is logged and the unproxied Throwable is returned back to the caller. + * @param original the Throwable to be proxied + * @param formatter hides the sensitive cookies. + * @return the proxied Throwable, or the original throwable if it cannot be proxied. + */ + public Throwable sanitise(Throwable original, SanitisedHttpHeaderFormatter formatter) { + + Class clazz = original.getClass(); + try { + Constructor defaultConstructor = clazz.getConstructor(); + + Class proxyClass = typeCache.findOrInsert(getClass().getClassLoader(), clazz.getName(), () -> + new ByteBuddy() + .subclass(clazz) + .defineField("methodInterceptor", Interceptor.class, Visibility.PRIVATE) + .defineConstructor(Visibility.PUBLIC) + .withParameters(Interceptor.class) + .intercept(FieldAccessor.ofField("methodInterceptor").setsArgumentAt(0) + .andThen(MethodCall.invoke(defaultConstructor))) + .method(ElementMatchers.any()) + .intercept(MethodDelegation.toField("methodInterceptor")) + .make() + .load(getClass().getClassLoader()) + .getLoaded()); + + return (Throwable) proxyClass + .getConstructor(Interceptor.class) + .newInstance(new Interceptor(original, formatter)); + } catch (Exception e) { + LOG.warn("Unable to proxy throwable class {} - {}", clazz, e.toString()); // No need to log stack trace here + } + return original; + } + + /** + * Intercepts methods of a {@link Throwable} so that the message can be modified to sanitize the values of any recognised cookies. + * Any cause is similarly wrapped before being returned. + */ + public static class Interceptor { + + private static final ConcurrentHashMap COOKIE_NAME_PATTERN_CACHE = new ConcurrentHashMap<>(); + private static Pattern cookieNamePattern(String cookieName) { + return COOKIE_NAME_PATTERN_CACHE.computeIfAbsent(cookieName, name -> Pattern.compile(name + "\\s*=")); + } + + private final Throwable target; + private final SanitisedHttpHeaderFormatter formatter; + + /** + * Enhance a throwable, using the given {@link SanitisedHttpHeaderFormatter} to recognise and sanitise cookie values. + * @param target the target throwable to enhance + * @param formatter provides the sanitising logic + */ + public Interceptor(Throwable target, SanitisedHttpHeaderFormatter formatter) { + this.target = target; + this.formatter = formatter; + } + + /** + * Default method interceptor, to delegate all other method calls to the target. + * @param method the method being proxied + * @param args the arguments of the method being proxied + * @return the response from the target object + * @throws Exception if the target method throws an exception + */ + @RuntimeType + public Object intercept(@Origin Method method, @AllArguments Object[] args) throws Exception { + return method.invoke(target, args); + } + + private String sanitiseCookies(String message) { + if (message == null) { + return null; + } + return formatter.cookiesToHide().stream() + // Find earliest 'cookiename=' in message + .map(cookie -> cookieNamePattern(cookie).matcher(message)) + .filter(Matcher::find) + .map(Matcher::start) + .min(Integer::compareTo) + .map(cookiesStart -> { + // Assume the cookies run to the end of the message + String cookies = message.substring(cookiesStart); + String sanitizedCookies = formatter.formatCookieHeaderValue(cookies); + return message.substring(0, cookiesStart) + sanitizedCookies; + }) + .orElse(message); + } + + public String getMessage() { + return target.getClass().getName() + ": " + sanitiseCookies(target.getMessage()); + } + + public String getLocalizedMessage() { + return target.getClass().getName() + ": " + sanitiseCookies(target.getLocalizedMessage()); + } + + public Throwable getCause() { + Throwable cause = target.getCause(); + return cause == null ? null : instance().sanitise(cause, formatter); + } + + public Throwable fillInStackTrace() { + return target; + } + + public String toString() { + return "Sanitized: " + target.toString(); + } + } +} diff --git a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableInterceptorTest.java b/components/common/src/test/java/com/hotels/styx/common/format/ThrowableSanitiserTest.java similarity index 87% rename from components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableInterceptorTest.java rename to components/common/src/test/java/com/hotels/styx/common/format/ThrowableSanitiserTest.java index df62d19e64..ce6d41ba37 100644 --- a/components/common/src/test/java/com/hotels/styx/common/format/SanitisingThrowableInterceptorTest.java +++ b/components/common/src/test/java/com/hotels/styx/common/format/ThrowableSanitiserTest.java @@ -26,7 +26,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.core.StringEndsWith.endsWith; -class SanitisingThrowableInterceptorTest { +class ThrowableSanitiserTest { SanitisedHttpHeaderFormatter formatter = new SanitisedHttpHeaderFormatter( emptyList(), asList("secret-cookie", "private-cookie") @@ -43,7 +43,7 @@ static Exception throwAndReturn(Exception ex) { @Test public void messagesWithNoCookiesAreNotChanged() { Exception e = throwAndReturn(new Exception("This does not contain any cookies.")); - Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable proxy = ThrowableSanitiser.instance().sanitise(e, formatter); String prefix = "java.lang.Exception: "; assertThat(proxy.getMessage(), equalTo(prefix + e.getMessage())); @@ -54,7 +54,7 @@ public void messagesWithNoCookiesAreNotChanged() { @Test public void messagesWithUnrecognizedCookiesAreNotChanged() { Exception e = throwAndReturn(new Exception("Some cookies: cookie1=c1;cookie2=c2")); - Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable proxy = ThrowableSanitiser.instance().sanitise(e, formatter); assertThat(proxy.getMessage(), endsWith(e.getMessage())); assertThat(proxy.getLocalizedMessage(), endsWith(e.getLocalizedMessage())); assertThat(proxy.toString(), endsWith(e.toString())); @@ -63,7 +63,7 @@ public void messagesWithUnrecognizedCookiesAreNotChanged() { @Test public void messagesWithRecognizedCookiesAreSanitized() { Exception e = throwAndReturn(new Exception("Some cookies: cookie1=c1;secret-cookie=secret;cookie2=c2;private-cookie=private")); - Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable proxy = ThrowableSanitiser.instance().sanitise(e, formatter); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("cookie2=c2")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); @@ -74,7 +74,7 @@ public void messagesWithRecognizedCookiesAreSanitized() { public void exceptionCausesAreSanitized() { Exception inner = throwAndReturn(new Exception("Inner: cookie1=c1;secret-cookie=secret")); Exception outer = throwAndReturn(new Exception("Outer: cookie2=c2;private-cookie=private", inner)); - Throwable outerProxy = SanitisingThrowableFactory.instance().create(outer, formatter); + Throwable outerProxy = ThrowableSanitiser.instance().sanitise(outer, formatter); assertThat(outerProxy.getMessage(), containsString("cookie2=c2")); assertThat(outerProxy.getMessage(), containsString("private-cookie=****")); assertThat(outerProxy.getCause().getMessage(), containsString("cookie1=c1")); @@ -84,14 +84,14 @@ public void exceptionCausesAreSanitized() { @Test public void nullMessagesAreAllowed() { Exception e = throwAndReturn(new Exception((String) null)); - Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable proxy = ThrowableSanitiser.instance().sanitise(e, formatter); assertThat(proxy.getMessage(), equalTo("java.lang.Exception: null")); } @Test public void exceptionsWithNoDefaultConstructorAreNotProxied() { Exception e = throwAndReturn(new NoDefaultConstructorException("Ooops, no default constructor")); - Throwable notProxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable notProxy = ThrowableSanitiser.instance().sanitise(e, formatter); assertThat(notProxy.getMessage(), equalTo("Ooops, no default constructor")); assertThat(notProxy.getClass().getSuperclass(), not(instanceOf(NoDefaultConstructorException.class))); // i.e. not proxied. } @@ -99,7 +99,7 @@ public void exceptionsWithNoDefaultConstructorAreNotProxied() { @Test public void exceptionsOfFinalClassAreNotProxied() { Exception e = throwAndReturn(new FinalClassException()); - Throwable notProxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable notProxy = ThrowableSanitiser.instance().sanitise(e, formatter); assertThat(notProxy.getMessage(), equalTo("This is a final class.")); assertThat(notProxy.getClass().getSuperclass(), not(instanceOf(FinalClassException.class))); // i.e. not proxied. } @@ -107,7 +107,7 @@ public void exceptionsOfFinalClassAreNotProxied() { @Test public void messagesWithInvalidCookiesAreSanitized() { Exception e = throwAndReturn(new Exception("Some cookies: cookie1=c1;secret-cookie=secret;bad-cookie=bad\u0000bad;private-cookie=private")); - Throwable proxy = SanitisingThrowableFactory.instance().create(e, formatter); + Throwable proxy = ThrowableSanitiser.instance().sanitise(e, formatter); assertThat(proxy.getMessage(), containsString("cookie1=c1")); assertThat(proxy.getMessage(), containsString("bad-cookie=bad\u0000bad")); assertThat(proxy.getMessage(), containsString("secret-cookie=****")); diff --git a/pom.xml b/pom.xml index d04852ec3d..15614456eb 100644 --- a/pom.xml +++ b/pom.xml @@ -118,7 +118,7 @@ 1.0.2 3.3.0.RELEASE 3.0.3 - 3.3.0 + 1.10.6 1.3.21 @@ -336,7 +336,7 @@ net.bytebuddy byte-buddy - 1.10.6 + ${bytebuddy.version} diff --git a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt index 17873f3aae..e06a70f283 100644 --- a/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt +++ b/system-tests/ft-suite/src/test/kotlin/com/hotels/styx/logging/HttpMessageLoggingSpec.kt @@ -15,7 +15,9 @@ */ package com.hotels.styx.logging -import ch.qos.logback.classic.Level.* +import ch.qos.logback.classic.Level.INFO +import ch.qos.logback.classic.Level.ERROR +import ch.qos.logback.classic.Level.DEBUG import com.hotels.styx.api.HttpHeaderNames.HOST import com.hotels.styx.api.HttpRequest import com.hotels.styx.api.HttpResponseStatus.BAD_REQUEST