diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java index 5f0e050ef4..25a4737ec0 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java @@ -22,6 +22,7 @@ public final class DefaultJedisClientConfig implements JedisClientConfig { private final boolean ssl; private final SSLSocketFactory sslSocketFactory; private final SSLParameters sslParameters; + private final SslOptions sslOptions; private final HostnameVerifier hostnameVerifier; private final HostAndPortMapper hostAndPortMapper; @@ -32,29 +33,23 @@ public final class DefaultJedisClientConfig implements JedisClientConfig { private final AuthXManager authXManager; - private DefaultJedisClientConfig(RedisProtocol protocol, int connectionTimeoutMillis, - int soTimeoutMillis, int blockingSocketTimeoutMillis, - Supplier credentialsProvider, int database, String clientName, boolean ssl, - SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, - HostnameVerifier hostnameVerifier, HostAndPortMapper hostAndPortMapper, - ClientSetInfoConfig clientSetInfoConfig, boolean readOnlyForRedisClusterReplicas, - AuthXManager authXManager) { - this.redisProtocol = protocol; - this.connectionTimeoutMillis = connectionTimeoutMillis; - this.socketTimeoutMillis = soTimeoutMillis; - this.blockingSocketTimeoutMillis = blockingSocketTimeoutMillis; - this.credentialsProvider = credentialsProvider; - this.database = database; - this.clientName = clientName; - this.ssl = ssl; - this.sslSocketFactory = sslSocketFactory; - this.sslParameters = sslParameters; - this.hostnameVerifier = hostnameVerifier; - this.hostAndPortMapper = hostAndPortMapper; - this.clientSetInfoConfig = clientSetInfoConfig; - this.readOnlyForRedisClusterReplicas = readOnlyForRedisClusterReplicas; - this.authXManager = authXManager; - + private DefaultJedisClientConfig(DefaultJedisClientConfig.Builder builder) { + this.redisProtocol = builder.redisProtocol; + this.connectionTimeoutMillis = builder.connectionTimeoutMillis; + this.socketTimeoutMillis = builder.socketTimeoutMillis; + this.blockingSocketTimeoutMillis = builder.blockingSocketTimeoutMillis; + this.credentialsProvider = builder.credentialsProvider; + this.database = builder.database; + this.clientName = builder.clientName; + this.ssl = builder.ssl; + this.sslSocketFactory = builder.sslSocketFactory; + this.sslParameters = builder.sslParameters; + this.sslOptions = builder.sslOptions; + this.hostnameVerifier = builder.hostnameVerifier; + this.hostAndPortMapper = builder.hostAndPortMapper; + this.clientSetInfoConfig = builder.clientSetInfoConfig; + this.readOnlyForRedisClusterReplicas = builder.readOnlyForRedisClusterReplicas; + this.authXManager = builder.authXManager; } @Override @@ -123,6 +118,11 @@ public SSLParameters getSslParameters() { return sslParameters; } + @Override + public SslOptions getSslOptions() { + return sslOptions; + } + @Override public HostnameVerifier getHostnameVerifier() { return hostnameVerifier; @@ -164,6 +164,7 @@ public static class Builder { private boolean ssl = false; private SSLSocketFactory sslSocketFactory = null; private SSLParameters sslParameters = null; + private SslOptions sslOptions = null; private HostnameVerifier hostnameVerifier = null; private HostAndPortMapper hostAndPortMapper = null; @@ -172,7 +173,7 @@ public static class Builder { private boolean readOnlyForRedisClusterReplicas = false; - private AuthXManager authXManager; + private AuthXManager authXManager = null; private Builder() { } @@ -183,15 +184,13 @@ public DefaultJedisClientConfig build() { new DefaultRedisCredentials(user, password)); } - return new DefaultJedisClientConfig(redisProtocol, connectionTimeoutMillis, - socketTimeoutMillis, blockingSocketTimeoutMillis, credentialsProvider, database, - clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, - clientSetInfoConfig, readOnlyForRedisClusterReplicas, authXManager); + return new DefaultJedisClientConfig(this); } /** * Shortcut to {@link redis.clients.jedis.DefaultJedisClientConfig.Builder#protocol(RedisProtocol)} with * {@link RedisProtocol#RESP3}. + * @return this */ public Builder resp3() { return protocol(RedisProtocol.RESP3); @@ -268,6 +267,11 @@ public Builder sslParameters(SSLParameters sslParameters) { return this; } + public Builder sslOptions(SslOptions sslOptions) { + this.sslOptions = sslOptions; + return this; + } + public Builder hostnameVerifier(HostnameVerifier hostnameVerifier) { this.hostnameVerifier = hostnameVerifier; return this; @@ -304,6 +308,7 @@ public Builder from(JedisClientConfig instance) { this.ssl = instance.isSsl(); this.sslSocketFactory = instance.getSslSocketFactory(); this.sslParameters = instance.getSslParameters(); + this.sslOptions = instance.getSslOptions(); this.hostnameVerifier = instance.getHostnameVerifier(); this.hostAndPortMapper = instance.getHostAndPortMapper(); this.clientSetInfoConfig = instance.getClientSetInfoConfig(); @@ -313,24 +318,64 @@ public Builder from(JedisClientConfig instance) { } } + /** + * @deprecated Use {@link redis.clients.jedis.DefaultJedisClientConfig.Builder}. + */ + @Deprecated public static DefaultJedisClientConfig create(int connectionTimeoutMillis, int soTimeoutMillis, - int blockingSocketTimeoutMillis, String user, String password, int database, - String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, - SSLParameters sslParameters, HostnameVerifier hostnameVerifier, - HostAndPortMapper hostAndPortMapper, AuthXManager authXManager) { - return new DefaultJedisClientConfig(null, connectionTimeoutMillis, soTimeoutMillis, - blockingSocketTimeoutMillis, - new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(user, password)), database, - clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, null, - false, authXManager); + int blockingSocketTimeoutMillis, String user, String password, int database, String clientName, + boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, + HostnameVerifier hostnameVerifier, HostAndPortMapper hostAndPortMapper) { + Builder builder = builder(); + builder.connectionTimeoutMillis(connectionTimeoutMillis).socketTimeoutMillis(soTimeoutMillis) + .blockingSocketTimeoutMillis(blockingSocketTimeoutMillis); + if (user != null || password != null) { + // deliberately not handling 'user != null && password == null' here + builder.credentials(new DefaultRedisCredentials(user, password)); + } + builder.database(database).clientName(clientName); + builder.ssl(ssl).sslSocketFactory(sslSocketFactory).sslParameters(sslParameters).hostnameVerifier(hostnameVerifier); + builder.hostAndPortMapper(hostAndPortMapper); + return builder.build(); } + /** + * @deprecated Use + * {@link redis.clients.jedis.DefaultJedisClientConfig.Builder#from(redis.clients.jedis.JedisClientConfig)}. + */ + @Deprecated public static DefaultJedisClientConfig copyConfig(JedisClientConfig copy) { - return new DefaultJedisClientConfig(copy.getRedisProtocol(), copy.getConnectionTimeoutMillis(), - copy.getSocketTimeoutMillis(), copy.getBlockingSocketTimeoutMillis(), - copy.getCredentialsProvider(), copy.getDatabase(), copy.getClientName(), copy.isSsl(), - copy.getSslSocketFactory(), copy.getSslParameters(), copy.getHostnameVerifier(), - copy.getHostAndPortMapper(), copy.getClientSetInfoConfig(), - copy.isReadOnlyForRedisClusterReplicas(), copy.getAuthXManager()); + Builder builder = builder(); + builder.protocol(copy.getRedisProtocol()); + builder.connectionTimeoutMillis(copy.getConnectionTimeoutMillis()); + builder.socketTimeoutMillis(copy.getSocketTimeoutMillis()); + builder.blockingSocketTimeoutMillis(copy.getBlockingSocketTimeoutMillis()); + + Supplier credentialsProvider = copy.getCredentialsProvider(); + if (credentialsProvider != null) { + builder.credentialsProvider(credentialsProvider); + } else { + builder.user(copy.getUser()); + builder.password(copy.getPassword()); + } + + builder.database(copy.getDatabase()); + builder.clientName(copy.getClientName()); + + builder.ssl(copy.isSsl()); + builder.sslSocketFactory(copy.getSslSocketFactory()); + builder.sslParameters(copy.getSslParameters()); + builder.hostnameVerifier(copy.getHostnameVerifier()); + builder.sslOptions(copy.getSslOptions()); + builder.hostAndPortMapper(copy.getHostAndPortMapper()); + + builder.clientSetInfoConfig(copy.getClientSetInfoConfig()); + if (copy.isReadOnlyForRedisClusterReplicas()) { + builder.readOnlyForRedisClusterReplicas(); + } + + builder.authXManager(copy.getAuthXManager()); + + return builder.build(); } } diff --git a/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java b/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java index 0d41693d0f..1e88f21f32 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisSocketFactory.java @@ -1,12 +1,15 @@ package redis.clients.jedis; +import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.security.GeneralSecurityException; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; @@ -24,6 +27,7 @@ public class DefaultJedisSocketFactory implements JedisSocketFactory { private int socketTimeout = Protocol.DEFAULT_TIMEOUT; private boolean ssl = false; private SSLSocketFactory sslSocketFactory = null; + private SslOptions sslOptions = null; private SSLParameters sslParameters = null; private HostnameVerifier hostnameVerifier = null; private HostAndPortMapper hostAndPortMapper = null; @@ -49,6 +53,7 @@ public DefaultJedisSocketFactory(HostAndPort hostAndPort, JedisClientConfig conf this.ssl = config.isSsl(); this.sslSocketFactory = config.getSslSocketFactory(); this.sslParameters = config.getSslParameters(); + this.sslOptions = config.getSslOptions(); this.hostnameVerifier = config.getHostnameVerifier(); this.hostAndPortMapper = config.getHostAndPortMapper(); } @@ -89,25 +94,8 @@ public Socket createSocket() throws JedisConnectionException { socket = connectToFirstSuccessfulHost(_hostAndPort); socket.setSoTimeout(socketTimeout); - if (ssl) { - SSLSocketFactory _sslSocketFactory = this.sslSocketFactory; - if (null == _sslSocketFactory) { - _sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); - } - Socket plainSocket = socket; - socket = _sslSocketFactory.createSocket(socket, _hostAndPort.getHost(), _hostAndPort.getPort(), true); - - if (null != sslParameters) { - ((SSLSocket) socket).setSSLParameters(sslParameters); - } - socket = new SSLSocketWrapper((SSLSocket) socket, plainSocket); - - if (null != hostnameVerifier - && !hostnameVerifier.verify(_hostAndPort.getHost(), ((SSLSocket) socket).getSession())) { - String message = String.format( - "The connection to '%s' failed ssl/tls hostname verification.", _hostAndPort.getHost()); - throw new JedisConnectionException(message); - } + if (ssl || sslOptions != null) { + socket = createSslSocket(_hostAndPort, socket); } return socket; @@ -122,6 +110,50 @@ public Socket createSocket() throws JedisConnectionException { } } + /** + * ssl enable check is done before this. + */ + private Socket createSslSocket(HostAndPort _hostAndPort, Socket socket) throws IOException, GeneralSecurityException { + + Socket plainSocket = socket; + + SSLSocketFactory _sslSocketFactory; + SSLParameters _sslParameters; + + if (sslOptions != null) { + + SSLContext _sslContext = sslOptions.createSslContext(); + _sslSocketFactory = _sslContext.getSocketFactory(); + + _sslParameters = sslOptions.getSslParameters(); + + } else { + + _sslSocketFactory = this.sslSocketFactory; + _sslParameters = this.sslParameters; + } + + if (_sslSocketFactory == null) { + _sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + } + + SSLSocket sslSocket = (SSLSocket) _sslSocketFactory.createSocket(socket, + _hostAndPort.getHost(), _hostAndPort.getPort(), true); + + if (_sslParameters != null) { + sslSocket.setSSLParameters(_sslParameters); + } + + // allowing HostnameVerifier for both SslOptions and legacy ssl config + if (hostnameVerifier != null && !hostnameVerifier.verify(_hostAndPort.getHost(), sslSocket.getSession())) { + String message = String.format("The connection to '%s' failed ssl/tls hostname verification.", + _hostAndPort.getHost()); + throw new JedisConnectionException(message); + } + + return new SSLSocketWrapper(sslSocket, plainSocket); + } + public void updateHostAndPort(HostAndPort hostAndPort) { this.hostAndPort = hostAndPort; } diff --git a/src/main/java/redis/clients/jedis/JedisClientConfig.java b/src/main/java/redis/clients/jedis/JedisClientConfig.java index 82e9eb8e7f..ce7fd82de4 100644 --- a/src/main/java/redis/clients/jedis/JedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java @@ -46,6 +46,7 @@ default String getPassword() { return null; } + // TODO: return null default Supplier getCredentialsProvider() { return new DefaultRedisCredentialsProvider( new DefaultRedisCredentials(getUser(), getPassword())); @@ -78,6 +79,16 @@ default SSLParameters getSslParameters() { return null; } + /** + * {@link JedisClientConfig#isSsl()}, {@link JedisClientConfig#getSslSocketFactory()} and + * {@link JedisClientConfig#getSslParameters()} will be ignored if + * {@link JedisClientConfig#getSslOptions() this} is set. + * @return ssl options + */ + default SslOptions getSslOptions() { + return null; + } + default HostnameVerifier getHostnameVerifier() { return null; } diff --git a/src/main/java/redis/clients/jedis/JedisPooled.java b/src/main/java/redis/clients/jedis/JedisPooled.java index c735fda7c1..dc7d231450 100644 --- a/src/main/java/redis/clients/jedis/JedisPooled.java +++ b/src/main/java/redis/clients/jedis/JedisPooled.java @@ -292,10 +292,12 @@ public JedisPooled(final GenericObjectPoolConfig poolConfig, final S } public JedisPooled(final GenericObjectPoolConfig poolConfig, final String host, int port, - final int connectionTimeout, final int soTimeout, final int infiniteSoTimeout, - final String user, final String password, final int database, final String clientName) { - this(new HostAndPort(host, port), DefaultJedisClientConfig.create(connectionTimeout, soTimeout, - infiniteSoTimeout, user, password, database, clientName, false, null, null, null, null, null), + final int connectionTimeout, final int soTimeout, final int infiniteSoTimeout, final String user, + final String password, final int database, final String clientName) { + this(new HostAndPort(host, port), + DefaultJedisClientConfig.builder().connectionTimeoutMillis(connectionTimeout).socketTimeoutMillis(soTimeout) + .blockingSocketTimeoutMillis(infiniteSoTimeout).user(user).password(password).database(database) + .clientName(clientName).build(), poolConfig); } @@ -304,9 +306,12 @@ public JedisPooled(final GenericObjectPoolConfig poolConfig, final S final String password, final int database, final String clientName, final boolean ssl, final SSLSocketFactory sslSocketFactory, final SSLParameters sslParameters, final HostnameVerifier hostnameVerifier) { - this(new HostAndPort(host, port), DefaultJedisClientConfig.create(connectionTimeout, soTimeout, - infiniteSoTimeout, user, password, database, clientName, ssl, sslSocketFactory, sslParameters, - hostnameVerifier, null, null), poolConfig); + this(new HostAndPort(host, port), + DefaultJedisClientConfig.builder().connectionTimeoutMillis(connectionTimeout).socketTimeoutMillis(soTimeout) + .blockingSocketTimeoutMillis(infiniteSoTimeout).user(user).password(password).database(database) + .clientName(clientName).ssl(ssl).sslSocketFactory(sslSocketFactory).sslParameters(sslParameters) + .hostnameVerifier(hostnameVerifier).build(), + poolConfig); } public JedisPooled(final URI uri) { diff --git a/src/main/java/redis/clients/jedis/SslOptions.java b/src/main/java/redis/clients/jedis/SslOptions.java new file mode 100644 index 0000000000..31b15c66fc --- /dev/null +++ b/src/main/java/redis/clients/jedis/SslOptions.java @@ -0,0 +1,493 @@ +package redis.clients.jedis; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.net.URL; +import java.util.Arrays; +import java.util.Objects; + +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509ExtendedTrustManager; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Options to configure SSL options for the connections kept to Redis servers. + * + * @author Mark Paluch + */ +public class SslOptions { + + private static final Logger logger = LoggerFactory.getLogger(SslOptions.class); + + private final String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + + private final String trustManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + + private final String keyStoreType; + + private final String trustStoreType; + + private final Resource keystoreResource; + + private final char[] keystorePassword; + + private final Resource truststoreResource; + + private final char[] truststorePassword; + + private final SSLParameters sslParameters; + + private final SslVerifyMode sslVerifyMode; + + private final String sslProtocol; // protocol for SSLContext + + private SslOptions(Builder builder) { + this.keyStoreType = builder.keyStoreType; + this.trustStoreType = builder.trustStoreType; + this.keystoreResource = builder.keystoreResource; + this.keystorePassword = builder.keystorePassword; + this.truststoreResource = builder.truststoreResource; + this.truststorePassword = builder.truststorePassword; + this.sslParameters = builder.sslParameters; + this.sslVerifyMode = builder.sslVerifyMode; + this.sslProtocol = builder.sslProtocol; + } + + /** + * Returns a new {@link SslOptions.Builder} to construct {@link SslOptions}. + * + * @return a new {@link SslOptions.Builder} to construct {@link SslOptions}. + */ + public static SslOptions.Builder builder() { + return new SslOptions.Builder(); + } + + /** + * Builder for {@link SslOptions}. + */ + public static class Builder { + + private String keyStoreType; + + private String trustStoreType; + + private Resource keystoreResource; + + private char[] keystorePassword = null; + + private Resource truststoreResource; + + private char[] truststorePassword = null; + + private SSLParameters sslParameters; + + private SslVerifyMode sslVerifyMode = SslVerifyMode.FULL; + + private String sslProtocol = "TLS"; // protocol for SSLContext + + private Builder() { + } + + /** + * Sets the KeyStore type. Defaults to {@link KeyStore#getDefaultType()} if not set. + * + * @param keyStoreType the keystore type to use, must not be {@code null}. + * @return {@code this} + */ + public Builder keyStoreType(String keyStoreType) { + this.keyStoreType = Objects.requireNonNull(keyStoreType, "KeyStoreType must not be null"); + return this; + } + + /** + * Sets the KeyStore type. Defaults to {@link KeyStore#getDefaultType()} if not set. + * + * @param trustStoreType the keystore type to use, must not be {@code null}. + * @return {@code this} + */ + public Builder trustStoreType(String trustStoreType) { + this.trustStoreType = Objects.requireNonNull(trustStoreType, "TrustStoreType must not be null"); + return this; + } + + /** + * Sets the Keystore file to load client certificates. The key store file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The keystore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param keystore the keystore file, must not be {@code null}. + * @return {@code this} + */ + public Builder keystore(File keystore) { + return keystore(keystore, null); + } + + /** + * Sets the Keystore file to load client certificates. The keystore file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The keystore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param keystore the keystore file, must not be {@code null}. + * @param keystorePassword the keystore password. May be empty to omit password and the keystore integrity check. + * @return {@code this} + */ + public Builder keystore(File keystore, char[] keystorePassword) { + + Objects.requireNonNull(keystore, "Keystore must not be null"); + assertFile("Keystore", keystore); + + return keystore(Resource.from(keystore), keystorePassword); + } + + /** + * Sets the Keystore resource to load client certificates. The keystore file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The keystore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param keystore the keystore URL, must not be {@code null}. + * @return {@code this} + */ + public Builder keystore(URL keystore) { + return keystore(keystore, null); + } + + /** + * Sets the Keystore resource to load client certificates. The keystore file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The keystore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param keystore the keystore file, must not be {@code null}. + * @param keystorePassword + * @return {@code this} + */ + public Builder keystore(URL keystore, char[] keystorePassword) { + + Objects.requireNonNull(keystore, "Keystore must not be null"); + + return keystore(Resource.from(keystore), keystorePassword); + } + + /** + * Sets the Java Keystore resource to load client certificates. The keystore file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The keystore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param resource the provider that opens a {@link InputStream} to the keystore file, must not be {@code null}. + * @param keystorePassword the keystore password. May be empty to omit password and the keystore integrity check. + * @return {@code this} + */ + public Builder keystore(Resource resource, char[] keystorePassword) { + + this.keystoreResource = Objects.requireNonNull(resource, "Keystore InputStreamProvider must not be null"); + + this.keystorePassword = getPassword(keystorePassword); + + return this; + } + + /** + * Sets the Truststore file to load trusted certificates. The truststore file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The truststore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param truststore the truststore file, must not be {@code null}. + * @return {@code this} + */ + public Builder truststore(File truststore) { + return truststore(truststore, null); + } + + /** + * Sets the Truststore file to load trusted certificates. The truststore file must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The truststore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param truststore the truststore file, must not be {@code null}. + * @param truststorePassword the truststore password. May be empty to omit password and the truststore integrity check. + * @return {@code this} + */ + public Builder truststore(File truststore, char[] truststorePassword) { + + Objects.requireNonNull(truststore, "Truststore must not be null"); + assertFile("Truststore", truststore); + + return truststore(Resource.from(truststore), truststorePassword); + } + + /** + * Sets the Truststore resource to load trusted certificates. The truststore resource must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The truststore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param truststore the truststore file, must not be {@code null}. + * @return {@code this} + */ + public Builder truststore(URL truststore) { + return truststore(truststore, null); + } + + /** + * Sets the Truststore resource to load trusted certificates. The truststore resource must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The truststore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param truststore the truststore file, must not be {@code null}. + * @param truststorePassword the truststore password. May be empty to omit password and the truststore integrity check. + * @return {@code this} + */ + public Builder truststore(URL truststore, char[] truststorePassword) { + + Objects.requireNonNull(truststore, "Truststore must not be null"); + + return truststore(Resource.from(truststore), truststorePassword); + } + + /** + * Sets the Truststore resource to load trusted certificates. The truststore resource must be supported by + * {@link java.security.KeyStore} which is {@link KeyStore#getDefaultType()} by default. The truststore is reloaded on + * each connection attempt that allows to replace certificates during runtime. + * + * @param resource the provider that opens a {@link InputStream} to the keystore file, must not be {@code null}. + * @param truststorePassword the truststore password. May be empty to omit password and the truststore integrity check. + * @return {@code this} + */ + public Builder truststore(Resource resource, char[] truststorePassword) { + + this.truststoreResource = Objects.requireNonNull(resource, "Truststore InputStreamProvider must not be null"); + + this.truststorePassword = getPassword(truststorePassword); + + return this; + } + + /** + * Sets a configured {@link SSLParameters}. + * + * @param sslParameters a {@link SSLParameters} object. + * @return {@code this} + */ + public Builder sslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + return this; + } + + /** + * Sets the {@link SslVerifyMode}. + * + * @param sslVerifyMode the {@link SslVerifyMode}. + * @return {@code this} + */ + public Builder sslVerifyMode(SslVerifyMode sslVerifyMode) { + this.sslVerifyMode = sslVerifyMode; + return this; + } + + /** + * The SSL/TLS protocol to be used to initialize {@link SSLContext}. + * @param protocol the ssl/tls protocol + * @return {@code this} + */ + public Builder sslProtocol(String protocol) { + this.sslProtocol = protocol; + return this; + } + + /** + * Create a new instance of {@link SslOptions} + * + * @return new instance of {@link SslOptions} + */ + public SslOptions build() { + if (this.sslParameters == null) { + this.sslParameters = new SSLParameters(); + } + return new SslOptions(this); + } + } + + /** + * A {@link SSLContext} object that is configured with values from this {@link SslOptions} object. + * + * @return {@link SSLContext} + * @throws IOException thrown when loading the keystore or the truststore fails. + * @throws GeneralSecurityException thrown when loading the keystore or the truststore fails. + */ + public SSLContext createSslContext() throws IOException, GeneralSecurityException { + + KeyManager[] keyManagers = null; + TrustManager[] trustManagers = null; + + if (sslVerifyMode == SslVerifyMode.FULL) { + this.sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + } else if (sslVerifyMode == SslVerifyMode.CA) { + this.sslParameters.setEndpointIdentificationAlgorithm(""); + } else if (sslVerifyMode == SslVerifyMode.INSECURE) { + trustManagers = new TrustManager[] { INSECURE_TRUST_MANAGER }; + } + + if (keystoreResource != null) { + + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + try (InputStream keystoreStream = keystoreResource.get()) { + keyStore.load(keystoreStream, keystorePassword); + } + + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(keyManagerAlgorithm); + keyManagerFactory.init(keyStore, keystorePassword); + keyManagers = keyManagerFactory.getKeyManagers(); + } + + if (trustManagers == null && truststoreResource != null) { + + KeyStore trustStore = KeyStore.getInstance(trustStoreType); + try (InputStream truststoreStream = truststoreResource.get()) { + trustStore.load(truststoreStream, truststorePassword); + } + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(trustManagerAlgorithm); + trustManagerFactory.init(trustStore); + trustManagers = trustManagerFactory.getTrustManagers(); + } + + SSLContext sslContext = SSLContext.getInstance(sslProtocol); + sslContext.init(keyManagers, trustManagers, null); + + return sslContext; + } + + /** + * {@link #createSslContext()} must be called before this. + * @return {@link SSLParameters} + */ + public SSLParameters getSslParameters() { + return sslParameters; + } + + private static char[] getPassword(char[] chars) { + return chars != null ? Arrays.copyOf(chars, chars.length) : null; + } + + /** + * Assert that {@code file} {@link File#exists() exists}. + * + * @param keyword file recognizer + * @param file + * @throws IllegalArgumentException if the file doesn't exist + */ + public static void assertFile(String keyword, File file) { + if (!file.exists()) { + throw new IllegalArgumentException(String.format("%s file %s does not exist", keyword, file)); + } + if (!file.isFile()) { + throw new IllegalArgumentException(String.format("%s file %s is not a file", keyword, file)); + } + } + + /** + * Supplier for a {@link InputStream} representing a resource. The resulting {@link InputStream} must be closed by + * the calling code. + */ + @FunctionalInterface + public interface Resource { + + /** + * Create a {@link Resource} that obtains a {@link InputStream} from a {@link URL}. + * + * @param url the URL to obtain the {@link InputStream} from. + * @return a {@link Resource} that opens a connection to the URL and obtains the {@link InputStream} for it. + */ + static Resource from(URL url) { + + Objects.requireNonNull(url, "URL must not be null"); + + return () -> url.openConnection().getInputStream(); + } + + /** + * Create a {@link Resource} that obtains a {@link InputStream} from a {@link File}. + * + * @param file the File to obtain the {@link InputStream} from. + * @return a {@link Resource} that obtains the {@link FileInputStream} for the given {@link File}. + */ + static Resource from(File file) { + + Objects.requireNonNull(file, "File must not be null"); + + return () -> new FileInputStream(file); + } + + /** + * Obtains the {@link InputStream}. + * + * @return the {@link InputStream}. + * @throws IOException + */ + InputStream get() throws IOException; + + } + + private static final X509Certificate[] EMPTY_X509_CERTIFICATES = {}; + + private static final TrustManager INSECURE_TRUST_MANAGER = new X509ExtendedTrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String s) { + if (logger.isDebugEnabled()) { + logger.debug("Accepting a client certificate: " + chain[0].getSubjectDN()); + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String s) { + if (logger.isDebugEnabled()) { + logger.debug("Accepting a server certificate: " + chain[0].getSubjectDN()); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String s, Socket socket) + throws CertificateException { + checkClientTrusted(chain, s); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String s, SSLEngine sslEngine) + throws CertificateException { + checkClientTrusted(chain, s); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String s, Socket socket) + throws CertificateException { + checkServerTrusted(chain, s); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String s, SSLEngine sslEngine) + throws CertificateException { + checkServerTrusted(chain, s); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return EMPTY_X509_CERTIFICATES; + } + }; + +} diff --git a/src/main/java/redis/clients/jedis/SslVerifyMode.java b/src/main/java/redis/clients/jedis/SslVerifyMode.java new file mode 100644 index 0000000000..c0f999acca --- /dev/null +++ b/src/main/java/redis/clients/jedis/SslVerifyMode.java @@ -0,0 +1,24 @@ +package redis.clients.jedis; + +/** + * Enumeration of SSL/TLS hostname verification modes. + */ +public enum SslVerifyMode { + + /** + * DO NOT USE THIS IN PRODUCTION. + *

+ * No verification at all. + */ + INSECURE, + + /** + * Verify the CA and certificate without verifying that the hostname matches. + */ + CA, + + /** + * Full certificate verification. + */ + FULL; +} diff --git a/src/test/java/redis/clients/jedis/SSLACLJedisClusterTest.java b/src/test/java/redis/clients/jedis/SSLACLJedisClusterTest.java index 16eb63c0e0..fbe57f66f7 100644 --- a/src/test/java/redis/clients/jedis/SSLACLJedisClusterTest.java +++ b/src/test/java/redis/clients/jedis/SSLACLJedisClusterTest.java @@ -10,6 +10,7 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; @@ -43,15 +44,20 @@ public class SSLACLJedisClusterTest extends JedisClusterTestBase { @BeforeClass public static void prepare() { - // We need to set up certificates first before connecting to the endpoint with enabled TLS - SSLJedisTest.setupTrustStore(); - // TODO(imalinovskyi): Remove hardcoded connection settings // once this test is refactored to support RE org.junit.Assume.assumeTrue("Not running ACL test on this version of Redis", RedisVersionUtil.checkRedisMajorVersionNumber(6, new EndpointConfig(new HostAndPort("localhost", 8379), "default", "cluster", true))); + + // We need to set up certificates first before connecting to the endpoint with enabled TLS + SSLJedisTest.setupTrustStore(); + } + + @AfterClass + public static void unprepare() { + SSLJedisTest.cleanupTrustStore(); } @Test diff --git a/src/test/java/redis/clients/jedis/SSLACLJedisTest.java b/src/test/java/redis/clients/jedis/SSLACLJedisTest.java index 8151729e6e..a6b2d4c059 100644 --- a/src/test/java/redis/clients/jedis/SSLACLJedisTest.java +++ b/src/test/java/redis/clients/jedis/SSLACLJedisTest.java @@ -2,14 +2,7 @@ import static org.junit.Assert.*; -import java.io.FileInputStream; -import java.io.InputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyStore; -import java.security.SecureRandom; -import java.security.cert.X509Certificate; -import javax.net.ssl.*; - +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -35,6 +28,11 @@ public static void prepare() { RedisVersionUtil.checkRedisMajorVersionNumber(6, endpoint)); } + @AfterClass + public static void unprepare() { + SSLJedisTest.cleanupTrustStore(); + } + @Test public void connectWithSsl() { try (Jedis jedis = new Jedis(endpoint.getHost(), endpoint.getPort(), true)) { @@ -76,77 +74,4 @@ public void connectWithUri() { assertEquals("PONG", jedis.ping()); } } - - /** - * Creates an SSLSocketFactory that trusts all certificates in truststore.jceks. - */ - static SSLSocketFactory createTrustStoreSslSocketFactory() throws Exception { - - KeyStore trustStore = KeyStore.getInstance("jceks"); - try (InputStream inputStream = new FileInputStream("src/test/resources/truststore.jceks")) { - trustStore.load(inputStream, null); - } - - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("PKIX"); - trustManagerFactory.init(trustStore); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagers, new SecureRandom()); - return sslContext.getSocketFactory(); - } - - /** - * Creates an SSLSocketFactory with a trust manager that does not trust any certificates. - */ - static SSLSocketFactory createTrustNoOneSslSocketFactory() throws Exception { - TrustManager[] unTrustManagers = new TrustManager[] { new X509TrustManager() { - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - - public void checkClientTrusted(X509Certificate[] chain, String authType) { - throw new RuntimeException(new InvalidAlgorithmParameterException()); - } - - public void checkServerTrusted(X509Certificate[] chain, String authType) { - throw new RuntimeException(new InvalidAlgorithmParameterException()); - } - } }; - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, unTrustManagers, new SecureRandom()); - return sslContext.getSocketFactory(); - } - - /** - * Very basic hostname verifier implementation for testing. NOT recommended for production. - */ - static class BasicHostnameVerifier implements HostnameVerifier { - - private static final String COMMON_NAME_RDN_PREFIX = "CN="; - - @Override - public boolean verify(String hostname, SSLSession session) { - X509Certificate peerCertificate; - try { - peerCertificate = (X509Certificate) session.getPeerCertificates()[0]; - } catch (SSLPeerUnverifiedException e) { - throw new IllegalStateException("The session does not contain a peer X.509 certificate.", e); - } - String peerCertificateCN = getCommonName(peerCertificate); - return hostname.equals(peerCertificateCN); - } - - private String getCommonName(X509Certificate peerCertificate) { - String subjectDN = peerCertificate.getSubjectDN().getName(); - String[] dnComponents = subjectDN.split(","); - for (String dnComponent : dnComponents) { - dnComponent = dnComponent.trim(); - if (dnComponent.startsWith(COMMON_NAME_RDN_PREFIX)) { - return dnComponent.substring(COMMON_NAME_RDN_PREFIX.length()); - } - } - throw new IllegalArgumentException("The certificate has no common name."); - } - } } diff --git a/src/test/java/redis/clients/jedis/SSLJedisClusterTest.java b/src/test/java/redis/clients/jedis/SSLJedisClusterTest.java index f4763fe875..bc43bff7b6 100644 --- a/src/test/java/redis/clients/jedis/SSLJedisClusterTest.java +++ b/src/test/java/redis/clients/jedis/SSLJedisClusterTest.java @@ -7,15 +7,16 @@ import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import redis.clients.jedis.exceptions.JedisClusterOperationException; import redis.clients.jedis.SSLJedisTest.BasicHostnameVerifier; +import redis.clients.jedis.SSLJedisTest.LocalhostVerifier; public class SSLJedisClusterTest extends JedisClusterTestBase { @@ -45,6 +46,11 @@ public static void prepare() { SSLJedisTest.setupTrustStore(); // set up trust store for SSL tests } + @AfterClass + public static void unprepare() { + SSLJedisTest.cleanupTrustStore(); + } + @Test public void testSSLDiscoverNodesAutomatically() { try (JedisCluster jc = new JedisCluster(Collections.singleton(new HostAndPort("localhost", 8379)), @@ -231,14 +237,4 @@ public void defaultHostAndPortUsedIfMapReturnsNull() { assertTrue(clusterNodes.containsKey("127.0.0.1:7381")); } } - - public class LocalhostVerifier extends BasicHostnameVerifier { - @Override - public boolean verify(String hostname, SSLSession session) { - if (hostname.equals("127.0.0.1")) { - hostname = "localhost"; - } - return super.verify(hostname, session); - } - } } diff --git a/src/test/java/redis/clients/jedis/SSLJedisSentinelPoolTest.java b/src/test/java/redis/clients/jedis/SSLJedisSentinelPoolTest.java index 7468c9abfa..4fda9646c3 100644 --- a/src/test/java/redis/clients/jedis/SSLJedisSentinelPoolTest.java +++ b/src/test/java/redis/clients/jedis/SSLJedisSentinelPoolTest.java @@ -3,6 +3,7 @@ import java.util.HashSet; import java.util.Set; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -24,6 +25,11 @@ public static void prepare() { sentinels.add(HostAndPorts.getSentinelServers().get(4)); } + @AfterClass + public static void unprepare() { + SSLJedisTest.cleanupTrustStore(); + } + @Test public void sentinelWithoutSslConnectsToRedisWithSsl() { diff --git a/src/test/java/redis/clients/jedis/SSLJedisTest.java b/src/test/java/redis/clients/jedis/SSLJedisTest.java index 4ef4f969bb..b0c11a4aab 100644 --- a/src/test/java/redis/clients/jedis/SSLJedisTest.java +++ b/src/test/java/redis/clients/jedis/SSLJedisTest.java @@ -20,6 +20,7 @@ import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; +import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -27,11 +28,6 @@ public class SSLJedisTest { protected static final EndpointConfig endpoint = HostAndPorts.getRedisEndpoint("standalone0-tls"); - @BeforeClass - public static void prepare() { - setupTrustStore(); - } - public static void setupTrustStore() { setJvmTrustStore("src/test/resources/truststore.jceks", "jceks"); } @@ -43,6 +39,25 @@ private static void setJvmTrustStore(String trustStoreFilePath, String trustStor System.setProperty("javax.net.ssl.trustStoreType", trustStoreType); } + public static void cleanupTrustStore() { + clearJvmTrustStore(); + } + + private static void clearJvmTrustStore() { + System.clearProperty("javax.net.ssl.trustStore"); + System.clearProperty("javax.net.ssl.trustStoreType"); + } + + @BeforeClass + public static void prepare() { + setupTrustStore(); + } + + @AfterClass + public static void unprepare() { + cleanupTrustStore(); + } + @Test public void connectWithSsl() { try (Jedis jedis = new Jedis(endpoint.getHost(), endpoint.getPort(), true)) { @@ -171,4 +186,14 @@ private String getCommonName(X509Certificate peerCertificate) { throw new IllegalArgumentException("The certificate has no common name."); } } + + static class LocalhostVerifier extends BasicHostnameVerifier { + @Override + public boolean verify(String hostname, SSLSession session) { + if (hostname.equals("127.0.0.1")) { + hostname = "localhost"; + } + return super.verify(hostname, session); + } + } } diff --git a/src/test/java/redis/clients/jedis/SSLOptionsJedisClusterTest.java b/src/test/java/redis/clients/jedis/SSLOptionsJedisClusterTest.java new file mode 100644 index 0000000000..bc568791c1 --- /dev/null +++ b/src/test/java/redis/clients/jedis/SSLOptionsJedisClusterTest.java @@ -0,0 +1,184 @@ +package redis.clients.jedis; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.util.Collections; +import java.util.Map; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLParameters; + +import org.junit.Assert; +import org.junit.Test; + +import redis.clients.jedis.exceptions.JedisClusterOperationException; +import redis.clients.jedis.SSLJedisTest.BasicHostnameVerifier; +import redis.clients.jedis.SSLJedisTest.LocalhostVerifier; + +public class SSLOptionsJedisClusterTest extends JedisClusterTestBase { + + private static final int DEFAULT_REDIRECTIONS = 5; + private static final ConnectionPoolConfig DEFAULT_POOL_CONFIG = new ConnectionPoolConfig(); + + private final HostAndPortMapper hostAndPortMap = (HostAndPort hostAndPort) -> { + String host = hostAndPort.getHost(); + int port = hostAndPort.getPort(); + if (host.equals("127.0.0.1")) { + host = "localhost"; + port = port + 1000; + } + return new HostAndPort(host, port); + }; + + // don't map IP addresses so that we try to connect with host 127.0.0.1 + private final HostAndPortMapper portMap = (HostAndPort hostAndPort) -> { + if ("localhost".equals(hostAndPort.getHost())) { + return hostAndPort; + } + return new HostAndPort(hostAndPort.getHost(), hostAndPort.getPort() + 1000); + }; + + @Test + public void testSSLDiscoverNodesAutomatically() { + try (JedisCluster jc2 = new JedisCluster(new HostAndPort("localhost", 8379), + DefaultJedisClientConfig.builder().password("cluster") + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks").build()) + .hostAndPortMapper(hostAndPortMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + Map clusterNodes = jc2.getClusterNodes(); + assertEquals(3, clusterNodes.size()); + assertTrue(clusterNodes.containsKey("127.0.0.1:7379")); + assertTrue(clusterNodes.containsKey("127.0.0.1:7380")); + assertTrue(clusterNodes.containsKey("127.0.0.1:7381")); + jc2.get("foo"); + } + } + + @Test + public void testSSLWithoutPortMap() { + try (JedisCluster jc = new JedisCluster(Collections.singleton(new HostAndPort("localhost", 8379)), + DefaultJedisClientConfig.builder().password("cluster") + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks").build()) + .build(), DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + Map clusterNodes = jc.getClusterNodes(); + assertEquals(3, clusterNodes.size()); + assertTrue(clusterNodes.containsKey("127.0.0.1:7379")); + assertTrue(clusterNodes.containsKey("127.0.0.1:7380")); + assertTrue(clusterNodes.containsKey("127.0.0.1:7381")); + } + } + + @Test + public void connectByIpAddress() { + try (JedisCluster jc = new JedisCluster(new HostAndPort("127.0.0.1", 7379), + DefaultJedisClientConfig.builder().password("cluster") + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks").build()) + .hostAndPortMapper(hostAndPortMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + jc.get("foo"); + } + } + + @Test + public void connectToNodesFailsWithSSLParametersAndNoHostMapping() { + final SSLParameters sslParameters = new SSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + + try (JedisCluster jc = new JedisCluster(new HostAndPort("localhost", 8379), + DefaultJedisClientConfig.builder().password("cluster") + .sslOptions(SslOptions.builder() + .sslParameters(sslParameters) + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks").build()) + .hostAndPortMapper(portMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + jc.get("foo"); + Assert.fail("It should fail after all cluster attempts."); + } catch (JedisClusterOperationException e) { + // initial connection to localhost works, but subsequent connections to nodes use 127.0.0.1 + // and fail hostname verification + assertEquals("No more cluster attempts left.", e.getMessage()); + } + } + + @Test + public void connectToNodesSucceedsWithSSLParametersAndHostMapping() { + final SSLParameters sslParameters = new SSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + + try (JedisCluster jc = new JedisCluster(new HostAndPort("localhost", 8379), + DefaultJedisClientConfig.builder().password("cluster") + .sslOptions(SslOptions.builder() + .sslParameters(sslParameters) + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks").build()) + .hostAndPortMapper(hostAndPortMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + jc.get("foo"); + } + } + + @Test + public void connectByIpAddressFailsWithSSLParameters() { + final SSLParameters sslParameters = new SSLParameters(); + sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + + try (JedisCluster jc = new JedisCluster(new HostAndPort("127.0.0.1", 8379), + DefaultJedisClientConfig.builder().password("cluster") + .sslOptions(SslOptions.builder() + .sslParameters(sslParameters) + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks").build()) + .hostAndPortMapper(hostAndPortMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + } catch (JedisClusterOperationException e) { + assertEquals("Could not initialize cluster slots cache.", e.getMessage()); + } + } + + @Test + public void connectWithCustomHostNameVerifier() { + HostnameVerifier hostnameVerifier = new BasicHostnameVerifier(); + HostnameVerifier localhostVerifier = new LocalhostVerifier(); + + SslOptions sslOptions = SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .sslVerifyMode(SslVerifyMode.CA).build(); + + try (JedisCluster jc = new JedisCluster(new HostAndPort("localhost", 8379), + DefaultJedisClientConfig.builder().password("cluster").sslOptions(sslOptions) + .hostnameVerifier(hostnameVerifier).hostAndPortMapper(portMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + jc.get("foo"); + Assert.fail("It should fail after all cluster attempts."); + } catch (JedisClusterOperationException e) { + // initial connection made with 'localhost' but subsequent connections to nodes use 127.0.0.1 + // which causes custom hostname verification to fail + assertEquals("No more cluster attempts left.", e.getMessage()); + } + + try (JedisCluster jc2 = new JedisCluster(new HostAndPort("127.0.0.1", 8379), + DefaultJedisClientConfig.builder().password("cluster").sslOptions(sslOptions) + .hostnameVerifier(hostnameVerifier).hostAndPortMapper(portMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + } catch (JedisClusterOperationException e) { + assertEquals("Could not initialize cluster slots cache.", e.getMessage()); + } + + try (JedisCluster jc3 = new JedisCluster(new HostAndPort("localhost", 8379), + DefaultJedisClientConfig.builder().password("cluster").sslOptions(sslOptions) + .hostnameVerifier(localhostVerifier).hostAndPortMapper(portMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + jc3.get("foo"); + } + } + +} diff --git a/src/test/java/redis/clients/jedis/SSLOptionsJedisPooledTest.java b/src/test/java/redis/clients/jedis/SSLOptionsJedisPooledTest.java new file mode 100644 index 0000000000..9aea9b945b --- /dev/null +++ b/src/test/java/redis/clients/jedis/SSLOptionsJedisPooledTest.java @@ -0,0 +1,62 @@ +package redis.clients.jedis; + +import static org.junit.Assert.assertEquals; + +import java.io.File; + +import org.junit.Test; + +public class SSLOptionsJedisPooledTest { + + protected static final EndpointConfig endpoint = HostAndPorts.getRedisEndpoint("standalone0-tls"); + + protected static final EndpointConfig aclEndpoint = HostAndPorts.getRedisEndpoint("standalone0-acl-tls"); + + @Test + public void connectWithClientConfig() { + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), + endpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithSslInsecure() { + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), + endpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .sslVerifyMode(SslVerifyMode.INSECURE) + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithSslContextProtocol() { + try (JedisPooled jedis = new JedisPooled(endpoint.getHostAndPort(), + endpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .sslProtocol("SSL") + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithAcl() { + try (JedisPooled jedis = new JedisPooled(aclEndpoint.getHostAndPort(), + aclEndpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } +} diff --git a/src/test/java/redis/clients/jedis/SSLOptionsJedisSentinelPoolTest.java b/src/test/java/redis/clients/jedis/SSLOptionsJedisSentinelPoolTest.java new file mode 100644 index 0000000000..062ef49052 --- /dev/null +++ b/src/test/java/redis/clients/jedis/SSLOptionsJedisSentinelPoolTest.java @@ -0,0 +1,100 @@ +package redis.clients.jedis; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.junit.BeforeClass; +import org.junit.Test; + +public class SSLOptionsJedisSentinelPoolTest { + + private static final String MASTER_NAME = "aclmaster"; + + private static Set sentinels = new HashSet<>(); + + private static final HostAndPortMapper SSL_PORT_MAPPER = (HostAndPort hap) + -> new HostAndPort(hap.getHost(), hap.getPort() + 10000); + + private static final GenericObjectPoolConfig POOL_CONFIG = new GenericObjectPoolConfig<>(); + + @BeforeClass + public static void prepare() { + sentinels.add(HostAndPorts.getSentinelServers().get(4)); + } + + @Test + public void sentinelWithoutSslConnectsToRedisWithSsl() { + + DefaultJedisClientConfig masterConfig = DefaultJedisClientConfig.builder() + .user("acljedis").password("fizzbuzz").clientName("master-client") + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .sslVerifyMode(SslVerifyMode.CA).build()) + .hostAndPortMapper(SSL_PORT_MAPPER).build(); + + DefaultJedisClientConfig sentinelConfig = DefaultJedisClientConfig.builder() + .user("sentinel").password("foobared").clientName("sentinel-client").build(); + + try (JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels, masterConfig, sentinelConfig)) { + pool.getResource().close(); + } + + try (JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels, POOL_CONFIG, + masterConfig, sentinelConfig)) { + pool.getResource().close(); + } + } + + @Test + public void sentinelWithSslConnectsToRedisWithoutSsl() { + + DefaultJedisClientConfig masterConfig = DefaultJedisClientConfig.builder() + .user("acljedis").password("fizzbuzz").clientName("master-client").build(); + + DefaultJedisClientConfig sentinelConfig = DefaultJedisClientConfig.builder() + .user("sentinel").password("foobared").clientName("sentinel-client") + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .sslVerifyMode(SslVerifyMode.CA).build()) + .hostAndPortMapper(SSL_PORT_MAPPER).build(); + + try (JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels, masterConfig, sentinelConfig)) { + pool.getResource().close(); + } + + try (JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels, POOL_CONFIG, + masterConfig, sentinelConfig)) { + pool.getResource().close(); + } + } + + @Test + public void sentinelWithSslConnectsToRedisWithSsl() { + + SslOptions sslOptions = SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .sslVerifyMode(SslVerifyMode.CA).build(); + + DefaultJedisClientConfig masterConfig = DefaultJedisClientConfig.builder() + .user("acljedis").password("fizzbuzz").clientName("master-client").sslOptions(sslOptions) + .hostAndPortMapper(SSL_PORT_MAPPER).build(); + + DefaultJedisClientConfig sentinelConfig = DefaultJedisClientConfig.builder() + .user("sentinel").password("foobared").clientName("sentinel-client").sslOptions(sslOptions) + .hostAndPortMapper(SSL_PORT_MAPPER).build(); + + try (JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels, masterConfig, sentinelConfig)) { + pool.getResource().close(); + } + + try (JedisSentinelPool pool = new JedisSentinelPool(MASTER_NAME, sentinels, POOL_CONFIG, + masterConfig, sentinelConfig)) { + pool.getResource().close(); + } + } + +} diff --git a/src/test/java/redis/clients/jedis/SSLOptionsJedisTest.java b/src/test/java/redis/clients/jedis/SSLOptionsJedisTest.java new file mode 100644 index 0000000000..cc0f7f598e --- /dev/null +++ b/src/test/java/redis/clients/jedis/SSLOptionsJedisTest.java @@ -0,0 +1,75 @@ +package redis.clients.jedis; + +import static org.junit.Assert.assertEquals; + +import java.io.File; + +import org.junit.Test; + +public class SSLOptionsJedisTest { + + protected static final EndpointConfig endpoint = HostAndPorts.getRedisEndpoint("standalone0-tls"); + + protected static final EndpointConfig aclEndpoint = HostAndPorts.getRedisEndpoint("standalone0-acl-tls"); + + @Test + public void connectWithSsl() { + try (Jedis jedis = new Jedis(endpoint.getHostAndPort(), + DefaultJedisClientConfig.builder() + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + jedis.auth(endpoint.getPassword()); + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithClientConfig() { + try (Jedis jedis = new Jedis(endpoint.getHostAndPort(), + endpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithSslInsecure() { + try (Jedis jedis = new Jedis(endpoint.getHostAndPort(), + endpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .sslVerifyMode(SslVerifyMode.INSECURE) + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithSslContextProtocol() { + try (Jedis jedis = new Jedis(endpoint.getHostAndPort(), + endpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .sslProtocol("SSL") + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } + + @Test + public void connectWithAcl() { + try (Jedis jedis = new Jedis(aclEndpoint.getHostAndPort(), + aclEndpoint.getClientConfigBuilder() + .sslOptions(SslOptions.builder() + .truststore(new File("src/test/resources/truststore.jceks")) + .trustStoreType("jceks") + .build()).build())) { + assertEquals("PONG", jedis.ping()); + } + } +} diff --git a/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java b/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java index b8df3910cd..f59f248b7a 100644 --- a/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java +++ b/src/test/java/redis/clients/jedis/csc/SSLJedisPooledClientSideCacheTest.java @@ -1,5 +1,6 @@ package redis.clients.jedis.csc; +import org.junit.AfterClass; import org.junit.BeforeClass; import redis.clients.jedis.HostAndPorts; import redis.clients.jedis.SSLJedisTest; @@ -13,4 +14,9 @@ public static void prepare() { endpoint = HostAndPorts.getRedisEndpoint("standalone0-tls"); } + @AfterClass + public static void unprepare() { + SSLJedisTest.cleanupTrustStore(); + } + }