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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user