diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index 052af641..e94f6007 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -953,6 +953,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). + +=== 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..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 @@ -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; /** @@ -220,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; @@ -231,20 +238,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", @@ -266,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/NettyRoutingFilter.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java index 49ff8b26..ec75e525 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 @@ -43,6 +43,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.AbstractServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter.filterRequest; @@ -131,8 +132,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/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 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 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 1695aba9..7d18fc64 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 @@ -40,11 +40,14 @@ import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; +import static org.springframework.http.MediaType.TEXT_HTML; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) @@ -65,7 +68,10 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests { testClient.get().uri("/delay/3") .header("Host", "www.hystrixfailure.org") .exchange() - .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT); + .expectStatus().isEqualTo(HttpStatus.GATEWAY_TIMEOUT) + .expectBody() + .jsonPath("$.status") + .isEqualTo(String.valueOf(HttpStatus.GATEWAY_TIMEOUT.value())); } @Test @@ -103,6 +109,23 @@ public class HystrixGatewayFilterFactoryTests extends BaseWebClientTests { .expectStatus().is5xxServerError(); } + @Test + public void hystrixFilterErrorPage() { + testClient.get().uri("/delay/3") + .header("Host", "www.hystrixconnectfail.org") + .accept(TEXT_HTML) + .exchange() + .expectStatus().is5xxServerError() + .expectBody().consumeWith(res -> { + 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) 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(); + } + + } + +} 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/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/SSLTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/test/ssl/SingleCertSSLTests.java similarity index 85% 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..824b97c3 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 @@ -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; @@ -49,8 +47,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() { @@ -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 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