Adds support for trusted-proxies property.
spring.cloud.gateway.trusted-proxies
This commit is contained in:
@@ -489,7 +489,7 @@ Route filters allow the modification of the incoming HTTP request or outgoing HT
|
||||
Route filters are scoped to a particular route.
|
||||
Spring Cloud Gateway includes many built-in GatewayFilter Factories.
|
||||
|
||||
NOTE: For more detailed examples of how to use any of the following filters, take a look at the https://github.com/spring-cloud/spring-cloud-gateway/tree/master/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory[unit tests].
|
||||
NOTE: For more detailed examples of how to use any of the following filters, take a look at the https://github.com/spring-cloud/spring-cloud-gateway/tree/3.1.x/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory[unit tests].
|
||||
|
||||
=== The `AddRequestHeader` `GatewayFilter` Factory
|
||||
|
||||
@@ -620,31 +620,6 @@ spring:
|
||||
----
|
||||
====
|
||||
|
||||
=== The `DedupeResponseHeader` `GatewayFilter` Factory
|
||||
|
||||
The DedupeResponseHeader GatewayFilter factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a space-separated list of header names.
|
||||
The following example configures a `DedupeResponseHeader` `GatewayFilter`:
|
||||
|
||||
.application.yml
|
||||
====
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: dedupe_response_header_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
----
|
||||
====
|
||||
|
||||
This removes duplicate values of `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` response headers in cases when both the gateway CORS logic and the downstream logic add them.
|
||||
|
||||
The `DedupeResponseHeader` filter also accepts an optional `strategy` parameter.
|
||||
The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`.
|
||||
|
||||
[[spring-cloud-circuitbreaker-filter-factory]]
|
||||
=== Spring Cloud CircuitBreaker GatewayFilter Factory
|
||||
|
||||
@@ -802,6 +777,30 @@ public RouteLocator routes(RouteLocatorBuilder builder) {
|
||||
----
|
||||
====
|
||||
|
||||
=== The `DedupeResponseHeader` `GatewayFilter` Factory
|
||||
|
||||
The `DedupeResponseHeader` GatewayFilter factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a space-separated list of header names.
|
||||
The following example configures a `DedupeResponseHeader` `GatewayFilter`:
|
||||
|
||||
.application.yml
|
||||
====
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: dedupe_response_header_route
|
||||
uri: https://example.org
|
||||
filters:
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
----
|
||||
====
|
||||
|
||||
This removes duplicate values of `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` response headers in cases when both the gateway CORS logic and the downstream logic add them.
|
||||
|
||||
The `DedupeResponseHeader` filter also accepts an optional `strategy` parameter.
|
||||
The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`.
|
||||
|
||||
|
||||
[[fallback-headers]]
|
||||
@@ -849,6 +848,142 @@ You can overwrite the names of the headers in the configuration by setting the v
|
||||
|
||||
For more information on circuit breakers and the gateway see the <<spring-cloud-circuitbreaker-filter-factory, Spring Cloud CircuitBreaker Factory section>>.
|
||||
|
||||
=== The `JsonToGrpc` `GatewayFilter` Factory
|
||||
|
||||
The JSONToGRPCFilter GatewayFilter Factory converts a JSON payload to a gRPC request.
|
||||
|
||||
The filter takes the following arguments:
|
||||
|
||||
* `protoDescriptor`: Proto descriptor file.
|
||||
|
||||
This file can be generated using `protoc` and specifying the `--descriptor_set_out` flag:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
protoc --proto_path=src/main/resources/proto/ \
|
||||
--descriptor_set_out=src/main/resources/proto/hello.pb \
|
||||
src/main/resources/proto/hello.proto
|
||||
----
|
||||
|
||||
* `protoFile`: Proto definition file.
|
||||
|
||||
* `service`: Short name of the service that handles the request.
|
||||
|
||||
* `method`: Method name in the service that handles the request.
|
||||
|
||||
NOTE: `streaming` is not supported.
|
||||
|
||||
|
||||
*application.yml.*
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public RouteLocator routes(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("json-grpc", r -> r.path("/json/hello").filters(f -> {
|
||||
String protoDescriptor = "file:src/main/proto/hello.pb";
|
||||
String protoFile = "file:src/main/proto/hello.proto";
|
||||
String service = "HelloService";
|
||||
String method = "hello";
|
||||
return f.jsonToGRPC(protoDescriptor, protoFile, service, method);
|
||||
}).uri(uri))
|
||||
----
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: json-grpc
|
||||
uri: https://localhost:6565/testhello
|
||||
predicates:
|
||||
- Path=/json/**
|
||||
filters:
|
||||
- name: JsonToGrpc
|
||||
args:
|
||||
protoDescriptor: file:proto/hello.pb
|
||||
protoFile: file:proto/hello.proto
|
||||
service: HelloService
|
||||
method: hello
|
||||
|
||||
----
|
||||
|
||||
When a request is made through the gateway to `/json/hello`, the request is transformed by using the definition provided in `hello.proto`, sent to `HelloService/hello`, and the response back is transformed to JSON.
|
||||
|
||||
By default, it creates a `NettyChannel` by using the default `TrustManagerFactory`. However, you can customize this `TrustManager` by creating a bean of type `GrpcSslConfigurer`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@Configuration
|
||||
public class GRPCLocalConfiguration {
|
||||
@Bean
|
||||
public GRPCSSLContext sslContext() {
|
||||
TrustManager trustManager = trustAllCerts();
|
||||
return new GRPCSSLContext(trustManager);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[local-cache-response-filter]]
|
||||
=== The `LocalResponseCache` `GatewayFilter` Factory
|
||||
|
||||
This filter allows caching the response body and headers to follow these rules:
|
||||
|
||||
* It can only cache bodiless GET requests.
|
||||
* It caches the response only for one of the following status codes: HTTP 200 (OK), HTTP 206 (Partial Content), or HTTP 301 (Moved Permanently).
|
||||
* Response data is not cached if `Cache-Control` header does not allow it (`no-store` present in the request or `no-store` or `private` present in the response).
|
||||
* If the response is already cached and a new request is performed with no-cache value in `Cache-Control` header, it returns a bodiless response with 304 (Not Modified).
|
||||
|
||||
This filter configures the local response cache per route and is available only if the `spring.cloud.gateway.filter.local-response-cache.enabled` property is enabled. And a <<local-cache-response-global-filter, local response cache configured globally>> is also available as feature.
|
||||
|
||||
It accepts the first parameter to override the time to expire a cache entry (expressed in `s` for seconds, `m` for minutes, and `h` for hours) and a second parameter to set the maximum size of the cache to evict entries for this route (`KB`, `MB`, or `GB`).
|
||||
|
||||
The following listing shows how to add local response cache `GatewayFilter`:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public RouteLocator routes(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.localResponseCache(Duration.ofMinutes(30), "500MB")
|
||||
).uri(uri))
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
or this
|
||||
|
||||
.application.yaml
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: resource
|
||||
uri: http://localhost:9000
|
||||
predicates:
|
||||
- Path=/resource
|
||||
filters:
|
||||
- LocalResponseCache=30m,500MB
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: This filter also automatically calculates the `max-age` value in the HTTP `Cache-Control` header.
|
||||
Only if `max-age` is present on the original response is the value rewritten with the number of seconds set in the `timeToLive` configuration parameter.
|
||||
In consecutive calls, this value is recalculated with the number of seconds left until the response expires.
|
||||
|
||||
NOTE: To enable this feature, add `com.github.ben-manes.caffeine:caffeine` and `spring-boot-starter-cache` as project dependencies.
|
||||
|
||||
WARNING: If your project creates custom `CacheManager` beans, it will either need to be marked with `@Primary` or injected using `@Qualifier`.
|
||||
|
||||
|
||||
=== The `MapRequestHeader` `GatewayFilter` Factory
|
||||
|
||||
The `MapRequestHeader` `GatewayFilter` factory takes `fromHeader` and `toHeader` parameters.
|
||||
@@ -872,7 +1007,77 @@ spring:
|
||||
----
|
||||
====
|
||||
|
||||
This adds `X-Request-Red:<values>` header to the downstream request with updated values from the incoming HTTP request's `Blue` header.
|
||||
This adds the `X-Request-Red:<values>` header to the downstream request with updated values from the incoming HTTP request's `Blue` header.
|
||||
|
||||
=== The `ModifyRequestBody` `GatewayFilter` Factory
|
||||
|
||||
You can use the `ModifyRequestBody` filter to modify the request body before it is sent downstream by the gateway.
|
||||
|
||||
NOTE: This filter can be configured only by using the Java DSL.
|
||||
|
||||
The following listing shows how to modify a request body `GatewayFilter`:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public RouteLocator routes(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
|
||||
(exchange, s) -> Mono.just(new Hello(s.toUpperCase())))).uri(uri))
|
||||
.build();
|
||||
}
|
||||
|
||||
static class Hello {
|
||||
String message;
|
||||
|
||||
public Hello() { }
|
||||
|
||||
public Hello(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: If the request has no body, the `RewriteFilter` is passed `null`. `Mono.empty()` should be returned to assign a missing body in the request.
|
||||
|
||||
====
|
||||
|
||||
|
||||
=== The `ModifyResponseBody` `GatewayFilter` Factory
|
||||
|
||||
You can use the `ModifyResponseBody` filter to modify the response body before it is sent back to the client.
|
||||
|
||||
NOTE: This filter can be configured only by using the Java DSL.
|
||||
|
||||
The following listing shows how to modify a response body `GatewayFilter`:
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
public RouteLocator routes(RouteLocatorBuilder builder) {
|
||||
return builder.routes()
|
||||
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
|
||||
.filters(f -> f.prefixPath("/httpbin")
|
||||
.modifyResponseBody(String.class, String.class,
|
||||
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri))
|
||||
.build();
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: If the response has no body, the `RewriteFilter` is passed `null`. `Mono.empty()` should be returned to assign a missing body in the response.
|
||||
====
|
||||
|
||||
=== The `PrefixPath` `GatewayFilter` Factory
|
||||
|
||||
@@ -1094,7 +1299,7 @@ spring:
|
||||
|
||||
This removes the `X-Request-Foo` header before it is sent downstream.
|
||||
|
||||
=== `RemoveResponseHeader` `GatewayFilter` Factory
|
||||
=== The `RemoveResponseHeader` `GatewayFilter` Factory
|
||||
|
||||
The `RemoveResponseHeader` `GatewayFilter` factory takes a `name` parameter.
|
||||
It is the name of the header to be removed.
|
||||
@@ -2100,7 +2305,7 @@ or check if an exchange has already been routed.
|
||||
HttpHeadersFilters are applied to requests before sending them downstream, such as in the `NettyRoutingFilter`.
|
||||
|
||||
=== 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
|
||||
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].
|
||||
@@ -2118,7 +2323,7 @@ The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The
|
||||
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
|
||||
The `XForwarded` Headers Filter creates various a `X-Forwarded-*` headers to send to the downstream service. It users 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 `X-Forwarded-For` header.
|
||||
|
||||
Creating of individual headers can be controlled by the following boolean properties (defaults to true):
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.gateway.config;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
@@ -52,6 +53,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;
|
||||
@@ -110,6 +112,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;
|
||||
@@ -269,9 +272,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
|
||||
@@ -282,9 +285,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
|
||||
@@ -685,6 +688,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) {
|
||||
|
||||
@@ -69,6 +69,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;
|
||||
}
|
||||
@@ -104,11 +110,20 @@ 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).toString();
|
||||
.append("failOnRouteDefinitionError", failOnRouteDefinitionError)
|
||||
.append("trustedProxies", trustedProxies).toString();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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 value = context.getEnvironment().getProperty(PROPERTY);
|
||||
if (!StringUtils.hasText(value)) {
|
||||
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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -56,8 +56,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;
|
||||
@@ -347,6 +349,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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.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;
|
||||
@@ -58,7 +65,7 @@ public class ForwardedHeadersFilterTests {
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
|
||||
.header(HttpHeaders.HOST, "myhost").build();
|
||||
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
|
||||
|
||||
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
|
||||
|
||||
@@ -77,25 +84,36 @@ 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").build();
|
||||
.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
|
||||
@@ -103,7 +121,7 @@ public class ForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
|
||||
.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));
|
||||
|
||||
@@ -123,7 +141,7 @@ public class ForwardedHeadersFilterTests {
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("2001:db8:cafe:0:0:0:0:17"), 80))
|
||||
.header(HttpHeaders.HOST, "myhost").build();
|
||||
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
|
||||
|
||||
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
|
||||
|
||||
@@ -142,7 +160,7 @@ public class ForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
|
||||
.remoteAddress(InetSocketAddress.createUnresolved("unresolvable-hostname", 80)).build();
|
||||
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
|
||||
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
|
||||
|
||||
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
|
||||
|
||||
@@ -195,4 +213,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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.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,16 +50,19 @@ 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");
|
||||
@@ -65,7 +75,7 @@ public class XForwardedHeadersFilterTests {
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
|
||||
.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));
|
||||
|
||||
@@ -84,7 +94,7 @@ public class XForwardedHeadersFilterTests {
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
|
||||
.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));
|
||||
|
||||
@@ -104,7 +114,7 @@ public class XForwardedHeadersFilterTests {
|
||||
.header(X_FORWARDED_FOR_HEADER, "192.168.0.2").header(X_FORWARDED_HOST_HEADER, "example.com")
|
||||
.header(X_FORWARDED_PORT_HEADER, "443").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));
|
||||
|
||||
@@ -125,7 +135,7 @@ public class XForwardedHeadersFilterTests {
|
||||
.header(X_FORWARDED_PORT_HEADER, "443").header(X_FORWARDED_PROTO_HEADER, "https")
|
||||
.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);
|
||||
@@ -149,7 +159,7 @@ public class XForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/prefix/get")
|
||||
.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);
|
||||
|
||||
@@ -173,7 +183,7 @@ public class XForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/resource/resource/")
|
||||
.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);
|
||||
|
||||
@@ -198,7 +208,7 @@ public class XForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/foo/bar")
|
||||
.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);
|
||||
|
||||
@@ -219,7 +229,7 @@ public class XForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/get")
|
||||
.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);
|
||||
@@ -244,7 +254,7 @@ public class XForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/get")
|
||||
.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);
|
||||
@@ -269,7 +279,7 @@ public class XForwardedHeadersFilterTests {
|
||||
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
|
||||
.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);
|
||||
@@ -287,7 +297,7 @@ public class XForwardedHeadersFilterTests {
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
|
||||
.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));
|
||||
|
||||
@@ -302,11 +312,92 @@ public class XForwardedHeadersFilterTests {
|
||||
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
|
||||
.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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.web.server.firewall.StrictServerWebExchangeFirewall;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
@@ -134,6 +135,13 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
StrictServerWebExchangeFirewall strictServerWebExchangeFirewall() {
|
||||
StrictServerWebExchangeFirewall firewall = new StrictServerWebExchangeFirewall();
|
||||
firewall.setAllowUrlEncodedPercent(true);
|
||||
return firewall;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,3 +22,4 @@ spring:
|
||||
filters:
|
||||
- SetPath=/httpbin/
|
||||
- SetStatus=200
|
||||
trusted-proxies: .*
|
||||
|
||||
Reference in New Issue
Block a user