Merge remote-tracking branch 'origin/4.1.x' into 4.1.x

This commit is contained in:
Olga Maciaszek-Sharma
2025-05-29 19:05:26 +02:00
26 changed files with 1197 additions and 120 deletions

View File

@@ -109,6 +109,7 @@
*** xref:spring-cloud-gateway-server-mvc/filters/requestsize.adoc[]
*** xref:spring-cloud-gateway-server-mvc/filters/setrequesthostheader.adoc[]
*** xref:spring-cloud-gateway-server-mvc/filters/tokenrelay.adoc[]
** xref:spring-cloud-gateway-server-mvc/httpheadersfilters.adoc[]
** xref:spring-cloud-gateway-server-mvc/writing-custom-predicates-and-filters.adoc[]
** xref:spring-cloud-gateway-server-mvc/working-with-servlets-and-filters.adoc[]

View File

@@ -0,0 +1,45 @@
[[httpheadersfilters]]
= HttpHeadersFilters
HttpHeadersFilters are applied to the requests before sending them downstream, such as in the `NettyRoutingFilter`.
[[forwarded-headers-filter]]
== Forwarded Headers Filter
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. To activate this filter set the `spring.cloud.gateway.mvc.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
[[removehopbyhop-headers-filter]]
== RemoveHopByHop Headers Filter
The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3[IETF].
.The default removed headers are:
* Connection
* Keep-Alive
* Proxy-Authenticate
* Proxy-Authorization
* TE
* Trailer
* Transfer-Encoding
* Upgrade
//To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers` property to the list of header names to remove.
[[xforwarded-headers-filter]]
== XForwarded Headers Filter
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers. To activate this filter set the `spring.cloud.gateway.mvc.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
Creating of individual headers can be controlled by the following boolean properties (defaults to true):
- `spring.cloud.gateway.x-forwarded.for-enabled`
- `spring.cloud.gateway.x-forwarded.host-enabled`
- `spring.cloud.gateway.x-forwarded.port-enabled`
- `spring.cloud.gateway.x-forwarded.proto-enabled`
- `spring.cloud.gateway.x-forwarded.prefix-enabled`
Appending multiple headers can be controlled by the following boolean properties (defaults to true):
- `spring.cloud.gateway.x-forwarded.for-append`
- `spring.cloud.gateway.x-forwarded.host-append`
- `spring.cloud.gateway.x-forwarded.port-append`
- `spring.cloud.gateway.x-forwarded.proto-append`
- `spring.cloud.gateway.x-forwarded.prefix-append`

View File

@@ -5,7 +5,7 @@
[[forwarded-headers-filter]]
== Forwarded Headers Filter
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header.
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. To activate this filter set the `spring.cloud.gateway.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
[[removehopbyhop-headers-filter]]
== RemoveHopByHop Headers Filter
@@ -25,7 +25,7 @@ To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers`
[[xforwarded-headers-filter]]
== XForwarded Headers Filter
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers.
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers. To activate this filter set the `spring.cloud.gateway.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
Creating of individual headers can be controlled by the following boolean properties (defaults to true):

View File

@@ -129,6 +129,7 @@
|spring.cloud.gateway.mvc.streaming-buffer-size | `+++16384+++` | Buffer size for streaming media mime-types.
|spring.cloud.gateway.mvc.streaming-media-types | | Mime-types that are streaming.
|spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled | `+++true+++` | Enables the transfer-encoding-normalization-request-headers-filter.
|spring.cloud.gateway.mvc.trusted-proxies | | Regular expression defining proxies that are trusted when they appear in a Forwarded of X-Forwarded header.
|spring.cloud.gateway.mvc.weight-calculator-filter.enabled | `+++true+++` | Enables the weight-calculator-filter.
|spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
@@ -170,6 +171,7 @@
|spring.cloud.gateway.routes | | List of Routes.
|spring.cloud.gateway.set-status.original-status-header-name | | The name of the header which contains http code of the proxied request.
|spring.cloud.gateway.streaming-media-types | |
|spring.cloud.gateway.trusted-proxies | | Regular expression defining proxies that are trusted when they appear in a Forwarded or X-Forwarded header.
|spring.cloud.gateway.x-forwarded.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.x-forwarded.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
|spring.cloud.gateway.x-forwarded.for-enabled | `+++true+++` | If X-Forwarded-For is enabled.

View File

@@ -41,6 +41,7 @@ import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopRequest
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopResponseHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHttp2StatusResponseHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.TransferEncodingNormalizationRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.TrustedProxies;
import org.springframework.cloud.gateway.server.mvc.filter.WeightCalculatorFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties;
@@ -50,6 +51,7 @@ import org.springframework.cloud.gateway.server.mvc.handler.RestClientProxyExcha
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -100,10 +102,9 @@ public class GatewayServerMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = GatewayMvcProperties.PREFIX, name = "forwarded-request-headers-filter.enabled",
matchIfMissing = true)
public ForwardedRequestHeadersFilter forwardedRequestHeadersFilter() {
return new ForwardedRequestHeadersFilter();
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
public ForwardedRequestHeadersFilter forwardedRequestHeadersFilter(GatewayMvcProperties properties) {
return new ForwardedRequestHeadersFilter(properties.getTrustedProxies());
}
@Bean
@@ -207,9 +208,10 @@ public class GatewayServerMvcAutoConfiguration {
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = XForwardedRequestHeadersFilterProperties.PREFIX, name = ".enabled",
matchIfMissing = true)
public XForwardedRequestHeadersFilter xForwardedRequestHeadersFilter(
XForwardedRequestHeadersFilterProperties props) {
return new XForwardedRequestHeadersFilter(props);
@Conditional(TrustedProxies.XForwardedTrustedProxiesCondition.class)
public XForwardedRequestHeadersFilter xForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props,
GatewayMvcProperties gatewayMvcProperties) {
return new XForwardedRequestHeadersFilter(props, gatewayMvcProperties.getTrustedProxies());
}
@Bean

View File

@@ -65,6 +65,12 @@ public class GatewayMvcProperties {
*/
private int streamingBufferSize = 16384;
/**
* Regular expression defining proxies that are trusted when they appear in a
* Forwarded of X-Forwarded header.
*/
private String trustedProxies;
public List<RouteProperties> getRoutes() {
return routes;
}
@@ -101,6 +107,14 @@ public class GatewayMvcProperties {
this.streamingBufferSize = streamingBufferSize;
}
public String getTrustedProxies() {
return trustedProxies;
}
public void setTrustedProxies(String trustedProxies) {
this.trustedProxies = trustedProxies;
}
@Override
public String toString() {
return new ToStringCreator(this).append("httpClient", httpClient)
@@ -108,6 +122,7 @@ public class GatewayMvcProperties {
.append("routesMap", routesMap)
.append("streamingMediaTypes", streamingMediaTypes)
.append("streamingBufferSize", streamingBufferSize)
.append("trustedProxies", trustedProxies)
.toString();
}

View File

@@ -24,7 +24,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
@@ -34,11 +39,26 @@ import org.springframework.web.servlet.function.ServerRequest;
public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestHttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardedRequestHeadersFilter.class);
/**
* Forwarded header.
*/
public static final String FORWARDED_HEADER = "Forwarded";
private final TrustedProxies trustedProxies;
@Deprecated
public ForwardedRequestHeadersFilter() {
trustedProxies = s -> true;
log.warn(GatewayMvcProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
}
public ForwardedRequestHeadersFilter(String trustedProxiesRegex) {
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
}
/* for testing */
static List<Forwarded> parse(List<String> values) {
ArrayList<Forwarded> forwardeds = new ArrayList<>();
@@ -46,8 +66,11 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
return forwardeds;
}
for (String value : values) {
Forwarded forwarded = parse(value);
forwardeds.add(forwarded);
String[] forwardedValues = StringUtils.tokenizeToStringArray(value, ",");
for (String forwardedValue : forwardedValues) {
Forwarded forwarded = parse(forwardedValue);
forwardeds.add(forwarded);
}
}
return forwardeds;
}
@@ -89,6 +112,13 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
@Override
public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
if (request.servletRequest().getRemoteAddr() != null
&& !trustedProxies.isTrusted(request.servletRequest().getRemoteAddr())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.servletRequest().getRemoteHost()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -102,7 +132,10 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
List<Forwarded> forwardeds = parse(original.get(FORWARDED_HEADER));
for (Forwarded f : forwardeds) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
// only add if "for" value matches trustedProxies
if (trustedProxies.isTrusted(f.get("for"))) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
}
}
// TODO: add new forwarded
@@ -124,6 +157,10 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
forValue = "[" + forValue + "]";
}
}
if (!trustedProxies.isTrusted(forValue)) {
// don't add for value
return;
}
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2013-2025 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
*
* https://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.cloud.gateway.server.mvc.filter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@FunctionalInterface
public interface TrustedProxies {
boolean isTrusted(String host);
static TrustedProxies from(@NonNull String trustedProxies) {
Assert.hasText(trustedProxies, "trustedProxies must not be empty");
Pattern pattern = Pattern.compile(trustedProxies);
return value -> pattern.matcher(value).matches();
}
class ForwardedTrustedProxiesCondition extends AllNestedConditions {
public ForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".forwarded-request-headers-filter.enabled",
matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists(GatewayMvcProperties.PREFIX + ".trusted-proxies")
static class OnTrustedProxiesNotEmpty {
}
}
class XForwardedTrustedProxiesCondition extends AllNestedConditions {
public XForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = XForwardedRequestHeadersFilterProperties.PREFIX + ".enabled",
matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists(GatewayMvcProperties.PREFIX + ".trusted-proxies")
static class OnTrustedProxiesNotEmpty {
}
}
class OnPropertyExistsCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
try {
String value = metadata.getAnnotations().get(ConditionalOnPropertyExists.class).getString("value");
String property = context.getEnvironment().getProperty(value);
if (!StringUtils.hasText(property)) {
return ConditionOutcome.noMatch(value + " property is not set or is empty.");
}
return ConditionOutcome.match(value + " property is not empty.");
}
catch (NoSuchElementException e) {
return ConditionOutcome.noMatch("Missing required property 'value' of @ConditionalOnPropertyExists");
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyExistsCondition.class)
@interface ConditionalOnPropertyExists {
/**
* @return the property
*/
String value();
}
}

View File

@@ -16,18 +16,24 @@
package org.springframework.cloud.gateway.server.mvc.filter;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.function.ServerRequest;
@@ -37,6 +43,8 @@ import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequ
@ConfigurationProperties("spring.cloud.gateway.x-forwarded")
public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestHttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(XForwardedRequestHeadersFilter.class);
/** Default http port. */
public static final int HTTP_PORT = 80;
@@ -100,12 +108,30 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
/** If appending X-Forwarded-Prefix as a list is enabled. */
private boolean prefixAppend = true;
private final TrustedProxies trustedProxies;
@Deprecated
public XForwardedRequestHeadersFilter() {
this(new XForwardedRequestHeadersFilterProperties());
this(new XForwardedRequestHeadersFilterProperties(), s -> true);
log.warn(GatewayMvcProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to X-Forwarded header.");
}
@Deprecated
public XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props) {
this(props, s -> true);
log.warn(GatewayMvcProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to X-Forwarded header.");
}
public XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props, String trustedProxies) {
this(props, TrustedProxies.from(trustedProxies));
}
private XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props,
TrustedProxies trustedProxies) {
Assert.notNull(trustedProxies, "trustedProxies must not be null");
// TODO: remove individual properties in 4.2.0
// this.properties = properties;
PropertyMapper map = PropertyMapper.get();
@@ -121,6 +147,8 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
map.from(props::isPortAppend).to(b -> this.portAppend = b);
map.from(props::isProtoAppend).to(b -> this.protoAppend = b);
map.from(props::isPrefixAppend).to(b -> this.prefixAppend = b);
this.trustedProxies = trustedProxies;
}
@DeprecatedConfigurationProperty(replacement = PREFIX + ".order")
@@ -372,6 +400,13 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
@Override
public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
if (request.servletRequest().getRemoteAddr() != null
&& !trustedProxies.isTrusted(request.servletRequest().getRemoteAddr())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.servletRequest().getRemoteHost()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -379,10 +414,12 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
updated.addAll(entry.getKey(), entry.getValue());
}
InetSocketAddress remoteAddress = request.remoteAddress().orElse(null);
if (isForEnabled() && remoteAddress != null && remoteAddress.getAddress() != null) {
String remoteAddr = remoteAddress.getAddress().getHostAddress();
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend());
if (isForEnabled()) {
String remoteAddr = null;
if (request.servletRequest().getRemoteAddr() != null) {
remoteAddr = request.servletRequest().getRemoteAddr();
}
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend(), trustedProxies::isTrusted);
}
String proto = request.uri().getScheme();
@@ -457,17 +494,22 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
}
private void write(HttpHeaders headers, String name, String value, boolean append) {
if (value == null) {
return;
}
write(headers, name, value, append, s -> true);
}
private void write(HttpHeaders headers, String name, String value, boolean append, Predicate<String> shouldWrite) {
if (append) {
headers.add(name, value);
if (value != null) {
headers.add(name, value);
}
// these headers should be treated as a single comma separated header
List<String> values = headers.get(name);
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
if (headers.containsKey(name)) {
List<String> values = headers.get(name).stream().filter(shouldWrite).toList();
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
}
}
else {
else if (value != null && shouldWrite.test(value)) {
headers.set(name, value);
}
}
@@ -476,11 +518,6 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
return HTTPS_SCHEME.equals(scheme) ? HTTPS_PORT : HTTP_PORT;
}
private boolean hasHeader(ServerRequest request, String name) {
HttpHeaders headers = request.headers().asHttpHeaders();
return headers.containsKey(name) && StringUtils.hasLength(headers.getFirst(name));
}
private String toHostHeader(ServerRequest request) {
int port = request.uri().getPort();
String host = request.uri().getHost();

View File

@@ -584,7 +584,7 @@ public class ServerMvcIntegrationTests {
.isOk();
}
public static final MediaType FORM_URL_ENCODED_CONTENT_TYPE = new MediaType(APPLICATION_FORM_URLENCODED,
private static final MediaType FORM_URL_ENCODED_CONTENT_TYPE = new MediaType(APPLICATION_FORM_URLENCODED,
StandardCharsets.UTF_8);
@Test

View File

@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
@@ -61,7 +62,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -81,28 +82,39 @@ public class ForwardedRequestHeadersFilterTests {
public void forwardedHeaderExists() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https; for=23.45.67.89")
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers.get(FORWARDED_HEADER)).hasSize(2);
assertThat(headers.get(FORWARDED_HEADER)).hasSize(3);
List<Forwarded> forwardeds = ForwardedRequestHeadersFilter.parse(headers.get(FORWARDED_HEADER));
assertThat(forwardeds).hasSize(2);
Forwarded addedForwardedHeader = forwardeds.get(0);
Forwarded existingForwardedHeader = forwardeds.get(1);
assertThat(existingForwardedHeader.getValues()).containsEntry("proto", "http")
.containsEntry("for", "\"10.0.0.1:80\"");
assertThat(addedForwardedHeader.getValues()).containsEntry("proto", "https")
.containsEntry("for", "23.45.67.89");
assertThat(forwardeds).hasSize(3);
Optional<Forwarded> added = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").contains("10.0.0.1:80"))
.findFirst();
assertThat(added).isPresent();
added.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "http").containsEntry("for", "\"10.0.0.1:80\"");
});
Optional<Forwarded> existing = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").equals("23.45.67.89"))
.findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("for", "23.45.67.89");
});
existing = forwardeds.stream().filter(forwarded -> forwarded.get("for").equals("12.34.56.78")).findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "https").containsEntry("for", "12.34.56.78");
});
}
@Test
@@ -113,7 +125,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -136,7 +148,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("2001:db8:cafe:0:0:0:0:17");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -158,7 +170,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("unresolvable-hostname");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -211,4 +223,62 @@ public class ForwardedRequestHeadersFilterTests {
}
}
@Test
public void forwardedHeadersNotTrusted() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter("11\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).doesNotContainKeys(FORWARDED_HEADER);
}
// verify that existing forwarded header is not forwarded if not trusted
@Test
public void untrustedForwardedForNotAppended() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=\"127.0.0.1:80\",for=10.0.0.11")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
filtered = forwardedHeaders.stream().filter(value -> value.contains("10.0.0.11")).findFirst();
assertThat(filtered).isNotEmpty();
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=127.0.0.1")
.buildRequest(null);
servletRequest.setRemoteAddr(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2013-2023 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
*
* https://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.cloud.gateway.server.mvc.filter;
import java.util.Collections;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.servlet.function.ServerRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER;
/**
* @author Spencer Gibb
*/
public class XForwardedRequestHeadersFilterTests {
@Test
public void remoteAddressIsNull() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.header(HttpHeaders.HOST, "myhost")
.buildRequest(null);
servletRequest.setRemoteAddr(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), ".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER)
.containsKeys(X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER, X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_HOST_HEADER)).isEqualTo("myhost");
assertThat(headers.getFirst(X_FORWARDED_PORT_HEADER)).isEqualTo("80");
assertThat(headers.getFirst(X_FORWARDED_PROTO_HEADER)).isEqualTo("http");
}
@Test
public void trustedProxiesConditionMatches() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, RestClientAutoConfiguration.class,
SslAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
GatewayServerMvcAutoConfiguration.class))
.withPropertyValues(GatewayMvcProperties.PREFIX + ".trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(XForwardedRequestHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, RestClientAutoConfiguration.class,
SslAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
GatewayServerMvcAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(XForwardedRequestHeadersFilter.class);
});
}
@Test
public void emptyTrustedProxiesFails() {
Assertions
.assertThatThrownBy(
() -> new XForwardedRequestHeadersFilter(new XForwardedRequestHeadersFilterProperties(), ""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void xForwardedHeadersNotTrusted() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), "11\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
}
// verify that existing forwarded header is not forwarded if not trusted
@Test
public void untrustedXForwardedForNotAppended() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.header(X_FORWARDED_FOR_HEADER, "10.0.0.10")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), "10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1")
.contains("10.0.0.1", "10.0.0.10");
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.buildRequest(null);
servletRequest.setRemoteAddr(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), "10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1");
}
}

View File

@@ -22,7 +22,11 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class TestUtils {
public final class TestUtils {
private TestUtils() {
}
public static Map<String, Object> getMap(Map<String, Object> map, String mapKey) {
assertThat(map).isNotEmpty().containsKey(mapKey);

View File

@@ -6,4 +6,8 @@ logging:
org.springframework.retry: TRACE
spring:
mvc:
log-request-details: true
log-request-details: true
cloud:
gateway:
mvc:
trusted-proxies: .*

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2013-2024 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
*
* https://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.
*/
/*
* Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://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.cloud.gateway.config;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.netty.handler.codec.http.HttpRequest;
import reactor.netty.http.server.ConnectionInfo;
import reactor.netty.transport.AddressUtils;
import static reactor.netty.http.server.ConnectionInfo.getDefaultHostPort;
/**
* Default implementation for handling {@code X-Forwarded}/{@code Forwarded} headers.
*
* @author Andrey Shlykov
* @since 0.9.12
*/
final class DefaultNettyHttpForwardedHeaderHandler implements BiFunction<ConnectionInfo, HttpRequest, ConnectionInfo> {
static final DefaultNettyHttpForwardedHeaderHandler INSTANCE = new DefaultNettyHttpForwardedHeaderHandler();
static final String FORWARDED_HEADER = "Forwarded";
static final String X_FORWARDED_IP_HEADER = "X-Forwarded-For";
static final String X_FORWARDED_HOST_HEADER = "X-Forwarded-Host";
static final String X_FORWARDED_PORT_HEADER = "X-Forwarded-Port";
static final String X_FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("for=\"?([^;,\"]+)\"?");
/**
* Specifies whether the Http Server applies a strict {@code Forwarded} header
* validation. By default, it is enabled and strict validation is used.
* @since 1.0.8
* @deprecated The system property is used for backwards compatibility and will be
* removed in version 1.2.0.
*/
@Deprecated
static final String FORWARDED_HEADER_VALIDATION = "reactor.netty.http.server.forwarded.strictValidation";
static final boolean DEFAULT_FORWARDED_HEADER_VALIDATION = Boolean
.parseBoolean(System.getProperty(FORWARDED_HEADER_VALIDATION, "true"));
@Override
public ConnectionInfo apply(ConnectionInfo connectionInfo, HttpRequest request) {
String forwardedHeader = request.headers().get(FORWARDED_HEADER);
if (forwardedHeader != null) {
return parseForwardedInfo(connectionInfo, forwardedHeader);
}
return parseXForwardedInfo(connectionInfo, request);
}
private ConnectionInfo parseForwardedInfo(ConnectionInfo connectionInfo, String forwardedHeader) {
String forwarded = forwardedHeader.split(",", 2)[0];
Matcher protoMatcher = FORWARDED_PROTO_PATTERN.matcher(forwarded);
if (protoMatcher.find()) {
connectionInfo = connectionInfo.withScheme(protoMatcher.group(1).trim());
}
Matcher hostMatcher = FORWARDED_HOST_PATTERN.matcher(forwarded);
if (hostMatcher.find()) {
connectionInfo = connectionInfo.withHostAddress(AddressUtils.parseAddress(hostMatcher.group(1),
getDefaultHostPort(connectionInfo.getScheme()), DEFAULT_FORWARDED_HEADER_VALIDATION));
}
Matcher forMatcher = FORWARDED_FOR_PATTERN.matcher(forwarded);
if (forMatcher.find()) {
connectionInfo = connectionInfo.withRemoteAddress(AddressUtils.parseAddress(forMatcher.group(1).trim(),
connectionInfo.getRemoteAddress().getPort(), DEFAULT_FORWARDED_HEADER_VALIDATION));
}
return connectionInfo;
}
private ConnectionInfo parseXForwardedInfo(ConnectionInfo connectionInfo, HttpRequest request) {
String ipHeader = request.headers().get(X_FORWARDED_IP_HEADER);
if (ipHeader != null) {
connectionInfo = connectionInfo.withRemoteAddress(
AddressUtils.parseAddress(ipHeader.split(",", 2)[0], connectionInfo.getRemoteAddress().getPort()));
}
String protoHeader = request.headers().get(X_FORWARDED_PROTO_HEADER);
if (protoHeader != null) {
connectionInfo = connectionInfo.withScheme(protoHeader.split(",", 2)[0].trim());
}
String hostHeader = request.headers().get(X_FORWARDED_HOST_HEADER);
if (hostHeader != null) {
connectionInfo = connectionInfo
.withHostAddress(AddressUtils.parseAddress(hostHeader.split(",", 2)[0].trim(),
getDefaultHostPort(connectionInfo.getScheme()), DEFAULT_FORWARDED_HEADER_VALIDATION));
}
String portHeader = request.headers().get(X_FORWARDED_PORT_HEADER);
if (portHeader != null && !portHeader.isEmpty()) {
String portStr = portHeader.split(",", 2)[0].trim();
if (portStr.chars().allMatch(Character::isDigit)) {
int port = Integer.parseInt(portStr);
connectionInfo = connectionInfo.withHostAddress(
AddressUtils.createUnresolved(connectionInfo.getHostAddress().getHostString(), port),
connectionInfo.getHostName(), port);
}
else if (DEFAULT_FORWARDED_HEADER_VALIDATION) {
throw new IllegalArgumentException("Failed to parse a port from " + portHeader);
}
}
return connectionInfo;
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.config;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -59,6 +60,7 @@ import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfigurat
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.embedded.netty.NettyServerCustomizer;
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint;
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter;
@@ -123,6 +125,7 @@ import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilte
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.TransferEncodingNormalizationHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.TrustedProxies;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver;
@@ -312,9 +315,9 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.forwarded.enabled", matchIfMissing = true)
public ForwardedHeadersFilter forwardedHeadersFilter() {
return new ForwardedHeadersFilter();
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
public ForwardedHeadersFilter forwardedHeadersFilter(GatewayProperties properties) {
return new ForwardedHeadersFilter(properties.getTrustedProxies());
}
// HttpHeaderFilter beans
@@ -325,9 +328,9 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.x-forwarded.enabled", matchIfMissing = true)
public XForwardedHeadersFilter xForwardedHeadersFilter() {
return new XForwardedHeadersFilter();
@Conditional(TrustedProxies.XForwardedTrustedProxiesCondition.class)
public XForwardedHeadersFilter xForwardedHeadersFilter(GatewayProperties properties) {
return new XForwardedHeadersFilter(properties.getTrustedProxies());
}
@Bean
@@ -757,6 +760,21 @@ public class GatewayAutoConfiguration {
};
}
@Bean
@TrustedProxies.ConditionalOnPropertyExists
public NettyServerCustomizer gatewayNettyServerCustomizer(GatewayProperties gatewayProperties) {
TrustedProxies trustedProxies = TrustedProxies.from(gatewayProperties.getTrustedProxies());
return httpServer -> httpServer.forwarded((connectionInfo, httpRequest) -> {
InetSocketAddress remoteAddress = connectionInfo.getRemoteAddress();
if (remoteAddress != null && trustedProxies.isTrusted(remoteAddress.getHostString())) {
// update remote address
return DefaultNettyHttpForwardedHeaderHandler.INSTANCE.apply(connectionInfo, httpRequest);
}
return connectionInfo;
});
}
@Bean
public HttpClientSslConfigurer httpClientSslConfigurer(ServerProperties serverProperties,
HttpClientProperties httpClientProperties) {

View File

@@ -68,6 +68,12 @@ public class GatewayProperties {
*/
private boolean failOnRouteDefinitionError = true;
/**
* Regular expression defining proxies that are trusted when they appear in a
* Forwarded or X-Forwarded header.
*/
private String trustedProxies;
public List<RouteDefinition> getRoutes() {
return routes;
}
@@ -103,12 +109,21 @@ public class GatewayProperties {
this.failOnRouteDefinitionError = failOnRouteDefinitionError;
}
public String getTrustedProxies() {
return trustedProxies;
}
public void setTrustedProxies(String trustedProxies) {
this.trustedProxies = trustedProxies;
}
@Override
public String toString() {
return new ToStringCreator(this).append("routes", routes)
.append("defaultFilters", defaultFilters)
.append("streamingMediaTypes", streamingMediaTypes)
.append("failOnRouteDefinitionError", failOnRouteDefinitionError)
.append("trustedProxies", trustedProxies)
.toString();
}

View File

@@ -25,7 +25,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.CollectionUtils;
@@ -36,11 +41,26 @@ import org.springframework.web.server.ServerWebExchange;
public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardedHeadersFilter.class);
/**
* Forwarded header.
*/
public static final String FORWARDED_HEADER = "Forwarded";
private final TrustedProxies trustedProxies;
@Deprecated
public ForwardedHeadersFilter() {
trustedProxies = s -> true;
log.warn(GatewayProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
}
public ForwardedHeadersFilter(String trustedProxiesRegex) {
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
}
/* for testing */
static List<Forwarded> parse(List<String> values) {
ArrayList<Forwarded> forwardeds = new ArrayList<>();
@@ -48,8 +68,11 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
return forwardeds;
}
for (String value : values) {
Forwarded forwarded = parse(value);
forwardeds.add(forwarded);
String[] forwardedValues = StringUtils.tokenizeToStringArray(value, ",");
for (String forwardedValue : forwardedValues) {
Forwarded forwarded = parse(forwardedValue);
forwardeds.add(forwarded);
}
}
return forwardeds;
}
@@ -92,6 +115,14 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
@Override
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
if (request.getRemoteAddress() != null
&& !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.getRemoteAddress()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -105,7 +136,10 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
List<Forwarded> forwardeds = parse(original.get(FORWARDED_HEADER));
for (Forwarded f : forwardeds) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
// only add if "for" value matches trustedProxies
if (trustedProxies.isTrusted(f.get("for"))) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
}
}
// TODO: add new forwarded
@@ -114,6 +148,7 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
Forwarded forwarded = new Forwarded().put("host", host).put("proto", uri.getScheme());
InetSocketAddress remoteAddress = request.getRemoteAddress();
// TODO: only add if "remoteAddress" value matches trustedProxies
if (remoteAddress != null) {
// If remoteAddress is unresolved, calling getHostAddress() would cause a
// NullPointerException.
@@ -128,11 +163,14 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
forValue = "[" + forValue + "]";
}
}
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;
if (trustedProxies.isTrusted(forValue)) {
// only add for value if trusted
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;
}
forwarded.put("for", forValue);
}
forwarded.put("for", forValue);
}
// TODO: support by?

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2013-2025 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
*
* https://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.cloud.gateway.filter.headers;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@FunctionalInterface
public interface TrustedProxies {
/**
* Property name.
*/
String PROPERTY = GatewayProperties.PREFIX + ".trusted-proxies";
boolean isTrusted(String host);
static TrustedProxies from(@NonNull String trustedProxies) {
Assert.hasText(trustedProxies, "trustedProxies must not be empty");
Pattern pattern = Pattern.compile(trustedProxies);
return value -> pattern.matcher(value).matches();
}
class ForwardedTrustedProxiesCondition extends AllNestedConditions {
public ForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".forwarded.enabled", matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists
static class OnTrustedProxiesNotEmpty {
}
}
class XForwardedTrustedProxiesCondition extends AllNestedConditions {
public XForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".x-forwarded.enabled", matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists
static class OnTrustedProxiesNotEmpty {
}
}
class OnPropertyExistsCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
try {
String property = context.getEnvironment().getProperty(PROPERTY);
if (!StringUtils.hasText(property)) {
return ConditionOutcome.noMatch(PROPERTY + " property is not set or is empty.");
}
return ConditionOutcome.match(PROPERTY + " property is not empty.");
}
catch (NoSuchElementException e) {
return ConditionOutcome.noMatch("Missing required property 'value' of @ConditionalOnPropertyExists");
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyExistsCondition.class)
@interface ConditionalOnPropertyExists {
}
}

View File

@@ -20,9 +20,15 @@ import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ObjectUtils;
@@ -35,6 +41,8 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
@ConfigurationProperties("spring.cloud.gateway.x-forwarded")
public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(XForwardedHeadersFilter.class);
/** Default http port. */
public static final int HTTP_PORT = 80;
@@ -98,6 +106,19 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
/** If appending X-Forwarded-Prefix as a list is enabled. */
private boolean prefixAppend = true;
private final TrustedProxies trustedProxies;
@Deprecated
public XForwardedHeadersFilter() {
trustedProxies = s -> true;
log.warn(GatewayProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
}
public XForwardedHeadersFilter(String trustedProxiesRegex) {
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
}
@Override
public int getOrder() {
return this.order;
@@ -197,8 +218,15 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
@Override
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
if (request.getRemoteAddress() != null
&& !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.getRemoteAddress()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -206,9 +234,13 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
updated.addAll(entry.getKey(), entry.getValue());
}
if (isForEnabled() && request.getRemoteAddress() != null && request.getRemoteAddress().getAddress() != null) {
String remoteAddr = request.getRemoteAddress().getAddress().getHostAddress();
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend());
if (isForEnabled()) {
String remoteAddr = null;
if (request.getRemoteAddress() != null && request.getRemoteAddress().getAddress() != null) {
remoteAddr = request.getRemoteAddress().getHostString();
}
// match xforwarded for against trusted proxies
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend(), trustedProxies::isTrusted);
}
String proto = request.getURI().getScheme();
@@ -284,17 +316,22 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
}
private void write(HttpHeaders headers, String name, String value, boolean append) {
if (value == null) {
return;
}
write(headers, name, value, append, s -> true);
}
private void write(HttpHeaders headers, String name, String value, boolean append, Predicate<String> shouldWrite) {
if (append) {
headers.add(name, value);
if (value != null) {
headers.add(name, value);
}
// these headers should be treated as a single comma separated header
List<String> values = headers.get(name);
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
if (headers.containsKey(name)) {
List<String> values = headers.get(name).stream().filter(shouldWrite).toList();
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
}
}
else {
else if (value != null && shouldWrite.test(value)) {
headers.set(name, value);
}
}
@@ -303,11 +340,6 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
return HTTPS_SCHEME.equals(scheme) ? HTTPS_PORT : HTTP_PORT;
}
private boolean hasHeader(ServerHttpRequest request, String name) {
HttpHeaders headers = request.getHeaders();
return headers.containsKey(name) && StringUtils.hasLength(headers.getFirst(name));
}
private String toHostHeader(ServerHttpRequest request) {
int port = request.getURI().getPort();
String host = request.getURI().getHost();

View File

@@ -53,8 +53,10 @@ import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint;
import org.springframework.cloud.gateway.config.GatewayAutoConfigurationTests.CustomHttpClientFactory.CustomSslConfigurer;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
@@ -196,8 +198,8 @@ public class GatewayAutoConfigurationTests {
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasSingleBean(TokenRelayGatewayFilterFactory.class);
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class)
.hasSingleBean(TokenRelayGatewayFilterFactory.class);
});
}
@@ -215,8 +217,8 @@ public class GatewayAutoConfigurationTests {
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasBean("myReactiveOAuth2AuthorizedClientManager");
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class)
.hasBean("myReactiveOAuth2AuthorizedClientManager");
});
}
@@ -279,8 +281,8 @@ public class GatewayAutoConfigurationTests {
HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
.withPropertyValues("server.http2.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class);
assertThat(context).hasSingleBean(GRPCResponseHeadersFilter.class);
assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class)
.hasSingleBean(GRPCResponseHeadersFilter.class);
HttpClient httpClient = context.getBean(HttpClient.class);
assertThat(httpClient.configuration().protocols()).contains(HttpProtocol.HTTP11, HttpProtocol.H2);
});
@@ -294,8 +296,8 @@ public class GatewayAutoConfigurationTests {
HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
.withPropertyValues("server.http2.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(GRPCRequestHeadersFilter.class);
assertThat(context).doesNotHaveBean(GRPCResponseHeadersFilter.class);
assertThat(context).doesNotHaveBean(GRPCRequestHeadersFilter.class)
.doesNotHaveBean(GRPCResponseHeadersFilter.class);
});
}
@@ -326,6 +328,32 @@ public class GatewayAutoConfigurationTests {
});
}
@Test
public void forwardedHeaderFiltersNotEnabledByDefault() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
ServerPropertiesConfig.class))
.run(context -> {
assertThat(context).doesNotHaveBean(XForwardedHeadersFilter.class)
.doesNotHaveBean(ForwardedHeadersFilter.class);
});
}
@Test
public void forwardedHeaderFiltersEnabledWithProperties() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
ServerPropertiesConfig.class))
.withPropertyValues("spring.cloud.gateway.forwarded.enabled=true",
"spring.cloud.gateway.x-forwarded.enabled=true", "spring.cloud.gateway.trusted-proxies=.*")
.run(context -> {
assertThat(context).hasSingleBean(XForwardedHeadersFilter.class)
.hasSingleBean(ForwardedHeadersFilter.class);
});
}
@Configuration
@EnableConfigurationProperties(ServerProperties.class)
@AutoConfigureBefore(GatewayAutoConfiguration.class)

View File

@@ -66,7 +66,8 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = { "spring.cloud.gateway.httpclient.connect-timeout=500",
"spring.cloud.gateway.httpclient.response-timeout=2s",
"logging.level.org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory=TRACE" })
"logging.level.org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory=TRACE",
"spring.cloud.gateway.trusted-proxies=.*", "spring.cloud.gateway.x-forwarded.enabled=true" })
@DirtiesContext
// default filter AddResponseHeader suppresses bug
// https://github.com/spring-cloud/spring-cloud-gateway/issues/1315,

View File

@@ -24,9 +24,16 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.Forwarded;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -59,7 +66,7 @@ public class ForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -79,26 +86,37 @@ public class ForwardedHeadersFilterTests {
public void forwardedHeaderExists() throws UnknownHostException {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https; for=23.45.67.89")
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers.get(FORWARDED_HEADER)).hasSize(2);
assertThat(headers.get(FORWARDED_HEADER)).hasSize(3);
List<Forwarded> forwardeds = ForwardedHeadersFilter.parse(headers.get(FORWARDED_HEADER));
assertThat(forwardeds).hasSize(2);
Forwarded addedForwardedHeader = forwardeds.get(0);
Forwarded existingForwardedHeader = forwardeds.get(1);
assertThat(existingForwardedHeader.getValues()).containsEntry("proto", "http")
.containsEntry("for", "\"10.0.0.1:80\"");
assertThat(addedForwardedHeader.getValues()).containsEntry("proto", "https")
.containsEntry("for", "23.45.67.89");
assertThat(forwardeds).hasSize(3);
Optional<Forwarded> added = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").contains("10.0.0.1:80"))
.findFirst();
assertThat(added).isPresent();
added.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "http").containsEntry("for", "\"10.0.0.1:80\"");
});
Optional<Forwarded> existing = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").equals("23.45.67.89"))
.findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("for", "23.45.67.89");
});
existing = forwardeds.stream().filter(forwarded -> forwarded.get("for").equals("12.34.56.78")).findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "https").containsEntry("for", "12.34.56.78");
});
}
@Test
@@ -107,7 +125,7 @@ public class ForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -128,7 +146,7 @@ public class ForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -148,7 +166,7 @@ public class ForwardedHeadersFilterTests {
.remoteAddress(InetSocketAddress.createUnresolved("unresolvable-hostname", 80))
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -201,4 +219,83 @@ public class ForwardedHeadersFilterTests {
}
}
@Test
public void trustedProxiesConditionMatches() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.withPropertyValues("spring.cloud.gateway.trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(ForwardedHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(ForwardedHeadersFilter.class);
});
}
@Test
public void emptyTrustedProxiesFails() {
Assertions.assertThatThrownBy(() -> new ForwardedHeadersFilter(""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void forwardedHeadersNotTrusted() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter("11\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys(FORWARDED_HEADER);
}
// verify that existing forwarded header is not forwarded if not trusted
@Test
public void untrustedForwardedForNotAppended() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=\"127.0.0.1:80\",for=10.0.0.11")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
filtered = forwardedHeaders.stream().filter(value -> value.contains("10.0.0.11")).findFirst();
assertThat(filtered).isNotEmpty();
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=127.0.0.1")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
}
}

View File

@@ -21,8 +21,15 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.util.LinkedHashSet;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
@@ -43,17 +50,20 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
*/
public class XForwardedHeadersFilterTests {
public static final String ALLOW_ALL_REGEX = ".*";
@Test
public void remoteAddressIsNull() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER, X_FORWARDED_PROTO_HEADER);
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER)
.containsKeys(X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER, X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_HOST_HEADER)).isEqualTo("localhost:8080");
assertThat(headers.getFirst(X_FORWARDED_PORT_HEADER)).isEqualTo("8080");
@@ -67,7 +77,7 @@ public class XForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -87,7 +97,7 @@ public class XForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -110,7 +120,7 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_PROTO_HEADER, "https")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -134,7 +144,7 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_PREFIX_HEADER, "/prefix")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setForAppend(false);
filter.setHostAppend(false);
filter.setPortAppend(false);
@@ -159,7 +169,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
@@ -184,7 +194,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
@@ -210,7 +220,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
@@ -232,7 +242,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
filter.setForEnabled(false);
@@ -258,7 +268,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
filter.setForEnabled(false);
@@ -284,7 +294,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setForEnabled(false);
filter.setHostEnabled(false);
filter.setPortEnabled(false);
@@ -303,7 +313,7 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_FOR_HEADER, "10.0.0.1")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -319,11 +329,92 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_FOR_HEADER, "10.0.0.1")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys(X_FORWARDED_PROTO_HEADER, X_FORWARDED_HOST_HEADER);
}
@Test
public void trustedProxiesConditionMatches() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.withPropertyValues(GatewayProperties.PREFIX + ".trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(XForwardedHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(XForwardedHeadersFilter.class);
});
}
@Test
public void emptyTrustedProxiesFails() {
Assertions.assertThatThrownBy(() -> new XForwardedHeadersFilter(""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void xForwardedHeadersNotTrusted() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter("11\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
}
// : verify that existing x-forwarded-* headers are not forwarded
// if x-forwarded-for is not trusted
@Test
public void untrustedXForwardedForNotAppended() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.header(X_FORWARDED_FOR_HEADER, "10.0.0.10")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1")
.contains("10.0.0.1", "10.0.0.10");
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1");
}
}

View File

@@ -57,7 +57,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.test.TestUtils.getMap;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = { "spring.cloud.gateway.forwarded.enabled=true", "spring.cloud.gateway.x-forwarded.enabled=true",
"spring.cloud.gateway.trusted-proxies=.*",
"logging.level.org.springframework.cloud.gateway.filter.headers=TRACE" })
@DirtiesContext
@SuppressWarnings("unchecked")
class GatewayIntegrationTests extends BaseWebClientTests {

View File

@@ -22,3 +22,4 @@ spring:
filters:
- SetPath=/httpbin/
- SetStatus=200
trusted-proxies: .*