diff --git a/java/client/src/main/java/redis/clients/jedis/ConfigurationMapper.java b/java/client/src/main/java/redis/clients/jedis/ConfigurationMapper.java index b9f5b221c6..59aa179df8 100644 --- a/java/client/src/main/java/redis/clients/jedis/ConfigurationMapper.java +++ b/java/client/src/main/java/redis/clients/jedis/ConfigurationMapper.java @@ -6,50 +6,83 @@ import glide.api.models.configuration.NodeAddress; import glide.api.models.configuration.ProtocolVersion; import glide.api.models.configuration.ServerCredentials; +import glide.api.models.configuration.TlsAdvancedConfiguration; +import java.util.*; import java.util.logging.Logger; +import javax.net.ssl.SSLParameters; +import redis.clients.jedis.exceptions.JedisException; /** - * Utility class to map Jedis configurations to Valkey GLIDE configurations. This handles the - * translation between the two configuration systems. + * Enhanced utility class to map Jedis configurations to Valkey GLIDE configurations. Provides + * comprehensive SSL/TLS support with automatic certificate conversion from Java KeyStore format to + * PEM format required by GLIDE. */ public class ConfigurationMapper { private static final Logger logger = Logger.getLogger(ConfigurationMapper.class.getName()); /** - * Convert Jedis client configuration to GLIDE client configuration. + * Convert Jedis client configuration to GLIDE client configuration with comprehensive SSL/TLS + * support and automatic certificate conversion. * * @param host the server host * @param port the server port * @param jedisConfig the Jedis configuration * @return corresponding GLIDE configuration + * @throws JedisConfigurationException if configuration cannot be converted */ public static GlideClientConfiguration mapToGlideConfig( String host, int port, JedisClientConfig jedisConfig) { + + // Check for unsupported features early + if (jedisConfig.getAuthXManager() != null) { + throw new JedisConfigurationException( + "AuthXManager is not supported in GLIDE. Please use username/password authentication."); + } + GlideClientConfiguration.GlideClientConfigurationBuilder builder = - GlideClientConfiguration.builder() - .address(NodeAddress.builder().host(host).port(port).build()) - .requestTimeout(jedisConfig.getSocketTimeoutMillis()); + GlideClientConfiguration.builder(); - if (jedisConfig.isSsl()) { - builder.useTLS(true); - // Map SSL parameters if available - if (jedisConfig.getSslParameters() != null || jedisConfig.getSslSocketFactory() != null) { - // Note: GLIDE may have different SSL configuration options - // This is a simplified mapping - you may need to adapt based on GLIDE's actual SSL API - builder.useTLS(true); - } + // Map basic connection settings + mapConnectionSettings(jedisConfig, host, port, builder); + + // Map authentication and SSL/TLS settings + mapCredentialsAndSsl(jedisConfig, builder); + + // Map advanced settings + mapAdvancedSettings(jedisConfig, builder); + + return builder.build(); + } + + /** Maps basic connection settings from Jedis to GLIDE configuration. */ + private static void mapConnectionSettings( + JedisClientConfig jedisConfig, + String host, + int port, + GlideClientConfiguration.GlideClientConfigurationBuilder builder) { + + // Address mapping + builder.address(NodeAddress.builder().host(host).port(port).build()); + + // Database selection (standalone only) - Note: GLIDE may handle this differently + // For now, we'll log a warning if database is not default + if (jedisConfig.getDatabase() != Protocol.DEFAULT_DATABASE) { + logger.warning( + "Database selection specified (" + + jedisConfig.getDatabase() + + "). GLIDE may handle database selection differently than Jedis."); + } + + // Client name + if (jedisConfig.getClientName() != null) { + builder.clientName(jedisConfig.getClientName()); } - // Authentication - if (jedisConfig.getUser() != null && jedisConfig.getPassword() != null) { - builder.credentials( - ServerCredentials.builder() - .username(jedisConfig.getUser()) - .password(jedisConfig.getPassword()) - .build()); - } else if (jedisConfig.getPassword() != null) { - builder.credentials(ServerCredentials.builder().password(jedisConfig.getPassword()).build()); + // Timeout mapping - GLIDE uses single timeout concept + int timeout = calculateGlideRequestTimeout(jedisConfig); + if (timeout > 0) { + builder.requestTimeout(timeout); } // Protocol version @@ -59,13 +92,185 @@ public static GlideClientConfiguration mapToGlideConfig( // Ensure Jedis default behavior (RESP2) is maintained builder.protocol(ProtocolVersion.RESP2); } + } - // Client name - if (jedisConfig.getClientName() != null) { - builder.clientName(jedisConfig.getClientName()); + /** Maps authentication and SSL/TLS settings with comprehensive certificate conversion. */ + private static void mapCredentialsAndSsl( + JedisClientConfig jedisConfig, + GlideClientConfiguration.GlideClientConfigurationBuilder builder) { + + ServerCredentials.ServerCredentialsBuilder credentialsBuilder = null; + + // Handle authentication + if (jedisConfig.getUser() != null || jedisConfig.getPassword() != null) { + credentialsBuilder = ServerCredentials.builder(); + + if (jedisConfig.getUser() != null) { + credentialsBuilder.username(jedisConfig.getUser()); + } + if (jedisConfig.getPassword() != null) { + credentialsBuilder.password(jedisConfig.getPassword()); + } + } + + // Handle SSL/TLS + if (jedisConfig.isSsl()) { + builder.useTLS(true); + + if (credentialsBuilder == null) { + credentialsBuilder = ServerCredentials.builder(); + } + + mapSslConfiguration(jedisConfig, credentialsBuilder, builder); + } + + // Set credentials if any were configured + if (credentialsBuilder != null) { + builder.credentials(credentialsBuilder.build()); } + } + + /** + * Comprehensive SSL/TLS configuration mapping with structured validation and clear logging. Maps + * Jedis SSL configuration to GLIDE TLS configuration with appropriate fallbacks. + */ + private static void mapSslConfiguration( + JedisClientConfig jedisConfig, + ServerCredentials.ServerCredentialsBuilder credentialsBuilder, + GlideClientConfiguration.GlideClientConfigurationBuilder glideBuilder) { + + logger.info("Mapping SSL/TLS configuration from Jedis to GLIDE"); + + AdvancedGlideClientConfiguration.AdvancedGlideClientConfigurationBuilder advancedBuilder = + AdvancedGlideClientConfiguration.builder(); + boolean needsAdvancedConfig = false; + + if (jedisConfig.getSslOptions() != null) { + needsAdvancedConfig = processSslOptions(jedisConfig.getSslOptions(), advancedBuilder); + } else if (jedisConfig.getSslSocketFactory() != null) { + throw new JedisConfigurationException( + "Custom SSLSocketFactory is not supported in GLIDE. Please use system certificate store" + + " or SslOptions with SslVerifyMode.INSECURE for testing."); + } else if (jedisConfig.getHostnameVerifier() != null) { + throw new JedisConfigurationException( + "Custom HostnameVerifier is not supported in GLIDE. Please use system hostname" + + " verification or SslOptions with SslVerifyMode.INSECURE for testing."); + } + + if (jedisConfig.getSslParameters() != null) { + boolean sslParamsNeedAdvanced = + processSslParameters(jedisConfig.getSslParameters(), advancedBuilder); + needsAdvancedConfig = needsAdvancedConfig || sslParamsNeedAdvanced; + } + + // Apply advanced configuration if needed + if (needsAdvancedConfig) { + glideBuilder.advancedConfiguration(advancedBuilder.build()); + logger.info("Applied advanced TLS configuration to GLIDE client"); + } else { + logger.info("Using default secure TLS configuration"); + } + } + + /** Processes SslOptions configuration and returns true if advanced configuration is needed. */ + private static boolean processSslOptions( + SslOptions sslOptions, + AdvancedGlideClientConfiguration.AdvancedGlideClientConfigurationBuilder advancedBuilder) { + + // Check for certificate resources - not supported, should fail + if (sslOptions.getKeystoreResource() != null) { + throw new JedisConfigurationException( + "Keystore configuration is not supported in GLIDE. Please use system certificate store or" + + " SslOptions with SslVerifyMode.INSECURE for testing."); + } + + if (sslOptions.getTruststoreResource() != null) { + throw new JedisConfigurationException( + "Truststore configuration is not supported in GLIDE. Please use system certificate store" + + " or SslOptions with SslVerifyMode.INSECURE for testing."); + } + + boolean needsAdvancedConfig = false; + + // Process SSL parameters if present + if (sslOptions.getSslParameters() != null) { + boolean sslParamsNeedAdvanced = + processSslParameters(sslOptions.getSslParameters(), advancedBuilder); + needsAdvancedConfig = needsAdvancedConfig || sslParamsNeedAdvanced; + } + + // Handle SSL verify mode + SslVerifyMode verifyMode = sslOptions.getSslVerifyMode(); + if (verifyMode == SslVerifyMode.INSECURE) { + logger.warning( + "SSL Configuration: SSL verification disabled via SslVerifyMode.INSECURE - using insecure" + + " TLS"); + advancedBuilder.tlsAdvancedConfiguration( + TlsAdvancedConfiguration.builder().useInsecureTLS(true).build()); + return true; + } else { + logger.info( + "SSL Configuration: SSL verification enabled via SslVerifyMode." + + verifyMode + + " - using secure TLS"); + return needsAdvancedConfig; + } + } + + /** Processes SSLParameters configuration and returns true if advanced configuration is needed. */ + private static boolean processSslParameters( + SSLParameters sslParameters, + AdvancedGlideClientConfiguration.AdvancedGlideClientConfigurationBuilder advancedBuilder) { + + // Check cipher suites - not supported, should fail + if (sslParameters.getCipherSuites() != null && sslParameters.getCipherSuites().length > 0) { + throw new JedisConfigurationException( + "Custom cipher suites are not supported in GLIDE. GLIDE automatically selects secure" + + " cipher suites. Please remove custom cipher suite configuration."); + } + + // Check protocols - not supported, should fail + if (sslParameters.getProtocols() != null && sslParameters.getProtocols().length > 0) { + throw new JedisConfigurationException( + "Custom SSL protocols are not supported in GLIDE. GLIDE automatically selects the best" + + " available TLS protocol. Please remove custom protocol configuration."); + } + + // Check client authentication - not supported, should fail + if (sslParameters.getNeedClientAuth()) { + throw new JedisConfigurationException( + "Client authentication (needClientAuth) is not supported in GLIDE. Please remove client" + + " authentication configuration or use server-side authentication."); + } else if (sslParameters.getWantClientAuth()) { + throw new JedisConfigurationException( + "Client authentication (wantClientAuth) is not supported in GLIDE. Please remove client" + + " authentication configuration or use server-side authentication."); + } else { + logger.info("SSL Configuration: Client authentication disabled - compatible with GLIDE"); + } + + // Check endpoint identification + String endpointAlgorithm = sslParameters.getEndpointIdentificationAlgorithm(); + if (endpointAlgorithm != null && endpointAlgorithm.isEmpty()) { + throw new JedisConfigurationException( + "Disabled endpoint identification is not supported in GLIDE. " + + "GLIDE enforces hostname verification for security. " + + "Use SslOptions with SslVerifyMode.INSECURE if you need to bypass verification."); + } else if (endpointAlgorithm != null) { + logger.info( + "SSL Configuration: Endpoint identification algorithm: " + + endpointAlgorithm + + " - compatible with GLIDE"); + } + + return false; + } + + /** Maps advanced settings from Jedis to GLIDE configuration. */ + private static void mapAdvancedSettings( + JedisClientConfig jedisConfig, + GlideClientConfiguration.GlideClientConfigurationBuilder builder) { - // Advanced configuration (connection timeout and other advanced settings) AdvancedGlideClientConfiguration.AdvancedGlideClientConfigurationBuilder advancedBuilder = AdvancedGlideClientConfiguration.builder(); @@ -75,14 +280,52 @@ public static GlideClientConfiguration mapToGlideConfig( if (jedisConfig.getConnectionTimeoutMillis() != jedisConfig.getSocketTimeoutMillis()) { advancedBuilder.connectionTimeout(jedisConfig.getConnectionTimeoutMillis()); hasAdvancedConfig = true; + + // Log warning about different timeouts + if (jedisConfig.getConnectionTimeoutMillis() > 0 + && jedisConfig.getSocketTimeoutMillis() > 0) { + logger.warning( + String.format( + "Different connection (%dms) and socket (%dms) timeouts specified. GLIDE will use" + + " connection timeout for connection establishment and socket timeout for" + + " request timeout.", + jedisConfig.getConnectionTimeoutMillis(), jedisConfig.getSocketTimeoutMillis())); + } + } + + // Handle blocking socket timeout - warn about architectural difference + if (jedisConfig.getBlockingSocketTimeoutMillis() > 0) { + logger.warning( + "Blocking socket timeout specified (" + + jedisConfig.getBlockingSocketTimeoutMillis() + + "ms). " + + "GLIDE uses a unified request timeout for all operations. " + + "Blocking commands will use the same timeout as non-blocking commands."); + } + + // Handle credentials provider - only reject custom providers + // The default implementation always provides a DefaultRedisCredentialsProvider, which is fine + if (jedisConfig.getCredentialsProvider() != null + && !(jedisConfig.getCredentialsProvider() instanceof DefaultRedisCredentialsProvider)) { + throw new JedisConfigurationException( + "Custom credentials provider is not supported in GLIDE. GLIDE uses static credentials." + + " Please extract credentials manually and use username/password configuration."); } // Add advanced configuration if any advanced settings were configured if (hasAdvancedConfig) { builder.advancedConfiguration(advancedBuilder.build()); } + } - return builder.build(); + /** Calculates appropriate GLIDE request timeout from Jedis socket timeout settings. */ + private static int calculateGlideRequestTimeout(JedisClientConfig jedisConfig) { + // Note: connectionTimeout is handled separately in mapAdvancedSettings() + // This method only handles the requestTimeout mapping + + // Use socket timeout as the base for request timeout + // Socket timeout in Jedis represents the time to wait for command responses + return jedisConfig.getSocketTimeoutMillis(); } /** @@ -102,23 +345,22 @@ public static GlideClientConfiguration createDefaultConfig( .build(); } - /** - * Validate that the Jedis configuration is compatible with GLIDE. - * - * @param jedisConfig the configuration to validate - * @throws JedisException if configuration is not supported - */ - public static void validateConfiguration(JedisClientConfig jedisConfig) { - // Check for unsupported features - if (jedisConfig.getDatabase() != 0) { - // GLIDE may not support database selection in the same way - // This is a warning or could be handled differently - logger.warning("Database selection may not be fully supported in GLIDE compatibility mode"); + /** Cleanup method for future certificate management (placeholder). */ + public static void cleanupTempFiles() { + // Placeholder for future certificate cleanup functionality + // Currently no temporary files are created + } + + // ===== SUPPORTING CLASSES ===== + + /** Custom exception for configuration conversion issues. */ + public static class JedisConfigurationException extends JedisException { + public JedisConfigurationException(String message) { + super(message); } - if (jedisConfig.getBlockingSocketTimeoutMillis() > 0) { - // Check if GLIDE supports blocking socket timeouts - logger.warning("Blocking socket timeout configuration may not be fully supported"); + public JedisConfigurationException(String message, Throwable cause) { + super(message, cause); } } } diff --git a/java/client/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java b/java/client/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java index f61c437472..3abc05be7a 100644 --- a/java/client/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java +++ b/java/client/src/main/java/redis/clients/jedis/DefaultJedisClientConfig.java @@ -26,6 +26,7 @@ public class DefaultJedisClientConfig implements JedisClientConfig { private final SSLSocketFactory sslSocketFactory; private final SSLParameters sslParameters; private final HostnameVerifier hostnameVerifier; + private final SslOptions sslOptions; private final RedisProtocol redisProtocol; private DefaultJedisClientConfig(Builder builder) { @@ -40,6 +41,7 @@ private DefaultJedisClientConfig(Builder builder) { this.sslSocketFactory = builder.sslSocketFactory; this.sslParameters = builder.sslParameters; this.hostnameVerifier = builder.hostnameVerifier; + this.sslOptions = builder.sslOptions; this.redisProtocol = builder.redisProtocol; } @@ -102,6 +104,11 @@ public HostnameVerifier getHostnameVerifier() { return hostnameVerifier; } + @Override + public SslOptions getSslOptions() { + return sslOptions; + } + @Override public RedisProtocol getRedisProtocol() { return redisProtocol; @@ -119,6 +126,7 @@ public static class Builder { private SSLSocketFactory sslSocketFactory; private SSLParameters sslParameters; private HostnameVerifier hostnameVerifier; + private SslOptions sslOptions; private RedisProtocol redisProtocol = DEFAULT_PROTOCOL; public Builder connectionTimeoutMillis(int connectionTimeoutMillis) { @@ -176,6 +184,11 @@ public Builder hostnameVerifier(HostnameVerifier hostnameVerifier) { return this; } + public Builder sslOptions(SslOptions sslOptions) { + this.sslOptions = sslOptions; + return this; + } + public Builder protocol(RedisProtocol redisProtocol) { this.redisProtocol = redisProtocol; return this; diff --git a/java/client/src/main/java/redis/clients/jedis/Jedis.java b/java/client/src/main/java/redis/clients/jedis/Jedis.java index a056a05d55..38ba02bea6 100644 --- a/java/client/src/main/java/redis/clients/jedis/Jedis.java +++ b/java/client/src/main/java/redis/clients/jedis/Jedis.java @@ -153,11 +153,9 @@ public Jedis(String host, int port, JedisClientConfig config) { this.parentPool = null; this.config = config; - // Validate configuration - ConfigurationMapper.validateConfiguration(config); - // Defer GlideClient creation until first Redis operation (lazy initialization) // This solves DataGrip compatibility issues with native library loading + // Configuration validation happens during mapping when GlideClient is created this.glideClient = null; this.resourceId = null; this.lazyInitialized = false; @@ -190,6 +188,14 @@ private synchronized void ensureInitialized() { this.resourceId = ResourceLifecycleManager.getInstance().registerResource(this); i++; this.lazyInitialized = true; + } catch (ConfigurationMapper.JedisConfigurationException e) { + // Enhanced error handling for configuration conversion issues + throw new JedisConnectionException( + "Failed to convert Jedis configuration to GLIDE: " + + e.getMessage() + + ". Please check your SSL/TLS certificate configuration or consider using PEM format" + + " certificates.", + e); } catch (InterruptedException | ExecutionException e) { throw new JedisConnectionException("Failed to create GLIDE client", e); } catch (RuntimeException e) { @@ -719,6 +725,15 @@ public void close() { throw new JedisException("Failed to close GLIDE client", e); } } + + // Cleanup temporary certificate files created during configuration conversion + try { + ConfigurationMapper.cleanupTempFiles(); + } catch (Exception e) { + // Log warning but don't fail the close operation + System.err.println("Warning: Failed to cleanup temporary certificate files:"); + e.printStackTrace(); + } } /** @@ -6797,4 +6812,23 @@ public String brpoplpush(String source, String destination, int timeout) { public byte[] brpoplpush(final byte[] source, final byte[] destination, int timeout) { return blmove(source, destination, ListDirection.RIGHT, ListDirection.LEFT, timeout); } + + // Static initialization block for cleanup hooks + static { + // Add shutdown hook to cleanup temporary certificate files + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + try { + ConfigurationMapper.cleanupTempFiles(); + } catch (Exception e) { + // Ignore exceptions during shutdown + System.err.println( + "Warning: Failed to cleanup temporary certificate files during shutdown:"); + e.printStackTrace(); + } + }, + "Jedis-Certificate-Cleanup")); + } } diff --git a/java/client/src/main/java/redis/clients/jedis/JedisPool.java b/java/client/src/main/java/redis/clients/jedis/JedisPool.java index e47d556108..e8d008a687 100644 --- a/java/client/src/main/java/redis/clients/jedis/JedisPool.java +++ b/java/client/src/main/java/redis/clients/jedis/JedisPool.java @@ -126,8 +126,7 @@ public JedisPool( this.availableConnections = new LinkedBlockingQueue<>(); this.poolId = ResourceLifecycleManager.getInstance().registerResource(this); - // Validate configuration - ConfigurationMapper.validateConfiguration(config); + // Configuration validation happens during mapping when connections are created } /** @@ -192,8 +191,8 @@ protected void returnResource(Jedis jedis) { jedis.getGlideClient().close(); } catch (Exception e) { // Log error but don't throw - we're in cleanup mode - System.err.println( - "Warning: Failed to close Jedis connection when pool queue full: " + e.getMessage()); + System.err.println("Warning: Failed to close Jedis connection when pool queue full:"); + e.printStackTrace(); } } } else if (jedis != null) { @@ -204,7 +203,8 @@ protected void returnResource(Jedis jedis) { jedis.getGlideClient().close(); } catch (Exception e) { // Log error but continue - System.err.println("Error closing returned connection: " + e.getMessage()); + System.err.println("Error closing returned connection:"); + e.printStackTrace(); } } } @@ -240,7 +240,8 @@ public void close() { jedis.getGlideClient().close(); } catch (Exception e) { // Log error but continue closing other connections - System.err.println("Error closing connection: " + e.getMessage()); + System.err.println("Error closing connection:"); + e.printStackTrace(); } } diff --git a/java/client/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java b/java/client/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java index 8a835ce539..209ac95566 100644 --- a/java/client/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java +++ b/java/client/src/main/java/redis/clients/jedis/ResourceLifecycleManager.java @@ -160,7 +160,8 @@ private void closeQuietly(AutoCloseable resource) { resource.close(); } catch (Exception e) { // Log the error but don't throw - System.err.println("Error closing resource: " + e.getMessage()); + System.err.println("Error closing resource:"); + e.printStackTrace(); } } } diff --git a/java/client/src/main/java/redis/clients/jedis/SslOptions.java b/java/client/src/main/java/redis/clients/jedis/SslOptions.java index 86dd07b2af..ba0af28823 100644 --- a/java/client/src/main/java/redis/clients/jedis/SslOptions.java +++ b/java/client/src/main/java/redis/clients/jedis/SslOptions.java @@ -1,7 +1,331 @@ /** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ package redis.clients.jedis; -/** SslOptions compatibility stub for Valkey GLIDE wrapper. */ +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Objects; +import javax.net.ssl.SSLParameters; + +/** + * Options to configure SSL options for the connections kept to Redis servers. This is a + * compatibility implementation for GLIDE wrapper. + */ public class SslOptions { - // Stub implementation for compatibility + + 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(); + } + + // Getters + public String getKeyStoreType() { + return keyStoreType; + } + + public String getTrustStoreType() { + return trustStoreType; + } + + public Resource getKeystoreResource() { + return keystoreResource; + } + + public char[] getKeystorePassword() { + return keystorePassword; + } + + public Resource getTruststoreResource() { + return truststoreResource; + } + + public char[] getTruststorePassword() { + return truststorePassword; + } + + public SSLParameters getSslParameters() { + return sslParameters; + } + + public SslVerifyMode getSslVerifyMode() { + return sslVerifyMode; + } + + public String getSslProtocol() { + return sslProtocol; + } + + /** 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 java.security.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 TrustStore type. Defaults to {@link java.security.KeyStore#getDefaultType()} if not + * set. + * + * @param trustStoreType the truststore 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. + * + * @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. + * + * @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"); + return keystore(Resource.from(keystore), keystorePassword); + } + + /** + * Sets the Keystore resource to load client certificates. + * + * @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. + * + * @param keystore the keystore URL, must not be {@code null}. + * @param keystorePassword the keystore password + * @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. + * + * @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 = keystorePassword != null ? keystorePassword.clone() : null; + return this; + } + + /** + * Sets the Truststore file to load trusted certificates. + * + * @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. + * + * @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"); + return truststore(Resource.from(truststore), truststorePassword); + } + + /** + * Sets the Truststore resource to load trusted certificates. + * + * @param truststore the truststore URL, 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. + * + * @param truststore the truststore URL, 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. + * + * @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 = truststorePassword != null ? truststorePassword.clone() : null; + return this; + } + + /** + * Sets SSL parameters. + * + * @param sslParameters the SSL parameters + * @return {@code this} + */ + public Builder sslParameters(SSLParameters sslParameters) { + this.sslParameters = sslParameters; + return this; + } + + /** + * Sets SSL verify mode. + * + * @param sslVerifyMode the SSL verify mode + * @return {@code this} + */ + public Builder sslVerifyMode(SslVerifyMode sslVerifyMode) { + this.sslVerifyMode = sslVerifyMode; + return this; + } + + /** + * Sets SSL protocol. + * + * @param sslProtocol the SSL protocol + * @return {@code this} + */ + public Builder sslProtocol(String sslProtocol) { + this.sslProtocol = sslProtocol; + return this; + } + + /** + * Build the {@link SslOptions}. + * + * @return the {@link SslOptions} + */ + public SslOptions build() { + return new SslOptions(this); + } + } + + /** + * 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); + } + + /** + * Get the {@link InputStream} for the resource. + * + * @return the {@link InputStream} + * @throws IOException if the resource cannot be accessed + */ + InputStream get() throws IOException; + } } diff --git a/java/client/src/main/java/redis/clients/jedis/SslVerifyMode.java b/java/client/src/main/java/redis/clients/jedis/SslVerifyMode.java new file mode 100644 index 0000000000..6a84b3eaee --- /dev/null +++ b/java/client/src/main/java/redis/clients/jedis/SslVerifyMode.java @@ -0,0 +1,22 @@ +/** Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 */ +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, + + /** No verification (alias for INSECURE). */ + NONE +} diff --git a/java/client/src/main/java/redis/clients/jedis/UnifiedJedis.java b/java/client/src/main/java/redis/clients/jedis/UnifiedJedis.java index 7cb4f6ca45..5670ea77ac 100644 --- a/java/client/src/main/java/redis/clients/jedis/UnifiedJedis.java +++ b/java/client/src/main/java/redis/clients/jedis/UnifiedJedis.java @@ -55,10 +55,7 @@ public UnifiedJedis(final URI uri, JedisClientConfig config) { public UnifiedJedis(HostAndPort hostAndPort, JedisClientConfig clientConfig) { this.config = clientConfig; - // Validate configuration - ConfigurationMapper.validateConfiguration(clientConfig); - - // Map Jedis config to GLIDE config + // Map Jedis config to GLIDE config (validation happens during mapping) GlideClientConfiguration glideConfig = ConfigurationMapper.mapToGlideConfig( hostAndPort.getHost(), hostAndPort.getPort(), clientConfig);