From 9c00e60ee4cfbf160ab272380ebc2381642a8039 Mon Sep 17 00:00:00 2001 From: Spring Operator Date: Sat, 16 Mar 2019 08:43:40 -0700 Subject: [PATCH 1/3] URL Cleanup (#906) This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). # Fixed URLs ## Fixed Success These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * http://maven.apache.org/xsd/maven-4.0.0.xsd with 8 occurrences migrated to: https://maven.apache.org/xsd/maven-4.0.0.xsd ([https](https://maven.apache.org/xsd/maven-4.0.0.xsd) result 200). * http://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch with 1 occurrences migrated to: https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch ([https](https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch) result 200). * http://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin with 1 occurrences migrated to: https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin ([https](https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin) result 200). * http://www.apache.org/licenses/LICENSE-2.0 with 3 occurrences migrated to: https://www.apache.org/licenses/LICENSE-2.0 ([https](https://www.apache.org/licenses/LICENSE-2.0) result 200). * http://www.spring.io with 1 occurrences migrated to: https://www.spring.io ([https](https://www.spring.io) result 301). # Ignored These URLs were intentionally ignored. * http://maven.apache.org/POM/4.0.0 with 16 occurrences * http://www.w3.org/2001/XMLSchema-instance with 8 occurrences --- docs/pom.xml | 2 +- docs/src/main/asciidoc/ghpages.sh | 4 ++-- mvnw | 2 +- mvnw.cmd | 2 +- pom.xml | 6 +++--- spring-cloud-gateway-core/pom.xml | 2 +- spring-cloud-gateway-dependencies/pom.xml | 2 +- spring-cloud-gateway-mvc/pom.xml | 2 +- spring-cloud-gateway-sample/pom.xml | 2 +- spring-cloud-gateway-webflux/pom.xml | 2 +- spring-cloud-starter-gateway/pom.xml | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/pom.xml b/docs/pom.xml index 5605c6ab..d0b4536e 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.cloud diff --git a/docs/src/main/asciidoc/ghpages.sh b/docs/src/main/asciidoc/ghpages.sh index 57c5da3a..55e76be1 100755 --- a/docs/src/main/asciidoc/ghpages.sh +++ b/docs/src/main/asciidoc/ghpages.sh @@ -40,7 +40,7 @@ function check_if_anything_to_sync() { function retrieve_current_branch() { # Code getting the name of the current branch. For master we want to publish as we did until now - # http://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch + # https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch # If there is a branch already passed will reuse it - otherwise will try to find it CURRENT_BRANCH=${BRANCH} if [[ -z "${CURRENT_BRANCH}" ]] ; then @@ -147,7 +147,7 @@ function copy_docs_for_current_version() { COMMIT_CHANGES="yes" else echo -e "Current branch is [${CURRENT_BRANCH}]" - # http://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin + # https://stackoverflow.com/questions/29300806/a-bash-script-to-check-if-a-string-is-present-in-a-comma-separated-list-of-strin if [[ ",${WHITELISTED_BRANCHES_VALUE}," = *",${CURRENT_BRANCH},"* ]] ; then mkdir -p ${ROOT_FOLDER}/${CURRENT_BRANCH} echo -e "Branch [${CURRENT_BRANCH}] is whitelisted! Will copy the current docs to the [${CURRENT_BRANCH}] folder" diff --git a/mvnw b/mvnw index a69491ac..675cb6b0 100755 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 diff --git a/mvnw.cmd b/mvnw.cmd index 2b934e89..4b98b78c 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -7,7 +7,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM https://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an diff --git a/pom.xml b/pom.xml index 25fadc85..e571ac52 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.cloud @@ -28,7 +28,7 @@ Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 + https://www.apache.org/licenses/LICENSE-2.0 @@ -37,7 +37,7 @@ Spencer Gibb sgibb at pivotal.io Pivotal Software, Inc. - http://www.spring.io + https://www.spring.io Project lead diff --git a/spring-cloud-gateway-core/pom.xml b/spring-cloud-gateway-core/pom.xml index dcb68188..6114a440 100644 --- a/spring-cloud-gateway-core/pom.xml +++ b/spring-cloud-gateway-core/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 9d99e3b4..ca494d03 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-dependencies-parent diff --git a/spring-cloud-gateway-mvc/pom.xml b/spring-cloud-gateway-mvc/pom.xml index eccc068a..0b04cdf1 100644 --- a/spring-cloud-gateway-mvc/pom.xml +++ b/spring-cloud-gateway-mvc/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-gateway-mvc diff --git a/spring-cloud-gateway-sample/pom.xml b/spring-cloud-gateway-sample/pom.xml index cff08dd5..84020cef 100644 --- a/spring-cloud-gateway-sample/pom.xml +++ b/spring-cloud-gateway-sample/pom.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-gateway-sample diff --git a/spring-cloud-gateway-webflux/pom.xml b/spring-cloud-gateway-webflux/pom.xml index 35fd47de..5a2dfce3 100644 --- a/spring-cloud-gateway-webflux/pom.xml +++ b/spring-cloud-gateway-webflux/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-gateway-webflux diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index ab5f156b..5d5a300a 100644 --- a/spring-cloud-starter-gateway/pom.xml +++ b/spring-cloud-starter-gateway/pom.xml @@ -1,6 +1,6 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.cloud From 0beff09af2495c20486204fb0fa025bd709dfbd3 Mon Sep 17 00:00:00 2001 From: vpavlyuk Date: Wed, 20 Mar 2019 16:00:50 -0400 Subject: [PATCH 2/3] Dedupe response header filter (#866) Filter that de-duplicates response filter values. Comes handy when e.g. both the gateway CORS logic and a downstream add Access-Control-Allow-Credentials and Access-Control-Allow-Origin headers. See https://github.com/spring-cloud/spring-cloud-gateway/issues/728 for additional context. --- .../main/asciidoc/spring-cloud-gateway.adoc | 22 ++- .../config/GatewayAutoConfiguration.java | 6 + ...upeResponseHeaderGatewayFilterFactory.java | 149 ++++++++++++++++++ .../route/builder/GatewayFilterSpec.java | 13 ++ ...sponseHeaderGatewayFilterFactoryTests.java | 55 +++++++ ...seHeaderGatewayFilterFactoryUnitTests.java | 113 +++++++++++++ .../cloud/gateway/test/AdhocTestSuite.java | 2 + .../src/test/resources/application.yml | 21 +++ 8 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 31615a79..3a3cbaaa 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -369,7 +369,7 @@ spring: cloud: gateway: routes: - - id: add_request_header_route + - id: add_response_header_route uri: http://example.org filters: - AddResponseHeader=X-Response-Foo, Bar @@ -377,6 +377,26 @@ spring: This will add `X-Response-Foo:Bar` header to the downstream response's headers for all matching requests. +=== DedupeResponseHeader GatewayFilter Factory +The DedupeResponseHeader GatewayFilter Factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a list of header names, space separated. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: dedupe_response_header_route + uri: http://example.org + filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin +---- + +This will remove duplicate values of `Access-Control-Allow-Credentials` and `Access-Control-Allow-Origin` response headers in cases when both the gateway CORS logic and the downstream add them. + +The DedupeResponseHeader filter also accepts an optional `strategy` parameter. The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`. + === Hystrix GatewayFilter Factory https://github.com/Netflix/Hystrix[Hystrix] is a library from Netflix that implements the https://martinfowler.com/bliki/CircuitBreaker.html[circuit breaker pattern]. The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 907420da..b922c904 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -58,6 +58,7 @@ import org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter; import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; @@ -502,6 +503,11 @@ public class GatewayAutoConfiguration { } } + @Bean + public DedupeResponseHeaderGatewayFilterFactory dedupeResponseHeaderGatewayFilterFactory() { + return new DedupeResponseHeaderGatewayFilterFactory(); + } + @Bean public ModifyRequestBodyGatewayFilterFactory modifyRequestBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) { return new ModifyRequestBodyGatewayFilterFactory(codecConfigurer); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..f9c547ed --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactory.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.cloud.gateway.filter.factory; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.http.HttpHeaders; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/* +Use case: Both your legacy backend and your API gateway add CORS header values. So, your consumer ends up with + Access-Control-Allow-Credentials: true, true + Access-Control-Allow-Origin: https://musk.mars, https://musk.mars +(The one from the gateway will be the first of the two.) To fix, add + DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin + +Configuration parameters: +- name + String representing response header names, space separated. Required. +- strategy + RETAIN_FIRST - Default. Retain the first value only. + RETAIN_LAST - Retain the last value only. + RETAIN_UNIQUE - Retain all unique values in the order of their first encounter. + +Example 1 + default-filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials + +Response header Access-Control-Allow-Credentials: true, false +Modified response header Access-Control-Allow-Credentials: true + +Example 2 + default-filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_LAST + +Response header Access-Control-Allow-Credentials: true, false +Modified response header Access-Control-Allow-Credentials: false + +Example 3 + default-filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_UNIQUE + +Response header Access-Control-Allow-Credentials: true, true +Modified response header Access-Control-Allow-Credentials: true + */ + +/** + * @author Vitaliy Pavlyuk + */ +public class DedupeResponseHeaderGatewayFilterFactory + extends AbstractGatewayFilterFactory { + + private static final String STRATEGY_KEY = "strategy"; + + public DedupeResponseHeaderGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(NAME_KEY, STRATEGY_KEY); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> { + dedupe(exchange.getResponse().getHeaders(), config); + })); + } + + public enum Strategy { + /* + Default: Retain the first value only. + */ + RETAIN_FIRST, + + /* + Retain the last value only. + */ + RETAIN_LAST, + + /* + Retain all unique values in the order of their first encounter. + */ + RETAIN_UNIQUE + } + + void dedupe(HttpHeaders headers, Config config) { + String names = config.getName(); + Strategy strategy = config.getStrategy(); + if (headers == null || names == null || strategy == null) { + return; + } + for (String name : names.split(" ")) { + dedupe(headers, name.trim(), strategy); + } + } + + private void dedupe(HttpHeaders headers, String name, Strategy strategy) { + List values = headers.get(name); + if (values == null || values.size() <= 1) { + return; + } + switch (strategy) { + case RETAIN_FIRST: + headers.set(name, values.get(0)); + break; + case RETAIN_LAST: + headers.set(name, values.get(values.size() - 1)); + break; + case RETAIN_UNIQUE: + headers.put(name, values.stream().distinct().collect(Collectors.toList())); + break; + default: + break; + } + } + + public static class Config extends AbstractGatewayFilterFactory.NameConfig { + private Strategy strategy = Strategy.RETAIN_FIRST; + + public Strategy getStrategy() { + return strategy; + } + + public Config setStrategy(Strategy strategy) { + this.strategy = strategy; + return this; + } + } +} diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java index 74d207e3..7cbadf00 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java @@ -39,6 +39,8 @@ import org.springframework.cloud.gateway.filter.factory.AbstractChangeRequestUri import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy; import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; @@ -173,6 +175,17 @@ public class GatewayFilterSpec extends UriSpec { .apply(c -> c.setName(headerName).setValue(headerValue))); } + /** + * A filter that removes duplication on a response header before it is returned to the client by the Gateway. + * @param headerName the header name(s), space separated + * @param strategy RETAIN_FIRST, RETAIN_LAST, or RETAIN_UNIQUE + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec dedupeResponseHeader(String headerName, String strategy) { + return filter(getBean(DedupeResponseHeaderGatewayFilterFactory.class) + .apply(c -> c.setStrategy(Strategy.valueOf(strategy)).setName(headerName))); + } + /** * Wraps the route in a Hystrix command. * Depends on @{code org.springframework.cloud::spring-cloud-starter-netflix-hystrix} being on the classpath, diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..c96f08f8 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.cloud.gateway.filter.factory; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class DedupeResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { + + @Test + public void dedupeResponseHeaderFilterWorks() { + testClient.get() + .uri("/headers") + .header("Host", "www.deduperesponseheader.org") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("Access-Control-Allow-Credentials", "true") + .expectHeader().valueEquals("Access-Control-Allow-Origin", "https://musk.mars") + .expectHeader().valueEquals("Scout-Cookie", "S'mores") + .expectHeader().valueEquals("Next-Week-Lottery-Numbers", "4", "2", "42"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java new file mode 100644 index 00000000..3d2c291e --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/DedupeResponseHeaderGatewayFilterFactoryUnitTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.springframework.cloud.gateway.filter.factory; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpHeaders; + +import java.util.ArrayList; +import java.util.Arrays; + +public class DedupeResponseHeaderGatewayFilterFactoryUnitTests { + + private static final String NAME_1 = HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN; + private static final String NAME_2 = HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS; + + private HttpHeaders headers; + private DedupeResponseHeaderGatewayFilterFactory.Config config; + private DedupeResponseHeaderGatewayFilterFactory filter; + + @Before + public void setUp() { + headers = Mockito.mock(HttpHeaders.class); + config = new DedupeResponseHeaderGatewayFilterFactory.Config(); + filter = new DedupeResponseHeaderGatewayFilterFactory(); + } + + @Test + public void dedupNullName() { + filter.dedupe(headers, config); + Mockito.verify(headers, Mockito.never()).get(Mockito.anyString()); + Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void dedupNullValues() { + config.setName(NAME_1); + Mockito.when(headers.get(NAME_1)).thenReturn(null); + filter.dedupe(headers, config); + Mockito.verify(headers).get(NAME_1); + Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void dedupEmptyValues() { + config.setName(NAME_1); + Mockito.when(headers.get(NAME_1)).thenReturn(new ArrayList<>()); + filter.dedupe(headers, config); + Mockito.verify(headers).get(NAME_1); + Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void dedupSingleValue() { + config.setName(NAME_1); + Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("1")); + filter.dedupe(headers, config); + Mockito.verify(headers).get(NAME_1); + Mockito.verify(headers, Mockito.never()).set(Mockito.anyString(), Mockito.anyString()); + } + + + @Test + public void dedupMultipleValuesRetainFirst() { + config.setName(NAME_1 + " " + NAME_2); + Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4")); + Mockito.when(headers.get(NAME_2)).thenReturn(Arrays.asList("true", "false")); + filter.dedupe(headers, config); + Mockito.verify(headers).get(NAME_1); + Mockito.verify(headers).set(NAME_1, "2"); + Mockito.verify(headers).get(NAME_2); + Mockito.verify(headers).set(NAME_2, "true"); + Mockito.verify(headers, Mockito.times(2)).set(Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void dedupMultipleValuesRetainLast() { + config.setName(NAME_1); + config.setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_LAST); + Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4")); + filter.dedupe(headers, config); + Mockito.verify(headers).get(NAME_1); + Mockito.verify(headers).set(NAME_1, "4"); + Mockito.verify(headers).set(Mockito.anyString(), Mockito.anyString()); + } + + @Test + public void dedupMultipleValuesRetainUnique() { + config.setName(NAME_1); + config.setStrategy(DedupeResponseHeaderGatewayFilterFactory.Strategy.RETAIN_UNIQUE); + Mockito.when(headers.get(NAME_1)).thenReturn(Arrays.asList("2", "3", "3", "4")); + filter.dedupe(headers, config); + Mockito.verify(headers).get(NAME_1); + Mockito.verify(headers).put(Mockito.eq(NAME_1), Mockito.eq(Arrays.asList("2", "3", "4"))); + Mockito.verify(headers).put(Mockito.anyString(), Mockito.anyList()); + } +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java index 49ff4f8a..224ac172 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/AdhocTestSuite.java @@ -27,6 +27,7 @@ import org.junit.runners.model.Statement; import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactoryTests; import org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactoryTests; +import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactoryTests; import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactoryTests; import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactoryTests; import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactoryTests; @@ -90,6 +91,7 @@ import static org.junit.Assume.assumeThat; SetPathGatewayFilterFactoryIntegrationTests.class, AddRequestParameterGatewayFilterFactoryTests.class, SetResponseHeaderGatewayFilterFactoryTests.class, + DedupeResponseHeaderGatewayFilterFactoryTests.class, RewriteResponseHeaderGatewayFilterFactoryTests.class, PrincipalNameKeyResolverIntegrationTests.class, RedisRateLimiterTests.class, diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index c91b96a5..22a60d80 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -66,6 +66,27 @@ spring: - AddResponseHeader=X-Request-Foo, Bar # ===================================== + - id: dedupe_response_header_test + uri: ${test.uri} + predicates: + - Host=**.deduperesponseheader.org + - Path=/headers + filters: + - AddResponseHeader=Access-Control-Allow-Credentials, true + - AddResponseHeader=Access-Control-Allow-Credentials, false + - AddResponseHeader=Access-Control-Allow-Origin, https://musk.mars + - AddResponseHeader=Access-Control-Allow-Origin, * + - AddResponseHeader=Scout-Cookie, Thin Mints + - AddResponseHeader=Scout-Cookie, S'mores + - AddResponseHeader=Next-Week-Lottery-Numbers, 4 + - AddResponseHeader=Next-Week-Lottery-Numbers, 2 + - AddResponseHeader=Next-Week-Lottery-Numbers, 2 + - AddResponseHeader=Next-Week-Lottery-Numbers, 42 + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST + - DedupeResponseHeader=Scout-Cookie, RETAIN_LAST + - DedupeResponseHeader=Next-Week-Lottery-Numbers, RETAIN_UNIQUE + + # ===================================== - id: rewrite_response_header_test uri: ${test.uri} predicates: From 3168821a2e7e5f739a9d270730a5a74bffab7ae7 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Wed, 20 Mar 2019 16:01:55 -0400 Subject: [PATCH 3/3] Use raw path when passing path to PathContainer.parsePath (#926) Fixes gh-882 --- .../handler/predicate/PathRoutePredicateFactory.java | 2 +- .../predicate/PathRoutePredicateFactoryTests.java | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index 5be0dcec..e7f32c23 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -64,7 +64,7 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory { - PathContainer path = parsePath(exchange.getRequest().getURI().getPath()); + PathContainer path = parsePath(exchange.getRequest().getURI().getRawPath()); boolean match = config.pathPattern.matches(path); traceMatch("Pattern", config.pathPattern.getPatternString(), path, match); diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java index 70e50843..5cc161ad 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java @@ -61,6 +61,16 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests { .expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); } + @Test + public void pathRouteWorksWithPercent() { + testClient.get().uri("/abc/123%/function") + .header(HttpHeaders.HOST, "www.path.org") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName()) + .expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test"); + } + @EnableAutoConfiguration @SpringBootConfiguration @Import(DefaultTestConfig.class)