diff --git a/spring-web/src/main/java/org/springframework/web/filter/reactive/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/reactive/ForwardedHeaderFilter.java
index 43c8b32f76c5d7da17a17ea1af994fc879ffc8f1..4416d1f2e8ce167623502ddfe70745d3d477b90e 100644
--- a/spring-web/src/main/java/org/springframework/web/filter/reactive/ForwardedHeaderFilter.java
+++ b/spring-web/src/main/java/org/springframework/web/filter/reactive/ForwardedHeaderFilter.java
@@ -16,115 +16,42 @@
package org.springframework.web.filter.reactive;
-import java.net.URI;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
import reactor.core.publisher.Mono;
-import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
-import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
-import org.springframework.web.util.UriComponentsBuilder;
+import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
/**
- * Extract values from "Forwarded" and "X-Forwarded-*" headers, and use them to
- * override {@link ServerHttpRequest#getURI()} to reflect the client-originated
- * protocol and address.
+ * Extract values from "Forwarded" and "X-Forwarded-*" headers to override the
+ * request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects the
+ * client-originated protocol and address.
*
- *
This filter can also be used in a {@link #setRemoveOnly removeOnly} mode
- * where "Forwarded" and "X-Forwarded-*" headers are eliminated, and not used.
+ *
Alternatively if {@link #setRemoveOnly removeOnly} is set to "true", then
+ * "Forwarded" and "X-Forwarded-*" headers are only removed, and not used.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @deprecated as of 5.1 this filter is deprecated in favor of using
+ * {@link ForwardedHeaderTransformer} which can be declared as a bean with the
+ * name "forwardedHeaderTransformer" or registered explicitly in
+ * {@link org.springframework.web.server.adapter.WebHttpHandlerBuilder
+ * WebHttpHandlerBuilder}.
* @since 5.0
* @see https://tools.ietf.org/html/rfc7239
*/
-public class ForwardedHeaderFilter implements WebFilter {
-
- static final Set FORWARDED_HEADER_NAMES = new LinkedHashSet<>(5);
-
- static {
- FORWARDED_HEADER_NAMES.add("Forwarded");
- FORWARDED_HEADER_NAMES.add("X-Forwarded-Host");
- FORWARDED_HEADER_NAMES.add("X-Forwarded-Port");
- FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
- FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
- FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
- }
-
-
- private boolean removeOnly;
-
-
- /**
- * Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are
- * removed only and the information in them ignored.
- * @param removeOnly whether to discard and ignore forwarded headers
- */
- public void setRemoveOnly(boolean removeOnly) {
- this.removeOnly = removeOnly;
- }
-
+@Deprecated
+public class ForwardedHeaderFilter extends ForwardedHeaderTransformer implements WebFilter {
@Override
public Mono filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
- if (!hasForwardedHeaders(request)) {
- return chain.filter(exchange);
- }
-
- ServerWebExchange mutatedExchange;
- if (this.removeOnly) {
- mutatedExchange = exchange.mutate().request(this::removeForwardedHeaders).build();
- }
- else {
- mutatedExchange = exchange.mutate()
- .request(builder -> {
- URI uri = UriComponentsBuilder.fromHttpRequest(request).build().toUri();
- builder.uri(uri);
- String prefix = getForwardedPrefix(request);
- if (prefix != null) {
- builder.path(prefix + uri.getPath());
- builder.contextPath(prefix);
- }
- removeForwardedHeaders(builder);
- })
- .build();
- }
-
- return chain.filter(mutatedExchange);
- }
-
- private boolean hasForwardedHeaders(ServerHttpRequest request) {
- HttpHeaders headers = request.getHeaders();
- for (String headerName : FORWARDED_HEADER_NAMES) {
- if (headers.containsKey(headerName)) {
- return true;
- }
+ if (hasForwardedHeaders(request)) {
+ exchange = exchange.mutate().request(apply(request)).build();
}
- return false;
- }
-
- @Nullable
- private static String getForwardedPrefix(ServerHttpRequest request) {
- HttpHeaders headers = request.getHeaders();
- String prefix = headers.getFirst("X-Forwarded-Prefix");
- if (prefix != null) {
- int endIndex = prefix.length();
- while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') {
- endIndex--;
- }
- prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix);
- }
- return prefix;
- }
-
- private ServerHttpRequest.Builder removeForwardedHeaders(ServerHttpRequest.Builder builder) {
- return builder.headers(map -> FORWARDED_HEADER_NAMES.forEach(map::remove));
+ return chain.filter(exchange);
}
}
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
new file mode 100644
index 0000000000000000000000000000000000000000..174545ecd4371cb69213fb3066598abba1518723
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.web.server.adapter;
+
+import java.net.URI;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.lang.Nullable;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Extract values from "Forwarded" and "X-Forwarded-*" headers to override the
+ * request URI (i.e. {@link ServerHttpRequest#getURI()}) so it reflects the
+ * client-originated protocol and address.
+ *
+ * Alternatively if {@link #setRemoveOnly removeOnly} is set to "true", then
+ * "Forwarded" and "X-Forwarded-*" headers are only removed, and not used.
+ *
+ * @author Rossen Stoyanchev
+ * @since 5.1
+ * @see https://tools.ietf.org/html/rfc7239
+ */
+public class ForwardedHeaderTransformer implements Function {
+
+ static final Set FORWARDED_HEADER_NAMES = new LinkedHashSet<>(5);
+
+ static {
+ FORWARDED_HEADER_NAMES.add("Forwarded");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Host");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Port");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
+ FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
+ }
+
+
+ private boolean removeOnly;
+
+
+ /**
+ * Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are
+ * removed only and the information in them ignored.
+ * @param removeOnly whether to discard and ignore forwarded headers
+ */
+ public void setRemoveOnly(boolean removeOnly) {
+ this.removeOnly = removeOnly;
+ }
+
+ /**
+ * Whether the "remove only" mode is on.
+ */
+ public boolean isRemoveOnly() {
+ return this.removeOnly;
+ }
+
+
+ /**
+ * Apply and remove, or remove Forwarded type headers.
+ * @param request the request
+ */
+ @Override
+ public ServerHttpRequest apply(ServerHttpRequest request) {
+
+ if (hasForwardedHeaders(request)) {
+ ServerHttpRequest.Builder builder = request.mutate();
+ if (!this.removeOnly) {
+ URI uri = UriComponentsBuilder.fromHttpRequest(request).build().toUri();
+ builder.uri(uri);
+ String prefix = getForwardedPrefix(request);
+ if (prefix != null) {
+ builder.path(prefix + uri.getPath());
+ builder.contextPath(prefix);
+ }
+ }
+ removeForwardedHeaders(builder);
+ request = builder.build();
+ }
+
+ return request;
+ }
+
+ /**
+ * Whether the request has any Forwarded headers.
+ * @param request the request
+ */
+ protected boolean hasForwardedHeaders(ServerHttpRequest request) {
+ HttpHeaders headers = request.getHeaders();
+ for (String headerName : FORWARDED_HEADER_NAMES) {
+ if (headers.containsKey(headerName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void removeForwardedHeaders(ServerHttpRequest.Builder builder) {
+ builder.headers(map -> FORWARDED_HEADER_NAMES.forEach(map::remove));
+ }
+
+ @Nullable
+ private static String getForwardedPrefix(ServerHttpRequest request) {
+ HttpHeaders headers = request.getHeaders();
+ String prefix = headers.getFirst("X-Forwarded-Prefix");
+ if (prefix != null) {
+ int endIndex = prefix.length();
+ while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') {
+ endIndex--;
+ }
+ prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix);
+ }
+ return prefix;
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java
index bd6bfd2d53297db2b1dd2196d55a443e5d971b48..8495bb3e1c3d9d90412e2fdc1f1195c739c6e8ec 100644
--- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java
+++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java
@@ -92,6 +92,9 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
private LocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
+ @Nullable
+ private ForwardedHeaderTransformer forwardedHeaderTransformer;
+
@Nullable
private ApplicationContext applicationContext;
@@ -169,6 +172,27 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
return this.localeContextResolver;
}
+ /**
+ * Enable processing of forwarded headers, either extracting and removing,
+ * or remove only.
+ * By default this is not set.
+ * @param transformer the transformer to use
+ * @since 5.1
+ */
+ public void setForwardedHeaderTransformer(ForwardedHeaderTransformer transformer) {
+ Assert.notNull(transformer, "ForwardedHeaderTransformer is required");
+ this.forwardedHeaderTransformer = transformer;
+ }
+
+ /**
+ * Return the configured {@link ForwardedHeaderTransformer}.
+ * @since 5.1
+ */
+ @Nullable
+ public ForwardedHeaderTransformer getForwardedHeaderTransformer() {
+ return this.forwardedHeaderTransformer;
+ }
+
/**
* Configure the {@code ApplicationContext} associated with the web application,
* if it was initialized with one via
@@ -208,6 +232,10 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
@Override
public Mono handle(ServerHttpRequest request, ServerHttpResponse response) {
+ if (this.forwardedHeaderTransformer != null) {
+ request = this.forwardedHeaderTransformer.apply(request);
+ }
+
ServerWebExchange exchange = createExchange(request, response);
logExchange(exchange);
diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java b/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java
index 95d3f67940723852ef94ce7a861685615f8be428..68786ea928e0e4a952003468187da7ada5998073 100644
--- a/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java
+++ b/spring-web/src/main/java/org/springframework/web/server/adapter/WebHttpHandlerBuilder.java
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
+import java.util.stream.Collectors;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
@@ -74,6 +75,9 @@ public final class WebHttpHandlerBuilder {
/** Well-known name for the LocaleContextResolver in the bean factory. */
public static final String LOCALE_CONTEXT_RESOLVER_BEAN_NAME = "localeContextResolver";
+ /** Well-known name for the ForwardedHeaderTransformer in the bean factory. */
+ public static final String FORWARDED_HEADER_TRANSFORMER_BEAN_NAME = "forwardedHeaderTransformer";
+
private final WebHandler webHandler;
@@ -93,6 +97,9 @@ public final class WebHttpHandlerBuilder {
@Nullable
private LocaleContextResolver localeContextResolver;
+ @Nullable
+ private ForwardedHeaderTransformer forwardedHeaderTransformer;
+
/**
* Private constructor to use when initialized from an ApplicationContext.
@@ -114,6 +121,7 @@ public final class WebHttpHandlerBuilder {
this.sessionManager = other.sessionManager;
this.codecConfigurer = other.codecConfigurer;
this.localeContextResolver = other.localeContextResolver;
+ this.forwardedHeaderTransformer = other.forwardedHeaderTransformer;
}
@@ -181,6 +189,22 @@ public final class WebHttpHandlerBuilder {
// Fall back on default
}
+ try {
+ builder.localeContextResolver(
+ context.getBean(LOCALE_CONTEXT_RESOLVER_BEAN_NAME, LocaleContextResolver.class));
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ // Fall back on default
+ }
+
+ try {
+ builder.forwardedHeaderTransformer(
+ context.getBean(FORWARDED_HEADER_TRANSFORMER_BEAN_NAME, ForwardedHeaderTransformer.class));
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ // Fall back on default
+ }
+
return builder;
}
@@ -192,6 +216,7 @@ public final class WebHttpHandlerBuilder {
public WebHttpHandlerBuilder filter(WebFilter... filters) {
if (!ObjectUtils.isEmpty(filters)) {
this.filters.addAll(Arrays.asList(filters));
+ updateFilters();
}
return this;
}
@@ -202,9 +227,29 @@ public final class WebHttpHandlerBuilder {
*/
public WebHttpHandlerBuilder filters(Consumer> consumer) {
consumer.accept(this.filters);
+ updateFilters();
return this;
}
+ private void updateFilters() {
+
+ if (this.filters.isEmpty()) {
+ return;
+ }
+
+ List filtersToUse = this.filters.stream()
+ .peek(filter -> {
+ if (filter instanceof ForwardedHeaderTransformer && this.forwardedHeaderTransformer == null) {
+ this.forwardedHeaderTransformer = (ForwardedHeaderTransformer) filter;
+ }
+ })
+ .filter(filter -> !(filter instanceof ForwardedHeaderTransformer))
+ .collect(Collectors.toList());
+
+ this.filters.clear();
+ this.filters.addAll(filtersToUse);
+ }
+
/**
* Add the given exception handler(s).
* @param handlers the exception handler(s)
@@ -284,11 +329,33 @@ public final class WebHttpHandlerBuilder {
return (this.localeContextResolver != null);
}
+ /**
+ * Configure the {@link ForwardedHeaderTransformer} for extracting and/or
+ * removing forwarded headers.
+ * @param transformer the transformer
+ * @since 5.1
+ */
+ public WebHttpHandlerBuilder forwardedHeaderTransformer(ForwardedHeaderTransformer transformer) {
+ this.forwardedHeaderTransformer = transformer;
+ return this;
+ }
+
+ /**
+ * Whether a {@code ForwardedHeaderTransformer} is configured or not, either
+ * detected from an {@code ApplicationContext} or explicitly configured via
+ * {@link #forwardedHeaderTransformer(ForwardedHeaderTransformer)}.
+ * @since 5.1
+ */
+ public boolean hasForwardedHeaderTransformer() {
+ return (this.forwardedHeaderTransformer != null);
+ }
+
/**
* Build the {@link HttpHandler}.
*/
public HttpHandler build() {
+
WebHandler decorated = new FilteringWebHandler(this.webHandler, this.filters);
decorated = new ExceptionHandlingWebHandler(decorated, this.exceptionHandlers);
@@ -302,6 +369,9 @@ public final class WebHttpHandlerBuilder {
if (this.localeContextResolver != null) {
adapted.setLocaleContextResolver(this.localeContextResolver);
}
+ if (this.forwardedHeaderTransformer != null) {
+ adapted.setForwardedHeaderTransformer(this.forwardedHeaderTransformer);
+ }
if (this.applicationContext != null) {
adapted.setApplicationContext(this.applicationContext);
}
diff --git a/spring-web/src/test/java/org/springframework/web/filter/reactive/ForwardedHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/reactive/ForwardedHeaderFilterTests.java
deleted file mode 100644
index 307933716846164d0f712ba4487d96b0976c9ca2..0000000000000000000000000000000000000000
--- a/spring-web/src/test/java/org/springframework/web/filter/reactive/ForwardedHeaderFilterTests.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright 2002-2018 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.springframework.web.filter.reactive;
-
-import java.net.URI;
-import java.time.Duration;
-
-import org.junit.Test;
-import reactor.core.publisher.Mono;
-
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.server.reactive.ServerHttpRequest;
-import org.springframework.lang.Nullable;
-import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
-import org.springframework.mock.web.test.server.MockServerWebExchange;
-import org.springframework.web.server.ServerWebExchange;
-import org.springframework.web.server.WebFilterChain;
-
-import static org.junit.Assert.*;
-
-/**
- * Unit tests for {@link ForwardedHeaderFilter}.
- * @author Arjen Poutsma
- * @author Rossen Stoyanchev
- */
-public class ForwardedHeaderFilterTests {
-
- private static final String BASE_URL = "http://example.com/path";
-
-
- private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
-
- private final TestWebFilterChain filterChain = new TestWebFilterChain();
-
-
- @Test
- public void removeOnly() {
-
- this.filter.setRemoveOnly(true);
-
- HttpHeaders headers = new HttpHeaders();
- headers.add("Forwarded", "for=192.0.2.60;proto=http;by=203.0.113.43");
- headers.add("X-Forwarded-Host", "example.com");
- headers.add("X-Forwarded-Port", "8080");
- headers.add("X-Forwarded-Proto", "http");
- headers.add("X-Forwarded-Prefix", "prefix");
- headers.add("X-Forwarded-Ssl", "on");
- this.filter.filter(getExchange(headers), this.filterChain).block(Duration.ZERO);
-
- this.filterChain.assertForwardedHeadersRemoved();
- }
-
- @Test
- public void xForwardedHeaders() throws Exception {
- HttpHeaders headers = new HttpHeaders();
- headers.add("X-Forwarded-Host", "84.198.58.199");
- headers.add("X-Forwarded-Port", "443");
- headers.add("X-Forwarded-Proto", "https");
- headers.add("foo", "bar");
- this.filter.filter(getExchange(headers), this.filterChain).block(Duration.ZERO);
-
- assertEquals(new URI("https://84.198.58.199/path"), this.filterChain.uri);
- this.filterChain.assertForwardedHeadersRemoved();
- }
-
- @Test
- public void forwardedHeader() throws Exception {
- HttpHeaders headers = new HttpHeaders();
- headers.add("Forwarded", "host=84.198.58.199;proto=https");
- this.filter.filter(getExchange(headers), this.filterChain).block(Duration.ZERO);
-
- assertEquals(new URI("https://84.198.58.199/path"), this.filterChain.uri);
- this.filterChain.assertForwardedHeadersRemoved();
- }
-
- @Test
- public void xForwardedPrefix() throws Exception {
- HttpHeaders headers = new HttpHeaders();
- headers.add("X-Forwarded-Prefix", "/prefix");
- this.filter.filter(getExchange(headers), this.filterChain).block(Duration.ZERO);
-
- assertEquals(new URI("http://example.com/prefix/path"), this.filterChain.uri);
- assertEquals("/prefix/path", this.filterChain.requestPathValue);
- this.filterChain.assertForwardedHeadersRemoved();
- }
-
- @Test
- public void xForwardedPrefixTrailingSlash() throws Exception {
- HttpHeaders headers = new HttpHeaders();
- headers.add("X-Forwarded-Prefix", "/prefix////");
- this.filter.filter(getExchange(headers), this.filterChain).block(Duration.ZERO);
-
- assertEquals(new URI("http://example.com/prefix/path"), this.filterChain.uri);
- assertEquals("/prefix/path", this.filterChain.requestPathValue);
- this.filterChain.assertForwardedHeadersRemoved();
- }
-
- private MockServerWebExchange getExchange(HttpHeaders headers) {
- MockServerHttpRequest request = MockServerHttpRequest.get(BASE_URL).headers(headers).build();
- return MockServerWebExchange.from(request);
- }
-
-
- private static class TestWebFilterChain implements WebFilterChain {
-
- @Nullable
- private HttpHeaders headers;
-
- @Nullable
- private URI uri;
-
- @Nullable String requestPathValue;
-
-
- @Nullable
- public HttpHeaders getHeaders() {
- return this.headers;
- }
-
- @Nullable
- public String getHeader(String name) {
- assertNotNull(this.headers);
- return this.headers.getFirst(name);
- }
-
- public void assertForwardedHeadersRemoved() {
- assertNotNull(this.headers);
- ForwardedHeaderFilter.FORWARDED_HEADER_NAMES
- .forEach(name -> assertFalse(this.headers.containsKey(name)));
- }
-
- @Nullable
- public URI getUri() {
- return this.uri;
- }
-
- @Override
- public Mono filter(ServerWebExchange exchange) {
- ServerHttpRequest request = exchange.getRequest();
- this.headers = request.getHeaders();
- this.uri = request.getURI();
- this.requestPathValue = request.getPath().value();
- return Mono.empty();
- }
- }
-
-}
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
new file mode 100644
index 0000000000000000000000000000000000000000..fd5a7e52c84e94e6577833c76f5c9e9afe4fbf4e
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2002-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.server.adapter;
+
+import java.net.URI;
+
+import org.junit.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for {@link ForwardedHeaderTransformer}.
+ * @author Rossen Stoyanchev
+ */
+public class ForwardedHeaderTransformerTests {
+
+ private static final String BASE_URL = "http://example.com/path";
+
+
+ private final ForwardedHeaderTransformer requestMutator = new ForwardedHeaderTransformer();
+
+
+ @Test
+ public void removeOnly() {
+
+ this.requestMutator.setRemoveOnly(true);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Forwarded", "for=192.0.2.60;proto=http;by=203.0.113.43");
+ headers.add("X-Forwarded-Host", "example.com");
+ headers.add("X-Forwarded-Port", "8080");
+ headers.add("X-Forwarded-Proto", "http");
+ headers.add("X-Forwarded-Prefix", "prefix");
+ headers.add("X-Forwarded-Ssl", "on");
+ ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
+
+ assertForwardedHeadersRemoved(request);
+ }
+
+ @Test
+ public void xForwardedHeaders() throws Exception {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("X-Forwarded-Host", "84.198.58.199");
+ headers.add("X-Forwarded-Port", "443");
+ headers.add("X-Forwarded-Proto", "https");
+ headers.add("foo", "bar");
+ ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
+
+ assertEquals(new URI("https://84.198.58.199/path"), request.getURI());
+ assertForwardedHeadersRemoved(request);
+ }
+
+ @Test
+ public void forwardedHeader() throws Exception {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("Forwarded", "host=84.198.58.199;proto=https");
+ ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
+
+ assertEquals(new URI("https://84.198.58.199/path"), request.getURI());
+ assertForwardedHeadersRemoved(request);
+ }
+
+ @Test
+ public void xForwardedPrefix() throws Exception {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("X-Forwarded-Prefix", "/prefix");
+ ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
+
+ assertEquals(new URI("http://example.com/prefix/path"), request.getURI());
+ assertEquals("/prefix/path", request.getPath().value());
+ assertForwardedHeadersRemoved(request);
+ }
+
+ @Test
+ public void xForwardedPrefixTrailingSlash() throws Exception {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add("X-Forwarded-Prefix", "/prefix////");
+ ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
+
+ assertEquals(new URI("http://example.com/prefix/path"), request.getURI());
+ assertEquals("/prefix/path", request.getPath().value());
+ assertForwardedHeadersRemoved(request);
+ }
+
+ private MockServerHttpRequest getRequest(HttpHeaders headers) {
+ return MockServerHttpRequest.get(BASE_URL).headers(headers).build();
+ }
+
+ private void assertForwardedHeadersRemoved(ServerHttpRequest request) {
+ ForwardedHeaderTransformer.FORWARDED_HEADER_NAMES
+ .forEach(name -> assertFalse(request.getHeaders().containsKey(name)));
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java
index 31eb78d4d3c1ac42c853432d44517514b9e532c3..e26164785951d9048c5c902eb6e6030098e285a8 100644
--- a/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java
+++ b/spring-web/src/test/java/org/springframework/web/server/adapter/WebHttpHandlerBuilderTests.java
@@ -17,6 +17,7 @@
package org.springframework.web.server.adapter;
import java.nio.charset.StandardCharsets;
+import java.util.Collections;
import org.junit.Test;
import reactor.core.publisher.Flux;
@@ -31,6 +32,7 @@ import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
+import org.springframework.web.filter.reactive.ForwardedHeaderFilter;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;
@@ -62,6 +64,17 @@ public class WebHttpHandlerBuilderTests {
assertEquals("FilterB::FilterA", response.getBodyAsString().block(ofMillis(5000)));
}
+ @Test
+ public void forwardedHeaderFilter() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.register(ForwardedHeaderFilterConfig.class);
+ context.refresh();
+
+ WebHttpHandlerBuilder builder = WebHttpHandlerBuilder.applicationContext(context);
+ builder.filters(filters -> assertEquals(Collections.emptyList(), filters));
+ assertTrue(builder.hasForwardedHeaderTransformer());
+ }
+
@Test // SPR-15074
public void orderedWebExceptionHandlerBeans() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@@ -166,6 +179,20 @@ public class WebHttpHandlerBuilderTests {
}
}
+ @Configuration
+ @SuppressWarnings({"unused", "deprecation"})
+ static class ForwardedHeaderFilterConfig {
+
+ @Bean
+ public ForwardedHeaderFilter forwardedHeaderFilter() {
+ return new ForwardedHeaderFilter();
+ }
+
+ @Bean
+ public WebHandler webHandler() {
+ return exchange -> Mono.error(new Exception());
+ }
+ }
@Configuration
@SuppressWarnings("unused")
diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java
index b325ee0e0850ff099fcf6a341f32a81338ef1e92..58ae4d73612791387cbd07315bda726846cdf611 100644
--- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java
+++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestMappingIntegrationTests.java
@@ -16,6 +16,7 @@
package org.springframework.web.reactive.result.method.annotation;
+import java.net.URI;
import java.time.Duration;
import org.junit.Test;
@@ -23,11 +24,16 @@ import org.reactivestreams.Publisher;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.RequestEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.config.EnableWebFlux;
+import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
import static org.junit.Assert.*;
@@ -46,7 +52,7 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr
@Override
protected ApplicationContext initApplicationContext() {
AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext();
- wac.register(WebConfig.class, TestRestController.class);
+ wac.register(WebConfig.class, TestRestController.class, LocalConfig.class);
wac.refresh();
return wac;
}
@@ -62,6 +68,20 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr
assertEquals(3, headers.getContentLength());
}
+ @Test
+ public void forwardedHeaders() {
+
+ // One integration test to verify triggering of Forwarded header support.
+ // More fine-grained tests in ForwardedHeaderTransformerTests.
+
+ RequestEntity request = RequestEntity
+ .get(URI.create("http://localhost:" + this.port + "/uri"))
+ .header("Forwarded", "host=84.198.58.199;proto=https")
+ .build();
+ ResponseEntity entity = getRestTemplate().exchange(request, String.class);
+ assertEquals("https://84.198.58.199/uri", entity.getBody());
+ }
+
@Test
public void stream() throws Exception {
String[] expected = {"0", "1", "2", "3", "4"};
@@ -84,10 +104,25 @@ public class RequestMappingIntegrationTests extends AbstractRequestMappingIntegr
return "Foo";
}
+ @GetMapping("/uri")
+ public String uri(ServerHttpRequest request) {
+ return request.getURI().toString();
+ }
+
@GetMapping("/stream")
public Publisher stream() {
return testInterval(Duration.ofMillis(50), 5);
}
}
+
+ @Configuration
+ static class LocalConfig {
+
+ @Bean
+ public ForwardedHeaderTransformer forwardedHeaderTransformer() {
+ return new ForwardedHeaderTransformer();
+ }
+ }
+
}
diff --git a/src/docs/asciidoc/web/webflux.adoc b/src/docs/asciidoc/web/webflux.adoc
index 4860aa355df6c3ab5233514e73239a08fde3a8c2..a896253cc9e7a279af61380ac4776035d05e3bec 100644
--- a/src/docs/asciidoc/web/webflux.adoc
+++ b/src/docs/asciidoc/web/webflux.adoc
@@ -508,6 +508,12 @@ The table below lists the components that `WebHttpHandlerBuilder` detects:
| 0..1
| The resolver for `LocaleContext` exposed through a method on `ServerWebExchange`.
`AcceptHeaderLocaleContextResolver` by default.
+
+| "forwardedHeaderTransformer"
+| `ForwardedHeaderTransformer`
+| 0..1
+| For processing Forwarded type headers, either extracting and removing, or removing them only.
+ Not used by default.
|===
@@ -553,6 +559,39 @@ parsing multipart data in full. By contrast `@RequestBody` can be used to decode
content to `Flux` without collecting to a `MultiValueMap`.
+[[webflux-forwarded-headers]]
+==== Forwarded Headers
+[.small]#<>#
+
+As a request goes through proxies such as load balancers the host, port, and
+scheme may change and that makes it a challenge to create links that point to the correct
+host, port, and scheme from a client perspective.
+
+https://tools.ietf.org/html/rfc7239[RFC 7239] defines the "Forwarded" HTTP header
+that proxies can use to provide information about the original request. There are other
+non-standard headers too including "X-Forwarded-Host", "X-Forwarded-Port",
+"X-Forwarded-Proto", "X-Forwarded-Ssl", and "X-Forwarded-Prefix".
+
+`ForwardedHeaderTransformer` is a component that modifies the host, port, and scheme of
+the request, based on Forwarded headers, and then removes those headers. Simply declare
+it as a bean with the name "forwardedHeaderTransformer" and it will be
+<> and used.
+
+There are security considerations for forwarded headers since an application can't know
+if the headers were added by a proxy as intended, or with a malicious client. This is why
+a proxy at the boundary of trust should be configured to remove untrusted Forwarded coming
+from the outside. You can also configure the `ForwardedHeaderTransformer` with
+`removeOnly=true` in which case it will remove but not use the headers.
+
+[NOTE]
+====
+In 5.1 `ForwardedHeaderFilter` was deprecated and superceded by
+`ForwardedHeaderTransformer` so forwarded headers can be processed earlier, before the
+exchange is created. If the filter is configured anyway, it is taken out of the list of
+filters, and `ForwardedHeaderTransformer` is used instead.
+====
+
+
[[webflux-filters]]
=== Filters
@@ -567,34 +606,6 @@ the bean declaration or by implementing `Ordered`.
The following describe the available `WebFilter` implementations:
-[[webflux-filters-forwarded-headers]]
-==== Forwarded Headers
-[.small]#<>#
-
-As a request goes through proxies such as load balancers the host, port, and
-scheme may change presenting a challenge for applications that need to create links
-to resources since the links should reflect the host, port, and scheme of the
-original request as seen from a client perspective.
-
-https://tools.ietf.org/html/rfc7239[RFC 7239] defines the "Forwarded" HTTP header
-for proxies to use to provide information about the original request. There are also
-other non-standard headers in use such as "X-Forwarded-Host", "X-Forwarded-Port",
-and "X-Forwarded-Proto".
-
-`ForwardedHeaderFilter` detects, extracts, and uses information from the "Forwarded"
-header, or from "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto".
-It wraps the request in order to overlay its host, port, and scheme and also "hides"
-the forwarded headers for subsequent processing.
-
-Note that there are security considerations when using forwarded headers as explained
-in Section 8 of RFC 7239. At the application level it is difficult to determine whether
-forwarded headers can be trusted or not. This is why the network upstream should be
-configured correctly to filter out untrusted forwarded headers from the outside.
-
-Applications that don't have a proxy and don't need to use forwarded headers can
-configure the `ForwardedHeaderFilter` to remove and ignore such headers.
-
-
[[webflux-filters-cors]]
==== CORS
[.small]#<>#
diff --git a/src/docs/asciidoc/web/webmvc.adoc b/src/docs/asciidoc/web/webmvc.adoc
index 86cdfe3213c23debd847a22414a8602d9e90b54d..019306fe675204b45f8802dbb8f56d288cb7a1b9 100644
--- a/src/docs/asciidoc/web/webmvc.adoc
+++ b/src/docs/asciidoc/web/webmvc.adoc
@@ -1116,32 +1116,27 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method
-[[webflux-filters-forwarded-headers]]
+[[filters-forwarded-headers]]
=== Forwarded Headers
-[.small]#<>#
+[.small]#<>#
As a request goes through proxies such as load balancers the host, port, and
-scheme may change presenting a challenge for applications that need to create links
-to resources since the links should reflect the host, port, and scheme of the
-original request as seen from a client perspective.
+scheme may change and that makes it a challenge to create links that point to the correct
+host, port, and scheme from a client perspective.
https://tools.ietf.org/html/rfc7239[RFC 7239] defines the "Forwarded" HTTP header
-for proxies to use to provide information about the original request. There are also
-other non-standard headers in use such as "X-Forwarded-Host", "X-Forwarded-Port",
-and "X-Forwarded-Proto".
-
-`ForwardedHeaderFilter` detects, extracts, and uses information from the "Forwarded"
-header, or from "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto".
-It wraps the request in order to overlay its host, port, and scheme and also "hides"
-the forwarded headers for subsequent processing.
-
-Note that there are security considerations when using forwarded headers as explained
-in Section 8 of RFC 7239. At the application level it is difficult to determine whether
-forwarded headers can be trusted or not. This is why the network upstream should be
-configured correctly to filter out untrusted forwarded headers from the outside.
-
-Applications that don't have a proxy and don't need to use forwarded headers can
-configure the `ForwardedHeaderFilter` to remove and ignore such headers.
+that proxies can use to provide information about the original request. There are other
+non-standard headers too including "X-Forwarded-Host", "X-Forwarded-Port",
+"X-Forwarded-Proto", "X-Forwarded-Ssl", and "X-Forwarded-Prefix".
+
+`ForwardedHeaderFilter` is a Servlet filter that modifies the host, port, and scheme of
+the request, based on Forwarded headers, and then removes those headers.
+
+There are security considerations for forwarded headers since an application can't know
+if the headers were added by a proxy as intended, or with a malicious client. This is why
+a proxy at the boundary of trust should be configured to remove untrusted Forwarded coming
+from the outside. You can also configure the `ForwardedHeaderFilter` with
+`removeOnly=true` in which case it will remove but not use the headers.