Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
import java.util.function.Supplier;
import java.util.stream.Collectors;

import javax.net.ssl.SSLContext;

/**
* <p>Client is the starting point for all interactions with ClickHouse. </p>
*
Expand Down Expand Up @@ -145,10 +147,16 @@
private final Supplier<String> queryIdGenerator;
private final CredentialsManager credentialsManager;

private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,

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

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Constructor has 8 parameters, which is greater than 7 authorized.

See more on https://sonarcloud.io/project/issues?id=ClickHouse_clickhouse-java&issues=AZ9HqWtfVWej1L9_isBX&open=AZ9HqWtfVWej1L9_isBX&pullRequest=2918
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy,
Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager) {
Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager,
SSLContext sslContext) {
Map<String, Object> parsedConfiguration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration));
// A pre-built SSLContext is a live object and cannot travel through the string-based configuration
// map, so it is injected into the parsed (object) configuration directly.
if (sslContext != null) {
parsedConfiguration.put(ClientConfigProperties.SSL_CONTEXT.getKey(), sslContext);
}
this.credentialsManager = cManager;
this.session = Session.extractFrom(parsedConfiguration);
this.configuration = new ConcurrentHashMap<>(parsedConfiguration);
Expand Down Expand Up @@ -270,6 +278,7 @@
private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy;
private Object metricRegistry = null;
private Supplier<String> queryIdGenerator;
private SSLContext sslContext = null;

public Builder() {
this.endpoints = new HashSet<>();
Expand Down Expand Up @@ -785,6 +794,29 @@
return this;
}

/**
* Supplies a pre-built {@link SSLContext} to be used for secure connections instead of one built
* from the configured trust/key material (trust store, CA certificate, client certificate/key).
*
* <p>When a context is set, the client uses it as is - it is the application's responsibility to
* configure it correctly. Trust- and key-material options ({@link Builder#setSSLTrustStore(String)},
* {@link Builder#setRootCertificate(String)}, {@link Builder#setClientCertificate(String)}, ...)
* are then ignored because they only feed the context the client would otherwise build.
* {@link SSLMode} still applies, but only to server hostname verification: {@link SSLMode#TRUST}
* and {@link SSLMode#VERIFY_CA} skip the hostname check while {@link SSLMode#STRICT} (default)
* enforces it.</p>
*
* <p>This is primarily useful when certificates and keys are held in memory (for example, loaded
* from a secret store) and must never be written to disk.</p>
*
* @param sslContext a fully configured SSL context; {@code null} clears any previously set context
* @return same instance of the builder
*/
public Builder setSSLContext(SSLContext sslContext) {
this.sslContext = sslContext;
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 @@ -1229,7 +1261,8 @@
}

return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor,
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager);
Comment thread
polyglotAI-bot marked this conversation as resolved.
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager,
this.sslContext);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -118,6 +120,15 @@ public enum ClientConfigProperties {

SSL_MODE("ssl_mode", SSLMode.class, SSLMode.STRICT.name()),

/**
* A pre-built {@link javax.net.ssl.SSLContext} supplied by the application. When set, the client uses
* it as is instead of building one from the configured trust/key material, and {@link #SSL_MODE} then
* only controls server hostname verification. The value is a live object, so it can only be provided
* programmatically (for example via {@code Client.Builder#setSSLContext}); it is never parsed from a
* string and has no textual representation in a configuration map.
*/
SSL_CONTEXT("ssl_context", SSLContext.class),

Comment thread
polyglotAI-bot marked this conversation as resolved.
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
Expand Up @@ -159,6 +159,15 @@ public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegi
* @return SSLContext
*/
public SSLContext createSSLContext(Map<String, Object> configuration) {
// A pre-built SSLContext supplied by the application is used as is; the client does not build one
// from the configured trust/key material. Server hostname verification is still governed by the
// SSL mode where the connection socket factory is created (see createHttpClient).
final Object customSSLContext = configuration.get(ClientConfigProperties.SSL_CONTEXT.getKey());
if (customSSLContext instanceof SSLContext) {
LOG.debug("Using application-supplied SSLContext; trust/key material options are ignored.");
return (SSLContext) customSSLContext;
}

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());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.clickhouse.client.api;

import com.clickhouse.client.api.enums.SSLMode;
import com.clickhouse.client.api.internal.HttpAPIClientHelper;
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import javax.net.ssl.SSLContext;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ClientBuilderTest {

Expand Down Expand Up @@ -86,6 +90,78 @@ public void testSslModeInvalidValueRejected() {
.build());
}

@Test
public void testSetSSLContextStoredInConfiguration() throws Exception {
SSLContext customContext = SSLContext.getInstance("TLS");
customContext.init(null, null, null);

try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.setSSLContext(customContext)
.build()) {
Assert.assertSame(extractConfiguration(client).get(ClientConfigProperties.SSL_CONTEXT.getKey()),
customContext, "The application-supplied SSLContext should be stored in the configuration");
}

// Without setSSLContext the key must be absent so the client builds its own context.
try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.build()) {
Assert.assertNull(extractConfiguration(client).get(ClientConfigProperties.SSL_CONTEXT.getKey()),
"No SSLContext should be stored when none is supplied");
}
}

@Test
public void testCreateSSLContextReturnsCustomContext() throws Exception {
SSLContext customContext = SSLContext.getInstance("TLS");
customContext.init(null, null, null);

try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.setSSLContext(customContext)
.build()) {
HttpAPIClientHelper helper = extractHttpClientHelper(client);
SSLContext resolved = helper.createSSLContext(extractConfiguration(client));
Assert.assertSame(resolved, customContext,
"createSSLContext must return the application-supplied context as is");
}

// When no custom context is configured, createSSLContext builds a context (not the custom one).
try (Client client = new Client.Builder()
.addEndpoint("https://localhost:8443")
.setUsername("default")
.setPassword("")
.build()) {
HttpAPIClientHelper helper = extractHttpClientHelper(client);
Map<String, Object> configWithCustom = new HashMap<>(extractConfiguration(client));
configWithCustom.put(ClientConfigProperties.SSL_CONTEXT.getKey(), customContext);
Assert.assertSame(helper.createSSLContext(configWithCustom), customContext,
"createSSLContext must honor a custom context supplied via the configuration map");
Assert.assertNotSame(helper.createSSLContext(extractConfiguration(client)), customContext,
"createSSLContext must build its own context when none is supplied");
}
}

private static HttpAPIClientHelper extractHttpClientHelper(Client client) throws Exception {
Field helperField = Client.class.getDeclaredField("httpClientHelper");
helperField.setAccessible(true);
return (HttpAPIClientHelper) helperField.get(client);
}

@SuppressWarnings("unchecked")
private static Map<String, Object> extractConfiguration(Client client) throws Exception {
Field configField = Client.class.getDeclaredField("configuration");
configField.setAccessible(true);
return (Map<String, Object>) configField.get(client);
}

private static String extractFirstEndpointUri(Client client) throws Exception {
Field endpointsField = Client.class.getDeclaredField("endpoints");
endpointsField.setAccessible(true);
Expand Down
4 changes: 4 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This document lists stable, user-visible behavior in `client-v2` and `jdbc-v2` t
- 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. 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).
- Custom SSL context: `Client.Builder.setSSLContext(SSLContext)` supplies a fully pre-built `javax.net.ssl.SSLContext`. When set, the client uses it as is instead of building one from the trust/key material (trust store, CA certificate, client certificate/key), so those options are ignored; `ssl_mode` then only controls server hostname verification. This is primarily for material assembled in memory that must never be written to disk.
- 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.
Expand Down Expand Up @@ -44,6 +45,7 @@ Compatibility-sensitive traits:
- `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`.
- Custom SSL context precedence is compatibility-sensitive: when an application-supplied `SSLContext` is set (`Client.Builder.setSSLContext(SSLContext)`), it is used as is and the trust/key material options (trust store, CA certificate, client certificate/key) are ignored. `ssl_mode` still applies but only to server hostname verification (`TRUST`/`VERIFY_CA` skip it, `STRICT` enforces it). The supplied context is a live object and is never parsed from or represented as a string.
- 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).
- JSONEachRow reading depends on the selected parser factory and request format settings: parser materialization determines Java value types, the reader infers minimal schema from the first row, and JSON number server settings are applied only when `QuerySettings` resolves to `ClickHouseFormat.JSONEachRow` and `json_disable_number_quoting` is enabled.
- JSONEachRow schema inference is intentionally best-effort: scalar values use Java-to-ClickHouse type mappings, while JSON arrays and objects are identified structurally as `Array` and `Map`. For arrays, maps, and some nested or ambiguous values, the inferred type may not include the most specific element, key, value, or nested ClickHouse type.
Expand All @@ -54,6 +56,7 @@ Compatibility-sensitive traits:
- 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. 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.
- Custom SSL context via properties: A fully pre-built `javax.net.ssl.SSLContext` may be passed as a live object in the connection `Properties` under the `ssl_context` key (added with `Properties.put`, since it is not a string). It is forwarded to the underlying `client-v2` transport and used as is; `ssl_mode` then only controls hostname verification. This supports diskless, in-memory TLS material behind connection pools that only expose `java.util.Properties`.
- 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.
Expand Down Expand Up @@ -93,4 +96,5 @@ Compatibility-sensitive traits:
- 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.
- Connection `Properties` values must be strings, with one scoped exception: the `ssl_context` key may carry a live `javax.net.ssl.SSLContext` object. Any other non-string property value still throws `IllegalArgumentException` during connection configuration.
- 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.
Loading
Loading