diff --git a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java index e304e961f1..d5468f3a46 100644 --- a/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java @@ -20,6 +20,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; @@ -28,25 +29,22 @@ public final class DefaultJedisClientConfig implements JedisClientConfig { private final boolean readOnlyForRedisClusterReplicas; - 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) { - 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; + 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; } @Override @@ -110,6 +108,11 @@ public SSLParameters getSslParameters() { return sslParameters; } + @Override + public SslOptions getSslOptions() { + return sslOptions; + } + @Override public HostnameVerifier getHostnameVerifier() { return hostnameVerifier; @@ -151,6 +154,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; @@ -168,15 +172,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); + 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); @@ -253,6 +255,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; @@ -278,19 +285,49 @@ public static DefaultJedisClientConfig create(int connectionTimeoutMillis, int s int blockingSocketTimeoutMillis, String user, String password, int database, String clientName, boolean ssl, SSLSocketFactory sslSocketFactory, SSLParameters sslParameters, HostnameVerifier hostnameVerifier, HostAndPortMapper hostAndPortMapper) { - return new DefaultJedisClientConfig(null, - connectionTimeoutMillis, soTimeoutMillis, blockingSocketTimeoutMillis, - new DefaultRedisCredentialsProvider(new DefaultRedisCredentials(user, password)), database, - clientName, ssl, sslSocketFactory, sslParameters, hostnameVerifier, hostAndPortMapper, null, - false); + 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 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()); + 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(); + } + 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..04b4e673eb 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,52 @@ 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); + } + + if (sslOptions == null) { + // limiting HostnameVerifier only for 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 abe1f35237..ef5478e30e 100644 --- a/src/main/java/redis/clients/jedis/JedisClientConfig.java +++ b/src/main/java/redis/clients/jedis/JedisClientConfig.java @@ -44,6 +44,7 @@ default String getPassword() { return null; } + // TODO: return null default Supplier getCredentialsProvider() { return new DefaultRedisCredentialsProvider( new DefaultRedisCredentials(getUser(), getPassword())); @@ -76,6 +77,16 @@ default HostnameVerifier getHostnameVerifier() { return null; } + /** + * {@link JedisClientConfig#isSsl()}, {@link JedisClientConfig#getSslSocketFactory()}, + * {@link JedisClientConfig#getSslParameters()} and {@link JedisClientConfig#getHostnameVerifier()} will be ignored if + * {@link JedisClientConfig#getSslOptions() this} is set. + * @return ssl options + */ + default SslOptions getSslOptions() { + return null; + } + default HostAndPortMapper getHostAndPortMapper() { return null; } 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..bd3bea6709 --- /dev/null +++ b/src/main/java/redis/clients/jedis/SslOptions.java @@ -0,0 +1,500 @@ +/* + * Copyright 2011-Present, Redis Ltd. and Contributors + * All rights reserved. + * + * Licensed under the MIT License. + * + * This file contains contributions from third-party contributors + * 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 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 java.util.function.Supplier; + +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. + */ +public class SslOptions { + + private static final Logger logger = LoggerFactory.getLogger(SslOptions.class); + + private final String keyManagerAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); + //private final String keyManagerAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); // Lettuce + + 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 Supplier sslParameters; + + private final SslVerifyMode sslVerifyMode; + + private final String sslContextProtocol; + + 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.sslContextProtocol = builder.sslContextProtocol; + } + + /** + * 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 = new char[0]; + + private Resource truststoreResource; + + private char[] truststorePassword = new char[0]; + + private Supplier sslParameters = SSLParameters::new; + + private SslVerifyMode sslVerifyMode = SslVerifyMode.FULL; + + private String sslContextProtocol = "TLS"; + + 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); + //return keystore(keystore, new char[0]); // Lettuce + } + + /** + * 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, String truststorePassword) { + + Objects.requireNonNull(truststore, "Truststore must not be null"); + assertFile("Truststore", truststore); + + return truststore(Resource.from(truststore), getPassword(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, String truststorePassword) { + + Objects.requireNonNull(truststore, "Truststore must not be null"); + + return truststore(Resource.from(truststore), getPassword(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} + */ + private Builder truststore(Resource resource, char[] truststorePassword) { + + this.truststoreResource = Objects.requireNonNull(resource, "Truststore InputStreamProvider must not be null"); + + this.truststorePassword = getPassword(truststorePassword); + + return this; + } + + public Builder sslParameters(Supplier sslParameters) { + this.sslParameters = sslParameters; + return this; + } + + public Builder sslVerifyMode(SslVerifyMode sslVerifyMode) { + this.sslVerifyMode = sslVerifyMode; + return this; + } + + public Builder sslContextProtocol(String protocol) { + this.sslContextProtocol = protocol; + return this; + } + + /** + * Create a new instance of {@link SslOptions} + * + * @return new instance of {@link SslOptions} + */ + public SslOptions build() { + 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 { + + TrustManager[] trustManagers = null; + + if (sslVerifyMode == SslVerifyMode.INSECURE) { + trustManagers = new TrustManager[] { INSECURE_TRUST_MANAGER }; + } + + KeyManager[] keyManagers = null; + 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) { + // already processed + } else if (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(sslContextProtocol); + sslContext.init(keyManagers, trustManagers, null); + + return sslContext; + } + + public SSLParameters getSslParameters() { + SSLParameters _sslParameters = sslParameters.get(); + + if (sslVerifyMode == SslVerifyMode.FULL) { + _sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); + } else if (sslVerifyMode == SslVerifyMode.CA) { + _sslParameters.setEndpointIdentificationAlgorithm(""); + } + + return _sslParameters; + } + + private static char[] getPassword(String password) { + return password != null ? password.toCharArray() : null; + } + + 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..e737240350 --- /dev/null +++ b/src/main/java/redis/clients/jedis/SslVerifyMode.java @@ -0,0 +1,22 @@ +package redis.clients.jedis; + +/** + * Enumeration of SSL/TLS hostname verification modes. + */ +public enum SslVerifyMode { + + /** + * 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..1729cab8c5 --- /dev/null +++ b/src/test/java/redis/clients/jedis/SSLOptionsJedisClusterTest.java @@ -0,0 +1,181 @@ +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 javax.net.ssl.SSLSocketFactory; + +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 + @org.junit.Ignore // TODO: drop support for custom hostname verifier (with SslOptions) ?? + public void connectWithCustomHostNameVerifier() { + HostnameVerifier hostnameVerifier = new BasicHostnameVerifier(); + HostnameVerifier localhostVerifier = new LocalhostVerifier(); + + try (JedisCluster jc = new JedisCluster(new HostAndPort("localhost", 8379), + DefaultJedisClientConfig.builder().password("cluster").ssl(true) + .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").ssl(true) + .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").ssl(true) + .hostnameVerifier(localhostVerifier).hostAndPortMapper(portMap).build(), + DEFAULT_REDIRECTIONS, DEFAULT_POOL_CONFIG)) { + jc3.get("foo"); + } + } + +} 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..2ea1d233da --- /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 connectWithConfig() { + 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() + .sslContextProtocol("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(); + } + }