From 6eb6634b32552ad426a8de1c10abc48115c07af5 Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Wed, 26 Jun 2019 10:37:12 -0400 Subject: [PATCH 1/2] Suppprt remapping headers in request --- .../main/asciidoc/spring-cloud-gateway.adoc | 20 ++- .../config/GatewayAutoConfiguration.java | 6 + .../MapRequestHeaderGatewayFilterFactory.java | 124 ++++++++++++++++ .../route/builder/GatewayFilterSpec.java | 12 ++ ...equestHeaderGatewayFilterFactoryTests.java | 138 ++++++++++++++++++ .../test/HttpBinCompatibleController.java | 8 + ...lication-request-map-header-web-filter.yml | 16 ++ 7 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java create mode 100644 spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index ef56b2cc..1244d3fa 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -585,6 +585,24 @@ their default values: You can find more information on how Hystrix works with Gateway in the <>. +=== MapRequestHeader GatewayFilter Factory +The MapRequestHeader GatewayFilter Factory takes 'fromHeader' and 'toHeader' parameters. It creates a new named header (toHeader) and the value is extracted out of an existing named header (fromHeader) from the incoming http request. If the input header does not exist then the filter has no impact. If the new named header already exists then it's values will be augmented with the new values. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Bar, X-Request-Foo +---- + +This will add `X-Request-Foo:` header to the downstream request's with updated values from the incoming http request `Bar` header. + === PrefixPath GatewayFilter Factory The PrefixPath GatewayFilter Factory takes a single `prefix` parameter. @@ -814,7 +832,7 @@ spring: - id: rewritelocationresponseheader_route uri: http://example.org filters: - - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , + - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , ---- For example, for a request `POST https://api.example.com/some/object/name`, `Location` response header value `https://object-service.prod.example.net/v2/some/object/id` will be rewritten as `https://api.example.com/some/object/id`. 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 40bf8731..b11734fb 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 @@ -66,6 +66,7 @@ import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGate import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; @@ -393,6 +394,11 @@ public class GatewayAutoConfiguration { return new AddRequestHeaderGatewayFilterFactory(); } + @Bean + public MapRequestHeaderGatewayFilterFactory mapRequestHeaderGatewayFilterFactory() { + return new MapRequestHeaderGatewayFilterFactory(); + } + @Bean public AddRequestParameterGatewayFilterFactory addRequestParameterGatewayFilterFactory() { return new AddRequestParameterGatewayFilterFactory(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..ce72cfba --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactory.java @@ -0,0 +1,124 @@ +/* + * 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 + * + * 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.factory; + +import java.util.Arrays; +import java.util.List; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.core.style.ToStringCreator; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; + +/** + * @author Tony Clarke + */ +public class MapRequestHeaderGatewayFilterFactory extends + AbstractGatewayFilterFactory { + + /** + * From Header key. + */ + public static final String FROM_HEADER_KEY = "fromHeader"; + + /** + * To Header key. + */ + public static final String TO_HEADER_KEY = "toHeader"; + + public MapRequestHeaderGatewayFilterFactory() { + super(Config.class); + } + + public List shortcutFieldOrder() { + return Arrays.asList(FROM_HEADER_KEY, TO_HEADER_KEY); + } + + @Override + public GatewayFilter apply(MapRequestHeaderGatewayFilterFactory.Config config) { + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, + GatewayFilterChain chain) { + if (!exchange.getRequest().getHeaders() + .containsKey(config.getFromHeader())) { + return chain.filter(exchange); + } + List headerValues = exchange.getRequest().getHeaders() + .get(config.getFromHeader()); + + ServerHttpRequest request = exchange.getRequest().mutate() + .headers(i -> i.addAll(config.getToHeader(), headerValues)) + .build(); + + return chain.filter(exchange.mutate().request(request).build()); + } + + @Override + public String toString() { + // @formatter:off + return filterToStringCreator(MapRequestHeaderGatewayFilterFactory.this) + .append(FROM_HEADER_KEY, config.getFromHeader()) + .append(TO_HEADER_KEY, config.getToHeader()) + .toString(); + // @formatter:on + } + }; + } + + public static class Config { + + private String fromHeader; + + private String toHeader; + + public String getFromHeader() { + return this.fromHeader; + } + + public Config setFromHeader(String fromHeader) { + this.fromHeader = fromHeader; + return this; + } + + public String getToHeader() { + return this.toHeader; + } + + public Config setToHeader(String toHeader) { + this.toHeader = toHeader; + return this; + } + + @Override + public String toString() { + // @formatter:off + return new ToStringCreator(this) + .append("fromHeader", fromHeader) + .append("toHeader", toHeader) + .toString(); + // @formatter:on + } + + } + +} 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 23c499b1..494ba8d4 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 @@ -43,6 +43,7 @@ import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGate import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy; import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.HystrixGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory; @@ -216,6 +217,17 @@ public class GatewayFilterSpec extends UriSpec { return filter(factory.apply(this.routeBuilder.getId(), configConsumer)); } + /** + * Maps headers from one name to another. + * @param fromHeader the header name of the original header. + * @param toHeader the header name of the new header. + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec mapRequestHeader(String fromHeader, String toHeader) { + return filter(getBean(MapRequestHeaderGatewayFilterFactory.class) + .apply(c -> c.setFromHeader(fromHeader).setToHeader(toHeader))); + } + /** * A filter that can be used to modify the request body. This filter is BETA and may * be subject to change in a future release. diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..9d57bc07 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/MapRequestHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,138 @@ +/* + * 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 + * + * 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.factory; + +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory.Config; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; + +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; + +/** + * @author Tony Clarke + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ActiveProfiles(profiles = "request-map-header-web-filter") +public class MapRequestHeaderGatewayFilterFactoryTests extends BaseWebClientTests { + + @Test + public void mapRequestHeaderFilterWorks() { + testClient.get().uri("/headers").header("Host", "www.maprequestheader.org") + .header("a", "tome").exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).containsEntry("X-Request-Example", "tome"); + }); + } + + @Test + public void mapRequestHeaderFilterWorksJavaDsl() { + testClient.get().uri("/headers").header("Host", "www.maprequestheaderjava.org") + .header("b", "tome").exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).containsEntry("X-Request-Example-Java", "tome"); + }); + } + + @SuppressWarnings("unchecked") + @Test + public void mapRequestHeaderWithMultiValueFilterWorks() { + testClient.get().uri("/multivalueheaders") + .header("Host", "www.maprequestheader.org").header("a", "tome", "toyou") + .exchange().expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).containsKey("X-Request-Example"); + List values = (List) headers.get("X-Request-Example"); + assertThat(values).contains("tome", "toyou"); + }); + } + + @Test + public void mapRequestHeaderWithNullValueFilterWorks() { + testClient.get().uri("/headers").header("Host", "www.maprequestheader.org") + .header("a", (String) null).exchange().expectBody(Map.class) + .consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).doesNotContainKey("X-Request-Example"); + }); + } + + @Test + public void mapRequestHeaderWhenInputHeaderDoesNotExist() { + testClient.get().uri("/headers").header("Host", "www.maprequestheader.org") + .exchange().expectBody(Map.class).consumeWith(result -> { + Map headers = getMap(result.getResponseBody(), + "headers"); + assertThat(headers).doesNotContainKey("X-Request-Example"); + }); + } + + @Test + public void toStringFormat() { + Config config = new Config().setFromHeader("myfromheader") + .setToHeader("mytoheader"); + GatewayFilter filter = new MapRequestHeaderGatewayFilterFactory().apply(config); + assertThat(filter.toString()).contains("myfromheader").contains("mytoheader"); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes().route("map_request_header_java_test", + r -> r.path("/headers").and().host("**.maprequestheaderjava.org") + .filters(f -> f.prefixPath("/httpbin").mapRequestHeader("b", + "X-Request-Example-Java")) + .uri(uri)) + .build(); + } + + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java index 3a94eacb..72373f20 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/HttpBinCompatibleController.java @@ -59,6 +59,14 @@ public class HttpBinCompatibleController { return result; } + @RequestMapping(path = "/multivalueheaders", method = { RequestMethod.GET, + RequestMethod.POST }, produces = MediaType.APPLICATION_JSON_VALUE) + public Map multiValueHeaders(ServerWebExchange exchange) { + Map result = new HashMap<>(); + result.put("headers", exchange.getRequest().getHeaders()); + return result; + } + @RequestMapping(path = "/delay/{sec}", produces = MediaType.APPLICATION_JSON_VALUE) public Mono> get(ServerWebExchange exchange, @PathVariable int sec) throws InterruptedException { diff --git a/spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml b/spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml new file mode 100644 index 00000000..fefe27db --- /dev/null +++ b/spring-cloud-gateway-core/src/test/resources/application-request-map-header-web-filter.yml @@ -0,0 +1,16 @@ +spring: + cloud: + gateway: + routes: + - id: map_request_header_test_singleresponse + uri: ${test.uri} + predicates: + - Path=/headers + filters: + - MapRequestHeader=a, X-Request-Example + - id: map_request_header_test_multiresponse + uri: ${test.uri} + predicates: + - Path=/multivalueheaders + filters: + - MapRequestHeader=a, X-Request-Example From f7b9bb76c8177ecf47cadb92725f07db242a3bde Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Wed, 24 Jul 2019 17:25:38 -0400 Subject: [PATCH 2/2] formatting --- .../cloud/gateway/filter/GatewayMetricsFilter.java | 3 ++- .../gateway/support/tagsprovider/GatewayHttpTagsProvider.java | 4 ++-- .../support/tagsprovider/GatewayHttpTagsProviderTests.java | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java index 45ff2ce7..5a6da70a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/GatewayMetricsFilter.java @@ -55,7 +55,8 @@ public class GatewayMetricsFilter implements GlobalFilter, Ordered { @Deprecated public GatewayMetricsFilter(MeterRegistry meterRegistry) { - this(meterRegistry, Arrays.asList(new GatewayHttpTagsProvider(), new GatewayRouteTagsProvider())); + this(meterRegistry, Arrays.asList(new GatewayHttpTagsProvider(), + new GatewayRouteTagsProvider())); } @Override diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProvider.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProvider.java index d0bed4be..54c461ca 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProvider.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProvider.java @@ -61,8 +61,8 @@ public class GatewayHttpTagsProvider implements GatewayTagsProvider { } } - return Tags.of("outcome", outcome, "status", status, - "httpStatusCode", httpStatusCodeStr, "httpMethod", httpMethod); + return Tags.of("outcome", outcome, "status", status, "httpStatusCode", + httpStatusCodeStr, "httpMethod", httpMethod); } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProviderTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProviderTests.java index d2e63d90..f82cc0c9 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProviderTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/support/tagsprovider/GatewayHttpTagsProviderTests.java @@ -40,7 +40,8 @@ public class GatewayHttpTagsProviderTests { private static final String ROUTE_URI = "http://gatewaytagsprovider.org:80"; private static final Tags DEFAULT_TAGS = Tags.of("outcome", OK.series().name(), - "status", OK.name(), "httpStatusCode", String.valueOf(OK.value()), "httpMethod", "GET"); + "status", OK.name(), "httpStatusCode", String.valueOf(OK.value()), + "httpMethod", "GET"); @Test public void httpTags() {