diff --git a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index fef40c53..bd83f4a9 100644 --- a/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -16,6 +16,7 @@ import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderFilterFactory; import org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderFilterFactory; import org.springframework.cloud.gateway.filter.factory.RewritePathFilterFactory; +import org.springframework.cloud.gateway.filter.factory.SetPathFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetResponseHeaderFilterFactory; import org.springframework.cloud.gateway.filter.factory.SetStatusFilterFactory; import org.springframework.cloud.gateway.handler.GatewayFilteringWebHandler; @@ -140,6 +141,11 @@ public class GatewayAutoConfiguration { return new RewritePathFilterFactory(); } + @Bean + public SetPathFilterFactory setPathFilterFactory() { + return new SetPathFilterFactory(); + } + @Bean public SetResponseHeaderFilterFactory setResponseHeaderFilterFactory() { return new SetResponseHeaderFilterFactory(); diff --git a/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java index f9b69f76..8596fcfe 100644 --- a/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java +++ b/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java @@ -44,7 +44,7 @@ public interface GatewayFilter { */ Mono filter(ServerWebExchange exchange, WebFilterChain chain); - default T getAttribute(ServerWebExchange exchange, String attributeName, Class type) { + static T getAttribute(ServerWebExchange exchange, String attributeName, Class type) { if (exchange.getAttributes().containsKey(attributeName)) { Object attr = exchange.getAttributes().get(attributeName); if (type.isAssignableFrom(attr.getClass())) { diff --git a/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java b/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java index 43247244..a8c715ea 100644 --- a/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java +++ b/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java @@ -10,6 +10,8 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilterChain; import org.springframework.web.util.UriComponentsBuilder; +import static org.springframework.cloud.gateway.filter.GatewayFilter.getAttribute; + import reactor.core.publisher.Mono; /** diff --git a/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathFilterFactory.java b/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathFilterFactory.java new file mode 100644 index 00000000..c9de95a6 --- /dev/null +++ b/src/main/java/org/springframework/cloud/gateway/filter/factory/SetPathFilterFactory.java @@ -0,0 +1,37 @@ +package org.springframework.cloud.gateway.filter.factory; + +import java.net.URI; +import java.util.Map; + +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.util.UriTemplate; + +import static org.springframework.cloud.gateway.filter.GatewayFilter.getAttribute; +import static org.springframework.cloud.gateway.handler.predicate.UrlPredicateFactory.URL_PREDICATE_VARS_ATTR; + +/** + * @author Spencer Gibb + */ +public class SetPathFilterFactory implements FilterFactory { + + @Override + @SuppressWarnings("unchecked") + public GatewayFilter apply(String template, String[] args) { + UriTemplate uriTemplate = new UriTemplate(template); + + //TODO: caching can happen here + return (exchange, chain) -> { + Map variables = getAttribute(exchange, URL_PREDICATE_VARS_ATTR, Map.class); + ServerHttpRequest req = exchange.getRequest(); + URI uri = uriTemplate.expand(variables); + String newPath = uri.getPath(); + + ServerHttpRequest request = req.mutate() + .path(newPath) + .build(); + + return chain.filter(exchange.mutate().request(request).build()); + }; + } +} diff --git a/src/main/java/org/springframework/cloud/gateway/handler/predicate/UrlPredicateFactory.java b/src/main/java/org/springframework/cloud/gateway/handler/predicate/UrlPredicateFactory.java index 96c487cc..4a3e0679 100644 --- a/src/main/java/org/springframework/cloud/gateway/handler/predicate/UrlPredicateFactory.java +++ b/src/main/java/org/springframework/cloud/gateway/handler/predicate/UrlPredicateFactory.java @@ -1,5 +1,6 @@ package org.springframework.cloud.gateway.handler.predicate; +import java.util.Map; import java.util.function.Predicate; import org.springframework.util.AntPathMatcher; @@ -12,6 +13,8 @@ import org.springframework.web.server.support.HttpRequestPathHelper; */ public class UrlPredicateFactory implements PredicateFactory { + public static final String URL_PREDICATE_VARS_ATTR = "urlPredicateVars"; + private PathMatcher pathMatcher = new AntPathMatcher(); private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); @@ -40,7 +43,12 @@ public class UrlPredicateFactory implements PredicateFactory { public Predicate apply(String pattern, String[] args) { return exchange -> { String lookupPath = getPathHelper().getLookupPathForRequest(exchange); - return getPathMatcher().match(pattern, lookupPath); + boolean match = getPathMatcher().match(pattern, lookupPath); + if (match) { + Map variables = getPathMatcher().extractUriTemplateVariables(pattern, lookupPath); + exchange.getAttributes().put(URL_PREDICATE_VARS_ATTR, variables); + } + return match; //TODO: support trailingSlashMatch }; } diff --git a/src/test/java/org/springframework/cloud/gateway/filter/factory/SetPathFilterFactoryTests.java b/src/test/java/org/springframework/cloud/gateway/filter/factory/SetPathFilterFactoryTests.java new file mode 100644 index 00000000..355c01b4 --- /dev/null +++ b/src/test/java/org/springframework/cloud/gateway/filter/factory/SetPathFilterFactoryTests.java @@ -0,0 +1,60 @@ +package org.springframework.cloud.gateway.filter.factory; + +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.handler.predicate.UrlPredicateFactory; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.server.adapter.DefaultServerWebExchange; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import reactor.core.publisher.Mono; + +import java.util.HashMap; + +/** + * @author Spencer Gibb + */ +public class SetPathFilterFactoryTests { + + @Test + public void rewritePathFilterWorks() { + HashMap variables = new HashMap<>(); + testRewriteFilter("/baz/bar", "/foo/bar", "/baz/bar", variables); + } + + @Test + public void setPathFilterWithTemplateVarsWorks() { + HashMap variables = new HashMap<>(); + variables.put("id", "123"); + testRewriteFilter("/bar/baz/{id}", "/foo/123", "/bar/baz/123", variables); + } + + private void testRewriteFilter(String template, String actualPath, String expectedPath, HashMap variables) { + GatewayFilter filter = new SetPathFilterFactory().apply(template, new String[]{}); + + MockServerHttpRequest request = MockServerHttpRequest + .get("http://localhost"+ actualPath) + .build(); + + DefaultServerWebExchange exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse()); + exchange.getAttributes().put(UrlPredicateFactory.URL_PREDICATE_VARS_ATTR, variables); + + WebFilterChain filterChain = mock(WebFilterChain.class); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + when(filterChain.filter(captor.capture())).thenReturn(Mono.empty()); + + filter.filter(exchange, filterChain); + + ServerWebExchange webExchange = captor.getValue(); + + Assertions.assertThat(webExchange.getRequest().getURI().getPath()).isEqualTo(expectedPath); + } +} diff --git a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java index 1e103671..676ff47f 100644 --- a/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java +++ b/src/test/java/org/springframework/cloud/gateway/test/GatewayIntegrationTests.java @@ -258,6 +258,26 @@ public class GatewayIntegrationTests { ); } + @Test + public void setPathFilterWorks() { + Mono result = webClient.exchange( + GET("http://localhost:" + port + "/foo/get") + .header("Host", "www.setpath.org") + .build() + ); + + verify( () -> + StepVerifier.create(result) + .consumeNextWith( + response -> { + HttpStatus statusCode = response.statusCode(); + assertThat(statusCode).isEqualTo(HttpStatus.OK); + }) + .expectComplete() + .verify(Duration.ofSeconds(3)) + ); + } + @Test public void setResponseHeaderFilterWorks() { Mono result = webClient.exchange( @@ -281,14 +301,14 @@ public class GatewayIntegrationTests { @Test @Ignore("TODO: figure out how to set the status before response committed") - public void setStatusStringWorks() { - setStatusStringTest("www.setstatusstring.org", HttpStatus.BAD_REQUEST); + public void setStatusIntWorks() { + setStatusStringTest("www.setstatusint.org", HttpStatus.UNAUTHORIZED); } @Test @Ignore("TODO: figure out how to set the status before response committed") - public void setStatusIntWorks() { - setStatusStringTest("www.setstatusint.org", HttpStatus.UNAUTHORIZED); + public void setStatusStringWorks() { + setStatusStringTest("www.setstatusstring.org", HttpStatus.BAD_REQUEST); } private void setStatusStringTest(String host, HttpStatus status) { diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index de980073..a381593e 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -57,6 +57,15 @@ spring: - AddResponseHeader=X-Request-Foo, Bar - RemoveResponseHeader=X-Request-Foo + # ===================================== + - id: set_path_test + uri: http://httpbin.org:80 + predicates: + - Host=**.setpath.org + - Url=/foo/{segment} + filters: + - SetPath=/{segment} + # ===================================== - id: set_response_header_test uri: http://httpbin.org:80 @@ -68,15 +77,6 @@ spring: - AddResponseHeader=X-Request-Foo, Bar2 - SetResponseHeader=X-Request-Foo, Bar - # ===================================== - - id: set_status_string_test - uri: http://httpbin.org:80 - predicates: - - Host=**.setstatusstring.org - - Url=/headers - filters: - - SetStatus=BAD_REQUEST - # ===================================== - id: set_status_int_test uri: http://httpbin.org:80 @@ -86,6 +86,15 @@ spring: filters: - SetStatus=401 + # ===================================== + - id: set_status_string_test + uri: http://httpbin.org:80 + predicates: + - Host=**.setstatusstring.org + - Url=/headers + filters: + - SetStatus=BAD_REQUEST + # ===================================== - id: rewrite_path_test uri: http://httpbin.org:80