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 502f01b7b2..267ff5bd76 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 @@ -57,6 +57,9 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { @Nullable private SslInfo sslInfo; + @Nullable + private InetSocketAddress remoteAddress; + private Flux body; private final ServerHttpRequest originalRequest; @@ -68,6 +71,7 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { this.uri = original.getURI(); this.headers = HttpHeaders.writableHttpHeaders(original.getHeaders()); this.httpMethodValue = original.getMethodValue(); + this.remoteAddress = original.getRemoteAddress(); this.body = original.getBody(); this.originalRequest = original; } @@ -117,10 +121,16 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { 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.httpMethodValue, this.sslInfo, this.body, this.originalRequest); + this.httpMethodValue, this.sslInfo, this.remoteAddress, this.body, this.originalRequest); } private URI getUriToUse() { @@ -169,18 +179,22 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { @Nullable private final SslInfo sslInfo; + @Nullable + private InetSocketAddress remoteAddress; + private final Flux body; private final ServerHttpRequest originalRequest; public MutatedServerHttpRequest(URI uri, @Nullable String contextPath, - String methodValue, @Nullable SslInfo sslInfo, + String methodValue, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress, Flux body, ServerHttpRequest originalRequest) { super(uri, contextPath, originalRequest.getHeaders()); this.methodValue = methodValue; - this.sslInfo = sslInfo != null ? sslInfo : originalRequest.getSslInfo(); + this.remoteAddress = (remoteAddress != null ? remoteAddress : originalRequest.getRemoteAddress()); + this.sslInfo = (sslInfo != null ? sslInfo : originalRequest.getSslInfo()); this.body = body; this.originalRequest = originalRequest; } @@ -204,7 +218,7 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { @Override @Nullable public InetSocketAddress getRemoteAddress() { - return this.originalRequest.getRemoteAddress(); + return this.remoteAddress; } @Override 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 a7673c3f47..9995cdefad 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 @@ -175,6 +175,12 @@ public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage */ Builder sslInfo(SslInfo sslInfo); + /** + * Set the address of the remote client. + * @since 5.3 + */ + 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 8555446aa7..ddafbe2cf6 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.Locale; 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.HttpServletRequestWrapper; 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 @@ import org.springframework.web.util.UrlPathHelper; 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 class ForwardedHeaderFilter extends OncePerRequestFilter { */ 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 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { 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 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { 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 class ForwardedHeaderFilter extends OncePerRequestFilter { 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 17d2cf00c8..34ac8ef8bc 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,11 +16,13 @@ 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; @@ -28,6 +30,7 @@ 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; /** @@ -50,8 +53,11 @@ import org.springframework.web.util.UriComponentsBuilder; */ 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"); @@ -60,6 +66,7 @@ public class ForwardedHeaderTransformer implements FunctionRFC 7239, @@ -809,6 +840,24 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable { } } + 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 be753121d5..d5afe96f30 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 @@ -55,6 +55,8 @@ public class ForwardedHeaderFilterTests { 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(); @@ -74,6 +76,114 @@ public class ForwardedHeaderFilterTests { 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 { @@ -224,11 +334,12 @@ public class ForwardedHeaderFilterTests { @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) { @@ -249,6 +360,7 @@ public class ForwardedHeaderFilterTests { 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(); @@ -258,10 +370,12 @@ public class ForwardedHeaderFilterTests { 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"); } @@ -273,6 +387,7 @@ public class ForwardedHeaderFilterTests { 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); @@ -283,11 +398,14 @@ public class ForwardedHeaderFilterTests { 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 a2b235432a..5149db28b9 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; @@ -35,10 +37,8 @@ public class ForwardedHeaderTransformerTests { private static final String BASE_URL = "https://example.com/path"; - private final ForwardedHeaderTransformer requestMutator = new ForwardedHeaderTransformer(); - @Test void removeOnly() { this.requestMutator.setRemoveOnly(true); @@ -50,6 +50,7 @@ public class ForwardedHeaderTransformerTests { 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); @@ -154,6 +155,55 @@ public class ForwardedHeaderTransformerTests { 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();