From 9d178e2217d2f0ca12221dea8a2bdea844e5ac27 Mon Sep 17 00:00:00 2001 From: Spencer Gibb Date: Tue, 21 Sep 2021 18:17:31 -0400 Subject: [PATCH] HTTP2 Support (#2363) Adds HttpProtocol.H2 if server.http2.enabled=true. Deprecates defaultConfigurationType as it is no longer used. Updates to use new HttpClient ProtocolSslContextSpec for configuring ssl. Fixes gh-7 Fixes gh-2206 --- .../http2/pom.xml | 67 ++++++++++++ .../gateway/tests/http2/Http2Application.java | 86 +++++++++++++++ .../http2/src/main/resources/application.yml | 22 ++++ .../http2/src/main/resources/sample.jks | Bin 0 -> 2264 bytes .../tests/http2/Http2ApplicationTests.java | 97 +++++++++++++++++ .../http2/config/NosslConfiguration.java | 33 ++++++ .../tests/http2/config/NosslTests.java | 65 +++++++++++ .../src/test/resources/application-nossl.yml | 13 +++ .../pom.xml | 1 + .../config/GatewayAutoConfiguration.java | 101 ++++++++++-------- .../gateway/config/HttpClientProperties.java | 3 + .../config/GatewayAutoConfigurationTests.java | 13 ++- 12 files changed, 455 insertions(+), 46 deletions(-) create mode 100644 spring-cloud-gateway-integration-tests/http2/pom.xml create mode 100644 spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java create mode 100644 spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml create mode 100644 spring-cloud-gateway-integration-tests/http2/src/main/resources/sample.jks create mode 100644 spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java create mode 100644 spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslConfiguration.java create mode 100644 spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslTests.java create mode 100644 spring-cloud-gateway-integration-tests/http2/src/test/resources/application-nossl.yml diff --git a/spring-cloud-gateway-integration-tests/http2/pom.xml b/spring-cloud-gateway-integration-tests/http2/pom.xml new file mode 100644 index 00000000..5b31be14 --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + + http2 + jar + + Spring Cloud Gateway HTTP2 Integration Test + Spring Cloud Gateway HTTP2 Integration Test + + + + + + org.springframework.cloud + spring-cloud-gateway-integration-tests + 3.1.0-SNAPSHOT + .. + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-gateway + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + io.netty + netty-tcnative-boringssl-static + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + org.assertj + assertj-core + test + + + + + + maven-deploy-plugin + + true + + + + + + diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java b/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java new file mode 100644 index 00000000..cb68162e --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/src/main/java/org/springframework/cloud/gateway/tests/http2/Http2Application.java @@ -0,0 +1,86 @@ +/* + * Copyright 2013-2021 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 + * + * https://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.tests.http2; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; +import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; +import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; +import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Spencer Gibb + */ +// curl -i --insecure https://localhost:8443/hello +@SpringBootConfiguration +@EnableAutoConfiguration +@RestController +@LoadBalancerClients({ + @LoadBalancerClient(name = "myservice", configuration = Http2Application.MyServiceConf.class), + @LoadBalancerClient(name = "nossl", configuration = Http2Application.NosslServiceConf.class) +}) +public class Http2Application { + + @GetMapping("hello") + public String hello() { + return "Hello"; + } + + @Bean + public RouteLocator myRouteLocator(RouteLocatorBuilder builder) { + return builder.routes().route(r -> r.path("/myprefix/**").filters(f -> f.stripPrefix(1)).uri("lb://myservice")) + .route(r -> r.path("/nossl/**").filters(f -> f.stripPrefix(1)).uri("lb://nossl")) + .route(r -> r.path("/neverssl/**").filters(f -> f.stripPrefix(1)).uri("http://neverssl.com")) + .route(r -> r.path("/httpbin/**").uri("https://nghttp2.org")).build(); + } + + public static void main(String[] args) { + SpringApplication.run(Http2Application.class, args); + } + + static class MyServiceConf { + + @Bean + public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) { + Integer port = env.getProperty("local.server.port", Integer.class, 8443); + return ServiceInstanceListSuppliers.from("myservice", + new DefaultServiceInstance("myservice-1", "myservice", "localhost", port, true)); + } + + } + + static class NosslServiceConf { + + @Bean + public ServiceInstanceListSupplier noSslStaticServiceInstanceListSupplier() { + int port = Integer.parseInt(System.getProperty("nossl.port", "8080")); + return ServiceInstanceListSuppliers.from("nossl", + new DefaultServiceInstance("nossl-1", "nossl", "localhost", port, false)); + } + + } + +} \ No newline at end of file diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml b/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml new file mode 100644 index 00000000..8438b1d5 --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/src/main/resources/application.yml @@ -0,0 +1,22 @@ +logging: + level: + org.springframework.cloud.gateway: TRACE + reactor.netty.http.client: DEBUG + +server: + ssl: + key-store: classpath:sample.jks + key-store-password: secret + key-password: password + http2: + enabled: true + +spring: + cloud: + gateway: +# httpserver: +# wiretap: true + httpclient: + wiretap: true + ssl: + use-insecure-trust-manager: true \ No newline at end of file diff --git a/spring-cloud-gateway-integration-tests/http2/src/main/resources/sample.jks b/spring-cloud-gateway-integration-tests/http2/src/main/resources/sample.jks new file mode 100644 index 0000000000000000000000000000000000000000..6aa9a28053a591e41453e665e5024e8a8cb78b3d GIT binary patch literal 2264 zcmchYX*3iJ7sqE|hQS!q5Mv)4GM2$i#uAFqC`%7x7baWA*i&dRX>3`uq(XS?3XSYp z%38`&ib7E$8j~$cF^}gt?|I+noW8#w?uYxk=iGD8|K9Vzd#pVc0002(2k@T|2@MMI zqxqr2AhQO*TVi`j@((S;e;g;l$#dAA{>vf0kX$R(Qn4oKgGEYjZ5zti2dw?Z6A zh%LuFCNI?9o+Z1duJL-++e#cjO`zlK?u9s030=k_*wD1#-$FbIDRDnA^vo@fm( zzjt(3VJrGOr0iHXSTM|rYN#>RZ@Dp`PwB2zrDQffLvuoR2~V3ReYa0&vU^dXd8isV zsAf*@!8s%xBvHLseXn6f?1kefe(8uAmAbaF$x{Ykzb6c6jdUwY1$y4tFzsj7 zIghr!T#ODfu@Po!a29@kXQ8kY#(LE<0o7?7PQ|eMeY@Equ?R-6*f@Na3o&stDQ=6( zQzDSQhCnS(9Bu9W_~giknP0vECqUsr4_9y_}nEU`cy z4}dApnAip92wMwgzciAFpc3i}+-#Zlq+iF7d1y}d4Qsp8=%l1N8NIs161I`HmkcpQ zY4*CUCFJJf(2!M{`&qQ}3($KeTQ=)mMrBs`DOb;%Of0tC)9he_p~w&CO#DfCgx(%s z{@|D(brX_Gb}ZDLmGej*JgEl0Et>q~kgTXuJg-PwvRjNx8sBbIShxD=xOySzw{;^X zAvrh5HTg>Xq@<{#^!Kg}B?qz@b<{ebD)yaSf&RChBIJQo-?Ahzw@qopSe^e&>^IuU zydM4Y1_C&>k7u|}=; z63R7$H6zat=hNExxEwXu1fQ*ytuEkP!{w{|#6TIEq1#*ck=6_NM*ILF65tmD-O5&R zMI!-MT<3U~t@}(CN4@RlZ~1I>C=!ywF)dNI{VvH;5Y3(Z4jY^%_c&fsm4Q`<1g|qX z&!h29jXjVE3nJnet*L)XL?-8<>qDbVGP%i^NwOZfwWO7?Mr!X7 zl}sG@9S_5}}td}$xrWIYY=e(VVBiv%A+M-{M z!3_^Tc=pV?niT!{D`!{e@W;MvrZ(OER{x7itVAtwE~spPtPtma|J=5dv&_oE!5H#` zdgXJ;+gJ4hI}*9QX9jpL`Gb)yCe%1}t!&O-^sihyZys%%5uF~WhsR_w(q7;vV5d4P zr%ZUA2}kO+L^2ePTgGT9Ua71w<+)poSyjTdLq&xbUn`<6&SpwFp(HRHUyU6J3WZ_! zfztko79+94Tq%mTYj53(RYcL&1~5`I#+w3`(Q|r+P(aT z%?r(^?IWw~19CB&uvXf(f7&BnEE{zwK4piVU`I4j1j?v5d4N<7VUJ8nM`$7S*mfKR z#9-JzPRZ?{M!@L+0N^V)IyeeP2T|^UK|m0QD+Ibs!wEoml^N!YO#vW~j~jraX(0A3 z6Kux?IRLez`O^X;{!4g%BhcRn>^H*qKZ3*|{_YGuz)KCJcu;)DSES5D2tDE`C02YR0R%Vy1T7k|RQ;3g<0icA$AuP0pOvc~jGl zz+NeKv_FT_;GWK&8XlDUv&hv9kxg?@c!bu?83i=YQ$S!K09Y)Glg3Hz?@|)ZCBlVz zP8i}#XZkMoje3I=h&I!!s_m?Qi@1MR`yv7X*yEs47qOs^t^?&=;*IQ!q&)gq_Sx5* z?fhU8Q*PSe*w7y)FH#P!9R^Xw!lTT+zI39L<&8cViaj$A(Z2Cg7!{V?uuyi#vlNCg z40i}2ivw&y&1-&Nh&WMG`&aIt>)(#tKTJ}^@696Kw1-{IzSOTnFF+0@k$o3%ZHS;Q#;t literal 0 HcmV?d00001 diff --git a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java new file mode 100644 index 00000000..b690adbc --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/Http2ApplicationTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013-2021 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 + * + * https://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.tests.http2; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.http.Http2SslContextSpec; +import reactor.netty.http.HttpProtocol; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientResponse; +import reactor.netty.resources.ConnectionProvider; +import reactor.test.StepVerifier; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.NettyDataBufferFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; + +/** + * @author Spencer Gibb + */ +@ExtendWith(OutputCaptureExtension.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +public class Http2ApplicationTests { + + @LocalServerPort + int port; + + @Test + public void http2Works(CapturedOutput output) { + String uri = "https://localhost:" + port + "/myprefix/hello"; + String expected = "Hello"; + assertResponse(uri, expected); + Assertions.assertThat(output).contains("Negotiated application-level protocol [h2]", "PRI * HTTP/2.0"); + } + + public static void assertResponse(String uri, String expected ) { + Flux responseFlux = getHttpClient().request(HttpMethod.GET) + .uri(uri) + .send(Mono.empty()) + .response((res, byteBufFlux) -> { + assertThat(res.status()).isEqualTo(HttpResponseStatus.OK); + NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); + return DataBufferUtils.join(byteBufFlux.map(bufferFactory::wrap)) + .map(dataBuffer -> dataBuffer.toString(StandardCharsets.UTF_8)) + .map(s -> { + assertThat(s).isEqualTo(expected); + return res; + }); + }); + + StepVerifier.create(responseFlux).expectNextCount(1).expectComplete().verify(); + } + + static HttpClient getHttpClient() { + return HttpClient.create(ConnectionProvider.builder("test").maxConnections(100) + .pendingAcquireTimeout(Duration.ofMillis(0)) + .pendingAcquireMaxCount(-1).build()) + .protocol(HttpProtocol.HTTP11, HttpProtocol.H2) + .secure(sslContextSpec -> { + Http2SslContextSpec clientSslCtxt = + Http2SslContextSpec.forClient() + .configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE)); + sslContextSpec.sslContext(clientSslCtxt); + }); + } + +} diff --git a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslConfiguration.java b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslConfiguration.java new file mode 100644 index 00000000..f303049d --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslConfiguration.java @@ -0,0 +1,33 @@ +/* + * Copyright 2013-2021 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 + * + * https://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.tests.http2.config; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Configuration +@EnableAutoConfiguration +public class NosslConfiguration { + + @GetMapping + public String home() { + return "nossl"; + } +} diff --git a/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslTests.java b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslTests.java new file mode 100644 index 00000000..0f4d5c5a --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/src/test/java/org/springframework/cloud/gateway/tests/http2/config/NosslTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013-2021 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 + * + * https://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.tests.http2.config; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.cloud.gateway.tests.http2.Http2Application; +import org.springframework.util.SocketUtils; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import static org.springframework.cloud.gateway.tests.http2.Http2ApplicationTests.assertResponse; + +/** + * @author Spencer Gibb + */ +@ExtendWith(OutputCaptureExtension.class) +@SpringBootTest(classes = Http2Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) +public class NosslTests { + + @LocalServerPort + int port; + + @BeforeAll + static void beforeAll() { + int noSslPort = SocketUtils.findAvailableTcpPort(); + System.setProperty("nossl.port", String.valueOf(noSslPort)); + } + + @AfterAll + static void afterAll() { + System.clearProperty("nossl.port"); + } + + @Test + public void http2Works(CapturedOutput output) { + String uri = "https://localhost:" + port + "/myprefix/hello"; + String expected = "Hello"; + assertResponse(uri, expected); + Assertions.assertThat(output).contains("Negotiated application-level protocol [h2]", "PRI * HTTP/2.0"); + } + + +} diff --git a/spring-cloud-gateway-integration-tests/http2/src/test/resources/application-nossl.yml b/spring-cloud-gateway-integration-tests/http2/src/test/resources/application-nossl.yml new file mode 100644 index 00000000..6143bf9d --- /dev/null +++ b/spring-cloud-gateway-integration-tests/http2/src/test/resources/application-nossl.yml @@ -0,0 +1,13 @@ +server: + ssl: + enabled: false + key-store: ~ + key-store-password: ~ + key-password: ~ + http2: + enabled: false + +spring: + cloud: + gateway: + enabled: false \ No newline at end of file diff --git a/spring-cloud-gateway-integration-tests/pom.xml b/spring-cloud-gateway-integration-tests/pom.xml index 6e396521..f0197462 100644 --- a/spring-cloud-gateway-integration-tests/pom.xml +++ b/spring-cloud-gateway-integration-tests/pom.xml @@ -21,6 +21,7 @@ + http2 mvc-failure-analyzer diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index e51e0fc8..60c9634b 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -23,15 +23,18 @@ import java.util.Set; import java.util.function.Supplier; import io.netty.channel.ChannelOption; -import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Flux; +import reactor.netty.http.Http11SslContextSpec; +import reactor.netty.http.Http2SslContextSpec; +import reactor.netty.http.HttpProtocol; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.WebsocketClientSpec; import reactor.netty.http.server.WebsocketServerSpec; import reactor.netty.resources.ConnectionProvider; +import reactor.netty.tcp.SslProvider.ProtocolSslContextSpec; import reactor.netty.transport.ProxyProvider; import org.springframework.beans.factory.BeanFactory; @@ -641,7 +644,8 @@ public class GatewayAutoConfiguration { @Bean @ConditionalOnMissingBean - public HttpClient gatewayHttpClient(HttpClientProperties properties, List customizers) { + public HttpClient gatewayHttpClient(HttpClientProperties properties, ServerProperties serverProperties, + List customizers) { // configure pool resources ConnectionProvider connectionProvider = buildConnectionProvider(properties); @@ -658,57 +662,66 @@ public class GatewayAutoConfiguration { spec.maxInitialLineLength((int) properties.getMaxInitialLineLength().toBytes()); } return spec; - }).tcpConfiguration(tcpClient -> { - - if (properties.getConnectTimeout() != null) { - tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, - properties.getConnectTimeout()); - } - - // configure proxy if proxy host is set. - HttpClientProperties.Proxy proxy = properties.getProxy(); - - if (StringUtils.hasText(proxy.getHost())) { - - tcpClient = tcpClient.proxy(proxySpec -> { - ProxyProvider.Builder builder = proxySpec.type(proxy.getType()).host(proxy.getHost()); - - PropertyMapper map = PropertyMapper.get(); - - map.from(proxy::getPort).whenNonNull().to(builder::port); - map.from(proxy::getUsername).whenHasText().to(builder::username); - map.from(proxy::getPassword).whenHasText() - .to(password -> builder.password(s -> password)); - map.from(proxy::getNonProxyHostsPattern).whenHasText().to(builder::nonProxyHosts); - }); - } - return tcpClient; }); + if (serverProperties.getHttp2().isEnabled()) { + httpClient = httpClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2); + } + + if (properties.getConnectTimeout() != null) { + httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout()); + } + + // configure proxy if proxy host is set. + if (StringUtils.hasText(properties.getProxy().getHost())) { + HttpClientProperties.Proxy proxy = properties.getProxy(); + + httpClient = httpClient.proxy(proxySpec -> { + ProxyProvider.Builder builder = proxySpec.type(proxy.getType()).host(proxy.getHost()); + + PropertyMapper map = PropertyMapper.get(); + + map.from(proxy::getPort).whenNonNull().to(builder::port); + map.from(proxy::getUsername).whenHasText().to(builder::username); + map.from(proxy::getPassword).whenHasText().to(password -> builder.password(s -> password)); + map.from(proxy::getNonProxyHostsPattern).whenHasText().to(builder::nonProxyHosts); + }); + } + HttpClientProperties.Ssl ssl = properties.getSsl(); if ((ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) || ssl.getTrustedX509CertificatesForTrustManager().length > 0 || ssl.isUseInsecureTrustManager()) { httpClient = httpClient.secure(sslContextSpec -> { // configure ssl - SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + ProtocolSslContextSpec clientSslContext = (serverProperties.getHttp2().isEnabled()) + ? Http2SslContextSpec.forClient() : Http11SslContextSpec.forClient(); + clientSslContext.configure(sslContextBuilder -> { + X509Certificate[] trustedX509Certificates = ssl.getTrustedX509CertificatesForTrustManager(); + if (trustedX509Certificates.length > 0) { + sslContextBuilder.trustManager(trustedX509Certificates); + } + else if (ssl.isUseInsecureTrustManager()) { + sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } - X509Certificate[] trustedX509Certificates = ssl.getTrustedX509CertificatesForTrustManager(); - if (trustedX509Certificates.length > 0) { - sslContextBuilder = sslContextBuilder.trustManager(trustedX509Certificates); - } - else if (ssl.isUseInsecureTrustManager()) { - sslContextBuilder = sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); - } + try { + sslContextBuilder.keyManager(ssl.getKeyManagerFactory()); + } + catch (Exception e) { + logger.error(e); + } + }); - try { - sslContextBuilder = sslContextBuilder.keyManager(ssl.getKeyManagerFactory()); - } - catch (Exception e) { - logger.error(e); - } - - sslContextSpec.sslContext(sslContextBuilder).defaultConfiguration(ssl.getDefaultConfigurationType()) - .handshakeTimeout(ssl.getHandshakeTimeout()) + sslContextSpec.sslContext(clientSslContext).handshakeTimeout(ssl.getHandshakeTimeout()) + .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) + .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); + }); + } + else if (serverProperties.getHttp2().isEnabled()) { + httpClient = httpClient.secure(sslContextSpec -> { + Http2SslContextSpec clientSslCtxt = Http2SslContextSpec.forClient() + .configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE)); + sslContextSpec.sslContext(clientSslCtxt).handshakeTimeout(ssl.getHandshakeTimeout()) .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); }); diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java index a7054cd4..59295613 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientProperties.java @@ -403,6 +403,7 @@ public class HttpClientProperties { private Duration closeNotifyReadTimeout = Duration.ZERO; /** The default ssl configuration type. Defaults to TCP. */ + @Deprecated private SslProvider.DefaultConfigurationType defaultConfigurationType = SslProvider.DefaultConfigurationType.TCP; /** Keystore path for Netty HttpClient. */ @@ -568,10 +569,12 @@ public class HttpClientProperties { this.closeNotifyReadTimeout = closeNotifyReadTimeout; } + @Deprecated public SslProvider.DefaultConfigurationType getDefaultConfigurationType() { return defaultConfigurationType; } + @Deprecated public void setDefaultConfigurationType(SslProvider.DefaultConfigurationType defaultConfigurationType) { this.defaultConfigurationType = defaultConfigurationType; } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index a39c91b9..cbff76b2 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -33,7 +33,9 @@ import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.ServerProperties; import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner; import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint; import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint; @@ -68,7 +70,8 @@ public class GatewayAutoConfigurationTests { public void nettyHttpClientDefaults() { new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, - SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class)) + SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class, + ServerPropertiesConfig.class)) .withPropertyValues("debug=true").run(context -> { assertThat(context).hasSingleBean(HttpClient.class); assertThat(context).hasBean("gatewayHttpClient"); @@ -94,7 +97,7 @@ public class GatewayAutoConfigurationTests { new ReactiveWebApplicationContextRunner() .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class, - HttpClientCustomizedConfig.class)) + HttpClientCustomizedConfig.class, ServerPropertiesConfig.class)) .withPropertyValues("spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true", "spring.cloud.gateway.httpclient.connect-timeout=10", "spring.cloud.gateway.httpclient.response-timeout=10s", @@ -228,6 +231,12 @@ public class GatewayAutoConfigurationTests { assertThat(spec2.protocols()).isNull(); } + @Configuration + @EnableConfigurationProperties(ServerProperties.class) + protected static class ServerPropertiesConfig { + + } + @EnableAutoConfiguration @SpringBootConfiguration protected static class Config {