diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index d0b3a132..b18628b0 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -841,6 +841,34 @@ 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. +=== RewriteLocationResponseHeader GatewayFilter Factory +The RewriteLocationResponseHeader GatewayFilter Factory modifies the value of `Location` response header, usually to get rid of backend specific details. It takes `stripVersionMode`, `locationHeaderName`, `hostValue`, and `protocolsRegex` parameters. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: rewritelocationresponseheader_route + uri: http://example.org + filters: + - 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`. + +Parameter `stripVersionMode` has the following possible values: `NEVER_STRIP`, `AS_IN_REQUEST` (default), `ALWAYS_STRIP`. + + * `NEVER_STRIP` - Version will not be stripped, even if the original request path contains no version + * `AS_IN_REQUEST` - Version will be stripped only if the original request path contains no version + * `ALWAYS_STRIP` - Version will be stripped, even if the original request path contains version + +Parameter `hostValue`, if provided, will be used to replace the `host:port` portion of the response `Location` header. If not provided, the value of the `Host` request header will be used. + +Parameter `protocolsRegex` must be a valid regex `String`, against which the protocol name will be matched. If not matched, the filter will do nothing. Default is `http|https|ftp|ftps`. + === 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. 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 edf1bac8..a54d9310 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 @@ -79,6 +79,7 @@ import org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUr import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory; @@ -502,6 +503,11 @@ public class GatewayAutoConfiguration { return new RewriteResponseHeaderGatewayFilterFactory(); } + @Bean + public RewriteLocationResponseHeaderGatewayFilterFactory rewriteLocationResponseHeaderGatewayFilterFactory() { + return new RewriteLocationResponseHeaderGatewayFilterFactory(); + } + @Bean public SetStatusGatewayFilterFactory setStatusGatewayFilterFactory() { return new SetStatusGatewayFilterFactory(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactory.java new file mode 100644 index 00000000..2370e3fa --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactory.java @@ -0,0 +1,269 @@ +/* + * 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 + * + * 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 java.util.regex.Pattern; + +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.http.HttpHeaders; +import org.springframework.web.server.ServerWebExchange; + +import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator; + +/* +This filter intent is to modify the value of Location response header, ridding it of backend specific details. +Typical scenario: POST response from a legacy backend contains a Location header with backend's hostname. + You want to replace the backend's hostname:port with your documented gateway's hostname:port. +Additionally, sometimes backend returns a 'versioned' Location, and you may want to strip the version portion. + +Note that the protocol portion of the URL will not be replaced by this filter. If you need that too, +either follow this filter with a generic rewrite response header filter, or subclass this filter. + +Configuration parameters: +- stripVersion + NEVER_STRIP - Version will not be stripped, even if the original request path contains no version + AS_IN_REQUEST - Default. Version will be stripped only if the original request path contains no version + ALWAYS_STRIP - Version will be stripped, even if the original request path contains version +- locationHeaderName + String representing a custom location header name. Default - Location +- hostValue + A String. If provided, will be used to replace the host:port portion of the response Location header. + Default - The value of request Host header is used to replace. +- protocols + A valid regex String, against which the protocol name will be matched. If not matched, the filter will do nothing. + Default - https?|ftps? (which is the same as http|https|ftp|ftps) + +Example 1 + default-filters: + - RewriteLocationResponseHeader + +Host request header: api.example.com:443 +POST request path: /some/object/name +Location response header: https://object-service.prod.example.net/v2/some/object/id +Modified Location response header: https://api.example.com:443/some/object/id + +Example 2 + default-filters: + - name: RewriteLocationResponseHeader + args: + stripVersion: ALWAYS_STRIP + locationHeaderName: Link + hostValue: example-api.com + +Host request header (irrelevant): api.example.com:443 +POST request path: /v1/some/object/name +Link response header: https://object-service.prod.example.net/v1/some/object/id +Modified Link response header: https://example-api.com/some/object/id + +Example 3 + default-filters: + - name: RewriteLocationResponseHeader + args: + stripVersion: NEVER_STRIP + protocols: https|ftps # only replace host:port for https or ftps, but not http or ftp + +Host request header: api.example.com:443 +1. POST request path: /some/object/name +Location response header: https://object-service.prod.example.net/v1/some/object/id +Modified Location response header: https://api.example.com/v1/some/object/id +2. POST request path: /some/other/object/name +Location response header: http://object-service.prod.example.net/v1/some/object/id +Modified (not) Location response header: http://object-service.prod.example.net/v1/some/object/id + */ + +/** + * @author Vitaliy Pavlyuk + */ +public class RewriteLocationResponseHeaderGatewayFilterFactory extends + AbstractGatewayFilterFactory { + + private static final String STRIP_VERSION_KEY = "stripVersion"; + + private static final String LOCATION_HEADER_NAME_KEY = "locationHeaderName"; + + private static final String HOST_VALUE_KEY = "hostValue"; + + private static final String PROTOCOLS_KEY = "protocols"; + + private static final Pattern VERSIONED_PATH = Pattern.compile("^/v\\d+/.*"); + + private static final String DEFAULT_PROTOCOLS = "https?|ftps?"; + + private static final Pattern DEFAULT_HOST_PORT = compileHostPortPattern( + DEFAULT_PROTOCOLS); + + private static final Pattern DEFAULT_HOST_PORT_VERSION = compileHostPortVersionPattern( + DEFAULT_PROTOCOLS); + + public RewriteLocationResponseHeaderGatewayFilterFactory() { + super(Config.class); + } + + private static Pattern compileHostPortPattern(String protocols) { + return Pattern.compile("(?<=^(?:" + protocols + ")://)[^:/]+(?::\\d+)?(?=/)"); + } + + private static Pattern compileHostPortVersionPattern(String protocols) { + return Pattern.compile( + "(?<=^(?:" + protocols + ")://)[^:/]+(?::\\d+)?(?:/v\\d+)?(?=/)"); + } + + @Override + public List shortcutFieldOrder() { + return Arrays.asList(STRIP_VERSION_KEY, LOCATION_HEADER_NAME_KEY, HOST_VALUE_KEY, + PROTOCOLS_KEY); + } + + @Override + public GatewayFilter apply(Config config) { + return new GatewayFilter() { + @Override + public Mono filter(ServerWebExchange exchange, + GatewayFilterChain chain) { + return chain.filter(exchange) + .then(Mono.fromRunnable(() -> rewriteLocation(exchange, config))); + } + + @Override + public String toString() { + // @formatter:off + return filterToStringCreator( + RewriteLocationResponseHeaderGatewayFilterFactory.this) + .append("stripVersion", config.stripVersion) + .append("locationHeaderName", config.locationHeaderName) + .append("hostValue", config.hostValue) + .append("protocols", config.protocols) + .toString(); + // @formatter:on + } + }; + } + + void rewriteLocation(ServerWebExchange exchange, Config config) { + final String location = exchange.getResponse().getHeaders() + .getFirst(config.getLocationHeaderName()); + final String host = config.getHostValue() != null ? config.getHostValue() + : exchange.getRequest().getHeaders().getFirst(HttpHeaders.HOST); + final String path = exchange.getRequest().getURI().getPath(); + if (location != null && host != null) { + final String fixedLocation = fixedLocation(location, host, path, + config.getStripVersion(), config.getHostPortPattern(), + config.getHostPortVersionPattern()); + exchange.getResponse().getHeaders().set(config.getLocationHeaderName(), + fixedLocation); + } + } + + String fixedLocation(String location, String host, String path, + StripVersion stripVersion, Pattern hostPortPattern, + Pattern hostPortVersionPattern) { + final boolean doStrip = StripVersion.ALWAYS_STRIP.equals(stripVersion) + || (StripVersion.AS_IN_REQUEST.equals(stripVersion) + && !VERSIONED_PATH.matcher(path).matches()); + final Pattern pattern = doStrip ? hostPortVersionPattern : hostPortPattern; + return pattern.matcher(location).replaceFirst(host); + } + + public enum StripVersion { + + /** + * Version will not be stripped, even if the original request path contains no + * version. + */ + NEVER_STRIP, + + /** + * Version will be stripped only if the original request path contains no version. + * This is the Default. + */ + AS_IN_REQUEST, + + /** + * Version will be stripped, even if the original request path contains version. + */ + ALWAYS_STRIP + + } + + public static class Config { + + private StripVersion stripVersion = StripVersion.AS_IN_REQUEST; + + private String locationHeaderName = HttpHeaders.LOCATION; + + private String hostValue; + + private String protocols = DEFAULT_PROTOCOLS; + + private Pattern hostPortPattern = DEFAULT_HOST_PORT; + + private Pattern hostPortVersionPattern = DEFAULT_HOST_PORT_VERSION; + + public StripVersion getStripVersion() { + return stripVersion; + } + + public Config setStripVersion(StripVersion stripVersion) { + this.stripVersion = stripVersion; + return this; + } + + public String getLocationHeaderName() { + return locationHeaderName; + } + + public Config setLocationHeaderName(String locationHeaderName) { + this.locationHeaderName = locationHeaderName; + return this; + } + + public String getHostValue() { + return hostValue; + } + + public Config setHostValue(String hostValue) { + this.hostValue = hostValue; + return this; + } + + public String getProtocols() { + return protocols; + } + + public Config setProtocols(String protocols) { + this.protocols = protocols; + this.hostPortPattern = compileHostPortPattern(protocols); + this.hostPortVersionPattern = compileHostPortVersionPattern(protocols); + return this; + } + + public Pattern getHostPortPattern() { + return hostPortPattern; + } + + public Pattern getHostPortVersionPattern() { + return hostPortVersionPattern; + } + + } + +} 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 42a0be88..828ea441 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 @@ -55,6 +55,8 @@ 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.RewriteLocationResponseHeaderGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory.StripVersion; import org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory; @@ -579,6 +581,24 @@ public class GatewayFilterSpec extends UriSpec { c -> c.setReplacement(replacement).setRegexp(regex).setName(headerName))); } + /** + * A filter that rewrites the value of Location response header, ridding it of backend + * specific details. + * @param stripVersionMode NEVER_STRIP, AS_IN_REQUEST, or ALWAYS_STRIP + * @param locationHeaderName a location header name + * @param hostValue host value + * @param protocolsRegex a valid regex String, against which the protocol name will be + * matched + * @return a {@link GatewayFilterSpec} that can be used to apply additional filters + */ + public GatewayFilterSpec rewriteLocationResponseHeader(String stripVersionMode, + String locationHeaderName, String hostValue, String protocolsRegex) { + return filter(getBean(RewriteLocationResponseHeaderGatewayFilterFactory.class) + .apply(c -> c.setStripVersion(StripVersion.valueOf(stripVersionMode)) + .setLocationHeaderName(locationHeaderName).setHostValue(hostValue) + .setProtocols(protocolsRegex))); + } + /** * A filter that sets the status on the response before it is returned to the client * by the Gateway. diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactoryTests.java new file mode 100644 index 00000000..500d755d --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactoryTests.java @@ -0,0 +1,53 @@ +/* + * 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 + * + * 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 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 RewriteLocationResponseHeaderGatewayFilterFactoryTests + extends BaseWebClientTests { + + @Test + public void rewriteLocationResponseHeaderFilterWorks() { + testClient.post().uri("/headers") + .header("Host", "test1.rewritelocationresponseheader.org").exchange() + .expectStatus().isOk().expectHeader().valueEquals("Location", + "https://test1.rewritelocationresponseheader.org/some/object/id"); + } + + @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/RewriteLocationResponseHeaderGatewayFilterFactoryUnitTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactoryUnitTests.java new file mode 100644 index 00000000..7af9c7f1 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/RewriteLocationResponseHeaderGatewayFilterFactoryUnitTests.java @@ -0,0 +1,227 @@ +/* + * 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 + * + * 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.net.URI; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory.Config; +import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory.StripVersion; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class RewriteLocationResponseHeaderGatewayFilterFactoryUnitTests { + + @InjectMocks + private RewriteLocationResponseHeaderGatewayFilterFactory filter; + + @Mock + private ServerWebExchange exchange; + + @Mock + private ServerHttpRequest request; + + @Mock + private ServerHttpResponse response; + + @Mock + private HttpHeaders requestHeaders; + + @Mock + private HttpHeaders responseHeaders; + + private URI uri; + + private Config config; + + @Before + public void setUp() { + filter = new RewriteLocationResponseHeaderGatewayFilterFactory(); + Mockito.when(exchange.getRequest()).thenReturn(request); + Mockito.when(exchange.getResponse()).thenReturn(response); + Mockito.when(request.getHeaders()).thenReturn(requestHeaders); + Mockito.when(response.getHeaders()).thenReturn(responseHeaders); + config = new Config(); + } + + private void setupTest(String location, String host, String path) { + Mockito.when(responseHeaders.getFirst(HttpHeaders.LOCATION)).thenReturn(location); + Mockito.when(requestHeaders.getFirst(HttpHeaders.HOST)).thenReturn(host); + uri = URI.create("http://" + host + path); + Mockito.when(request.getURI()).thenReturn(uri); + } + + @Test + public void rewriteLocationNullLocation() { + setupTest(null, "host", "/path"); + filter.rewriteLocation(exchange, config); + Mockito.verify(responseHeaders, Mockito.never()).set(Mockito.anyString(), + Mockito.anyString()); + } + + @Test + public void rewriteLocationNullHost() { + setupTest("location", null, "/path"); + filter.rewriteLocation(exchange, config); + Mockito.verify(responseHeaders, Mockito.never()).set(Mockito.anyString(), + Mockito.anyString()); + } + + @Test + public void rewriteLocation() { + setupTest("location", "host", "/path"); + filter.rewriteLocation(exchange, config); + Mockito.verify(responseHeaders).set(Mockito.eq("Location"), + Mockito.eq("location")); + } + + @Test + public void rewriteLocationCustomHeaderName() { + setupTest("location", "host", "/path"); + Mockito.when(responseHeaders.getFirst("Link")).thenReturn("link"); + config.setLocationHeaderName("Link"); + filter.rewriteLocation(exchange, config); + Mockito.verify(responseHeaders).set(Mockito.eq("Link"), Mockito.eq("link")); + } + + @Test + public void rewriteLocationCustomHostValue() { + setupTest("https://replaceme/some/path", "host", "/some/path"); + config.setHostValue("different.host"); + filter.rewriteLocation(exchange, config); + Mockito.verify(responseHeaders).set(Mockito.eq("Location"), + Mockito.eq("https://different.host/some/path")); + } + + @Test + public void rewriteLocationCustomProtocols() { + setupTest("https://replaceme/some/path", "host", "/some/path"); + config.setProtocols("gopher|whatever"); + filter.rewriteLocation(exchange, config); + Mockito.verify(responseHeaders).set(Mockito.eq("Location"), + Mockito.eq("https://replaceme/some/path")); + } + + @Test + public void fixedLocationVersionedAlwaysStrip() { + String location = "https://backend-url.example.com:443/v1/path/to/riches"; + String host = "example.com:443"; + String path = "/v1/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.ALWAYS_STRIP, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://example.com:443/path/to/riches"); + } + + @Test + public void fixedLocationVersionedStripAsInRequest() { + String location = "https://backend-url.example.com:443/v1/path/to/riches"; + String host = "example.com:443"; + String path = "/v1/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.AS_IN_REQUEST, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://example.com:443/v1/path/to/riches"); + } + + @Test + public void fixedLocationVersionedDontStrip() { + String location = "https://backend-url.example.com:443/v1/path/to/riches"; + String host = "example.com:443"; + String path = "/v1/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.NEVER_STRIP, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://example.com:443/v1/path/to/riches"); + } + + @Test + public void fixedLocationUnversionedAlwaysStrip() { + String location = "https://backend-url.example.com:443/v2/path/to/riches"; + String host = "api.example.com:443"; + String path = "/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.ALWAYS_STRIP, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://api.example.com:443/path/to/riches"); + } + + @Test + public void fixedLocationUnversionedStripAsInRequest() { + String location = "https://backend-url.example.com:443/v2/path/to/riches"; + String host = "api.example.com:443"; + String path = "/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.AS_IN_REQUEST, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://api.example.com:443/path/to/riches"); + } + + @Test + public void fixedLocationUnversionedDontStrip() { + String location = "https://backend-url.example.com:443/v2/path/to/riches"; + String host = "api.example.com:443"; + String path = "/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.NEVER_STRIP, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://api.example.com:443/v2/path/to/riches"); + } + + @Test + public void fixedLocationNoPort() { + String location = "https://backend-url.example.com/v2/path/to/riches"; + String host = "api.example.com:443"; + String path = "/path/to/riches"; + setupTest(location, host, path); + assertThat(filter.fixedLocation(location, host, path, StripVersion.AS_IN_REQUEST, + config.getHostPortPattern(), config.getHostPortVersionPattern())) + .isEqualTo("https://api.example.com:443/path/to/riches"); + } + + @Test + public void toStringFormat() { + // @formatter:off + Config config = new Config().setStripVersion(StripVersion.ALWAYS_STRIP) + .setLocationHeaderName("mylocation") + .setHostValue("myhost") + .setProtocols("myproto"); + GatewayFilter filter = new RewriteLocationResponseHeaderGatewayFilterFactory() + .apply(config); + assertThat(filter.toString()) + .contains("ALWAYS_STRIP") + .contains("mylocation") + .contains("myhost") + .contains("myproto"); + // @formatter:on + + } + +} 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 015ef687..049dd70e 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 @@ -25,6 +25,7 @@ import org.junit.runners.Suite.SuiteClasses; import org.junit.runners.model.Statement; import org.springframework.cloud.gateway.filter.GatewayMetricsFilterTests; +import org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactoryTests; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -63,6 +64,7 @@ import static org.junit.Assume.assumeThat; org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactoryTest.class, org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactoryUnitTests.class, + RewriteLocationResponseHeaderGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactoryTests.class, org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactoryIntegrationTests.class, org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactoryTests.class, diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 7929ca31..c7a25b73 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -99,6 +99,16 @@ spring: - AddResponseHeader=X-Request-Foo, /42?user=ford&password=omg!what&flag=true - RewriteResponseHeader=X-Request-Foo, password=[^&]+, password=*** + # ===================================== + - id: rewrite_location_response_header_test + uri: ${test.uri} + predicates: + - Host=**.rewritelocationresponseheader.org + - Path=/headers + filters: + - AddResponseHeader=Location, https://backend.org:443/v1/some/object/id + - RewriteLocationResponseHeader + # ===================================== - id: forward_test uri: forward:/localcontroller