From 5aa126a83ab55e403aeb2aac021b8c8dd1c55db1 Mon Sep 17 00:00:00 2001 From: Triphon Penakov Date: Thu, 30 Aug 2018 09:30:47 +0300 Subject: [PATCH 1/6] Handle case to work with non standard content like "Content-Type: image" --- .../gateway/filter/NettyRoutingFilter.java | 6 +- .../filter/NettyWriteResponseFilter.java | 7 +- ...odifyResponseBodyGatewayFilterFactory.java | 6 +- .../NonStandardHeadersInResponseTests.java | 120 ++++++++++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/NonStandardHeadersInResponseTests.java 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 5f3d52d1..e95d49e7 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 @@ -22,6 +22,7 @@ import java.util.List; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpMethod; +import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; import reactor.ipc.netty.NettyPipeline; import reactor.ipc.netty.http.client.HttpClient; @@ -129,8 +130,9 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered { res.responseHeaders().forEach(entry -> headers.add(entry.getKey(), entry.getValue())); - if (headers.getContentType() != null) { - exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR, headers.getContentType()); + String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE); + if (StringUtils.hasLength(contentTypeValue)) { + exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR, contentTypeValue); } HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter( diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyWriteResponseFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyWriteResponseFilter.java index 237c4af5..f033bf1c 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyWriteResponseFilter.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyWriteResponseFilter.java @@ -75,7 +75,12 @@ public class NettyWriteResponseFilter implements GlobalFilter, Ordered { .retain() //TODO: needed? .map(factory::wrap); - MediaType contentType = response.getHeaders().getContentType(); + MediaType contentType = null; + try { + contentType = response.getHeaders().getContentType(); + } catch (Exception e) { + log.trace("invalid media type", e); + } return (isStreamingMediaType(contentType) ? response.writeAndFlushWith(body.map(Flux::just)) : response.writeWith(body)); })); 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 6089d16f..0d7d62a0 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 @@ -84,9 +84,11 @@ public class ModifyResponseBodyGatewayFilterFactory Class inClass = config.getInClass(); Class outClass = config.getOutClass(); - MediaType originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); + String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(originalResponseContentType); + //explicitly add it in this way instead of 'httpHeaders.setContentType(originalResponseContentType)' + //this will prevent exception in case of using non-standard media types like "Content-Type: image" + httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType); ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders); DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults()); diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/NonStandardHeadersInResponseTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/NonStandardHeadersInResponseTests.java new file mode 100644 index 00000000..10205ec4 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/filter/headers/NonStandardHeadersInResponseTests.java @@ -0,0 +1,120 @@ +/* + * 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.filter.headers; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +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.cloud.gateway.filter.GlobalFilter; +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.core.annotation.Order; +import org.springframework.http.HttpHeaders; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; + +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = DEFINED_PORT, + properties = {"server.port=62175"} +) +@DirtiesContext +public class NonStandardHeadersInResponseTests extends BaseWebClientTests { + + public static final String CONTENT_TYPE_IMAGE = "image"; + + @Test + public void nonStandardHeadersInResponse() { + URI uri = UriComponentsBuilder + .fromUriString(this.baseUri + "/get-image") + .build(true) + .toUri(); + + String contentType = WebClient.builder() + .baseUrl(baseUri) + .build() + .get() + .uri(uri) + .exchange() + .map(clientResponse -> clientResponse.headers().asHttpHeaders().getFirst(HttpHeaders.CONTENT_TYPE)) + .block(); + + assertEquals(CONTENT_TYPE_IMAGE, contentType); + } + + @EnableAutoConfiguration + @SpringBootConfiguration + @Import(DefaultTestConfig.class) + public static class TestConfig { + private static final Log log = LogFactory.getLog(TestConfig.class); + @Value("${test.uri}") + String uri; + @Value("${server.port}") + int port; + + @Bean + @Order(5001) + public GlobalFilter addNonStandardHeaderFilter() { + return (exchange, chain) -> { + log.info("addNonStandardHeaderFilter pre phase"); + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + log.info("addNonStandardHeaderFilter post phase"); + List contentTypes = exchange.getResponse().getHeaders().get(HttpHeaders.CONTENT_TYPE); + contentTypes.clear(); + contentTypes.add(CONTENT_TYPE_IMAGE); + })); + }; + } + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("non_standard_header_route", r -> + r.path("/get-image/**") + .filters(f -> f + .addRequestHeader(HttpHeaders.HOST, "www.addrequestparameter.org") + .stripPrefix(1) + ) + .uri("http://localhost:" + port + "/get")) + .route("internal_route", r -> + r.path("/get/**") + .filters(f -> f.prefixPath("/httpbin")) + .uri(uri)) + .build(); + } + + } + +} From 2444776250fb49d6ce009618dbb8d97302ad967c Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Sat, 29 Sep 2018 14:54:48 -0400 Subject: [PATCH 2/6] Add support for configuring TLS handshake timeouts --- .../main/asciidoc/spring-cloud-gateway.adoc | 16 +++++++ .../config/GatewayAutoConfiguration.java | 3 ++ .../gateway/config/HttpClientProperties.java | 40 +++++++++++++++- .../test/ssl/SSLHandshakeTimeoutTests.java | 48 +++++++++++++++++++ .../cloud/gateway/test/ssl/SSLTests.java | 15 ++---- 5 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 95a8fda0..dacfe0a7 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -950,6 +950,22 @@ spring: - cert2.pem ---- +=== 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/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 145970c7..fd3837cb 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 @@ -166,6 +166,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) { 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 ca9159ea..5ba01001 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 @@ -220,8 +220,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; @@ -266,11 +271,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/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..852efb5d --- /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 SSLTests { + + @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/SSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java index c936eb75..42589d49 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java @@ -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 SSLTests 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 From f1db5e063c8c533c8604ff6ced4d3afa6cfb6d12 Mon Sep 17 00:00:00 2001 From: Tony Clarke <36965832+tony-clarke-amdocs@users.noreply.github.com> Date: Fri, 5 Oct 2018 18:30:21 -0400 Subject: [PATCH 3/6] Certificate array in pem (#583) Fix a bug where previous trust manager effectively only read the first certificate in pem file. --- .../main/asciidoc/spring-cloud-gateway.adoc | 1 + .../gateway/config/HttpClientProperties.java | 12 ++-- .../gateway/test/ssl/MultiCertSSLTests.java | 35 +++++++++++ ...{SSLTests.java => SingleCertSSLTests.java} | 4 +- ...ssl.yml => application-multi-cert-ssl.yml} | 2 +- .../resources/application-single-cert-ssl.yml | 38 ++++++++++++ .../src/test/resources/multi-cert.pem | 62 +++++++++++++++++++ .../{scg-cert.pem => single-cert.pem} | 0 8 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/MultiCertSSLTests.java rename spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/{SSLTests.java => SingleCertSSLTests.java} (97%) rename spring-cloud-gateway-core/src/test/resources/{application-ssl.yml => application-multi-cert-ssl.yml} (94%) create mode 100644 spring-cloud-gateway-core/src/test/resources/application-single-cert-ssl.yml create mode 100644 spring-cloud-gateway-core/src/test/resources/multi-cert.pem rename spring-cloud-gateway-core/src/test/resources/{scg-cert.pem => single-cert.pem} (100%) diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 95a8fda0..31c919d0 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -950,6 +950,7 @@ 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. == Configuration 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 ca9159ea..62ecd2e3 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 @@ -25,11 +25,13 @@ import reactor.ipc.netty.resources.PoolResources; import java.io.IOException; import java.net.URL; +import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.List; /** @@ -231,20 +233,20 @@ public class HttpClientProperties { try { CertificateFactory certificateFactory = CertificateFactory .getInstance("X.509"); - ArrayList certs = new ArrayList<>(); + ArrayList allCerts = new ArrayList<>(); for (String trustedCert : ssl.getTrustedX509Certificates()) { try { URL url = ResourceUtils.getURL(trustedCert); - X509Certificate cert = (X509Certificate) certificateFactory - .generateCertificate(url.openStream()); - certs.add(cert); + Collection certs = certificateFactory + .generateCertificates(url.openStream()); + allCerts.addAll(certs); } catch (IOException e) { throw new WebServerException( "Could not load certificate '" + trustedCert + "'", e); } } - return certs.toArray(new X509Certificate[certs.size()]); + return allCerts.toArray(new X509Certificate[allCerts.size()]); } catch (CertificateException e1) { throw new WebServerException("Could not load CertificateFactory X.509", diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/MultiCertSSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/MultiCertSSLTests.java new file mode 100644 index 00000000..663e1dc1 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/MultiCertSSLTests.java @@ -0,0 +1,35 @@ +/* + * 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.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; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = RANDOM_PORT) +@DirtiesContext +@ActiveProfiles("multi-cert-ssl") +public class MultiCertSSLTests extends SingleCertSSLTests { + + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java similarity index 97% rename from spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java rename to spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java index c936eb75..c351a0b5 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java @@ -49,8 +49,8 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) @DirtiesContext -@ActiveProfiles("ssl") -public class SSLTests extends BaseWebClientTests { +@ActiveProfiles("single-cert-ssl") +public class SingleCertSSLTests extends BaseWebClientTests { @Before public void setup() { diff --git a/spring-cloud-gateway-core/src/test/resources/application-ssl.yml b/spring-cloud-gateway-core/src/test/resources/application-multi-cert-ssl.yml similarity index 94% rename from spring-cloud-gateway-core/src/test/resources/application-ssl.yml rename to spring-cloud-gateway-core/src/test/resources/application-multi-cert-ssl.yml index 248e6b08..52e21ea8 100644 --- a/spring-cloud-gateway-core/src/test/resources/application-ssl.yml +++ b/spring-cloud-gateway-core/src/test/resources/application-multi-cert-ssl.yml @@ -16,7 +16,7 @@ spring: httpclient: ssl: trustedX509Certificates: - - src/test/resources/scg-cert.pem + - src/test/resources/multi-cert.pem default-filters: - PrefixPath=/httpbin routes: diff --git a/spring-cloud-gateway-core/src/test/resources/application-single-cert-ssl.yml b/spring-cloud-gateway-core/src/test/resources/application-single-cert-ssl.yml new file mode 100644 index 00000000..a1da9aa1 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/resources/application-single-cert-ssl.yml @@ -0,0 +1,38 @@ +test: + hostport: httpbin.org:80 + uri: lb://testservice + +server: + ssl: + enabled: true + key-alias: scg + key-store-password: scg1234 + key-store: classpath:scg-keystore.p12 + key-store-type: PKCS12 + +spring: + cloud: + gateway: + httpclient: + ssl: + trustedX509Certificates: + - src/test/resources/single-cert.pem + default-filters: + - PrefixPath=/httpbin + routes: + - id: default_path_to_httpbin + uri: ${test.uri} + order: 10000 + predicates: + - name: Path + args: + pattern: /** + +logging: + level: + org.springframework.cloud.gateway: TRACE + org.springframework.http.server.reactive: DEBUG + org.springframework.web.reactive: DEBUG + reactor.ipc.netty: DEBUG + redisratelimiter: DEBUG + diff --git a/spring-cloud-gateway-core/src/test/resources/multi-cert.pem b/spring-cloud-gateway-core/src/test/resources/multi-cert.pem new file mode 100644 index 00000000..d90ce1d4 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/resources/multi-cert.pem @@ -0,0 +1,62 @@ +-----BEGIN CERTIFICATE----- +MIIGKzCCBROgAwIBAgIQByMt0ja6e5B8OohEG0rsHzANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E +aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTgwMzE2MDAwMDAwWhcN +MTkwMzIxMTIwMDAwWjBtMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJUGFsbyBBbHRvMR8wHQYDVQQKExZQaXZvdGFsIFNvZnR3YXJl +LCBJbmMuMRQwEgYDVQQDDAsqLnNwcmluZy5pbzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANjtAeOmtB/ZPoX5r2MDWPmLVaLHFBLUViLsUMInkfpakesg +bl6vkWaVZpvarbOxzHuiYjsXEjlgpZTYVguv3UZHoDoD5k/E9k0N6UBdx5edXnsc +Ssty/fuH60MkDAsw89AILN4MAlkG2DIRJfGZGqCfA2k9Z35tz9l0OEavfqxNlUyT +X3AkCDyiEl5bfo2XevNw46O/21TKHwE1uDWszH2AOszv5TxdyICzCZUmjesb0awe +9PB6Mlv83U+MZgWS9SoHlQjfpz+tUVGm3PnHQ6Oqo4nJVb+wbgQ6aXyOpo/W5GQE +5svJcsF3AoDalPIsGU66EMYrooscUnoiDitViOkCAwEAAaOCAuUwggLhMB8GA1Ud +IwQYMBaAFA+AYRyCMWHVLyjnjUY4tCzhxtniMB0GA1UdDgQWBBSDcxmAlUjp2j+3 +9OZAjqL6dY8AqTAhBgNVHREEGjAYggsqLnNwcmluZy5pb4IJc3ByaW5nLmlvMA4G +A1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwawYD +VR0fBGQwYjAvoC2gK4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NzY2Etc2hh +Mi1nNi5jcmwwL6AtoCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zc2NhLXNo +YTItZzYuY3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEW +HGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQICMHwGCCsGAQUF +BwEBBHAwbjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEYG +CCsGAQUFBzAChjpodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRT +SEEyU2VjdXJlU2VydmVyQ0EuY3J0MAwGA1UdEwEB/wQCMAAwggEEBgorBgEEAdZ5 +AgQCBIH1BIHyAPAAdQCkuQmQtBhYFIe7E6LMZ3AKPDWYBPkb37jjd80OyA3cEAAA +AWIwOqM0AAAEAwBGMEQCIBr9GyZ5+YtlqoKhV/exaukHme/YbpXl/O1+6sG/kOro +AiBEI3IfRziaa9QMlm0fp2wGSbhpArTQ03w5Yq795QAhtgB3AG9Tdqwx8DEZ2JkA +pFEV/3cVHBHZAsEAKQaNsgiaN9kTAAABYjA6pHYAAAQDAEgwRgIhAMyltKcFRCoE +BSoeBUd8OFU7gKlWKsmxAZkVKpLuYbMKAiEAgb7snjGCWpX1qRdg+TlKnyn3bocK +i2pROA/1lzAOqzowDQYJKoZIhvcNAQELBQADggEBACDyzGb8YxHFQBO20l7LYbW8 +rNat/FhB5H8JCEaup7pvdVCNvyUOOR3fm+by+a56QI1cb0vx8N6+j/rVXccniL88 +26FGO2IQeAnNa0Mc8ANJdLrYzwCSiObIFXh2/Cu86oe7SJNjbxJpX3C2OKxmHVVP +AofQsz59V5O1JBkblRAfK9yBOroeBAMRvNxHuolleuip4IKD6URGXQzDyXqNbWs7 +O3zyXtGAoCUAxUtoOcZNZWvXF3eYFxMnEmsuNjO6UHtfvJGB6pqjWp/00j5cioE3 +RLqeByg4E9a9iM5onXdSSGzAYusdZE53IAezV0w8KNJuOKHYLwpO59tvU901OK0= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIEjTCCAvWgAwIBAgIEIZEBKTANBgkqhkiG9w0BAQwFADBsMRAwDgYDVQQGEwdV +bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD +VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRAwDgYDVQQDEwdVbmtub3du +MB4XDTE4MDcxODEyNTY0OFoXDTQ1MTIwMzEyNTY0OFowbDEQMA4GA1UEBhMHVW5r +bm93bjEQMA4GA1UECBMHVW5rbm93bjEQMA4GA1UEBxMHVW5rbm93bjEQMA4GA1UE +ChMHVW5rbm93bjEQMA4GA1UECxMHVW5rbm93bjEQMA4GA1UEAxMHVW5rbm93bjCC +AaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAJhdOwHjozNaUdocb+p/Dn6E +UUXzkNGwmUZwRSQKnEK9IV/PTwrzK+fPJS8vpUzEpj37QIGBTSNuebQ3w5G4cAu6 +xFEk4e56C823Vio5Hae/+fos7goaR+ihKMy2lGPwFGNECqGmUZItE1+4pdjYyia7 +FTWluAVuTgue3VODl9ziDw+2Zrt6Axtnb5heQXP8ElEZldoePpUmbLWa03Xvfatb +4ZKkoCLOKhcgM5F2tyR+VlZnqm2dhvO+J778MsU4ToVAUGrVIkeQi0BHTi5Rzy2c +2kDLHHuJTYqN65sl5LyKDu003KXioelZVUeH3Hgtrj0Tt96oYSixvHSGwDmcXpux +4sbzTS7x/KeQAjLIFj/3rxQP0TSuTCe/XrqiCpFflLntTI9En9CWto6SXFRigval +zCITgImQthSfFMxFDqNtNQo5hTAu8VY2/FurMbkukQ5l2+vU4dKBccNxChj5m7p3 +8C4G7Rh08AEYaNYcNZlaz++c37rW7P6vWX8m6C21CQIDAQABozcwNTAUBgNVHREE +DTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFDOSE31Vw2QdyVviz+H5+tY5HRR4MA0G +CSqGSIb3DQEBDAUAA4IBgQAJljlIubbQvw/2UVymsyF939XKus/7muiJLtQt0J4J +sSuGiSQmkyJajBRj9+qsLLTtdL6F2+BFJtP8S/zZixrEzktuRZ3b40MLtyI4hVDt +fmgj3UMmphVgbmDv71WvRmUXFfBX5Zka7zRW+lFvO5dLytegKi3Vc8zOFRUcvY9w +uBEOipAkUqH6y+lI/lJ72MrXpkxbkAq2fYffZK4e6KNpKG7pP0txEkwNvosKSAjA +yL+Ye5bK6sYbscwsqvXw9nWwjuFpj/0zvD/tM1jMtUtV1+9HKC3vsfAaL9QnwnS6 +qhSaos8j7fw9SYdAoO+yth2w1ETxKXrzcF+LEkhzj2R0zygycLZvWj51lIWDDOvy +m3IUFTY8fX7CBgLIGnHAcdWK6a+FkWIb8WybWkv2n2SxM4AnzXb5epk/K2Uo30hY +mr9wrywvo92xUjXEOmQpAbFx0hcVjYOtvpHdUeZTGLLQ7sWJGQFalRT+GsTy+Lph +10719GizKXQW2Mx/8XOr1Ow= +-----END CERTIFICATE----- diff --git a/spring-cloud-gateway-core/src/test/resources/scg-cert.pem b/spring-cloud-gateway-core/src/test/resources/single-cert.pem similarity index 100% rename from spring-cloud-gateway-core/src/test/resources/scg-cert.pem rename to spring-cloud-gateway-core/src/test/resources/single-cert.pem From b473aaee1c8641c7207b771852c395d7f1ccad85 Mon Sep 17 00:00:00 2001 From: mmanciop Date: Sat, 6 Oct 2018 00:33:08 +0200 Subject: [PATCH 4/6] Support Spring WebFlux error-handling mechanism for Hystrix-issued timeouts (#557) Fixes gh-553 --- .../factory/HystrixGatewayFilterFactory.java | 8 +++--- .../HystrixGatewayFilterFactoryTests.java | 25 ++++++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) 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 4de837b4..2b93514d 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 @@ -30,6 +30,7 @@ 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; @@ -42,7 +43,6 @@ 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 static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setResponseStatus; import reactor.core.publisher.Mono; import rx.Observable; @@ -101,9 +101,8 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory>) throwable -> { if (throwable instanceof HystrixRuntimeException) { HystrixRuntimeException e = (HystrixRuntimeException) throwable; - if (e.getFailureType() == TIMEOUT) { //TODO: optionally set status - setResponseStatus(exchange, HttpStatus.GATEWAY_TIMEOUT); - return exchange.getResponse().setComplete(); + if (e.getFailureType() == TIMEOUT) { + return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT)); } } return Mono.error(throwable); @@ -191,4 +190,5 @@ public class HystrixGatewayFilterFactory extends AbstractGatewayFilterFactory { + final String body = new String(res.getResponseBody(), UTF_8); + + Assert.isTrue(body.contains("

Whitelabel Error Page

"), + "Cannot find the expected white-label error page title in the response"); + Assert.isTrue(body.contains("(type=Internal Server Error, status=500)"), + "Cannot find the expected error status report in the response"); + }); + } + @EnableAutoConfiguration @SpringBootConfiguration @Import(DefaultTestConfig.class) From bb1ea6a1234f4cba1a4708fa9b5a3574edc99ddc Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Fri, 5 Oct 2018 18:34:25 -0400 Subject: [PATCH 5/6] Only use scheme://host:port from routeUri fixes gh-465 --- .../filter/RouteToRequestUrlFilter.java | 9 ++++++--- .../filter/RouteToRequestUrlFilterTests.java | 20 +++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) 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/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(); From 6a2f9d14b53ff3d2dd525cc296ca1639cf0ceff4 Mon Sep 17 00:00:00 2001 From: Tony Clarke Date: Sat, 29 Sep 2018 14:54:48 -0400 Subject: [PATCH 6/6] Add support for configuring TLS handshake timeouts --- .../main/asciidoc/spring-cloud-gateway.adoc | 19 +++++++- .../config/GatewayAutoConfiguration.java | 3 ++ .../gateway/config/HttpClientProperties.java | 40 +++++++++++++++- .../test/ssl/SSLHandshakeTimeoutTests.java | 48 +++++++++++++++++++ .../gateway/test/ssl/SingleCertSSLTests.java | 15 ++---- 5 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SSLHandshakeTimeoutTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 31c919d0..63229ef8 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -950,7 +950,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/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 145970c7..fd3837cb 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 @@ -166,6 +166,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) { 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 62ecd2e3..d479b3c1 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/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 c351a0b5..824b97c3 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 @@ -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