Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand All @@ -402,13 +406,25 @@ 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
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);
}
Expand Down
6 changes: 6 additions & 0 deletions client-v2/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@
<version>1.18.36</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
Copy link

Copilot AI Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The slf4j-simple dependency is added without a <scope>test</scope>. If it's only needed for tests, scope it to test to avoid polluting production classpaths.

Suggested change
<version>2.0.16</version>
<version>2.0.16</version>
<scope>test</scope>
Copilot uses AI. Check for mistakes.
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
18 changes: 18 additions & 0 deletions client-v2/src/main/java/com/clickhouse/client/api/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1025,6 +1030,19 @@ public Builder typeHintMapping(Map<ClickHouseDataType, Class<?>> 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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -218,6 +222,9 @@ public <T> 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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -256,8 +260,17 @@ public CloseableHttpClient createHttpClient(boolean initSslContext, Map<String,
// Top Level builders
HttpClientBuilder clientBuilder = HttpClientBuilder.create();
SSLContext sslContext = initSslContext ? createSSLContext(configuration) : null;
LayeredConnectionSocketFactory sslConnectionSocketFactory = sslContext == null ? new DummySSLConnectionSocketFactory()
: new SSLConnectionSocketFactory(sslContext);
LayeredConnectionSocketFactory sslConnectionSocketFactory;
if (sslContext != null) {
String socketSNI = (String)configuration.get(ClientConfigProperties.SSL_SOCKET_SNI.getKey());
if (socketSNI != null && !socketSNI.trim().isEmpty()) {
sslConnectionSocketFactory = new CustomSSLConnectionFactory(socketSNI, sslContext, (hostname, session) -> true);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is using a hostname verifier that always returns true ((hostname, session) -> true), which bypasses hostname verification entirely. This is a security risk as it makes the connection vulnerable to man-in-the-middle attacks. Consider using a proper hostname verifier or making this configurable.

} else {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
}
} else {
sslConnectionSocketFactory = new DummySSLConnectionSocketFactory();
}
// Socket configuration
SocketConfig.Builder soCfgBuilder = SocketConfig.custom();
ClientConfigProperties.SOCKET_OPERATION_TIMEOUT.<Integer>applyIfSet(configuration,
Expand Down Expand Up @@ -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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -239,7 +239,7 @@ public void testDefaultSettings() {
.setSocketSndbuf(100000)
.build()) {
Map<String, String> 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");
Expand Down Expand Up @@ -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.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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("<br/> <br/> Default: `none` <br/> Enum: `none` <br/> Key: `none` "


);
for (ClientConfigProperties p : ClientConfigProperties.values()) {
String defaultValue = p.getDefaultValue() == null ? "-" : "`" + p.getDefaultValue() + "`";
System.out.println("<br/> <br/> Default: " +defaultValue + " <br/> Enum: `ClientConfigProperties." + p.name() + "` <br/> Key: `" + p.getKey() +"` "


);
}
}
}
Loading