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.
This commit is contained in:
spencergibb
2022-02-03 12:31:58 -05:00
parent b54167d634
commit b2df97d0c4
4 changed files with 471 additions and 179 deletions

View File

@@ -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<HttpClientCustomizer> 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<HttpClientCustomizer> customizers) {
return new HttpClientFactory(properties, serverProperties, customizers);
}
@Bean

View File

@@ -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<HttpClient> {
protected final HttpClientProperties properties;
protected final ServerProperties serverProperties;
protected final List<HttpClientCustomizer> customizers;
public HttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties,
List<HttpClientCustomizer> 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<Certificate> allCerts = new ArrayList<>();
for (String trustedCert : ssl.getTrustedX509Certificates()) {
try {
URL url = ResourceUtils.getURL(trustedCert);
Collection<? extends Certificate> 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;
}
}

View File

@@ -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

View File

@@ -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<HttpClientCustomizer> 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<HttpClientCustomizer> 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 {