Skip to content

Commit

Permalink
Merge pull request #10266 from ejba/x-forwarded-prefix
Browse files Browse the repository at this point in the history
Enable forwarded for prefix header configuration
  • Loading branch information
stuartwdouglas authored Jul 10, 2020
2 parents 0e0457c + 2935fbc commit af0996b
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 4 deletions.
44 changes: 44 additions & 0 deletions docs/src/main/asciidoc/vertx.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}

}
Original file line number Diff line number Diff line change
@@ -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"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public String rawMethod() {
@Override
public String uri() {
if (!modified) {
return delegate.uri();
return forwardedParser.uri();
}
return uri;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit af0996b

Please sign in to comment.