Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable forwarded for prefix header configuration #10266

Merged
merged 1 commit into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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`
gsmet marked this conversation as resolved.
Show resolved Hide resolved


== 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;
}