|
11 | 11 | import com.clickhouse.client.api.DataTransferException; |
12 | 12 | import com.clickhouse.client.api.ServerException; |
13 | 13 | import com.clickhouse.client.api.enums.ProxyType; |
| 14 | +import com.clickhouse.client.api.enums.SSLMode; |
14 | 15 | import com.clickhouse.client.api.http.ClickHouseHttpProto; |
15 | 16 | import com.clickhouse.client.api.transport.Endpoint; |
16 | | -import com.clickhouse.client.config.ClickHouseDefaultSslContextProvider; |
17 | 17 | import com.clickhouse.data.ClickHouseFormat; |
18 | 18 | import net.jpountz.lz4.LZ4Factory; |
19 | 19 | import org.apache.commons.compress.compressors.CompressorStreamFactory; |
|
85 | 85 | import java.net.URLEncoder; |
86 | 86 | import java.net.UnknownHostException; |
87 | 87 | import java.nio.charset.StandardCharsets; |
88 | | -import java.security.NoSuchAlgorithmException; |
89 | 88 | import java.util.Arrays; |
90 | 89 | import java.util.Base64; |
91 | 90 | import java.util.Collection; |
@@ -131,7 +130,7 @@ public class HttpAPIClientHelper { |
131 | 130 |
|
132 | 131 | LZ4Factory lz4Factory; |
133 | 132 |
|
134 | | - private final ClickHouseDefaultSslContextProvider sslContextProvider = new ClickHouseDefaultSslContextProvider(); |
| 133 | + private final SslContextProvider sslContextProvider = new SslContextProvider(); |
135 | 134 |
|
136 | 135 | public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegistry, boolean initSslContext, LZ4Factory lz4Factory) { |
137 | 136 | this.metricsRegistry = metricsRegistry; |
@@ -159,34 +158,46 @@ public HttpAPIClientHelper(Map<String, Object> configuration, Object metricsRegi |
159 | 158 | * @return SSLContext |
160 | 159 | */ |
161 | 160 | public SSLContext createSSLContext(Map<String, Object> configuration) { |
162 | | - SSLContext sslContext; |
163 | | - try { |
164 | | - sslContext = SSLContext.getDefault(); |
165 | | - } catch (NoSuchAlgorithmException e) { |
166 | | - throw new ClientException("Failed to create default SSL context", e); |
167 | | - } |
| 161 | + final SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration); |
168 | 162 | final String trustStorePath = (String) configuration.get(ClientConfigProperties.SSL_TRUST_STORE.getKey()); |
169 | 163 | final String caCertificate = (String) configuration.get(ClientConfigProperties.CA_CERTIFICATE.getKey()); |
170 | 164 | final String sslCertificate = (String) configuration.get(ClientConfigProperties.SSL_CERTIFICATE.getKey()); |
171 | 165 | final String sslKey = (String) configuration.get(ClientConfigProperties.SSL_KEY.getKey()); |
172 | | - if (trustStorePath != null) { |
173 | | - try { |
174 | | - sslContext = sslContextProvider.getSslContextFromKeyStore( |
175 | | - trustStorePath, |
176 | | - (String) configuration.get(ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey()), |
177 | | - (String) configuration.get(ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey()) |
178 | | - ); |
179 | | - } catch (SSLException e) { |
180 | | - throw new ClientMisconfigurationException("Failed to create SSL context from a keystore", e); |
| 166 | + |
| 167 | + SslContextProvider.Builder builder = sslContextProvider.builder(); |
| 168 | + |
| 169 | + // The client certificate/key (mTLS) are independent of how the server certificate is verified, |
| 170 | + // so they are applied whenever configured, regardless of the SSL mode. |
| 171 | + if (sslCertificate != null && !sslCertificate.isEmpty()) { |
| 172 | + builder.clientCertificate(sslCertificate, sslKey); |
| 173 | + } |
| 174 | + |
| 175 | + if (sslMode == SSLMode.TRUST) { |
| 176 | + // TRUST accepts any server certificate and skips the hostname check (the latter is applied |
| 177 | + // where the connection socket factory is created). A configured trust store or CA |
| 178 | + // certificate has no effect in this mode and is ignored with a warning. |
| 179 | + if (trustStorePath != null || caCertificate != null) { |
| 180 | + LOG.warn("SSL mode '{}' trusts any server certificate; the configured {} is ignored.", |
| 181 | + SSLMode.TRUST, trustStorePath != null ? "trust store" : "CA certificate"); |
181 | 182 | } |
182 | | - } else if (caCertificate != null || sslCertificate != null|| sslKey != null) { |
183 | | - try { |
184 | | - sslContext = sslContextProvider.getSslContextFromCerts(sslCertificate, sslKey, caCertificate); |
185 | | - } catch (SSLException e) { |
186 | | - throw new ClientMisconfigurationException("Failed to create SSL context from certificates", e); |
| 183 | + builder.trustAllCertificates(); |
| 184 | + } else if (trustStorePath != null) { |
| 185 | + // VERIFY_CA / STRICT: validate against the trust store. A trust store and a CA certificate |
| 186 | + // cannot both take effect, so the CA certificate is ignored with a warning. |
| 187 | + if (caCertificate != null) { |
| 188 | + LOG.warn("Both a trust store and a CA certificate are configured; using the trust store and" |
| 189 | + + " ignoring the CA certificate. Import the CA certificate into the trust store instead."); |
187 | 190 | } |
| 191 | + builder.trustStore(trustStorePath, |
| 192 | + (String) configuration.get(ClientConfigProperties.SSL_KEY_STORE_PASSWORD.getKey()), |
| 193 | + (String) configuration.get(ClientConfigProperties.SSL_KEYSTORE_TYPE.getKey())); |
| 194 | + } else if (caCertificate != null) { |
| 195 | + // VERIFY_CA / STRICT: validate against the CA certificate. |
| 196 | + builder.rootCertificate(caCertificate); |
188 | 197 | } |
189 | | - return sslContext; |
| 198 | + // else VERIFY_CA / STRICT with no trust material: the JVM default trust store is used. |
| 199 | + |
| 200 | + return builder.build(); |
190 | 201 | } |
191 | 202 |
|
192 | 203 | private static final long CONNECTION_INACTIVITY_CHECK = 5000L; |
@@ -272,7 +283,11 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String, |
272 | 283 | LayeredConnectionSocketFactory sslConnectionSocketFactory; |
273 | 284 | if (sslContext != null) { |
274 | 285 | String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey()); |
275 | | - if (socketSNI != null && !socketSNI.trim().isEmpty()) { |
| 286 | + SSLMode sslMode = ClientConfigProperties.SSL_MODE.getOrDefault(configuration); |
| 287 | + // Trust and VerifyCa skip hostname verification. The same applies when a custom SNI is |
| 288 | + // set because the connection hostname will not match the certificate. |
| 289 | + boolean trustAllHostnames = sslMode == SSLMode.TRUST || sslMode == SSLMode.VERIFY_CA; |
| 290 | + if (socketSNI != null && !socketSNI.trim().isEmpty() || trustAllHostnames) { |
276 | 291 | sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true); |
277 | 292 | } else { |
278 | 293 | sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext); |
@@ -881,6 +896,10 @@ public RuntimeException wrapException(String message, Exception cause, String qu |
881 | 896 | return (RuntimeException) cause; |
882 | 897 | } |
883 | 898 |
|
| 899 | + if (cause instanceof SSLException) { |
| 900 | + return new ClickHouseException("SSL Problem", cause, queryId); |
| 901 | + } |
| 902 | + |
884 | 903 | if (cause instanceof ConnectionRequestTimeoutException || |
885 | 904 | cause instanceof NoHttpResponseException || |
886 | 905 | cause instanceof ConnectTimeoutException || |
|
0 commit comments