Merge branch 'configure-httpclientfactory' into 3.1.x

This commit is contained in:
spencergibb
2022-02-16 13:28:37 -05:00
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;
@@ -155,15 +145,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;
@@ -173,9 +160,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
@@ -672,139 +656,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 {