Skip to content

Commit

Permalink
Distinguish missing header from empty, handle multiple components
Browse files Browse the repository at this point in the history
  • Loading branch information
chemicL committed Sep 23, 2024
1 parent 78350be commit 3ca54a8
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public final class ConnectionInfo {

final boolean isInetAddress;

@Nullable
final String forwardedPrefix;

static ConnectionInfo from(Channel channel, HttpRequest request, boolean secured, SocketAddress remoteAddress,
Expand Down Expand Up @@ -96,11 +97,11 @@ static ConnectionInfo from(Channel channel, HttpRequest request, boolean secured

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

ConnectionInfo(SocketAddress hostAddress, String hostName, int hostPort,
SocketAddress remoteAddress, String scheme, boolean isInetAddress, String forwardedPrefix) {
SocketAddress remoteAddress, String scheme, boolean isInetAddress, @Nullable String forwardedPrefix) {
this.hostAddress = hostAddress;
this.hostName = hostName;
this.hostPort = hostPort;
Expand Down Expand Up @@ -216,6 +217,7 @@ public int getHostPort() {
* @return the X-Forwarded-Prefix
* @since 1.1.23
*/
@Nullable
public String getForwardedPrefix() {
return forwardedPrefix;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
*/
package reactor.netty.http.server;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -45,6 +49,8 @@ final class DefaultHttpForwardedHeaderHandler implements BiFunction<ConnectionIn
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 @@ -118,9 +124,34 @@ else if (DEFAULT_FORWARDED_HEADER_VALIDATION) {
}

String prefixHeader = request.headers().get(X_FORWARDED_PREFIX_HEADER);
if (prefixHeader != null && !prefixHeader.isEmpty()) {
connectionInfo = connectionInfo.withForwardedPrefix(prefixHeader);
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));
}
return prefix.toString();
}

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 @@ -164,5 +164,6 @@ default Flux<HttpContent> receiveContent() {
* @return the X-Forwarded-Prefix
* @since 1.1.23
*/
@Nullable
String forwardedPrefix();
}
Original file line number Diff line number Diff line change
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,7 +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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -235,7 +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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -252,7 +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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -268,7 +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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -286,7 +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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -304,7 +305,7 @@ 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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -322,17 +323,48 @@ void xForwardedPrefix(boolean useCustomForwardedHandler) {
useCustomForwardedHandler);
}

@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 -> {
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);
}

@ParameterizedTest(name = "{displayName}({arguments})")
@ValueSource(booleans = {true, false})
void xForwardedMultipleHeaders(boolean useCustomForwardedHandler) {
Expand Down Expand Up @@ -371,7 +403,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
getForwardedHandler(useCustomForwardedHandler),
httpClient -> httpClient,
Expand All @@ -392,7 +424,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
getForwardedHandler(useCustomForwardedHandler),
httpClient -> httpClient,
Expand All @@ -416,7 +448,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -438,7 +470,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -460,7 +492,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand All @@ -486,7 +518,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
getForwardedHandler(useCustomForwardedHandler),
httpClient -> httpClient.secure(ssl -> ssl.sslContext(clientSslContext)),
Expand All @@ -509,7 +541,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()).isEqualTo("");
Assertions.assertThat(serverRequest.forwardedPrefix()).isNull();
},
useCustomForwardedHandler);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ private ConnectionInfo parseXForwardedInfo(ConnectionInfo connectionInfo, HttpRe
}

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

Expand Down

0 comments on commit 3ca54a8

Please sign in to comment.