diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java index 8c99316de5..f9c15eaa99 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -133,10 +133,16 @@ public class ElasticsearchProperties { private final Sniffer sniffer = new Sniffer(); + private final Ssl ssl = new Ssl(); + public Sniffer getSniffer() { return this.sniffer; } + public Ssl getSsl() { + return this.ssl; + } + public static class Sniffer { /** @@ -167,6 +173,23 @@ public class ElasticsearchProperties { } + public static class Ssl { + + /** + * SSL bundle name. + */ + private String bundle; + + public String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + + } + } } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java index bbf626f07e..52351b1726 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientBuilderConfiguration; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientConfiguration; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.RestClientSnifferConfiguration; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Import; @@ -34,7 +35,7 @@ import org.springframework.context.annotation.Import; * @author Stephane Nicoll * @since 2.1.0 */ -@AutoConfiguration +@AutoConfiguration(after = SslAutoConfiguration.class) @ConditionalOnClass(RestClientBuilder.class) @EnableConfigurationProperties(ElasticsearchProperties.class) @Import({ RestClientBuilderConfiguration.class, RestClientConfiguration.class, RestClientSnifferConfiguration.class }) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java index 193827aed8..f22fcef11c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientConfigurations.java @@ -19,8 +19,12 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.net.URI; import java.time.Duration; import java.util.List; +import java.util.Set; import java.util.stream.Stream; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; @@ -29,6 +33,7 @@ import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; import org.apache.http.impl.nio.reactor.IOReactorConfig; +import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy; import org.elasticsearch.client.RestClient; import org.elasticsearch.client.RestClientBuilder; import org.elasticsearch.client.sniff.Sniffer; @@ -41,6 +46,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandi import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.ssl.SslOptions; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; @@ -81,13 +89,17 @@ class ElasticsearchRestClientConfigurations { @Bean RestClientBuilder elasticsearchRestClientBuilder(ElasticsearchConnectionDetails connectionDetails, - ObjectProvider builderCustomizers) { + ObjectProvider builderCustomizers, ObjectProvider sslBundles) { RestClientBuilder builder = RestClient.builder(connectionDetails.getNodes() .stream() .map((node) -> new HttpHost(node.hostname(), node.port(), node.protocol().getScheme())) .toArray(HttpHost[]::new)); builder.setHttpClientConfigCallback((httpClientBuilder) -> { builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(httpClientBuilder)); + String sslBundleName = this.properties.getRestclient().getSsl().getBundle(); + if (StringUtils.hasText(sslBundleName)) { + configureSsl(httpClientBuilder, sslBundles.getObject().getBundle(sslBundleName)); + } return httpClientBuilder; }); builder.setRequestConfigCallback((requestConfigBuilder) -> { @@ -102,6 +114,19 @@ class ElasticsearchRestClientConfigurations { return builder; } + private void configureSsl(HttpAsyncClientBuilder httpClientBuilder, SslBundle sslBundle) { + SSLContext sslcontext = sslBundle.createSslContext(); + SslOptions sslOptions = sslBundle.getOptions(); + String[] enabledProtocols = toArray(sslOptions.getEnabledProtocols()); + String[] ciphers = toArray(sslOptions.getCiphers()); + httpClientBuilder.setSSLStrategy( + new SSLIOSessionStrategy(sslcontext, enabledProtocols, ciphers, (HostnameVerifier) null)); + } + + private static String[] toArray(Set set) { + return (set != null) ? set.toArray(String[]::new) : null; + } + } @Configuration(proxyBeanMethods = false) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java index 18d1d1e28e..25b0da05ae 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/elasticsearch/ElasticsearchRestClientAutoConfigurationTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.autoconfigure.elasticsearch; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import org.apache.http.HttpHost; @@ -24,7 +25,9 @@ import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; import org.apache.http.impl.nio.client.HttpAsyncClientBuilder; +import org.apache.http.nio.conn.SchemeIOSessionStrategy; import org.assertj.core.api.InstanceOfAssertFactories; import org.elasticsearch.client.Node; import org.elasticsearch.client.RestClient; @@ -35,10 +38,12 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchConnectionDetails.Node.Protocol; import org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientConfigurations.PropertiesElasticsearchConnectionDetails; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.then; @@ -57,8 +62,8 @@ import static org.mockito.Mockito.mock; */ class ElasticsearchRestClientAutoConfigurationTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class)); + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(ElasticsearchRestClientAutoConfiguration.class, SslAutoConfiguration.class)); @Test void configureShouldCreateRestClientBuilderAndRestClient() { @@ -282,6 +287,32 @@ class ElasticsearchRestClientAutoConfigurationTests { }); } + @Test + @SuppressWarnings("unchecked") + void configureWithSslBundle() { + List properties = new ArrayList<>(); + properties.add("spring.elasticsearch.restclient.ssl.bundle=mybundle"); + properties.add("spring.ssl.bundle.jks.mybundle.truststore.location=classpath:test.jks"); + properties.add("spring.ssl.bundle.jks.mybundle.options.ciphers=DESede"); + properties.add("spring.ssl.bundle.jks.mybundle.options.enabled-protocols=TLSv1.3"); + this.contextRunner.withPropertyValues(properties.toArray(String[]::new)).run((context) -> { + assertThat(context).hasSingleBean(RestClient.class); + RestClient restClient = context.getBean(RestClient.class); + Object client = ReflectionTestUtils.getField(restClient, "client"); + Object connmgr = ReflectionTestUtils.getField(client, "connmgr"); + Registry registry = (Registry) ReflectionTestUtils + .getField(connmgr, "ioSessionFactoryRegistry"); + SchemeIOSessionStrategy strategy = registry.lookup("https"); + assertThat(strategy).extracting("sslContext").isNotNull(); + assertThat(strategy).extracting("supportedCipherSuites") + .asInstanceOf(InstanceOfAssertFactories.ARRAY) + .containsExactly("DESede"); + assertThat(strategy).extracting("supportedProtocols") + .asInstanceOf(InstanceOfAssertFactories.ARRAY) + .containsExactly("TLSv1.3"); + }); + } + @Configuration(proxyBeanMethods = false) static class ConnectionDetailsConfiguration {