diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java index a67294e7a7..b94a89d536 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JdkClientHttpRequestFactoryBuilder.java @@ -22,9 +22,12 @@ import java.util.Collection; import java.util.List; import java.util.function.Consumer; +import javax.net.ssl.SSLParameters; + import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects; import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -90,11 +93,20 @@ public class JdkClientHttpRequestFactoryBuilder PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull(); map.from(settings::connectTimeout).to(builder::connectTimeout); map.from(settings::sslBundle).as(SslBundle::createSslContext).to(builder::sslContext); + map.from(settings::sslBundle).as(this::asSslParameters).to(builder::sslParameters); map.from(settings::redirects).as(this::asHttpClientRedirect).to(builder::followRedirects); this.httpClientCustomizer.accept(builder); return builder.build(); } + private SSLParameters asSslParameters(SslBundle sslBundle) { + SslOptions options = sslBundle.getOptions(); + SSLParameters parameters = new SSLParameters(); + parameters.setCipherSuites(options.getCiphers()); + parameters.setProtocols(options.getEnabledProtocols()); + return parameters; + } + private Redirect asHttpClientRedirect(Redirects redirects) { return switch (redirects) { case FOLLOW_WHEN_POSSIBLE, FOLLOW -> Redirect.NORMAL; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java index bcc44fe968..a93a67ca96 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/http/client/JettyClientHttpRequestFactoryBuilder.java @@ -33,6 +33,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects; import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslOptions; import org.springframework.http.client.JettyClientHttpRequestFactory; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -156,10 +157,19 @@ public final class JettyClientHttpRequestFactoryBuilder } private SslContextFactory.Client createSslContextFactory(SslBundle sslBundle) { + SslOptions options = sslBundle.getOptions(); SSLContext sslContext = sslBundle.createSslContext(); - SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(); - sslContextFactory.setSslContext(sslContext); - return sslContextFactory; + SslContextFactory.Client factory = new SslContextFactory.Client(); + factory.setSslContext(sslContext); + if (options.getCiphers() != null) { + factory.setIncludeCipherSuites(options.getCiphers()); + factory.setExcludeCipherSuites(); + } + if (options.getEnabledProtocols() != null) { + factory.setIncludeProtocols(options.getEnabledProtocols()); + factory.setExcludeProtocols(); + } + return factory; } private boolean followRedirects(Redirects redirects) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java index 4a117ff69c..bb9b9c2c2a 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/AbstractClientHttpRequestFactoryBuilderTests.java @@ -21,6 +21,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.Set; import java.util.function.Function; import javax.net.ssl.SSLHandshakeException; @@ -36,6 +37,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundleKey; +import org.springframework.boot.ssl.SslOptions; import org.springframework.boot.ssl.jks.JksSslStoreBundle; import org.springframework.boot.ssl.jks.JksSslStoreDetails; import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories; @@ -123,6 +125,27 @@ abstract class AbstractClientHttpRequestFactoryBuilderTests context.addServlet("test", TestServlet.class).addMapping("/")); + try { + webServer.start(); + int port = webServer.getPort(); + URI uri = new URI("https://localhost:%s".formatted(port)); + ClientHttpRequestFactory requestFactory = this.builder.build(ClientHttpRequestFactorySettings + .ofSslBundle(sslBundle(SslOptions.of(Set.of("TLS_AES_256_GCM_SHA384"), null)))); + ClientHttpRequest secureRequest = request(requestFactory, uri, httpMethod); + assertThatExceptionOfType(SSLHandshakeException.class).isThrownBy(() -> secureRequest.execute().getBody()); + } + finally { + webServer.stop(); + } + } + @ParameterizedTest @ValueSource(strings = { "GET", "POST", "PUT", "PATCH", "DELETE" }) void redirectDefault(String httpMethod) throws Exception { @@ -172,19 +195,26 @@ abstract class AbstractClientHttpRequestFactoryBuilderTests 0) { + ssl.setCiphers(ciphers); + } return ssl; } protected final SslBundle sslBundle() { + return sslBundle(SslOptions.NONE); + } + + protected final SslBundle sslBundle(SslOptions sslOptions) { JksSslStoreDetails storeDetails = JksSslStoreDetails.forLocation("classpath:test.jks"); JksSslStoreBundle stores = new JksSslStoreBundle(storeDetails, storeDetails); - return SslBundle.of(stores, SslBundleKey.of("password")); + return SslBundle.of(stores, SslBundleKey.of("password"), sslOptions); } protected HttpStatus getExpectedRedirect(HttpMethod httpMethod) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java index cf81a24c42..c111192bf6 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/ReflectiveComponentsClientHttpRequestFactoryBuilderTests.java @@ -69,6 +69,12 @@ class ReflectiveComponentsClientHttpRequestFactoryBuilderTests .withMessage("Unable to set redirect follow using reflection"); } + @Override + void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception { + assertThatIllegalStateException().isThrownBy(() -> super.connectWithSslBundleAndOptionsMismatch(httpMethod)) + .withMessage("Unable to set SSL bundler using reflection"); + } + @Test void buildWithClassCreatesFactory() { assertThat(ofTestRequestFactory().build()).isInstanceOf(TestClientHttpRequestFactory.class); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java index aefb561a3d..d771131a4c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/http/client/SimpleClientHttpRequestFactoryBuilderTests.java @@ -24,6 +24,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.test.util.ReflectionTestUtils; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + /** * Tests for {@link SimpleClientHttpRequestFactoryBuilder}. * @@ -47,6 +49,12 @@ class SimpleClientHttpRequestFactoryBuilderTests return (int) ReflectionTestUtils.getField(requestFactory, "readTimeout"); } + @Override + void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception { + assertThatIllegalStateException().isThrownBy(() -> super.connectWithSslBundleAndOptionsMismatch(httpMethod)) + .withMessage("SSL Options cannot be specified with Java connections"); + } + @ParameterizedTest @ValueSource(strings = { "GET", "POST", "PUT", "DELETE" }) @Override