Skip to content

Commit

Permalink
Merge #3436 into 1.2.0-RC1
Browse files Browse the repository at this point in the history
  • Loading branch information
chemicL committed Oct 1, 2024
2 parents 2e687fc + 05b30b8 commit d869001
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 4 deletions.
1 change: 1 addition & 0 deletions reactor-netty-http/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ task japicmp(type: JapicmpTask) {

compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ]
methodExcludes = [
"reactor.netty.http.server.HttpServerRequest#forwardedPrefix()"
]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ public final class ConnectionInfo {

final boolean isInetAddress;

@Nullable
final String forwardedPrefix;

static ConnectionInfo from(HttpRequest request, boolean secured, SocketAddress localAddress, SocketAddress remoteAddress,
@Nullable BiFunction<ConnectionInfo, HttpRequest, ConnectionInfo> forwardedHeaderHandler) {
String hostName = DEFAULT_HOST_NAME;
Expand Down Expand Up @@ -93,12 +96,18 @@ static ConnectionInfo from(HttpRequest request, boolean secured, SocketAddress l

ConnectionInfo(SocketAddress hostAddress, String hostName, int hostPort,
SocketAddress remoteAddress, String scheme, boolean isInetAddress) {
this(hostAddress, hostName, hostPort, remoteAddress, scheme, isInetAddress, null);
}

ConnectionInfo(SocketAddress hostAddress, String hostName, int hostPort,
SocketAddress remoteAddress, String scheme, boolean isInetAddress, @Nullable String forwardedPrefix) {
this.hostAddress = hostAddress;
this.hostName = hostName;
this.hostPort = hostPort;
this.isInetAddress = isInetAddress;
this.remoteAddress = remoteAddress;
this.scheme = scheme;
this.forwardedPrefix = forwardedPrefix;
}

/**
Expand Down Expand Up @@ -172,6 +181,18 @@ public ConnectionInfo withScheme(String scheme) {
return new ConnectionInfo(this.hostAddress, this.hostName, this.hostPort, this.remoteAddress, scheme, this.isInetAddress);
}

/**
* Return a new {@link ConnectionInfo} with the forwardedPrefix set.
* @param forwardedPrefix the prefix provided via X-Forwarded-Prefix header
* @return a new {@link ConnectionInfo}
* @since 1.1.23
*/
public ConnectionInfo withForwardedPrefix(String forwardedPrefix) {
requireNonNull(forwardedPrefix, "forwardedPrefix");
return new ConnectionInfo(this.hostAddress, this.hostName, this.hostPort, this.remoteAddress, this.scheme,
this.isInetAddress, forwardedPrefix);
}

/**
* Returns the connection host name.
* @return the connection host name
Expand All @@ -190,6 +211,16 @@ public int getHostPort() {
return hostPort != -1 ? hostPort : getDefaultHostPort(scheme);
}

/**
* Returns the X-Forwarded-Prefix if it was part of the request headers.
* @return the X-Forwarded-Prefix
* @since 1.1.23
*/
@Nullable
public String getForwardedPrefix() {
return forwardedPrefix;
}

/**
* Returns the default host port number based on scheme.
* @param scheme a connection scheme like "http", "https", or "wss"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved.
* Copyright (c) 2020-2024 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,6 +15,8 @@
*/
package reactor.netty.http.server;

import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -39,11 +41,14 @@ final class DefaultHttpForwardedHeaderHandler implements BiFunction<ConnectionIn
static final String X_FORWARDED_HOST_HEADER = "X-Forwarded-Host";
static final String X_FORWARDED_PORT_HEADER = "X-Forwarded-Port";
static final String X_FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
static final String X_FORWARDED_PREFIX_HEADER = "X-Forwarded-Prefix";

static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("for=\"?([^;,\"]+)\"?");

private static final String[] EMPTY_STRING_ARRAY = {};

/**
* Specifies whether the Http Server applies a strict {@code Forwarded} header validation.
* By default, it is enabled and strict validation is used.
Expand Down Expand Up @@ -115,6 +120,40 @@ else if (DEFAULT_FORWARDED_HEADER_VALIDATION) {
throw new IllegalArgumentException("Failed to parse a port from " + portHeader);
}
}

String prefixHeader = request.headers().get(X_FORWARDED_PREFIX_HEADER);
if (prefixHeader != null) {
connectionInfo = connectionInfo.withForwardedPrefix(parseForwardedPrefix(prefixHeader));
}
return connectionInfo;
}

private static String parseForwardedPrefix(String prefixHeader) {
StringBuilder prefix = new StringBuilder(prefixHeader.length());
String[] rawPrefixes = tokenizeToStringArray(prefixHeader);
for (String rawPrefix : rawPrefixes) {
int endIndex = rawPrefix.length();
while (endIndex > 1 && rawPrefix.charAt(endIndex - 1) == '/') {
endIndex--;
}
prefix.append((endIndex != rawPrefix.length() ? rawPrefix.substring(0, endIndex) : rawPrefix));
}
String parsedPrefix = prefix.toString();
if (!parsedPrefix.isEmpty() && DEFAULT_FORWARDED_HEADER_VALIDATION && parsedPrefix.charAt(0) != '/') {
throw new IllegalArgumentException("X-Forwarded-Prefix did not start with a slash (\"/\"): " + prefixHeader);
}
return parsedPrefix;
}

private static String[] tokenizeToStringArray(String str) {
StringTokenizer st = new StringTokenizer(str, ",");
ArrayList<String> tokens = new ArrayList<>();
while (st.hasMoreTokens()) {
String token = st.nextToken().trim();
if (!token.isEmpty()) {
tokens.add(token);
}
}
return !tokens.isEmpty() ? tokens.toArray(EMPTY_STRING_ARRAY) : EMPTY_STRING_ARRAY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,11 @@ public ZonedDateTime timestamp() {
return timestamp;
}

@Override
public String forwardedPrefix() {
return connectionInfo.getForwardedPrefix();
}

@Override
@SuppressWarnings("unchecked")
public NettyOutbound send(Publisher<? extends ByteBuf> source) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2023 VMware, Inc. or its affiliates, All Rights Reserved.
* Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -158,4 +158,12 @@ default Flux<HttpContent> receiveContent() {
* @since 1.0.28
*/
ZonedDateTime timestamp();

/**
* Returns the X-Forwarded-Prefix if it was part of the request headers.
* @return the X-Forwarded-Prefix
* @since 1.1.23
*/
@Nullable
String forwardedPrefix();
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2023 VMware, Inc. or its affiliates, All Rights Reserved.
* Copyright (c) 2018-2024 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -42,6 +42,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import reactor.core.publisher.Mono;
Expand Down Expand Up @@ -218,6 +219,7 @@ void xForwardedFor(boolean useCustomForwardedHandler) {
serverRequest -> {
Assertions.assertThat(serverRequest.remoteAddress().getHostString()).isEqualTo("1abc:2abc:3abc:0:0:0:5abc:6abc");
Assertions.assertThat(serverRequest.remoteAddress().getPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -234,6 +236,7 @@ void xForwardedHost(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(port);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("1abc:2abc:3abc:0:0:0:5abc:6abc");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(port);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -250,6 +253,7 @@ void xForwardedHostEmptyHostHeader(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(port);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("1abc:2abc:3abc:0:0:0:5abc:6abc");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(port);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -265,6 +269,7 @@ void xForwardedHostPortIncluded(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(9090);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("1abc:2abc:3abc:0:0:0:5abc:6abc");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(9090);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -282,6 +287,7 @@ void xForwardedHostAndPort(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("192.168.0.1");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -299,6 +305,78 @@ void xForwardedHostPortIncludedAndXForwardedPort(boolean useCustomForwardedHandl
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("192.168.0.1");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}

@ParameterizedTest(name = "{displayName}({arguments})")
@ValueSource(booleans = {true, false})
void xForwardedPrefix(boolean useCustomForwardedHandler) {
testClientRequest(
clientRequestHeaders -> {
clientRequestHeaders.add("X-Forwarded-Prefix", "/test-prefix");
},
serverRequest -> {
Assertions.assertThat(serverRequest.forwardedPrefix()).isEqualTo("/test-prefix");
},
useCustomForwardedHandler);
}

@Test
void xForwardedPrefixWithoutForwardSlash() {
testClientRequest(
clientRequestHeaders -> {
clientRequestHeaders.add("X-Forwarded-Prefix", "forward-slash-missing");
},
serverRequest -> {

},
null,
httpClient -> httpClient,
httpServer -> httpServer.port(8080),
false,
true);
}

@ParameterizedTest
@CsvSource(value = {
"/first,/second | /first/second",
"/first,/second/ | /first/second",
"/first/,/second/ | /first/second",
"/first/,/second// | /first/second"
}, delimiter = '|')
void xForwardedPrefixDelimited(String input, String output) {
testClientRequest(
clientRequestHeaders -> {
clientRequestHeaders.add("X-Forwarded-Prefix", input);
},
serverRequest -> {
Assertions.assertThat(serverRequest.forwardedPrefix()).isEqualTo(output);
},
false);
}

@ParameterizedTest(name = "{displayName}({arguments})")
@ValueSource(booleans = {true, false})
void xForwardedPrefixEmpty(boolean useCustomForwardedHandler) {
testClientRequest(
clientRequestHeaders -> {
clientRequestHeaders.add("X-Forwarded-Prefix", "");
},
serverRequest -> {
Assertions.assertThat(serverRequest.forwardedPrefix()).isEqualTo("");
},
useCustomForwardedHandler);
}

@ParameterizedTest(name = "{displayName}({arguments})")
@ValueSource(booleans = {true, false})
void xForwardedPrefixMissing(boolean useCustomForwardedHandler) {
testClientRequest(
clientRequestHeaders -> {},
serverRequest -> {
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -314,13 +392,15 @@ void xForwardedMultipleHeaders(boolean useCustomForwardedHandler) {
clientRequestHeaders.add("X-Forwarded-Port", "8081");
clientRequestHeaders.add("X-Forwarded-Proto", "http");
clientRequestHeaders.add("X-Forwarded-Proto", "https");
clientRequestHeaders.add("X-Forwarded-Prefix", "/test-prefix");
},
serverRequest -> {
Assertions.assertThat(serverRequest.hostAddress().getHostString()).isEqualTo("192.168.0.1");
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("192.168.0.1");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.scheme()).isEqualTo("http");
Assertions.assertThat(serverRequest.forwardedPrefix()).isEqualTo("/test-prefix");
},
useCustomForwardedHandler);
}
Expand All @@ -339,6 +419,7 @@ void xForwardedHostAndEmptyPort(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(port);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("192.168.0.1");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(port);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
getForwardedHandler(useCustomForwardedHandler),
httpClient -> httpClient,
Expand All @@ -359,6 +440,7 @@ void xForwardedHostAndNonNumericPort(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("192.168.0.1");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
getForwardedHandler(useCustomForwardedHandler),
httpClient -> httpClient,
Expand All @@ -382,6 +464,7 @@ void xForwardedForHostAndPort(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostAddress().getPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("a.example.com");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -403,6 +486,7 @@ void xForwardedForHostAndPortAndProto(boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostName()).isEqualTo("a.example.com");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.scheme()).isEqualTo("http");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -424,6 +508,7 @@ void xForwardedForMultipleHostAndPortAndProto(boolean useCustomForwardedHandler)
Assertions.assertThat(serverRequest.hostName()).isEqualTo("a.example.com");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8080);
Assertions.assertThat(serverRequest.scheme()).isEqualTo("http");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -449,6 +534,7 @@ void xForwardedForAndPortOnly(boolean useCustomForwardedHandler) throws SSLExcep
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(8443);
Assertions.assertThat(serverRequest.hostName()).isEqualTo("a.example.com");
Assertions.assertThat(serverRequest.scheme()).isEqualTo("https");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
getForwardedHandler(useCustomForwardedHandler),
httpClient -> httpClient.secure(ssl -> ssl.sslContext(clientSslContext)),
Expand All @@ -471,6 +557,7 @@ void xForwardedProtoOnly(String protocol, boolean useCustomForwardedHandler) {
Assertions.assertThat(serverRequest.hostName()).isEqualTo("a.example.com");
Assertions.assertThat(serverRequest.hostPort()).isEqualTo(getDefaultHostPort(protocol));
Assertions.assertThat(serverRequest.scheme()).isEqualTo(protocol);
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand Down
Loading

0 comments on commit d869001

Please sign in to comment.