From d349a9f4cfdf96846ac7b4d301e610df2f3e55e0 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 27 May 2025 17:05:29 +0200 Subject: [PATCH 01/10] Fix runtime hints for native images. (#3806) Signed-off-by: Olga Maciaszek-Sharma # Conflicts: # spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java # spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java --- .../GatewayServerMvcAutoConfiguration.java | 9 +- .../GatewayMvcAotRuntimeHintsRegistrar.java | 82 ------------ .../GatewayMvcRuntimeHintsProcessor.java | 120 ++++++++++++++++++ 3 files changed, 126 insertions(+), 85 deletions(-) delete mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java create mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java index 9baad4ce..b6807943 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java @@ -28,9 +28,9 @@ import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; import org.springframework.boot.web.client.RestClientCustomizer; import org.springframework.cloud.gateway.server.mvc.common.ArgumentSupplierBeanPostProcessor; -import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcAotRuntimeHintsRegistrar; import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties; import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcPropertiesBeanDefinitionRegistrar; +import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcRuntimeHintsProcessor; import org.springframework.cloud.gateway.server.mvc.config.RouterFunctionHolderFactory; import org.springframework.cloud.gateway.server.mvc.filter.FormFilter; import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter; @@ -51,7 +51,6 @@ import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscovere import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.env.Environment; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.JdkClientHttpRequestFactory; @@ -67,7 +66,6 @@ import org.springframework.web.client.RestClient; @AutoConfiguration(after = { RestTemplateAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true) @Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class) -@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class) public class GatewayServerMvcAutoConfiguration { @Bean @@ -219,4 +217,9 @@ public class GatewayServerMvcAutoConfiguration { return new XForwardedRequestHeadersFilterProperties(); } + @Bean + static GatewayMvcRuntimeHintsProcessor gatewayMvcRuntimeHintsProcessor() { + return new GatewayMvcRuntimeHintsProcessor(); + } + } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java deleted file mode 100644 index e4482d3a..00000000 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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. - */ - -package org.springframework.cloud.gateway.server.mvc.config; - -import java.util.Arrays; -import java.util.Set; - -import org.springframework.aot.hint.ExecutableMode; -import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.ReflectionHints; -import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.BodyFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerHandlerSupplier; -import org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions; -import org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions; -import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions; -import org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates; -import org.springframework.lang.NonNull; -import org.springframework.util.ClassUtils; - -/** - * AOT runtime hints registrar on the gateway server mvc. - * - * @author Jürgen Wißkirchen - */ -public class GatewayMvcAotRuntimeHintsRegistrar implements RuntimeHintsRegistrar { - - private static final Set> FUNCTION_PROVIDERS = Set.of(HandlerFunctions.class, - LoadBalancerHandlerSupplier.class, FilterFunctions.class, BeforeFilterFunctions.class, - AfterFilterFunctions.class, TokenRelayFilterFunctions.class, BodyFilterFunctions.class, - CircuitBreakerFilterFunctions.class, GatewayRouterFunctions.class, LoadBalancerFilterFunctions.class, - GatewayRequestPredicates.class, Bucket4jFilterFunctions.class); - - private static final Set> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class, - RouteProperties.class); - - @Override - public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) { - final ReflectionHints reflectionHints = hints.reflection(); - FUNCTION_PROVIDERS.forEach(clazz -> addHintsForClass(reflectionHints, clazz, classLoader)); - - PROPERTIES.forEach(clazz -> reflectionHints.registerType(clazz, MemberCategory.PUBLIC_FIELDS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)); - } - - /** - * Add hints for the given class. Since we need to register mostly static methods, the - * annotation way with @Reflective does not work here. - * @param reflectionHints the reflection hints - * @param clazz the class to add hints for - * @param classLoader the class loader - */ - private void addHintsForClass(ReflectionHints reflectionHints, Class clazz, ClassLoader classLoader) { - if (!ClassUtils.isPresent(clazz.getName(), classLoader)) { - return; // safety net - } - Arrays.stream(clazz.getMethods()) - .forEach(method -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE)); - } - -} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java new file mode 100644 index 00000000..fb78da79 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java @@ -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.config; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; +import org.springframework.core.type.filter.AssignableTypeFilter; + +/** + * A {@link BeanFactoryInitializationAotProcessor} responsible for registering reflection + * hints for Gateway MVC beans. + * + * @author Jürgen Wißkirchen + * @author Olga Maciaszek-Sharma + * @since 4.3.0 + */ +public class GatewayMvcRuntimeHintsProcessor implements BeanFactoryInitializationAotProcessor { + + private static final Log LOG = LogFactory.getLog(GatewayMvcRuntimeHintsProcessor.class); + + private static final String GATEWAY_MVC_FILTER_PACKAGE_NAME = "org.springframework.cloud.gateway.server.mvc.filter"; + + private static final String GATEWAY_MVC_PREDICATE_PACKAGE_NAME = "org.springframework.cloud.gateway.server.mvc.predicate"; + + private static final Map> beansConditionalOnClasses = Map.of( + "io.github.bucket4j.BucketConfiguration", + Set.of("org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions"), + "org.springframework.cloud.client.circuitbreaker.CircuitBreaker", + Set.of("org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions"), + "org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient", + Set.of("org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions"), + "org.springframework.retry.support.RetryTemplate", + Set.of("org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions"), + "org.springframework.security.oauth2.client.OAuth2AuthorizedClient", + Set.of("org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions")); + + private static final Set> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class, + RouteProperties.class); + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + return (generationContext, beanFactoryInitializationCode) -> { + ReflectionHints hints = generationContext.getRuntimeHints().reflection(); + Set> typesToRegister = Stream + .of(getTypesToRegister(GATEWAY_MVC_FILTER_PACKAGE_NAME), + getTypesToRegister(GATEWAY_MVC_PREDICATE_PACKAGE_NAME), PROPERTIES) + .flatMap(Set::stream) + .collect(Collectors.toSet()); + typesToRegister.forEach(clazz -> hints.registerType(TypeReference.of(clazz), + hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))); + }; + } + + private static Set> getTypesToRegister(String packageName) { + Set> classesToAdd = new HashSet<>(); + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AssignableTypeFilter(Object.class)); + Set components = provider.findCandidateComponents(packageName); + for (BeanDefinition component : components) { + Class clazz; + try { + clazz = Class.forName(component.getBeanClassName()); + if (shouldRegisterClass(clazz)) { + classesToAdd.add(clazz); + } + } + catch (NoClassDefFoundError | ClassNotFoundException exception) { + if (LOG.isDebugEnabled()) { + LOG.debug(exception); + } + } + } + return classesToAdd; + } + + private static boolean shouldRegisterClass(Class clazz) { + Set conditionClasses = beansConditionalOnClasses.getOrDefault(clazz.getName(), Collections.emptySet()); + for (String conditionClass : conditionClasses) { + try { + GatewayMvcRuntimeHintsProcessor.class.getClassLoader().loadClass(conditionClass); + } + catch (ClassNotFoundException e) { + return false; + } + } + return true; + } + +} From 738f2e8f7257d4989910f2f7d925d22bd1ed47e9 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Wed, 28 May 2025 16:54:12 +0200 Subject: [PATCH 02/10] Include interfaces for runtime hints scan. Signed-off-by: Olga Maciaszek-Sharma --- .../config/GatewayMvcRuntimeHintsProcessor.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java index fb78da79..b133a5cc 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.TypeReference; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.config.BeanDefinition; @@ -84,7 +85,7 @@ public class GatewayMvcRuntimeHintsProcessor implements BeanFactoryInitializatio private static Set> getTypesToRegister(String packageName) { Set> classesToAdd = new HashSet<>(); - ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + ClassPathScanningCandidateComponentProvider provider = buildProvider(); provider.addIncludeFilter(new AssignableTypeFilter(Object.class)); Set components = provider.findCandidateComponents(packageName); for (BeanDefinition component : components) { @@ -104,6 +105,17 @@ public class GatewayMvcRuntimeHintsProcessor implements BeanFactoryInitializatio return classesToAdd; } + private static ClassPathScanningCandidateComponentProvider buildProvider() { + return new ClassPathScanningCandidateComponentProvider(false) { + @SuppressWarnings("NullableProblems") + @Override + protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { + // Include both concrete classes and interfaces + return beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation(); + } + }; + } + private static boolean shouldRegisterClass(Class clazz) { Set conditionClasses = beansConditionalOnClasses.getOrDefault(clazz.getName(), Collections.emptySet()); for (String conditionClass : conditionClasses) { From 33ea007a6baa3d9b79f94a9932cd6fe3ecf0cae9 Mon Sep 17 00:00:00 2001 From: sgibb Date: Thu, 18 Apr 2024 11:52:24 -0400 Subject: [PATCH 03/10] Adds support for trusted-proxies property. spring.cloud.gateway.trusted-proxies and spring.cloud.gateway.mvc.trusted-proxies --- docs/modules/ROOT/nav.adoc | 1 + .../httpheadersfilters.adoc | 45 +++++ .../httpheadersfilters.adoc | 4 +- .../GatewayServerMvcAutoConfiguration.java | 16 +- .../mvc/config/GatewayMvcProperties.java | 15 ++ .../filter/ForwardedRequestHeadersFilter.java | 43 ++++- .../server/mvc/filter/TrustedProxies.java | 120 +++++++++++++ .../XForwardedRequestHeadersFilter.java | 75 ++++++-- .../server/mvc/ServerMvcIntegrationTests.java | 2 +- .../ForwardedRequestHeadersFilterTests.java | 102 +++++++++-- .../XForwardedRequestHeadersFilterTests.java | 162 ++++++++++++++++++ .../gateway/server/mvc/test/TestUtils.java | 6 +- .../src/test/resources/application.yml | 6 +- ...efaultNettyHttpForwardedHeaderHandler.java | 137 +++++++++++++++ .../config/GatewayAutoConfiguration.java | 31 +++- .../gateway/config/GatewayProperties.java | 15 ++ .../headers/ForwardedHeadersFilter.java | 52 +++++- .../filter/headers/TrustedProxies.java | 114 ++++++++++++ .../headers/XForwardedHeadersFilter.java | 66 +++++-- .../config/GatewayAutoConfigurationTests.java | 44 ++++- ...yGatewayFilterFactoryIntegrationTests.java | 3 +- .../headers/ForwardedHeadersFilterTests.java | 129 ++++++++++++-- .../headers/XForwardedHeadersFilterTests.java | 119 +++++++++++-- .../gateway/test/GatewayIntegrationTests.java | 5 +- .../resources/application-remote-address.yml | 1 + 25 files changed, 1193 insertions(+), 120 deletions(-) create mode 100644 docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/httpheadersfilters.adoc create mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/TrustedProxies.java create mode 100644 spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/DefaultNettyHttpForwardedHeaderHandler.java create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index edcb0484..fdccafb9 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -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[] diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/httpheadersfilters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/httpheadersfilters.adoc new file mode 100644 index 00000000..d60c00d7 --- /dev/null +++ b/docs/modules/ROOT/pages/spring-cloud-gateway-server-mvc/httpheadersfilters.adoc @@ -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` + diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc index e2a6ac82..d73d0445 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway/httpheadersfilters.adoc @@ -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): diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java index b6807943..31d0a727 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java @@ -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 diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java index ac553b7d..3c4c2494 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcProperties.java @@ -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 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(); } diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java index e4fedaf9..bb085b1f 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilter.java @@ -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 parse(List values) { ArrayList 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 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; diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/TrustedProxies.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/TrustedProxies.java new file mode 100644 index 00000000..2489f50d --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/TrustedProxies.java @@ -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(); + + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java index bee93756..e8b556c6 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java @@ -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 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 values = headers.get(name); - String delimitedValue = StringUtils.collectionToCommaDelimitedString(values); - headers.set(name, delimitedValue); + if (headers.containsKey(name)) { + List 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(); diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java index aaf409af..20f1408e 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/ServerMvcIntegrationTests.java @@ -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 diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilterTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilterTests.java index 4da36cdc..b5174e55 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilterTests.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/ForwardedRequestHeadersFilterTests.java @@ -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 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 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 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 forwardedHeaders = headers.get(FORWARDED_HEADER); + Optional 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 forwardedHeaders = headers.get(FORWARDED_HEADER); + Optional filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst(); + assertThat(filtered).isEmpty(); + } + } diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java new file mode 100644 index 00000000..f9c27a91 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java @@ -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"); + } + +} diff --git a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestUtils.java b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestUtils.java index c0e6556c..d8bad981 100644 --- a/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestUtils.java +++ b/spring-cloud-gateway-server-mvc/src/test/java/org/springframework/cloud/gateway/server/mvc/test/TestUtils.java @@ -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 getMap(Map map, String mapKey) { assertThat(map).isNotEmpty().containsKey(mapKey); diff --git a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml index 9bc1759e..94788e61 100644 --- a/spring-cloud-gateway-server-mvc/src/test/resources/application.yml +++ b/spring-cloud-gateway-server-mvc/src/test/resources/application.yml @@ -6,4 +6,8 @@ logging: org.springframework.retry: TRACE spring: mvc: - log-request-details: true \ No newline at end of file + log-request-details: true + cloud: + gateway: + mvc: + trusted-proxies: .* \ No newline at end of file diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/DefaultNettyHttpForwardedHeaderHandler.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/DefaultNettyHttpForwardedHeaderHandler.java new file mode 100644 index 00000000..b8893728 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/DefaultNettyHttpForwardedHeaderHandler.java @@ -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 { + + 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; + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 3a63edaf..54ad4c1e 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -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; @@ -174,6 +177,7 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; @@ -312,9 +316,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 +329,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 +761,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) { diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java index 53ea714c..95333cd6 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayProperties.java @@ -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 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(); } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java index b95cb191..e8254917 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java @@ -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 parse(List values) { ArrayList 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 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? diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java new file mode 100644 index 00000000..da80a99a --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java @@ -0,0 +1,114 @@ +/* + * 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 { + + 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 { + + } + +} diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java index 2c1e8363..bbf78c42 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java @@ -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 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 values = headers.get(name); - String delimitedValue = StringUtils.collectionToCommaDelimitedString(values); - headers.set(name, delimitedValue); + if (headers.containsKey(name)) { + List 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(); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index c85a1969..58921e99 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -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) diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java index fb1439d1..f2ecee63 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactoryIntegrationTests.java @@ -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, diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java index 73cb0003..ae24a969 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java @@ -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 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 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 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 forwardedHeaders = headers.get(FORWARDED_HEADER); + Optional 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 forwardedHeaders = headers.get(FORWARDED_HEADER); + Optional filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst(); + assertThat(filtered).isEmpty(); + } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java index 5bfcd7e8..640b02e3 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java @@ -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"); + } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index 30bade58..12fa6eab 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -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 { diff --git a/spring-cloud-gateway-server/src/test/resources/application-remote-address.yml b/spring-cloud-gateway-server/src/test/resources/application-remote-address.yml index 12795440..013e452b 100644 --- a/spring-cloud-gateway-server/src/test/resources/application-remote-address.yml +++ b/spring-cloud-gateway-server/src/test/resources/application-remote-address.yml @@ -22,3 +22,4 @@ spring: filters: - SetPath=/httpbin/ - SetStatus=200 + trusted-proxies: .* From 5540919fca20cc9a1217c28d433e1c152e863bc1 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 28 May 2025 16:24:10 -0400 Subject: [PATCH 04/10] polishing --- .../cloud/gateway/config/GatewayAutoConfiguration.java | 1 - .../cloud/gateway/filter/headers/TrustedProxies.java | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 54ad4c1e..a8d30869 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -177,7 +177,6 @@ import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java index da80a99a..732454e8 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/headers/TrustedProxies.java @@ -39,6 +39,9 @@ import org.springframework.util.StringUtils; @FunctionalInterface public interface TrustedProxies { + /** + * Property name. + */ String PROPERTY = GatewayProperties.PREFIX + ".trusted-proxies"; boolean isTrusted(String host); From 79a413ada8ae70af8b599a789b4dbb229ff54876 Mon Sep 17 00:00:00 2001 From: spring-builds Date: Wed, 28 May 2025 21:04:17 +0000 Subject: [PATCH 05/10] Update SNAPSHOT to 4.1.8 --- docs/modules/ROOT/partials/_configprops.adoc | 2 ++ docs/pom.xml | 2 +- pom.xml | 8 ++++---- spring-cloud-gateway-dependencies/pom.xml | 4 ++-- spring-cloud-gateway-integration-tests/grpc/pom.xml | 2 +- spring-cloud-gateway-integration-tests/http2/pom.xml | 2 +- spring-cloud-gateway-integration-tests/httpclient/pom.xml | 2 +- .../mvc-failure-analyzer/pom.xml | 2 +- spring-cloud-gateway-integration-tests/pom.xml | 2 +- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-server-mvc/pom.xml | 2 +- spring-cloud-gateway-server/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway-mvc/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 16 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 81866e90..70a740c2 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -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. diff --git a/docs/pom.xml b/docs/pom.xml index 6a82d9f3..a391fec6 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 spring-cloud-gateway-docs jar diff --git a/pom.xml b/pom.xml index 178f6972..bea897f5 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 pom Spring Cloud Gateway @@ -15,7 +15,7 @@ org.springframework.cloud spring-cloud-build - 4.1.6-SNAPSHOT + 4.1.5 @@ -55,8 +55,8 @@ 1.0.8.RELEASE 17 1.9.1 - 3.1.5-SNAPSHOT - 4.1.6-SNAPSHOT + 3.1.4 + 4.1.5 diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 3b4ad82e..10caa013 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -6,12 +6,12 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.1.6-SNAPSHOT + 4.1.5 spring-cloud-gateway-dependencies - 4.1.8-SNAPSHOT + 4.1.8 pom spring-cloud-gateway-dependencies diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml index c8aad0bf..00d0b3c2 100644 --- a/spring-cloud-gateway-integration-tests/grpc/pom.xml +++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml @@ -18,7 +18,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml index 0dcf83d0..8053ab0f 100644 --- a/spring-cloud-gateway-integration-tests/http2/pom.xml +++ b/spring-cloud-gateway-integration-tests/http2/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-integration-tests/httpclient/pom.xml b/spring-cloud-gateway-integration-tests/httpclient/pom.xml index 08178c87..ab24974f 100644 --- a/spring-cloud-gateway-integration-tests/httpclient/pom.xml +++ b/spring-cloud-gateway-integration-tests/httpclient/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml index 8c679b46..11405840 100644 --- a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml +++ b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index cab51af8..afe74bed 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index 4b87b3db..b224fb30 100644 --- a/spring-cloud-gateway-mvc/pom.xml +++ b/spring-cloud-gateway-mvc/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index 1bf2a253..2d01e905 100644 --- a/spring-cloud-gateway-sample/pom.xml +++ b/spring-cloud-gateway-sample/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-gateway-server-mvc/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index a5b7bd44..054d0813 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. spring-cloud-gateway-server-mvc diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml index 72dbd3d9..53928846 100644 --- a/spring-cloud-gateway-server/pom.xml +++ b/spring-cloud-gateway-server/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. spring-cloud-gateway-server diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index b4402605..007a2afc 100644 --- a/spring-cloud-gateway-webflux/pom.xml +++ b/spring-cloud-gateway-webflux/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. diff --git a/spring-cloud-starter-gateway-mvc/pom.xml b/spring-cloud-starter-gateway-mvc/pom.xml index 83a6ea16..2610ffaa 100644 --- a/spring-cloud-starter-gateway-mvc/pom.xml +++ b/spring-cloud-starter-gateway-mvc/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. spring-cloud-starter-gateway-mvc diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index 04911c29..b20b8b02 100644 --- a/spring-cloud-starter-gateway/pom.xml +++ b/spring-cloud-starter-gateway/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.8 .. spring-cloud-starter-gateway From e963342e4cf51f9a7b79c14e7b25166e6951b06b Mon Sep 17 00:00:00 2001 From: spring-builds Date: Wed, 28 May 2025 21:07:05 +0000 Subject: [PATCH 06/10] Going back to snapshots --- docs/modules/ROOT/partials/_configprops.adoc | 2 -- docs/pom.xml | 2 +- pom.xml | 8 ++++---- spring-cloud-gateway-dependencies/pom.xml | 4 ++-- spring-cloud-gateway-integration-tests/grpc/pom.xml | 2 +- spring-cloud-gateway-integration-tests/http2/pom.xml | 2 +- spring-cloud-gateway-integration-tests/httpclient/pom.xml | 2 +- .../mvc-failure-analyzer/pom.xml | 2 +- spring-cloud-gateway-integration-tests/pom.xml | 2 +- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-server-mvc/pom.xml | 2 +- spring-cloud-gateway-server/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway-mvc/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 16 files changed, 19 insertions(+), 21 deletions(-) diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 70a740c2..81866e90 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -129,7 +129,6 @@ |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. @@ -171,7 +170,6 @@ |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. diff --git a/docs/pom.xml b/docs/pom.xml index a391fec6..6a82d9f3 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT spring-cloud-gateway-docs jar diff --git a/pom.xml b/pom.xml index bea897f5..178f6972 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT pom Spring Cloud Gateway @@ -15,7 +15,7 @@ org.springframework.cloud spring-cloud-build - 4.1.5 + 4.1.6-SNAPSHOT @@ -55,8 +55,8 @@ 1.0.8.RELEASE 17 1.9.1 - 3.1.4 - 4.1.5 + 3.1.5-SNAPSHOT + 4.1.6-SNAPSHOT diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 10caa013..3b4ad82e 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -6,12 +6,12 @@ spring-cloud-dependencies-parent org.springframework.cloud - 4.1.5 + 4.1.6-SNAPSHOT spring-cloud-gateway-dependencies - 4.1.8 + 4.1.8-SNAPSHOT pom spring-cloud-gateway-dependencies diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml index 00d0b3c2..c8aad0bf 100644 --- a/spring-cloud-gateway-integration-tests/grpc/pom.xml +++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml @@ -18,7 +18,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml index 8053ab0f..0dcf83d0 100644 --- a/spring-cloud-gateway-integration-tests/http2/pom.xml +++ b/spring-cloud-gateway-integration-tests/http2/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/httpclient/pom.xml b/spring-cloud-gateway-integration-tests/httpclient/pom.xml index ab24974f..08178c87 100644 --- a/spring-cloud-gateway-integration-tests/httpclient/pom.xml +++ b/spring-cloud-gateway-integration-tests/httpclient/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml index 11405840..8c679b46 100644 --- a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml +++ b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index afe74bed..cab51af8 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index b224fb30..4b87b3db 100644 --- a/spring-cloud-gateway-mvc/pom.xml +++ b/spring-cloud-gateway-mvc/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index 2d01e905..1bf2a253 100644 --- a/spring-cloud-gateway-sample/pom.xml +++ b/spring-cloud-gateway-sample/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-gateway-server-mvc/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index 054d0813..a5b7bd44 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. spring-cloud-gateway-server-mvc diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml index 53928846..72dbd3d9 100644 --- a/spring-cloud-gateway-server/pom.xml +++ b/spring-cloud-gateway-server/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. spring-cloud-gateway-server diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index 007a2afc..b4402605 100644 --- a/spring-cloud-gateway-webflux/pom.xml +++ b/spring-cloud-gateway-webflux/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. diff --git a/spring-cloud-starter-gateway-mvc/pom.xml b/spring-cloud-starter-gateway-mvc/pom.xml index 2610ffaa..83a6ea16 100644 --- a/spring-cloud-starter-gateway-mvc/pom.xml +++ b/spring-cloud-starter-gateway-mvc/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. spring-cloud-starter-gateway-mvc diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index b20b8b02..04911c29 100644 --- a/spring-cloud-starter-gateway/pom.xml +++ b/spring-cloud-starter-gateway/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8 + 4.1.8-SNAPSHOT .. spring-cloud-starter-gateway From 84603fc6c062572bd0dc5281bdb1dd35431d9352 Mon Sep 17 00:00:00 2001 From: spring-builds Date: Wed, 28 May 2025 21:07:06 +0000 Subject: [PATCH 07/10] Bumping versions to 4.1.9-SNAPSHOT after release --- docs/pom.xml | 2 +- pom.xml | 2 +- spring-cloud-gateway-dependencies/pom.xml | 2 +- spring-cloud-gateway-integration-tests/grpc/pom.xml | 2 +- spring-cloud-gateway-integration-tests/http2/pom.xml | 2 +- spring-cloud-gateway-integration-tests/httpclient/pom.xml | 2 +- .../mvc-failure-analyzer/pom.xml | 2 +- spring-cloud-gateway-integration-tests/pom.xml | 2 +- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-server-mvc/pom.xml | 2 +- spring-cloud-gateway-server/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway-mvc/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 6a82d9f3..33237b02 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT spring-cloud-gateway-docs jar diff --git a/pom.xml b/pom.xml index 178f6972..f0dbd8bb 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT pom Spring Cloud Gateway diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 3b4ad82e..d0d6d02f 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -11,7 +11,7 @@ spring-cloud-gateway-dependencies - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT pom spring-cloud-gateway-dependencies diff --git a/spring-cloud-gateway-integration-tests/grpc/pom.xml b/spring-cloud-gateway-integration-tests/grpc/pom.xml index c8aad0bf..9353a1e4 100644 --- a/spring-cloud-gateway-integration-tests/grpc/pom.xml +++ b/spring-cloud-gateway-integration-tests/grpc/pom.xml @@ -18,7 +18,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml index 0dcf83d0..f173952e 100644 --- a/spring-cloud-gateway-integration-tests/http2/pom.xml +++ b/spring-cloud-gateway-integration-tests/http2/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/httpclient/pom.xml b/spring-cloud-gateway-integration-tests/httpclient/pom.xml index 08178c87..2bf2fea9 100644 --- a/spring-cloud-gateway-integration-tests/httpclient/pom.xml +++ b/spring-cloud-gateway-integration-tests/httpclient/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml index 8c679b46..e06a6eb4 100644 --- a/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml +++ b/spring-cloud-gateway-integration-tests/mvc-failure-analyzer/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway-integration-tests - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index cab51af8..d852d56e 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index 4b87b3db..88849667 100644 --- a/spring-cloud-gateway-mvc/pom.xml +++ b/spring-cloud-gateway-mvc/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index 1bf2a253..6a9e5639 100644 --- a/spring-cloud-gateway-sample/pom.xml +++ b/spring-cloud-gateway-sample/pom.xml @@ -16,7 +16,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-gateway-server-mvc/pom.xml b/spring-cloud-gateway-server-mvc/pom.xml index a5b7bd44..01ab4b89 100644 --- a/spring-cloud-gateway-server-mvc/pom.xml +++ b/spring-cloud-gateway-server-mvc/pom.xml @@ -22,7 +22,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. spring-cloud-gateway-server-mvc diff --git a/spring-cloud-gateway-server/pom.xml b/spring-cloud-gateway-server/pom.xml index 72dbd3d9..067b3d8b 100644 --- a/spring-cloud-gateway-server/pom.xml +++ b/spring-cloud-gateway-server/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. spring-cloud-gateway-server diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index b4402605..413ea68a 100644 --- a/spring-cloud-gateway-webflux/pom.xml +++ b/spring-cloud-gateway-webflux/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. diff --git a/spring-cloud-starter-gateway-mvc/pom.xml b/spring-cloud-starter-gateway-mvc/pom.xml index 83a6ea16..5a499ae9 100644 --- a/spring-cloud-starter-gateway-mvc/pom.xml +++ b/spring-cloud-starter-gateway-mvc/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. spring-cloud-starter-gateway-mvc diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index 04911c29..76050005 100644 --- a/spring-cloud-starter-gateway/pom.xml +++ b/spring-cloud-starter-gateway/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-gateway - 4.1.8-SNAPSHOT + 4.1.9-SNAPSHOT .. spring-cloud-starter-gateway From 98fe0acc3a835c00f0d3387b9b23dbeeecaef1f7 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 28 May 2025 17:23:14 -0400 Subject: [PATCH 08/10] Adds trusted-proxies config --- docs/modules/ROOT/partials/_configprops.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index a8eed641..e1ede206 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -131,6 +131,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. @@ -173,6 +174,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. From b60f8db2c739c5ee22668296dd8beab78c3fa1ba Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 28 May 2025 17:31:08 -0400 Subject: [PATCH 09/10] Escapes curly-braces --- .../spring-cloud-gateway/request-predicates-factories.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/spring-cloud-gateway/request-predicates-factories.adoc b/docs/modules/ROOT/pages/spring-cloud-gateway/request-predicates-factories.adoc index c1cb13ee..2a786bb9 100644 --- a/docs/modules/ROOT/pages/spring-cloud-gateway/request-predicates-factories.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-gateway/request-predicates-factories.adoc @@ -192,7 +192,7 @@ This route matches if the request path was, for example: `/red/1` or `/red/1/` o If `matchTrailingSlash` is set to `false`, then request path `/red/1/` will not be matched. -If you have set `spring.webflux.base-path` property, this will influence the path matching. The property value will be automatically prepended to the path patterns. For example, with `spring.webflux.base-path=/app` and a path pattern of `/red/{segment}`, the full pattern used for matching would be `/app/red/{segment}`. +If you have set `spring.webflux.base-path` property, this will influence the path matching. The property value will be automatically prepended to the path patterns. For example, with `spring.webflux.base-path=/app` and a path pattern of `/red/\{segment\}`, the full pattern used for matching would be `/app/red/\{segment\}`. This predicate extracts the URI template variables (such as `segment`, defined in the preceding example) as a map of names and values and places it in the `ServerWebExchange.getAttributes()` with a key defined in `ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`. Those values are then available for use by <> From 789697336a15f1d30c33031f567bcf1ccfb2fa9f Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 28 May 2025 17:48:15 -0400 Subject: [PATCH 10/10] Updates for ssl bundles --- .../gateway/filter/headers/ForwardedHeadersFilterTests.java | 5 +++-- .../gateway/filter/headers/XForwardedHeadersFilterTests.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java index ae24a969..35679dcb 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterTests.java @@ -30,6 +30,7 @@ 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.reactive.ReactiveWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -222,7 +223,7 @@ public class ForwardedHeadersFilterTests { @Test public void trustedProxiesConditionMatches() { new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class)) .withPropertyValues("spring.cloud.gateway.trusted-proxies=11\\.0\\.0\\..*") .run(context -> { @@ -233,7 +234,7 @@ public class ForwardedHeadersFilterTests { @Test public void trustedProxiesConditionDoesNotMatch() { new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class)) .run(context -> { assertThat(context).doesNotHaveBean(ForwardedHeadersFilter.class); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java index 640b02e3..e90d5231 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java @@ -25,6 +25,7 @@ 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.reactive.ReactiveWebServerFactoryAutoConfiguration; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; @@ -339,7 +340,7 @@ public class XForwardedHeadersFilterTests { @Test public void trustedProxiesConditionMatches() { new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class)) .withPropertyValues(GatewayProperties.PREFIX + ".trusted-proxies=11\\.0\\.0\\..*") .run(context -> { @@ -350,7 +351,7 @@ public class XForwardedHeadersFilterTests { @Test public void trustedProxiesConditionDoesNotMatch() { new ReactiveWebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class, ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class)) .run(context -> { assertThat(context).doesNotHaveBean(XForwardedHeadersFilter.class);