Skip to content
Open
59 changes: 59 additions & 0 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -755,6 +756,34 @@
return this;
}

/**
* Defines how strictly the client verifies a server identity on secure connections.
*
* <p>Supported modes:</p>
* <ul>
* <li>{@link SSLMode#DISABLED} - SSL is not used; only meaningful with plain protocols</li>
* <li>{@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</li>
* <li>{@link SSLMode#VERIFY_CA} - validate the server certificate chain, but skip
* hostname verification</li>
* <li>{@link SSLMode#STRICT} - full verification of the certificate chain and the
* hostname (default)</li>
* </ul>
*
* <p>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 <b>not</b> make the client use
* encryption on a plain HTTP endpoint: the endpoint scheme always decides whether the
* connection is encrypted.</p>
*
* @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.
Expand Down Expand Up @@ -1127,7 +1156,7 @@
return this;
}

public Client build() {

Check failure on line 1159 in client-v2/src/main/java/com/clickhouse/client/api/Client.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 26 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ7TbFntIRleBA2bJ1df&open=AZ7TbFntIRleBA2bJ1df&pullRequest=2874
// check if endpoint are empty. so can not initiate client
if (this.endpoints.isEmpty()) {
throw new IllegalArgumentException("At least one endpoint is required");
Expand All @@ -1140,6 +1169,36 @@
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.");
}
}
}
}
Comment thread
chernser marked this conversation as resolved.

// Check timezone settings
String useTimeZoneValue = this.configuration.get(ClientConfigProperties.USE_TIMEZONE.getKey());
String serverTimeZoneValue = this.configuration.get(ClientConfigProperties.SERVER_TIMEZONE.getKey());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>The mode affects only connections that are already using a secure transport (for example,
* an {@code https://} endpoint). It does <b>not</b> enable encryption for plain protocols - an
* {@code http://} endpoint stays unencrypted whatever the mode is.</p>
*
* <p>Modes from the least to the most strict:</p>
* <ul>
* <li>{@link #DISABLED} - SSL is not used. Plain protocols only.</li>
* <li>{@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.</li>
* <li>{@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.</li>
* <li>{@link #STRICT} - full verification (default): certificate chain is validated and the
* hostname must match the certificate.</li>
* </ul>
*/
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()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a predefined map?

if (mode.name().equalsIgnoreCase(value)) {
return mode;
}
}
throw new IllegalArgumentException("Unknown SSL mode '" + value + "'");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -131,7 +130,7 @@ public class HttpAPIClientHelper {

LZ4Factory lz4Factory;

private final ClickHouseDefaultSslContextProvider sslContextProvider = new ClickHouseDefaultSslContextProvider();
private final SslContextProvider sslContextProvider = new SslContextProvider();

public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) {
this.metricsRegistry = metricsRegistry;
Expand Down Expand Up @@ -159,34 +158,46 @@ public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegi
* @return SSLContext
*/
public SSLContext createSSLContext(Map<String, Object> 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;
Expand Down Expand Up @@ -272,7 +283,11 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
LayeredConnectionSocketFactory sslConnectionSocketFactory;
if (sslContext != null) {
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());
if (socketSNI != null && !socketSNI.trim().isEmpty()) {
SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration);
// Trust and VerifyCa skip hostname verification. The same applies when a custom SNI is
// set because the connection hostname will not match the certificate.
boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA;
if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) {
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
} else {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we need to change it to ClientException

}

if (cause instanceof ConnectionRequestTimeoutException ||
cause instanceof NoHttpResponseException ||
cause instanceof ConnectTimeoutException ||
Expand Down
Loading
Loading