Skip to content

Commit

Permalink
[FEATURE]: Add clickhouse SSL\TLS connection
Browse files Browse the repository at this point in the history
  • Loading branch information
dmitrybugakov committed Aug 31, 2023
1 parent df1eea2 commit 8d85873
Show file tree
Hide file tree
Showing 17 changed files with 1,831 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import com.github.housepower.buffer.SocketBuffedReader;
import com.github.housepower.buffer.SocketBuffedWriter;
import com.github.housepower.client.ssl.SSLContextBuilder;
import com.github.housepower.data.Block;
import com.github.housepower.misc.Validate;
import com.github.housepower.protocol.*;
Expand All @@ -29,11 +30,14 @@
import com.github.housepower.stream.QueryResult;
import com.github.housepower.stream.ClickHouseQueryResult;

import javax.net.ssl.*;
import java.io.IOException;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.security.*;
import java.security.cert.CertificateException;
import java.sql.SQLException;
import java.time.Duration;
import java.util.Map;
Expand All @@ -44,23 +48,41 @@ public class NativeClient {

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

public static NativeClient connect(ClickHouseConfig configure) throws SQLException {
return connect(configure.host(), configure.port(), configure);
public static NativeClient connect(ClickHouseConfig config) throws SQLException {
return connect(config.host(), config.port(), config);
}

public static NativeClient connect(String host, int port, ClickHouseConfig configure) throws SQLException {
// TODO: Support proxy
// TODO: Move socket initialisation to separate factory (default & ssl)
public static NativeClient connect(String host, int port, ClickHouseConfig config) throws SQLException {
try {
SocketAddress endpoint = new InetSocketAddress(host, port);
// TODO support proxy
Socket socket = new Socket();
Socket socket;

boolean useSSL = config.ssl();
if (useSSL) {
LOG.debug("Client works in SSL mode!");
SSLContext context = new SSLContextBuilder(config).getSSLContext();
SSLSocketFactory factory = context.getSocketFactory();
socket = (SSLSocket) factory.createSocket();
} else {
socket = new Socket();
}
socket.setTcpNoDelay(true);
socket.setSendBufferSize(ClickHouseDefines.SOCKET_SEND_BUFFER_BYTES);
socket.setReceiveBufferSize(ClickHouseDefines.SOCKET_RECV_BUFFER_BYTES);
socket.setKeepAlive(configure.tcpKeepAlive());
socket.connect(endpoint, (int) configure.connectTimeout().toMillis());
socket.setKeepAlive(config.tcpKeepAlive());
socket.connect(endpoint, (int) config.connectTimeout().toMillis());

if (useSSL) ((SSLSocket) socket).startHandshake();

return new NativeClient(socket);
} catch (IOException ex) {
} catch (IOException |
NoSuchAlgorithmException |
KeyStoreException |
CertificateException |
UnrecoverableKeyException |
KeyManagementException ex) {
throw new SQLException(ex.getMessage(), ex);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.housepower.client.ssl;

import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

/**
* An implementation of X509TrustManager that trusts all certificates.
* This class is not secure and should only be used for debugging or
* in a completely isolated environment.
*/
public class PermissiveTrustManager implements X509TrustManager {

/**
* Checks the client certificates but does nothing.
* It effectively trusts all client certificates.
*
* @param x509Certificates Array of client certificates to check
* @param s The auth type (e.g., "RSA", "DSS")
*/
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) {
// Do nothing to bypass client checks
}

/**
* Checks the server certificates but does nothing.
* It effectively trusts all server certificates.
*
* @param x509Certificates Array of server certificates to check
* @param s The auth type (e.g., "RSA", "DSS")
*/
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) {
// Do nothing to bypass server checks
}

/**
* Returns an empty array of certificate authorities, indicating
* that all certificates are trusted, subject to the
* verification done in the checkClientTrusted and checkServerTrusted methods.
*
* @return An empty X509Certificate array
*/
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.housepower.client.ssl;

import com.github.housepower.client.NativeClient;
import com.github.housepower.log.Logger;
import com.github.housepower.log.LoggerFactory;
import com.github.housepower.settings.ClickHouseConfig;
import com.github.housepower.settings.KeyStoreConfig;
import com.github.housepower.settings.SettingKey;


import javax.net.ssl.*;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.security.cert.CertificateException;

public class SSLContextBuilder {
private static final Logger LOG = LoggerFactory.getLogger(NativeClient.class);

private ClickHouseConfig config;

private KeyStoreConfig keyStoreConfig;

public SSLContextBuilder(ClickHouseConfig config) {
this.config = config;
this.keyStoreConfig = new KeyStoreConfig(
(String) config.settings().get(SettingKey.keyStoreType),
(String) config.settings().get(SettingKey.keyStorePath),
(String) config.settings().get(SettingKey.keyStorePassword)
);
}

public SSLContext getSSLContext() throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, UnrecoverableKeyException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManager[] trustManager = null;
KeyManager[] keyManager = null;
SecureRandom secureRandom = new SecureRandom();
String sslMode = config.sslMode();
LOG.debug("Client SSL mode: '" + sslMode + "'");

switch (sslMode) {
case "disabled":
trustManager = new TrustManager[]{new PermissiveTrustManager()};
keyManager = new KeyManager[]{};
break;
case "verify_ca":
KeyStore keyStore = KeyStore.getInstance(keyStoreConfig.getKeyStoreType());
keyStore.load(Files.newInputStream(Paths.get(keyStoreConfig.getKeyStorePath()).toFile().toPath()),
keyStoreConfig.getKeyStorePassword().toCharArray());

KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStoreConfig.getKeyStorePassword().toCharArray());

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);

trustManager = trustManagerFactory.getTrustManagers();
keyManager = keyManagerFactory.getKeyManagers();
break;
default:
throw new IllegalArgumentException("Unknown SSL mode: '" + sslMode + "'");
}

sslContext.init(keyManager, trustManager, secureRandom);
return sslContext;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
@Immutable
public class ClickHouseConfig implements Serializable {


private final String host;
private final List<String> hosts;
private final int port;
Expand All @@ -46,11 +45,14 @@ public class ClickHouseConfig implements Serializable {
private final String charset; // use String because Charset is not serializable
private final Map<SettingKey, Serializable> settings;
private final boolean tcpKeepAlive;
private final boolean ssl;
private final String sslMode;
private final String clientName;

private ClickHouseConfig(String host, int port, String database, String user, String password,
Duration queryTimeout, Duration connectTimeout, boolean tcpKeepAlive,
String charset, String clientName, Map<SettingKey, Serializable> settings) {
boolean ssl, String sslMode, String charset, String clientName,
Map<SettingKey, Serializable> settings) {
this.host = host;
this.hosts = Arrays.asList(host.split(HOST_DELIMITER));
this.port = port;
Expand All @@ -60,6 +62,8 @@ private ClickHouseConfig(String host, int port, String database, String user, St
this.queryTimeout = queryTimeout;
this.connectTimeout = connectTimeout;
this.tcpKeepAlive = tcpKeepAlive;
this.ssl = ssl;
this.sslMode = sslMode;
this.charset = charset;
this.clientName = clientName;
this.settings = settings;
Expand Down Expand Up @@ -97,6 +101,14 @@ public Duration connectTimeout() {
return this.connectTimeout;
}

public boolean ssl() {
return this.ssl;
}

public String sslMode() {
return this.sslMode;
}

public Charset charset() {
return Charset.forName(charset);
}
Expand Down Expand Up @@ -162,6 +174,18 @@ public ClickHouseConfig withTcpKeepAlive(boolean enable) {
.build();
}

public ClickHouseConfig withSSL(boolean enable) {
return Builder.builder(this)
.ssl(enable)
.build();
}

public ClickHouseConfig withSSLMode(String mode) {
return Builder.builder(this)
.sslMode(mode)
.build();
}

public ClickHouseConfig withCharset(Charset charset) {
return Builder.builder(this)
.charset(charset)
Expand Down Expand Up @@ -212,6 +236,8 @@ public static final class Builder {
private Duration connectTimeout;
private Duration queryTimeout;
private boolean tcpKeepAlive;
private boolean ssl;
private String sslMode;
private Charset charset;
private String clientName;
private Map<SettingKey, Serializable> settings = new HashMap<>();
Expand All @@ -234,6 +260,8 @@ public static Builder builder(ClickHouseConfig cfg) {
.queryTimeout(cfg.queryTimeout())
.charset(cfg.charset())
.tcpKeepAlive(cfg.tcpKeepAlive())
.ssl(cfg.ssl())
.sslMode(cfg.sslMode())
.clientName(cfg.clientName())
.withSettings(cfg.settings());
}
Expand Down Expand Up @@ -288,6 +316,16 @@ public Builder tcpKeepAlive(boolean tcpKeepAlive) {
return this;
}

public Builder ssl(boolean ssl) {
this.withSetting(SettingKey.ssl, ssl);
return this;
}

public Builder sslMode(String sslMode) {
this.withSetting(SettingKey.ssl, ssl);
return this;
}

public Builder charset(String charset) {
this.withSetting(SettingKey.charset, charset);
return this;
Expand Down Expand Up @@ -330,15 +368,17 @@ public ClickHouseConfig build() {
this.connectTimeout = (Duration) this.settings.getOrDefault(SettingKey.connect_timeout, Duration.ZERO);
this.queryTimeout = (Duration) this.settings.getOrDefault(SettingKey.query_timeout, Duration.ZERO);
this.tcpKeepAlive = (boolean) this.settings.getOrDefault(SettingKey.tcp_keep_alive, false);
this.ssl = (boolean) this.settings.getOrDefault(SettingKey.ssl, false);
this.sslMode = (String) this.settings.getOrDefault(SettingKey.sslMode, "disabled");
this.charset = Charset.forName((String) this.settings.getOrDefault(SettingKey.charset, "UTF-8"));
this.clientName = (String) this.settings.getOrDefault(SettingKey.client_name,
String.format(Locale.ROOT, "%s %s", ClickHouseDefines.NAME, "client"));

revisit();
purgeSettings();

return new ClickHouseConfig(
host, port, database, user, password, queryTimeout, connectTimeout, tcpKeepAlive, charset.name(), clientName, settings);
return new ClickHouseConfig(host, port, database, user, password, queryTimeout, connectTimeout,
tcpKeepAlive, ssl, sslMode, charset.name(), clientName, settings);
}

private void revisit() {
Expand All @@ -360,6 +400,8 @@ private void purgeSettings() {
this.settings.remove(SettingKey.query_timeout);
this.settings.remove(SettingKey.connect_timeout);
this.settings.remove(SettingKey.tcp_keep_alive);
this.settings.remove(SettingKey.ssl);
this.settings.remove(SettingKey.sslMode);
this.settings.remove(SettingKey.charset);
this.settings.remove(SettingKey.client_name);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.github.housepower.settings;

public class KeyStoreConfig {
private String keyStoreType;
private String keyStorePath;
private String keyStorePassword;

public KeyStoreConfig() {
}

public KeyStoreConfig(String keyStoreType, String keyStorePath, String keyStorePassword) {
this.keyStoreType = keyStoreType;
this.keyStorePath = keyStorePath;
this.keyStorePassword = keyStorePassword;
}

public String getKeyStorePath() {
return this.keyStorePath;
}

public String getKeyStorePassword() {
return this.keyStorePassword;
}

public String getKeyStoreType() {
return this.keyStoreType;
}
}
Loading

0 comments on commit 8d85873

Please sign in to comment.