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 {