From a2031a2a7a4fea2f75cb99ac7a3fe5c5e5cc2932 Mon Sep 17 00:00:00 2001 From: Emanuel Alves Date: Tue, 16 Jun 2020 09:10:10 +0100 Subject: [PATCH] Add forwarded-host-header and forwarded-prefix-header configuration --- docs/src/main/asciidoc/vertx.adoc | 17 +++++ .../quarkus/vertx/http/EnvoyHeaderTest.java | 41 ++++++++++++ .../vertx/http/ForwardedForHeaderTest.java | 2 +- .../http/ForwardedForPrefixHeaderTest.java | 41 ++++++++++++ .../http/ForwardedForServerHeaderTest.java | 38 +++++++++++ .../http/ForwardedHandlerInitializer.java | 4 ++ ...yAddressForwardingDefaultSettingsTest.java | 46 ++++++++++++++ .../vertx/http/runtime/ForwardedParser.java | 63 +++++++++++++++++-- .../ForwardedServerRequestWrapper.java | 14 ++++- .../vertx/http/runtime/HttpConfiguration.java | 12 ++++ .../vertx/http/runtime/VertxHttpRecorder.java | 6 +- 11 files changed, 273 insertions(+), 11 deletions(-) create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/EnvoyHeaderTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForPrefixHeaderTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForServerHeaderTest.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ProxyAddressForwardingDefaultSettingsTest.java diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index ce2fa7115261c..56173a5102408 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -703,6 +703,23 @@ java.lang.IllegalStateException: Failed to create cache dir Assuming `/tmp/` is writeable this can be fixed by setting the `vertx.cacheDirBase` property to point to a directory in `/tmp/` for instance in OpenShift by creating an environment variable `JAVA_OPTS` with the value `-Dvertx.cacheDirBase=/tmp/vertx`. +== Proxy handling + +In case quarkus is called through a proxy and it shall behave like it is running on that proxy host, please set the properties + +---- +quarkus.http.proxy-address-forwarding=true <1> +quarkus.http.forwarded-host-header=X-Forwarded-Host <2> +quarkus.http.forwarded-prefix-header=X-Forwarded-Prefix <3> +---- + +<1> This settings allows the replacement of absolute URI using X-Forwarded-* headers.<2> +<2> Defines the request header quarkus shall replace its internal host with (Disabled by default). +<3> Defines the request header quarkus shall prefix the request's URI (Disabled by default). + +An example use case is the authentication handling with the quarkus OIDC extension behind a proxy. +Without these settings, quarkus sends the wrong redirect to the authentication server. + == Going further There are many other facets of Quarkus using Vert.x underneath: diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/EnvoyHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/EnvoyHeaderTest.java new file mode 100644 index 0000000000000..6564eeee05239 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/EnvoyHeaderTest.java @@ -0,0 +1,41 @@ +package io.quarkus.vertx.http; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class EnvoyHeaderTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy-address-forwarding=true\n" + + "quarkus.http.forwarded-prefix-header=X-Envoy-Original-Path\n"), + "application.properties")); + + @Test + public void test() { + RestAssured.given() + .header("X-Envoy-Original-Path", "prefix") + .get("/uri") + .then() + .body(Matchers.equalTo("/prefix/uri")); + } + + @Test + public void testWithEmptyPrefix() { + RestAssured.given() + .header("X-Envoy-Original-Path", "") + .get("/uri") + .then() + .body(Matchers.equalTo("/uri")); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForHeaderTest.java index 9e94592d18eef..0ba6439e1917f 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForHeaderTest.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForHeaderTest.java @@ -31,7 +31,7 @@ public void test() { .header("X-Forwarded-Host", "somehost") .get("/forward") .then() - .body(Matchers.equalTo("https|somehost|backend:4444")); + .body(Matchers.equalTo("https|localhost|backend:4444")); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForPrefixHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForPrefixHeaderTest.java new file mode 100644 index 0000000000000..67f225170fec8 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForPrefixHeaderTest.java @@ -0,0 +1,41 @@ +package io.quarkus.vertx.http; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ForwardedForPrefixHeaderTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy-address-forwarding=true\n" + + "quarkus.http.forwarded-prefix-header=X-Forwarded-Prefix\n"), + "application.properties")); + + @Test + public void testWithPrefix() { + RestAssured.given() + .header("X-Forwarded-Prefix", "prefix") + .get("/uri") + .then() + .body(Matchers.equalTo("/prefix/uri")); + } + + @Test + public void testWithEmptyPrefix() { + RestAssured.given() + .header("X-Forwarded-Prefix", "") + .get("/uri") + .then() + .body(Matchers.equalTo("/uri")); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForServerHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForServerHeaderTest.java new file mode 100644 index 0000000000000..08b514b97c1c4 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedForServerHeaderTest.java @@ -0,0 +1,38 @@ +package io.quarkus.vertx.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ForwardedForServerHeaderTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy-address-forwarding=true\n" + + "quarkus.http.forwarded-host-header=X-Forwarded-Server\n"), + "application.properties")); + + @Test + public void test() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .get("/forward") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444")); + } + +} diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java index 095b257b885c3..36f4b717f3250 100644 --- a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedHandlerInitializer.java @@ -13,6 +13,10 @@ public void register(@Observes Router router) { router.route("/forward").handler(rc -> rc.response() .end(rc.request().scheme() + "|" + rc.request().getHeader(HttpHeaders.HOST) + "|" + rc.request().remoteAddress().toString())); + + router.route("/uri").handler(rc -> rc.response() + .end(rc.request().uri())); + } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ProxyAddressForwardingDefaultSettingsTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ProxyAddressForwardingDefaultSettingsTest.java new file mode 100644 index 0000000000000..383daf4a0cd5b --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ProxyAddressForwardingDefaultSettingsTest.java @@ -0,0 +1,46 @@ +package io.quarkus.vertx.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hamcrest.Matchers; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class ProxyAddressForwardingDefaultSettingsTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(ForwardedHandlerInitializer.class) + .addAsResource(new StringAsset("quarkus.http.proxy-address-forwarding=true\n"), + "application.properties")); + + @Test + public void testWithHostHeader() { + assertThat(RestAssured.get("/forward").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Host", "somehost") + .header("X-Forwarded-Prefix", "prefix") + .get("/forward") + .then() + .body(Matchers.equalTo("https|localhost|backend:4444")); + } + + @Test + public void testWithPrefixHeader() { + RestAssured.given() + .header("X-Forwarded-Prefix", "prefix") + .get("/uri") + .then() + .body(Matchers.equalTo("/uri")); + } +} diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java index a9435b5764bcd..c5320cbe9bf41 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedParser.java @@ -19,6 +19,7 @@ package io.quarkus.vertx.http.runtime; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -38,7 +39,6 @@ class ForwardedParser { private static final AsciiString FORWARDED = AsciiString.cached("Forwarded"); private static final AsciiString X_FORWARDED_SSL = AsciiString.cached("X-Forwarded-Ssl"); private static final AsciiString X_FORWARDED_PROTO = AsciiString.cached("X-Forwarded-Proto"); - private static final AsciiString X_FORWARDED_HOST = AsciiString.cached("X-Forwarded-Host"); private static final AsciiString X_FORWARDED_PORT = AsciiString.cached("X-Forwarded-Port"); private static final AsciiString X_FORWARDED_FOR = AsciiString.cached("X-Forwarded-For"); @@ -48,17 +48,25 @@ class ForwardedParser { private final HttpServerRequest delegate; private final boolean allowForward; + private final Optional forwardedHostHeader; + private final Optional forwardedPrefixHeader; private boolean calculated; private String host; private int port = -1; private String scheme; + private String uri; private String absoluteURI; private SocketAddress remoteAddress; - ForwardedParser(HttpServerRequest delegate, boolean allowForward) { + ForwardedParser(HttpServerRequest delegate, + boolean allowForward, + Optional forwardedHostHeader, + Optional forwardedPrefixHeader) { this.delegate = delegate; this.allowForward = allowForward; + this.forwardedHostHeader = forwardedHostHeader; + this.forwardedPrefixHeader = forwardedPrefixHeader; } public String scheme() { @@ -80,6 +88,13 @@ boolean isSSL() { return scheme.equals(HTTPS_SCHEME); } + String uri() { + if (!calculated) + calculate(); + + return uri; + } + String absoluteURI() { if (!calculated) calculate(); @@ -98,6 +113,7 @@ private void calculate() { calculated = true; remoteAddress = delegate.remoteAddress(); scheme = delegate.scheme(); + uri = delegate.uri(); setHostAndPort(delegate.host(), port); String forwardedSsl = delegate.getHeader(X_FORWARDED_SSL); @@ -134,9 +150,11 @@ private void calculate() { port = -1; } - String hostHeader = delegate.getHeader(X_FORWARDED_HOST); - if (hostHeader != null) { - setHostAndPort(hostHeader.split(",")[0], port); + if (forwardedHostHeader.isPresent()) { + String hostHeader = delegate.getHeader(forwardedHostHeader.get()); + if (hostHeader != null) { + setHostAndPort(hostHeader.split(",")[0], port); + } } String portHeader = delegate.getHeader(X_FORWARDED_PORT); @@ -148,6 +166,13 @@ private void calculate() { if (forHeader != null) { remoteAddress = parseFor(forHeader.split(",")[0], remoteAddress.port()); } + + if (forwardedPrefixHeader.isPresent()) { + String prefixHeader = delegate.getHeader(AsciiString.cached(forwardedPrefixHeader.get())); + if (prefixHeader != null) { + uri = appendPrefixToUri(prefixHeader, uri); + } + } } if (((scheme.equals(HTTP_SCHEME) && port == 80) || (scheme.equals(HTTPS_SCHEME) && port == 443))) { @@ -156,7 +181,8 @@ private void calculate() { host = host + (port >= 0 ? ":" + port : ""); delegate.headers().set(HttpHeaders.HOST, host); - absoluteURI = scheme + "://" + host + delegate.uri(); + absoluteURI = scheme + "://" + host + uri; + log.debugf("Recalculated absoluteURI to %s", absoluteURI); } private void setHostAndPort(String hostToParse, int defaultPort) { @@ -191,4 +217,29 @@ private int parsePort(String portToParse, int defaultPort) { return defaultPort; } } + + private String appendPrefixToUri(String prefix, String uri) { + String parsed = stripSlash(prefix); + return parsed.isEmpty() ? uri : '/' + parsed + uri; + } + + private String stripSlash(String uri) { + String result; + if (!uri.isEmpty()) { + int beginIndex = 0; + if (uri.startsWith("/")) { + beginIndex = 1; + } + + int endIndex = uri.length(); + if (uri.endsWith("/")) { + endIndex = uri.length() - 1; + } + result = uri.substring(beginIndex, endIndex); + } else { + result = uri; + } + + return result; + } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java index 2475f42c608ff..ae3104d32432d 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardedServerRequestWrapper.java @@ -1,6 +1,7 @@ package io.quarkus.vertx.http.runtime; import java.util.Map; +import java.util.Optional; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; @@ -35,9 +36,16 @@ class ForwardedServerRequestWrapper implements HttpServerRequest { private String uri; private String absoluteURI; - ForwardedServerRequestWrapper(HttpServerRequest request, boolean allowForwarded) { + ForwardedServerRequestWrapper(HttpServerRequest request, + boolean allowForwarded, + Optional forwardedHostHeader, + Optional forwardedPrefixHeader) { delegate = request; - forwardedParser = new ForwardedParser(delegate, allowForwarded); + + forwardedParser = new ForwardedParser(delegate, + allowForwarded, + forwardedHostHeader, + forwardedPrefixHeader); } void changeTo(HttpMethod method, String uri) { @@ -137,7 +145,7 @@ public String rawMethod() { @Override public String uri() { if (!modified) { - return delegate.uri(); + return forwardedParser.uri(); } return uri; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java index 7a9ff48a56cf3..c5acd143888e7 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/HttpConfiguration.java @@ -197,6 +197,18 @@ public class HttpConfiguration { @ConfigItem public Map sameSiteCookie; + /** + * Allows replace the internal host with a header (disabled by default). + */ + @ConfigItem + public Optional forwardedHostHeader; + + /** + * Allows use a header to prefix the request's URI (disabled by default). + */ + @ConfigItem + public Optional forwardedPrefixHeader; + public int determinePort(LaunchMode launchMode) { return launchMode == LaunchMode.TEST ? testPort : port; } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java index e0f96ab7b8dc5..90d512cf91b2e 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/VertxHttpRecorder.java @@ -300,7 +300,11 @@ public void handle(Void e) { root = new Handler() { @Override public void handle(HttpServerRequest event) { - delegate.handle(new ForwardedServerRequestWrapper(event, httpConfiguration.allowForwarded)); + delegate.handle(new ForwardedServerRequestWrapper( + event, + httpConfiguration.allowForwarded, + httpConfiguration.forwardedHostHeader, + httpConfiguration.forwardedPrefixHeader)); } }; }