|
60 | 60 | import java.time.ZoneId; |
61 | 61 | import java.time.temporal.ChronoUnit; |
62 | 62 | import java.util.ArrayList; |
| 63 | +import java.util.Arrays; |
63 | 64 | import java.util.Collection; |
64 | 65 | import java.util.Collections; |
65 | 66 | import java.util.HashMap; |
|
81 | 82 | import java.util.function.Supplier; |
82 | 83 | import java.util.stream.Collectors; |
83 | 84 |
|
| 85 | +import javax.net.ssl.SSLContext; |
| 86 | + |
84 | 87 | /** |
85 | 88 | * <p>Client is the starting point for all interactions with ClickHouse. </p> |
86 | 89 | * |
@@ -148,8 +151,12 @@ public class Client implements AutoCloseable { |
148 | 151 |
|
149 | 152 | private Client(Collection<Endpoint> endpoints, Map<String,String> configuration, |
150 | 153 | ExecutorService sharedOperationExecutor, ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy, |
151 | | - Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager) { |
| 154 | + Object metricsRegistry, Supplier<String> queryIdGenerator, CredentialsManager cManager, |
| 155 | + SSLContext sslContext) { |
152 | 156 | Map<String, Object> parsedConfiguration = new ConcurrentHashMap<>(ClientConfigProperties.parseConfigMap(configuration)); |
| 157 | + if (sslContext != null) { |
| 158 | + parsedConfiguration.put(ClientConfigProperties.SSL_CONTEXT.getKey(), sslContext); |
| 159 | + } |
153 | 160 | this.credentialsManager = cManager; |
154 | 161 | this.session = Session.extractFrom(parsedConfiguration); |
155 | 162 | this.configuration = new ConcurrentHashMap<>(parsedConfiguration); |
@@ -270,6 +277,18 @@ public static class Builder { |
270 | 277 | private ColumnToMethodMatchingStrategy columnToMethodMatchingStrategy; |
271 | 278 | private Object metricRegistry = null; |
272 | 279 | 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 | + }; |
273 | 292 |
|
274 | 293 | public Builder() { |
275 | 294 | this.endpoints = new LinkedHashSet<>(); |
@@ -344,6 +363,11 @@ private Builder addEndpoint(Endpoint endpoint) { |
344 | 363 | * @param value - configuration option value |
345 | 364 | */ |
346 | 365 | 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 | + } |
347 | 371 | this.configuration.put(key, value); |
348 | 372 | if (key.equals(ClientConfigProperties.PRODUCT_NAME.getKey())) { |
349 | 373 | setClientName(value); |
@@ -785,6 +809,36 @@ public Builder setSSLMode(SSLMode sslMode) { |
785 | 809 | return this; |
786 | 810 | } |
787 | 811 |
|
| 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 | + |
788 | 842 | /** |
789 | 843 | * Configure client to use server timezone for date/datetime columns. Default is true. |
790 | 844 | * If this options is selected then server timezone should be set as well. |
@@ -1165,14 +1219,28 @@ public Client build() { |
1165 | 1219 |
|
1166 | 1220 | CredentialsManager cManager = new CredentialsManager(this.configuration); |
1167 | 1221 |
|
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()) && |
1169 | 1240 | configuration.containsKey(ClientConfigProperties.SSL_CERTIFICATE.getKey())) { |
1170 | 1241 | throw new ClientMisconfigurationException("Trust store and certificates cannot be used together"); |
1171 | 1242 | } |
1172 | 1243 |
|
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 | | - |
1176 | 1244 | // Resolve ssl_mode case-insensitively and normalize it to the canonical enum name so that |
1177 | 1245 | // downstream parsing is consistent and an unknown value is reported as a misconfiguration |
1178 | 1246 | // here instead of failing later with a generic enum-parsing error. |
@@ -1229,7 +1297,8 @@ public Client build() { |
1229 | 1297 | } |
1230 | 1298 |
|
1231 | 1299 | 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); |
1233 | 1302 | } |
1234 | 1303 | } |
1235 | 1304 |
|
|
0 commit comments