Set properties from SslOptions for Jetty and JDK HTTP clients

Update `ClientHttpRequestFactoryBuilder` implementations for both
Jetty and JDK to configure properties from SslOptions.

Fixes gh-43077
This commit is contained in:
Phillip Webb 2024-11-09 00:21:01 -08:00
parent 065e7c190e
commit ede1110e36
5 changed files with 71 additions and 5 deletions

View File

@ -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;

View File

@ -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) {

View File

@ -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<T extends ClientHttp
}
}
@ParameterizedTest
@ValueSource(strings = { "GET", "POST" })
void connectWithSslBundleAndOptionsMismatch(String httpMethod) throws Exception {
TomcatServletWebServerFactory webServerFactory = new TomcatServletWebServerFactory(0);
webServerFactory.setSsl(ssl("TLS_AES_128_GCM_SHA256"));
WebServer webServer = webServerFactory
.getWebServer((context) -> 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<T extends ClientHttp
return factory.createRequest(uri, HttpMethod.valueOf(method));
}
private Ssl ssl() {
private Ssl ssl(String... ciphers) {
Ssl ssl = new Ssl();
ssl.setClientAuth(ClientAuth.NEED);
ssl.setKeyPassword("password");
ssl.setKeyStore("classpath:test.jks");
ssl.setTrustStore("classpath:test.jks");
if (ciphers.length > 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) {

View File

@ -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);

View File

@ -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