Packages

Elixir client for ClickHouse database via FINE + clickhouse-cpp (native TCP)

Current section

Files

Jump to
natch native clickhouse-cpp clickhouse base sslsocket.cpp
Raw

native/clickhouse-cpp/clickhouse/base/sslsocket.cpp

#include "sslsocket.h"
#include "../client.h"
#include "../exceptions.h"
#include <stdexcept>
#include <openssl/ssl.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include <openssl/asn1.h>
namespace {
std::string getCertificateInfo(X509* cert)
{
if (!cert)
return "No certificate";
std::unique_ptr<BIO, decltype(&BIO_free)> mem_bio(BIO_new(BIO_s_mem()), &BIO_free);
X509_print(mem_bio.get(), cert);
char * data = nullptr;
auto len = BIO_get_mem_data(mem_bio.get(), &data);
if (len < 0)
return "Can't get certificate info due to BIO error " + std::to_string(len);
return std::string(data, len);
}
void throwSSLError(SSL * ssl, int error, const char * /*location*/, const char * /*statement*/, const std::string prefix = "OpenSSL error: ") {
const auto detail_error = ERR_get_error();
auto reason = ERR_reason_error_string(detail_error);
reason = reason ? reason : "Unknown SSL error";
std::string reason_str = reason;
if (ssl) {
// Print certificate only if handshake isn't completed
if (auto ssl_session = SSL_get_session(ssl); ssl_session && SSL_get_state(ssl) != TLS_ST_OK)
reason_str += "\nServer certificate: " + getCertificateInfo(SSL_SESSION_get0_peer(ssl_session));
}
// std::cerr << "!!! SSL error at " << location
// << "\n\tcaused by " << statement
// << "\n\t: "<< reason_str << "(" << error << ")"
// << "\n\t last err: " << ERR_peek_last_error()
// << std::endl;
throw clickhouse::OpenSSLError(prefix + std::to_string(error) + " : " + reason_str);
}
void configureSSL(const clickhouse::SSLParams::ConfigurationType & configuration, SSL * ssl, SSL_CTX * context = nullptr) {
std::unique_ptr<SSL_CONF_CTX, decltype(&SSL_CONF_CTX_free)> conf_ctx_holder(SSL_CONF_CTX_new(), SSL_CONF_CTX_free);
auto conf_ctx = conf_ctx_holder.get();
// To make both cmdline and flag file commands start with no prefix.
SSL_CONF_CTX_set1_prefix(conf_ctx, "");
// Allow all set of client commands, also turn on proper error reporting to reuse throwSSLError().
SSL_CONF_CTX_set_flags(conf_ctx, SSL_CONF_FLAG_CMDLINE | SSL_CONF_FLAG_FILE | SSL_CONF_FLAG_CLIENT | SSL_CONF_FLAG_SHOW_ERRORS | SSL_CONF_FLAG_CERTIFICATE );
if (ssl)
SSL_CONF_CTX_set_ssl(conf_ctx, ssl);
else if (context)
SSL_CONF_CTX_set_ssl_ctx(conf_ctx, context);
for (const auto & kv : configuration) {
const int err = SSL_CONF_cmd(conf_ctx, kv.first.c_str(), (kv.second ? kv.second->c_str() : nullptr));
// From the documentation:
// 2 - both key and value used
// 1 - only key used
// 0 - error during processing
// -2 - key not recodnized
// -3 - missing value
const bool value_present = !!kv.second;
if (err == 2 || (err == 1 && !value_present))
continue;
else if (err == 0)
throwSSLError(ssl, SSL_ERROR_NONE, nullptr, nullptr, "Failed to configure OpenSSL with command '" + kv.first + "' ");
else if (err == 1 && value_present)
throw clickhouse::OpenSSLError("Failed to configure OpenSSL: command '" + kv.first + "' needs no value");
else if (err == -2)
throw clickhouse::OpenSSLError("Failed to configure OpenSSL: unknown command '" + kv.first + "'");
else if (err == -3)
throw clickhouse::OpenSSLError("Failed to configure OpenSSL: command '" + kv.first + "' requires a value");
else
throw clickhouse::OpenSSLError("Failed to configure OpenSSL: command '" + kv.first + "' unknown error: " + std::to_string(err));
}
}
#define STRINGIFY_HELPER(x) #x
#define STRINGIFY(x) STRINGIFY_HELPER(x)
#define LOCATION __FILE__ ":" STRINGIFY(__LINE__)
struct SSLInitializer {
SSLInitializer() {
SSL_library_init();
SSLeay_add_ssl_algorithms();
SSL_load_error_strings();
}
};
SSL_CTX * prepareSSLContext(const clickhouse::SSLParams & context_params) {
static const SSLInitializer ssl_initializer;
const SSL_METHOD *method = TLS_client_method();
std::unique_ptr<SSL_CTX, decltype(&SSL_CTX_free)> ctx(SSL_CTX_new(method), &SSL_CTX_free);
if (!ctx)
throw clickhouse::OpenSSLError("Failed to initialize SSL context");
#define HANDLE_SSL_CTX_ERROR(statement) do { \
if (const auto ret_code = (statement); !ret_code) \
throwSSLError(nullptr, static_cast<int>(ERR_peek_error()), LOCATION, #statement); \
} while(false);
if (context_params.use_default_ca_locations)
HANDLE_SSL_CTX_ERROR(SSL_CTX_set_default_verify_paths(ctx.get()));
if (!context_params.path_to_ca_directory.empty())
HANDLE_SSL_CTX_ERROR(
SSL_CTX_load_verify_locations(
ctx.get(),
nullptr,
context_params.path_to_ca_directory.c_str())
);
for (const auto & f : context_params.path_to_ca_files)
HANDLE_SSL_CTX_ERROR(SSL_CTX_load_verify_locations(ctx.get(), f.c_str(), nullptr));
if (context_params.context_options != -1)
SSL_CTX_set_options(ctx.get(), context_params.context_options);
if (context_params.min_protocol_version != -1)
HANDLE_SSL_CTX_ERROR(
SSL_CTX_set_min_proto_version(ctx.get(), context_params.min_protocol_version));
if (context_params.max_protocol_version != -1)
HANDLE_SSL_CTX_ERROR(
SSL_CTX_set_max_proto_version(ctx.get(), context_params.max_protocol_version));
return ctx.release();
#undef HANDLE_SSL_CTX_ERROR
}
auto convertConfiguration(const decltype(clickhouse::ClientOptions::SSLOptions::configuration) & configuration)
{
auto result = decltype(clickhouse::SSLParams::configuration){};
for (const auto & cv : configuration)
result.push_back({cv.command, cv.value});
return result;
}
clickhouse::SSLParams GetSSLParams(const clickhouse::ClientOptions& opts) {
const auto& ssl_options = *opts.ssl_options;
return clickhouse::SSLParams{
ssl_options.path_to_ca_files,
ssl_options.path_to_ca_directory,
ssl_options.use_default_ca_locations,
ssl_options.context_options,
ssl_options.min_protocol_version,
ssl_options.max_protocol_version,
ssl_options.use_sni,
ssl_options.skip_verification,
ssl_options.host_flags,
convertConfiguration(ssl_options.configuration)
};
}
}
namespace clickhouse {
SSLContext::SSLContext(SSL_CTX & context)
: context_(&context, &SSL_CTX_free)
{
SSL_CTX_up_ref(context_.get());
}
SSLContext::SSLContext(const SSLParams & context_params)
: context_(prepareSSLContext(context_params), &SSL_CTX_free)
{
}
SSL_CTX * SSLContext::getContext() {
return context_.get();
}
// Allows caller to use returned value of `statement` if there was no error, throws exception otherwise.
#define HANDLE_SSL_ERROR(SSL_PTR, statement) [&] { \
if (const auto ret_code = (statement); ret_code <= 0) { \
throwSSLError(SSL_PTR, SSL_get_error(SSL_PTR, static_cast<int>(ret_code)), LOCATION, #statement); \
return static_cast<std::decay_t<decltype(ret_code)>>(0); \
} \
else \
return ret_code; \
} ()
/* // debug macro for tracing SSL state
#define LOG_SSL_STATE() std::cerr << "!!!!" << LOCATION << " @" << __FUNCTION__ \
<< "\t" << SSL_get_version(ssl_) << " state: " << SSL_state_string_long(ssl_) \
<< "\n\t handshake state: " << SSL_get_state(ssl_) \
<< std::endl
*/
SSLSocket::SSLSocket(const NetworkAddress& addr, const SocketTimeoutParams& timeout_params,
const SSLParams & ssl_params, SSLContext& context)
: Socket(addr, timeout_params)
, ssl_(SSL_new(context.getContext()), &SSL_free)
{
auto ssl = ssl_.get();
if (!ssl)
throw clickhouse::OpenSSLError("Failed to create SSL instance");
std::unique_ptr<ASN1_OCTET_STRING, decltype(&ASN1_OCTET_STRING_free)> ip_addr(a2i_IPADDRESS(addr.Host().c_str()), &ASN1_OCTET_STRING_free);
HANDLE_SSL_ERROR(ssl, SSL_set_fd(ssl, static_cast<int>(handle_)));
if (ssl_params.use_SNI)
HANDLE_SSL_ERROR(ssl, SSL_set_tlsext_host_name(ssl, addr.Host().c_str()));
if (ssl_params.host_flags != -1)
SSL_set_hostflags(ssl, ssl_params.host_flags);
HANDLE_SSL_ERROR(ssl, SSL_set1_host(ssl, addr.Host().c_str()));
// DO NOT use SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr), since
// we check verification result later, and that provides better error message.
if (ssl_params.configuration.size() > 0)
configureSSL(ssl_params.configuration, ssl);
SSL_set_connect_state(ssl);
HANDLE_SSL_ERROR(ssl, SSL_connect(ssl));
HANDLE_SSL_ERROR(ssl, SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY));
if (const auto verify_result = SSL_get_verify_result(ssl); !ssl_params.skip_verification && verify_result != X509_V_OK) {
auto error_message = X509_verify_cert_error_string(verify_result);
throw clickhouse::OpenSSLError("Failed to verify SSL connection, X509_v error: "
+ std::to_string(verify_result)
+ " " + error_message
+ "\nServer certificate: " + getCertificateInfo(SSL_get_peer_certificate(ssl)));
}
// Host name verification is done by OpenSSL itself, however if we are connecting to an ip-address,
// no verification is made, so we have to do it manually.
// Just in case if this is ever required, leave it here commented out.
// if (ip_addr) {
// // if hostname is actually an IP address
// HANDLE_SSL_ERROR(ssl, X509_check_ip(
// SSL_get_peer_certificate(ssl),
// ASN1_STRING_get0_data(ip_addr.get()),
// ASN1_STRING_length(ip_addr.get()),
// 0));
// }
}
void SSLSocket::validateParams(const SSLParams & ssl_params) {
// We need either SSL or SSL_CTX to properly validate configuration, so create a temporary one.
std::unique_ptr<SSL_CTX, decltype(&SSL_CTX_free)> ctx(SSL_CTX_new(TLS_client_method()), &SSL_CTX_free);
configureSSL(ssl_params.configuration, nullptr, ctx.get());
}
SSLSocketFactory::SSLSocketFactory(const ClientOptions& opts)
: NonSecureSocketFactory()
, ssl_params_(GetSSLParams(opts)) {
if (opts.ssl_options->ssl_context) {
ssl_context_ = std::make_unique<SSLContext>(*opts.ssl_options->ssl_context);
} else {
ssl_context_ = std::make_unique<SSLContext>(ssl_params_);
}
}
SSLSocketFactory::~SSLSocketFactory() = default;
std::unique_ptr<Socket> SSLSocketFactory::doConnect(const NetworkAddress& address, const ClientOptions& opts) {
SocketTimeoutParams timeout_params { opts.connection_connect_timeout, opts.connection_recv_timeout, opts.connection_send_timeout };
return std::make_unique<SSLSocket>(address, timeout_params, ssl_params_, *ssl_context_);
}
std::unique_ptr<InputStream> SSLSocket::makeInputStream() const {
return std::make_unique<SSLSocketInput>(ssl_.get());
}
std::unique_ptr<OutputStream> SSLSocket::makeOutputStream() const {
return std::make_unique<SSLSocketOutput>(ssl_.get());
}
SSLSocketInput::SSLSocketInput(SSL *ssl)
: ssl_(ssl)
{}
size_t SSLSocketInput::DoRead(void* buf, size_t len) {
size_t actually_read;
HANDLE_SSL_ERROR(ssl_, SSL_read_ex(ssl_, buf, len, &actually_read));
return actually_read;
}
SSLSocketOutput::SSLSocketOutput(SSL *ssl)
: ssl_(ssl)
{}
size_t SSLSocketOutput::DoWrite(const void* data, size_t len) {
if (len > std::numeric_limits<int>::max())
// FIXME(vnemkov): We should do multiple `SSL_write`s in this case.
throw AssertionError("Failed to write too big chunk at once "
+ std::to_string(len) + " > " + std::to_string(std::numeric_limits<int>::max()));
return static_cast<size_t>(HANDLE_SSL_ERROR(ssl_, SSL_write(ssl_, data, static_cast<int>(len))));
}
#undef HANDLE_SSL_ERROR
}