Add support for Reactor, Jetty, and JDK ClientHttpRequestFactory implementations.

Closes #901
This commit is contained in:
Mark Paluch
2025-02-18 15:00:05 +01:00
parent 87797a332f
commit bdb8ebacc4
6 changed files with 663 additions and 372 deletions

View File

@@ -0,0 +1,447 @@
/*
* Copyright 2025 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.vault.client;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProxySelector;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.*;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.SslContextBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.lang.Nullable;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.PemObject;
import org.springframework.vault.support.SslConfiguration;
/**
* Shared utility class to provide client configuration regardless of the used facade
* (synchronous or reactive).
*
* @author Mark Paluch
* @since 4.0
*/
class ClientConfiguration {
@SuppressWarnings("FieldMayBeFinal") // allow setting via reflection.
private static Log logger = LogFactory.getLog(ClientHttpRequestFactoryFactory.class);
static SSLContext getSSLContext(SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
return getSSLContext(sslConfiguration.getKeyStoreConfiguration(), sslConfiguration.getKeyConfiguration(),
getTrustManagers(sslConfiguration));
}
static SSLContext getSSLContext(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration,
SslConfiguration.KeyConfiguration keyConfiguration, @Nullable TrustManager[] trustManagers)
throws GeneralSecurityException, IOException {
KeyManager[] keyManagers = keyStoreConfiguration.isPresent()
? createKeyManagerFactory(keyStoreConfiguration, keyConfiguration).getKeyManagers() : null;
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
}
static KeyManagerFactory createKeyManagerFactory(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration,
SslConfiguration.KeyConfiguration keyConfiguration) throws GeneralSecurityException, IOException {
KeyStore keyStore = getKeyStore(keyStoreConfiguration);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
char[] keyPasswordToUse = keyConfiguration.getKeyPassword();
if (keyPasswordToUse == null) {
keyPasswordToUse = keyStoreConfiguration.getStorePassword() == null ? new char[0]
: keyStoreConfiguration.getStorePassword();
}
keyManagerFactory.init(keyStore, keyPasswordToUse);
if (StringUtils.hasText(keyConfiguration.getKeyAlias())) {
return new KeySelectingKeyManagerFactory(keyManagerFactory, keyConfiguration);
}
return keyManagerFactory;
}
static KeyStore getKeyStore(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration)
throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(getKeyStoreType(keyStoreConfiguration));
loadKeyStore(keyStoreConfiguration, keyStore);
return keyStore;
}
@Nullable
static TrustManager[] getTrustManagers(SslConfiguration sslConfiguration)
throws GeneralSecurityException, IOException {
return sslConfiguration.getTrustStoreConfiguration().isPresent()
? createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration()).getTrustManagers() : null;
}
private static String getKeyStoreType(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration) {
if (StringUtils.hasText(keyStoreConfiguration.getStoreType())
&& !SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) {
return keyStoreConfiguration.getStoreType();
}
return KeyStore.getDefaultType();
}
static TrustManagerFactory createTrustManagerFactory(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration)
throws GeneralSecurityException, IOException {
KeyStore trustStore = getKeyStore(keyStoreConfiguration);
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
return trustManagerFactory;
}
private static void loadKeyStore(SslConfiguration.KeyStoreConfiguration keyStoreConfiguration, KeyStore keyStore)
throws IOException, GeneralSecurityException {
if (logger.isDebugEnabled()) {
logger.debug("Loading keystore from %s".formatted(keyStoreConfiguration.getResource()));
}
InputStream inputStream = null;
try {
inputStream = keyStoreConfiguration.getResource().getInputStream();
if (SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) {
keyStore.load(null);
loadFromPem(keyStore, inputStream);
}
else {
keyStore.load(inputStream, keyStoreConfiguration.getStorePassword());
}
if (logger.isDebugEnabled()) {
logger.debug("Keystore loaded with %d entries".formatted(keyStore.size()));
}
}
finally {
if (inputStream != null) {
inputStream.close();
}
}
}
private static void loadFromPem(KeyStore keyStore, InputStream inputStream) throws IOException, KeyStoreException {
List<PemObject> pemObjects = PemObject.parse(new String(FileCopyUtils.copyToByteArray(inputStream)));
for (PemObject pemObject : pemObjects) {
if (pemObject.isCertificate()) {
X509Certificate cert = pemObject.getCertificate();
String alias = cert.getSubjectX500Principal().getName();
if (logger.isDebugEnabled()) {
logger.debug("Adding certificate with alias %s".formatted(alias));
}
keyStore.setCertificateEntry(alias, cert);
}
}
}
static boolean hasSslConfiguration(SslConfiguration sslConfiguration) {
return sslConfiguration.getTrustStoreConfiguration().isPresent()
|| sslConfiguration.getKeyStoreConfiguration().isPresent();
}
/**
* {@link ClientHttpConnector} for Reactor Netty.
*
* @author Mark Paluch
*/
public static class ReactorNetty {
public static HttpClient createClient(ClientOptions options, SslConfiguration sslConfiguration) {
HttpClient client = HttpClient.create();
if (hasSslConfiguration(sslConfiguration)) {
client = client.secure(builder -> {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
configureSsl(sslConfiguration, sslContextBuilder);
try {
builder.sslContext(sslContextBuilder.build());
}
catch (SSLException e) {
throw new RuntimeException(e);
}
});
}
client = client
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
Math.toIntExact(options.getConnectionTimeout().toMillis()))
.proxyWithSystemProperties();
return client;
}
private static void configureSsl(SslConfiguration sslConfiguration, SslContextBuilder sslContextBuilder) {
try {
if (sslConfiguration.getTrustStoreConfiguration().isPresent()) {
sslContextBuilder
.trustManager(createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration()));
}
if (sslConfiguration.getKeyStoreConfiguration().isPresent()) {
sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration.getKeyStoreConfiguration(),
sslConfiguration.getKeyConfiguration()));
}
if (!sslConfiguration.getEnabledProtocols().isEmpty()) {
sslContextBuilder.protocols(sslConfiguration.getEnabledProtocols());
}
if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) {
sslContextBuilder.ciphers(sslConfiguration.getEnabledCipherSuites());
}
}
catch (GeneralSecurityException | IOException e) {
throw new IllegalStateException(e);
}
}
}
/**
* Utility methods to create {@link ClientHttpRequestFactory} using the Jetty Client.
*
* @author Mark Paluch
*/
public static class JettyClient {
public static org.eclipse.jetty.client.HttpClient configureClient(
org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) {
httpClient.setConnectTimeout(options.getConnectionTimeout().toMillis());
httpClient.setAddressResolutionTimeout(options.getConnectionTimeout().toMillis());
return httpClient;
}
public static org.eclipse.jetty.client.HttpClient getHttpClient(SslConfiguration sslConfiguration)
throws IOException, GeneralSecurityException {
if (hasSslConfiguration(sslConfiguration)) {
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
if (sslConfiguration.getKeyStoreConfiguration().isPresent()) {
KeyStore keyStore = getKeyStore(sslConfiguration.getKeyStoreConfiguration());
sslContextFactory.setKeyStore(keyStore);
}
if (sslConfiguration.getTrustStoreConfiguration().isPresent()) {
KeyStore keyStore = getKeyStore(sslConfiguration.getTrustStoreConfiguration());
sslContextFactory.setTrustStore(keyStore);
}
SslConfiguration.KeyConfiguration keyConfiguration = sslConfiguration.getKeyConfiguration();
if (keyConfiguration.getKeyAlias() != null) {
sslContextFactory.setCertAlias(keyConfiguration.getKeyAlias());
}
if (keyConfiguration.getKeyPassword() != null) {
sslContextFactory.setKeyManagerPassword(new String(keyConfiguration.getKeyPassword()));
}
if (!sslConfiguration.getEnabledProtocols().isEmpty()) {
sslContextFactory
.setIncludeProtocols(sslConfiguration.getEnabledProtocols().toArray(new String[0]));
}
if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) {
sslContextFactory
.setIncludeCipherSuites(sslConfiguration.getEnabledCipherSuites().toArray(new String[0]));
}
ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
return new org.eclipse.jetty.client.HttpClient(new HttpClientTransportOverHTTP(connector));
}
return new org.eclipse.jetty.client.HttpClient();
}
}
/**
* {@link ClientHttpRequestFactory} using the JDK's HttpClient.
*
* @author Mark Paluch
*/
public static class JdkHttpClient {
public static java.net.http.HttpClient.Builder getBuilder(ClientOptions options,
SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder();
if (hasSslConfiguration(sslConfiguration)) {
SSLContext sslContext = getSSLContext(sslConfiguration);
String[] enabledProtocols = !sslConfiguration.getEnabledProtocols().isEmpty()
? sslConfiguration.getEnabledProtocols().toArray(new String[0]) : null;
String[] enabledCipherSuites = !sslConfiguration.getEnabledCipherSuites().isEmpty()
? sslConfiguration.getEnabledCipherSuites().toArray(new String[0]) : null;
SSLParameters parameters = new SSLParameters();
parameters.setProtocols(enabledProtocols);
parameters.setCipherSuites(enabledCipherSuites);
builder.sslContext(sslContext).sslParameters(parameters);
}
builder.proxy(ProxySelector.getDefault())
.followRedirects(java.net.http.HttpClient.Redirect.ALWAYS)
.connectTimeout(options.getConnectionTimeout());
return builder;
}
}
static class KeySelectingKeyManagerFactory extends KeyManagerFactory {
KeySelectingKeyManagerFactory(KeyManagerFactory factory, SslConfiguration.KeyConfiguration keyConfiguration) {
super(new KeyManagerFactorySpi() {
@Override
protected void engineInit(KeyStore keyStore, char[] chars)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
factory.init(keyStore, chars);
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
factory.init(managerFactoryParameters);
}
@Override
protected KeyManager[] engineGetKeyManagers() {
KeyManager[] keyManagers = factory.getKeyManagers();
if (keyManagers.length == 1 && keyManagers[0] instanceof X509ExtendedKeyManager) {
return new KeyManager[] { new KeySelectingX509KeyManager(
(X509ExtendedKeyManager) keyManagers[0], keyConfiguration) };
}
return keyManagers;
}
}, factory.getProvider(), factory.getAlgorithm());
}
}
private static class KeySelectingX509KeyManager extends X509ExtendedKeyManager {
private final X509ExtendedKeyManager delegate;
private final SslConfiguration.KeyConfiguration keyConfiguration;
KeySelectingX509KeyManager(X509ExtendedKeyManager delegate,
SslConfiguration.KeyConfiguration keyConfiguration) {
this.delegate = delegate;
this.keyConfiguration = keyConfiguration;
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return this.delegate.getClientAliases(keyType, issuers);
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return this.keyConfiguration.getKeyAlias();
}
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
return this.keyConfiguration.getKeyAlias();
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return this.delegate.getServerAliases(keyType, issuers);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseServerAlias(keyType, issuers, socket);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return this.delegate.getCertificateChain(alias);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return this.delegate.getPrivateKey(alias);
}
}
}

View File

@@ -18,13 +18,9 @@ package org.springframework.vault.client;
import java.io.IOException;
import java.net.ProxySelector;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.SslContextBuilder;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
@@ -33,10 +29,6 @@ import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBu
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.core5.http.nio.ssl.BasicClientTlsStrategy;
import org.apache.hc.core5.util.Timeout;
import org.eclipse.jetty.client.transport.HttpClientTransportOverHTTP;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.client.HttpClient;
import org.springframework.http.client.ClientHttpRequestFactory;
@@ -50,8 +42,6 @@ import org.springframework.util.ClassUtils;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.SslConfiguration;
import static org.springframework.vault.client.ClientHttpRequestFactoryFactory.*;
/**
* Factory for {@link ClientHttpConnector} that supports
* {@link ReactorClientHttpConnector} and {@link JettyClientHttpConnector}. This factory
@@ -85,13 +75,13 @@ public class ClientHttpConnectorFactory {
Assert.notNull(sslConfiguration, "SslConfiguration must not be null");
try {
if (reactorNettyPresent) {
return ReactorNetty.usingReactorNetty(options, sslConfiguration);
}
if (httpComponentsPresent) {
return HttpComponents.usingHttpComponents(options, sslConfiguration);
}
if (reactorNettyPresent) {
return ReactorNetty.usingReactorNetty(options, sslConfiguration);
}
if (jettyPresent) {
@@ -124,51 +114,7 @@ public class ClientHttpConnectorFactory {
}
public static HttpClient createClient(ClientOptions options, SslConfiguration sslConfiguration) {
HttpClient client = HttpClient.create();
if (hasSslConfiguration(sslConfiguration)) {
Http11SslContextSpec sslContextSpec = Http11SslContextSpec.forClient()
.configure(it -> configureSsl(sslConfiguration, it))
.get();
client = client.secure(builder -> builder.sslContext(sslContextSpec));
}
client = client
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS,
Math.toIntExact(options.getConnectionTimeout().toMillis()))
.proxyWithSystemProperties();
return client;
}
private static void configureSsl(SslConfiguration sslConfiguration, SslContextBuilder sslContextBuilder) {
try {
if (sslConfiguration.getTrustStoreConfiguration().isPresent()) {
sslContextBuilder
.trustManager(createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration()));
}
if (sslConfiguration.getKeyStoreConfiguration().isPresent()) {
sslContextBuilder.keyManager(createKeyManagerFactory(sslConfiguration.getKeyStoreConfiguration(),
sslConfiguration.getKeyConfiguration()));
}
if (!sslConfiguration.getEnabledProtocols().isEmpty()) {
sslContextBuilder.protocols(sslConfiguration.getEnabledProtocols());
}
if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) {
sslContextBuilder.ciphers(sslConfiguration.getEnabledCipherSuites());
}
}
catch (GeneralSecurityException | IOException e) {
throw new IllegalStateException(e);
}
return ClientConfiguration.ReactorNetty.createClient(options, sslConfiguration);
}
}
@@ -224,9 +170,9 @@ public class ClientHttpConnectorFactory {
.create()
.setDefaultConnectionConfig(connectionConfig);
if (hasSslConfiguration(sslConfiguration)) {
if (ClientConfiguration.hasSslConfiguration(sslConfiguration)) {
SSLContext sslContext = getSSLContext(sslConfiguration);
SSLContext sslContext = ClientConfiguration.getSSLContext(sslConfiguration);
String[] enabledProtocols = !sslConfiguration.getEnabledProtocols().isEmpty()
? sslConfiguration.getEnabledProtocols().toArray(new String[0]) : null;
@@ -278,59 +224,12 @@ public class ClientHttpConnectorFactory {
public static org.eclipse.jetty.client.HttpClient configureClient(
org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) {
httpClient.setConnectTimeout(options.getConnectionTimeout().toMillis());
httpClient.setAddressResolutionTimeout(options.getConnectionTimeout().toMillis());
return httpClient;
return ClientConfiguration.JettyClient.configureClient(httpClient, options);
}
public static org.eclipse.jetty.client.HttpClient getHttpClient(SslConfiguration sslConfiguration)
throws IOException, GeneralSecurityException {
if (hasSslConfiguration(sslConfiguration)) {
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
if (sslConfiguration.getKeyStoreConfiguration().isPresent()) {
KeyStore keyStore = ClientHttpRequestFactoryFactory
.getKeyStore(sslConfiguration.getKeyStoreConfiguration());
sslContextFactory.setKeyStore(keyStore);
}
if (sslConfiguration.getTrustStoreConfiguration().isPresent()) {
KeyStore keyStore = ClientHttpRequestFactoryFactory
.getKeyStore(sslConfiguration.getTrustStoreConfiguration());
sslContextFactory.setTrustStore(keyStore);
}
SslConfiguration.KeyConfiguration keyConfiguration = sslConfiguration.getKeyConfiguration();
if (keyConfiguration.getKeyAlias() != null) {
sslContextFactory.setCertAlias(keyConfiguration.getKeyAlias());
}
if (keyConfiguration.getKeyPassword() != null) {
sslContextFactory.setKeyManagerPassword(new String(keyConfiguration.getKeyPassword()));
}
if (!sslConfiguration.getEnabledProtocols().isEmpty()) {
sslContextFactory
.setIncludeProtocols(sslConfiguration.getEnabledProtocols().toArray(new String[0]));
}
if (!sslConfiguration.getEnabledCipherSuites().isEmpty()) {
sslContextFactory
.setIncludeCipherSuites(sslConfiguration.getEnabledCipherSuites().toArray(new String[0]));
}
ClientConnector connector = new ClientConnector();
connector.setSslContextFactory(sslContextFactory);
return new org.eclipse.jetty.client.HttpClient(new HttpClientTransportOverHTTP(connector));
}
return new org.eclipse.jetty.client.HttpClient();
return ClientConfiguration.JettyClient.getHttpClient(sslConfiguration);
}
}
@@ -360,30 +259,7 @@ public class ClientHttpConnectorFactory {
public static java.net.http.HttpClient.Builder getBuilder(ClientOptions options,
SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
java.net.http.HttpClient.Builder builder = java.net.http.HttpClient.newBuilder();
if (hasSslConfiguration(sslConfiguration)) {
SSLContext sslContext = getSSLContext(sslConfiguration);
String[] enabledProtocols = !sslConfiguration.getEnabledProtocols().isEmpty()
? sslConfiguration.getEnabledProtocols().toArray(new String[0]) : null;
String[] enabledCipherSuites = !sslConfiguration.getEnabledCipherSuites().isEmpty()
? sslConfiguration.getEnabledCipherSuites().toArray(new String[0]) : null;
SSLParameters parameters = new SSLParameters();
parameters.setProtocols(enabledProtocols);
parameters.setCipherSuites(enabledCipherSuites);
builder.sslContext(sslContext).sslParameters(parameters);
}
builder.proxy(ProxySelector.getDefault())
.followRedirects(java.net.http.HttpClient.Redirect.ALWAYS)
.connectTimeout(options.getConnectionTimeout());
return builder;
return ClientConfiguration.JdkHttpClient.getBuilder(options, sslConfiguration);
}
}

View File

@@ -27,29 +27,11 @@ import javax.net.ssl.X509ExtendedKeyManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.ProxySelector;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.X509Certificate;
import java.util.List;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.KeyManagerFactorySpi;
import javax.net.ssl.ManagerFactoryParameters;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509ExtendedKeyManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -67,17 +49,19 @@ import org.apache.hc.core5.util.Timeout;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.lang.Nullable;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.http.client.JettyClientHttpRequestFactory;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.reactive.ClientHttpConnector;
import org.springframework.http.client.reactive.JettyClientHttpConnector;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;
import org.springframework.vault.support.ClientOptions;
import org.springframework.vault.support.PemObject;
import org.springframework.vault.support.SslConfiguration;
import org.springframework.vault.support.SslConfiguration.KeyConfiguration;
import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration;
import static org.springframework.vault.client.ClientConfiguration.*;
/**
* Factory for {@link ClientHttpRequestFactory} that supports Apache HTTP Components,
@@ -95,10 +79,16 @@ public class ClientHttpRequestFactoryFactory {
@SuppressWarnings("FieldMayBeFinal") // allow setting via reflection.
private static Log logger = LogFactory.getLog(ClientHttpRequestFactoryFactory.class);
private static final boolean reactorNettyPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient",
ClientHttpConnectorFactory.class.getClassLoader());
private static final boolean httpComponentsPresent = ClassUtils.isPresent(
"org.apache.hc.client5.http.impl.classic.HttpClientBuilder",
ClientHttpRequestFactoryFactory.class.getClassLoader());
private static final boolean jettyPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient",
ClientHttpConnectorFactory.class.getClassLoader());
/**
* Create a {@link ClientHttpRequestFactory} for the given {@link ClientOptions} and
* {@link SslConfiguration}.
@@ -117,151 +107,42 @@ public class ClientHttpRequestFactoryFactory {
if (httpComponentsPresent) {
return HttpComponents.usingHttpComponents(options, sslConfiguration);
}
if (reactorNettyPresent) {
return ReactorNetty.usingReactorNetty(options, sslConfiguration);
}
if (jettyPresent) {
return JettyClient.usingJetty(options, sslConfiguration);
}
return JdkHttpClient.usingJdkHttpClient(options, sslConfiguration);
}
catch (GeneralSecurityException | IOException e) {
throw new IllegalStateException(e);
}
}
if (hasSslConfiguration(sslConfiguration)) {
logger.warn("VaultProperties has SSL configured but the SSL configuration "
+ "must be applied outside the Vault Client to use the JDK HTTP client");
/**
* {@link ClientHttpConnector} for Reactor Netty.
*
* @author Mark Paluch
* @since 4.0
*/
public static class ReactorNetty {
/**
* Create a {@link ReactorClientHttpRequestFactory} using Reactor Netty.
* @param options must not be {@literal null}
* @param sslConfiguration must not be {@literal null}
* @return a new and configured {@link ReactorClientHttpRequestFactory} instance.
*/
public static ReactorClientHttpRequestFactory usingReactorNetty(ClientOptions options,
SslConfiguration sslConfiguration) {
return new ReactorClientHttpRequestFactory(
ClientConfiguration.ReactorNetty.createClient(options, sslConfiguration));
}
return SimpleClient.usingSimpleClientHttpRequest(options);
}
static SSLContext getSSLContext(SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
return getSSLContext(sslConfiguration.getKeyStoreConfiguration(), sslConfiguration.getKeyConfiguration(),
getTrustManagers(sslConfiguration));
}
static SSLContext getSSLContext(KeyStoreConfiguration keyStoreConfiguration, KeyConfiguration keyConfiguration,
@Nullable TrustManager[] trustManagers) throws GeneralSecurityException, IOException {
KeyManager[] keyManagers = keyStoreConfiguration.isPresent()
? createKeyManagerFactory(keyStoreConfiguration, keyConfiguration).getKeyManagers() : null;
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, null);
return sslContext;
}
static KeyManagerFactory createKeyManagerFactory(KeyStoreConfiguration keyStoreConfiguration,
KeyConfiguration keyConfiguration) throws GeneralSecurityException, IOException {
KeyStore keyStore = getKeyStore(keyStoreConfiguration);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
char[] keyPasswordToUse = keyConfiguration.getKeyPassword();
if (keyPasswordToUse == null) {
keyPasswordToUse = keyStoreConfiguration.getStorePassword() == null ? new char[0]
: keyStoreConfiguration.getStorePassword();
}
keyManagerFactory.init(keyStore, keyPasswordToUse);
if (StringUtils.hasText(keyConfiguration.getKeyAlias())) {
return new KeySelectingKeyManagerFactory(keyManagerFactory, keyConfiguration);
}
return keyManagerFactory;
}
static KeyStore getKeyStore(KeyStoreConfiguration keyStoreConfiguration)
throws IOException, GeneralSecurityException {
KeyStore keyStore = KeyStore.getInstance(getKeyStoreType(keyStoreConfiguration));
loadKeyStore(keyStoreConfiguration, keyStore);
return keyStore;
}
@Nullable
static TrustManager[] getTrustManagers(SslConfiguration sslConfiguration)
throws GeneralSecurityException, IOException {
return sslConfiguration.getTrustStoreConfiguration().isPresent()
? createTrustManagerFactory(sslConfiguration.getTrustStoreConfiguration()).getTrustManagers() : null;
}
private static String getKeyStoreType(KeyStoreConfiguration keyStoreConfiguration) {
if (StringUtils.hasText(keyStoreConfiguration.getStoreType())
&& !SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) {
return keyStoreConfiguration.getStoreType();
}
return KeyStore.getDefaultType();
}
static TrustManagerFactory createTrustManagerFactory(KeyStoreConfiguration keyStoreConfiguration)
throws GeneralSecurityException, IOException {
KeyStore trustStore = getKeyStore(keyStoreConfiguration);
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
return trustManagerFactory;
}
private static void loadKeyStore(KeyStoreConfiguration keyStoreConfiguration, KeyStore keyStore)
throws IOException, GeneralSecurityException {
if (logger.isDebugEnabled()) {
logger.debug("Loading keystore from %s".formatted(keyStoreConfiguration.getResource()));
}
InputStream inputStream = null;
try {
inputStream = keyStoreConfiguration.getResource().getInputStream();
if (SslConfiguration.PEM_KEYSTORE_TYPE.equalsIgnoreCase(keyStoreConfiguration.getStoreType())) {
keyStore.load(null);
loadFromPem(keyStore, inputStream);
}
else {
keyStore.load(inputStream, keyStoreConfiguration.getStorePassword());
}
if (logger.isDebugEnabled()) {
logger.debug("Keystore loaded with %d entries".formatted(keyStore.size()));
}
}
finally {
if (inputStream != null) {
inputStream.close();
}
}
}
private static void loadFromPem(KeyStore keyStore, InputStream inputStream) throws IOException, KeyStoreException {
List<PemObject> pemObjects = PemObject.parse(new String(FileCopyUtils.copyToByteArray(inputStream)));
for (PemObject pemObject : pemObjects) {
if (pemObject.isCertificate()) {
X509Certificate cert = pemObject.getCertificate();
String alias = cert.getSubjectX500Principal().getName();
if (logger.isDebugEnabled()) {
logger.debug("Adding certificate with alias %s".formatted(alias));
}
keyStore.setCertificateEntry(alias, cert);
}
}
}
static boolean hasSslConfiguration(SslConfiguration sslConfiguration) {
return sslConfiguration.getTrustStoreConfiguration().isPresent()
|| sslConfiguration.getKeyStoreConfiguration().isPresent();
}
/**
@@ -318,9 +199,9 @@ public class ClientHttpRequestFactoryFactory {
.setSoTimeout(readTimeout)
.build());
if (hasSslConfiguration(sslConfiguration)) {
if (ClientConfiguration.hasSslConfiguration(sslConfiguration)) {
SSLContext sslContext = getSSLContext(sslConfiguration);
SSLContext sslContext = ClientConfiguration.getSSLContext(sslConfiguration);
String[] enabledProtocols = null;
@@ -347,83 +228,66 @@ public class ClientHttpRequestFactoryFactory {
}
static class KeySelectingKeyManagerFactory extends KeyManagerFactory {
/**
* Utility methods to create {@link ClientHttpRequestFactory} using the Jetty Client.
*
* @author Mark Paluch
* @since 4.5
*/
public static class JettyClient {
KeySelectingKeyManagerFactory(KeyManagerFactory factory, KeyConfiguration keyConfiguration) {
super(new KeyManagerFactorySpi() {
@Override
protected void engineInit(KeyStore keyStore, char[] chars)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
factory.init(keyStore, chars);
}
/**
* Create a {@link JettyClientHttpRequestFactory} using Jetty.
* @param options must not be {@literal null}
* @param sslConfiguration must not be {@literal null}
* @return a new and configured {@link JettyClientHttpConnector} instance.
* @throws GeneralSecurityException
* @throws IOException
*/
public static JettyClientHttpRequestFactory usingJetty(ClientOptions options, SslConfiguration sslConfiguration)
throws GeneralSecurityException, IOException {
return new JettyClientHttpRequestFactory(configureClient(getHttpClient(sslConfiguration), options));
}
@Override
protected void engineInit(ManagerFactoryParameters managerFactoryParameters)
throws InvalidAlgorithmParameterException {
factory.init(managerFactoryParameters);
}
public static org.eclipse.jetty.client.HttpClient configureClient(
org.eclipse.jetty.client.HttpClient httpClient, ClientOptions options) {
return ClientConfiguration.JettyClient.configureClient(httpClient, options);
}
@Override
protected KeyManager[] engineGetKeyManagers() {
KeyManager[] keyManagers = factory.getKeyManagers();
if (keyManagers.length == 1 && keyManagers[0] instanceof X509ExtendedKeyManager) {
return new KeyManager[] { new KeySelectingX509KeyManager(
(X509ExtendedKeyManager) keyManagers[0], keyConfiguration) };
}
return keyManagers;
}
}, factory.getProvider(), factory.getAlgorithm());
public static org.eclipse.jetty.client.HttpClient getHttpClient(SslConfiguration sslConfiguration)
throws IOException, GeneralSecurityException {
return ClientConfiguration.JettyClient.getHttpClient(sslConfiguration);
}
}
private static class KeySelectingX509KeyManager extends X509ExtendedKeyManager {
/**
* {@link ClientHttpRequestFactory} using the JDK's HttpClient.
*
* @author Mark Paluch
* @since 4.5
*/
public static class JdkHttpClient {
private final X509ExtendedKeyManager delegate;
/**
* Create a {@link JdkClientHttpRequestFactory} using the JDK's HttpClient.
* @param options must not be {@literal null}
* @param sslConfiguration must not be {@literal null}
* @return a new and configured {@link JdkClientHttpRequestFactory} instance.
* @throws GeneralSecurityException
* @throws IOException
*/
public static JdkClientHttpRequestFactory usingJdkHttpClient(ClientOptions options,
SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
private final KeyConfiguration keyConfiguration;
java.net.http.HttpClient.Builder builder = getBuilder(options, sslConfiguration);
KeySelectingX509KeyManager(X509ExtendedKeyManager delegate, KeyConfiguration keyConfiguration) {
this.delegate = delegate;
this.keyConfiguration = keyConfiguration;
return new JdkClientHttpRequestFactory(builder.build());
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
return this.delegate.getClientAliases(keyType, issuers);
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
return this.keyConfiguration.getKeyAlias();
}
public String chooseEngineClientAlias(String[] keyType, Principal[] issuers, SSLEngine engine) {
return this.keyConfiguration.getKeyAlias();
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return this.delegate.getServerAliases(keyType, issuers);
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return this.delegate.chooseServerAlias(keyType, issuers, socket);
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
return this.delegate.getCertificateChain(alias);
}
@Override
public PrivateKey getPrivateKey(String alias) {
return this.delegate.getPrivateKey(alias);
public static java.net.http.HttpClient.Builder getBuilder(ClientOptions options,
SslConfiguration sslConfiguration) throws GeneralSecurityException, IOException {
return ClientConfiguration.JdkHttpClient.getBuilder(options, sslConfiguration);
}
}

View File

@@ -46,6 +46,52 @@ class ClientHttpRequestFactoryFactoryIntegrationTests {
String url = new VaultEndpoint().createUriString("sys/health");
@Test
void reactorNettyClientShouldWork() {
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.ReactorNetty
.usingReactorNetty(new ClientOptions(), Settings.createSslConfiguration());
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
@Test
void reactorNettyClientWithExplicitEnabledCipherSuitesShouldWork() {
List<String> enabledCipherSuites = new ArrayList<String>();
enabledCipherSuites.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
enabledCipherSuites.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.ReactorNetty.usingReactorNetty(
new ClientOptions(), Settings.createSslConfiguration().withEnabledCipherSuites(enabledCipherSuites));
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
@Test
void reactorNettyClientWithExplicitEnabledProtocolsShouldWork() {
List<String> enabledProtocols = new ArrayList<String>();
enabledProtocols.add("TLSv1.2");
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.ReactorNetty.usingReactorNetty(
new ClientOptions(), Settings.createSslConfiguration().withEnabledProtocols(enabledProtocols));
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
@Test
void httpComponentsClientShouldWork() throws Exception {
@@ -117,6 +163,65 @@ class ClientHttpRequestFactoryFactoryIntegrationTests {
((DisposableBean) factory).destroy();
}
@Test
void jettyClientShouldWork() throws Exception {
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JettyClient.usingJetty(new ClientOptions(),
Settings.createSslConfiguration());
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
@Test
void jettyClientWithExplicitEnabledCipherSuitesShouldWork() throws Exception {
List<String> enabledCipherSuites = new ArrayList<String>();
enabledCipherSuites.add("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384");
enabledCipherSuites.add("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JettyClient.usingJetty(new ClientOptions(),
Settings.createSslConfiguration().withEnabledCipherSuites(enabledCipherSuites));
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
@Test
void jettyClientWithExplicitEnabledProtocolsShouldWork() throws Exception {
List<String> enabledProtocols = new ArrayList<String>();
enabledProtocols.add("TLSv1.2");
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JettyClient.usingJetty(new ClientOptions(),
Settings.createSslConfiguration().withEnabledProtocols(enabledProtocols));
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
@Test
void jdkHttpClientShouldWork() throws Exception {
ClientHttpRequestFactory factory = ClientHttpRequestFactoryFactory.JdkHttpClient
.usingJdkHttpClient(new ClientOptions(), Settings.createSslConfiguration());
RestTemplate template = new RestTemplate(factory);
String response = request(template);
assertThat(response).isNotNull().contains("initialized");
}
private String request(RestTemplate template) {
// Uninitialized and sealed can cause status 500

View File

@@ -76,6 +76,8 @@ class ReactiveVaultKeyValueMetadataTemplateIntegrationTests
@Test
void shouldReadMetadataForANewKVEntry() {
Version version = prepare().getVersion();
vaultKeyValueMetadataOperations.get(SECRET_NAME).as(StepVerifier::create).assertNext(metadataResponse -> {
assertThat(metadataResponse.getMaxVersions()).isEqualTo(0);
assertThat(metadataResponse.getCurrentVersion()).isEqualTo(1);
@@ -86,7 +88,7 @@ class ReactiveVaultKeyValueMetadataTemplateIntegrationTests
var version1 = metadataResponse.getVersions().get(0);
if (prepare().getVersion().isGreaterThanOrEqualTo(Version.parse("1.2.0"))) {
if (version.isGreaterThanOrEqualTo(Version.parse("1.2.0"))) {
assertThat(metadataResponse.getDeleteVersionAfter()).isEqualTo(Duration.ZERO);

View File

@@ -8,28 +8,25 @@ that is scoped only to Spring Vault's client components.
Spring Vault supports following HTTP imperative clients:
* Java's builtin `HttpURLConnection` (default client if no other is available)
* Java's builtin `HttpClient` (default client if no other is available)
* Apache Http Components
* Reactor Netty
* Jetty
Spring Vault's reactive integration supports the following reactive HTTP clients:
* Java's builtin reactive `HttpClient` (default client if no other is available)
* Reactor Netty
* Apache Http Components
* Reactor Netty
* Jetty
Using a specific client requires the according dependency to be available on the classpath
so Spring Vault can use the available client for communicating with Vault.
== Java's builtin `HttpURLConnection`
== Java's builtin `HttpClient`
Java's builtin `HttpURLConnection` is available out-of-the-box without additional
configuration. Using `HttpURLConnection` comes with a limitation regarding SSL configuration.
Spring Vault won't apply <<vault.client-ssl,customized SSL configuration>> as it would
require a deep reconfiguration of the JVM. This configuration would affect all
components relying on the default SSL context. Configuring SSL settings using
`HttpURLConnection` requires you providing these settings as System Properties. See
https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#InstallationAndCustomization[Customizing JSSE] for further details.
Java's builtin `HttpClient` is available out-of-the-box since Java 11 without additional
dependencies.
== External Clients
You can use external clients to access Vault's API. Simply add one of the following