Skip to content

Commit 7eea000

Browse files
Merge remote-tracking branch 'origin/main' into polyglot/client-v2-bfloat16
# Conflicts: # CHANGELOG.md
2 parents 687dd1d + b09551d commit 7eea000

12 files changed

Lines changed: 977 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@
99
binding, `Nullable(BFloat16)`, and `BFloat16` values held in `Dynamic`/`Variant` columns. On write the client keeps the
1010
high 16 bits of the `float`, matching the ClickHouse server's own `Float32``BFloat16` conversion. In the JDBC driver
1111
(`jdbc-v2`) `BFloat16` maps to `java.sql.Types.FLOAT` / `java.lang.Float` and is read and written through the standard
12-
`getFloat`/`setFloat` and `getObject` accessors. Previously reading or writing a `BFloat16` column failed with an
12+
`getFloat`/`setFloat` and `getObject` accessors, and reported as such by `ResultSetMetaData` and `DatabaseMetaData`.
13+
Previously reading or writing a `BFloat16` column failed with an
1314
unsupported-data-type error. (https://github.com/ClickHouse/clickhouse-java/issues/2279)
15+
- **[client-v2, jdbc-v2]** Added TLS cipher suite selection. `Client.Builder.setSSLCipherSuites(String...)` (client-v2)
16+
and the comma-separated `ssl_cipher_suites` connection property (client-v2 and jdbc-v2) restrict the cipher suites
17+
enabled on secure connections; when unset, the transport defaults are used. Cipher-suite selection is independent of the
18+
trust configuration and `ssl_mode`. (https://github.com/ClickHouse/clickhouse-java/issues/2882)
1419

1520
### Bug Fixes
1621

@@ -99,6 +104,17 @@
99104

100105
### New Features
101106

107+
- **[client-v2, jdbc-v2]** Added support for an application-supplied `javax.net.ssl.SSLContext`. In client-v2,
108+
`Client.Builder.setSSLContext(SSLContext)` hands the client a fully pre-built context that is used as is; in
109+
jdbc-v2 the same context may be passed as a live object in the connection `Properties` under the `ssl_context`
110+
key (added with `Properties.put`, since it is not a string). Trust/key material options cannot be combined with
111+
a custom context and are rejected; `ssl_mode` still applies but only to server hostname verification. This
112+
supports in-memory TLS material that must never be written to disk, including behind connection pools that only
113+
expose `java.util.Properties`.
114+
- Examples for client-v2 https://github.com/ClickHouse/clickhouse-java/blob/main/examples/client-v2/src/main/java/com/clickhouse/examples/client_v2/SSLExamples.java
115+
- Examples for jdbc-v2 https://github.com/ClickHouse/clickhouse-java/blob/main/examples/jdbc/src/main/java/com/clickhouse/examples/jdbc/SSLExamples.java
116+
(https://github.com/ClickHouse/clickhouse-java/pull/2918, https://github.com/ClickHouse/clickhouse-java/issues/2909)
117+
102118
- **[jdbc-v2, client-v2]** Implemented SSL modes configuration. Now it is possible to set `ssl_mode` to `DISABLED`,
103119
`TRUST`, `VERIFY_CA` and `STRICT`. Note for V1 users: `NONE` is supported only by JDBC driver and mapped to `TRUST`.
104120
Please migrate to the new naming.

client-v2/src/main/java/com/clickhouse/client/api/Client.java

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import java.time.ZoneId;
6161
import java.time.temporal.ChronoUnit;
6262
import java.util.ArrayList;
63+
import java.util.Arrays;
6364
import java.util.Collection;
6465
import java.util.Collections;
6566
import java.util.HashMap;
@@ -81,6 +82,8 @@
8182
import java.util.function.Supplier;
8283
import java.util.stream.Collectors;
8384

85+
import javax.net.ssl.SSLContext;
86+
8487
/**
8588
* <p>Client is the starting point for all interactions with ClickHouse. </p>
8689
*
@@ -148,8 +151,12 @@ public class Client implements AutoCloseable {
148151

149152
private Client(Collection<Endpoint> endpoints, Map<String,String> configuration,
150153
ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy,
151-
Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager) {
154+
Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager,
155+
SSLContext sslContext) {
152156
Map<String, Object> parsedConfiguration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration));
157+
if (sslContext != null) {
158+
parsedConfiguration.put(ClientConfigProperties.SSL_CONTEXT.getKey(), sslContext);
159+
}
153160
this.credentialsManager = cManager;
154161
this.session = Session.extractFrom(parsedConfiguration);
155162
this.configuration = new ConcurrentHashMap<>(parsedConfiguration);
@@ -270,6 +277,18 @@ public static class Builder {
270277
private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy;
271278
private Object metricRegistry = null;
272279
private Supplier<String> queryIdGenerator;
280+
private SSLContext sslContext = null;
281+
282+
// Trust/key material options that feed a context the client would otherwise build; none of them
283+
// may be combined with an application-supplied SSLContext (see build()).
284+
private static final ClientConfigProperties[] SSL_MATERIAL_PROPERTIES = {
285+
ClientConfigProperties.SSL_TRUST_STORE,
286+
ClientConfigProperties.SSL_KEYSTORE_TYPE,
287+
ClientConfigProperties.SSL_KEY_STORE_PASSWORD,
288+
ClientConfigProperties.SSL_KEY,
289+
ClientConfigProperties.CA_CERTIFICATE,
290+
ClientConfigProperties.SSL_CERTIFICATE,
291+
};
273292

274293
public Builder() {
275294
this.endpoints = new LinkedHashSet<>();
@@ -344,6 +363,11 @@ private Builder addEndpoint(Endpoint endpoint) {
344363
* @param value - configuration option value
345364
*/
346365
public Builder setOption(String key, String value) {
366+
if (key.equals(ClientConfigProperties.SSL_CONTEXT.getKey())) {
367+
throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
368+
+ "' cannot be set as a string; supply a javax.net.ssl.SSLContext object via "
369+
+ "Client.Builder.setSSLContext(...)");
370+
}
347371
this.configuration.put(key, value);
348372
if (key.equals(ClientConfigProperties.PRODUCT_NAME.getKey())) {
349373
setClientName(value);
@@ -785,6 +809,36 @@ public Builder setSSLMode(SSLMode sslMode) {
785809
return this;
786810
}
787811

812+
/**
813+
* Restricts the TLS cipher suites the client may negotiate on secure connections. When set, only
814+
* the listed cipher suites are enabled on the SSL socket (subject to what the JVM and the server
815+
* support); when not set, the transport defaults are used (Apache HttpClient enables the JVM's
816+
* default suites minus those it considers weak). Suite names use the standard JSSE names, for
817+
* example {@code TLS_AES_256_GCM_SHA384}.
818+
*
819+
* @param cipherSuites cipher suite names to enable
820+
* @return same instance of the builder
821+
*/
822+
public Builder setSSLCipherSuites(String... cipherSuites) {
823+
this.configuration.put(ClientConfigProperties.SSL_CIPHER_SUITES.getKey(),
824+
ClientConfigProperties.commaSeparated(Arrays.asList(cipherSuites)));
825+
return this;
826+
}
827+
828+
/**
829+
* Supplies a pre-built {@link SSLContext}. When set, it is used as is instead of a context built
830+
* from the configured trust/key material (which then cannot be set alongside it). {@link SSLMode}
831+
* still applies, but only to server hostname verification: {@link SSLMode#STRICT} (default) enforces
832+
* it while {@link SSLMode#TRUST} and {@link SSLMode#VERIFY_CA} skip it.
833+
*
834+
* @param sslContext a fully configured SSL context; {@code null} clears any previously set context
835+
* @return same instance of the builder
836+
*/
837+
public Builder setSSLContext(SSLContext sslContext) {
838+
this.sslContext = sslContext;
839+
return this;
840+
}
841+
788842
/**
789843
* Configure client to use server timezone for date/datetime columns. Default is true.
790844
* If this options is selected then server timezone should be set as well.
@@ -1165,14 +1219,28 @@ public Client build() {
11651219

11661220
CredentialsManager cManager = new CredentialsManager(this.configuration);
11671221

1168-
if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) &&
1222+
// A textual 'ssl_context' can never be a live context (also rejected in setOption).
1223+
if (configuration.containsKey(ClientConfigProperties.SSL_CONTEXT.getKey())) {
1224+
throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
1225+
+ "' cannot be set as a string; supply a javax.net.ssl.SSLContext object via "
1226+
+ "Client.Builder.setSSLContext(...)");
1227+
}
1228+
1229+
if (this.sslContext != null) {
1230+
// A custom SSLContext replaces any context the client would build, so trust/key material
1231+
// cannot be set alongside it. SSL_MODE (hostname verification) is still allowed.
1232+
for (ClientConfigProperties material : SSL_MATERIAL_PROPERTIES) {
1233+
if (configuration.containsKey(material.getKey())) {
1234+
throw new ClientMisconfigurationException("'" + material.getKey() + "' cannot be combined"
1235+
+ " with a custom SSLContext; the supplied context is used as is. Only 'ssl_mode'"
1236+
+ " (hostname verification) may be set alongside it.");
1237+
}
1238+
}
1239+
} else if (configuration.containsKey(ClientConfigProperties.SSL_TRUST_STORE.getKey()) &&
11691240
configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) {
11701241
throw new ClientMisconfigurationException("Trust store and certificates cannot be used together");
11711242
}
11721243

1173-
// A trust store and a CA certificate are not rejected here: for VERIFY_CA/STRICT the trust
1174-
// store takes precedence and the CA certificate is ignored with a warning (see createSSLContext).
1175-
11761244
// Resolve ssl_mode case-insensitively and normalize it to the canonical enum name so that
11771245
// downstream parsing is consistent and an unknown value is reported as a misconfiguration
11781246
// here instead of failing later with a generic enum-parsing error.
@@ -1229,7 +1297,8 @@ public Client build() {
12291297
}
12301298

12311299
return new Client(this.endpoints, this.configuration, this.sharedOperationExecutor,
1232-
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager);
1300+
this.columnToMethodMatchingStrategy, this.metricRegistry, this.queryIdGenerator, cManager,
1301+
this.sslContext);
12331302
}
12341303
}
12351304

client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
1010

11+
import javax.net.ssl.SSLContext;
12+
1113
import java.util.ArrayList;
1214
import java.util.Arrays;
1315
import java.util.Collection;
@@ -118,6 +120,8 @@ public enum ClientConfigProperties {
118120

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

123+
SSL_CONTEXT("ssl_context", SSLContext.class),
124+
121125
RETRY_ON_FAILURE("retry", Integer.class, "3"),
122126

123127
INPUT_OUTPUT_FORMAT("format", ClickHouseFormat.class),
@@ -204,6 +208,35 @@ public Object parseValue(String value) {
204208
* See <a href="https://clickhouse.com/docs/operations/settings/query-level#custom_settings">ClickHouse Docs</a>
205209
*/
206210
CUSTOM_SETTINGS_PREFIX("custom_settings_prefix", String.class, "custom_"),
211+
212+
/**
213+
* Comma-separated list of TLS cipher suites the client is allowed to negotiate on secure connections.
214+
* When set, only these cipher suites are enabled on the SSL socket (subject to what the JVM and server
215+
* support); when unset, the transport defaults are used (Apache HttpClient enables the JVM's default
216+
* suites minus those it considers weak).
217+
* <p>
218+
* The value is parsed into a sanitized list here, in the config-parsing layer (not in transport code):
219+
* blank tokens produced by a leading, trailing or doubled comma are dropped and each surviving name is
220+
* trimmed, so consumers receive a ready-to-use list. A null, empty or whitespace-only entry would
221+
* otherwise be rejected by the SSL socket and break the handshake even alongside valid suites.
222+
* <p>
223+
* Appended at the end of the enum on purpose: adding a constant in the middle would shift the ordinal
224+
* of every following constant (see {@code docs/changes_checklist.md}).
225+
*/
226+
SSL_CIPHER_SUITES("ssl_cipher_suites", List.class) {
227+
@Override
228+
@SuppressWarnings("unchecked")
229+
public Object parseValue(String value) {
230+
List<String> suites = (List<String>) super.parseValue(value);
231+
if (suites == null) {
232+
return null;
233+
}
234+
return suites.stream()
235+
.filter(s -> s != null && !s.trim().isEmpty())
236+
.map(String::trim)
237+
.collect(Collectors.toList());
238+
}
239+
},
207240
;
208241

209242
private static final Logger LOG = LoggerFactory.getLogger(ClientConfigProperties.class);

client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegi
154154
}
155155

156156
/**
157-
* Creates or returns default SSL context.
157+
* Creates an SSL context from the configured trust/key material.
158158
*
159159
* @return SSLContext
160160
*/
@@ -280,16 +280,45 @@ private HttpClientConnectionManager poolConnectionManager(LayeredConnectionSocke
280280
public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, Object> configuration) {
281281
// Top Level builders
282282
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
283-
SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
283+
// An application-supplied SSLContext is used as is; otherwise one is built from the configured
284+
// trust/key material. Server hostname verification below still applies via the SSL mode.
285+
SSLContext sslContext = null;
286+
if (initSslContext) {
287+
Object customSSLContext = configuration.get(ClientConfigProperties.SSL_CONTEXT.getKey());
288+
if (customSSLContext == null) {
289+
sslContext = createSSLContext(configuration);
290+
} else if (customSSLContext instanceof SSLContext) {
291+
sslContext = (SSLContext) customSSLContext;
292+
} else {
293+
throw new ClientMisconfigurationException("'" + ClientConfigProperties.SSL_CONTEXT.getKey()
294+
+ "' must be a javax.net.ssl.SSLContext instance but was "
295+
+ customSSLContext.getClass().getName() + "; supply it via Client.Builder.setSSLContext(...)");
296+
}
297+
}
284298
LayeredConnectionSocketFactory sslConnectionSocketFactory;
285299
if (sslContext != null) {
286300
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());
287301
SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration);
288302
// Trust and VerifyCa skip hostname verification. The same applies when a custom SNI is
289303
// set because the connection hostname will not match the certificate.
290304
boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA;
291-
if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) {
292-
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
305+
boolean hasSNI = socketSNI != null && !socketSNI.trim().isEmpty();
306+
// ssl_cipher_suites is parsed and sanitized in ClientConfigProperties (blank tokens dropped,
307+
// names trimmed); an unset or empty list means "no restriction" so the transport defaults apply.
308+
List<String> cipherSuites = ClientConfigProperties.SSL_CIPHER_SUITES.getOrDefault(configuration);
309+
String[] enabledCipherSuites = cipherSuites == null || cipherSuites.isEmpty() ? null
310+
: cipherSuites.toArray(new String[0]);
311+
if (hasSNI || trustAllHostnames || enabledCipherSuites != null) {
312+
// Skip hostname verification only for trust-all modes or when a custom SNI is used (the
313+
// connection hostname would not match the certificate); otherwise a null verifier makes the
314+
// factory fall back to the JDK/HttpClient default verifier, keeping STRICT verification.
315+
// java:S5527 - the permissive verifier is applied only for SSLMode.TRUST/VERIFY_CA (where the
316+
// user has explicitly opted out of hostname verification) or a custom SNI; STRICT keeps the
317+
// default verifying behaviour, so secure-by-default hostname verification is preserved.
318+
@SuppressWarnings("java:S5527")
319+
HostnameVerifier hostnameVerifier = trustAllHostnames || hasSNI ? (hostname, session) -> true : null;
320+
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, hostnameVerifier,
321+
enabledCipherSuites);
293322
} else {
294323
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
295324
}
@@ -1068,8 +1097,14 @@ public static class CustomSSLConnectionFactory extends SSLConnectionSocketFactor
10681097

10691098
private final SNIHostName defaultSNI;
10701099

1100+
// Retained for backward compatibility; delegates with no cipher-suite restriction (transport defaults).
10711101
public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier) {
1072-
super(sslContext, hostnameVerifier);
1102+
this(defaultSNI, sslContext, hostnameVerifier, null);
1103+
}
1104+
1105+
public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier,
1106+
String[] supportedCipherSuites) {
1107+
super(sslContext, null /* supportedProtocols */, supportedCipherSuites, hostnameVerifier);
10731108
this.defaultSNI = defaultSNI == null || defaultSNI.trim().isEmpty() ? null : new SNIHostName(defaultSNI);
10741109
}
10751110

0 commit comments

Comments
 (0)