diff --git a/clickhouse/base/sslsocket.cpp b/clickhouse/base/sslsocket.cpp index 641ff014..2b9a6cac 100644 --- a/clickhouse/base/sslsocket.cpp +++ b/clickhouse/base/sslsocket.cpp @@ -27,17 +27,16 @@ std::string getCertificateInfo(X509* cert) return std::string(data, len); } -void throwSSLError(SSL * ssl, int error, const char * /*location*/, const char * /*statement*/) { +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) { - // TODO: maybe print certificate only if handshake isn't completed (SSL_get_state(ssl) != TLS_ST_OK) - if (auto ssl_session = SSL_get_session(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 @@ -46,7 +45,44 @@ void throwSSLError(SSL * ssl, int error, const char * /*location*/, const char * // << "\n\t last err: " << ERR_peek_last_error() // << std::endl; - throw std::runtime_error(std::string("OpenSSL error: ") + std::to_string(error) + " : " + reason_str); + throw std::runtime_error(prefix + std::to_string(error) + " : " + reason_str); +} + +void configureSSL(const clickhouse::SSLParams::ConfigurationType & configuration, SSL * ssl, SSL_CTX * context = nullptr) { + std::unique_ptr 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 std::runtime_error("Failed to configure OpenSSL: command '" + kv.first + "' needs no value"); + else if (err == -2) + throw std::runtime_error("Failed to cofigure OpenSSL: unknown command '" + kv.first + "'"); + else if (err == -3) + throw std::runtime_error("Failed to cofigure OpenSSL: command '" + kv.first + "' requires a value"); + else + throw std::runtime_error("Failed to cofigure OpenSSL: command '" + kv.first + "' unknown error: " + std::to_string(err)); + } } #define STRINGIFY_HELPER(x) #x @@ -101,8 +137,17 @@ SSL_CTX * prepareSSLContext(const clickhouse::SSLParams & context_params) { #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; + const auto& ssl_options = *opts.ssl_options; return clickhouse::SSLParams{ ssl_options.path_to_ca_files, ssl_options.path_to_ca_directory, @@ -110,7 +155,10 @@ clickhouse::SSLParams GetSSLParams(const clickhouse::ClientOptions& opts) { ssl_options.context_options, ssl_options.min_protocol_version, ssl_options.max_protocol_version, - ssl_options.use_sni + ssl_options.use_sni, + ssl_options.skip_verification, + ssl_options.host_flags, + convertConfiguration(ssl_options.configuration) }; } @@ -141,7 +189,7 @@ SSL_CTX * SSLContext::getContext() { } \ else \ return ret_code; \ -}() +} () /* // debug macro for tracing SSL state #define LOG_SSL_STATE() std::cerr << "!!!!" << LOCATION << " @" << __FUNCTION__ \ @@ -158,49 +206,59 @@ SSLSocket::SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, if (!ssl) throw std::runtime_error("Failed to create SSL instance"); + std::unique_ptr ip_addr(a2i_IPADDRESS(addr.Host().c_str()), &ASN1_OCTET_STRING_free); + HANDLE_SSL_ERROR(ssl, SSL_set_fd(ssl, 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)); - auto peer_certificate = SSL_get_peer_certificate(ssl); - - if (!peer_certificate) - throw std::runtime_error("Failed to verify SSL connection: server provided no certificate."); - if (const auto verify_result = SSL_get_verify_result(ssl); verify_result != X509_V_OK) { + 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 std::runtime_error("Failed to verify SSL connection, X509_v error: " + std::to_string(verify_result) + " " + error_message - + "\nServer certificate: " + getCertificateInfo(peer_certificate)); + + "\nServer certificate: " + getCertificateInfo(SSL_get_peer_certificate(ssl))); } - if (ssl_params.use_SNI) { - auto hostname = addr.Host(); - char * out_name = nullptr; - - std::unique_ptr addr(a2i_IPADDRESS(hostname.c_str()), &ASN1_OCTET_STRING_free); - if (addr) { - // if hostname is actually an IP address - HANDLE_SSL_ERROR(ssl, X509_check_ip( - peer_certificate, - ASN1_STRING_get0_data(addr.get()), - ASN1_STRING_length(addr.get()), - 0)); - } else { - HANDLE_SSL_ERROR(ssl, X509_check_host(peer_certificate, hostname.c_str(), hostname.length(), 0, &out_name)); - } - } + // 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 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(*opts.ssl_options.ssl_context); + if (opts.ssl_options->ssl_context) { + ssl_context_ = std::make_unique(*opts.ssl_options->ssl_context); } else { ssl_context_ = std::make_unique(ssl_params_); } diff --git a/clickhouse/base/sslsocket.h b/clickhouse/base/sslsocket.h index bd478975..f37e4a5a 100644 --- a/clickhouse/base/sslsocket.h +++ b/clickhouse/base/sslsocket.h @@ -3,6 +3,8 @@ #include "socket.h" #include +#include +#include typedef struct ssl_ctx_st SSL_CTX; typedef struct ssl_st SSL; @@ -18,6 +20,10 @@ struct SSLParams int min_protocol_version; int max_protocol_version; bool use_SNI; + bool skip_verification; + int host_flags; + using ConfigurationType = std::vector>>; + ConfigurationType configuration; }; class SSLContext @@ -42,8 +48,7 @@ class SSLContext class SSLSocket : public Socket { public: - explicit SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, - SSLContext& context); + explicit SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, SSLContext& context); SSLSocket(SSLSocket &&) = default; ~SSLSocket() override = default; @@ -53,6 +58,7 @@ class SSLSocket : public Socket { std::unique_ptr makeInputStream() const override; std::unique_ptr makeOutputStream() const override; + static void validateParams(const SSLParams & ssl_params); private: std::unique_ptr ssl_; }; diff --git a/clickhouse/client.cpp b/clickhouse/client.cpp index 23fd8cca..3b763fb4 100644 --- a/clickhouse/client.cpp +++ b/clickhouse/client.cpp @@ -63,8 +63,8 @@ std::ostream& operator<<(std::ostream& os, const ClientOptions& opt) { << " compression_method:" << (opt.compression_method == CompressionMethod::LZ4 ? "LZ4" : "None"); #if defined(WITH_OPENSSL) - if (opt.ssl_options.use_ssl) { - const auto & ssl_options = opt.ssl_options; + if (opt.ssl_options) { + const auto & ssl_options = *opt.ssl_options; os << " SSL (" << " ssl_context: " << (ssl_options.ssl_context ? "provided by user" : "created internally") << " use_default_ca_locations: " << ssl_options.use_default_ca_locations @@ -80,13 +80,23 @@ std::ostream& operator<<(std::ostream& os, const ClientOptions& opt) { return os; } +ClientOptions& ClientOptions::SetSSLOptions(ClientOptions::SSLOptions options) +{ +#ifdef WITH_OPENSSL + ssl_options = options; + return *this; +#else + (void)options; + throw std::runtime_error("Library was built with no SSL support"); +#endif +} namespace { std::unique_ptr GetSocketFactory(const ClientOptions& opts) { (void)opts; #if defined(WITH_OPENSSL) - if (opts.ssl_options.use_ssl) + if (opts.ssl_options) return std::make_unique(opts); else #endif @@ -95,7 +105,6 @@ std::unique_ptr GetSocketFactory(const ClientOptions& opts) { } - class Client::Impl { public: Impl(const ClientOptions& opts); diff --git a/clickhouse/client.h b/clickhouse/client.h index 8f48cdaa..91f3d082 100644 --- a/clickhouse/client.h +++ b/clickhouse/client.h @@ -20,10 +20,9 @@ #include #include #include +#include -#if defined(WITH_OPENSSL) typedef struct ssl_ctx_st SSL_CTX; -#endif namespace clickhouse { @@ -105,10 +104,7 @@ struct ClientOptions { */ DECLARE_FIELD(max_compression_chunk_size, unsigned int, SetMaxCompressionChunkSize, 65535); -#if defined(WITH_OPENSSL) struct SSLOptions { - bool use_ssl = true; // not expected to be set manually. - /** There are two ways to configure an SSL connection: * - provide a pre-configured SSL_CTX, which is not modified and not owned by the Client. * - provide a set of options and allow the Client to create and configure SSL_CTX by itself. @@ -118,6 +114,9 @@ struct ClientOptions { * If NOT null client DONES NOT take ownership of context and it must be valid for client lifetime. * If null client initlaizes OpenSSL and creates his own context, initializes it using * other options, like path_to_ca_files, path_to_ca_directory, use_default_ca_locations, etc. + * + * Either way context is used to create an SSL-connection, which is then configured with + * whatever was provided as `configuration`, `host_flags`, `skip_verification` and `use_sni`. */ SSL_CTX * ssl_context = nullptr; auto & SetExternalSSLContext(SSL_CTX * new_ssl_context) { @@ -147,16 +146,45 @@ struct ClientOptions { */ DECLARE_FIELD(context_options, int, SetContextOptions, DEFAULT_VALUE); - /** Use SNI at ClientHello and verify that certificate is issued to the hostname we are trying to connect to + /** Use SNI at ClientHello */ DECLARE_FIELD(use_sni, bool, SetUseSNI, true); + /** Skip SSL session verification (server's certificate, etc). + * + * WARNING: settig to true will bypass all SSL session checks, which + * is dangerous, but can be used against self-signed certificates, e.g. for testing purposes. + */ + DECLARE_FIELD(skip_verification, bool, SetSkipVerification, false); + + /** Mode of verifying host ssl certificate against name of the host, set with SSL_set_hostflags. + * For details see https://www.openssl.org/docs/man1.1.1/man3/SSL_set_hostflags.html + */ + DECLARE_FIELD(host_flags, int, SetHostVerifyFlags, DEFAULT_VALUE); + + struct CommandAndValue { + std::string command; + std::optional value = std::nullopt; + }; + /** Extra configuration options, set with SSL_CONF_cmd. + * For deatils see https://www.openssl.org/docs/man1.1.1/man3/SSL_CONF_cmd.html + * + * Takes multiple pairs of command-value strings, all commands are supported, + * and prefix is empty. + * i.e. pass `sigalgs` or `SignatureAlgorithms` instead of `-sigalgs`. + * + * Rewrites any other options/flags if set in other ways. + */ + DECLARE_FIELD(configuration, std::vector, SetConfiguration, {}); + static const int DEFAULT_VALUE = -1; }; - // By default SSL is turned off, hence the `{false}` - DECLARE_FIELD(ssl_options, SSLOptions, SetSSLOptions, {false}); -#endif + // By default SSL is turned off. + std::optional ssl_options = std::nullopt; + + // Will throw an exception if client was built without SSL support. + ClientOptions& SetSSLOptions(SSLOptions options); #undef DECLARE_FIELD }; diff --git a/tests/simple/CMakeLists.txt b/tests/simple/CMakeLists.txt index 9f223630..e562f1db 100644 --- a/tests/simple/CMakeLists.txt +++ b/tests/simple/CMakeLists.txt @@ -1,4 +1,5 @@ ADD_EXECUTABLE (simple-test + ../../ut/utils.cpp main.cpp ) diff --git a/tests/simple/main.cpp b/tests/simple/main.cpp index 10e30bb7..470e9e4e 100644 --- a/tests/simple/main.cpp +++ b/tests/simple/main.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -14,22 +16,11 @@ using namespace clickhouse; using namespace std; -std::string getEnvOrDefault(const std::string& env, const std::string& default_val) -{ - const char* v = std::getenv(env.c_str()); - return v ? v : default_val; -} - inline void PrintBlock(const Block& block) { for (Block::Iterator bi(block); bi.IsValid(); bi.Next()) { std::cout << bi.Name() << " "; } - std::cout << std::endl; - - for (size_t i = 0; i < block.GetRowCount(); ++i) { - std::cout << (*block[0]->As())[i] << " " - << (*block[1]->As())[i] << "\n"; - } + std::cout << std::endl << block; } inline void ArrayExample(Client& client) { @@ -504,26 +495,23 @@ static void RunTests(Client& client) { int main() { try { + const auto localHostEndpoint = ClientOptions() + .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) + .SetPort( getEnvOrDefault("CLICKHOUSE_PORT", "9000")) + .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) + .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) + .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")); + { - Client client(ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_PORT", "9000"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) - .SetPingBeforeQuery(true)); + Client client(ClientOptions(localHostEndpoint) + .SetPingBeforeQuery(true)); RunTests(client); } { - Client client(ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_PORT", "9000"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::LZ4)); + Client client(ClientOptions(localHostEndpoint) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::LZ4)); RunTests(client); } } catch (const std::exception& e) { diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index 792dd4af..0c8c8a6c 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -866,22 +866,19 @@ TEST_P(ClientCase, DateTime64) { ASSERT_EQ(total_rows, data.size()); } +const auto LocalHostEndpoint = ClientOptions() + .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) + .SetPort( getEnvOrDefault("CLICKHOUSE_PORT", "9000")) + .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) + .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) + .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")); + INSTANTIATE_TEST_SUITE_P( Client, ClientCase, ::testing::Values( - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_PORT", "9000"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) + ClientOptions(LocalHostEndpoint) .SetPingBeforeQuery(true), - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_PORT", "9000"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) + ClientOptions(LocalHostEndpoint) .SetPingBeforeQuery(false) .SetCompressionMethod(CompressionMethod::LZ4) )); @@ -889,32 +886,37 @@ INSTANTIATE_TEST_SUITE_P( namespace { using namespace clickhouse; -const auto QUERIES = std::vector{"SELECT version()", "SELECT fqdn()", "SELECT buildId()", - "SELECT uptime()", "SELECT filesystemFree()", "SELECT now()"}; +const auto QUERIES = std::vector{ + "SELECT version()", + "SELECT fqdn()", + "SELECT buildId()", + "SELECT uptime()", + "SELECT filesystemFree()", + "SELECT now()" +}; } INSTANTIATE_TEST_SUITE_P(ClientLocalReadonly, ReadonlyClientTest, - ::testing::Values(ReadonlyClientTest::ParamType{ - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_PORT", "9000"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_USER", "default")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) - .SetSendRetries(1) - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::None), - QUERIES})); + ::testing::Values(ReadonlyClientTest::ParamType{ + ClientOptions(LocalHostEndpoint) + .SetSendRetries(1) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None), + QUERIES + } +)); INSTANTIATE_TEST_SUITE_P(ClientLocalFailed, ConnectionFailedClientTest, - ::testing::Values(ConnectionFailedClientTest::ParamType{ - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_PORT", "9000"))) - .SetUser("non_existing_user_clickhouse_cpp_test") - .SetPassword("wrongpwd") - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) - .SetSendRetries(1) - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::None), - "Authentication failed: password is incorrect"})); + ::testing::Values(ConnectionFailedClientTest::ParamType{ + ClientOptions() + .SetHost( getEnvOrDefault("CLICKHOUSE_HOST", "localhost")) + .SetPort( getEnvOrDefault("CLICKHOUSE_PORT", "9000")) + .SetUser("non_existing_user_clickhouse_cpp_test") + .SetPassword("wrongpwd") + .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_DB", "default")) + .SetSendRetries(1) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None), + ExpectingException{"Authentication failed: password is incorrect"} + } +)); diff --git a/ut/connection_failed_client_test.cpp b/ut/connection_failed_client_test.cpp index 697e9a93..3da045a9 100644 --- a/ut/connection_failed_client_test.cpp +++ b/ut/connection_failed_client_test.cpp @@ -14,7 +14,7 @@ namespace { TEST_P(ConnectionFailedClientTest, ValidateConnectionError) { const auto & client_options = std::get<0>(GetParam()); - const auto & exception_message = std::get<1>(GetParam()); + const auto & ee = std::get<1>(GetParam()); std::unique_ptr client; try { @@ -22,7 +22,8 @@ TEST_P(ConnectionFailedClientTest, ValidateConnectionError) { ASSERT_EQ(nullptr, client.get()) << "Connectiong established but it should have failed"; } catch (const std::exception & e) { const auto message = std::string_view(e.what()); - ASSERT_TRUE(message.find(exception_message) != std::string_view::npos) - << "Actual exception message: " << e.what() << std::endl; + ASSERT_TRUE(message.find(ee.exception_message) != std::string_view::npos) + << "Expected exception message: " << ee.exception_message << "\n" + << "Actual exception message : " << e.what() << std::endl; } } diff --git a/ut/connection_failed_client_test.h b/ut/connection_failed_client_test.h index 6c093c1f..90849330 100644 --- a/ut/connection_failed_client_test.h +++ b/ut/connection_failed_client_test.h @@ -7,6 +7,11 @@ #include #include +// Just a wrapper to stand out from strings. +struct ExpectingException { + std::string exception_message; +}; + /// Verify that connection fails with some specific message. class ConnectionFailedClientTest : public testing::TestWithParam< - std::tuple> {}; + std::tuple> {}; diff --git a/ut/ssl_ut.cpp b/ut/ssl_ut.cpp index 1e806188..92cc5bfc 100644 --- a/ut/ssl_ut.cpp +++ b/ut/ssl_ut.cpp @@ -40,7 +40,7 @@ INSTANTIATE_TEST_SUITE_P( ::testing::Values(ReadonlyClientTest::ParamType { ClientOptions() .SetHost( getEnvOrDefault("CLICKHOUSE_SECURE_HOST", "github.demo.trial.altinity.cloud")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_SECURE_PORT", "9440"))) + .SetPort( getEnvOrDefault("CLICKHOUSE_SECURE_PORT", "9440")) .SetUser( getEnvOrDefault("CLICKHOUSE_SECURE_USER", "demo")) .SetPassword( getEnvOrDefault("CLICKHOUSE_SECURE_PASSWORD", "demo")) .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_SECURE_DB", "default")) @@ -53,61 +53,121 @@ INSTANTIATE_TEST_SUITE_P( } )); + +const auto ClickHouseExplorerConfig = ClientOptions() + .SetHost( getEnvOrDefault("CLICKHOUSE_SECURE2_HOST", "gh-api.clickhouse.tech")) + .SetPort( getEnvOrDefault("CLICKHOUSE_SECURE2_PORT", "9440")) + .SetUser( getEnvOrDefault("CLICKHOUSE_SECURE2_USER", "explorer")) + .SetPassword( getEnvOrDefault("CLICKHOUSE_SECURE2_PASSWORD", "")) + .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_SECURE2_DB", "default")) + .SetSendRetries(1) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None); + + INSTANTIATE_TEST_SUITE_P( Remote_GH_API_TLS, ReadonlyClientTest, ::testing::Values(ReadonlyClientTest::ParamType { - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_SECURE2_HOST", "gh-api.clickhouse.tech")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_SECURE2_PORT", "9440"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_SECURE2_USER", "explorer")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_SECURE2_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_SECURE2_DB", "default")) - .SetSendRetries(1) - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::None) + ClientOptions(ClickHouseExplorerConfig) .SetSSLOptions(ClientOptions::SSLOptions() .SetPathToCADirectory(DEFAULT_CA_DIRECTORY_PATH)), QUERIES } )); +// For some reasons doen't work on MacOS. +// Looks like `VerifyCAPath` has no effect, while parsing and setting value works. +// Also for some reason SetPathToCADirectory() + SSL_CTX_load_verify_locations() works. +#if !defined(__APPLE__) +TEST(OpenSSLConfiguration, ValidValues) { + // Verify that Client with valid configuration set via SetConfiguration is able to connect. + + EXPECT_NO_THROW( + Client(ClientOptions(ClickHouseExplorerConfig) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetConfiguration({ + {"VerifyCAPath", DEFAULT_CA_DIRECTORY_PATH}, + {"MinProtocol", "TLSv1.3"}, + {"VerifyMode", "Peer"}, + {"no_comp"} // shorthand for command with no value + }) + )) + ); +} +#endif + +TEST(OpenSSLConfiguration, InValidValues) { + // Verify that invalid options cause throwing exception. + std::vector configurations = { + {"VerifyCAPath", std::nullopt}, // missing required value + {"VerifyCAPath"}, // missing required value, shorthand + {"MinProtocol", "NOT A STRING"}, // invalid value + {"MinProtocol", ""}, // invalid value + {"unknownCommand"}, // invalid command, shorthand + {"unknownCommand", "with unexpected value"}, // invalid command + some value + {"", std::nullopt}, // empty command with no value + {""}, // empty command with no value + {"no_comp", "unexpected value"} // value for command that doesn't require a value. + }; + + // Unfortunately, there is no way of checking configuration validity without + // creating a Client and pointing that client to a working CH server. + // So we mix valid and invalid configurations by providing the correct + // server config but incorrect single Command-Value pair individually. + for (const auto & cv : configurations) { + EXPECT_ANY_THROW( + Client(ClientOptions(ClickHouseExplorerConfig) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetConfiguration({cv}) + )); + ) << "On command \'" << cv.command << "\' and value: " << (cv.value ? *cv.value : ""); + } +} + + +//INSTANTIATE_TEST_SUITE_P( +// Remote_GH_API_TLS_WithStringConfig, ReadonlyClientTest, +// ::testing::Values(ReadonlyClientTest::ParamType { +// ClientOptions(ClickHouseExplorerConfig) +// .SetSSLOptions(ClientOptions::SSLOptions() +// .SetConfiguration({{"", DEFAULT_CA_DIRECTORY_PATH}}) +// QUERIES +// } +//)); + +INSTANTIATE_TEST_SUITE_P( + Remote_GH_API_TLS_no_CA, ReadonlyClientTest, + ::testing::Values(ReadonlyClientTest::ParamType { + ClientOptions(ClickHouseExplorerConfig) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetUseDefaultCALocations(false) + .SetSkipVerification(true)), // No CA loaded, but verfication is skipped + {"SELECT 1;"} + } +)); + INSTANTIATE_TEST_SUITE_P( Remote_GH_API_TLS_no_CA, ConnectionFailedClientTest, ::testing::Values(ConnectionFailedClientTest::ParamType { - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_SECURE2_HOST", "gh-api.clickhouse.tech")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_SECURE2_PORT", "9440"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_SECURE2_USER", "explorer")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_SECURE2_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_SECURE2_DB", "default")) - .SetSendRetries(1) - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::None) + ClientOptions(ClickHouseExplorerConfig) .SetSSLOptions(ClientOptions::SSLOptions() .SetUseDefaultCALocations(false)), - "X509_v error: 20 unable to get local issuer certificate" + ExpectingException{"X509_v error: 20 unable to get local issuer certificate"} } )); INSTANTIATE_TEST_SUITE_P( Remote_GH_API_TLS_wrong_TLS_version, ConnectionFailedClientTest, ::testing::Values(ConnectionFailedClientTest::ParamType { - ClientOptions() - .SetHost( getEnvOrDefault("CLICKHOUSE_SECURE2_HOST", "gh-api.clickhouse.tech")) - .SetPort( std::stoi(getEnvOrDefault("CLICKHOUSE_SECURE2_PORT", "9440"))) - .SetUser( getEnvOrDefault("CLICKHOUSE_SECURE2_USER", "explorer")) - .SetPassword( getEnvOrDefault("CLICKHOUSE_SECURE2_PASSWORD", "")) - .SetDefaultDatabase(getEnvOrDefault("CLICKHOUSE_SECURE2_DB", "default")) - .SetSendRetries(1) - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::None) + ClientOptions(ClickHouseExplorerConfig) .SetSSLOptions(ClientOptions::SSLOptions() .SetUseDefaultCALocations(false) .SetMaxProtocolVersion(SSL3_VERSION)), - "no protocols available" + ExpectingException{"no protocols available"} } )); + //// Special test that require properly configured TLS-enabled version of CH running locally //INSTANTIATE_TEST_SUITE_P( // LocalhostTLS_None, ReadonlyClientTest, diff --git a/ut/utils.cpp b/ut/utils.cpp index 87e66d70..9debe5e4 100644 --- a/ut/utils.cpp +++ b/ut/utils.cpp @@ -97,8 +97,7 @@ std::ostream& operator<<(std::ostream & ostr, const Block & block) { return ostr; } -std::ostream& operator<<(std::ostream& ostr, const in_addr& addr) -{ +std::ostream& operator<<(std::ostream& ostr, const in_addr& addr) { char buf[INET_ADDRSTRLEN]; const char* ip_str = inet_ntop(AF_INET, &addr, buf, sizeof(buf)); @@ -108,8 +107,7 @@ std::ostream& operator<<(std::ostream& ostr, const in_addr& addr) return ostr << ip_str; } -std::ostream& operator<<(std::ostream& ostr, const in6_addr& addr) -{ +std::ostream& operator<<(std::ostream& ostr, const in6_addr& addr) { char buf[INET6_ADDRSTRLEN]; const char* ip_str = inet_ntop(AF_INET6, &addr, buf, sizeof(buf)); @@ -118,9 +116,3 @@ std::ostream& operator<<(std::ostream& ostr, const in6_addr& addr) return ostr << ip_str; } - -std::string getEnvOrDefault(const std::string& env, const std::string& default_val) -{ - const char* v = std::getenv(env.c_str()); - return v ? v : default_val; -} diff --git a/ut/utils.h b/ut/utils.h index 90d232d7..9014d1f1 100644 --- a/ut/utils.h +++ b/ut/utils.h @@ -1,9 +1,10 @@ #pragma once #include -#include -#include +#include #include +#include +#include #include #include @@ -13,34 +14,28 @@ namespace clickhouse { } template -struct Timer -{ +struct Timer { using DurationType = ChronoDurationType; Timer() : started_at(Now()) {} - void Restart() - { + void Restart() { started_at = Now(); } - void Start() - { + void Start() { Restart(); } - auto Elapsed() const - { + auto Elapsed() const { return std::chrono::duration_cast(Now() - started_at); } private: - static auto Now() - { - std::chrono::nanoseconds ns = std::chrono::high_resolution_clock::now().time_since_epoch(); - return ns; + static auto Now() { + return std::chrono::high_resolution_clock::now().time_since_epoch(); } private: @@ -109,8 +104,7 @@ class MeasuresCollector { }; template -MeasuresCollector collect(MeasureFunc && f) -{ +MeasuresCollector collect(MeasureFunc && f) { return MeasuresCollector(std::forward(f)); } @@ -120,4 +114,28 @@ std::ostream& operator<<(std::ostream & ostr, const clickhouse::Block & block); std::ostream& operator<<(std::ostream& ostr, const in_addr& addr); std::ostream& operator<<(std::ostream& ostr, const in6_addr& addr); -std::string getEnvOrDefault(const std::string& env, const std::string& default_val); + +template +auto getEnvOrDefault(const std::string& env, const char * default_val) { + const char* v = std::getenv(env.c_str()); + const std::string value = v ? v : default_val; + + if constexpr (std::is_same_v) { + return value; + } else if constexpr (std::is_integral_v) { + // since std::from_chars is not available on GCC-7 on linux + if constexpr (std::is_signed_v) { + if constexpr (sizeof(ResultType) <= sizeof(int)) + return std::stoi(value); + else if constexpr (sizeof(ResultType) <= sizeof(long)) + return std::stol(value); + else if constexpr (sizeof(ResultType) <= sizeof(long long)) + return std::stoll(value); + } else if constexpr (std::is_unsigned_v) { + if constexpr (sizeof(ResultType) <= sizeof(unsigned long)) + return std::stoul(value); + else if constexpr (sizeof(ResultType) <= sizeof(unsigned long long)) + return std::stoull(value); + } + } +}