diff --git a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java index 95187b541..1f410f2ab 100644 --- a/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java +++ b/clickhouse-client/src/main/java/com/clickhouse/client/config/ClickHouseClientOption.java @@ -451,7 +451,14 @@ public enum ClickHouseClientOption implements ClickHouseOption { */ CONNECTION_TTL("connection_ttl", 0L, "Connection time to live in milliseconds. 0 or negative number means no limit."), - MEASURE_REQUEST_TIME("debug_measure_request_time", false, "Whether to measure request time. If true, the time will be logged in debug mode."); + MEASURE_REQUEST_TIME("debug_measure_request_time", false, "Whether to measure request time. If true, the time will be logged in debug mode."), + + /** + * SNI SSL parameter that will be set for each outbound SSL socket. + */ + SSL_SOCKET_SNI("ssl_socket_sni", "", " SNI SSL parameter that will be set for each outbound SSL socket.") + + ; private final String key; private final Serializable defaultValue; diff --git a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java index e5689b4f9..0c00f7015 100644 --- a/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java +++ b/clickhouse-http-client/src/main/java/com/clickhouse/client/http/ApacheHttpConnectionImpl.java @@ -11,6 +11,7 @@ import com.clickhouse.client.config.ClickHouseProxyType; import com.clickhouse.client.config.ClickHouseSslMode; import com.clickhouse.client.http.config.ClickHouseHttpOption; +import com.clickhouse.config.ClickHouseOption; import com.clickhouse.data.ClickHouseChecker; import com.clickhouse.data.ClickHouseExternalTable; import com.clickhouse.data.ClickHouseFormat; @@ -54,8 +55,11 @@ import org.apache.hc.core5.util.Timeout; import org.apache.hc.core5.util.VersionInfo; +import javax.net.ssl.SNIHostName; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -66,10 +70,9 @@ import java.io.UncheckedIOException; import java.net.ConnectException; import java.net.HttpURLConnection; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.SocketOption; -import java.net.SocketOptions; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; @@ -394,6 +397,7 @@ public static SocketFactory create(ClickHouseConfig config) { static class SSLSocketFactory extends SSLConnectionSocketFactory { private final ClickHouseConfig config; + private final SNIHostName defaultSNI; private SSLSocketFactory(ClickHouseConfig config) throws SSLException { super(ClickHouseSslContextProvider.getProvider().getSslContext(SSLContext.class, config) @@ -402,6 +406,8 @@ private SSLSocketFactory(ClickHouseConfig config) throws SSLException { ? new DefaultHostnameVerifier() : (hostname, session) -> true); // NOSONAR this.config = config; + String sni = config.getStrOption(ClickHouseClientOption.SSL_SOCKET_SNI); + defaultSNI = sni == null || sni.trim().isEmpty() ? null : new SNIHostName(sni); } @Override @@ -409,6 +415,16 @@ public Socket createSocket(HttpContext context) throws IOException { return AbstractSocketClient.setSocketOptions(config, new Socket()); } + @Override + protected void prepareSocket(SSLSocket socket, HttpContext context) throws IOException { + super.prepareSocket(socket, context); + if (defaultSNI != null) { + SSLParameters sslParams = socket.getSSLParameters(); + sslParams.setServerNames(Collections.singletonList(defaultSNI)); + socket.setSSLParameters(sslParams); + } + } + public static SSLSocketFactory create(ClickHouseConfig config) throws SSLException { return new SSLSocketFactory(config); } diff --git a/client-v2/pom.xml b/client-v2/pom.xml index a610c0ef5..7e64b8a8a 100644 --- a/client-v2/pom.xml +++ b/client-v2/pom.xml @@ -129,6 +129,12 @@ 1.18.36 test + + org.slf4j + slf4j-simple + 2.0.16 + test + diff --git a/client-v2/src/main/java/com/clickhouse/client/api/Client.java b/client-v2/src/main/java/com/clickhouse/client/api/Client.java index 7b176b64e..344643d44 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/Client.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/Client.java @@ -602,6 +602,11 @@ public Builder useHttpCompression(boolean enabled) { return this; } + /** + * Tell client that compression will be handled by application. + * @param enabled - indicates that feature is enabled. + * @return + */ public Builder appCompressedData(boolean enabled) { this.configuration.put(ClientConfigProperties.APP_COMPRESSED_DATA.getKey(), String.valueOf(enabled)); return this; @@ -1025,6 +1030,19 @@ public Builder typeHintMapping(Map> typeHintMapping return this; } + + /** + * SNI SSL parameter that will be set for each outbound SSL socket. + * SNI stands for Server Name Indication - an extension to the TLS protocol that allows multiple domains to share the same IP address. + * + * @param sni - SNI parameter + * @return this builder instance + */ + public Builder sslSocketSNI(String sni) { + this.configuration.put(ClientConfigProperties.SSL_SOCKET_SNI.getKey(), sni); + return this; + } + public Client build() { // check if endpoint are empty. so can not initiate client if (this.endpoints.isEmpty()) { diff --git a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java index cf8caee8f..89e0f5a12 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/ClientConfigProperties.java @@ -11,16 +11,15 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.function.Function; -import java.util.Map; import java.util.TimeZone; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; /** @@ -178,6 +177,11 @@ public Object parseValue(String value) { * Used by binary readers to convert values into desired Java type. */ TYPE_HINT_MAPPING("type_hint_mapping", Map.class), + + /** + * SNI SSL parameter that will be set for each outbound SSL socket. + */ + SSL_SOCKET_SNI("ssl_socket_sni", String.class,""), ; private static final Logger LOG = LoggerFactory.getLogger(ClientConfigProperties.class); @@ -218,6 +222,9 @@ public T getDefObjVal() { public static final String SERVER_SETTING_PREFIX = "clickhouse_setting_"; + // Key used to identify default value in configuration map + public static final String DEFAULT_KEY = "_default_"; + public static String serverSetting(String key) { return SERVER_SETTING_PREFIX + key; } diff --git a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java index 7f6f442f5..a9aadd66a 100644 --- a/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java +++ b/client-v2/src/main/java/com/clickhouse/client/api/internal/HttpAPIClientHelper.java @@ -60,8 +60,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SNIHostName; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSocket; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -256,8 +260,17 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map true); + } else { + sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext); + } + } else { + sslConnectionSocketFactory = new DummySSLConnectionSocketFactory(); + } // Socket configuration SocketConfig.Builder soCfgBuilder = SocketConfig.custom(); ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.applyIfSet(configuration, @@ -834,4 +847,25 @@ public long getTime() { return count > 0 ? runningAverage / count : 0; } } + + public static class CustomSSLConnectionFactory extends SSLConnectionSocketFactory { + + private final SNIHostName defaultSNI; + + public CustomSSLConnectionFactory(String defaultSNI, SSLContext sslContext, HostnameVerifier hostnameVerifier) { + super(sslContext, hostnameVerifier); + this.defaultSNI = defaultSNI == null || defaultSNI.trim().isEmpty() ? null : new SNIHostName(defaultSNI); + } + + @Override + protected void prepareSocket(SSLSocket socket, HttpContext context) throws IOException { + super.prepareSocket(socket, context); + + if (defaultSNI != null) { + SSLParameters sslParams = socket.getSSLParameters(); + sslParams.setServerNames(Collections.singletonList(defaultSNI)); + socket.setSSLParameters(sslParams); + } + } + } } diff --git a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java index 2d56941e9..da6b38658 100644 --- a/client-v2/src/test/java/com/clickhouse/client/ClientTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/ClientTests.java @@ -206,7 +206,7 @@ public void testDefaultSettings() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 31); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 32); // to check everything is set. Increment when new added. } try (Client client = new Client.Builder() @@ -239,7 +239,7 @@ public void testDefaultSettings() { .setSocketSndbuf(100000) .build()) { Map config = client.getConfiguration(); - Assert.assertEquals(config.size(), 32); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 33); // to check everything is set. Increment when new added. Assert.assertEquals(config.get(ClientConfigProperties.DATABASE.getKey()), "mydb"); Assert.assertEquals(config.get(ClientConfigProperties.MAX_EXECUTION_TIME.getKey()), "10"); Assert.assertEquals(config.get(ClientConfigProperties.COMPRESSION_LZ4_UNCOMPRESSED_BUF_SIZE.getKey()), "300000"); @@ -306,7 +306,7 @@ public void testWithOldDefaults() { Assert.assertEquals(config.get(p.getKey()), p.getDefaultValue(), "Default value doesn't match"); } } - Assert.assertEquals(config.size(), 31); // to check everything is set. Increment when new added. + Assert.assertEquals(config.size(), 32); // to check everything is set. Increment when new added. } } diff --git a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java index 9ff751068..ae495c786 100644 --- a/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/HttpTransportTests.java @@ -36,6 +36,7 @@ import org.testng.annotations.Test; import java.io.ByteArrayInputStream; +import java.net.InetAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -1100,6 +1101,23 @@ public void testTimeoutsWithRetry() { } } + @Test(groups = {"integration"}) + public void testSNIWithCloud() throws Exception { + if (!isCloud()) { + // skip for local env + return; + } + + ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); + String ip = InetAddress.getByName(node.getHost()).getHostAddress(); + try (Client c = new Client.Builder() + .addEndpoint(Protocol.HTTP, ip, node.getPort(), true) + .setUsername("default") + .setPassword(ClickHouseServerForTest.getPassword()) + .sslSocketSNI(node.getHost()).build()) { + c.execute("SELECT 1"); + } + } protected Client.Builder newClient() { ClickHouseNode node = getServer(ClickHouseProtocol.HTTP); diff --git a/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java b/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java index 5a1755777..ff42c0ced 100644 --- a/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java +++ b/client-v2/src/test/java/com/clickhouse/client/internal/SmallTests.java @@ -1,5 +1,6 @@ package com.clickhouse.client.internal; +import com.clickhouse.client.api.ClientConfigProperties; import com.clickhouse.client.api.data_formats.internal.ProcessParser; import com.clickhouse.client.api.metrics.OperationMetrics; import com.clickhouse.client.api.metrics.ServerMetrics; @@ -45,4 +46,19 @@ public void testTimezoneConvertion() { ZonedDateTime utcSameLocalDt = dt.withZoneSameLocal(ZoneId.of("UTC")); System.out.println("withZoneSameLocal: " + utcSameLocalDt); } + + @Test + public void testGenConfigParameters() { + System.out.println("

Default: `none`
Enum: `none`
Key: `none` " + + + ); + for (ClientConfigProperties p : ClientConfigProperties.values()) { + String defaultValue = p.getDefaultValue() == null ? "-" : "`" + p.getDefaultValue() + "`"; + System.out.println("

Default: " +defaultValue + "
Enum: `ClientConfigProperties." + p.name() + "`
Key: `" + p.getKey() +"` " + + + ); + } + } }