diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 73a145cb..2d02210b 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -781,7 +781,46 @@ The `GlobalFilter` interface has the same signature as `GatewayFilter`. These ar === Combined Global Filter and GatewayFilter Ordering -TODO: document ordering +When a request comes in (and matches a Route) the Filtering Web Handler will add all instances of `GlobalFilter` and all route specific instances of `GatewayFilter` to a filter chain. This combined filter chain is sorted by the `org.springframework.core.Ordered` interface, which can be set by implementing the `getOrder()` method or by using the `@Order` annotation. + +As Spring Cloud Gateway distinguishes between "pre" and "post" phases for filter logic execution (see: How It Works), the filter with the highest precedence will be the first in the "pre"-phase and the last in the "post"-phase. + +.ExampleConfiguration.java +[source,java] +---- +@Bean +@Order(-1) +public GlobalFilter a() { + return (exchange, chain) -> { + log.info("first pre filter"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("third post filter"); + })); + }; +} + +@Bean +@Order(0) +public GlobalFilter b() { + return (exchange, chain) -> { + log.info("second pre filter"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("second post filter"); + })); + }; +} + +@Bean +@Order(1) +public GlobalFilter c() { + return (exchange, chain) -> { + log.info("third pre filter"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("first post filter"); + })); + }; +} +---- === Forward Routing Filter @@ -990,7 +1029,7 @@ public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory { return chain.filter(exchange).then(Mono.fromRunnable(() -> { - ServerHttpReponse response = exchange.getResponse(); + ServerHttpResponse response = exchange.getResponse(); //Manipulate the response in some way })); }; diff --git a/pom.xml b/pom.xml index 2c19478a..b2d76650 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ org.springframework.cloud spring-cloud-build - 2.0.2.RELEASE + 2.0.2.BUILD-SNAPSHOT @@ -48,8 +48,8 @@ UTF-8 UTF-8 1.8 - 2.0.0.BUILD-SNAPSHOT - 2.0.0.BUILD-SNAPSHOT + 2.0.1.BUILD-SNAPSHOT + 2.0.1.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 23ff89a8..e61b79f7 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 @@ -89,6 +89,7 @@ import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping; import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory; @@ -216,8 +217,9 @@ public class GatewayAutoConfiguration { @Bean public NettyRoutingFilter routingFilter(HttpClient httpClient, - ObjectProvider> headersFilters) { - return new NettyRoutingFilter(httpClient, headersFilters); + ObjectProvider> headersFilters, + HttpClientProperties properties) { + return new NettyRoutingFilter(httpClient, headersFilters, properties); } @Bean @@ -437,6 +439,11 @@ public class GatewayAutoConfiguration { return new WeightRoutePredicateFactory(); } + @Bean + public CloudFoundryRouteServiceRoutePredicateFactory cloudFoundryRouteServiceRoutePredicateFactory() { + return new CloudFoundryRouteServiceRoutePredicateFactory(); + } + // GatewayFilter Factory beans @Bean diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java index ef627a59..da365d5f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java @@ -20,6 +20,8 @@ package org.springframework.cloud.gateway.config; import org.springframework.boot.context.properties.ConfigurationProperties; import reactor.ipc.netty.resources.PoolResources; +import java.time.Duration; + /** * Configuration properties for the Netty {@link reactor.ipc.netty.http.client.HttpClient} */ @@ -29,6 +31,9 @@ public class HttpClientProperties { /** The connect timeout in millis, the default is 45s. */ private Integer connectTimeout; + /** The response timeout. */ + private Duration responseTimeout; + /** Pool configuration for Netty HttpClient */ private Pool pool = new Pool(); @@ -42,6 +47,14 @@ public class HttpClientProperties { return connectTimeout; } + public Duration getResponseTimeout() { + return responseTimeout; + } + + public void setResponseTimeout(Duration responseTimeout) { + this.responseTimeout = responseTimeout; + } + public void setConnectTimeout(Integer connectTimeout) { this.connectTimeout = connectTimeout; } @@ -220,6 +233,7 @@ public class HttpClientProperties { public String toString() { return "HttpClientProperties{" + "connectTimeout=" + connectTimeout + + ", responseTimeout=" + responseTimeout + ", pool=" + pool + ", proxy=" + proxy + ", ssl=" + ssl + 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 ab36e983..27be9346 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 @@ -26,10 +26,13 @@ import reactor.core.publisher.Mono; import reactor.ipc.netty.NettyPipeline; import reactor.ipc.netty.http.client.HttpClient; import reactor.ipc.netty.http.client.HttpClientRequest; +import reactor.ipc.netty.http.client.HttpClientResponse; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.cloud.gateway.config.HttpClientProperties; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter.Type; +import org.springframework.cloud.gateway.support.TimeoutException; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpHeaders; @@ -55,11 +58,14 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { private final HttpClient httpClient; private final ObjectProvider> headersFilters; + private final HttpClientProperties properties; public NettyRoutingFilter(HttpClient httpClient, - ObjectProvider> headersFilters) { + ObjectProvider> headersFilters, + HttpClientProperties properties) { this.httpClient = httpClient; this.headersFilters = headersFilters; + this.properties = properties; } @Override @@ -93,7 +99,7 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { boolean preserveHost = exchange.getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); - return this.httpClient.request(method, url, req -> { + Mono responseMono = this.httpClient.request(method, url, req -> { final HttpClientRequest proxyRequest = req.options(NettyPipeline.SendOptions::flushOnEach) .headers(httpHeaders) .chunkedTransfer(chunkedTransfer) @@ -107,8 +113,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { return proxyRequest.sendHeaders() //I shouldn't need this .send(request.getBody().map(dataBuffer -> - ((NettyDataBuffer)dataBuffer).getNativeBuffer())); - }).doOnNext(res -> { + ((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 HttpHeaders headers = new HttpHeaders(); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java index f8c4c18c..95508ccc 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/WebsocketRoutingFilter.java @@ -104,7 +104,7 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered { private void changeSchemeIfIsWebSocketUpgrade(ServerWebExchange exchange) { // Check the Upgrade URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); - String scheme = requestUrl.getScheme(); + String scheme = requestUrl.getScheme().toLowerCase(); String upgrade = exchange.getRequest().getHeaders().getUpgrade(); // change the scheme if the socket client send a "http" or "https" if ("WebSocket".equalsIgnoreCase(upgrade) && ("http".equals(scheme) || "https".equals(scheme))) { @@ -117,8 +117,9 @@ public class WebsocketRoutingFilter implements GlobalFilter, Ordered { } } - private String convertHttpToWs(String scheme) { - return "http".equals(scheme) ? "ws" : "https".equals(scheme) ? "wws" : scheme; + /* for testing */ static String convertHttpToWs(String scheme) { + scheme = scheme.toLowerCase(); + return "http".equals(scheme) ? "ws" : "https".equals(scheme) ? "wss" : scheme; } private static class ProxyWebSocketHandler implements WebSocketHandler { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java index 47a6f735..3dff1d6c 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/RetryGatewayFilterFactory.java @@ -26,6 +26,7 @@ import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; +import org.springframework.cloud.gateway.support.TimeoutException; import reactor.core.publisher.Mono; import reactor.retry.Repeat; import reactor.retry.RepeatContext; @@ -146,12 +147,11 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory List toList(T item) { - ArrayList list = new ArrayList<>(); - list.add(item); - return list; + private static List toList(T... items) { + return new ArrayList<>(Arrays.asList(items)); } + @SuppressWarnings("unchecked") public static class RetryConfig { private int retries = 3; @@ -161,7 +161,7 @@ public class RetryGatewayFilterFactory extends AbstractGatewayFilterFactory methods = toList(HttpMethod.GET); - private List> exceptions = toList(IOException.class); + private List> exceptions = toList(IOException.class, TimeoutException.class); public RetryConfig setRetries(int retries) { this.retries = retries; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java index cfcf2de1..6089d16f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/rewrite/ModifyResponseBodyGatewayFilterFactory.java @@ -45,6 +45,8 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.server.ServerWebExchange; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; + /** * This filter is BETA and may be subject to change in a future release. */ @@ -82,7 +84,7 @@ public class ModifyResponseBodyGatewayFilterFactory Class inClass = config.getInClass(); Class outClass = config.getOutClass(); - MediaType originalResponseContentType = exchange.getAttribute("original_response_content_type"); + MediaType originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(originalResponseContentType); ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders); 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 665e9294..84e862b8 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 @@ -20,6 +20,7 @@ package org.springframework.cloud.gateway.handler; import java.util.function.Function; import org.springframework.cloud.gateway.config.GlobalCorsProperties; +import reactor.core.publisher.Mono; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.web.cors.CorsConfiguration; @@ -30,8 +31,6 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; -import reactor.core.publisher.Mono; - /** * @author Spencer Gibb */ @@ -90,12 +89,20 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping { } protected Mono lookupRoute(ServerWebExchange exchange) { - return this.routeLocator.getRoutes() - .filterWhen(route -> { - // add the current route we are testing - exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId()); - return route.getPredicate().apply(exchange); - }) + return this.routeLocator + .getRoutes() + //individually filter routes so that filterWhen error delaying is not a problem + .concatMap(route -> Mono + .just(route) + .filterWhen(r -> { + // add the current route we are testing + exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId()); + return r.getPredicate().apply(exchange); + }) + //instead of immediately stopping main flux due to error, log and swallow it + .doOnError(e -> logger.error("Error applying predicate for route: "+route.getId(), e)) + .onErrorResume(e -> Mono.empty()) + ) // .defaultIfEmpty() put a static Route not found // or .switchIfEmpty() // .switchIfEmpty(Mono.empty().log("noroute")) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java index a4c38c0d..c7e38ccb 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactory.java @@ -54,8 +54,11 @@ public class QueryRoutePredicateFactory extends AbstractRoutePredicateFactory values = exchange.getRequest().getQueryParams().get(config.param); + if (values == null) { + return false; + } for (String value : values) { - if (value.matches(config.regexp)) { + if (value != null && value.matches(config.regexp)) { return true; } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java index 42ea5417..58835691 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/Route.java @@ -114,7 +114,9 @@ public class Route implements Ordered { public B uri(URI uri) { this.uri = uri; - if (this.uri.getPort() < 0 && this.uri.getScheme().startsWith("http")) { + String scheme = this.uri.getScheme(); + Assert.hasText(scheme, "The parameter [" + this.uri + "] format is incorrect, scheme can not be empty"); + if (this.uri.getPort() < 0 && scheme.startsWith("http")) { // default known http ports int port = this.uri.getScheme().equals("https") ? 443 : 80; this.uri = UriComponentsBuilder.fromUri(this.uri) diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index dc805737..993d849a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -23,6 +23,7 @@ import org.springframework.cloud.gateway.handler.AsyncPredicate; import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BeforeRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.BetweenRoutePredicateFactory; +import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.CookieRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HeaderRoutePredicateFactory; import org.springframework.cloud.gateway.handler.predicate.HostRoutePredicateFactory; @@ -248,6 +249,12 @@ public class PredicateSpec extends UriSpec { .setWeight(weight))); } + public BooleanSpec cloudFoundryRouteService() { + return predicate( + getBean(CloudFoundryRouteServiceRoutePredicateFactory.class).apply(c -> { + })); + } + /** * A predicate which is always true * @return a {@link BooleanSpec} to be used to add logical operators diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java new file mode 100644 index 00000000..8d429bc2 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/TimeoutException.java @@ -0,0 +1,28 @@ +/* + * 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.support; + +public class TimeoutException extends Exception { + + public TimeoutException() { + } + + public TimeoutException(String message) { + super(message); + } +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index 7161b145..94a1b429 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -62,6 +62,7 @@ public class GatewayAutoConfigurationTests { GatewayAutoConfiguration.class)) .withPropertyValues("spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true", "spring.cloud.gateway.httpclient.connect-timeout=10", + "spring.cloud.gateway.httpclient.response-timeout=10s", "spring.cloud.gateway.httpclient.pool.type=fixed", "spring.cloud.gateway.httpclient.proxy.host=myhost") .run(context -> { diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java new file mode 100644 index 00000000..59f8643f --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/ForwardRoutingFilterTests.java @@ -0,0 +1,112 @@ +package org.springframework.cloud.gateway.filter; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.core.Ordered; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.reactive.DispatcherHandler; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; + +/** + * @author Arjun Curat + */ + +@RunWith(MockitoJUnitRunner.class) +public class ForwardRoutingFilterTests { + + private ServerWebExchange exchange; + + @Mock + private GatewayFilterChain chain; + + @Mock + private DispatcherHandler dispatcherHandler; + + @InjectMocks + private ForwardRoutingFilter forwardRoutingFilter; + + @Before + public void setup() { + exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localendpoint").build()); + } + + @Test + public void shouldNotFilterWhenGatewayRequestUrlSchemeIsNotForward() { + URI uri = UriComponentsBuilder.fromUriString("http://endpoint").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + forwardRoutingFilter.filter(exchange, chain); + + verifyZeroInteractions(dispatcherHandler); + verify(chain).filter(exchange); + verifyNoMoreInteractions(chain); + } + + @Test + public void shouldFilterWhenGatewayRequestUrlSchemeIsForward() { + URI uri = UriComponentsBuilder.fromUriString("forward://endpoint").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + + assertThat(exchange.getAttributes().get(GATEWAY_ALREADY_ROUTED_ATTR)).isNull(); + + forwardRoutingFilter.filter(exchange, chain); + + verifyNoMoreInteractions(chain); + verify(dispatcherHandler).handle(exchange); + + assertThat(exchange.getAttributes().get(GATEWAY_ALREADY_ROUTED_ATTR)).isEqualTo(true); + } + + @Test + public void shouldFilterAndKeepHostPathAsSpecified() { + + URI uri = UriComponentsBuilder.fromUriString("forward://host/outage").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ServerWebExchange.class); + + forwardRoutingFilter.filter(exchange, chain); + + verify(dispatcherHandler).handle(captor.capture()); + + ServerWebExchange webExchange = captor.getValue(); + + URI forwardedUrl = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + + assertThat(forwardedUrl).hasScheme("forward").hasHost("host").hasPath("/outage"); + + } + + + @Test + public void shouldNotFilterWhenGatewayRequestUrlSchemeIsForwardButAlreadyRouted() { + URI uri = UriComponentsBuilder.fromUriString("forward://host").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); + exchange.getAttributes().put(GATEWAY_ALREADY_ROUTED_ATTR, true); + + forwardRoutingFilter.filter(exchange, chain); + + verifyZeroInteractions(dispatcherHandler); + verify(chain).filter(exchange); + verifyNoMoreInteractions(chain); + } + + @Test + public void orderIsLowestPrecedence() { + assertThat(forwardRoutingFilter.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE); + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java new file mode 100644 index 00000000..a7e35903 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/WebscketRoutingFilterTests.java @@ -0,0 +1,35 @@ +/* + * 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 org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.gateway.filter.WebsocketRoutingFilter.convertHttpToWs; + +public class WebscketRoutingFilterTests { + + @Test + public void testConvertHttpToWs() { + assertThat(convertHttpToWs("http")).isEqualTo("ws"); + assertThat(convertHttpToWs("HTTP")).isEqualTo("ws"); + assertThat(convertHttpToWs("https")).isEqualTo("wss"); + assertThat(convertHttpToWs("HTTPS")).isEqualTo("wss"); + assertThat(convertHttpToWs("tcp")).isEqualTo("tcp"); + } +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java new file mode 100644 index 00000000..1b2613f3 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingTest.java @@ -0,0 +1,98 @@ +package org.springframework.cloud.gateway.handler; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.gateway.config.GlobalCorsProperties; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import static org.hamcrest.Matchers.containsString; + +/** + * @author Simon Baslé + */ +public class RoutePredicateHandlerMappingTest { + + @Rule + public OutputCapture outputCapture = new OutputCapture(); + + @Test + public void lookupRouteFromSyncPredicates() { + Route routeFalse = Route.async() + .id("routeFalse") + .uri("http://localhost") + .predicate(swe -> false) + .build(); + Route routeFail = Route.async() + .id("routeFail") + .uri("http://localhost") + .predicate(swe -> { throw new IllegalStateException("boom"); }) + .build(); + Route routeTrue = Route.async() + .id("routeTrue") + .uri("http://localhost") + .predicate(swe -> true) + .build(); + RouteLocator routeLocator = + () -> Flux.just(routeFalse, routeFail, routeTrue).hide(); + RoutePredicateHandlerMapping mapping = + new RoutePredicateHandlerMapping(null, routeLocator, new GlobalCorsProperties()); + + final Mono routeMono = + mapping.lookupRoute(Mockito.mock(ServerWebExchange.class)); + + StepVerifier.create(routeMono.map(Route::getId)) + .expectNext("routeTrue") + .verifyComplete(); + + outputCapture.expect(containsString("Error applying predicate for route: routeFail")); + outputCapture.expect(containsString("java.lang.IllegalStateException: boom")); + } + + @Test + public void lookupRouteFromAsyncPredicates() { + Route routeFalse = Route.async() + .id("routeFalse") + .uri("http://localhost") + .asyncPredicate(swe -> Mono.just(false)) + .build(); + Route routeError = Route.async() + .id("routeError") + .uri("http://localhost") + .asyncPredicate(swe -> Mono.error(new IllegalStateException("boom1"))) + .build(); + Route routeFail = Route.async() + .id("routeFail") + .uri("http://localhost") + .asyncPredicate(swe -> { throw new IllegalStateException("boom2"); }) + .build(); + Route routeTrue = Route.async() + .id("routeTrue") + .uri("http://localhost") + .asyncPredicate(swe -> Mono.just(true)) + .build(); + RouteLocator routeLocator = + () -> Flux.just(routeFalse, routeError, routeFail, routeTrue).hide(); + RoutePredicateHandlerMapping mapping = + new RoutePredicateHandlerMapping(null, routeLocator, new GlobalCorsProperties()); + + final Mono routeMono = + mapping.lookupRoute(Mockito.mock(ServerWebExchange.class)); + + StepVerifier.create(routeMono.map(Route::getId)) + .expectNext("routeTrue") + .verifyComplete(); + + outputCapture.expect(containsString("Error applying predicate for route: routeError")); + outputCapture.expect(containsString("java.lang.IllegalStateException: boom1")); + + outputCapture.expect(containsString("Error applying predicate for route: routeFail")); + outputCapture.expect(containsString("java.lang.IllegalStateException: boom2")); + } +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java new file mode 100644 index 00000000..14701593 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests.java @@ -0,0 +1,76 @@ +package org.springframework.cloud.gateway.handler.predicate; + +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.boot.web.server.LocalServerPort; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import com.fasterxml.jackson.databind.JsonNode; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +/** + * @author Toshiaki Maki + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class CloudFoundryRouteServiceRoutePredicateFactoryIntegrationTests + extends BaseWebClientTests { + @LocalServerPort + int port; + + @Test + public void predicateWorkWithProperties() { + testClient.get().uri("/").header("Host", "props.routeservice.example.com") + .header("X-CF-Forwarded-Url", + "http://localhost:" + port + "/actuator/health") + .header("X-CF-Proxy-Signature", "foo") + .header("X-CF-Proxy-Metadata", "bar").exchange() + .expectBody(JsonNode.class) + .consumeWith(r -> assertThat(r.getResponseBody().has("status")).isTrue()); + } + + @Test + public void predicateWillNotWorkUnlessHeadersAreEnough() { + testClient.get().uri("/").header("Host", "props.routeservice.example.com") + .header("X-CF-Forwarded-Url", + "http://localhost:" + port + "/actuator/health") + .header("X-CF-Proxy-Metadata", "bar").exchange().expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); + } + + @Test + public void predicateWorkWithDsl() { + testClient.get().uri("/").header("Host", "dsl.routeservice.example.com") + .header("X-CF-Forwarded-Url", + "http://localhost:" + port + "/actuator/health") + .header("X-CF-Proxy-Signature", "foo") + .header("X-CF-Proxy-Metadata", "bar").exchange() + .expectBody(JsonNode.class) + .consumeWith(r -> assertThat(r.getResponseBody().has("status")).isTrue()); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + @Bean + public RouteLocator routeLocator(RouteLocatorBuilder builder) { + return builder.routes().route(r -> r.cloudFoundryRouteService().and() + .header("Host", "dsl.routeservice.example.com") + .filters(f -> f.requestHeaderToRequestUri("X-CF-Forwarded-Url")) + .uri("http://example.com")).build(); + } + } +} \ No newline at end of file diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java new file mode 100644 index 00000000..f97e08e4 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/QueryRoutePredicateFactoryTests.java @@ -0,0 +1,96 @@ +/* + * 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.handler.predicate; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.rule.OutputCapture; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +public class QueryRoutePredicateFactoryTests extends BaseWebClientTests { + + @Rule + public OutputCapture output = new OutputCapture(); + + @Test + public void noQueryParamWorks() { + testClient.get().uri("/get") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin");; + + output.expect(not(containsString("Error applying predicate for route: foo_query_param"))); + } + + @Test + public void queryParamWorks() { + testClient.get().uri("/get?foo=bar") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "foo_query_param");; + } + + @Test + public void emptyQueryParamWorks() { + testClient.get().uri("/get?foo") + .exchange() + .expectStatus().isOk() + .expectHeader().valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin");; + + output.expect(not(containsString("Error applying predicate for route: foo_query_param"))); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + + @Value("${test.uri}") + private String uri; + + @Bean + RouteLocator queryRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("foo_query_param", r -> + r.query("foo", "bar") + .filters(f -> f.prefixPath("/httpbin")) + .uri(uri)) + .build(); + } + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java index 9eec371a..cfdcd33d 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteTests.java @@ -17,12 +17,17 @@ package org.springframework.cloud.gateway.route; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; import static org.assertj.core.api.Assertions.assertThat; public class RouteTests { + @Rule + public ExpectedException exception = ExpectedException.none(); + @Test public void defeaultHttpPort() { Route route = Route.async().id("1") @@ -47,7 +52,6 @@ public class RouteTests { .hasPort(443); } - @Test public void fullUri() { Route route = Route.async().id("1") @@ -59,4 +63,12 @@ public class RouteTests { .hasScheme("http") .hasPort(8080); } + + @Test + public void nullScheme() { + exception.expect(IllegalArgumentException.class); + Route.async().id("1") + .predicate(exchange -> true) + .uri("/pathonly"); + } } diff --git a/spring-cloud-gateway-core/src/test/resources/application.yml b/spring-cloud-gateway-core/src/test/resources/application.yml index 5dbdfa9c..71763f20 100644 --- a/spring-cloud-gateway-core/src/test/resources/application.yml +++ b/spring-cloud-gateway-core/src/test/resources/application.yml @@ -258,6 +258,14 @@ spring: filters: - RequestHeaderToRequestUri=X-CF-Forwarded-Url + # ===================================== + - id: cloudfoundry_routeservice_test + uri: ${test.uri} + predicates: + - CloudFoundryRouteService= + filters: + - RequestHeaderToRequestUri=X-CF-Forwarded-Url + # ===================================== - id: default_path_to_httpbin uri: ${test.uri}