From b2df97d0c467d246a56870d63fc637087eec0ef8 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Thu, 3 Feb 2022 12:31:58 -0500 Subject: [PATCH] Moves configuration of HttpClient. Moves configuration of HttpClient out of GatewayAutoConfiguration and into HttpClientFactory. This allows customization by the user and allows tests to see what has been configured on the HttpClient. When HTTP2 is enabled, the insecure trustmanager factory option must be set explicitly. --- .../config/GatewayAutoConfiguration.java | 153 +-------- .../gateway/config/HttpClientFactory.java | 314 ++++++++++++++++++ .../gateway/config/HttpClientProperties.java | 3 + .../config/GatewayAutoConfigurationTests.java | 180 ++++++++-- 4 files changed, 471 insertions(+), 179 deletions(-) create mode 100644 spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java 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 0254ebda..e4b3e601 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 @@ -16,26 +16,16 @@ package org.springframework.cloud.gateway.config; -import java.security.cert.X509Certificate; -import java.time.Duration; import java.util.List; import java.util.Set; import java.util.function.Supplier; -import io.netty.channel.ChannelOption; -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; import org.springframework.beans.factory.ObjectProvider; @@ -154,15 +144,12 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Primary; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.Environment; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.security.web.server.SecurityWebFilterChain; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import org.springframework.validation.Validator; import org.springframework.web.reactive.DispatcherHandler; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; @@ -172,9 +159,6 @@ import org.springframework.web.reactive.socket.server.WebSocketService; import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService; import org.springframework.web.reactive.socket.server.upgrade.ReactorNettyRequestUpgradeStrategy; -import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED; -import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED; - /** * @author Spencer Gibb * @author Ziemowit Stolarczyk @@ -665,139 +649,10 @@ public class GatewayAutoConfiguration { } @Bean - @ConditionalOnMissingBean - public HttpClient gatewayHttpClient(HttpClientProperties properties, ServerProperties serverProperties, - List customizers) { - - // configure pool resources - ConnectionProvider connectionProvider = buildConnectionProvider(properties); - - HttpClient httpClient = HttpClient.create(connectionProvider) - // TODO: move customizations to HttpClientCustomizers - .httpResponseDecoder(spec -> { - if (properties.getMaxHeaderSize() != null) { - // cast to int is ok, since @Max is Integer.MAX_VALUE - spec.maxHeaderSize((int) properties.getMaxHeaderSize().toBytes()); - } - if (properties.getMaxInitialLineLength() != null) { - // cast to int is ok, since @Max is Integer.MAX_VALUE - spec.maxInitialLineLength((int) properties.getMaxInitialLineLength().toBytes()); - } - return spec; - }); - - 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 - 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); - } - - try { - sslContextBuilder.keyManager(ssl.getKeyManagerFactory()); - } - catch (Exception e) { - logger.error(e); - } - }); - - 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()); - }); - } - - if (properties.isWiretap()) { - httpClient = httpClient.wiretap(true); - } - - if (properties.isCompression()) { - httpClient = httpClient.compress(true); - } - - if (!CollectionUtils.isEmpty(customizers)) { - customizers.sort(AnnotationAwareOrderComparator.INSTANCE); - for (HttpClientCustomizer customizer : customizers) { - httpClient = customizer.customize(httpClient); - } - } - - return httpClient; - } - - private ConnectionProvider buildConnectionProvider(HttpClientProperties properties) { - HttpClientProperties.Pool pool = properties.getPool(); - - ConnectionProvider connectionProvider; - if (pool.getType() == DISABLED) { - connectionProvider = ConnectionProvider.newConnection(); - } - else { - // create either Fixed or Elastic pool - ConnectionProvider.Builder builder = ConnectionProvider.builder(pool.getName()); - if (pool.getType() == FIXED) { - builder.maxConnections(pool.getMaxConnections()).pendingAcquireMaxCount(-1) - .pendingAcquireTimeout(Duration.ofMillis(pool.getAcquireTimeout())); - } - else { - // Elastic - builder.maxConnections(Integer.MAX_VALUE).pendingAcquireTimeout(Duration.ofMillis(0)) - .pendingAcquireMaxCount(-1); - } - - if (pool.getMaxIdleTime() != null) { - builder.maxIdleTime(pool.getMaxIdleTime()); - } - if (pool.getMaxLifeTime() != null) { - builder.maxLifeTime(pool.getMaxLifeTime()); - } - builder.evictInBackground(pool.getEvictionInterval()); - builder.metrics(pool.isMetrics()); - connectionProvider = builder.build(); - } - return connectionProvider; + @ConditionalOnMissingBean({ HttpClient.class, HttpClientFactory.class }) + public HttpClientFactory gatewayHttpClientFactory(HttpClientProperties properties, + ServerProperties serverProperties, List customizers) { + return new HttpClientFactory(properties, serverProperties, customizers); } @Bean diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java new file mode 100644 index 00000000..4d033750 --- /dev/null +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java @@ -0,0 +1,314 @@ +/* + * Copyright 2013-2022 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.config; + +import java.io.IOException; +import java.net.URL; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchProviderException; +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; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +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.HttpResponseDecoderSpec; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.tcp.SslProvider; +import reactor.netty.transport.ProxyProvider; + +import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ResourceUtils; +import org.springframework.util.StringUtils; + +import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED; +import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED; + +/** + * Factory Bean that allows users to extend and customize parts of the HttpClient. Also + * allows for testing the configuration of the HttpClient. + * + * @author Spencer Gibb + * @since 3.1.1 + */ +public class HttpClientFactory extends AbstractFactoryBean { + + protected final HttpClientProperties properties; + + protected final ServerProperties serverProperties; + + protected final List customizers; + + public HttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, + List customizers) { + this.properties = properties; + this.serverProperties = serverProperties; + this.customizers = customizers; + } + + @Override + public Class getObjectType() { + return HttpClient.class; + } + + @Override + protected HttpClient createInstance() { + // configure pool resources + ConnectionProvider connectionProvider = buildConnectionProvider(properties); + + HttpClient httpClient = HttpClient.create(connectionProvider) + // TODO: move customizations to HttpClientCustomizers + .httpResponseDecoder(this::httpResponseDecoder); + + if (serverProperties.getHttp2().isEnabled()) { + httpClient = httpClient.protocol(HttpProtocol.HTTP11, HttpProtocol.H2); + } + + if (properties.getConnectTimeout() != null) { + httpClient = httpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, properties.getConnectTimeout()); + } + + httpClient = configureProxy(httpClient); + + httpClient = configureSsl(httpClient); + + if (properties.isWiretap()) { + httpClient = httpClient.wiretap(true); + } + + if (properties.isCompression()) { + httpClient = httpClient.compress(true); + } + + httpClient = applyCustomizers(httpClient); + + return httpClient; + } + + private HttpClient applyCustomizers(HttpClient httpClient) { + if (!CollectionUtils.isEmpty(customizers)) { + customizers.sort(AnnotationAwareOrderComparator.INSTANCE); + for (HttpClientCustomizer customizer : customizers) { + httpClient = customizer.customize(httpClient); + } + } + return httpClient; + } + + protected HttpClient configureSsl(HttpClient httpClient) { + HttpClientProperties.Ssl ssl = properties.getSsl(); + if ((ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) + || getTrustedX509CertificatesForTrustManager().length > 0 || ssl.isUseInsecureTrustManager()) { + httpClient = httpClient.secure(sslContextSpec -> { + // configure ssl + configureSslContext(ssl, sslContextSpec); + }); + } + return httpClient; + } + + protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { + SslProvider.ProtocolSslContextSpec clientSslContext = (serverProperties.getHttp2().isEnabled()) + ? Http2SslContextSpec.forClient() : Http11SslContextSpec.forClient(); + clientSslContext.configure(sslContextBuilder -> { + X509Certificate[] trustedX509Certificates = getTrustedX509CertificatesForTrustManager(); + if (trustedX509Certificates.length > 0) { + setTrustManager(sslContextBuilder, trustedX509Certificates); + } + else if (ssl.isUseInsecureTrustManager()) { + setTrustManager(sslContextBuilder, InsecureTrustManagerFactory.INSTANCE); + } + + try { + sslContextBuilder.keyManager(getKeyManagerFactory()); + } + catch (Exception e) { + logger.error(e); + } + }); + + sslContextSpec.sslContext(clientSslContext).handshakeTimeout(ssl.getHandshakeTimeout()) + .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) + .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); + } + + protected HttpClient configureProxy(HttpClient httpClient) { + // configure proxy if proxy host is set. + if (StringUtils.hasText(properties.getProxy().getHost())) { + HttpClientProperties.Proxy proxy = properties.getProxy(); + + httpClient = httpClient.proxy(proxySpec -> { + configureProxyProvider(proxy, proxySpec); + }); + } + return httpClient; + } + + protected ProxyProvider.Builder configureProxyProvider(HttpClientProperties.Proxy proxy, + ProxyProvider.TypeSpec 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 builder; + } + + protected HttpResponseDecoderSpec httpResponseDecoder(HttpResponseDecoderSpec spec) { + if (properties.getMaxHeaderSize() != null) { + // cast to int is ok, since @Max is Integer.MAX_VALUE + spec.maxHeaderSize((int) properties.getMaxHeaderSize().toBytes()); + } + if (properties.getMaxInitialLineLength() != null) { + // cast to int is ok, since @Max is Integer.MAX_VALUE + spec.maxInitialLineLength((int) properties.getMaxInitialLineLength().toBytes()); + } + return spec; + } + + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + ArrayList allCerts = new ArrayList<>(); + for (String trustedCert : ssl.getTrustedX509Certificates()) { + try { + URL url = ResourceUtils.getURL(trustedCert); + Collection certs = certificateFactory.generateCertificates(url.openStream()); + allCerts.addAll(certs); + } + catch (IOException e) { + throw new RuntimeException("Could not load certificate '" + trustedCert + "'", e); + } + } + return allCerts.toArray(new X509Certificate[allCerts.size()]); + } + catch (CertificateException e1) { + throw new RuntimeException("Could not load CertificateFactory X.509", e1); + } + } + + protected KeyManagerFactory getKeyManagerFactory() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + try { + if (ssl.getKeyStore() != null && ssl.getKeyStore().length() > 0) { + KeyManagerFactory keyManagerFactory = KeyManagerFactory + .getInstance(KeyManagerFactory.getDefaultAlgorithm()); + char[] keyPassword = ssl.getKeyPassword() != null ? ssl.getKeyPassword().toCharArray() : null; + + if (keyPassword == null && ssl.getKeyStorePassword() != null) { + keyPassword = ssl.getKeyStorePassword().toCharArray(); + } + + keyManagerFactory.init(this.createKeyStore(), keyPassword); + + return keyManagerFactory; + } + + return null; + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + protected KeyStore createKeyStore() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + try { + KeyStore store = ssl.getKeyStoreProvider() != null + ? KeyStore.getInstance(ssl.getKeyStoreType(), ssl.getKeyStoreProvider()) + : KeyStore.getInstance(ssl.getKeyStoreType()); + try { + URL url = ResourceUtils.getURL(ssl.getKeyStore()); + store.load(url.openStream(), + ssl.getKeyStorePassword() != null ? ssl.getKeyStorePassword().toCharArray() : null); + } + catch (Exception e) { + throw new RuntimeException("Could not load key store ' " + ssl.getKeyStore() + "'", e); + } + + return store; + } + catch (KeyStoreException | NoSuchProviderException e) { + throw new RuntimeException("Could not load KeyStore for given type and provider", e); + } + } + + protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { + sslContextBuilder.trustManager(trustedX509Certificates); + } + + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + sslContextBuilder.trustManager(factory); + } + + protected ConnectionProvider buildConnectionProvider(HttpClientProperties properties) { + HttpClientProperties.Pool pool = properties.getPool(); + + ConnectionProvider connectionProvider; + if (pool.getType() == DISABLED) { + connectionProvider = ConnectionProvider.newConnection(); + } + else { + // create either Fixed or Elastic pool + ConnectionProvider.Builder builder = ConnectionProvider.builder(pool.getName()); + if (pool.getType() == FIXED) { + builder.maxConnections(pool.getMaxConnections()).pendingAcquireMaxCount(-1) + .pendingAcquireTimeout(Duration.ofMillis(pool.getAcquireTimeout())); + } + else { + // Elastic + builder.maxConnections(Integer.MAX_VALUE).pendingAcquireTimeout(Duration.ofMillis(0)) + .pendingAcquireMaxCount(-1); + } + + if (pool.getMaxIdleTime() != null) { + builder.maxIdleTime(pool.getMaxIdleTime()); + } + if (pool.getMaxLifeTime() != null) { + builder.maxLifeTime(pool.getMaxLifeTime()); + } + builder.evictInBackground(pool.getEvictionInterval()); + builder.metrics(pool.isMetrics()); + connectionProvider = builder.build(); + } + return connectionProvider; + } + +} 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 f0e2490f..bdce83ea 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 @@ -483,6 +483,7 @@ public class HttpClientProperties { this.trustedX509Certificates = trustedX509; } + @Deprecated public X509Certificate[] getTrustedX509CertificatesForTrustManager() { try { CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); @@ -505,6 +506,7 @@ public class HttpClientProperties { } } + @Deprecated public KeyManagerFactory getKeyManagerFactory() { try { if (getKeyStore() != null && getKeyStore().length() > 0) { @@ -528,6 +530,7 @@ public class HttpClientProperties { } } + @Deprecated public KeyStore createKeyStore() { try { KeyStore store = getKeyStoreProvider() != null 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 d1a27852..6c46143c 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 @@ -18,18 +18,30 @@ package org.springframework.cloud.gateway.config; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ssl.TrustManagerFactory; + +import io.netty.channel.ChannelOption; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.junit.Test; +import reactor.netty.http.HttpProtocol; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientConfig; import reactor.netty.http.client.WebsocketClientSpec; import reactor.netty.http.server.WebsocketServerSpec; +import reactor.netty.resources.ConnectionProvider; +import reactor.netty.tcp.SslProvider; +import reactor.netty.transport.ProxyProvider; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; @@ -48,6 +60,7 @@ import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; import org.springframework.web.filter.reactive.HiddenHttpMethodFilter; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; @@ -76,21 +89,19 @@ public class GatewayAutoConfigurationTests { ServerPropertiesConfig.class)) .withPropertyValues("debug=true").run(context -> { assertThat(context).hasSingleBean(HttpClient.class); - assertThat(context).hasBean("gatewayHttpClient"); HttpClient httpClient = context.getBean(HttpClient.class); - /* - * FIXME: 2.1.0 HttpClientOptions options = httpClient.options(); - * - * PoolResources poolResources = options.getPoolResources(); - * assertThat(poolResources).isNotNull(); //TODO: howto test - * PoolResources - * - * ClientProxyOptions proxyOptions = options.getProxyOptions(); - * assertThat(proxyOptions).isNull(); - * - * SslContext sslContext = options.sslContext(); - * assertThat(sslContext).isNull(); - */ + CustomHttpClientFactory factory = context.getBean(CustomHttpClientFactory.class); + + assertThat(factory.connectionProvider).isNotNull(); + assertThat(factory.connectionProvider.maxConnections()).isEqualTo(Integer.MAX_VALUE); // elastic + + assertThat(factory.proxyProvider).isNull(); + assertThat(factory.sslConfigured).isFalse(); + + assertThat(httpClient.configuration().isAcceptGzip()).isFalse(); + assertThat(httpClient.configuration().loggingHandler()).isNull(); + assertThat(httpClient.configuration().options()) + .doesNotContainKey(ChannelOption.CONNECT_TIMEOUT_MILLIS); }); } @@ -107,6 +118,7 @@ public class GatewayAutoConfigurationTests { "spring.cloud.gateway.httpclient.pool.type=fixed", "spring.cloud.gateway.httpclient.pool.metrics=true", "spring.cloud.gateway.httpclient.compression=true", + "spring.cloud.gateway.httpclient.wiretap=true", // greater than integer max value "spring.cloud.gateway.httpclient.max-initial-line-length=2147483647", "spring.cloud.gateway.httpclient.proxy.host=myhost", @@ -114,27 +126,30 @@ public class GatewayAutoConfigurationTests { .run(context -> { assertThat(context).hasSingleBean(HttpClient.class); HttpClient httpClient = context.getBean(HttpClient.class); + CustomHttpClientFactory factory = context.getBean(CustomHttpClientFactory.class); HttpClientProperties properties = context.getBean(HttpClientProperties.class); assertThat(properties.getMaxInitialLineLength().toBytes()).isLessThanOrEqualTo(Integer.MAX_VALUE); assertThat(properties.isCompression()).isEqualTo(true); assertThat(properties.getPool().getEvictionInterval()).hasSeconds(10); assertThat(properties.getPool().isMetrics()).isEqualTo(true); - /* - * FIXME: 2.1.0 HttpClientOptions options = httpClient.options(); - * - * PoolResources poolResources = options.getPoolResources(); - * assertThat(poolResources).isNotNull(); //TODO: howto test - * PoolResources - * - * ClientProxyOptions proxyOptions = options.getProxyOptions(); - * assertThat(proxyOptions).isNotNull(); - * assertThat(proxyOptions.getAddress().get().getHostName()).isEqualTo - * ("myhost"); - * - * SslContext sslContext = options.sslContext(); - * assertThat(sslContext).isNotNull(); - */ - // TODO: howto test SslContext + + assertThat(httpClient.configuration().isAcceptGzip()).isTrue(); + assertThat(httpClient.configuration().loggingHandler()).isNotNull(); + assertThat(httpClient.configuration().options()).containsKey(ChannelOption.CONNECT_TIMEOUT_MILLIS); + assertThat(httpClient.configuration().options().get(ChannelOption.CONNECT_TIMEOUT_MILLIS)) + .isEqualTo(10); + + assertThat(factory.connectionProvider).isNotNull(); + // fixed pool + assertThat(factory.connectionProvider.maxConnections()) + .isEqualTo(ConnectionProvider.DEFAULT_POOL_MAX_CONNECTIONS); + + assertThat(factory.proxyProvider).isNotNull(); + assertThat(factory.proxyProvider.build().getAddress().get().getHostName()).isEqualTo("myhost"); + + assertThat(factory.sslConfigured).isTrue(); + assertThat(factory.insecureTrustManagerSet).isTrue(); + assertThat(context).hasSingleBean(ReactorNettyRequestUpgradeStrategy.class); ReactorNettyRequestUpgradeStrategy upgradeStrategy = context .getBean(ReactorNettyRequestUpgradeStrategy.class); @@ -244,6 +259,8 @@ public class GatewayAutoConfigurationTests { .withPropertyValues("server.http2.enabled=true").run(context -> { assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class); assertThat(context).hasSingleBean(GRPCResponseHeadersFilter.class); + HttpClient httpClient = context.getBean(HttpClient.class); + assertThat(httpClient.configuration().protocols()).contains(HttpProtocol.HTTP11, HttpProtocol.H2); }); } @@ -259,10 +276,86 @@ public class GatewayAutoConfigurationTests { }); } + @Test + public void insecureTrustManagerNotEnabledByDefaultWhenHTTP2Enabled() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class, + HttpClientCustomizedConfig.class, ServerPropertiesConfig.class)) + .withPropertyValues("server.http2.enabled=true").run(context -> { + assertThat(context).hasSingleBean(HttpClient.class); + CustomHttpClientFactory factory = context.getBean(CustomHttpClientFactory.class); + assertThat(factory.insecureTrustManagerSet).isFalse(); + }); + } + + @Test + public void customHttpClientWorks() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class, + HttpClientCustomizedConfig.class, CustomHttpClientConfig.class)) + .run(context -> { + assertThat(context).hasSingleBean(HttpClient.class); + HttpClient httpClient = context.getBean(HttpClient.class); + assertThat(httpClient).isInstanceOf(CustomHttpClient.class); + }); + } + @Configuration @EnableConfigurationProperties(ServerProperties.class) + @AutoConfigureBefore(GatewayAutoConfiguration.class) protected static class ServerPropertiesConfig { + @Bean + @Primary + CustomHttpClientFactory customHttpClientFactory(HttpClientProperties properties, + ServerProperties serverProperties, List customizers) { + return new CustomHttpClientFactory(properties, serverProperties, customizers); + } + + } + + protected static class CustomHttpClientFactory extends HttpClientFactory { + + boolean insecureTrustManagerSet; + + boolean sslConfigured; + + private ConnectionProvider connectionProvider; + + private ProxyProvider.Builder proxyProvider; + + public CustomHttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, + List customizers) { + super(properties, serverProperties, customizers); + } + + @Override + protected ConnectionProvider buildConnectionProvider(HttpClientProperties properties) { + connectionProvider = super.buildConnectionProvider(properties); + return connectionProvider; + } + + @Override + protected ProxyProvider.Builder configureProxyProvider(HttpClientProperties.Proxy proxy, + ProxyProvider.TypeSpec proxySpec) { + proxyProvider = super.configureProxyProvider(proxy, proxySpec); + return proxyProvider; + } + + @Override + protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { + sslConfigured = true; + super.configureSslContext(ssl, sslContextSpec); + } + + @Override + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + insecureTrustManagerSet = factory == InsecureTrustManagerFactory.INSTANCE; + super.setTrustManager(sslContextBuilder, factory); + } + } @EnableAutoConfiguration @@ -271,6 +364,33 @@ public class GatewayAutoConfigurationTests { } + @EnableAutoConfiguration + @SpringBootConfiguration + @EnableConfigurationProperties(ServerProperties.class) + @AutoConfigureBefore(GatewayAutoConfiguration.class) + protected static class CustomHttpClientConfig { + + @Bean + public HttpClient customHttpClient() { + return new CustomHttpClient(); + } + + } + + protected static class CustomHttpClient extends HttpClient { + + @Override + public HttpClientConfig configuration() { + return null; + } + + @Override + protected HttpClient duplicate() { + return this; + } + + } + @EnableAutoConfiguration @SpringBootConfiguration protected static class RouteLocatorBuilderConfig {