From 0b58d771024f5d8d56464679d806f1bd30a0f299 Mon Sep 17 00:00:00 2001 From: spencergibb Date: Wed, 31 Aug 2022 11:48:16 -0400 Subject: [PATCH] Polish gh-2699 Updates HttpClientFactory for backwards compatibility If a user has extended HttpClientFactory and doesn't use the new SslConfigurer, the original protected methods need to be there. Those methods are marked as deprecated. --- .../gateway/config/HttpClientFactory.java | 151 +++++++++++++++--- .../config/GatewayAutoConfigurationTests.java | 110 +++++++++++++ 2 files changed, 240 insertions(+), 21 deletions(-) diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java index f2b66576..7f28268d 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/HttpClientFactory.java @@ -16,9 +16,18 @@ 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; @@ -26,6 +35,9 @@ 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; @@ -38,6 +50,7 @@ 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; @@ -64,7 +77,7 @@ public class HttpClientFactory extends AbstractFactoryBean { List customizers) { this.properties = properties; this.serverProperties = serverProperties; - this.sslConfigurer = new HttpClientSslConfigurer(properties.getSsl(), serverProperties); + this.sslConfigurer = null; this.customizers = customizers; } @@ -116,31 +129,45 @@ public class HttpClientFactory extends AbstractFactoryBean { } protected HttpClient configureSsl(HttpClient httpClient) { - return sslConfigurer.configureSsl(httpClient); + if (sslConfigurer != null) { + return sslConfigurer.configureSsl(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; } + @Deprecated protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { - sslConfigurer.configureSslContext(ssl, 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); + } - protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { - return sslConfigurer.getTrustedX509CertificatesForTrustManager(); - } + try { + sslContextBuilder.keyManager(getKeyManagerFactory()); + } + catch (Exception e) { + logger.error(e); + } + }); - protected KeyManagerFactory getKeyManagerFactory() { - return sslConfigurer.getKeyManagerFactory(); - } - - protected KeyStore createKeyStore() { - return sslConfigurer.createKeyStore(); - } - - protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { - sslConfigurer.setTrustManager(sslContextBuilder, trustedX509Certificates); - } - - protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { - sslConfigurer.setTrustManager(sslContextBuilder, factory); + sslContextSpec.sslContext(clientSslContext).handshakeTimeout(ssl.getHandshakeTimeout()) + .closeNotifyFlushTimeout(ssl.getCloseNotifyFlushTimeout()) + .closeNotifyReadTimeout(ssl.getCloseNotifyReadTimeout()); } private HttpClient applyCustomizers(HttpClient httpClient) { @@ -165,6 +192,88 @@ public class HttpClientFactory extends AbstractFactoryBean { return httpClient; } + @Deprecated + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + HttpClientProperties.Ssl ssl = properties.getSsl(); + + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + ArrayList allCerts = new ArrayList<>(); + for (String trustedCert : ssl.getTrustedX509Certificates()) { + try { + URL url = ResourceUtils.getURL(trustedCert); + Collection certs = certificateFactory.generateCertificates(url.openStream()); + allCerts.addAll(certs); + } + catch (IOException e) { + throw new RuntimeException("Could not load certificate '" + trustedCert + "'", e); + } + } + return allCerts.toArray(new X509Certificate[allCerts.size()]); + } + catch (CertificateException e1) { + throw new RuntimeException("Could not load CertificateFactory X.509", e1); + } + } + + @Deprecated + 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); + } + } + + @Deprecated + 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); + } + } + + @Deprecated + protected void setTrustManager(SslContextBuilder sslContextBuilder, X509Certificate... trustedX509Certificates) { + sslContextBuilder.trustManager(trustedX509Certificates); + } + + @Deprecated + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + sslContextBuilder.trustManager(factory); + } + protected ProxyProvider.Builder configureProxyProvider(HttpClientProperties.Proxy proxy, ProxyProvider.TypeSpec proxySpec) { ProxyProvider.Builder builder = proxySpec.type(proxy.getType()).host(proxy.getHost()); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java index dbb4bd14..32a3a5df 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/config/GatewayAutoConfigurationTests.java @@ -18,9 +18,12 @@ package org.springframework.cloud.gateway.config; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.security.KeyStore; +import java.security.cert.X509Certificate; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; import io.netty.channel.ChannelOption; @@ -164,6 +167,28 @@ public class GatewayAutoConfigurationTests { }); } + @Test + @Deprecated + public void nettyHttpClientNoSslConfigurerIsBackwardsCompatible() { + new ReactiveWebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class, + SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class, + NoSslConfigurerCustomHttpClientFactoryConfig.class)) + .withPropertyValues("spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true") + .run(context -> { + assertThat(context).hasSingleBean(HttpClient.class); + NoSslConfigurerHttpClientFactory factory = context.getBean(NoSslConfigurerHttpClientFactory.class); + + assertThat(factory.configureSslCalled).isTrue(); + assertThat(factory.configureSslContextCalled).isTrue(); + assertThat(factory.getTrustedX509CertificatesForTrustManagerCalled).isTrue(); + assertThat(factory.getKeyManagerFactoryCalled).isTrue(); + assertThat(factory.createKeyStoreCalled).isFalse(); + assertThat(factory.setTrustManagerCertCalled).isFalse(); + assertThat(factory.setTrustManagerFactoryCalled).isTrue(); + }); + } + @Test public void verboseActuatorEnabledByDefault() { try (ConfigurableApplicationContext ctx = SpringApplication.run(Config.class, "--spring.jmx.enabled=false", @@ -325,6 +350,21 @@ public class GatewayAutoConfigurationTests { } + @Configuration + @EnableConfigurationProperties(ServerProperties.class) + @AutoConfigureBefore(GatewayAutoConfiguration.class) + @Deprecated + protected static class NoSslConfigurerCustomHttpClientFactoryConfig { + + @Bean + @Primary + NoSslConfigurerHttpClientFactory noSslConfigurerHttpClientFactory(HttpClientProperties properties, + ServerProperties serverProperties, List customizers) { + return new NoSslConfigurerHttpClientFactory(properties, serverProperties, customizers); + } + + } + protected static class CustomHttpClientFactory extends HttpClientFactory { private ConnectionProvider connectionProvider; @@ -387,6 +427,76 @@ public class GatewayAutoConfigurationTests { } + /* + * Class to test backwards compatibility if no `SslConfigurer` used. + */ + @Deprecated + protected static class NoSslConfigurerHttpClientFactory extends HttpClientFactory { + + boolean configureSslCalled; + + boolean configureSslContextCalled; + + boolean getTrustedX509CertificatesForTrustManagerCalled; + + boolean getKeyManagerFactoryCalled; + + boolean createKeyStoreCalled; + + boolean setTrustManagerCertCalled; + + boolean setTrustManagerFactoryCalled; + + public NoSslConfigurerHttpClientFactory(HttpClientProperties properties, ServerProperties serverProperties, + List customizers) { + super(properties, serverProperties, customizers); + } + + @Override + protected HttpClient configureSsl(HttpClient httpClient) { + configureSslCalled = true; + return super.configureSsl(httpClient); + } + + @Override + protected void configureSslContext(HttpClientProperties.Ssl ssl, SslProvider.SslContextSpec sslContextSpec) { + configureSslContextCalled = true; + super.configureSslContext(ssl, sslContextSpec); + } + + @Override + protected X509Certificate[] getTrustedX509CertificatesForTrustManager() { + getTrustedX509CertificatesForTrustManagerCalled = true; + return super.getTrustedX509CertificatesForTrustManager(); + } + + @Override + protected KeyManagerFactory getKeyManagerFactory() { + getKeyManagerFactoryCalled = true; + return super.getKeyManagerFactory(); + } + + @Override + protected KeyStore createKeyStore() { + createKeyStoreCalled = true; + return super.createKeyStore(); + } + + @Override + protected void setTrustManager(SslContextBuilder sslContextBuilder, + X509Certificate... trustedX509Certificates) { + setTrustManagerCertCalled = true; + super.setTrustManager(sslContextBuilder, trustedX509Certificates); + } + + @Override + protected void setTrustManager(SslContextBuilder sslContextBuilder, TrustManagerFactory factory) { + setTrustManagerFactoryCalled = true; + super.setTrustManager(sslContextBuilder, factory); + } + + } + @EnableAutoConfiguration @SpringBootConfiguration protected static class Config {