diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java
index 58d94da9b..7ce51f064 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java
@@ -12,6 +12,7 @@
import com.clickhouse.client.api.data_formats.internal.ProcessParser;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.enums.ProxyType;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.client.api.insert.InsertResponse;
import com.clickhouse.client.api.insert.InsertSettings;
@@ -755,6 +756,34 @@ public Builder setClientKey(String path) {
return this;
}
+ /**
+ * Defines how strictly the client verifies a server identity on secure connections.
+ *
+ *
Supported modes:
+ *
+ * - {@link SSLMode#DISABLED} - SSL is not used; only meaningful with plain protocols
+ * - {@link SSLMode#TRUST} - encrypt, but accept any server certificate and skip
+ * hostname verification; a configured trust store or CA certificate is ignored (a warning
+ * is logged), while a client certificate/key is still applied for mTLS
+ * - {@link SSLMode#VERIFY_CA} - validate the server certificate chain, but skip
+ * hostname verification
+ * - {@link SSLMode#STRICT} - full verification of the certificate chain and the
+ * hostname (default)
+ *
+ *
+ * The mode applies only when a secure protocol is in use - for the HTTP transport that
+ * means an {@code https://} endpoint. Setting any mode does not make the client use
+ * encryption on a plain HTTP endpoint: the endpoint scheme always decides whether the
+ * connection is encrypted.
+ *
+ * @param sslMode ssl mode
+ * @return same instance of the builder
+ */
+ public Builder setSSLMode(SSLMode sslMode) {
+ this.configuration.put(ClientConfigProperties.SSL_MODE.getKey(), sslMode.name());
+ return this;
+ }
+
/**
* Configure client to use server timezone for date/datetime columns. Default is true.
* If this options is selected then server timezone should be set as well.
@@ -1140,6 +1169,36 @@ public Client build() {
throw new ClientMisconfigurationException("Trust store and certificates cannot be used together");
}
+ // A trust store and a CA certificate are not rejected here: for VERIFY_CA/STRICT the trust
+ // store takes precedence and the CA certificate is ignored with a warning (see createSSLContext).
+
+ // Resolve ssl_mode case-insensitively and normalize it to the canonical enum name so that
+ // downstream parsing is consistent and an unknown value is reported as a misconfiguration
+ // here instead of failing later with a generic enum-parsing error.
+ String sslModeValue = configuration.get(ClientConfigProperties.SSL_MODE.getKey());
+ if (sslModeValue != null) {
+ SSLMode sslMode;
+ try {
+ sslMode = SSLMode.fromValue(sslModeValue);
+ } catch (IllegalArgumentException e) {
+ throw new ClientMisconfigurationException("Invalid value '" + sslModeValue + "' for '"
+ + ClientConfigProperties.SSL_MODE.getKey() + "'", e);
+ }
+ configuration.put(ClientConfigProperties.SSL_MODE.getKey(), sslMode.name());
+
+ // SSLMode.DISABLED does not turn encryption off - the endpoint scheme decides that. So it
+ // contradicts a secure (https) endpoint and must be rejected here, before the client is created.
+ if (sslMode == SSLMode.DISABLED) {
+ for (Endpoint endpoint : this.endpoints) {
+ if ("https".equalsIgnoreCase(endpoint.getURI().getScheme())) {
+ throw new ClientMisconfigurationException("SSL mode '" + SSLMode.DISABLED
+ + "' cannot be used with a secure (https) endpoint. Use '" + SSLMode.TRUST
+ + "' to trust all certificates or use plain HTTP.");
+ }
+ }
+ }
+ }
+
// Check timezone settings
String useTimeZoneValue = this.configuration.get(ClientConfigProperties.USE_TIMEZONE.getKey());
String serverTimeZoneValue = this.configuration.get(ClientConfigProperties.SERVER_TIMEZONE.getKey());
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
index e548a90f9..7008a18ff 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java
@@ -1,6 +1,7 @@
package com.clickhouse.client.api;
import com.clickhouse.client.api.data_formats.internal.AbstractBinaryFormatReader;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.internal.ClickHouseLZ4OutputStream;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.data.ClickHouseFormat;
@@ -115,6 +116,8 @@ public enum ClientConfigProperties {
SSL_CERTIFICATE("sslcert", String.class),
+ SSL_MODE("ssl_mode", SSLMode.class, SSLMode.STRICT.name()),
+
RETRY_ON_FAILURE("retry", Integer.class, "3"),
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java b/client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java
new file mode 100644
index 000000000..f5d64392c
--- /dev/null
+++ b/client-v2/src/main/java/com/clickhouse/client/api/enums/SSLMode.java
@@ -0,0 +1,65 @@
+package com.clickhouse.client.api.enums;
+
+/**
+ * Defines how strictly the client verifies a server identity when a secure protocol is used.
+ *
+ * The mode affects only connections that are already using a secure transport (for example,
+ * an {@code https://} endpoint). It does not enable encryption for plain protocols - an
+ * {@code http://} endpoint stays unencrypted whatever the mode is.
+ *
+ * Modes from the least to the most strict:
+ *
+ * - {@link #DISABLED} - SSL is not used. Plain protocols only.
+ * - {@link #TRUST} - the hostname is not verified and any server certificate is accepted, which
+ * is susceptible to MITM attacks - use that only for testing or in fully trusted environments. A
+ * configured trust store or CA certificate has no effect in this mode and is ignored (a warning is
+ * logged); a configured client certificate/key is still applied for mTLS.
+ * - {@link #VERIFY_CA} - the server certificate chain is validated against the trust material
+ * (default JVM trust store, configured trust store, or a CA certificate), but the hostname is
+ * not checked against the certificate.
+ * - {@link #STRICT} - full verification (default): certificate chain is validated and the
+ * hostname must match the certificate.
+ *
+ */
+public enum SSLMode {
+
+ /**
+ * SSL is not used. Connection is not encrypted. Doesn't work with HTTPS.
+ * Reserved for TCP where protocol doesn't define encryption.
+ */
+ DISABLED,
+
+ /**
+ * The hostname is not verified and any server certificate is accepted. A configured trust store or
+ * CA certificate has no effect in this mode and is ignored (a warning is logged). A configured
+ * client certificate/key is still applied for mTLS.
+ */
+ TRUST,
+
+ /**
+ * Server certificate chain is validated, but the hostname is not verified.
+ */
+ VERIFY_CA,
+
+ /**
+ * Full verification: certificate chain is validated and the hostname must match
+ * the certificate. Default mode for HTTPs.
+ */
+ STRICT;
+
+ /**
+ * Case-insensitive variant of {@link #valueOf(String)}.
+ *
+ * @param value mode name in any case
+ * @return matching mode
+ * @throws IllegalArgumentException when the value does not match any mode
+ */
+ public static SSLMode fromValue(String value) {
+ for (SSLMode mode : values()) {
+ if (mode.name().equalsIgnoreCase(value)) {
+ return mode;
+ }
+ }
+ throw new IllegalArgumentException("Unknown SSL mode '" + value + "'");
+ }
+}
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
index 5ae4730b7..f3e695c5f 100644
--- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
+++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java
@@ -11,9 +11,9 @@
import com.clickhouse.client.api.DataTransferException;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.enums.ProxyType;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.client.api.transport.Endpoint;
-import com.clickhouse.client.config.ClickHouseDefaultSslContextProvider;
import com.clickhouse.data.ClickHouseFormat;
import net.jpountz.lz4.LZ4Factory;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
@@ -85,7 +85,6 @@
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
-import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
@@ -131,7 +130,7 @@ public class HttpAPIClientHelper {
LZ4Factory lz4Factory;
- private final ClickHouseDefaultSslContextProvider sslContextProvider = new ClickHouseDefaultSslContextProvider();
+ private final SslContextProvider sslContextProvider = new SslContextProvider();
public HttpAPIClientHelper(Map configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) {
this.metricsRegistry = metricsRegistry;
@@ -159,34 +158,46 @@ public HttpAPIClientHelper(Map configuration, Object metricsRegi
* @return SSLContext
*/
public SSLContext createSSLContext(Map configuration) {
- SSLContext sslContext;
- try {
- sslContext = SSLContext.getDefault();
- } catch (NoSuchAlgorithmException e) {
- throw new ClientException("Failed to create default SSL context", e);
- }
+ final SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration);
final String trustStorePath = (String) configuration.get(ClientConfigProperties.SSL_TRUST_STORE.getKey());
final String caCertificate = (String) configuration.get(ClientConfigProperties.CA_CERTIFICATE.getKey());
final String sslCertificate = (String) configuration.get(ClientConfigProperties.SSL_CERTIFICATE.getKey());
final String sslKey = (String) configuration.get(ClientConfigProperties.SSL_KEY.getKey());
- if (trustStorePath != null) {
- try {
- sslContext = sslContextProvider.getSslContextFromKeyStore(
- trustStorePath,
- (String) configuration.get(ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey()),
- (String) configuration.get(ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey())
- );
- } catch (SSLException e) {
- throw new ClientMisconfigurationException("Failed to create SSL context from a keystore", e);
+
+ SslContextProvider.Builder builder = sslContextProvider.builder();
+
+ // The client certificate/key (mTLS) are independent of how the server certificate is verified,
+ // so they are applied whenever configured, regardless of the SSL mode.
+ if (sslCertificate != null && !sslCertificate.isEmpty()) {
+ builder.clientCertificate(sslCertificate, sslKey);
+ }
+
+ if (sslMode == SSLMode.TRUST) {
+ // TRUST accepts any server certificate and skips the hostname check (the latter is applied
+ // where the connection socket factory is created). A configured trust store or CA
+ // certificate has no effect in this mode and is ignored with a warning.
+ if (trustStorePath != null || caCertificate != null) {
+ LOG.warn("SSL mode '{}' trusts any server certificate; the configured {} is ignored.",
+ SSLMode.TRUST, trustStorePath != null ? "trust store" : "CA certificate");
}
- } else if (caCertificate != null || sslCertificate != null|| sslKey != null) {
- try {
- sslContext = sslContextProvider.getSslContextFromCerts(sslCertificate, sslKey, caCertificate);
- } catch (SSLException e) {
- throw new ClientMisconfigurationException("Failed to create SSL context from certificates", e);
+ builder.trustAllCertificates();
+ } else if (trustStorePath != null) {
+ // VERIFY_CA / STRICT: validate against the trust store. A trust store and a CA certificate
+ // cannot both take effect, so the CA certificate is ignored with a warning.
+ if (caCertificate != null) {
+ LOG.warn("Both a trust store and a CA certificate are configured; using the trust store and"
+ + " ignoring the CA certificate. Import the CA certificate into the trust store instead.");
}
+ builder.trustStore(trustStorePath,
+ (String) configuration.get(ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey()),
+ (String) configuration.get(ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey()));
+ } else if (caCertificate != null) {
+ // VERIFY_CA / STRICT: validate against the CA certificate.
+ builder.rootCertificate(caCertificate);
}
- return sslContext;
+ // else VERIFY_CA / STRICT with no trust material: the JVM default trust store is used.
+
+ return builder.build();
}
private static final long CONNECTION_INACTIVITY_CHECK = 5000L;
@@ -272,7 +283,11 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map true);
} else {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
@@ -880,6 +895,10 @@ public RuntimeException wrapException(String message, Exception cause, String qu
return (RuntimeException) cause;
}
+ if (cause instanceof SSLException) {
+ return new ClickHouseException("SSL Problem", cause, queryId);
+ }
+
if (cause instanceof ConnectionRequestTimeoutException ||
cause instanceof NoHttpResponseException ||
cause instanceof ConnectTimeoutException ||
diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/SslContextProvider.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/SslContextProvider.java
new file mode 100644
index 000000000..65d3dd289
--- /dev/null
+++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/SslContextProvider.java
@@ -0,0 +1,281 @@
+package com.clickhouse.client.api.internal;
+
+import com.clickhouse.client.api.ClientMisconfigurationException;
+import com.clickhouse.data.ClickHouseUtils;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+
+/**
+ * Builds {@link SSLContext} instances for the {@code client-v2} HTTP transport.
+ *
+ * This is the {@code client-v2} owned counterpart of the deprecated v1
+ * {@code ClickHouseDefaultSslContextProvider}. It is kept separate so the v1 provider can evolve
+ * (or stay frozen) independently of {@code client-v2}.
+ *
+ * Contexts are assembled through {@link #builder()}, which sets key material (client
+ * certificate/key for mTLS) and trust material (trust store, CA certificate, or "trust all")
+ * independently, mirroring the structure of the v1 {@code getSslContextImpl}.
+ */
+public class SslContextProvider {
+
+ // Defaults copied from the v1 com.clickhouse.client.config.ClickHouseDefaults to avoid depending
+ // on the deprecated v1 configuration classes.
+ private static final String SSL_PROTOCOL = "TLS";
+ private static final String KEY_ALGORITHM = "RSA";
+ private static final String CERTIFICATE_TYPE = "X.509";
+
+ static final String PEM_HEADER_PREFIX = "---BEGIN ";
+ static final String PEM_HEADER_SUFFIX = " PRIVATE KEY---";
+ static final String PEM_FOOTER_PREFIX = "---END ";
+
+ /** Standard PEM encapsulation boundary (RFC 7468). Present in any PEM content, never in a file path. */
+ static final String PEM_BEGIN_MARKER = "-----BEGIN";
+
+ /**
+ * Opens a stream over PEM material that may be supplied either as a file path (also searched in the home
+ * directory and on the classpath) or directly as PEM content.
+ *
+ * @param certOrContent file path or PEM content of a certificate or a private key
+ * @return stream over the PEM content
+ * @throws IOException when the value is a path and the file cannot be opened
+ */
+ static InputStream getCertificateInputStream(String certOrContent) throws IOException {
+ if (certOrContent.contains(PEM_BEGIN_MARKER)) {
+ return new ByteArrayInputStream(certOrContent.getBytes(StandardCharsets.US_ASCII));
+ }
+ return ClickHouseUtils.getFileInputStream(certOrContent);
+ }
+
+ /**
+ * An insecure {@link javax.net.ssl.TrustManager}, that don't validate the
+ * certificate.
+ */
+ static class NonValidatingTrustManager implements X509TrustManager {
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+
+ @Override
+ @SuppressWarnings("squid:S4830")
+ public void checkClientTrusted(X509Certificate[] certs, String authType) {
+ // ignore
+ }
+
+ @Override
+ @SuppressWarnings("squid:S4830")
+ public void checkServerTrusted(X509Certificate[] certs, String authType) {
+ // ignore
+ }
+ }
+
+ static String getAlgorithm(String header, String defaultAlg) {
+ int startIndex = header.indexOf(PEM_HEADER_PREFIX);
+ int endIndex = startIndex < 0 ? startIndex
+ : header.indexOf(PEM_HEADER_SUFFIX, (startIndex += PEM_HEADER_PREFIX.length()));
+ return startIndex < endIndex ? header.substring(startIndex, endIndex) : defaultAlg;
+ }
+
+ public static PrivateKey getPrivateKey(String keyFile)
+ throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ String algorithm = KEY_ALGORITHM;
+ StringBuilder builder = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(getCertificateInputStream(keyFile)))) {
+ String line = reader.readLine();
+ if (line != null) {
+ algorithm = getAlgorithm(line, algorithm);
+
+ while ((line = reader.readLine()) != null) {
+ if (line.contains(PEM_FOOTER_PREFIX)) {
+ break;
+ }
+
+ builder.append(line);
+ }
+ }
+ }
+ byte[] encoded = Base64.getDecoder().decode(builder.toString());
+ KeyFactory kf = KeyFactory.getInstance(algorithm);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
+ return kf.generatePrivate(keySpec);
+ }
+
+ public KeyStore getKeyStore(String cert, String key) throws NoSuchAlgorithmException, InvalidKeySpecException,
+ IOException, CertificateException, KeyStoreException {
+ final KeyStore ks;
+ try {
+ ks = KeyStore.getInstance(KeyStore.getDefaultType());
+ ks.load(null, null); // needed to initialize the key store
+ } catch (KeyStoreException e) {
+ throw new NoSuchAlgorithmException(
+ String.format("%s KeyStore not available", KeyStore.getDefaultType()));
+ }
+
+ try (InputStream in = getCertificateInputStream(cert)) {
+ CertificateFactory factory = CertificateFactory.getInstance(CERTIFICATE_TYPE);
+ if (key == null || key.isEmpty()) {
+ int index = 1;
+ for (Certificate c : factory.generateCertificates(in)) {
+ ks.setCertificateEntry("cert" + (index++), c);
+ }
+ } else {
+ Certificate[] certChain = factory.generateCertificates(in).toArray(new Certificate[0]);
+ ks.setKeyEntry("key", getPrivateKey(key), null, certChain);
+ }
+ }
+ return ks;
+ }
+
+ /**
+ * Creates a new {@link Builder} for assembling an {@link SSLContext}. Key material (client
+ * certificate/key for mTLS) and trust material (trust store, CA certificate, or "trust all") are
+ * configured independently, mirroring the structure of the v1 {@code getSslContextImpl}.
+ *
+ * @return new builder
+ */
+ public Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Assembles an {@link SSLContext} from independently configured key and trust material.
+ *
+ * The two are orthogonal:
+ *
+ * - {@link #clientCertificate(String, String)} sets the client certificate/key applied for
+ * mTLS; it is independent of how the server certificate is verified.
+ * - {@link #trustAllCertificates()}, {@link #trustStore(String, String, String)} and
+ * {@link #rootCertificate(String)} are mutually exclusive trust strategies; the last one set
+ * wins. When none is set, the JVM default trust store is used.
+ *
+ */
+ public class Builder {
+
+ private String clientCert;
+ private String clientKey;
+ private String trustStorePath;
+ private String trustStorePassword;
+ private String trustStoreType;
+ private String rootCertificate;
+ private boolean trustAll;
+
+ /**
+ * Sets the client certificate and key applied for mutual TLS. Independent of the trust strategy.
+ *
+ * @param clientCert client certificate, file path or PEM content; may be null
+ * @param clientKey client private key, file path or PEM content; may be null
+ * @return this builder
+ */
+ public Builder clientCertificate(String clientCert, String clientKey) {
+ this.clientCert = clientCert;
+ this.clientKey = clientKey;
+ return this;
+ }
+
+ /**
+ * Trust strategy: accept any server certificate without validating it (no server identity check).
+ *
+ * @return this builder
+ */
+ public Builder trustAllCertificates() {
+ this.trustAll = true;
+ return this;
+ }
+
+ /**
+ * Trust strategy: validate the server certificate against the given trust store.
+ *
+ * @param path trust store file path
+ * @param password trust store password; may be null
+ * @param type trust store type; when null or empty the JVM default type is used
+ * @return this builder
+ */
+ public Builder trustStore(String path, String password, String type) {
+ this.trustStorePath = path;
+ this.trustStorePassword = password;
+ this.trustStoreType = type;
+ return this;
+ }
+
+ /**
+ * Trust strategy: validate the server certificate against the given CA certificate.
+ *
+ * @param rootCertificate CA certificate, file path or PEM content; may be null
+ * @return this builder
+ */
+ public Builder rootCertificate(String rootCertificate) {
+ this.rootCertificate = rootCertificate;
+ return this;
+ }
+
+ /**
+ * Builds the SSL context from the configured key and trust material.
+ *
+ * @return SSL context
+ * @throws ClientMisconfigurationException when the context cannot be created
+ */
+ public SSLContext build() {
+ try {
+ KeyManager[] kms = null;
+ if (clientCert != null && !clientCert.isEmpty()) {
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(getKeyStore(clientCert, clientKey), null);
+ kms = kmf.getKeyManagers();
+ }
+
+ TrustManager[] tms = null;
+ if (trustAll) {
+ tms = new TrustManager[]{new NonValidatingTrustManager()};
+ } else if (trustStorePath != null && !trustStorePath.isEmpty()) {
+ String type = trustStoreType == null || trustStoreType.isEmpty()
+ ? KeyStore.getDefaultType() : trustStoreType;
+ try (InputStream in = ClickHouseUtils.getFileInputStream(trustStorePath)) {
+ KeyStore trustStore = KeyStore.getInstance(type);
+ trustStore.load(in, trustStorePassword == null ? null : trustStorePassword.toCharArray());
+ TrustManagerFactory tmf = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(trustStore);
+ tms = tmf.getTrustManagers();
+ }
+ } else if (rootCertificate != null && !rootCertificate.isEmpty()) {
+ TrustManagerFactory tmf = TrustManagerFactory
+ .getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(getKeyStore(rootCertificate, null));
+ tms = tmf.getTrustManagers();
+ }
+
+ SSLContext ctx = SSLContext.getInstance(SSL_PROTOCOL);
+ ctx.init(kms, tms, new SecureRandom());
+ return ctx;
+ } catch (GeneralSecurityException | IOException e) {
+ throw new ClientMisconfigurationException("Failed to create SSL context", e);
+ }
+ }
+ }
+}
diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java
index eaa675349..43b2d0e80 100644
--- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java
+++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java
@@ -332,7 +332,7 @@ public void testDefaultSettings() {
Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match");
}
}
- Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added.
+ Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added.
}
try (Client client = new Client.Builder()
@@ -365,7 +365,7 @@ public void testDefaultSettings() {
.setSocketSndbuf(100000)
.build()) {
Map config = client.getConfiguration();
- Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added.
+ Assert.assertEquals(config.size(), 36); // to check everything is set. Increment when new added.
Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb");
Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10");
Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000");
@@ -389,6 +389,7 @@ public void testDefaultSettings() {
Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.getKey()), "20000");
Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_RCVBUF_OPT.getKey()), "100000");
Assert.assertEquals(config.get(ClientConfigProperties.SOCKET_SNDBUF_OPT.getKey()), "100000");
+ Assert.assertEquals(config.get(ClientConfigProperties.SSL_MODE.getKey()), "STRICT");
}
}
@@ -432,7 +433,7 @@ public void testWithOldDefaults() {
Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match");
}
}
- Assert.assertEquals(config.size(), 34); // to check everything is set. Increment when new added.
+ Assert.assertEquals(config.size(), 35); // to check everything is set. Increment when new added.
}
}
diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java
index 5c6384a60..6d1a27010 100644
--- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java
+++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java
@@ -1,5 +1,6 @@
package com.clickhouse.client;
+import com.clickhouse.client.api.ClickHouseException;
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.ClientConfigProperties;
import com.clickhouse.client.api.ClientException;
@@ -7,6 +8,7 @@
import com.clickhouse.client.api.ClientMisconfigurationException;
import com.clickhouse.client.api.ConnectionInitiationException;
import com.clickhouse.client.api.ConnectionReuseStrategy;
+import com.clickhouse.client.api.DataTransferException;
import com.clickhouse.client.api.ServerException;
import com.clickhouse.client.api.Session;
import com.clickhouse.client.api.command.CommandResponse;
@@ -14,6 +16,7 @@
import com.clickhouse.client.api.data_formats.ClickHouseBinaryFormatReader;
import com.clickhouse.client.api.enums.Protocol;
import com.clickhouse.client.api.enums.ProxyType;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.client.api.insert.InsertResponse;
import com.clickhouse.client.api.insert.InsertSettings;
@@ -54,9 +57,11 @@
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.testcontainers.utility.ThrowingFunction;
import org.testng.Assert;
+import org.testng.SkipException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import javax.net.ssl.SSLHandshakeException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
@@ -291,6 +296,180 @@ public void testSecureConnection() {
}
}
+ @Test(groups = { "integration" })
+ public void testSSLModeTrust() {
+ if (isCloud()) {
+ throw new SkipException("Test uses a self-signed certificate, not applicable to cloud");
+ }
+
+ ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP);
+
+ // Default mode (Strict) without any trust material - the self-signed certificate must be rejected
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .build()) {
+ Assert.expectThrows(Exception.class, () -> client.queryAll("SELECT 1"));
+ }
+
+ // Trust mode - the same certificate is accepted without any trust material
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setOption(ClientConfigProperties.SSL_MODE.getKey(), SSLMode.TRUST.name())
+ .build()) {
+ List records = client.queryAll("SELECT timezone()");
+ Assert.assertEquals(records.get(0).getString(1), "UTC");
+ } catch (Exception e) {
+ Assert.fail("Trust SSL mode should accept a self-signed certificate", e);
+ }
+
+ // Trust mode ignores a configured trust store (a warning is logged). A non-existent path proves
+ // the trust store is never loaded - the connection still succeeds by trusting any certificate.
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setSSLMode(SSLMode.TRUST)
+ .setSSLTrustStore("non-existent-trust-store.jks")
+ .build()) {
+ List records = client.queryAll("SELECT timezone()");
+ Assert.assertEquals(records.get(0).getString(1), "UTC");
+ } catch (Exception e) {
+ Assert.fail("Trust SSL mode should ignore a configured trust store", e);
+ }
+
+ // Trust mode ignores a configured CA certificate as well (a warning is logged).
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setSSLMode(SSLMode.TRUST)
+ .setRootCertificate("non-existent-ca.crt")
+ .build()) {
+ List records = client.queryAll("SELECT timezone()");
+ Assert.assertEquals(records.get(0).getString(1), "UTC");
+ } catch (Exception e) {
+ Assert.fail("Trust SSL mode should ignore a configured CA certificate", e);
+ }
+ }
+
+ @Test(groups = { "integration" })
+ public void testSSLModeVerifyCa() {
+ if (isCloud()) {
+ throw new SkipException("Test uses a self-signed certificate, not applicable to cloud");
+ }
+
+ ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP);
+ // server certificate has CN=localhost, so connecting via 127.0.0.1 fails hostname verification
+ final String endpointByIp = "https://127.0.0.1:" + secureServer.getPort();
+ final String serverCertificate = "containers/clickhouse-server/certs/localhost.crt";
+
+ // Strict mode (default): certificate chain is trusted, but the hostname does not match
+ try (Client client = new Client.Builder()
+ .addEndpoint(endpointByIp)
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setRootCertificate(serverCertificate)
+ .build()) {
+ Assert.expectThrows(Exception.class, () -> client.queryAll("SELECT 1"));
+ }
+
+ // VerifyCa mode: certificate chain is validated, hostname mismatch is ignored
+ try (Client client = new Client.Builder()
+ .addEndpoint(endpointByIp)
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setRootCertificate(serverCertificate)
+ .setSSLMode(SSLMode.VERIFY_CA)
+ .build()) {
+ List records = client.queryAll("SELECT timezone()");
+ Assert.assertEquals(records.get(0).getString(1), "UTC");
+ } catch (Exception e) {
+ Assert.fail("VerifyCa SSL mode should ignore hostname mismatch", e);
+ }
+
+ // VerifyCa mode still validates the certificate chain - without the CA it must fail
+ try (Client client = new Client.Builder()
+ .addEndpoint(endpointByIp)
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setSSLMode(SSLMode.VERIFY_CA)
+ .build()) {
+ Assert.expectThrows(Exception.class, () -> client.queryAll("SELECT 1"));
+ }
+ }
+
+ @Test(groups = { "integration" })
+ public void testSSLModeDisabled() {
+ if (isCloud()) {
+ throw new SkipException("Plain HTTP is not available on cloud");
+ }
+
+ ClickHouseNode server = getServer(ClickHouseProtocol.HTTP);
+
+ // Disabled mode with a plain HTTP endpoint - SSL is simply not used
+ try (Client client = new Client.Builder()
+ .addEndpoint("http://" + server.getHost() + ":" + server.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setSSLMode(SSLMode.DISABLED)
+ .build()) {
+ List records = client.queryAll("SELECT timezone()");
+ Assert.assertEquals(records.get(0).getString(1), "UTC");
+ } catch (Exception e) {
+ Assert.fail("Disabled SSL mode should work with a plain HTTP endpoint", e);
+ }
+
+ ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP);
+ // Disabled mode contradicts a secure (https) endpoint - the scheme decides encryption, not the mode
+ Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setSSLMode(SSLMode.DISABLED)
+ .build());
+ }
+
+ @Test(groups = { "integration" })
+ public void testSSLModeStrictWithTrustStoreAndCaCertificate() {
+ if (isCloud()) {
+ throw new SkipException("Test uses a self-signed certificate, not applicable to cloud");
+ }
+
+ ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP);
+ // A trust store and a CA certificate cannot both take effect: the trust store is used and the
+ // CA certificate is ignored (a warning is logged). The connection still succeeds via the trust store.
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .build()) {
+ ClientException ex = Assert.expectThrows(ClientException.class, () -> client.queryAll("SELECT timezone()"));
+
+ Assert.assertTrue(ex.getCause() instanceof ClickHouseException);
+ Assert.assertTrue(ex.getCause().getMessage().startsWith("SSL Problem"));
+ }
+
+ // A trust store and a CA certificate cannot both take effect: the trust store is used and the
+ // CA certificate is ignored (a warning is logged). The connection still succeeds via the trust store.
+ try (Client client = new Client.Builder()
+ .addEndpoint("https://localhost:" + secureServer.getPort())
+ .setUsername("default")
+ .setPassword(ClickHouseServerForTest.getPassword())
+ .setSSLTrustStore("containers/clickhouse-server/certs/KeyStore.jks")
+ .setSSLTrustStorePassword("iloveclickhouse")
+ .setRootCertificate("containers/clickhouse-server/certs/localhost.crt")
+ .build()) {
+ List records = client.queryAll("SELECT timezone()");
+ Assert.assertEquals(records.get(0).getString(1), "UTC");
+ } catch (Exception e) {
+ Assert.fail("Trust store should be used when a CA certificate is also configured", e);
+ }
+ }
+
@Test(groups = { "integration" }, dataProvider = "NoResponseFailureProvider")
public void testInsertAndNoHttpResponseFailure(String body, int maxRetries, ThrowingFunction function,
boolean shouldFail) {
diff --git a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java
index 81261dc33..113b33090 100644
--- a/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java
+++ b/client-v2/src/test/java/com/clickhouse/client/SettingsTests.java
@@ -1,13 +1,21 @@
package com.clickhouse.client;
import com.clickhouse.client.api.ClientConfigProperties;
+import com.clickhouse.client.api.ClientMisconfigurationException;
import com.clickhouse.client.api.Session;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.insert.InsertSettings;
import com.clickhouse.client.api.internal.ServerSettings;
+import com.clickhouse.client.api.internal.SslContextProvider;
import com.clickhouse.client.api.query.QuerySettings;
import org.testng.Assert;
import org.testng.annotations.Test;
+import javax.net.ssl.SSLContext;
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.security.KeyStore;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Collections;
@@ -25,6 +33,65 @@ void testClientSettings() {
Assert.assertEquals(listB, source);
}
+ @Test
+ void testSSLModeFromValue() {
+ // Every constant resolves from its exact name and is case-insensitive.
+ for (SSLMode mode : SSLMode.values()) {
+ Assert.assertEquals(SSLMode.fromValue(mode.name()), mode);
+ Assert.assertEquals(SSLMode.fromValue(mode.name().toLowerCase()), mode);
+ Assert.assertEquals(SSLMode.fromValue(mode.name().toUpperCase()), mode);
+ }
+
+ // VERIFY_CA matches only with the underscore - matching does not normalize separators.
+ Assert.assertEquals(SSLMode.fromValue("verify_ca"), SSLMode.VERIFY_CA);
+ Assert.assertEquals(SSLMode.fromValue("Verify_Ca"), SSLMode.VERIFY_CA);
+ Assert.assertThrows(IllegalArgumentException.class, () -> SSLMode.fromValue("verifyca"));
+
+ // Unknown and null values are rejected.
+ Assert.assertThrows(IllegalArgumentException.class, () -> SSLMode.fromValue("insecure"));
+ Assert.assertThrows(IllegalArgumentException.class, () -> SSLMode.fromValue(""));
+ Assert.assertThrows(IllegalArgumentException.class, () -> SSLMode.fromValue(null));
+ }
+
+ @Test
+ void testSslContextFromKeyStore() throws Exception {
+ SslContextProvider provider = new SslContextProvider();
+ final String type = "PKCS12";
+ final String password = "secret";
+ File trustStore = createEmptyTrustStore(type, password);
+ try {
+ // Happy path: a readable trust store with the right password yields a usable TLS context.
+ SSLContext ctx = provider.builder().trustStore(trustStore.getAbsolutePath(), password, type).build();
+ Assert.assertNotNull(ctx);
+ Assert.assertEquals(ctx.getProtocol(), "TLS");
+
+ // Wrong password fails the integrity check.
+ Assert.assertThrows(ClientMisconfigurationException.class,
+ () -> provider.builder().trustStore(trustStore.getAbsolutePath(), "wrong", type).build());
+
+ // Missing file.
+ Assert.assertThrows(ClientMisconfigurationException.class,
+ () -> provider.builder().trustStore(trustStore.getAbsolutePath() + ".missing", password, type).build());
+
+ // Unknown keystore type.
+ Assert.assertThrows(ClientMisconfigurationException.class,
+ () -> provider.builder().trustStore(trustStore.getAbsolutePath(), password, "NOT_A_TYPE").build());
+ } finally {
+ Files.deleteIfExists(trustStore.toPath());
+ }
+ }
+
+ private static File createEmptyTrustStore(String type, String password) throws Exception {
+ KeyStore ks = KeyStore.getInstance(type);
+ ks.load(null, null);
+ File file = File.createTempFile("client-v2-truststore", "." + type.toLowerCase());
+ file.deleteOnExit();
+ try (OutputStream out = Files.newOutputStream(file.toPath())) {
+ ks.store(out, password.toCharArray());
+ }
+ return file;
+ }
+
@Test
void testMergeSettings() {
{
diff --git a/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java b/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java
index 044a37de0..36489ecc5 100644
--- a/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java
+++ b/client-v2/src/test/java/com/clickhouse/client/api/ClientBuilderTest.java
@@ -1,5 +1,6 @@
package com.clickhouse.client.api;
+import com.clickhouse.client.api.enums.SSLMode;
import org.testng.Assert;
import org.testng.annotations.Test;
@@ -22,6 +23,44 @@ public void testAddEndpointToleratesUnderscoreHostname() throws Exception {
}
}
+ @Test
+ public void testSslModeDisabledRejectedForHttpsRegardlessOfCase() {
+ // Value supplied as a raw (non-canonical case) string via setOption must still be recognized
+ // as DISABLED and rejected with ClientMisconfigurationException for an https endpoint.
+ for (String value : new String[] { "DISABLED", "disabled", "Disabled" }) {
+ Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .setOption(ClientConfigProperties.SSL_MODE.getKey(), value)
+ .build());
+ }
+ }
+
+ @Test
+ public void testSslModeInvalidValueRejected() {
+ Assert.expectThrows(ClientMisconfigurationException.class, () -> new Client.Builder()
+ .addEndpoint("https://localhost:8443")
+ .setUsername("default")
+ .setPassword("")
+ .setOption(ClientConfigProperties.SSL_MODE.getKey(), "insecure")
+ .build());
+ }
+
+ @Test
+ public void testSslModeNormalizedToCanonicalName() throws Exception {
+ // A non-canonical case value is accepted and normalized to the canonical enum name.
+ try (Client client = new Client.Builder()
+ .addEndpoint("http://localhost:8123")
+ .setUsername("default")
+ .setPassword("")
+ .setOption(ClientConfigProperties.SSL_MODE.getKey(), "trust")
+ .build()) {
+ Assert.assertEquals(client.getConfiguration().get(ClientConfigProperties.SSL_MODE.getKey()),
+ SSLMode.TRUST.name());
+ }
+ }
+
private static String extractFirstEndpointUri(Client client) throws Exception {
Field endpointsField = Client.class.getDeclaredField("endpoints");
endpointsField.setAccessible(true);
diff --git a/docs/features.md b/docs/features.md
index be63e9f99..29cdfb8e4 100644
--- a/docs/features.md
+++ b/docs/features.md
@@ -5,7 +5,8 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
## `client-v2`
- HTTP and HTTPS connectivity: Connects to ClickHouse over HTTP(S), supports endpoint paths, and exposes a basic `ping` health check.
-- TLS configuration: Supports trust stores, client certificates/keys, SSL certificate authentication, and SNI for HTTPS connections.
+- TLS configuration: Supports trust stores, client certificates/keys, SSL certificate authentication, and SNI for HTTPS connections. Trust material (root CA and client certificate/key) can be supplied either as a file path or directly as PEM content.
+- SSL verification modes: `Client.Builder.setSSLMode(SSLMode)` (or the `ssl_mode` property) controls how strictly the server identity is verified on secure connections: `DISABLED` (SSL not used; plain protocols only), `TRUST` (accept any server certificate and skip hostname verification; a configured trust store or CA certificate is ignored with a warning, while a client certificate/key is still applied for mTLS if configured), `VERIFY_CA` (validate the certificate chain but skip hostname verification), and `STRICT` (full chain and hostname verification, default).
- Authentication modes: Supports username/password credentials, ClickHouse auth headers, bearer tokens, and optional HTTP Basic authentication.
- Runtime credential updates: Existing `Client` instances can update username/password or bearer-token credentials for subsequent requests without rebuilding the client.
- Proxy support: Can send requests through configured HTTP proxies, including proxy credentials.
@@ -41,13 +42,15 @@ Compatibility-sensitive traits:
- `Geometry` handling is shape-sensitive: supported values are 1D through 4D Java arrays representing the nested geometry variants, and unsupported shapes or non-array values are rejected during serialization.
- `Geometry` write inference is dimension-based rather than fully type-specific: point, ring/line string, polygon/multi-line string, and multi-polygon are selected from array depth, so writing `Geometry` cannot currently distinguish `Ring` from `LineString` or `Polygon` from `MultiLineString`.
- Session precedence is part of the contract: client session defaults apply to each request, operation settings may override them, and only the client `session_id` is mutable at runtime while other client session properties remain fixed for the lifetime of the client.
+- SSL mode behavior is compatibility-sensitive: the default is `STRICT`. `ssl_mode` does not enable or disable encryption - the endpoint scheme decides that. `DISABLED` is only valid with a plain `http://` endpoint; combining it with an `https://` endpoint throws `ClientMisconfigurationException`. `TRUST` accepts any server certificate and skips hostname verification; a configured trust store or CA certificate has no effect in this mode and is ignored (a warning is logged), while a client certificate/key is still applied for mTLS. For `VERIFY_CA` and `STRICT`, a trust store and a CA certificate cannot both take effect: when both are configured the trust store is used and the CA certificate is ignored (a warning is logged). A trust store and a client certificate (`sslcert`) still cannot be configured together and throw `ClientMisconfigurationException`. `ssl_mode` values are matched case-insensitively and normalized to the canonical enum name (`DISABLED`, `TRUST`, `VERIFY_CA`, `STRICT`) when the client is built; an unrecognized value throws `ClientMisconfigurationException`.
+- Certificate-as-content support is compatibility-sensitive: any certificate or key value containing a PEM begin marker (`-----BEGIN`) is treated as inline PEM content, otherwise it is treated as a file path (also searched in the home directory and on the classpath).
## `jdbc-v2`
- JDBC driver registration: Registers through the standard JDBC service mechanism and is available through `DriverManager`.
- JDBC URL parsing: Accepts `jdbc:clickhouse:` and `jdbc:ch:` URLs with host, port, optional HTTP path, optional database, and query parameters.
-- SSL URL support: Supports HTTPS connections through URL and property configuration, including default protocol and port handling.
+- SSL URL support: Supports HTTPS connections through URL and property configuration, including default protocol and port handling. The `ssl_mode` property selects the verification strictness (`disabled`, `trust`, `verify_ca`, `strict`); values are case-insensitive and the traditional JDBC value `none` is accepted as an alias for `trust`. Root CA and client certificate/key may be supplied as a file path or as inline PEM content.
- Driver and client properties: Separates JDBC-specific properties from passthrough client options used by the underlying `client-v2` transport.
- DataSource support: Provides a JDBC `DataSource` implementation backed by the same driver configuration model.
- Connection lifecycle: Supports connection close, validity checks, ping-based health checks, and network timeout management.
@@ -85,4 +88,5 @@ Compatibility-sensitive traits:
- `getString()` formatting for temporal values is stable output: `Date` uses `yyyy-MM-dd`, `DateTime` uses `yyyy-MM-dd HH:mm:ss`, and `DateTime64` preserves fractional precision, all interpreted in server timezone context where applicable.
- Date and timestamp setters with `Calendar` are timezone-sensitive by design. Preserving the current day-shift and instant-preserving behavior is important for compatibility.
- `setObject()` temporal behavior is specific and should not drift: `LocalDateTime` and `Instant` are rendered through `fromUnixTimestamp64Nano(...)`, while `Timestamp` and `Date` use quoted textual forms.
+- JDBC `ssl_mode` handling is compatibility-sensitive: values are case-insensitive, `none` is aliased to `trust` (the no-verification mode), and an unrecognized value throws `SQLException` during connection configuration. The normalized canonical mode name is forwarded to the underlying `client-v2` transport.
- INSERT result semantics depend on server-side `async_insert` and `wait_for_async_insert`. The driver does not override these settings, so it follows whatever the server profile or user configuration sets. When `async_insert=1` and `wait_for_async_insert=0`, `Statement.executeUpdate(...)` and `PreparedStatement.executeUpdate(...)` may return `0` (or an under-counted value), and parsing/data errors in the INSERT body may not be reported synchronously as a `SQLException`. Set `async_insert=0` (or `wait_for_async_insert=1`) per connection or statement to restore synchronous row counts and error reporting.
diff --git a/examples/client-v2/README.md b/examples/client-v2/README.md
index 0f6bab14e..e807c07c3 100644
--- a/examples/client-v2/README.md
+++ b/examples/client-v2/README.md
@@ -78,10 +78,15 @@ Notes:
## SSL Examples
-`com.clickhouse.examples.client_v2.SSLExamples` shows how to connect securely to a server whose
-certificate is signed by a custom (private) CA. Only the CA certificate is passed to the client
-with `Client.Builder.setRootCertificate()` - no trust store configuration is required, and the JVM
-default trust store stays untouched.
+`com.clickhouse.examples.client_v2.SSLExamples` shows how to connect securely to a server:
+
+- **Custom CA certificate** - the server certificate is signed by a custom (private) CA. Only the
+ CA certificate is passed to the client with `Client.Builder.setRootCertificate()` (as a file path
+ or directly as a PEM string) - no trust store configuration is required, and the JVM default
+ trust store stays untouched.
+- **Self-signed certificate without verification** - `Client.Builder.setSSLMode(SSLMode.TRUST)`
+ accepts any server certificate and skips hostname verification. The connection is encrypted, but
+ the server identity is not verified - use it only for testing or in fully trusted environments.
The example runs in one of two modes.
@@ -113,7 +118,8 @@ mvn exec:java -Dexec.mainClass="com.clickhouse.examples.client_v2.SSLExamples" \
-DchRootCert="/path/to/ca.crt"
```
-`-DchRootCert` is required in this mode and must point to the CA certificate in PEM format.
+`-DchRootCert` must point to the CA certificate in PEM format. When it is omitted, only the
+self-signed (`SSLMode.TRUST`) example runs - useful when you do not have the CA certificate at hand.
### Setting up a Docker dev instance with a self-signed certificate manually
diff --git a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
index dc62b65c2..520b82a03 100644
--- a/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
+++ b/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
@@ -1,6 +1,7 @@
package com.clickhouse.examples.client_v2;
import com.clickhouse.client.api.Client;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.query.GenericRecord;
import lombok.extern.slf4j.Slf4j;
@@ -22,6 +23,9 @@
* Passing the CA certificate as a PEM string instead of a file path - useful when the
* certificate comes from an environment variable or a secret manager (typical for
* Kubernetes/cloud deployments) and you do not want to write it to disk.
+ * Connecting to a server with a self-signed certificate without any trust material -
+ * {@link SSLMode#TRUST} accepts any server certificate and skips hostname verification.
+ * Use it only for testing or in fully trusted environments.
*
*
* More SSL examples (mTLS, trust stores, SNI) will be added to this class later.
@@ -41,7 +45,8 @@
* {@code chPort} - ClickHouse HTTPS port, default {@code 8443}
* {@code chDatabase} - database name, default {@code default}
* {@code chUser} and {@code chPassword} - credentials (standalone mode)
- * {@code chRootCert} - path to the root CA certificate in PEM format (required in standalone mode)
+ * {@code chRootCert} - path to the root CA certificate in PEM format. When omitted in
+ * standalone mode, only the self-signed (TRUST) example runs
* {@code chImage} - Docker image for local mode, default {@code clickhouse/clickhouse-server:latest}
*
*/
@@ -58,16 +63,17 @@ public static void main(String[] args) {
final String user = System.getProperty("chUser", "default");
final String password = System.getProperty("chPassword", "");
final String rootCert = trimToNull(System.getProperty("chRootCert"));
- if (rootCert == null) {
- log.error("chRootCert is required when chHost is set. "
- + "Pass the path to the CA certificate (PEM) that signed the server certificate.");
- return;
- }
log.info("Running in standalone mode against {}:{}", host, port);
String endpoint = "https://" + host + ":" + port;
- connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
- connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
+ connectToSelfSignedServer(endpoint, database, user, password);
+ if (rootCert != null) {
+ connectWithCustomRootCertificate(endpoint, database, user, password, rootCert);
+ connectWithRootCertificateAsString(endpoint, database, user, password, rootCert);
+ } else {
+ log.info("chRootCert is not set - skipping the custom CA certificate examples. "
+ + "Pass the path to the CA certificate (PEM) that signed the server certificate to run them.");
+ }
return;
}
@@ -76,6 +82,8 @@ public static void main(String[] args) {
final String image = System.getProperty("chImage", "clickhouse/clickhouse-server:latest");
log.info("Running in local mode (set -DchHost to verify your own server)");
try (SecureServerSupport server = SecureServerSupport.start(image)) {
+ connectToSelfSignedServer(server.getEndpoint(), database,
+ SecureServerSupport.USER, SecureServerSupport.PASSWORD);
connectWithCustomRootCertificate(server.getEndpoint(), database,
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(server.getEndpoint(), database,
@@ -85,6 +93,35 @@ public static void main(String[] args) {
}
}
+ /**
+ * Connects to a ClickHouse server with a self-signed certificate without providing
+ * any trust material. {@link SSLMode#TRUST} makes the client accept any server
+ * certificate and skip hostname verification.
+ *
+ * Warning: the connection is encrypted, but the server identity is NOT verified,
+ * which makes it susceptible to man-in-the-middle attacks. Use this mode only for testing
+ * or in fully trusted environments. Prefer {@link Client.Builder#setRootCertificate(String)}
+ * with the signing CA certificate whenever possible.
+ */
+ static void connectToSelfSignedServer(String endpoint, String database, String user, String password) {
+ log.info("Connecting to {} accepting any server certificate (SSLMode.TRUST)", endpoint);
+ try (Client client = new Client.Builder()
+ .addEndpoint(endpoint)
+ .setUsername(user)
+ .setPassword(password)
+ .setDefaultDatabase(database)
+ // Accept the self-signed certificate and skip hostname verification.
+ .setSSLMode(SSLMode.TRUST)
+ .build()) {
+
+ List rows = client.queryAll("SELECT currentUser() AS user, version() AS version");
+ log.info("Connected (server certificate not verified) as '{}' to ClickHouse {}",
+ rows.get(0).getString("user"), rows.get(0).getString("version"));
+ } catch (Exception e) {
+ log.error("Connection with SSLMode.TRUST failed", e);
+ }
+ }
+
/**
* Connects to a ClickHouse server using a custom root CA certificate.
* Use this when the server certificate is signed by a private CA (corporate CA,
diff --git a/examples/jdbc/README.md b/examples/jdbc/README.md
index 1fedefa5d..4cb796317 100644
--- a/examples/jdbc/README.md
+++ b/examples/jdbc/README.md
@@ -24,10 +24,16 @@ Addition options can be passed to the application:
## SSL Examples
-`com.clickhouse.examples.jdbc.SSLExamples` shows how to connect securely to a server whose
-certificate is signed by a custom (private) CA. Only the CA certificate is passed with the
-`sslrootcert` connection property - no trust store configuration is required, and the JVM default
-trust store stays untouched.
+`com.clickhouse.examples.jdbc.SSLExamples` shows how to connect securely to a server:
+
+- **Custom CA certificate** - the server certificate is signed by a custom (private) CA. Only the
+ CA certificate is passed with the `sslrootcert` connection property (as a file path or directly
+ as a PEM string) - no trust store configuration is required, and the JVM default trust store
+ stays untouched.
+- **Self-signed certificate without verification** - the `ssl_mode=trust` connection property
+ (`ssl_mode=none` is accepted as an alias) accepts any server certificate and skips hostname
+ verification. The connection is encrypted, but the server identity is not verified - use it only
+ for testing or in fully trusted environments.
The example runs in one of two modes.
@@ -57,7 +63,8 @@ mvn exec:java -Dexec.mainClass="com.clickhouse.examples.jdbc.SSLExamples" \
-DchRootCert="/path/to/ca.crt"
```
-`-DchRootCert` is required in this mode and must point to the CA certificate in PEM format.
+`-DchRootCert` must point to the CA certificate in PEM format. When it is omitted, only the
+self-signed (`ssl_mode=trust`) example runs - useful when you do not have the CA certificate at hand.
### Setting up a Docker dev instance with a self-signed certificate manually
diff --git a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
index 11a022fb5..f6a1cef1f 100644
--- a/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
+++ b/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
@@ -27,6 +27,10 @@
* Passing the CA certificate as a PEM string instead of a file path - useful when the
* certificate comes from an environment variable or a secret manager (typical for
* Kubernetes/cloud deployments) and you do not want to write it to disk.
+ * Connecting to a server with a self-signed certificate without any trust material -
+ * the {@code ssl_mode=trust} connection property accepts any server certificate and skips
+ * hostname verification ({@code ssl_mode=none} is accepted as an alias). Use it only for
+ * testing or in fully trusted environments.
*
*
* More SSL examples (mTLS, trust stores, SNI) will be added to this class later.
@@ -45,7 +49,8 @@
* {@code chUrl} - ClickHouse JDBC URL, e.g. {@code jdbc:clickhouse://my-host:8443/default}.
* When set, standalone mode is used
* {@code chUser} and {@code chPassword} - credentials (standalone mode)
- * {@code chRootCert} - path to the root CA certificate in PEM format (required in standalone mode)
+ * {@code chRootCert} - path to the root CA certificate in PEM format. When omitted in
+ * standalone mode, only the self-signed (trust) example runs
* {@code chImage} - Docker image for local mode, default {@code clickhouse/clickhouse-server:latest}
*
*/
@@ -60,18 +65,19 @@ public static void main(String[] args) {
final String user = System.getProperty("chUser", "default");
final String password = System.getProperty("chPassword", "");
final String rootCert = trimToNull(System.getProperty("chRootCert"));
- if (rootCert == null) {
- log.error("chRootCert is required when chUrl is set. "
- + "Pass the path to the CA certificate (PEM) that signed the server certificate.");
- return;
- }
log.info("Running in standalone mode against {}", url);
try {
- connectWithCustomRootCertificate(url, user, password, rootCert);
- connectWithRootCertificateAsString(url, user, password, rootCert);
+ connectToSelfSignedServer(url, user, password);
+ if (rootCert != null) {
+ connectWithCustomRootCertificate(url, user, password, rootCert);
+ connectWithRootCertificateAsString(url, user, password, rootCert);
+ } else {
+ log.info("chRootCert is not set - skipping the custom CA certificate examples. "
+ + "Pass the path to the CA certificate (PEM) that signed the server certificate to run them.");
+ }
} catch (SQLException | IOException e) {
- log.error("Secure connection with a custom root CA certificate failed", e);
+ log.error("Secure connection failed", e);
}
return;
}
@@ -81,6 +87,8 @@ public static void main(String[] args) {
final String image = System.getProperty("chImage", "clickhouse/clickhouse-server:latest");
log.info("Running in local mode (set -DchUrl to verify your own server)");
try (SecureServerSupport server = SecureServerSupport.start(image)) {
+ connectToSelfSignedServer(server.getJdbcUrl(),
+ SecureServerSupport.USER, SecureServerSupport.PASSWORD);
connectWithCustomRootCertificate(server.getJdbcUrl(),
SecureServerSupport.USER, SecureServerSupport.PASSWORD, server.getCaCertPath());
connectWithRootCertificateAsString(server.getJdbcUrl(),
@@ -93,6 +101,37 @@ public static void main(String[] args) {
Runtime.getRuntime().exit(0);
}
+ /**
+ * Connects to a ClickHouse server with a self-signed certificate without providing
+ * any trust material. The {@code ssl_mode=trust} connection property makes the driver
+ * accept any server certificate and skip hostname verification. The traditional JDBC
+ * value {@code ssl_mode=none} is accepted as an alias.
+ *
+ * Warning: the connection is encrypted, but the server identity is NOT verified,
+ * which makes it susceptible to man-in-the-middle attacks. Use this mode only for testing
+ * or in fully trusted environments. Prefer {@code sslrootcert} with the signing CA
+ * certificate whenever possible.
+ */
+ static void connectToSelfSignedServer(String url, String user, String password) throws SQLException {
+ log.info("Connecting to {} accepting any server certificate (ssl_mode=trust)", url);
+
+ Properties properties = new Properties();
+ properties.setProperty(ClientConfigProperties.USER.getKey(), user); // user
+ properties.setProperty(ClientConfigProperties.PASSWORD.getKey(), password); // password
+ properties.setProperty("ssl", "true"); // enable TLS even if the URL has no https scheme
+ // Accept the self-signed certificate and skip hostname verification.
+ properties.setProperty(ClientConfigProperties.SSL_MODE.getKey(), "trust"); // ssl_mode
+
+ try (Connection connection = DriverManager.getConnection(url, properties);
+ Statement stmt = connection.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT currentUser() AS user, version() AS version")) {
+ if (rs.next()) {
+ log.info("Connected (server certificate not verified) as '{}' to ClickHouse {}",
+ rs.getString("user"), rs.getString("version"));
+ }
+ }
+ }
+
/**
* Connects to a ClickHouse server using a custom root CA certificate.
* Use this when the server certificate is signed by a private CA (corporate CA,
diff --git a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
index eca74aa53..99669b981 100644
--- a/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
+++ b/jdbc-v2/src/main/java/com/clickhouse/jdbc/internal/JdbcConfiguration.java
@@ -2,6 +2,7 @@
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.ClientConfigProperties;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.http.ClickHouseHttpProto;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.jdbc.Driver;
@@ -332,7 +333,8 @@ private Map parseUrl(String url) throws SQLException {
* @param urlProperties - properties parsed from URL
* @param providedProperties - properties object provided by application
*/
- private void buildFinalProperties(Map urlProperties, Properties providedProperties) {
+ private void buildFinalProperties(Map urlProperties, Properties providedProperties)
+ throws SQLException {
// Copy provided properties
Map props = new HashMap<>();
@@ -379,6 +381,22 @@ private void buildFinalProperties(Map urlProperties, Properties
}
}
+ String sslMode = clientProperties.get(ClientConfigProperties.SSL_MODE.getKey());
+ if (sslMode != null) {
+ if ("none".equalsIgnoreCase(sslMode)) {
+ // JDBC drivers traditionally use 'none' for the no-verification SSL mode - alias it to 'trust'
+ clientProperties.put(ClientConfigProperties.SSL_MODE.getKey(), SSLMode.TRUST.name());
+ } else {
+ try {
+ // values are case-insensitive in JDBC - normalize before passing to the client
+ clientProperties.put(ClientConfigProperties.SSL_MODE.getKey(), SSLMode.fromValue(sslMode).name());
+ } catch (IllegalArgumentException e) {
+ throw new SQLException("Unknown value '" + sslMode + "' for property '"
+ + ClientConfigProperties.SSL_MODE.getKey() + "'", e);
+ }
+ }
+ }
+
// Fill list of client properties information, add not specified properties (doesn't affect client properties)
for (ClientConfigProperties clientProp : ClientConfigProperties.values()) {
DriverPropertyInfo propertyInfo = propertyInfos.get(clientProp.getKey());
diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java
index 0ce66c84c..1b3d5cdae 100644
--- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java
+++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/ConnectionTest.java
@@ -13,6 +13,7 @@
import com.github.tomakehurst.wiremock.common.ConsoleNotifier;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import org.testng.Assert;
+import org.testng.SkipException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -763,6 +764,78 @@ public void testSecureConnection() throws Exception {
}
}
+ @Test(groups = { "integration" })
+ public void testSSLModeTrust() throws Exception {
+ if (isCloud()) {
+ throw new SkipException("Test uses a self-signed certificate, not applicable to cloud");
+ }
+ ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP);
+ String jdbcUrl = "jdbc:clickhouse:" + secureServer.getBaseUri();
+
+ Properties properties = new Properties();
+ properties.put(ClientConfigProperties.USER.getKey(), "default");
+ properties.put(ClientConfigProperties.PASSWORD.getKey(), ClickHouseServerForTest.getPassword());
+
+ // Default mode (strict) without any trust material - the self-signed certificate must be rejected
+ Assert.expectThrows(Exception.class, () -> {
+ try (Connection conn = new ConnectionImpl(jdbcUrl, properties);
+ Statement stmt = conn.createStatement()) {
+ stmt.executeQuery("SELECT 1");
+ }
+ });
+
+ // 'none' is a JDBC alias for the 'trust' mode
+ for (String mode : new String[] { "none", "trust", "Trust" }) {
+ Properties trustProperties = new Properties();
+ trustProperties.putAll(properties);
+ trustProperties.put(ClientConfigProperties.SSL_MODE.getKey(), mode);
+
+ try (Connection conn = new ConnectionImpl(jdbcUrl, trustProperties);
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT number FROM system.numbers LIMIT 10")) {
+
+ int count = 0;
+ while (rs.next()) { count++; }
+ Assert.assertEquals(count, 10, "Failed for ssl_mode '" + mode + "'");
+ }
+ }
+ }
+
+ @Test(groups = { "integration" })
+ public void testSSLModeVerifyCa() throws Exception {
+ if (isCloud()) {
+ throw new SkipException("Test uses a self-signed certificate, not applicable to cloud");
+ }
+ ClickHouseNode secureServer = getSecureServer(ClickHouseProtocol.HTTP);
+ // server certificate has CN=localhost, so connecting via 127.0.0.1 fails hostname verification
+ String jdbcUrl = "jdbc:clickhouse://127.0.0.1:" + secureServer.getPort() + "/";
+
+ Properties properties = new Properties();
+ properties.put(ClientConfigProperties.USER.getKey(), "default");
+ properties.put(ClientConfigProperties.PASSWORD.getKey(), ClickHouseServerForTest.getPassword());
+ properties.put(DriverProperties.SECURE_CONNECTION.getKey(), "true");
+ properties.put(ClientConfigProperties.CA_CERTIFICATE.getKey(), "containers/clickhouse-server/certs/localhost.crt");
+
+ // Default mode (strict): certificate chain is trusted, but the hostname does not match
+ Assert.expectThrows(Exception.class, () -> {
+ try (Connection conn = new ConnectionImpl(jdbcUrl, properties);
+ Statement stmt = conn.createStatement()) {
+ stmt.executeQuery("SELECT 1");
+ }
+ });
+
+ // verify_ca: certificate chain is validated, hostname mismatch is ignored
+ properties.put(ClientConfigProperties.SSL_MODE.getKey(), "verify_ca");
+ try (Connection conn = new ConnectionImpl(jdbcUrl, properties);
+ Statement stmt = conn.createStatement();
+ ResultSet rs = stmt.executeQuery("SELECT number FROM system.numbers LIMIT 10")) {
+
+ int count = 0;
+ while (rs.next()) { count++; }
+ Assert.assertEquals(count, 10);
+ }
+ }
+
@Test(groups = { "integration" })
public void testSelectingDatabase() throws Exception {
if (isCloud()) {
diff --git a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java
index 9e0abe8bc..eba15719c 100644
--- a/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java
+++ b/jdbc-v2/src/test/java/com/clickhouse/jdbc/internal/JdbcConfigurationTest.java
@@ -2,6 +2,7 @@
import com.clickhouse.client.api.Client;
import com.clickhouse.client.api.ClientConfigProperties;
+import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.data.ClickHouseDataType;
import com.clickhouse.jdbc.DriverProperties;
@@ -17,6 +18,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
@@ -169,6 +172,54 @@ public void testConfigurationProperties() throws Exception {
assertEquals(p.value, "default1");
}
+ @DataProvider(name = "sslModeValues")
+ public Object[][] sslModeValues() {
+ return new Object[][] {
+ // input value, expected client property value
+ { "none", SSLMode.TRUST.name() }, // JDBC alias for the no-verification mode
+ { "NONE", SSLMode.TRUST.name() },
+ { "disabled", SSLMode.DISABLED.name() },
+ { "Disabled", SSLMode.DISABLED.name() },
+ { "trust", SSLMode.TRUST.name() },
+ { "Trust", SSLMode.TRUST.name() },
+ { "verify_ca", SSLMode.VERIFY_CA.name() },
+ { "VERIFY_CA", SSLMode.VERIFY_CA.name() },
+ { "strict", SSLMode.STRICT.name() },
+ { "Strict", SSLMode.STRICT.name() },
+ };
+ }
+
+ @Test
+ public void testSSLModeDatasetCoversAllModes() {
+ Set covered = Arrays.stream(sslModeValues())
+ .map(row -> (String) row[1])
+ .collect(Collectors.toSet());
+ Set allModes = Arrays.stream(SSLMode.values())
+ .map(Enum::name)
+ .collect(Collectors.toSet());
+ assertEquals(covered, allModes,
+ "SSLMode constants changed - update the 'sslModeValues' dataset and the ssl_mode handling in JdbcConfiguration");
+ }
+
+ @Test(dataProvider = "sslModeValues")
+ public void testSSLModeProperty(String value, String expected) throws Exception {
+ // passed via Properties
+ Properties properties = new Properties();
+ properties.setProperty(ClientConfigProperties.SSL_MODE.getKey(), value);
+ JdbcConfiguration configuration = new JdbcConfiguration("jdbc:clickhouse://localhost:8123/", properties);
+ assertEquals(configuration.getClientProperties().get(ClientConfigProperties.SSL_MODE.getKey()), expected);
+
+ // passed as a URL parameter
+ configuration = new JdbcConfiguration("jdbc:clickhouse://localhost:8123/?ssl_mode=" + value, new Properties());
+ assertEquals(configuration.getClientProperties().get(ClientConfigProperties.SSL_MODE.getKey()), expected);
+ }
+
+ @Test
+ public void testSSLModeInvalidValue() {
+ assertThrows(SQLException.class,
+ () -> new JdbcConfiguration("jdbc:clickhouse://localhost:8123/?ssl_mode=insecure", new Properties()));
+ }
+
@DataProvider(name = "typeMappingsPropertyKey")
public Object[][] typeMappingsPropertyKey() {
return new Object[][] {