Merge remote-tracking branch 'upstream/2.0.x' into rewrite_response_header_filter

This commit is contained in:
Vitaliy Pavlyuk
2018-10-23 15:46:59 -04:00
34 changed files with 288 additions and 126 deletions

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-gateway-docs</artifactId>
<packaging>pom</packaging>

View File

@@ -791,7 +791,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.
== Global Filters
@@ -968,7 +968,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

View File

@@ -5,7 +5,7 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Cloud Gateway</name>
@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>2.0.4.BUILD-SNAPSHOT</version>
<version>2.0.4.RELEASE</version>
<relativePath/>
</parent>
<scm>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-core</artifactId>

View File

@@ -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()));
}

View File

@@ -167,6 +167,9 @@ public class GatewayAutoConfiguration {
// configure ssl
HttpClientProperties.Ssl ssl = properties.getSsl();
opts.sslHandshakeTimeoutMillis(ssl.getHandshakeTimeoutMillis());
opts.sslCloseNotifyFlushTimeoutMillis(ssl.getCloseNotifyFlushTimeoutMillis());
opts.sslCloseNotifyReadTimeoutMillis(ssl.getCloseNotifyReadTimeoutMillis());
X509Certificate[] trustedX509Certificates = ssl
.getTrustedX509CertificatesForTrustManager();
if (trustedX509Certificates.length > 0) {
@@ -367,7 +370,7 @@ public class GatewayAutoConfiguration {
@Bean
@ConditionalOnBean(DispatcherHandler.class)
public ForwardRoutingFilter forwardRoutingFilter(DispatcherHandler dispatcherHandler) {
public ForwardRoutingFilter forwardRoutingFilter(ObjectProvider<DispatcherHandler> dispatcherHandler) {
return new ForwardRoutingFilter(dispatcherHandler);
}
@@ -453,8 +456,8 @@ public class GatewayAutoConfiguration {
}
@Bean
public ReadBodyPredicateFactory readBodyPredicateFactory(ServerCodecConfigurer codecConfigurer) {
return new ReadBodyPredicateFactory(codecConfigurer);
public ReadBodyPredicateFactory readBodyPredicateFactory() {
return new ReadBodyPredicateFactory();
}
@Bean
@@ -494,7 +497,7 @@ public class GatewayAutoConfiguration {
@ConditionalOnClass({HystrixObservableCommand.class, RxReactiveStreams.class})
protected static class HystrixConfiguration {
@Bean
public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) {
public HystrixGatewayFilterFactory hystrixGatewayFilterFactory(ObjectProvider<DispatcherHandler> dispatcherHandler) {
return new HystrixGatewayFilterFactory(dispatcherHandler);
}
}

View File

@@ -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<String> trustedX509Certificates = new ArrayList<>();
// use netty default SSL timeouts
private long handshakeTimeoutMillis = 10000L;
private long closeNotifyFlushTimeoutMillis = 3000L;
private long closeNotifyReadTimeoutMillis = 0L;
public List<String> 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

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -4,6 +4,9 @@ import java.net.URI;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.Ordered;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ServerWebExchange;
@@ -12,15 +15,13 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setAlreadyRouted;
import reactor.core.publisher.Mono;
public class ForwardRoutingFilter implements GlobalFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardRoutingFilter.class);
private final DispatcherHandler dispatcherHandler;
private final ObjectProvider<DispatcherHandler> dispatcherHandler;
public ForwardRoutingFilter(DispatcherHandler dispatcherHandler) {
public ForwardRoutingFilter(ObjectProvider<DispatcherHandler> dispatcherHandler) {
this.dispatcherHandler = dispatcherHandler;
}
@@ -45,6 +46,6 @@ public class ForwardRoutingFilter implements GlobalFilter, Ordered {
log.trace("Forwarding to URI: "+requestUrl);
}
return this.dispatcherHandler.handle(exchange);
return this.dispatcherHandler.getIfAvailable().handle(exchange);
}
}

View File

@@ -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);
}

View File

@@ -23,43 +23,45 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixObservableCommand;
import com.netflix.hystrix.HystrixObservableCommand.Setter;
import com.netflix.hystrix.exception.HystrixRuntimeException;
import static com.netflix.hystrix.exception.HystrixRuntimeException.FailureType.TIMEOUT;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
import reactor.core.publisher.Mono;
import rx.Observable;
import rx.RxReactiveStreams;
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.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;
import org.springframework.web.util.UriComponentsBuilder;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.containsEncodedParts;
/**
* 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<HystrixGatewayFilterFactory.Config> {
public static final String FALLBACK_URI = "fallbackUri";
private final DispatcherHandler dispatcherHandler;
private final ObjectProvider<DispatcherHandler> dispatcherHandler;
public HystrixGatewayFilterFactory(DispatcherHandler dispatcherHandler) {
public HystrixGatewayFilterFactory(ObjectProvider<DispatcherHandler> dispatcherHandler) {
super(Config.class);
this.dispatcherHandler = dispatcherHandler;
}
@@ -101,8 +103,24 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<Hy
}).onErrorResume((Function<Throwable, Mono<Void>>) 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);
@@ -149,7 +167,8 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory<Hy
ServerHttpRequest request = this.exchange.getRequest().mutate().uri(requestUrl).build();
ServerWebExchange mutated = exchange.mutate().request(request).build();
return RxReactiveStreams.toObservable(HystrixGatewayFilterFactory.this.dispatcherHandler.handle(mutated));
DispatcherHandler dispatcherHandler = HystrixGatewayFilterFactory.this.dispatcherHandler.getIfAvailable();
return RxReactiveStreams.toObservable(dispatcherHandler.handle(mutated));
}
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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");

View File

@@ -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<HttpMessageReader<?>> 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<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> 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);
}));
};
}

View File

@@ -39,7 +39,7 @@ public class CachingRouteDefinitionLocator implements RouteDefinitionLocator {
public CachingRouteDefinitionLocator(RouteDefinitionLocator delegate) {
this.delegate = delegate;
routeDefinitions = CacheFlux.lookup(cache, "routeDefs", RouteDefinition.class)
.onCacheMissResume(() -> this.delegate.getRouteDefinitions());
.onCacheMissResume(this.delegate::getRouteDefinitions);
}

View File

@@ -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");

View File

@@ -569,7 +569,4 @@ public class GatewayFilterSpec extends UriSpec {
}));
}
private String routeId() {
return routeBuilder.getId();
}
}

View File

@@ -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<T> implements ServerResponse {
private static final Set<HttpMethod> SAFE_METHODS = EnumSet.of(HttpMethod.GET, HttpMethod.HEAD);
private final ServerWebExchange exchange;
private final BodyInserter<T, ? super ServerHttpResponse> inserter;

View File

@@ -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;

View File

@@ -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() {

View File

@@ -91,7 +91,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver {
public InetSocketAddress resolve(ServerWebExchange exchange) {
List<String> xForwardedValues = extractXForwardedValues(exchange);
Collections.reverse(xForwardedValues);
if (xForwardedValues.size() != 0) {
if (!xForwardedValues.isEmpty()) {
int index = Math.min(xForwardedValues.size(), maxTrustedIndex) - 1;
return InetSocketAddress.createUnresolved(xForwardedValues.get(index), 0);
}
@@ -101,7 +101,7 @@ public class XForwardedRemoteAddressResolver implements RemoteAddressResolver {
private List<String> extractXForwardedValues(ServerWebExchange exchange) {
List<String> 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) {

View File

@@ -1,5 +1,7 @@
package org.springframework.cloud.gateway.filter;
import java.net.URI;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -7,6 +9,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.Ordered;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
@@ -14,10 +18,11 @@ 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.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ALREADY_ROUTED_ATTR;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
@@ -33,6 +38,9 @@ public class ForwardRoutingFilterTests {
@Mock
private GatewayFilterChain chain;
@Mock
private ObjectProvider<DispatcherHandler> objectProvider;
@Mock
private DispatcherHandler dispatcherHandler;
@@ -42,6 +50,7 @@ public class ForwardRoutingFilterTests {
@Before
public void setup() {
exchange = MockServerWebExchange.from(MockServerHttpRequest.get("localendpoint").build());
when(objectProvider.getIfAvailable()).thenReturn(this.dispatcherHandler);
}
@Test

View File

@@ -27,6 +27,7 @@ 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.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
@@ -44,11 +45,11 @@ public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests {
testClient.get()
.uri("/delay/5")
.exchange()
.expectStatus().is5xxServerError()
.expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT)
.expectBody(Map.class)
.consumeWith(result -> {
Map body = result.getResponseBody();
assertThat(body).containsEntry("message", "Response took longer than timeout: PT3S");
assertThat(body).containsEntry("message", "Response took longer than configured timeout");
});
}

View File

@@ -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();

View File

@@ -50,7 +50,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
import static org.springframework.http.MediaType.TEXT_HTML;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@SpringBootTest(webEnvironment = RANDOM_PORT, properties = "debug=true")
@DirtiesContext
public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests {
@@ -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

View File

@@ -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");
}
}

View File

@@ -17,7 +17,6 @@
package org.springframework.cloud.gateway.test.ssl;
import static org.junit.Assert.assertTrue;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import javax.net.ssl.SSLException;
@@ -30,16 +29,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 io.netty.handler.ssl.SslContext;
@@ -61,20 +59,17 @@ public class SingleCertSSLTests extends BaseWebClientTests {
opt -> opt.sslContext(sslContext));
baseUri = "https://localhost:" + port;
this.webClient = WebClient.builder().clientConnector(httpConnector)
.baseUrl(baseUri).build();
.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

View File

@@ -5,12 +5,12 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.4.BUILD-SNAPSHOT</version>
<version>2.0.4.RELEASE</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-gateway-dependencies</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-gateway-dependencies</name>

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -10,7 +10,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>

View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>2.0.2.BUILD-SNAPSHOT</version>
<version>2.0.3.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-starter-gateway</artifactId>