diff --git a/pom.xml b/pom.xml index 06beb9d8..74e0848b 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ org.springframework.cloud spring-cloud-build - 2.0.3.RELEASE + 2.0.4.BUILD-SNAPSHOT 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 0ee16bc5..cb4bfe1d 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 @@ -25,6 +25,7 @@ import com.netflix.hystrix.HystrixObservableCommand; import io.netty.channel.ChannelOption; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.ipc.netty.http.client.HttpClient; import reactor.ipc.netty.http.client.HttpClientOptions; import reactor.ipc.netty.options.ClientProxyOptions; @@ -122,11 +123,14 @@ import org.springframework.core.env.Environment; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.util.StringUtils; import org.springframework.validation.Validator; +import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import org.springframework.web.reactive.socket.client.WebSocketClient; import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED; import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED; @@ -243,6 +247,19 @@ public class GatewayAutoConfiguration { } } + //TODO: remove when not needed anymore + // either https://jira.spring.io/browse/SPR-17291 or + // https://github.com/spring-projects/spring-boot/issues/14520 needs to be fixed + @Bean + public HiddenHttpMethodFilter hiddenHttpMethodFilter() { + return new HiddenHttpMethodFilter() { + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return chain.filter(exchange); + } + }; + } + @Bean public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) { return new RouteLocatorBuilder(context); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java index 2879c5ab..7c72e644 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/discovery/DiscoveryClientRouteDefinitionLocator.java @@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.discovery; import java.net.URI; import java.util.Map; +import java.util.function.Predicate; import reactor.core.publisher.Flux; @@ -35,7 +36,6 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.util.StringUtils; /** - * TODO: developer configuration, in zuul, this was opt out, should be opt in * TODO: change to RouteLocator? use java dsl * @author Spencer Gibb */ @@ -44,6 +44,7 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc private final DiscoveryClient discoveryClient; private final DiscoveryLocatorProperties properties; private final String routeIdPrefix; + private final SimpleEvaluationContext evalCtxt; public DiscoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) { this.discoveryClient = discoveryClient; @@ -53,30 +54,37 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc } else { this.routeIdPrefix = this.discoveryClient.getClass().getSimpleName() + "_"; } + evalCtxt = SimpleEvaluationContext + .forReadOnlyDataBinding() + .withInstanceMethods() + .build(); } @Override public Flux getRouteDefinitions() { - SimpleEvaluationContext evalCtxt = SimpleEvaluationContext - .forReadOnlyDataBinding() - .withInstanceMethods() - .build(); SpelExpressionParser parser = new SpelExpressionParser(); Expression includeExpr = parser.parseExpression(properties.getIncludeExpression()); Expression urlExpr = parser.parseExpression(properties.getUrlExpression()); + Predicate includePredicate; + if (properties.getIncludeExpression() == null || "true".equalsIgnoreCase(properties.getIncludeExpression())) { + includePredicate = instance -> true; + } else { + includePredicate = instance -> { + Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class); + if (include == null) { + return false; + } + return include; + }; + } + return Flux.fromIterable(discoveryClient.getServices()) .map(discoveryClient::getInstances) .filter(instances -> !instances.isEmpty()) .map(instances -> instances.get(0)) - .filter(instance -> { - Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class); - if (include == null) { - return false; - } - return include; - }) + .filter(includePredicate) .map(instance -> { String serviceId = instance.getServiceId(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java index 27be9346..49ff8b26 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java @@ -19,9 +19,12 @@ package org.springframework.cloud.gateway.filter; import java.net.URI; import java.util.List; +import java.util.concurrent.TimeUnit; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.timeout.ReadTimeoutException; +import io.netty.handler.timeout.ReadTimeoutHandler; import reactor.core.publisher.Mono; import reactor.ipc.netty.NettyPipeline; import reactor.ipc.netty.http.client.HttpClient; @@ -111,17 +114,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { proxyRequest.header(HttpHeaders.HOST, host); } + if (properties.getResponseTimeout() != null) { + proxyRequest.context(ctx -> ctx.addHandlerFirst( + new ReadTimeoutHandler(properties.getResponseTimeout().toMillis(), TimeUnit.MILLISECONDS))); + } + return proxyRequest.sendHeaders() //I shouldn't need this .send(request.getBody().map(dataBuffer -> ((NettyDataBuffer) dataBuffer).getNativeBuffer())); }); - if (properties.getResponseTimeout() != null) { - responseMono.timeout(properties.getResponseTimeout(), - Mono.error(new TimeoutException("Response took longer than timeout: " + - properties.getResponseTimeout()))); - } - return responseMono.doOnNext(res -> { ServerHttpResponse response = exchange.getResponse(); // put headers and status so filters can modify the response @@ -150,6 +152,10 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { // Defer committing the response until all route filters have run // Put client response as ServerWebExchange attribute and write response later NettyWriteResponseFilter exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res); - }).then(chain.filter(exchange)); + }) + .onErrorMap(t -> properties.getResponseTimeout() != null && t instanceof ReadTimeoutException, + t -> new TimeoutException("Response took longer than timeout: " + + properties.getResponseTimeout())) + .then(chain.filter(exchange)); } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java index 53ea2623..b330aa50 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/FilteringWebHandler.java @@ -23,7 +23,10 @@ import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + import org.springframework.cloud.gateway.filter.GatewayFilter; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.OrderedGatewayFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; @@ -31,13 +34,10 @@ import org.springframework.cloud.gateway.route.Route; import org.springframework.core.Ordered; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.web.server.ServerWebExchange; -import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.web.server.WebHandler; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; -import reactor.core.publisher.Mono; - /** * WebHandler that delegates to a chain of {@link GlobalFilter} instances and * {@link GatewayFilterFactory} instances then to the target {@link WebHandler}. @@ -82,7 +82,9 @@ public class FilteringWebHandler implements WebHandler { //TODO: needed or cached? AnnotationAwareOrderComparator.sort(combined); - logger.debug("Sorted gatewayFilterFactories: "+ combined); + if (logger.isDebugEnabled()) { + logger.debug("Sorted gatewayFilterFactories: "+ combined); + } return new DefaultGatewayFilterChain(combined).filter(exchange); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java index bcbe5d29..2b1d9ac0 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMapping.java @@ -61,7 +61,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { if (managmentPort != null && exchange.getRequest().getURI().getPort() == managmentPort.intValue()) { return Mono.empty(); } - exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName()); + exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName()); return lookupRoute(exchange) // .log("route-predicate-handler-mapping", Level.FINER) //name this @@ -146,4 +146,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { protected void validateRoute(Route route, ServerWebExchange exchange) { } + protected String getSimpleName() { + return "RoutePredicateHandlerMapping"; + } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java index 507ec2d9..5003ed37 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactory.java @@ -17,6 +17,7 @@ package org.springframework.cloud.gateway.handler.predicate; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.function.Predicate; @@ -40,6 +41,7 @@ import static org.springframework.http.server.PathContainer.parsePath; */ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory { private static final Log log = LogFactory.getLog(RoutePredicateFactory.class); + private static final String MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY = "matchOptionalTrailingSeparator"; private PathPatternParser pathPatternParser = new PathPatternParser(); @@ -53,12 +55,13 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory shortcutFieldOrder() { - return Collections.singletonList(PATTERN_KEY); + return Arrays.asList(PATTERN_KEY, MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY); } @Override public Predicate apply(Config config) { synchronized (this.pathPatternParser) { + pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchOptionalTrailingSeparator()); config.pathPattern = this.pathPatternParser.parse(config.pattern); } return exchange -> { @@ -88,6 +91,7 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory c.setPattern(pattern))); } + /** + * A predicate that checks if the path of the request matches the given pattern + * @param pattern the pattern to check the path against. + * The pattern is a {@link org.springframework.util.PathMatcher} pattern + * @param matchOptionalTrailingSeparator set to false if you do not want this path to match + * when there is a trailing / + * @return a {@link BooleanSpec} to be used to add logical operators + */ + public BooleanSpec path(String pattern, boolean matchOptionalTrailingSeparator) { + return asyncPredicate(getBean(PathRoutePredicateFactory.class) + .applyAsync(c -> c.setPattern(pattern).setMatchOptionalTrailingSeparator(matchOptionalTrailingSeparator))); + } + /** * This predicate is BETA and may be subject to change in a future release. * A predicate that checks the contents of the request body diff --git a/spring-cloud-gateway-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-gateway-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 00000000..c67b417b --- /dev/null +++ b/spring-cloud-gateway-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,22 @@ +{ + "properties": [ + { + "name": "spring.cloud.gateway.enabled", + "type": "java.lang.Boolean", + "description": "Enables gateway functionality.", + "defaultValue": "true" + }, + { + "name": "spring.cloud.gateway.forwarded.enabled", + "type": "java.lang.Boolean", + "description": "Enables the ForwardedHeadersFilter.", + "defaultValue": "true" + }, + { + "name": "spring.cloud.gateway.metrics.enabled", + "type": "java.lang.Boolean", + "description": "Enables the collection of metrics data.", + "defaultValue": "false" + } + ] +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java new file mode 100644 index 00000000..aefd24ae --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/NettyRoutingFilterIntegrationTests.java @@ -0,0 +1,61 @@ +/* + * 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; + +import java.util.Map; + +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.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(properties = "spring.cloud.gateway.httpclient.response-timeout=3s", webEnvironment = RANDOM_PORT) +@DirtiesContext +@SuppressWarnings("unchecked") +public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests { + + @Test + public void responseTimeoutWorks() { + testClient.get() + .uri("/delay/5") + .exchange() + .expectStatus().is5xxServerError() + .expectBody(Map.class) + .consumeWith(result -> { + Map body = result.getResponseBody(); + assertThat(body).containsEntry("message", "Response took longer than timeout: PT3S"); + }); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java index 5603c75c..70e50843 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/PathRoutePredicateFactoryTests.java @@ -44,6 +44,12 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests { .expectStatus().isOk() .expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName()) .expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test"); + + //since the configuration does not allow the trailing / to match this should fail + testClient.get().uri("/abc/123/function/") + .header(HttpHeaders.HOST, "www.path.org") + .exchange() + .expectStatus().is4xxClientError(); } @Test diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 6ae8ed00..41e8bb5d 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -128,7 +128,7 @@ spring: - id: path_test uri: ${test.uri} predicates: - - Path=/{org}/{scope}/function + - Path=/{org}/{scope}/function,false - Host=**.path.org filters: - SetPath=/anything/{org}{scope} diff --git a/spring-cloud-gateway-dependencies/pom.xml b/spring-cloud-gateway-dependencies/pom.xml index 807035c1..032b78d7 100644 --- a/spring-cloud-gateway-dependencies/pom.xml +++ b/spring-cloud-gateway-dependencies/pom.xml @@ -5,7 +5,7 @@ spring-cloud-dependencies-parent org.springframework.cloud - 2.0.3.RELEASE + 2.0.4.BUILD-SNAPSHOT