diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 18154464..98262f75 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -847,7 +847,7 @@ spring: predicates: - Path=/foo/** filters: - - RewritePath=/foo/(?.*), /$\{segment} + - RewritePath=/foo(?/?.*), $\{segment} ---- For a request path of `/foo/bar`, this will set the path to `/bar` before making the downstream request. Notice the `$` Should be replaced with `$\` because of the YAML spec. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java index a56c1a0a..dcda3e57 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; +import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionLocator; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; @@ -56,6 +57,8 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis protected List GatewayFilters; + protected List routePredicates; + protected RouteDefinitionWriter routeDefinitionWriter; protected RouteLocator routeLocator; @@ -65,10 +68,12 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis public AbstractGatewayControllerEndpoint( RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List GatewayFilters, + List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { this.routeDefinitionLocator = routeDefinitionLocator; this.globalFilters = globalFilters; this.GatewayFilters = GatewayFilters; + this.routePredicates = routePredicates; this.routeDefinitionWriter = routeDefinitionWriter; this.routeLocator = routeLocator; } @@ -96,6 +101,11 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis return getNamesToOrders(this.GatewayFilters); } + @GetMapping("/routepredicates") + public Mono> routepredicates() { + return getNamesToOrders(this.routePredicates); + } + private Mono> getNamesToOrders(List list) { return Flux.fromIterable(list).reduce(new HashMap<>(), this::putItem); } @@ -117,14 +127,35 @@ public class AbstractGatewayControllerEndpoint implements ApplicationEventPublis */ @PostMapping("/routes/{id}") @SuppressWarnings("unchecked") - public Mono> save(@PathVariable String id, - @RequestBody Mono route) { - return this.routeDefinitionWriter.save(route.map(r -> { - r.setId(id); - log.debug("Saving route: " + route); - return r; - })).then(Mono.defer(() -> Mono - .just(ResponseEntity.created(URI.create("/routes/" + id)).build()))); + public Mono> save(@PathVariable String id, + @RequestBody RouteDefinition route) { + + return Mono.just(route).filter(this::validateRouteDefinition) + .flatMap(routeDefinition -> this.routeDefinitionWriter + .save(Mono.just(routeDefinition).map(r -> { + r.setId(id); + log.debug("Saving route: " + route); + return r; + })) + .then(Mono.defer(() -> Mono.just(ResponseEntity + .created(URI.create("/routes/" + id)).build())))) + .switchIfEmpty( + Mono.defer(() -> Mono.just(ResponseEntity.badRequest().build()))); + } + + private boolean validateRouteDefinition(RouteDefinition routeDefinition) { + boolean hasValidFilterDefinitions = routeDefinition.getFilters().stream() + .allMatch(filterDefinition -> GatewayFilters.stream() + .anyMatch(gatewayFilterFactory -> filterDefinition.getName() + .equals(gatewayFilterFactory.name()))); + + boolean hasValidPredicateDefinitions = routeDefinition.getPredicates().stream() + .allMatch(predicateDefinition -> routePredicates.stream() + .anyMatch(routePredicate -> predicateDefinition.getName() + .equals(routePredicate.name()))); + log.debug("FilterDefinitions valid: " + hasValidFilterDefinitions); + log.debug("PredicateDefinitions valid: " + hasValidPredicateDefinitions); + return hasValidFilterDefinitions && hasValidPredicateDefinitions; } @DeleteMapping("/routes/{id}") diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java index 59fb9603..55cab3d5 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java @@ -28,6 +28,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEn import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; +import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.cloud.gateway.route.RouteLocator; @@ -44,8 +45,10 @@ public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint public GatewayControllerEndpoint(List globalFilters, List gatewayFilters, + List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { - super(null, globalFilters, gatewayFilters, routeDefinitionWriter, routeLocator); + super(null, globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, + routeLocator); } // TODO: Flush out routes without a definition diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java index 2c954260..2ebbfd82 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayLegacyControllerEndpoint.java @@ -27,6 +27,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEn import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; +import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionLocator; @@ -45,8 +46,9 @@ public class GatewayLegacyControllerEndpoint extends AbstractGatewayControllerEn public GatewayLegacyControllerEndpoint(RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List GatewayFilters, + List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { - super(routeDefinitionLocator, globalFilters, GatewayFilters, + super(routeDefinitionLocator, globalFilters, GatewayFilters, routePredicates, routeDefinitionWriter, routeLocator); } 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 52b5995f..612fa8ef 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 @@ -727,9 +727,10 @@ public class GatewayAutoConfiguration { public GatewayControllerEndpoint gatewayControllerEndpoint( List globalFilters, List gatewayFilters, + List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { return new GatewayControllerEndpoint(globalFilters, gatewayFilters, - routeDefinitionWriter, routeLocator); + routePredicates, routeDefinitionWriter, routeLocator); } @Bean @@ -739,9 +740,11 @@ public class GatewayAutoConfiguration { RouteDefinitionLocator routeDefinitionLocator, List globalFilters, List gatewayFilters, + List routePredicates, RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { return new GatewayLegacyControllerEndpoint(routeDefinitionLocator, - globalFilters, gatewayFilters, routeDefinitionWriter, routeLocator); + globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, + routeLocator); } } diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java index 4a3291c1..f415679a 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpointTests.java @@ -16,8 +16,12 @@ package org.springframework.cloud.gateway.actuate; +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import org.assertj.core.util.Maps; import org.junit.Test; @@ -28,13 +32,23 @@ import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.gateway.filter.FilterDefinition; +import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory; +import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; +import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition; +import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.PermitAllSecurityConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.server.ServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @@ -93,6 +107,92 @@ public class GatewayControllerEndpointTests { Maps.newHashMap("name", "value"))); } + @Test + public void testRouteFilters() { + testClient.get() + .uri("http://localhost:" + port + "/actuator/gateway/routefilters") + .exchange().expectStatus().isOk().expectBody(Map.class) + .consumeWith(result -> { + Map responseBody = result.getResponseBody(); + assertThat(responseBody).isNotEmpty(); + }); + } + + @Test + public void testRoutePredicates() { + testClient.get() + .uri("http://localhost:" + port + "/actuator/gateway/routepredicates") + .exchange().expectStatus().isOk().expectBody(Map.class) + .consumeWith(result -> { + Map responseBody = result.getResponseBody(); + assertThat(responseBody).isNotEmpty(); + }); + } + + @Test + public void testPostValidRouteDefinition() { + + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + + FilterDefinition prefixPathFilterDefinition = new FilterDefinition( + "PrefixPath=/test-path"); + FilterDefinition redirectToFilterDefinition = new FilterDefinition( + "RemoveResponseHeader=Sensitive-Header"); + FilterDefinition testFilterDefinition = new FilterDefinition("TestFilter"); + testRouteDefinition.setFilters(Arrays.asList(prefixPathFilterDefinition, + redirectToFilterDefinition, testFilterDefinition)); + + PredicateDefinition hostRoutePredicateDefinition = new PredicateDefinition( + "Host=myhost.org"); + PredicateDefinition methodRoutePredicateDefinition = new PredicateDefinition( + "Method=GET"); + PredicateDefinition testPredicateDefinition = new PredicateDefinition( + "Test=value"); + testRouteDefinition.setPredicates(Arrays.asList(hostRoutePredicateDefinition, + methodRoutePredicateDefinition, testPredicateDefinition)); + + testClient.post() + .uri("http://localhost:" + port + "/actuator/gateway/routes/test-route") + .accept(MediaType.APPLICATION_JSON_UTF8) + .body(BodyInserters.fromObject(testRouteDefinition)).exchange() + .expectStatus().isCreated(); + } + + @Test + public void testPostRouteWithNotExistingFilter() { + + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + + FilterDefinition filterDefinition = new FilterDefinition( + "NotExistingFilter=test-config"); + testRouteDefinition.setFilters(Collections.singletonList(filterDefinition)); + + testClient.post() + .uri("http://localhost:" + port + "/actuator/gateway/routes/test-route") + .accept(MediaType.APPLICATION_JSON_UTF8) + .body(BodyInserters.fromObject(testRouteDefinition)).exchange() + .expectStatus().isBadRequest(); + } + + @Test + public void testPostRouteWithNotExistingPredicate() { + + RouteDefinition testRouteDefinition = new RouteDefinition(); + testRouteDefinition.setUri(URI.create("http://example.org")); + + PredicateDefinition predicateDefinition = new PredicateDefinition( + "NotExistingPredicate=test-config"); + testRouteDefinition.setPredicates(Collections.singletonList(predicateDefinition)); + + testClient.post() + .uri("http://localhost:" + port + "/actuator/gateway/routes/test-route") + .accept(MediaType.APPLICATION_JSON_UTF8) + .body(BodyInserters.fromObject(testRouteDefinition)).exchange() + .expectStatus().isBadRequest(); + } + @SpringBootConfiguration @EnableAutoConfiguration @Import(PermitAllSecurityConfiguration.class) @@ -106,6 +206,39 @@ public class GatewayControllerEndpointTests { .build(); } + @Bean + public TestFilterGatewayFilterFactory customGatewayFilterFactory() { + return new TestFilterGatewayFilterFactory(); + } + + @Bean + public TestRoutePredicateFactory customGatewayPredicateFactory() { + return new TestRoutePredicateFactory(Object.class); + } + + } + + private static class TestFilterGatewayFilterFactory + extends AbstractGatewayFilterFactory { + + @Override + public GatewayFilter apply(Object config) { + return null; + } + + } + + private static class TestRoutePredicateFactory extends AbstractRoutePredicateFactory { + + TestRoutePredicateFactory(Class configClass) { + super(configClass); + } + + @Override + public Predicate apply(Object config) { + return (GatewayPredicate) serverWebExchange -> true; + } + } } diff --git a/spring-cloud-starter-gateway/src/main/resources/META-INF/spring.provides b/spring-cloud-starter-gateway/src/main/resources/META-INF/spring.provides deleted file mode 100644 index ff520021..00000000 --- a/spring-cloud-starter-gateway/src/main/resources/META-INF/spring.provides +++ /dev/null @@ -1 +0,0 @@ -provides: spring-cloud-gateway-core \ No newline at end of file