From 5ca54b6eca20dd1f78f1c7d199ecf0dbf99dd8c6 Mon Sep 17 00:00:00 2001 From: Vitaliy Pavlyuk Date: Thu, 4 Oct 2018 20:49:16 -0400 Subject: [PATCH] Rewrite response header filter factory. --- .../main/asciidoc/spring-cloud-gateway.adoc | 18 ++++ .../config/GatewayAutoConfiguration.java | 6 ++ ...iteResponseHeaderGatewayFilterFactory.java | 87 +++++++++++++++++++ .../route/builder/GatewayFilterSpec.java | 15 +++- ...sponseHeaderGatewayFilterFactoryTests.java | 52 +++++++++++ .../cloud/gateway/test/AdhocTestSuite.java | 1 + .../src/test/resources/application.yml | 10 +++ 7 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index d8245214..99aac709 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -703,6 +703,24 @@ spring: For a request path of `/foo/bar`, this will set the path to `/bar` before making the downstream request. Notice the `$\` which is replaced with `$` because of the YAML spec. +=== RewriteResponseHeader GatewayFilter Factory +The RewriteResponseHeader GatewayFilter Factory takes `name`, `regexp`, and `replacement` parameters. It uses Java regular expressions for a flexible way to rewrite the response header value. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: rewriteresponseheader_route + uri: http://example.org + filters: + - RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=*** +---- + +For a header value of `/42?user=ford&password=omg!what&flag=true`, it will be set to `/42?user=ford&password=\***&flag=true` before making the downstream request. Please use `$\` to mean `$` because of the YAML spec. + === SaveSession GatewayFilter Factory The SaveSession GatewayFilter Factory forces a `WebSession::save` operation _before_ forwarding the call downstream. This is of particular use when using something like http://projects.spring.io/spring-session/[Spring Session] with a lazy data store and need to ensure the session state has been saved before making the forwarded call. 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 39539ca9..21cc0205 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 @@ -70,6 +70,7 @@ import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewa import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SecureHeadersProperties; @@ -585,6 +586,11 @@ public class GatewayAutoConfiguration { return new SetResponseHeaderGatewayFilterFactory(); } + @Bean + public RewriteResponseHeaderGatewayFilterFactory rewriteResponseHeaderGatewayFilterFactory() { + return new RewriteResponseHeaderGatewayFilterFactory(); + } + @Bean public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() { return new SetStatusGatewayFilterFactory(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..ec7d2d55 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactory.java @@ -0,0 +1,87 @@ +/* + * 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.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Vitaliy Pavlyuk + */ +public class RewriteResponseHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory { + + public static final String REGEXP_KEY = "regexp"; + public static final String REPLACEMENT_KEY = "replacement"; + + public RewriteResponseHeaderGatewayFilterFactory() { + super(Config.class); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(NAME_KEY, REGEXP_KEY, REPLACEMENT_KEY); + } + + @Override + public GatewayFilter apply(Config config) { + return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> { + rewriteHeader(exchange, config); + })); + } + + protected void rewriteHeader(ServerWebExchange exchange, Config config) { + final String name = config.getName(); + final String value = exchange.getResponse().getHeaders().getFirst(name); + if (value == null) { + return; + } + final String newValue = rewrite(value, config.getRegexp(), config.getReplacement()); + exchange.getResponse().getHeaders().set(name, newValue); + } + + String rewrite(String value, String regexp, String replacement) { + return value.replaceAll(regexp, replacement.replace("$\\", "$")); + } + + public static class Config extends AbstractGatewayFilterFactory.NameConfig { + private String regexp; + private String replacement; + + public String getRegexp() { + return regexp; + } + + public Config setRegexp(String regexp) { + this.regexp = regexp; + return this; + } + + public String getReplacement() { + return replacement; + } + + public Config setReplacement(String replacement) { + this.replacement = replacement; + 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 a59f2443..7777c173 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 @@ -51,6 +51,7 @@ import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewa import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory; @@ -477,6 +478,18 @@ public class GatewayFilterSpec extends UriSpec { .apply(c -> c.setName(headerName).setValue(headerValue))); } + /** + * A filter that rewrites a header value on the response before it is returned to the client by the Gateway. + * @param headerName the header name + * @param regex a Java regular expression to match the path against + * @param replacement the replacement for the path + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec rewriteResponseHeader(String headerName, String regex, String replacement) { + return filter(getBean(RewriteResponseHeaderGatewayFilterFactory.class) + .apply(c -> c.setReplacement(replacement).setRegexp(regex).setName(headerName))); + } + /** * A filter that sets the status on the response before it is returned to the client by the Gateway. * @param status the status to set on the response @@ -557,7 +570,7 @@ public class GatewayFilterSpec extends UriSpec { }.apply(c -> { })); } - + /** * A filter that sets the maximum permissible size of a Request. diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..ad4dde3e --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteResponseHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2017 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 RewriteResponseHeaderGatewayFilterFactoryTests extends BaseWebClientTests { + + @Test + public void rewriteResponseHeaderFilterWorks() { + testClient.get() + .uri("/headers") + .header("Host", "www.rewriteresponseheader.org") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals("X-Request-Foo", "/42?user=ford&password=***&flag=true"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { } + +} 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 eddce122..db3560a4 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 @@ -76,6 +76,7 @@ import static org.junit.Assume.assumeThat; org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactoryIntegrationTests.class, org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactoryTests.class, + org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.WeightCalculatorWebFilterTests.class, org.springframework.cloud.gateway.filter.RouteToRequestUrlFilterTests.class, org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilterTests.class, diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index a241e7dd..157db07d 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -65,6 +65,16 @@ spring: filters: - AddResponseHeader=X-Request-Foo, Bar + # ===================================== + - id: rewrite_response_header_test + uri: ${test.uri} + predicates: + - Host=**.rewriteresponseheader.org + - Path=/headers + filters: + - AddResponseHeader=X-Request-Foo, /42?user=ford&password=omg!what&flag=true + - RewriteResponseHeader=X-Request-Foo, password=[^&]+, password=*** + # ===================================== - id: forward_test uri: forward:/localcontroller