RedisClusterClientIT.java

package redis.clients.jedis.tls;

import static org.junit.jupiter.api.Assertions.*;
import static redis.clients.jedis.util.TlsUtil.*;

import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLParameters;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import redis.clients.jedis.*;
import redis.clients.jedis.util.TlsUtil;
import redis.clients.jedis.exceptions.JedisClusterOperationException;

public class RedisClusterClientIT extends RedisClusterTestBase {

  private static final int DEFAULT_REDIRECTIONS = 5;
  private static final ConnectionPoolConfig DEFAULT_POOL_CONFIG = new ConnectionPoolConfig();

  /**
   * Provides different SslOptions configurations for parametrized tests.
   */
  protected static Stream<Arguments> sslOptionsProvider() {
    return Stream.of(Arguments.of("truststore", createSslOptions()),
      Arguments.of("insecure", SslOptions.builder().sslVerifyMode(SslVerifyMode.INSECURE).build()),
      Arguments.of("ssl-protocol",
        SslOptions.builder().sslProtocol("SSL").truststore(trustStorePath.toFile())
            .trustStoreType("jceks").sslVerifyMode(SslVerifyMode.CA).build()));
  }

  /**
   * Tests SSL discover nodes with various SSL configurations.
   */
  @ParameterizedTest(name = "testSSLDiscoverNodesAutomatically_{0}")
  @MethodSource("sslOptionsProvider")
  void testSSLDiscoverNodesAutomatically(String testName, SslOptions ssl) {
    try (RedisClusterClient jc = RedisClusterClient.builder()
        .nodes(Collections.singleton(tlsEndpoint.getHostAndPort()))
        .clientConfig(DefaultJedisClientConfig.builder().password(tlsEndpoint.getPassword())
            .sslOptions(ssl).build())
        .maxAttempts(DEFAULT_REDIRECTIONS).poolConfig(DEFAULT_POOL_CONFIG).build()) {
      Map<String, ?> clusterNodes = jc.getClusterNodes();
      assertTrue(clusterNodes.containsKey(tlsEndpoint.getHostAndPort(0).toString()));
      assertTrue(clusterNodes.containsKey(tlsEndpoint.getHostAndPort(1).toString()));
      assertTrue(clusterNodes.containsKey(tlsEndpoint.getHostAndPort(2).toString()));
      assertEquals("PONG", jc.ping());
    }
  }

  /**
   * Tests that connecting with ssl=true flag (system truststore) works.
   */
  @Test
  void connectWithSslFlag() {
    try (RedisClusterClient jc = RedisClusterClient.builder()
        .nodes(Collections.singleton(tlsEndpoint.getHostAndPort()))
        .clientConfig(
          DefaultJedisClientConfig.builder().password(tlsEndpoint.getPassword()).ssl(true).build())
        .maxAttempts(DEFAULT_REDIRECTIONS).poolConfig(DEFAULT_POOL_CONFIG).build()) {
      assertEquals("PONG", jc.ping());
    }
  }

  /**
   * Tests that connecting to nodes succeeds with SSL parameters and hostname verification.
   */
  @ParameterizedTest(name = "connectToNodesSucceedsWithSSLParametersAndHostMapping_{0}")
  @MethodSource("sslOptionsProvider")
  void connectToNodesSucceedsWithSSLParametersAndHostMapping(String testName, SslOptions ssl) {
    try (RedisClusterClient jc = RedisClusterClient.builder()
        .nodes(Collections.singleton(tlsEndpoint.getHostAndPort()))
        .clientConfig(DefaultJedisClientConfig.builder().password(tlsEndpoint.getPassword())
            .sslOptions(ssl).build())
        .maxAttempts(DEFAULT_REDIRECTIONS).poolConfig(DEFAULT_POOL_CONFIG).build()) {
      assertEquals("PONG", jc.ping());
    }
  }

  /**
   * Tests connecting with custom hostname verifier and SslOptions (from
   * SSLOptionsRedisClusterClientIT).
   */
  @Test
  public void connectWithCustomHostNameVerifierAndSslOptions() {
    HostnameVerifier hostnameVerifier = new TlsUtil.BasicHostnameVerifier();

    SslOptions sslOptions = SslOptions.builder().truststore(trustStorePath.toFile())
        .trustStoreType("jceks").sslVerifyMode(SslVerifyMode.CA).build();

    try (RedisClusterClient jc = RedisClusterClient.builder()
        .nodes(Collections.singleton(tlsEndpoint.getHostAndPort()))
        .clientConfig(DefaultJedisClientConfig.builder().password(tlsEndpoint.getPassword())
            .sslOptions(sslOptions).hostnameVerifier(hostnameVerifier).build())
        .maxAttempts(DEFAULT_REDIRECTIONS).poolConfig(DEFAULT_POOL_CONFIG).build()) {
      assertEquals("PONG", jc.ping());
    }
  }

  /**
   * Tests connecting with custom SSL socket factory.
   */
  @Test
  void connectWithCustomSocketFactory() {
    try (RedisClusterClient jc = RedisClusterClient.builder()
        .nodes(Collections.singleton(tlsEndpoint.getHostAndPort()))
        .clientConfig(
          DefaultJedisClientConfig.builder().password(tlsEndpoint.getPassword()).ssl(true)
              .sslSocketFactory(sslSocketFactoryForEnv(tlsEndpoint.getCertificatesLocation()))
              .build())
        .maxAttempts(DEFAULT_REDIRECTIONS).poolConfig(DEFAULT_POOL_CONFIG).build()) {
      assertEquals("PONG", jc.ping());
    }
  }

  /**
   * Tests that connecting with an empty trust store fails.
   */
  @Test
  void connectWithEmptyTrustStore() throws Exception {
    try (RedisClusterClient jc = RedisClusterClient.builder()
        .nodes(Collections.singleton(tlsEndpoint.getHostAndPort()))
        .clientConfig(DefaultJedisClientConfig.builder().password(tlsEndpoint.getPassword())
            .ssl(true).sslSocketFactory(createTrustNoOneSslSocketFactory()).build())
        .maxAttempts(DEFAULT_REDIRECTIONS).poolConfig(DEFAULT_POOL_CONFIG).build()) {
      jc.get("foo");
      fail("Should have thrown an exception");
    } catch (JedisClusterOperationException e) {
      assertEquals("Could not initialize cluster slots cache.", e.getMessage());
    }
  }

  /**
   * Verifies that hostname verification is enabled by default for cluster connections. Cluster
   * initialization should fail when hostname doesn't match certificate CN/SAN.
   */
  @Test
  public void connectWrongHost() {
    // Cluster init with hostname mismatch should fail
    RedisClusterClient.Builder builder = RedisClusterClient.builder();
    builder.nodes(Collections.singleton(tlsEndpointWrongHost.getHostAndPort()))
        .clientConfig(DefaultJedisClientConfig.builder()
            .password(tlsEndpointWrongHost.getPassword()).ssl(true).build());
    assertThrows(JedisClusterOperationException.class, builder::build);
  }

  /**
   * Verifies that hostname verification can be disabled for cluster connections by providing custom
   * SSLParameters without endpoint identification algorithm.
   */
  @Test
  public void connectWrongHostWithSslParameters() {
    // Custom SSLParameters without endpoint identification allows cluster connection despite
    // hostname mismatch
    JedisClientConfig config = DefaultJedisClientConfig.builder().ssl(true)
        .sslParameters(new SSLParameters()).password(tlsEndpointWrongHost.getPassword()).build();
    RedisClusterClient.Builder builder = RedisClusterClient.builder();
    builder.nodes(Collections.singleton(tlsEndpointWrongHost.getHostAndPort()))
        .clientConfig(config);
    try (RedisClusterClient client = builder.build()) {
      assertEquals("PONG", client.ping());
    }
  }
}