diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 197aff92..5fd2fe36 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -616,6 +616,26 @@ spring: In either case, the HTTP status of the response will be set to 401. +=== StripPrefix GatewayFilter Factory +The StripPrefix GatewayFilter Factory takes one paramter, `parts`. The `parts` parameter indicated the number of parts in the path to strip from the request before sending it downstream. + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + routes: + - id: nameRoot + uri: http://nameservice + predicates: + - Path=/name/** + filters: + - StripPrefix=2 +---- + +When a request is made through the gateway to `/name/bar/foo` the request made to `nameservice` will look like `http://nameservice/foo`. + == Global Filters The `GlobalFilter` interface has the same signature as `GatewayFilter`. These are special filters that are conditionally applied to all routes. (This interface and usage are subject to change in future milestones). 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 2e8b9af0..e68aea07 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 @@ -61,6 +61,7 @@ import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFact import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.filter.ratelimit.PrincipalNameKeyResolver; import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; @@ -409,6 +410,11 @@ public class GatewayAutoConfiguration { return new SaveSessionGatewayFilterFactory(); } + @Bean + public StripPrefixGatewayFilterFactory stripPrefixGatewayFilterFactory() { + return new StripPrefixGatewayFilterFactory(); + } + @Configuration @ConditionalOnClass(Health.class) protected static class GatewayActuatorConfiguration { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactory.java new file mode 100644 index 00000000..9b82a341 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactory.java @@ -0,0 +1,66 @@ +/* + * 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 java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.tuple.Tuple; +import org.springframework.util.StringUtils; + +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.addOriginalRequestUrl; + +/** + * This filter removes the first part of the path, known as the prefix, from the request + * before sending it downstream + * @author Ryan Baxter + */ +public class StripPrefixGatewayFilterFactory implements GatewayFilterFactory { + + public static final String PARTS_KEY = "parts"; + + @Override + public List argNames() { + return Arrays.asList(PARTS_KEY); + } + + @Override + public GatewayFilter apply(Tuple args) { + final int parts = args.getInt(PARTS_KEY); + return apply(parts); + } + + public GatewayFilter apply(int parts) { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + addOriginalRequestUrl(exchange, request.getURI()); + String path = request.getURI().getRawPath(); + String newPath = "/" + Arrays.stream(StringUtils.tokenizeToStringArray(path, "/")) + .skip(parts).collect(Collectors.joining("/")); + ServerHttpRequest newRequest = request.mutate() + .path(newPath) + .build(); + + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newRequest.getURI()); + + return chain.filter(exchange.mutate().request(newRequest).build()); + }; + } +} 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 5300cef2..bdc1732f 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 @@ -45,6 +45,7 @@ import org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFact import org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory; +import org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.route.Route; import org.springframework.core.Ordered; @@ -232,4 +233,8 @@ public class GatewayFilterSpec extends UriSpec { public GatewayFilterSpec saveSession() { return filter(getBean(SaveSessionGatewayFilterFactory.class).apply(EMPTY_TUPLE)); } + + public GatewayFilterSpec stripPrefix(int parts) { + return filter(getBean(StripPrefixGatewayFilterFactory.class).apply(parts)); + } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactoryTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactoryTest.java new file mode 100644 index 00000000..d29a2fe9 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactoryTest.java @@ -0,0 +1,84 @@ +/* + * 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 reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.LinkedHashSet; + +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.tuple.TupleBuilder.tuple; + +/** + * @author Ryan Baxter + */ +public class StripPrefixGatewayFilterFactoryTest { + + @Test + public void testStripPrefix() throws Exception { + testStripPrefixFilter("/foo/bar", "/bar", 1); + testStripPrefixFilter("/foo/bar", "/", 2); + testStripPrefixFilter("/foo/bar", "/foo/bar", 0); + testStripPrefixFilter("", "/", 1); + testStripPrefixFilter("/", "/", 1); + testStripPrefixFilter("/", "/", 2); + testStripPrefixFilter("", "/", 2); + testStripPrefixFilter("/this/is/a/long/path/with/a/lot/of/slashes", "/path/with/a/lot/of/slashes", 4); + } + + + private void testStripPrefixFilter(String actualPath, String expectedPath, int parts) { + GatewayFilter filter = new StripPrefixGatewayFilterFactory().apply( + tuple().of(StripPrefixGatewayFilterFactory.PARTS_KEY, parts)); + + MockServerHttpRequest request = MockServerHttpRequest + .get("http://localhost"+ actualPath) + .build(); + + ServerWebExchange exchange = MockServerWebExchange.from(request); + + GatewayFilterChain filterChain = mock(GatewayFilterChain.class); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + when(filterChain.filter(captor.capture())).thenReturn(Mono.empty()); + + filter.filter(exchange, filterChain); + + ServerWebExchange webExchange = captor.getValue(); + + assertThat(webExchange.getRequest().getURI()).hasPath(expectedPath); + + URI requestUrl = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + assertThat(requestUrl).hasScheme("http").hasHost("localhost").hasNoPort().hasPath(expectedPath); + LinkedHashSet uris = webExchange.getRequiredAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); + assertThat(uris).contains(request.getURI()); + } + +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactoryTestIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactoryTestIntegrationTests.java new file mode 100644 index 00000000..20700a76 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/StripPrefixGatewayFilterFactoryTestIntegrationTests.java @@ -0,0 +1,66 @@ +/* + * 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 reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +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.http.HttpStatus; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.client.ClientResponse; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.cloud.gateway.test.TestUtils.assertStatus; + +/** + * @author Ryan Baxter + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class StripPrefixGatewayFilterFactoryTestIntegrationTests extends BaseWebClientTests { + + @Test + public void stripPrefixFilterDefaultValuesWork() { + Mono result = webClient.get() + .uri("/foo/bar/get") + .header("Host", "www.stripprefix.org") + .exchange(); + + StepVerifier.create(result) + .consumeNextWith( + response -> { + assertStatus(response, HttpStatus.OK); + }) + .expectComplete() + .verify(DURATION); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(BaseWebClientTests.DefaultTestConfig.class) + public static class TestConfig { } + +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 0fefddc8..ef5b8be1 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -149,6 +149,15 @@ spring: filters: - SetPath=/{segment} + # ===================================== + - id: strip_prefix_test + uri: ${test.uri} + predicates: + - Host=**.stripprefix.org + - Path=/foo/** + filters: + - StripPrefix=2 + # ===================================== - id: set_response_header_test uri: ${test.uri}