diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index ce2fa7115261c..11dde733aa90c 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -703,6 +703,50 @@ 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`. +== Running behind a reverse proxy + +Quarkus could be accessed through proxies that additionally generate headers (e.g. `X-Forwarded-Host`) to keep +information from the client-facing side of the proxy servers that is altered or lost when they are involved. +In those scenarios, Quarkus can be configured to automatically update information like protocol, host, port and URI +reflecting the values in these headers. + +IMPORTANT: Activating this feature makes the server exposed to several security issues (i.e. information spoofing). +Consider activate it only when running behind a reverse proxy. + +To setup this feature, please include the following lines in `src/main/resources/application.properties`: +[source] +---- +quarkus.http.proxy-address-forwarding=true +---- + +To consider only de-facto standard header (`Forwarded` header), please include the following lines in `src/main/resources/application.properties`: +[source] +---- +quarkus.http.allow-forwarded=true +---- + +To consider only non-standard headers, please include the following lines instead in `src/main/resources/application.properties`: + +[source] +---- +quarkus.http.proxy-address-forwarding=true +quarkus.http.proxy.enable-forwarded-host=true +quarkus.http.proxy.enable-forwarded-prefix=true +---- + +Both configurations related with standard and non-standard headers can be combine. +In this case, the `Forwarded` header will have precedence in presence of both set of headers. + +Supported forwarding address headers are: + +* `Forwarded` +* `X-Forwarded-Proto` +* `X-Forwarded-Host` +* `X-Forwarded-Port` +* `X-Forwarded-Ssl` +* `X-Forwarded-Prefix` + + == 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/ConfigureForwardedHeadersTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ConfigureForwardedHeadersTest.java new file mode 100644 index 0000000000000..42436f95513c9 --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ConfigureForwardedHeadersTest.java @@ -0,0 +1,42 @@ +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 ConfigureForwardedHeadersTest { + + @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.proxy.enable-forwarded-host=true\n" + + "quarkus.http.proxy.enable-forwarded-prefix=true\n" + + "quarkus.http.proxy.forwarded-host-header=X-Forwarded-Server\n" + + "quarkus.http.proxy.forwarded-prefix-header=X-Envoy-Path\n"), + "application.properties")); + + @Test + public void test() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Server", "somehost") + .header("X-Envoy-Path", "/prefix") + .get("/path") + .then() + .body(Matchers.equalTo("https|somehost|backend:4444|/prefix/path|https://somehost/prefix/path")); + } + +} 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..a707aaf8ac13c 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,12 @@ 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("/path").handler(rc -> rc.response() + .end(rc.request().scheme() + + "|" + rc.request().getHeader(HttpHeaders.HOST) + + "|" + rc.request().remoteAddress().toString() + + "|" + rc.request().uri() + + "|" + rc.request().absoluteURI())); } } diff --git a/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedPrefixHeaderTest.java b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedPrefixHeaderTest.java new file mode 100644 index 0000000000000..a90875c85e04f --- /dev/null +++ b/extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/ForwardedPrefixHeaderTest.java @@ -0,0 +1,90 @@ +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 ForwardedPrefixHeaderTest { + + @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.proxy.enable-forwarded-prefix=true\n"), + "application.properties")); + + @Test + public void test() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Prefix", "/prefix") + .get("/path") + .then() + .body(Matchers.equalTo("https|localhost|backend:4444|/prefix/path|https://localhost/prefix/path")); + } + + @Test + public void testWithASlashAtEnding() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Prefix", "/prefix/") + .get("/path") + .then() + .body(Matchers.equalTo("https|localhost|backend:4444|/prefix/path|https://localhost/prefix/path")); + } + + @Test + public void testWhenPrefixIsEmpty() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Prefix", "") + .get("/path") + .then() + .body(Matchers.equalTo("https|localhost|backend:4444|/path|https://localhost/path")); + } + + @Test + public void testWhenPrefixIsASlash() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Prefix", "/") + .get("/path") + .then() + .body(Matchers.equalTo("https|localhost|backend:4444|/path|https://localhost/path")); + } + + @Test + public void testWhenPrefixIsADoubleSlash() { + assertThat(RestAssured.get("/path").asString()).startsWith("http|"); + + RestAssured.given() + .header("X-Forwarded-Proto", "https") + .header("X-Forwarded-For", "backend:4444") + .header("X-Forwarded-Prefix", "//") + .get("/path") + .then() + .body(Matchers.equalTo("https|localhost|backend:4444|/path|https://localhost/path")); + } + +} 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 ddb6223dcffed..995c72ca97cf1 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 @@ -52,6 +52,7 @@ class ForwardedParser { private String host; private int port = -1; private String scheme; + private String uri; private String absoluteURI; private SocketAddress remoteAddress; @@ -93,11 +94,19 @@ SocketAddress remoteAddress() { return remoteAddress; } + String uri() { + if (!calculated) + calculate(); + + return uri; + } + private void calculate() { calculated = true; remoteAddress = delegate.remoteAddress(); scheme = delegate.scheme(); setHostAndPort(delegate.host(), port); + uri = delegate.uri(); String forwardedSsl = delegate.getHeader(X_FORWARDED_SSL); boolean isForwardedSslOn = forwardedSsl != null && forwardedSsl.equalsIgnoreCase("on"); @@ -140,6 +149,13 @@ private void calculate() { } } + if (forwardingProxyOptions.enableForwardedPrefix) { + String prefixHeader = delegate.getHeader(forwardingProxyOptions.forwardedPrefixHeader); + if (prefixHeader != null) { + uri = appendPrefixToUri(prefixHeader, uri); + } + } + String portHeader = delegate.getHeader(X_FORWARDED_PORT); if (portHeader != null) { port = parsePort(portHeader.split(",")[0], port); @@ -157,7 +173,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.debug("Recalculated absoluteURI to " + absoluteURI); } private void setHostAndPort(String hostToParse, int defaultPort) { @@ -192,4 +209,29 @@ private int parsePort(String portToParse, int defaultPort) { return defaultPort; } } + + private String appendPrefixToUri(String prefix, String uri) { + String parsed = stripSlashes(prefix); + return parsed.isEmpty() ? uri : '/' + parsed + uri; + } + + private String stripSlashes(String uri) { + String result; + if (!uri.isEmpty()) { + int beginIndex = 0; + if (uri.startsWith("/")) { + beginIndex = 1; + } + + int endIndex = uri.length(); + if (uri.endsWith("/") && uri.length() > 1) { + 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 58aedee25785f..3499ed859b8d7 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 @@ -137,7 +137,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/ForwardingProxyOptions.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java index ad940f6887ea8..90ec58978c830 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ForwardingProxyOptions.java @@ -6,16 +6,22 @@ public class ForwardingProxyOptions { boolean proxyAddressForwarding; boolean allowForwarded; boolean enableForwardedHost; + boolean enableForwardedPrefix; AsciiString forwardedHostHeader; + AsciiString forwardedPrefixHeader; public ForwardingProxyOptions(final boolean proxyAddressForwarding, final boolean allowForwarded, final boolean enableForwardedHost, - final AsciiString forwardedHostHeader) { + final AsciiString forwardedHostHeader, + final boolean enableForwardedPrefix, + final AsciiString forwardedPrefixHeader) { this.proxyAddressForwarding = proxyAddressForwarding; this.allowForwarded = allowForwarded; this.enableForwardedHost = enableForwardedHost; + this.enableForwardedPrefix = enableForwardedPrefix; this.forwardedHostHeader = forwardedHostHeader; + this.forwardedPrefixHeader = forwardedPrefixHeader; } public static ForwardingProxyOptions from(HttpConfiguration httpConfiguration) { @@ -25,8 +31,11 @@ public static ForwardingProxyOptions from(HttpConfiguration httpConfiguration) { .orElse(httpConfiguration.proxy.allowForwarded); final boolean enableForwardedHost = httpConfiguration.proxy.enableForwardedHost; + final boolean enableForwardedPrefix = httpConfiguration.proxy.enableForwardedPrefix; + final AsciiString forwardedPrefixHeader = AsciiString.cached(httpConfiguration.proxy.forwardedPrefixHeader); final AsciiString forwardedHostHeader = AsciiString.cached(httpConfiguration.proxy.forwardedHostHeader); - return new ForwardingProxyOptions(proxyAddressForwarding, allowForwarded, enableForwardedHost, forwardedHostHeader); + return new ForwardingProxyOptions(proxyAddressForwarding, allowForwarded, enableForwardedHost, forwardedHostHeader, + enableForwardedPrefix, forwardedPrefixHeader); } } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java index 5e672c5c1116f..2d255f93e5ab5 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/ProxyConfig.java @@ -34,4 +34,15 @@ public class ProxyConfig { @ConfigItem(defaultValue = "X-Forwarded-Host") public String forwardedHostHeader; + /** + * Enable prefix the received request's path with a forwarded prefix header. + */ + @ConfigItem(defaultValue = "false") + public boolean enableForwardedPrefix; + + /** + * Configure the forwarded prefix header to be used if prefixing enabled. + */ + @ConfigItem(defaultValue = "X-Forwarded-Prefix") + public String forwardedPrefixHeader; }