From 2b19ce99f7de32237f044351b1c2ffc67b3a5acf Mon Sep 17 00:00:00 2001 From: Kirill Serebrennikov Date: Fri, 6 Sep 2019 15:13:34 +0300 Subject: [PATCH] Add support for X-Forwarded-For and Forwarded for Closes gh-23260 --- .../DefaultServerHttpRequestBuilder.java | 17 ++- .../server/reactive/ServerHttpRequest.java | 5 + .../web/filter/ForwardedHeaderFilter.java | 53 +++++++- .../adapter/ForwardedHeaderTransformer.java | 31 ++++- .../web/util/UriComponentsBuilder.java | 53 +++++++- .../filter/ForwardedHeaderFilterTests.java | 120 +++++++++++++++++- .../ForwardedHeaderTransformerTests.java | 52 ++++++++ 7 files changed, 323 insertions(+), 8 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 963f5356d614..10cdfd8380ea 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -61,6 +61,9 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { @Nullable private SslInfo sslInfo; + @Nullable + private InetSocketAddress remoteAddress; + private Flux body; private final ServerHttpRequest originalRequest; @@ -130,10 +133,17 @@ public ServerHttpRequest.Builder sslInfo(SslInfo sslInfo) { return this; } + @Override + public ServerHttpRequest.Builder remoteAddress(InetSocketAddress remoteAddress) { + this.remoteAddress = remoteAddress; + return this; + } + @Override public ServerHttpRequest build() { return new MutatedServerHttpRequest(getUriToUse(), this.contextPath, this.httpHeaders, - this.httpMethodValue, this.cookies, this.sslInfo, this.body, this.originalRequest); + this.httpMethodValue, this.cookies, this.remoteAddress, this.sslInfo, this.body, + this.originalRequest); } private URI getUriToUse() { @@ -194,12 +204,13 @@ private static class MutatedServerHttpRequest extends AbstractServerHttpRequest public MutatedServerHttpRequest(URI uri, @Nullable String contextPath, HttpHeaders headers, String methodValue, MultiValueMap cookies, - @Nullable SslInfo sslInfo, Flux body, ServerHttpRequest originalRequest) { + @Nullable InetSocketAddress remoteAddress, @Nullable SslInfo sslInfo, Flux body, + ServerHttpRequest originalRequest) { super(uri, contextPath, headers); this.methodValue = methodValue; this.cookies = cookies; - this.remoteAddress = originalRequest.getRemoteAddress(); + this.remoteAddress = remoteAddress; this.sslInfo = sslInfo != null ? sslInfo : originalRequest.getSslInfo(); this.body = body; this.originalRequest = originalRequest; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 47b2e4c128dc..e03d3b808a2d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -166,6 +166,11 @@ interface Builder { */ Builder sslInfo(SslInfo sslInfo); + /** + * Set the address of the remote client. + */ + Builder remoteAddress(InetSocketAddress remoteAddress); + /** * Build a {@link ServerHttpRequest} decorator with the mutated properties. */ diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java index 8b5f6a6d6aa2..08654cb98eb4 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.function.Supplier; +import java.util.regex.Pattern; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -32,6 +33,7 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.HttpStatus; import org.springframework.http.server.ServletServerHttpRequest; @@ -67,7 +69,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { private static final Set FORWARDED_HEADER_NAMES = - Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(6, Locale.ENGLISH)); + Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(10, Locale.ENGLISH)); static { FORWARDED_HEADER_NAMES.add("Forwarded"); @@ -76,6 +78,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto"); FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix"); FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-For"); } @@ -217,6 +220,10 @@ public Enumeration getHeaderNames() { */ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRemovingRequest { + private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For"; + private static final String FORWARDED_HEADER = "Forwarded"; + private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:^[^,]*for=.+)"); + @Nullable private final String scheme; @@ -227,6 +234,14 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem private final int port; + @Nullable + private final String remoteHost; + + @Nullable + private final String remoteAddr; + + private final int remotePort; + private final ForwardedPrefixExtractor forwardedPrefixExtractor; @@ -242,6 +257,25 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem this.host = uriComponents.getHost(); this.port = (port == -1 ? (this.secure ? 443 : 80) : port); + HttpHeaders headers = httpRequest.getHeaders(); + boolean hasForwardedFor = StringUtils.hasText(headers.getFirst(X_FORWARDED_FOR_HEADER)) || + (StringUtils.hasText(headers.getFirst(FORWARDED_HEADER)) && + FORWARDED_FOR_PATTERN.matcher(headers.getFirst(FORWARDED_HEADER)).matches()); + if (hasForwardedFor) { + UriComponents remoteUriComponents = UriComponentsBuilder.newInstance() + .host(request.getRemoteHost()) + .port(request.getRemotePort()) + .adaptFromForwardedForHeader(headers) + .build(); + this.remoteHost = remoteUriComponents.getHost(); + this.remoteAddr = this.remoteHost; + this.remotePort = remoteUriComponents.getPort(); + } else { + this.remoteHost = request.getRemoteHost(); + this.remoteAddr = request.getRemoteAddr(); + this.remotePort = request.getRemotePort(); + } + String baseUrl = this.scheme + "://" + this.host + (port == -1 ? "" : ":" + port); Supplier delegateRequest = () -> (HttpServletRequest) getRequest(); this.forwardedPrefixExtractor = new ForwardedPrefixExtractor(delegateRequest, pathHelper, baseUrl); @@ -284,6 +318,23 @@ public String getRequestURI() { public StringBuffer getRequestURL() { return this.forwardedPrefixExtractor.getRequestUrl(); } + + @Override + @Nullable + public String getRemoteHost() { + return this.remoteHost; + } + + @Override + @Nullable + public String getRemoteAddr() { + return this.remoteAddr; + } + + @Override + public int getRemotePort() { + return remotePort; + } } diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java index b3990166cb19..ee777ca7f8b4 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java @@ -16,17 +16,21 @@ package org.springframework.web.server.adapter; +import java.net.InetSocketAddress; import java.net.URI; import java.util.Collections; import java.util.Locale; import java.util.Set; import java.util.function.Function; +import java.util.regex.Pattern; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpHeaders; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; import org.springframework.util.LinkedCaseInsensitiveMap; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -49,8 +53,11 @@ */ public class ForwardedHeaderTransformer implements Function { + private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For"; + private static final String FORWARDED_HEADER = "Forwarded"; + private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:^[^,]*for=.+)"); static final Set FORWARDED_HEADER_NAMES = - Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)); + Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(10, Locale.ENGLISH)); static { FORWARDED_HEADER_NAMES.add("Forwarded"); @@ -59,6 +66,7 @@ public class ForwardedHeaderTransformer implements Function uriVariables) { return this; } + /** + * Adapt this builders's host+port from the "for" parameter of the "Forwarded" + * header or from "X-Forwarded-For" if "Forwarded" is not found. If neither + * "Forwarded" nor "X-Forwarded-For" is found no changes are made to the + * builder. + * @param headers the HTTP headers to consider + * @return this UriComponentsBuilder + */ + public UriComponentsBuilder adaptFromForwardedForHeader(HttpHeaders headers) { + String forwardedHeader = headers.getFirst("Forwarded"); + if (StringUtils.hasText(forwardedHeader)) { + String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0]; + Matcher matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse); + if (matcher.find()) { + adaptForwardedForHost(matcher.group(1).trim()); + } + } + else { + String forHeader = headers.getFirst("X-Forwarded-For"); + if (StringUtils.hasText(forHeader)) { + String forwardedForToUse = StringUtils.tokenizeToStringArray(forHeader, ",")[0]; + host(forwardedForToUse); + } + } + return this; + } + /** * Adapt this builder's scheme+host+port from the given headers, specifically * "Forwarded" (RFC 7239, @@ -918,6 +949,24 @@ private void adaptForwardedHost(String hostToUse) { } } + private void adaptForwardedForHost(String hostToUse) { + String hostName = hostToUse; + int portSeparatorIdx = hostToUse.lastIndexOf(':'); + if (portSeparatorIdx > hostToUse.lastIndexOf(']')) { + String hostPort = hostToUse.substring(portSeparatorIdx + 1); + // check if port is not obfuscated + if (hostPort.matches(FORWARDED_FOR_NUMERIC_PORT_PATTERN)) { + port(Integer.parseInt(hostPort)); + } + hostName = hostToUse.substring(0, portSeparatorIdx); + } + if (hostName.matches(HOST_IPV6_PATTERN)) { + host(hostName.substring(hostName.indexOf('[') + 1, hostName.indexOf(']'))); + } else { + host(hostName); + } + } + private void resetHierarchicalComponents() { this.userInfo = null; this.host = null; diff --git a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java index 4fb97248e02f..5387dba5d656 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java @@ -51,6 +51,8 @@ public class ForwardedHeaderFilterTests { private static final String X_FORWARDED_PORT = "x-forwarded-port"; private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix"; private static final String X_FORWARDED_SSL = "x-forwarded-ssl"; + private static final String X_FORWARDED_FOR = "x-forwarded-for"; + private static final String FORWARDED = "forwarded"; private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); @@ -70,6 +72,114 @@ public void setup() throws Exception { this.filterChain = new MockFilterChain(new HttpServlet() {}); } + @Test + public void forwardedForEmpty() throws Exception { + this.request.addHeader(X_FORWARDED_FOR, ""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_ADDR); + assertThat(actual.getRemoteHost()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_HOST); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void forwardedForSingleIdentifier() throws Exception { + this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void forwardedForMultipleIdentifiers() throws Exception { + this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195, 70.41.3.18, 150.172.238.178"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void standardizedForwardedForIpV4Identifier() throws Exception { + this.request.addHeader(FORWARDED, "for=203.0.113.195"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void standardizedForwardedForIpV6Identifier() throws Exception { + this.request.addHeader(FORWARDED, "for=\"[2001:db8:cafe::17]\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("2001:db8:cafe::17"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void standardizedForwardedForUnknownIdentifier() throws Exception { + this.request.addHeader(FORWARDED, "for=unknown"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("unknown"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void standardizedForwardedForObfuscatedIdentifier() throws Exception { + this.request.addHeader(FORWARDED, "for=_abc-12_d.e"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("_abc-12_d.e"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + public void standardizedForwardedForIpV4IdentifierWithPort() throws Exception { + this.request.addHeader(FORWARDED, "for=\"203.0.113.195:47011\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195"); + assertThat(actual.getRemotePort()).isEqualTo(47011); + } + + @Test + public void standardizedForwardedForIpV6IdentifierWithPort() throws Exception { + this.request.addHeader(FORWARDED, "For=\"[2001:db8:cafe::17]:47011\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("2001:db8:cafe::17"); + assertThat(actual.getRemotePort()).isEqualTo(47011); + } + + @Test + public void standardizedForwardedForUnknownIdentifierWithPort() throws Exception { + this.request.addHeader(FORWARDED, "for=\"unknown:47011\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("unknown"); + assertThat(actual.getRemotePort()).isEqualTo(47011); + } + + @Test + public void standardizedForwardedForObfuscatedIdentifierWithPort() throws Exception { + this.request.addHeader(FORWARDED, "for=\"_abc-12_d.e:47011\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("_abc-12_d.e"); + assertThat(actual.getRemotePort()).isEqualTo(47011); + } + + @Test + public void standardizedForwardedForMultipleIdentifiers() throws Exception { + this.request.addHeader(FORWARDED, "for=203.0.113.195;proto=http, for=\"[2001:db8:cafe::17]\", for=unknown"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195"); + assertThat(actual.getRemotePort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } @Test public void contextPathEmpty() throws Exception { @@ -220,11 +330,12 @@ public String getHeader(String header) { @Test public void shouldFilter() { - testShouldFilter("Forwarded"); + testShouldFilter(FORWARDED); testShouldFilter(X_FORWARDED_HOST); testShouldFilter(X_FORWARDED_PORT); testShouldFilter(X_FORWARDED_PROTO); testShouldFilter(X_FORWARDED_SSL); + testShouldFilter(X_FORWARDED_FOR); } private void testShouldFilter(String headerName) { @@ -245,6 +356,7 @@ public void forwardedRequest() throws Exception { this.request.addHeader(X_FORWARDED_HOST, "84.198.58.199"); this.request.addHeader(X_FORWARDED_PORT, "443"); this.request.addHeader("foo", "bar"); + this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195"); this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain); HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest(); @@ -254,10 +366,12 @@ public void forwardedRequest() throws Exception { assertThat(actual.getServerName()).isEqualTo("84.198.58.199"); assertThat(actual.getServerPort()).isEqualTo(443); assertThat(actual.isSecure()).isTrue(); + assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("203.0.113.195"); assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull(); assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull(); assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull(); + assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull(); assertThat(actual.getHeader("foo")).isEqualTo("bar"); } @@ -269,6 +383,7 @@ public void forwardedRequestInRemoveOnlyMode() throws Exception { this.request.addHeader(X_FORWARDED_PORT, "443"); this.request.addHeader(X_FORWARDED_SSL, "on"); this.request.addHeader("foo", "bar"); + this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195"); this.filter.setRemoveOnly(true); this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain); @@ -279,11 +394,14 @@ public void forwardedRequestInRemoveOnlyMode() throws Exception { assertThat(actual.getServerName()).isEqualTo("localhost"); assertThat(actual.getServerPort()).isEqualTo(80); assertThat(actual.isSecure()).isFalse(); + assertThat(actual.getRemoteAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_ADDR); + assertThat(actual.getRemoteHost()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_HOST); assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull(); assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull(); assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull(); assertThat(actual.getHeader(X_FORWARDED_SSL)).isNull(); + assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull(); assertThat(actual.getHeader("foo")).isEqualTo("bar"); } diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java index 3f5a674fe50d..53cbbfb7702d 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java @@ -16,7 +16,9 @@ package org.springframework.web.server.adapter; +import java.net.InetSocketAddress; import java.net.URI; +import java.net.URISyntaxException; import org.junit.jupiter.api.Test; @@ -51,6 +53,7 @@ public void removeOnly() { headers.add("X-Forwarded-Proto", "http"); headers.add("X-Forwarded-Prefix", "prefix"); headers.add("X-Forwarded-Ssl", "on"); + headers.add("X-Forwarded-For", "203.0.113.195"); ServerHttpRequest request = this.requestMutator.apply(getRequest(headers)); assertForwardedHeadersRemoved(request); @@ -133,6 +136,55 @@ public void shouldNotDoubleEncode() throws Exception { assertForwardedHeadersRemoved(request); } + @Test + public void noForwardedFor() throws URISyntaxException { + HttpHeaders headers = new HttpHeaders(); + headers.add("Forwarded", "host=84.198.58.199;proto=https"); + + InetSocketAddress remoteAddress = new InetSocketAddress("example.client", 47011); + + ServerHttpRequest request = MockServerHttpRequest + .method(HttpMethod.GET, new URI("https://example.com/a%20b?q=a%2Bb")) + .remoteAddress(remoteAddress) + .headers(headers) + .build(); + + request = this.requestMutator.apply(request); + assertThat(request.getRemoteAddress()).isEqualTo(remoteAddress); + } + + @Test + public void forwardedFor() throws URISyntaxException { + HttpHeaders headers = new HttpHeaders(); + headers.add("Forwarded", "for=\"203.0.113.195:4711\";host=84.198.58.199;proto=https"); + + InetSocketAddress remoteAddress = new InetSocketAddress("example.client", 47011); + + ServerHttpRequest request = MockServerHttpRequest + .method(HttpMethod.GET, new URI("https://example.com/a%20b?q=a%2Bb")) + .remoteAddress(remoteAddress) + .headers(headers) + .build(); + + request = this.requestMutator.apply(request); + assertThat(request.getRemoteAddress().getHostName()).isEqualTo("203.0.113.195"); + assertThat(request.getRemoteAddress().getPort()).isEqualTo(4711); + } + + @Test + public void xForwardedFor() throws URISyntaxException { + HttpHeaders headers = new HttpHeaders(); + headers.add("x-forwarded-for", "203.0.113.195, 70.41.3.18, 150.172.238.178"); + + ServerHttpRequest request = MockServerHttpRequest + .method(HttpMethod.GET, new URI("https://example.com/a%20b?q=a%2Bb")) + .headers(headers) + .build(); + + request = this.requestMutator.apply(request); + assertThat(request.getRemoteAddress().getHostName()).isEqualTo("203.0.113.195"); + } + private MockServerHttpRequest getRequest(HttpHeaders headers) { return MockServerHttpRequest.get(BASE_URL).headers(headers).build();