diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 3152d9d6..8d5cda74 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -773,7 +773,7 @@ spring: statuses: BAD_GATEWAY ---- -NOTE: At this time a URI using the `forward` protocol does not support using the retry filter. +NOTE: When using the retry filter with a `forward:` prefixed URL, the target endpoint should be written carefully so that in case of an error it does not do anything that could result in a response being sent to the client and committed. For example, if the target endpoint is an annotated controller, the target controller method should not return `ResponseEntity` with an error status code. Instead it should throw an `Exception`, or signal an error, e.g. via a `Mono.error(ex)` return value, which the retry filter can be configured to handle by retrying. === RequestSize GatewayFilter Factory The RequestSize GatewayFilter Factory can restrict a request from reaching the downstream service , when the request size is greater than the permissible limit. The filter takes `RequestSize` as parameter which is the permissible size limit of the request defined in bytes. @@ -976,7 +976,24 @@ spring: - cert2.pem ---- -If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore. +If the Spring Cloud Gateway is not provisioned with trusted certificates the default trust store is used (which can be overriden with system property javax.net.ssl.trustStore). + +=== TLS Handshake + +The Gateway maintains a client pool that it uses to route to backends. When communicating over https the client initiates a TLS handshake. A number of timeouts are assoicated with this handshake. These timeouts can be configured (defaults shown): + +.application.yml +[source,yaml] +---- +spring: + cloud: + gateway: + httpclient: + ssl: + handshake-timeout-millis: 10000 + close-notify-flush-timeout-millis: 3000 + close-notify-read-timeout-millis: 0 +---- == Configuration 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 52421b1f..b27cfd63 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 @@ -182,7 +182,7 @@ http POST :8080/admin/gateway/routes/apiaddreqhead uri=http://httpbin.org:80 pre return this.routeDefinitionLocator.getRouteDefinitions() .filter(route -> route.getId().equals(id)) .singleOrEmpty() - .map(route -> ResponseEntity.ok(route)) + .map(ResponseEntity::ok) .switchIfEmpty(Mono.just(ResponseEntity.notFound().build())); } 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 fc963221..887c2a29 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 @@ -29,6 +29,7 @@ import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; import reactor.netty.resources.ConnectionProvider; import reactor.netty.tcp.ProxyProvider; +import reactor.netty.tcp.SslProvider; import rx.RxReactiveStreams; import org.springframework.beans.factory.ObjectProvider; @@ -208,15 +209,21 @@ public class GatewayAutoConfiguration { || ssl.isUseInsecureTrustManager()) { httpClient = httpClient.secure(sslContextSpec -> { // configure ssl + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + X509Certificate[] trustedX509Certificates = ssl .getTrustedX509CertificatesForTrustManager(); if (trustedX509Certificates.length > 0) { - sslContextSpec.sslContext(SslContextBuilder.forClient() - .trustManager(trustedX509Certificates)); + sslContextBuilder.trustManager(trustedX509Certificates); } else if (ssl.isUseInsecureTrustManager()) { - sslContextSpec.sslContext(SslContextBuilder.forClient() - .trustManager(InsecureTrustManagerFactory.INSTANCE)); + sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); } + + sslContextSpec.sslContext(sslContextBuilder) + .defaultConfiguration(SslProvider.DefaultConfigurationType.NONE) + .handshakeTimeoutMillis(ssl.getHandshakeTimeoutMillis()) + .closeNotifyFlushTimeoutMillis(ssl.getCloseNotifyFlushTimeoutMillis()) + .closeNotifyReadTimeoutMillis(ssl.getCloseNotifyReadTimeoutMillis()); }); } @@ -450,8 +457,8 @@ public class GatewayAutoConfiguration { } @Bean - public ReadBodyPredicateFactory readBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) { - return new ReadBodyPredicateFactory(codecConfigurer); + public ReadBodyPredicateFactory readBodyPredicateFactory() { + return new ReadBodyPredicateFactory(); } @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 8233f3d9..e98deaee 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 @@ -222,8 +222,13 @@ public class HttpClientProperties { public class Ssl { /** Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production. */ private boolean useInsecureTrustManager = false; - + private List trustedX509Certificates = new ArrayList<>(); + + // use netty default SSL timeouts + private long handshakeTimeoutMillis = 10000L; + private long closeNotifyFlushTimeoutMillis = 3000L; + private long closeNotifyReadTimeoutMillis = 0L; public List getTrustedX509Certificates() { return trustedX509Certificates; @@ -268,11 +273,42 @@ public class HttpClientProperties { this.useInsecureTrustManager = useInsecureTrustManager; } + public long getHandshakeTimeoutMillis() { + return handshakeTimeoutMillis; + } + + public void setHandshakeTimeoutMillis(long handshakeTimeoutMillis) { + this.handshakeTimeoutMillis = handshakeTimeoutMillis; + } + + public long getCloseNotifyFlushTimeoutMillis() { + return closeNotifyFlushTimeoutMillis; + } + + public void setCloseNotifyFlushTimeoutMillis(long closeNotifyFlushTimeoutMillis) { + this.closeNotifyFlushTimeoutMillis = closeNotifyFlushTimeoutMillis; + } + + public long getCloseNotifyReadTimeoutMillis() { + return closeNotifyReadTimeoutMillis; + } + + public void setCloseNotifyReadTimeoutMillis(long closeNotifyReadTimeoutMillis) { + this.closeNotifyReadTimeoutMillis = closeNotifyReadTimeoutMillis; + } + @Override public String toString() { return "Ssl {useInsecureTrustManager=" + useInsecureTrustManager - + ", trustedX509Certificates=" + trustedX509Certificates + "}"; + + ", trustedX509Certificates=" + trustedX509Certificates + + ", handshakeTimeoutMillis=" + handshakeTimeoutMillis + + ", closeNotifyFlushTimeoutMillis=" + + closeNotifyFlushTimeoutMillis + + ", closeNotifyReadTimeoutMillis=" + + closeNotifyReadTimeoutMillis + "}"; } + + } @Override diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java index 16760f23..a7f758b2 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/AdaptCachedBodyGlobalFilter.java @@ -39,6 +39,7 @@ public class AdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered { return body; } }; + exchange.getAttributes().remove(CACHED_REQUEST_BODY_KEY); return chain.filter(exchange.mutate().request(decorator).build()); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java index 5923e871..e3545ddf 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/FilterDefinition.java @@ -41,7 +41,7 @@ public class FilterDefinition { } public FilterDefinition(String text) { - int eqIdx = text.indexOf("="); + int eqIdx = text.indexOf('='); if (eqIdx <= 0) { setName(text); return; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java index fd879517..e76046e5 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilter.java @@ -68,11 +68,14 @@ public class RouteToRequestUrlFilter implements GlobalFilter, Ordered { routeUri = URI.create(routeUri.getSchemeSpecificPart()); } - URI requestUrl = UriComponentsBuilder.fromUri(uri) - .uri(routeUri) + URI mergedUrl = UriComponentsBuilder.fromUri(uri) + // .uri(routeUri) + .scheme(routeUri.getScheme()) + .host(routeUri.getHost()) + .port(routeUri.getPort()) .build(encoded) .toUri(); - exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl); return chain.filter(exchange); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java index 6d42ec39..f37b9e1e 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactory.java @@ -36,10 +36,12 @@ import rx.Subscription; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; -import org.springframework.http.HttpStatus; +import org.springframework.cloud.gateway.support.TimeoutException; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; @@ -52,6 +54,7 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.c /** * Depends on `spring-cloud-starter-netflix-hystrix`, {@see http://cloud.spring.io/spring-cloud-netflix/} * @author Spencer Gibb + * @author Michele Mancioppi */ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory { private final ObjectProvider dispatcherHandler; @@ -98,8 +101,24 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory>) throwable -> { if (throwable instanceof HystrixRuntimeException) { HystrixRuntimeException e = (HystrixRuntimeException) throwable; - if (e.getFailureType() == TIMEOUT) { - return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT)); + HystrixRuntimeException.FailureType failureType = e.getFailureType(); + + switch (failureType) { + case TIMEOUT: + return Mono.error(new TimeoutException()); + case COMMAND_EXCEPTION: { + Throwable cause = e.getCause(); + + /* + * We forsake here the null check for cause as HystrixRuntimeException will + * always have a cause if the failure type is COMMAND_EXCEPTION. + */ + if (cause instanceof ResponseStatusException || AnnotatedElementUtils + .findMergedAnnotation(cause.getClass(), ResponseStatus.class) != null) { + return Mono.error(cause); + } + } + default: break; } } return Mono.error(throwable); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java index d2301572..0e5644a9 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/headers/HttpHeadersFilter.java @@ -62,10 +62,6 @@ public interface HttpHeadersFilter { } default boolean supports(Type type) { - if (type.equals(Type.REQUEST)) { - return true; - } - - return false; + return type.equals(Type.REQUEST); } } 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 5003ed37..d8d64ed9 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 @@ -18,7 +18,6 @@ package org.springframework.cloud.gateway.handler.predicate; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.function.Predicate; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java index f202b9d4..f0798034 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/PredicateDefinition.java @@ -42,7 +42,7 @@ public class PredicateDefinition { } public PredicateDefinition(String text) { - int eqIdx = text.indexOf("="); + int eqIdx = text.indexOf('='); if (eqIdx <= 0) { throw new ValidationException("Unable to parse PredicateDefinition text '" + text + "'" + ", must be of the form name=value"); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java index 32ea6f9d..05133916 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/ReadBodyPredicateFactory.java @@ -17,22 +17,24 @@ package org.springframework.cloud.gateway.handler.predicate; +import java.util.List; import java.util.Map; import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.cloud.gateway.support.BodyInserterContext; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; import org.springframework.cloud.gateway.handler.AsyncPredicate; -import org.springframework.cloud.gateway.support.DefaultServerRequest; -import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.web.reactive.function.BodyInserter; -import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; +import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.server.ServerWebExchange; @@ -47,11 +49,10 @@ public class ReadBodyPredicateFactory private static final String TEST_ATTRIBUTE = "read_body_predicate_test_attribute"; private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject"; - private final ServerCodecConfigurer codecConfigurer; + private static final List> messageReaders = HandlerStrategies.withDefaults().messageReaders(); - public ReadBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) { + public ReadBodyPredicateFactory() { super(Config.class); - this.codecConfigurer = codecConfigurer; } @Override @@ -66,43 +67,47 @@ public class ReadBodyPredicateFactory // exception will be thrown. The below if/else caches the body object as a request attribute in the ServerWebExchange // so if this filter is run more than once (due to more than one route using it) we do not try to read the // request body multiple times - if(cachedBody != null) { + if (cachedBody != null) { try { boolean test = config.predicate.test(cachedBody); exchange.getAttributes().put(TEST_ATTRIBUTE, test); - } catch(ClassCastException e) { - if(LOGGER.isDebugEnabled()) { + return Mono.just(test); + } catch (ClassCastException e) { + if (LOGGER.isDebugEnabled()) { LOGGER.debug("Predicate test failed because class in predicate does not match the cached body object", e); } } - modifiedBody = Mono.just(cachedBody); + return Mono.just(false); } else { - ServerRequest serverRequest = new DefaultServerRequest(exchange); - // TODO: flux or mono - modifiedBody = serverRequest.bodyToMono(inClass) - // .log("modify_request_mono", Level.INFO) - .flatMap(body -> { - // TODO: migrate to async - exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, body); - boolean test = config.predicate.test(body); - exchange.getAttributes().put(TEST_ATTRIBUTE, test); - return Mono.just(body); + //Join all the DataBuffers so we have a single DataBuffer for the body + return DataBufferUtils.join(exchange.getRequest().getBody()) + .flatMap(dataBuffer -> { + //Update the retain counts so we can read the body twice, once to parse into an object + //that we can test the predicate against and a second time when the HTTP client sends + //the request downstream + //Note: if we end up reading the body twice we will run into a problem, but as of right + //now there is no good use case for doing this + DataBufferUtils.retain(dataBuffer); + //Make a slice for each read so each read has its own read/write indexes + Flux cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); + + ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { + @Override + public Flux getBody() { + return cachedFlux; + } + }; + return ServerRequest.create(exchange.mutate().request(mutatedRequest).build(), messageReaders) + .bodyToMono(inClass) + .doOnNext(objectValue -> { + exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, objectValue); + exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, cachedFlux); + }) + .map(objectValue -> config.predicate.test(objectValue)); }); + } - BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, inClass); - CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, - exchange.getRequest().getHeaders()); - return bodyInserter.insert(outputMessage, new BodyInserterContext()) - // .log("modify_request", Level.INFO) - .then(Mono.defer(() -> { - boolean test = (Boolean) exchange.getAttributes() - .getOrDefault(TEST_ATTRIBUTE, Boolean.FALSE); - exchange.getAttributes().remove(TEST_ATTRIBUTE); - exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, - outputMessage.getBody()); - return Mono.just(test); - })); }; } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java index 990cda15..90bdb296 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java @@ -39,7 +39,7 @@ public class CachingRouteDefinitionLocator implements RouteDefinitionLocator, Ap public CachingRouteDefinitionLocator(RouteDefinitionLocator delegate) { this.delegate = delegate; routeDefinitions = CacheFlux.lookup(cache, "routeDefs", RouteDefinition.class) - .onCacheMissResume(() -> this.delegate.getRouteDefinitions()); + .onCacheMissResume(this.delegate::getRouteDefinitions); } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java index c5ef5f47..f34c6b1a 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteDefinition.java @@ -56,7 +56,7 @@ public class RouteDefinition { public RouteDefinition() {} public RouteDefinition(String text) { - int eqIdx = text.indexOf("="); + int eqIdx = text.indexOf('='); if (eqIdx <= 0) { throw new ValidationException("Unable to parse RouteDefinition text '" + text + "'" + ", must be of the form name=value"); 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 aeacf39b..19a31a3e 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 @@ -567,7 +567,4 @@ public class GatewayFilterSpec extends UriSpec { return filter(getBean(RequestSizeGatewayFilterFactory.class).apply(c -> c.setMaxSize(size))); } - private String routeId() { - return routeBuilder.getId(); - } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java index dbc03da1..13d390c2 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/DefaultServerResponse.java @@ -17,18 +17,15 @@ package org.springframework.cloud.gateway.support; -import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Set; import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.result.view.ViewResolver; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseCookie; import org.springframework.http.codec.HttpMessageWriter; @@ -43,8 +40,6 @@ import org.springframework.web.server.ServerWebExchange; public class DefaultServerResponse implements ServerResponse { - private static final Set SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD); - private final ServerWebExchange exchange; private final BodyInserter inserter; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java index c2f0679a..13b3ead8 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java @@ -25,7 +25,6 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; 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 index 8d429bc2..2a8e518c 100644 --- 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 @@ -17,6 +17,11 @@ package org.springframework.cloud.gateway.support; +import static org.springframework.http.HttpStatus.GATEWAY_TIMEOUT; + +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(value = GATEWAY_TIMEOUT, reason = "Response took longer than configured timeout") public class TimeoutException extends Exception { public TimeoutException() { diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java index c146245e..dc8431c3 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/support/ipresolver/XForwardedRemoteAddressResolver.java @@ -91,7 +91,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver { public InetSocketAddress resolve(ServerWebExchange exchange) { List xForwardedValues = extractXForwardedValues(exchange); Collections.reverse(xForwardedValues); - if (xForwardedValues.size() != 0) { + if (!xForwardedValues.isEmpty()) { int index = Math.min(xForwardedValues.size(), maxTrustedIndex) - 1; return new InetSocketAddress(xForwardedValues.get(index), 0); } @@ -101,7 +101,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver { private List extractXForwardedValues(ServerWebExchange exchange) { List xForwardedValues = exchange.getRequest().getHeaders() .get(X_FORWARDED_FOR); - if (xForwardedValues == null || xForwardedValues.size() == 0) { + if (xForwardedValues == null || xForwardedValues.isEmpty()) { return Collections.emptyList(); } if (xForwardedValues.size() > 1) { 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 index 86c583b2..5e7220b5 100644 --- 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 @@ -46,7 +46,8 @@ public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests { .uri("/delay/5") .exchange() .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT) - .expectBody().jsonPath("$.status").isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())); + .expectBody() + .jsonPath("$.status").isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())); } @EnableAutoConfiguration diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java index d857f6ae..97ec6c5a 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/RouteToRequestUrlFilterTests.java @@ -48,9 +48,10 @@ public class RouteToRequestUrlFilterTests { .get("http://localhost/get?a=b") .build(); - ServerWebExchange webExchange = testFilter(request, "http://myhost"); + ServerWebExchange webExchange = testFilter(request, "http://myhost/mypath"); URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); assertThat(uri).hasScheme("http").hasHost("myhost") + .hasPath("/get") .hasParameter("a", "b"); } @@ -60,6 +61,17 @@ public class RouteToRequestUrlFilterTests { .get("http://localhost/getb") .build(); + ServerWebExchange webExchange = testFilter(request, "lb://myhost"); + URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + assertThat(uri).hasScheme("lb").hasHost("myhost"); + } + + @Test + public void happyPathLbPlusScheme() { + MockServerHttpRequest request = MockServerHttpRequest + .get("http://localhost/getb") + .build(); + ServerWebExchange webExchange = testFilter(request, "lb:http://myhost"); URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); assertThat(uri).hasScheme("http").hasHost("myhost"); @@ -114,7 +126,7 @@ public class RouteToRequestUrlFilterTests { .method(HttpMethod.GET, url) .build(); - ServerWebExchange webExchange = testFilter(request, "http://myhost"); + ServerWebExchange webExchange = testFilter(request, "http://myhost/abc%20def/get"); URI uri = webExchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); assertThat(uri).hasScheme("http").hasHost("myhost") .hasPath("/abc def/get"); @@ -170,9 +182,9 @@ public class RouteToRequestUrlFilterTests { } } - private ServerWebExchange testFilter(MockServerHttpRequest request, String url) { + private ServerWebExchange testFilter(MockServerHttpRequest request, String routeUri) { Route value = Route.async().id("1") - .uri(URI.create(url)) + .uri(URI.create(routeUri)) .order(0) .predicate(swe -> true) .build(); diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java index 54773770..a47faecf 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/factory/HystrixGatewayFilterFactoryTests.java @@ -74,6 +74,20 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests { .isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())); } + /* + * Tests that timeouts bubbling from the underpinning WebClient are treated the same as + * Hystrix timeouts in terms of outside response. (Internally, timeouts from the WebClient + * are seen as command failures and trigger the opening of circuit breakers the same way + * timeouts do; it may be confusing in terms of the Hystrix metrics though) + */ + @Test + public void hystrixTimeoutFromWebClient() { + testClient.get().uri("/delay/10") + .header("Host", "www.hystrixresponsestall.org") + .exchange() + .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT); + } + @Test public void hystrixFilterFallback() { testClient.get().uri("/delay/3?a=b") @@ -157,11 +171,18 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests { .filters(f -> f.prefixPath("/httpbin") .hystrix(config -> {})) .uri("lb:badservice")) + /* + * This is a route encapsulated in a hystrix command that is ready to wait + * for a response far longer than the underpinning WebClient would. + */ + .route("hystrix_response_stall", r -> r.host("**.hystrixresponsestall.org") + .filters(f -> f.prefixPath("/httpbin") + .hystrix(config -> config.setName("stalling-command"))) + .uri(uri)) .build(); } } - protected static class TestBadRibbonConfig { @LocalServerPort diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java new file mode 100644 index 00000000..b6c6ebef --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2013-2017 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.test.ssl; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.JsonPathAssertions; +import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; + +@RunWith(SpringRunner.class) +// this test works because it assumes TLS hand shake cannot be done in 1ms. It takes closer to 80ms +@SpringBootTest(webEnvironment = RANDOM_PORT, properties = {"spring.cloud.gateway.httpclient.ssl.handshake-timeout-millis=1"}) +@DirtiesContext +@ActiveProfiles("ssl") +public class SSLHandshakeTimeoutTests extends SingleCertSSLTests { + + @Test + @Override // here we validate that it the handshake times out + public void testSslTrust() { + ResponseSpec responseSpec = testClient.get().uri("/ssltrust").exchange(); + responseSpec.expectStatus().is5xxServerError(); + JsonPathAssertions jsonPath = responseSpec.expectBody().jsonPath("message"); + jsonPath.isEqualTo("handshake timed out"); + } + + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java index f7c20595..2938b4c5 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java @@ -32,16 +32,15 @@ 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.http.ResponseEntity; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.WebClient; import static org.junit.Assert.assertTrue; @@ -66,19 +65,16 @@ public class SingleCertSSLTests extends BaseWebClientTests { baseUri = "https://localhost:" + port; this.webClient = WebClient.builder().clientConnector(httpConnector) .baseUrl(baseUri).build(); + this.testClient = WebTestClient.bindToServer(httpConnector).baseUrl(baseUri).build(); } catch (SSLException e) { throw new RuntimeException(e); } } - + @Test public void testSslTrust() { - ClientResponse clientResponse = webClient.get().uri("/ssltrust") - .exchange().block(); - HttpStatus statusCode = clientResponse.statusCode(); - assertTrue(statusCode.is2xxSuccessful()); - + testClient.get().uri("/ssltrust").exchange().expectStatus().is2xxSuccessful(); } @EnableAutoConfiguration