From cfb9a126450be1e701fb416ff1d7f5ddd83e12d4 Mon Sep 17 00:00:00 2001 From: Vasily Nemkov Date: Mon, 25 Oct 2021 13:31:52 +0300 Subject: [PATCH 1/4] Implemented connecting to ClickHouse server over TLS - using OpenSSL - set `-DWITH_OPENSSL:BOOL=ON` to enable - can use non-system OpenSSL with `-DOPENSSL_ROOT_DIR=` - Client behavior is customizable with `ClientOptions::SSLOptions` - min/max protocol versions - SNI extension ON\OFF - CA files or directory - other tweaks available via `SSL_CTX_set_options` - OR provide pre-configured `SSL_CTX` with any customizations. Tests to verify TLS connection to an external host. Tests to verify TLS connection to locally started CH. --- .github/workflows/linux.yml | 34 ++--- .github/workflows/linux_ssl.yml | 16 +++ CMakeLists.txt | 11 +- clickhouse/CMakeLists.txt | 9 ++ clickhouse/base/socket.cpp | 171 +++++++++++----------- clickhouse/base/socket.h | 39 +++--- clickhouse/base/sslsocket.cpp | 211 ++++++++++++++++++++++++++++ clickhouse/base/sslsocket.h | 81 +++++++++++ clickhouse/client.cpp | 241 +++++++++++++++++++------------- clickhouse/client.h | 65 ++++++++- clickhouse/columns/date.h | 6 + clickhouse/columns/decimal.h | 2 + clickhouse/columns/enum.h | 2 + clickhouse/columns/numeric.h | 1 + cmake/openssl.cmake | 9 ++ tests/simple/CMakeLists.txt | 3 +- ut/client_ut.cpp | 199 ++++++++++++++++++++++++++ ut/socket_ut.cpp | 4 +- 18 files changed, 872 insertions(+), 232 deletions(-) create mode 100644 .github/workflows/linux_ssl.yml create mode 100644 clickhouse/base/sslsocket.cpp create mode 100644 clickhouse/base/sslsocket.h create mode 100644 cmake/openssl.cmake diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 529e4558..eeb3ec74 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -5,18 +5,24 @@ on: branches: [ master ] pull_request: branches: [ master ] + workflow_call: + inputs: + extra_cmake_flags: + required: false + type: string + extra_install: + required: false + type: string + gtest_args: + required: false + type: string env: - # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) BUILD_TYPE: Release CH_SERVER_VERSION: 21.3.17.2 jobs: build: - # The CMake configure and build commands are platform agnostic and should work equally - # well on Windows or Mac. You can convert this to a matrix build if you need - # cross-platform coverage. - # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix runs-on: ubuntu-latest strategy: matrix: @@ -39,16 +45,16 @@ jobs: - name: Install dependencies run: | - sudo apt-get install -y ${{ matrix.INSTALL }} + sudo apt-get install -y ${{ matrix.INSTALL }} ${{ inputs.extra_install }} - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: | cmake \ - -D CMAKE_C_COMPILER=${{ matrix.C_COMPILER}} \ - -D CMAKE_CXX_COMPILER=${{ matrix.CXX_COMPILER}} \ - -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_TESTS=ON + -DCMAKE_C_COMPILER=${{ matrix.C_COMPILER}} \ + -DCMAKE_CXX_COMPILER=${{ matrix.CXX_COMPILER}} \ + -B ${{github.workspace}}/build \ + -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DBUILD_TESTS=ON \ + ${{ inputs.extra_cmake_flags }} - name: Build @@ -70,8 +76,4 @@ jobs: - name: Test working-directory: ${{github.workspace}}/build/ut - # Execute tests defined by the CMake configuration. - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - #run: ctest -C ${{env.BUILD_TYPE}} - run: ./clickhouse-cpp-ut "${{env.GTEST_FILTER}}" - + run: ./clickhouse-cpp-ut "${{ inputs.gtest_args }}" diff --git a/.github/workflows/linux_ssl.yml b/.github/workflows/linux_ssl.yml new file mode 100644 index 00000000..5ed39c0d --- /dev/null +++ b/.github/workflows/linux_ssl.yml @@ -0,0 +1,16 @@ +name: Linux-ssl +# Almost the same as regular Linux builds, BUT with enabled SSL support, requires OpenSSL installed + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + uses: Enmk/clickhouse-cpp/.github/workflows/linux.yml@master + with: + extra_cmake_flags: -DWITH_OPENSSL=ON + extra_install: libssl1.1 libssl-dev + gtest_args: --gtest_filter="-*LocalhostTLS*" diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f6c5e00..5dad9515 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,17 +2,20 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.0.2) INCLUDE (cmake/cpp17.cmake) INCLUDE (cmake/subdirs.cmake) +INCLUDE (cmake/openssl.cmake) OPTION(BUILD_BENCHMARK "Build benchmark" OFF) OPTION(BUILD_TESTS "Build tests" OFF) +OPTION(WITH_OPENSSL "Use OpenSSL for TLS connections" OFF) PROJECT (CLICKHOUSE-CLIENT) USE_CXX17() + USE_OPENSSL() - IF ("${CMAKE_BUILD_TYPE}" STREQUAL "") - set(CMAKE_BUILD_TYPE "Debug") - ENDIF() + IF ("${CMAKE_BUILD_TYPE}" STREQUAL "") + set(CMAKE_BUILD_TYPE "Debug") + ENDIF() IF (UNIX) IF (APPLE) @@ -43,4 +46,4 @@ PROJECT (CLICKHOUSE-CLIENT) tests/simple ut ) - ENDIF (BUILD_TESTS) + ENDIF (BUILD_TESTS) diff --git a/clickhouse/CMakeLists.txt b/clickhouse/CMakeLists.txt index 7e10ffd3..c84c093e 100644 --- a/clickhouse/CMakeLists.txt +++ b/clickhouse/CMakeLists.txt @@ -31,6 +31,10 @@ SET ( clickhouse-cpp-lib-src query.cpp ) +if (WITH_OPENSSL) + LIST(APPEND clickhouse-cpp-lib-src base/sslsocket.cpp) +endif() + ADD_LIBRARY (clickhouse-cpp-lib SHARED ${clickhouse-cpp-lib-src}) SET_TARGET_PROPERTIES(clickhouse-cpp-lib PROPERTIES LINKER_LANGUAGE CXX) TARGET_LINK_LIBRARIES (clickhouse-cpp-lib @@ -100,3 +104,8 @@ INSTALL(FILES columns/uuid.h DESTINATION include/clickhouse/columns/) # types INSTALL(FILES types/type_parser.h DESTINATION include/clickhouse/types/) INSTALL(FILES types/types.h DESTINATION include/clickhouse/types/) + +if (WITH_OPENSSL) + target_link_libraries(clickhouse-cpp-lib OpenSSL::SSL) + target_link_libraries(clickhouse-cpp-lib-static OpenSSL::SSL) +endif() diff --git a/clickhouse/base/socket.cpp b/clickhouse/base/socket.cpp index e7788edf..a61d89fd 100644 --- a/clickhouse/base/socket.cpp +++ b/clickhouse/base/socket.cpp @@ -73,10 +73,66 @@ void SetNonBlock(SOCKET fd, bool value) { #endif } +ssize_t Poll(struct pollfd* fds, int nfds, int timeout) noexcept { +#if defined(_win_) + return WSAPoll(fds, nfds, timeout); +#else + return poll(fds, nfds, timeout); +#endif +} + +SOCKET SocketConnect(const NetworkAddress& addr) { + int last_err = 0; + for (auto res = addr.Info(); res != nullptr; res = res->ai_next) { + SOCKET s(socket(res->ai_family, res->ai_socktype, res->ai_protocol)); + + if (s == -1) { + continue; + } + + SetNonBlock(s, true); + + if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) { + int err = errno; + if (err == EINPROGRESS || err == EAGAIN || err == EWOULDBLOCK) { + pollfd fd; + fd.fd = s; + fd.events = POLLOUT; + fd.revents = 0; + ssize_t rval = Poll(&fd, 1, 5000); + + if (rval == -1) { + throw std::system_error(errno, std::system_category(), "fail to connect"); + } + if (rval > 0) { + socklen_t len = sizeof(err); + getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len); + + if (!err) { + SetNonBlock(s, false); + return s; + } + last_err = err; + } + } + } else { + SetNonBlock(s, false); + return s; + } + } + if (last_err > 0) { + throw std::system_error(last_err, std::system_category(), "fail to connect"); + } + throw std::system_error( + errno, std::system_category(), "fail to connect" + ); +} + } // namespace NetworkAddress::NetworkAddress(const std::string& host, const std::string& port) - : info_(nullptr) + : host_(host), + info_(nullptr) { struct addrinfo hints; memset(&hints, 0, sizeof(hints)); @@ -112,29 +168,37 @@ NetworkAddress::~NetworkAddress() { const struct addrinfo* NetworkAddress::Info() const { return info_; } - - -SocketHolder::SocketHolder() - : handle_(-1) -{ +const std::string & NetworkAddress::Host() const { + return host_; } -SocketHolder::SocketHolder(SOCKET s) - : handle_(s) -{ -} -SocketHolder::SocketHolder(SocketHolder&& other) noexcept +Socket::Socket(const NetworkAddress& addr) + : handle_(SocketConnect(addr)) +{} + +Socket::Socket(Socket&& other) noexcept : handle_(other.handle_) { other.handle_ = -1; } -SocketHolder::~SocketHolder() { +Socket& Socket::operator=(Socket&& other) noexcept { + if (this != &other) { + Close(); + + handle_ = other.handle_; + other.handle_ = -1; + } + + return *this; +} + +Socket::~Socket() { Close(); } -void SocketHolder::Close() noexcept { +void Socket::Close() { if (handle_ != -1) { #if defined(_win_) closesocket(handle_); @@ -145,11 +209,7 @@ void SocketHolder::Close() noexcept { } } -bool SocketHolder::Closed() const noexcept { - return handle_ == -1; -} - -void SocketHolder::SetTcpKeepAlive(int idle, int intvl, int cnt) noexcept { +void Socket::SetTcpKeepAlive(int idle, int intvl, int cnt) noexcept { int val = 1; #if defined(_unix_) @@ -169,7 +229,7 @@ void SocketHolder::SetTcpKeepAlive(int idle, int intvl, int cnt) noexcept { #endif } -void SocketHolder::SetTcpNoDelay(bool nodelay) noexcept { +void Socket::SetTcpNoDelay(bool nodelay) noexcept { int val = nodelay; #if defined(_unix_) setsockopt(handle_, IPPROTO_TCP, TCP_NODELAY, &val, sizeof(val)); @@ -178,22 +238,14 @@ void SocketHolder::SetTcpNoDelay(bool nodelay) noexcept { #endif } -SocketHolder& SocketHolder::operator = (SocketHolder&& other) noexcept { - if (this != &other) { - Close(); - - handle_ = other.handle_; - other.handle_ = -1; - } - - return *this; +std::unique_ptr Socket::makeInputStream() const { + return std::make_unique(handle_); } -SocketHolder::operator SOCKET () const noexcept { - return handle_; +std::unique_ptr Socket::makeOutputStream() const { + return std::make_unique(handle_); } - SocketInput::SocketInput(SOCKET s) : s_(s) { @@ -262,61 +314,4 @@ NetrworkInitializer::NetrworkInitializer() { (void)Singleton(); } - -SOCKET SocketConnect(const NetworkAddress& addr) { - int last_err = 0; - for (auto res = addr.Info(); res != nullptr; res = res->ai_next) { - SOCKET s(socket(res->ai_family, res->ai_socktype, res->ai_protocol)); - - if (s == -1) { - continue; - } - - SetNonBlock(s, true); - - if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) { - int err = errno; - if (err == EINPROGRESS || err == EAGAIN || err == EWOULDBLOCK) { - pollfd fd; - fd.fd = s; - fd.events = POLLOUT; - fd.revents = 0; - ssize_t rval = Poll(&fd, 1, 5000); - - if (rval == -1) { - throw std::system_error(errno, std::system_category(), "fail to connect"); - } - if (rval > 0) { - socklen_t len = sizeof(err); - getsockopt(s, SOL_SOCKET, SO_ERROR, (char*)&err, &len); - - if (!err) { - SetNonBlock(s, false); - return s; - } - last_err = err; - } - } - } else { - SetNonBlock(s, false); - return s; - } - } - if (last_err > 0) { - throw std::system_error(last_err, std::system_category(), "fail to connect"); - } - throw std::system_error( - errno, std::system_category(), "fail to connect" - ); -} - - -ssize_t Poll(struct pollfd* fds, int nfds, int timeout) noexcept { -#if defined(_win_) - return WSAPoll(fds, nfds, timeout); -#else - return poll(fds, nfds, timeout); -#endif -} - } diff --git a/clickhouse/base/socket.h b/clickhouse/base/socket.h index 256608dc..b4da52fd 100644 --- a/clickhouse/base/socket.h +++ b/clickhouse/base/socket.h @@ -23,6 +23,9 @@ # endif #endif +#include + + struct addrinfo; namespace clickhouse { @@ -37,23 +40,21 @@ class NetworkAddress { ~NetworkAddress(); const struct addrinfo* Info() const; + const std::string & Host() const; private: + const std::string host_; struct addrinfo* info_; }; -class SocketHolder { +class Socket { public: - SocketHolder(); - SocketHolder(SOCKET s); - SocketHolder(SocketHolder&& other) noexcept; - - ~SocketHolder(); + Socket(const NetworkAddress& addr); + Socket(Socket&& other) noexcept; + Socket& operator=(Socket&& other) noexcept; - void Close() noexcept; - - bool Closed() const noexcept; + virtual ~Socket(); /// @params idle the time (in seconds) the connection needs to remain /// idle before TCP starts sending keepalive probes. @@ -65,21 +66,18 @@ class SocketHolder { /// @params nodelay whether to enable TCP_NODELAY void SetTcpNoDelay(bool nodelay) noexcept; - SocketHolder& operator = (SocketHolder&& other) noexcept; - - operator SOCKET () const noexcept; + virtual std::unique_ptr makeInputStream() const; + virtual std::unique_ptr makeOutputStream() const; -private: - SocketHolder(const SocketHolder&) = delete; - SocketHolder& operator = (const SocketHolder&) = delete; +protected: + Socket(const Socket&) = delete; + Socket& operator = (const Socket&) = delete; + void Close(); SOCKET handle_; }; -/** - * - */ class SocketInput : public InputStream { public: explicit SocketInput(SOCKET s); @@ -108,9 +106,4 @@ static struct NetrworkInitializer { NetrworkInitializer(); } gNetrworkInitializer; -/// -SOCKET SocketConnect(const NetworkAddress& addr); - -ssize_t Poll(struct pollfd* fds, int nfds, int timeout) noexcept; - } diff --git a/clickhouse/base/sslsocket.cpp b/clickhouse/base/sslsocket.cpp new file mode 100644 index 00000000..120f861b --- /dev/null +++ b/clickhouse/base/sslsocket.cpp @@ -0,0 +1,211 @@ +#include "sslsocket.h" + +#include + +#include +#include +#include +#include + +#include + +namespace { + +std::string getCertificateInfo(X509* cert) +{ + std::unique_ptr mem_bio(BIO_new(BIO_s_mem()), &BIO_free); + X509_print(mem_bio.get(), cert); + char * data = nullptr; + size_t len = BIO_get_mem_data(mem_bio.get(), &data); + + return std::string(data, len); +} + +void throwSSLError(SSL * ssl, int error, const char * location, const char * statement) { + 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) { + if (auto ssl_session = SSL_get_session(ssl)) + if (auto server_certificate = SSL_SESSION_get0_peer(ssl_session)) + reason_str += "\nServer certificate: " + getCertificateInfo(server_certificate); + } + + 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 std::runtime_error(std::string("OpenSSL error: ") + std::to_string(error) + " : " + reason_str); +} + +#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 ctx(SSL_CTX_new(method), &SSL_CTX_free); + + if (!ctx) + throw std::runtime_error("Failed to initialize SSL context"); + +#define HANDLE_SSL_CTX_ERROR(statement) do { \ + if (const auto ret_code = statement; !ret_code) \ + throwSSLError(nullptr, ERR_peek_error(), LOCATION, STRINGIFY(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(); +} + + + +} + +#define HANDLE_SSL_ERROR(statement) do { \ + if (const auto ret_code = statement; ret_code <= 0) \ + throwSSLError(ssl_, SSL_get_error(ssl_, ret_code), LOCATION, STRINGIFY(statement)); \ +} while(false); + +namespace clickhouse { + +SSLContext::SSLContext(SSL_CTX & context) + : context_(&context) +{ + SSL_CTX_up_ref(context_); +} + +SSLContext::SSLContext(const SSLParams & context_params) + : context_(prepareSSLContext(context_params)) +{ +} + +SSLContext::~SSLContext() { + SSL_CTX_free(context_); +} + +SSL_CTX * SSLContext::getContext() { + return context_; +} + +/* // 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 SSLParams & ssl_params, SSLContext& context) + : Socket(addr), + ssl_(SSL_new(context.getContext())) +{ + if (!ssl_) + throw std::runtime_error("Failed to create SSL instance"); + + HANDLE_SSL_ERROR(SSL_set_fd(ssl_, handle_)); + if (ssl_params.use_SNI) + HANDLE_SSL_ERROR(SSL_set_tlsext_host_name(ssl_, addr.Host().c_str())); + + SSL_set_connect_state(ssl_); + HANDLE_SSL_ERROR(SSL_connect(ssl_)); + HANDLE_SSL_ERROR(SSL_set_mode(ssl_, SSL_MODE_AUTO_RETRY)); + + if(const auto verify_result = SSL_get_verify_result(ssl_); verify_result != X509_V_OK) { + auto error_message = X509_verify_cert_error_string(verify_result); + auto ssl_session = SSL_get_session(ssl_); + auto cert = SSL_SESSION_get0_peer(ssl_session); + + throw std::runtime_error("Failed to verify SSL connection, X509_v error: " + + std::to_string(verify_result) + + " " + error_message + "\n" + getCertificateInfo(cert)); + } + + if (ssl_params.use_SNI) { + auto ssl_session = SSL_get_session(ssl_); + auto peer_cert = SSL_SESSION_get0_peer(ssl_session); + 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(X509_check_ip( + peer_cert, + ASN1_STRING_get0_data(addr.get()), + ASN1_STRING_length(addr.get()), + 0)); + } else { + HANDLE_SSL_ERROR(X509_check_host(peer_cert, hostname.c_str(), hostname.length(), 0, &out_name)); + } + } +} + +SSLSocket::~SSLSocket() { + SSL_free(ssl_); +} + +std::unique_ptr SSLSocket::makeInputStream() const { + return std::make_unique(ssl_); +} + +std::unique_ptr SSLSocket::makeOutputStream() const { + return std::make_unique(ssl_); +} + +SSLSocketInput::SSLSocketInput(SSL *ssl) + : ssl_(ssl) +{} + +SSLSocketInput::~SSLSocketInput() = default; + +size_t SSLSocketInput::DoRead(void* buf, size_t len) { + size_t actually_read; + HANDLE_SSL_ERROR(SSL_read_ex(ssl_, buf, len, &actually_read)); + return actually_read; +} + +SSLSocketOutput::SSLSocketOutput(SSL *ssl) + : ssl_(ssl) +{} + +SSLSocketOutput::~SSLSocketOutput() = default; + +void SSLSocketOutput::DoWrite(const void* data, size_t len) { + HANDLE_SSL_ERROR(SSL_write(ssl_, data, len)); +} + +} diff --git a/clickhouse/base/sslsocket.h b/clickhouse/base/sslsocket.h new file mode 100644 index 00000000..8a3b38f4 --- /dev/null +++ b/clickhouse/base/sslsocket.h @@ -0,0 +1,81 @@ +#pragma once + +#include "socket.h" + +typedef struct ssl_ctx_st SSL_CTX; +typedef struct ssl_st SSL; + +namespace clickhouse { + +struct SSLParams +{ + std::vector path_to_ca_files; + std::string path_to_ca_directory; + bool use_default_ca_locations; + int context_options; + int min_protocol_version; + int max_protocol_version; + bool use_SNI; +}; + +class SSLContext +{ +public: + explicit SSLContext(SSL_CTX & context); + explicit SSLContext(const SSLParams & context_params); + ~SSLContext(); + + SSLContext(const SSLContext &) = delete; + SSLContext& operator=(const SSLContext &) = delete; + SSLContext(SSLContext &&) = delete; + SSLContext& operator=(SSLContext &) = delete; + +private: + friend class SSLSocket; + SSL_CTX * getContext(); + +private: + SSL_CTX * const context_; +}; + +class SSLSocket : public Socket { +public: + explicit SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, SSLContext& context); + SSLSocket(SSLSocket &&) = default; + ~SSLSocket(); + + SSLSocket(const SSLSocket & ) = delete; + SSLSocket& operator=(const SSLSocket & ) = delete; + + std::unique_ptr makeInputStream() const override; + std::unique_ptr makeOutputStream() const override; + +private: + SSL *ssl_; +}; + +class SSLSocketInput : public InputStream { +public: + explicit SSLSocketInput(SSL *ssl); + ~SSLSocketInput(); + +protected: + size_t DoRead(void* buf, size_t len) override; + +private: + SSL *ssl_; +}; + +class SSLSocketOutput : public OutputStream { +public: + explicit SSLSocketOutput(SSL *ssl); + ~SSLSocketOutput(); + +protected: + void DoWrite(const void* data, size_t len) override; + +private: + SSL *ssl_; +}; + +} diff --git a/clickhouse/client.cpp b/clickhouse/client.cpp index 6d06b4fb..093f58df 100644 --- a/clickhouse/client.cpp +++ b/clickhouse/client.cpp @@ -19,12 +19,14 @@ #include #include +#if WITH_OPENSSL +#include "base/sslsocket.h" +#endif + #define DBMS_NAME "ClickHouse" #define DBMS_VERSION_MAJOR 1 #define DBMS_VERSION_MINOR 2 -#define REVISION 54405 - #define DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES 50264 #define DBMS_MIN_REVISION_WITH_TOTAL_ROWS_IN_PROGRESS 51554 #define DBMS_MIN_REVISION_WITH_BLOCK_INFO 51903 @@ -32,12 +34,13 @@ #define DBMS_MIN_REVISION_WITH_SERVER_TIMEZONE 54058 #define DBMS_MIN_REVISION_WITH_QUOTA_KEY_IN_CLIENT_INFO 54060 //#define DBMS_MIN_REVISION_WITH_TABLES_STATUS 54226 -//#define DBMS_MIN_REVISION_WITH_TIME_ZONE_PARAMETER_IN_DATETIME_DATA_TYPE 54337 #define DBMS_MIN_REVISION_WITH_SERVER_DISPLAY_NAME 54372 #define DBMS_MIN_REVISION_WITH_VERSION_PATCH 54401 #define DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE 54405 #define DBMS_MIN_REVISION_WITH_TIME_ZONE_PARAMETER_IN_DATETIME_DATA_TYPE 54337 +#define REVISION DBMS_MIN_REVISION_WITH_LOW_CARDINALITY_TYPE + namespace clickhouse { struct ClientInfo { @@ -62,8 +65,23 @@ std::ostream& operator<<(std::ostream& os, const ClientOptions& opt) { << " send_retries:" << opt.send_retries << " retry_timeout:" << opt.retry_timeout.count() << " compression_method:" - << (opt.compression_method == CompressionMethod::LZ4 ? "LZ4" : "None") - << ")"; + << (opt.compression_method == CompressionMethod::LZ4 ? "LZ4" : "None"); +#if WITH_OPENSSL + if (opt.ssl_options.use_ssl) { + 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 + << " use_default_ca_locations: " << ssl_options.use_default_ca_locations + << " path_to_ca_files: " << ssl_options.path_to_ca_files.size() << " items" + << " path_to_ca_directory: " << ssl_options.path_to_ca_directory + << " min_protocol_version: " << ssl_options.min_protocol_version + << " min_protocol_version: " << ssl_options.max_protocol_version + << " context_options: " << ssl_options.context_options + << ")"; + } +#endif + os << ")"; return os; } @@ -139,15 +157,19 @@ class Client::Impl { QueryEvents* events_; int compression_ = CompressionState::Disable; - SocketHolder socket_; + std::unique_ptr socket_input_; + std::unique_ptr buffered_input_; + std::unique_ptr input_; - SocketInput socket_input_; - BufferedInput buffered_input_; - CodedInputStream input_; + std::unique_ptr socket_output_; + std::unique_ptr buffered_output_; + std::unique_ptr output_; - SocketOutput socket_output_; - BufferedOutput buffered_output_; - CodedOutputStream output_; + std::unique_ptr socket_; + +#if WITH_OPENSSL + std::unique_ptr ssl_context_; +#endif ServerInfo server_info_; }; @@ -156,13 +178,6 @@ class Client::Impl { Client::Impl::Impl(const ClientOptions& opts) : options_(opts) , events_(nullptr) - , socket_(-1) - , socket_input_(socket_) - , buffered_input_(&socket_input_) - , input_(&buffered_input_) - , socket_output_(socket_) - , buffered_output_(&socket_output_) - , output_(&buffered_output_) { // TODO: throw on big-endianness of platform @@ -274,8 +289,8 @@ void Client::Impl::Insert(const std::string& table_name, const Block& block) { } void Client::Impl::Ping() { - WireFormat::WriteUInt64(&output_, ClientCodes::Ping); - output_.Flush(); + WireFormat::WriteUInt64(output_.get(), ClientCodes::Ping); + output_->Flush(); uint64_t server_packet; const bool ret = ReceivePacket(&server_packet); @@ -286,26 +301,64 @@ void Client::Impl::Ping() { } void Client::Impl::ResetConnection() { - SocketHolder s(SocketConnect(NetworkAddress(options_.host, std::to_string(options_.port)))); - if (s.Closed()) { - throw std::system_error(errno, std::system_category()); + std::unique_ptr socket; + + const auto address = NetworkAddress(options_.host, std::to_string(options_.port)); +#if WITH_OPENSSL + // TODO: maybe do not re-create context multiple times upon reconnection - that doesn't make sense. + std::unique_ptr ssl_context; + if (options_.ssl_options.use_ssl) { + const auto ssl_options = options_.ssl_options; + const auto ssl_params = 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 + }; + + if (ssl_options.ssl_context) + ssl_context = std::make_unique(*ssl_options.ssl_context); + else { + ssl_context = std::make_unique(ssl_params); + } + + socket = std::make_unique(address, ssl_params, *ssl_context); } + else +#endif + socket = std::make_unique(address); if (options_.tcp_keepalive) { - s.SetTcpKeepAlive(options_.tcp_keepalive_idle.count(), + socket->SetTcpKeepAlive(options_.tcp_keepalive_idle.count(), options_.tcp_keepalive_intvl.count(), options_.tcp_keepalive_cnt); } if (options_.tcp_nodelay) { - s.SetTcpNoDelay(options_.tcp_nodelay); + socket->SetTcpNoDelay(options_.tcp_nodelay); } - socket_ = std::move(s); - socket_input_ = SocketInput(socket_); - socket_output_ = SocketOutput(socket_); - buffered_input_.Reset(); - buffered_output_.Reset(); + auto socket_input = socket->makeInputStream(); + auto socket_output = socket->makeOutputStream(); + auto buffered_input = std::make_unique(socket_input.get()); + auto buffered_output = std::make_unique(socket_output.get()); + auto input = std::make_unique(buffered_input.get()); + auto output = std::make_unique(buffered_output.get()); + + std::swap(socket_input, socket_input_); + std::swap(socket_output, socket_output_); + std::swap(buffered_input, buffered_input_); + std::swap(buffered_output, buffered_output_); + std::swap(input, input_); + std::swap(output, output_); + std::swap(socket, socket_); + +#if WITH_OPENSSL + std::swap(ssl_context_, ssl_context); +#endif if (!Handshake()) { throw std::runtime_error("fail to connect to " + options_.host); @@ -329,7 +382,7 @@ bool Client::Impl::Handshake() { bool Client::Impl::ReceivePacket(uint64_t* server_packet) { uint64_t packet_type = 0; - if (!input_.ReadVarint64(&packet_type)) { + if (!input_->ReadVarint64(&packet_type)) { return false; } if (server_packet) { @@ -352,22 +405,22 @@ bool Client::Impl::ReceivePacket(uint64_t* server_packet) { case ServerCodes::ProfileInfo: { Profile profile; - if (!WireFormat::ReadUInt64(&input_, &profile.rows)) { + if (!WireFormat::ReadUInt64(input_.get(), &profile.rows)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &profile.blocks)) { + if (!WireFormat::ReadUInt64(input_.get(), &profile.blocks)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &profile.bytes)) { + if (!WireFormat::ReadUInt64(input_.get(), &profile.bytes)) { return false; } - if (!WireFormat::ReadFixed(&input_, &profile.applied_limit)) { + if (!WireFormat::ReadFixed(input_.get(), &profile.applied_limit)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &profile.rows_before_limit)) { + if (!WireFormat::ReadUInt64(input_.get(), &profile.rows_before_limit)) { return false; } - if (!WireFormat::ReadFixed(&input_, &profile.calculated_rows_before_limit)) { + if (!WireFormat::ReadFixed(input_.get(), &profile.calculated_rows_before_limit)) { return false; } @@ -381,14 +434,14 @@ bool Client::Impl::ReceivePacket(uint64_t* server_packet) { case ServerCodes::Progress: { Progress info; - if (!WireFormat::ReadUInt64(&input_, &info.rows)) { + if (!WireFormat::ReadUInt64(input_.get(), &info.rows)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &info.bytes)) { + if (!WireFormat::ReadUInt64(input_.get(), &info.bytes)) { return false; } if (REVISION >= DBMS_MIN_REVISION_WITH_TOTAL_ROWS_IN_PROGRESS) { - if (!WireFormat::ReadUInt64(&input_, &info.total_rows)) { + if (!WireFormat::ReadUInt64(input_.get(), &info.total_rows)) { return false; } } @@ -486,20 +539,20 @@ bool Client::Impl::ReceiveData() { Block block; if (REVISION >= DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES) { - if (!WireFormat::SkipString(&input_)) { + if (!WireFormat::SkipString(input_.get())) { return false; } } if (compression_ == CompressionState::Enable) { - CompressedInput compressed(&input_); + CompressedInput compressed(input_.get()); CodedInputStream coded(&compressed); if (!ReadBlock(&block, &coded)) { return false; } } else { - if (!ReadBlock(&block, &input_)) { + if (!ReadBlock(&block, input_.get())) { return false; } } @@ -522,23 +575,23 @@ bool Client::Impl::ReceiveException(bool rethrow) { do { bool has_nested = false; - if (!WireFormat::ReadFixed(&input_, ¤t->code)) { + if (!WireFormat::ReadFixed(input_.get(), ¤t->code)) { exception_received = false; break; } - if (!WireFormat::ReadString(&input_, ¤t->name)) { + if (!WireFormat::ReadString(input_.get(), ¤t->name)) { exception_received = false; break; } - if (!WireFormat::ReadString(&input_, ¤t->display_text)) { + if (!WireFormat::ReadString(input_.get(), ¤t->display_text)) { exception_received = false; break; } - if (!WireFormat::ReadString(&input_, ¤t->stack_trace)) { + if (!WireFormat::ReadString(input_.get(), ¤t->stack_trace)) { exception_received = false; break; } - if (!WireFormat::ReadFixed(&input_, &has_nested)) { + if (!WireFormat::ReadFixed(input_.get(), &has_nested)) { exception_received = false; break; } @@ -563,13 +616,13 @@ bool Client::Impl::ReceiveException(bool rethrow) { } void Client::Impl::SendCancel() { - WireFormat::WriteUInt64(&output_, ClientCodes::Cancel); - output_.Flush(); + WireFormat::WriteUInt64(output_.get(), ClientCodes::Cancel); + output_->Flush(); } void Client::Impl::SendQuery(const std::string& query) { - WireFormat::WriteUInt64(&output_, ClientCodes::Query); - WireFormat::WriteString(&output_, std::string()); + WireFormat::WriteUInt64(output_.get(), ClientCodes::Query); + WireFormat::WriteString(output_.get(), std::string()); /// Client info. if (server_info_.revision >= DBMS_MIN_REVISION_WITH_CLIENT_INFO) { @@ -582,23 +635,23 @@ void Client::Impl::SendQuery(const std::string& query) { info.client_revision = REVISION; - WireFormat::WriteFixed(&output_, info.query_kind); - WireFormat::WriteString(&output_, info.initial_user); - WireFormat::WriteString(&output_, info.initial_query_id); - WireFormat::WriteString(&output_, info.initial_address); - WireFormat::WriteFixed(&output_, info.iface_type); + WireFormat::WriteFixed(output_.get(), info.query_kind); + WireFormat::WriteString(output_.get(), info.initial_user); + WireFormat::WriteString(output_.get(), info.initial_query_id); + WireFormat::WriteString(output_.get(), info.initial_address); + WireFormat::WriteFixed(output_.get(), info.iface_type); - WireFormat::WriteString(&output_, info.os_user); - WireFormat::WriteString(&output_, info.client_hostname); - WireFormat::WriteString(&output_, info.client_name); - WireFormat::WriteUInt64(&output_, info.client_version_major); - WireFormat::WriteUInt64(&output_, info.client_version_minor); - WireFormat::WriteUInt64(&output_, info.client_revision); + WireFormat::WriteString(output_.get(), info.os_user); + WireFormat::WriteString(output_.get(), info.client_hostname); + WireFormat::WriteString(output_.get(), info.client_name); + WireFormat::WriteUInt64(output_.get(), info.client_version_major); + WireFormat::WriteUInt64(output_.get(), info.client_version_minor); + WireFormat::WriteUInt64(output_.get(), info.client_revision); if (server_info_.revision >= DBMS_MIN_REVISION_WITH_QUOTA_KEY_IN_CLIENT_INFO) - WireFormat::WriteString(&output_, info.quota_key); + WireFormat::WriteString(output_.get(), info.quota_key); if (server_info_.revision >= DBMS_MIN_REVISION_WITH_VERSION_PATCH) { - WireFormat::WriteUInt64(&output_, info.client_version_patch); + WireFormat::WriteUInt64(output_.get(), info.client_version_patch); } } @@ -606,16 +659,16 @@ void Client::Impl::SendQuery(const std::string& query) { //if (settings) // settings->serialize(*out); //else - WireFormat::WriteString(&output_, std::string()); + WireFormat::WriteString(output_.get(), std::string()); - WireFormat::WriteUInt64(&output_, Stages::Complete); - WireFormat::WriteUInt64(&output_, compression_); - WireFormat::WriteString(&output_, query); + WireFormat::WriteUInt64(output_.get(), Stages::Complete); + WireFormat::WriteUInt64(output_.get(), compression_); + WireFormat::WriteString(output_.get(), query); // Send empty block as marker of // end of data SendData(Block()); - output_.Flush(); + output_->Flush(); } @@ -641,10 +694,10 @@ void Client::Impl::WriteBlock(const Block& block, CodedOutputStream* output) { } void Client::Impl::SendData(const Block& block) { - WireFormat::WriteUInt64(&output_, ClientCodes::Data); + WireFormat::WriteUInt64(output_.get(), ClientCodes::Data); if (server_info_.revision >= DBMS_MIN_REVISION_WITH_TEMPORARY_TABLES) { - WireFormat::WriteString(&output_, std::string()); + WireFormat::WriteString(output_.get(), std::string()); } if (compression_ == CompressionState::Enable) { @@ -679,30 +732,30 @@ void Client::Impl::SendData(const Block& block) { // Original data size WriteUnaligned(p, (uint32_t)tmp.size()); - WireFormat::WriteFixed(&output_, CityHash128( + WireFormat::WriteFixed(output_.get(), CityHash128( (const char*)buf.data(), buf.size())); - WireFormat::WriteBytes(&output_, buf.data(), buf.size()); + WireFormat::WriteBytes(output_.get(), buf.data(), buf.size()); break; } } } else { - WriteBlock(block, &output_); + WriteBlock(block, output_.get()); } - output_.Flush(); + output_->Flush(); } bool Client::Impl::SendHello() { - WireFormat::WriteUInt64(&output_, ClientCodes::Hello); - WireFormat::WriteString(&output_, std::string(DBMS_NAME) + " client"); - WireFormat::WriteUInt64(&output_, DBMS_VERSION_MAJOR); - WireFormat::WriteUInt64(&output_, DBMS_VERSION_MINOR); - WireFormat::WriteUInt64(&output_, REVISION); - WireFormat::WriteString(&output_, options_.default_database); - WireFormat::WriteString(&output_, options_.user); - WireFormat::WriteString(&output_, options_.password); + WireFormat::WriteUInt64(output_.get(), ClientCodes::Hello); + WireFormat::WriteString(output_.get(), std::string(DBMS_NAME) + " client"); + WireFormat::WriteUInt64(output_.get(), DBMS_VERSION_MAJOR); + WireFormat::WriteUInt64(output_.get(), DBMS_VERSION_MINOR); + WireFormat::WriteUInt64(output_.get(), REVISION); + WireFormat::WriteString(output_.get(), options_.default_database); + WireFormat::WriteString(output_.get(), options_.user); + WireFormat::WriteString(output_.get(), options_.password); - output_.Flush(); + output_->Flush(); return true; } @@ -710,38 +763,38 @@ bool Client::Impl::SendHello() { bool Client::Impl::ReceiveHello() { uint64_t packet_type = 0; - if (!input_.ReadVarint64(&packet_type)) { + if (!input_->ReadVarint64(&packet_type)) { return false; } if (packet_type == ServerCodes::Hello) { - if (!WireFormat::ReadString(&input_, &server_info_.name)) { + if (!WireFormat::ReadString(input_.get(), &server_info_.name)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &server_info_.version_major)) { + if (!WireFormat::ReadUInt64(input_.get(), &server_info_.version_major)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &server_info_.version_minor)) { + if (!WireFormat::ReadUInt64(input_.get(), &server_info_.version_minor)) { return false; } - if (!WireFormat::ReadUInt64(&input_, &server_info_.revision)) { + if (!WireFormat::ReadUInt64(input_.get(), &server_info_.revision)) { return false; } if (server_info_.revision >= DBMS_MIN_REVISION_WITH_SERVER_TIMEZONE) { - if (!WireFormat::ReadString(&input_, &server_info_.timezone)) { + if (!WireFormat::ReadString(input_.get(), &server_info_.timezone)) { return false; } } if (server_info_.revision >= DBMS_MIN_REVISION_WITH_SERVER_DISPLAY_NAME) { - if (!WireFormat::ReadString(&input_, &server_info_.display_name)) { + if (!WireFormat::ReadString(input_.get(), &server_info_.display_name)) { return false; } } if (server_info_.revision >= DBMS_MIN_REVISION_WITH_VERSION_PATCH) { - if (!WireFormat::ReadUInt64(&input_, &server_info_.version_patch)) { + if (!WireFormat::ReadUInt64(input_.get(), &server_info_.version_patch)) { return false; } } diff --git a/clickhouse/client.h b/clickhouse/client.h index e080fb91..581a7748 100644 --- a/clickhouse/client.h +++ b/clickhouse/client.h @@ -21,6 +21,10 @@ #include #include +#if WITH_OPENSSL +typedef struct ssl_ctx_st SSL_CTX; +#endif + namespace clickhouse { struct ServerInfo { @@ -40,9 +44,9 @@ enum class CompressionMethod { }; struct ClientOptions { -#define DECLARE_FIELD(name, type, setter, default) \ - type name = default; \ - inline ClientOptions& setter(const type& value) { \ +#define DECLARE_FIELD(name, type, setter, default_value) \ + type name = default_value; \ + inline auto & setter(const type& value) { \ name = value; \ return *this; \ } @@ -91,6 +95,59 @@ struct ClientOptions { */ DECLARE_FIELD(backward_compatibility_lowcardinality_as_wrapped_column, bool, SetBakcwardCompatibilityFeatureLowCardinalityAsWrappedColumn, true); +#if 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. + */ + + /** Pre-configured SSL-context for SSL-connection. + * 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. + */ + SSL_CTX * ssl_context = nullptr; + auto & UseExternalSSLContext(SSL_CTX * new_ssl_context) { + ssl_context = new_ssl_context; + return *this; + } + + /** Means to validate the server-supplied certificate against trusted Certificate Authority (CA). + * If no CAs are configured, the server's identity can't be validated, and the Client would err. + * See https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_default_verify_paths.html + */ + /// Load deafult CA certificates from deafult locations. + DECLARE_FIELD(use_default_ca_locations, bool, UseDefaultCaLocations, true); + /// Path to the CA files to verify server certificate, may be empty. + DECLARE_FIELD(path_to_ca_files, std::vector, PathToCAFiles, {}); + /// Path to the directory with CA files used to validate server certificate, may be empty. + DECLARE_FIELD(path_to_ca_directory, std::string, PathToCADirectory, ""); + + /** Min and max protocol versions to use, set with SSL_CTX_set_min_proto_version and SSL_CTX_set_max_proto_version + * for details see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_min_proto_version.html + */ + DECLARE_FIELD(min_protocol_version, int, MinProtocolVersion, DEFAULT_VALUE); + DECLARE_FIELD(max_protocol_version, int, MaxProtocolVersion, DEFAULT_VALUE); + + /** Options to be set with SSL_CTX_set_options, + * for details see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_options.html + */ + DECLARE_FIELD(context_options, int, ContextOptions, DEFAULT_VALUE); + + /** Use SNI at ClientHello and verify that certificate is issued to the hostname we are trying to connect to + */ + DECLARE_FIELD(use_sni, bool, UseSNI, true); + + static const int DEFAULT_VALUE = -1; + }; + + // By default SSL is turned off. + DECLARE_FIELD(ssl_options, SSLOptions, SetSSLOptions, {false}); +#endif + #undef DECLARE_FIELD }; @@ -130,7 +187,7 @@ class Client { const ServerInfo& GetServerInfo() const; private: - ClientOptions options_; + const ClientOptions options_; class Impl; std::unique_ptr impl_; diff --git a/clickhouse/columns/date.h b/clickhouse/columns/date.h index 90019353..6923a4be 100644 --- a/clickhouse/columns/date.h +++ b/clickhouse/columns/date.h @@ -10,6 +10,8 @@ namespace clickhouse { /** */ class ColumnDate : public Column { public: + using ValueType = std::time_t; + ColumnDate(); /// Appends one element to the end of column. @@ -49,6 +51,8 @@ class ColumnDate : public Column { /** */ class ColumnDateTime : public Column { public: + using ValueType = std::time_t; + ColumnDateTime(); explicit ColumnDateTime(std::string timezone); @@ -92,6 +96,8 @@ class ColumnDateTime : public Column { /** */ class ColumnDateTime64 : public Column { public: + using ValueType = Int64; + explicit ColumnDateTime64(size_t precision); ColumnDateTime64(size_t precision, std::string timezone); diff --git a/clickhouse/columns/decimal.h b/clickhouse/columns/decimal.h index 8b8cd38c..c88ac37f 100644 --- a/clickhouse/columns/decimal.h +++ b/clickhouse/columns/decimal.h @@ -10,6 +10,8 @@ namespace clickhouse { */ class ColumnDecimal : public Column { public: + using ValueType = Int128; + ColumnDecimal(size_t precision, size_t scale); void Append(const Int128& value); diff --git a/clickhouse/columns/enum.h b/clickhouse/columns/enum.h index c3728158..2f1a6e4c 100644 --- a/clickhouse/columns/enum.h +++ b/clickhouse/columns/enum.h @@ -8,6 +8,8 @@ namespace clickhouse { template class ColumnEnum : public Column { public: + using ValueType = T; + ColumnEnum(TypeRef type); ColumnEnum(TypeRef type, const std::vector& data); diff --git a/clickhouse/columns/numeric.h b/clickhouse/columns/numeric.h index 04a18aa2..1bb5b985 100644 --- a/clickhouse/columns/numeric.h +++ b/clickhouse/columns/numeric.h @@ -12,6 +12,7 @@ template class ColumnVector : public Column { public: using DataType = T; + using ValueType = T; ColumnVector(); diff --git a/cmake/openssl.cmake b/cmake/openssl.cmake new file mode 100644 index 00000000..6241c037 --- /dev/null +++ b/cmake/openssl.cmake @@ -0,0 +1,9 @@ +MACRO (USE_OPENSSL) + + if (WITH_OPENSSL) + find_package(OpenSSL REQUIRED) + message("Found OpenSSL version: ${OPENSSL_VERSION} at ${OPENSSL_INCLUDE_DIR}") + add_compile_definitions(WITH_OPENSSL=1) + endif() + +ENDMACRO(USE_OPENSSL) diff --git a/tests/simple/CMakeLists.txt b/tests/simple/CMakeLists.txt index e4a71e7d..56c791dd 100644 --- a/tests/simple/CMakeLists.txt +++ b/tests/simple/CMakeLists.txt @@ -4,4 +4,5 @@ ADD_EXECUTABLE (simple-test TARGET_LINK_LIBRARIES (simple-test clickhouse-cpp-lib -) \ No newline at end of file +) + diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index 717cff82..94b29e72 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -2,6 +2,7 @@ #include #include +#include using namespace clickhouse; @@ -888,3 +889,201 @@ INSTANTIATE_TEST_CASE_P( .SetPingBeforeQuery(false) .SetCompressionMethod(CompressionMethod::LZ4) )); + +#if WITH_OPENSSL + +namespace { + +struct DateTimeValue { + explicit DateTimeValue(const time_t & v) + : value(v) + {} + + template + explicit DateTimeValue(const T & v) + : value(v) + {} + + const time_t value; +}; + +std::ostream& operator<<(std::ostream & ostr, const DateTimeValue & time) { + const auto t = std::localtime(&time.value); + char buffer[] = "2015-05-18 07:40:12\0\0"; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", t); + + return ostr << buffer; +} + +template +bool doPrintValue(const ColumnRef & c, const size_t row, std::ostream & ostr) { + if (const auto & casted_c = c->As()) { + ostr << static_cast(casted_c->At(row)); + return true; + } + return false; +} + +std::ostream & printColumnValue(const ColumnRef& c, const size_t row, std::ostream & ostr) { + const auto r = false + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + /* || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr)*/; + if (!r) + ostr << "Unable to print value of type " << c->GetType().GetName(); + + return ostr; +} + +std::ostream& operator<<(std::ostream & ostr, const Block & block) { + if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) + return ostr; + + for (size_t col = 0; col < block.GetColumnCount(); ++col) { + const auto & c = block[col]; + ostr << c->GetType().GetName() << " ["; + + for (size_t row = 0; row < block.GetRowCount(); ++row) { + printColumnValue(c, row, ostr); + if (row != block.GetRowCount() - 1) + ostr << ", "; + } + ostr << "]"; + + if (col != block.GetColumnCount() - 1) + ostr << "\n"; + } + + return ostr; +} + +} + +class ReadonlyClientTest : public testing::TestWithParam > /*queries*/> { +protected: + void SetUp() override { + client_ = std::make_unique(std::get<0>(GetParam())); + } + + void TearDown() override { + client_.reset(); + } + + std::unique_ptr client_; +}; + +TEST_P(ReadonlyClientTest, Select) { + + const auto & queries = std::get<1>(GetParam()); + for (const auto & query : queries) { + client_->Select(query, + [& query](const Block& block) { + if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) + return; + std::cout << query << " => " + << "\n\trows: " << block.GetRowCount() + << ", columns: " << block.GetColumnCount() + << ", data:\n\t" << block << std::endl; + } + ); + } +} + +#include + +const auto QUERIES = std::vector { + "SELECT version()", + "SELECT fqdn()", + "SELECT buildId()", + "SELECT uptime()", + "SELECT filesystemFree()", + "SELECT now()" +}; + +INSTANTIATE_TEST_CASE_P( + RemoteTLS, ReadonlyClientTest, + ::testing::Values(std::tuple > { + ClientOptions() + .SetHost("github.demo.trial.altinity.cloud") + .SetPort(9440) + .SetUser("demo") + .SetPassword("demo") + .SetDefaultDatabase("default") + .SetSendRetries(1) + .SetPingBeforeQuery(true) + // On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .PathToCADirectory("/etc/ssl/certs")), + QUERIES + } +)); + +INSTANTIATE_TEST_CASE_P( + Remote_GH_API_TLS, ReadonlyClientTest, + ::testing::Values(std::tuple > { + ClientOptions() + .SetHost("gh-api.clickhouse.tech") + .SetPort(9440) + .SetUser("explorer") + .SetSendRetries(1) + .SetPingBeforeQuery(true) + // On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .PathToCADirectory("/etc/ssl/certs")), + QUERIES + } +)); + +// Special test that require properly configured TLS-enabled version of CH running locally +INSTANTIATE_TEST_CASE_P( + LocalhostTLS_None, ReadonlyClientTest, + ::testing::Values(std::tuple > { + ClientOptions() + .SetHost("127.0.0.1") + .SetPort(9440) + .SetUser("default") + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .PathToCADirectory("./CA/") + .UseSNI(false)), + QUERIES + } +)); + +INSTANTIATE_TEST_CASE_P( + LocalhostTLS_LZ4, ReadonlyClientTest, + ::testing::Values(std::tuple > { + ClientOptions() + .SetHost("127.0.0.1") + .SetPort(9440) + .SetUser("default") + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::LZ4) + .SetSSLOptions(ClientOptions::SSLOptions() + .PathToCADirectory("./CA/") + .UseSNI(false)), + QUERIES + } +)); + +#endif diff --git a/ut/socket_ut.cpp b/ut/socket_ut.cpp index 7ffeb14b..4348391b 100644 --- a/ut/socket_ut.cpp +++ b/ut/socket_ut.cpp @@ -17,14 +17,14 @@ TEST(Socketcase, connecterror) { server.start(); std::this_thread::sleep_for(std::chrono::seconds(1)); try { - SocketConnect(addr); + Socket socket(addr); } catch (const std::system_error& e) { FAIL(); } std::this_thread::sleep_for(std::chrono::seconds(1)); server.stop(); try { - SocketConnect(addr); + Socket socket(addr); FAIL(); } catch (const std::system_error& e) { ASSERT_NE(EINPROGRESS,e.code().value()); From ceab158bb7e6f3e0f09144b12adb40c95d890b76 Mon Sep 17 00:00:00 2001 From: Vasily Nemkov Date: Mon, 1 Nov 2021 22:40:22 +0200 Subject: [PATCH 2/4] Fixed TLS-enabled build + cleanup --- clickhouse/CMakeLists.txt | 3 +- clickhouse/client.cpp | 1 - ut/client_ut.cpp | 64 +++++++++++++++++++-------------------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/clickhouse/CMakeLists.txt b/clickhouse/CMakeLists.txt index c84c093e..e2a9f2f4 100644 --- a/clickhouse/CMakeLists.txt +++ b/clickhouse/CMakeLists.txt @@ -1,10 +1,10 @@ SET ( clickhouse-cpp-lib-src - base/coded.cpp base/compressed.cpp base/input.cpp base/output.cpp base/platform.cpp base/socket.cpp + base/coded.cpp columns/array.cpp columns/date.cpp @@ -72,7 +72,6 @@ INSTALL(FILES query.h DESTINATION include/clickhouse/) # base INSTALL(FILES base/buffer.h DESTINATION include/clickhouse/base/) -INSTALL(FILES base/coded.h DESTINATION include/clickhouse/base/) INSTALL(FILES base/compressed.h DESTINATION include/clickhouse/base/) INSTALL(FILES base/input.h DESTINATION include/clickhouse/base/) INSTALL(FILES base/output.h DESTINATION include/clickhouse/base/) diff --git a/clickhouse/client.cpp b/clickhouse/client.cpp index 093f58df..f505c9ae 100644 --- a/clickhouse/client.cpp +++ b/clickhouse/client.cpp @@ -1,7 +1,6 @@ #include "client.h" #include "protocol.h" -#include "base/coded.h" #include "base/compressed.h" #include "base/socket.h" #include "base/wire_format.h" diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index 94b29e72..dc0a163e 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -1053,37 +1053,37 @@ INSTANTIATE_TEST_CASE_P( } )); -// Special test that require properly configured TLS-enabled version of CH running locally -INSTANTIATE_TEST_CASE_P( - LocalhostTLS_None, ReadonlyClientTest, - ::testing::Values(std::tuple > { - ClientOptions() - .SetHost("127.0.0.1") - .SetPort(9440) - .SetUser("default") - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::None) - .SetSSLOptions(ClientOptions::SSLOptions() - .PathToCADirectory("./CA/") - .UseSNI(false)), - QUERIES - } -)); - -INSTANTIATE_TEST_CASE_P( - LocalhostTLS_LZ4, ReadonlyClientTest, - ::testing::Values(std::tuple > { - ClientOptions() - .SetHost("127.0.0.1") - .SetPort(9440) - .SetUser("default") - .SetPingBeforeQuery(true) - .SetCompressionMethod(CompressionMethod::LZ4) - .SetSSLOptions(ClientOptions::SSLOptions() - .PathToCADirectory("./CA/") - .UseSNI(false)), - QUERIES - } -)); +//// Special test that require properly configured TLS-enabled version of CH running locally +//INSTANTIATE_TEST_CASE_P( +// LocalhostTLS_None, ReadonlyClientTest, +// ::testing::Values(std::tuple > { +// ClientOptions() +// .SetHost("127.0.0.1") +// .SetPort(9440) +// .SetUser("default") +// .SetPingBeforeQuery(true) +// .SetCompressionMethod(CompressionMethod::None) +// .SetSSLOptions(ClientOptions::SSLOptions() +// .PathToCADirectory("./CA/") +// .UseSNI(false)), +// QUERIES +// } +//)); + +//INSTANTIATE_TEST_CASE_P( +// LocalhostTLS_LZ4, ReadonlyClientTest, +// ::testing::Values(std::tuple > { +// ClientOptions() +// .SetHost("127.0.0.1") +// .SetPort(9440) +// .SetUser("default") +// .SetPingBeforeQuery(true) +// .SetCompressionMethod(CompressionMethod::LZ4) +// .SetSSLOptions(ClientOptions::SSLOptions() +// .PathToCADirectory("./CA/") +// .UseSNI(false)), +// QUERIES +// } +//)); #endif From 9cb9ce44adaa9de62f2aee44f9eaf5ebbcebe622 Mon Sep 17 00:00:00 2001 From: Vasily Nemkov Date: Mon, 8 Nov 2021 17:15:51 +0200 Subject: [PATCH 3/4] TLS: Fixed issues found during PR review --- .github/workflows/linux_ssl.yml | 4 +- CMakeLists.txt | 14 +- clickhouse/CMakeLists.txt | 17 +-- clickhouse/base/socket.cpp | 4 +- clickhouse/base/socket.h | 2 +- clickhouse/base/sslsocket.cpp | 95 +++++++------- clickhouse/base/sslsocket.h | 17 ++- clickhouse/client.cpp | 10 +- clickhouse/client.h | 32 +++-- cmake/openssl.cmake | 11 +- ut/CMakeLists.txt | 12 +- ut/client_ut.cpp | 198 ----------------------------- ut/connection_failed_client_ut.cpp | 28 ++++ ut/connection_failed_client_ut.h | 12 ++ ut/readonly_client_ut.cpp | 36 ++++++ ut/readonly_client_ut.h | 18 +++ ut/ssl_ut.cpp | 129 +++++++++++++++++++ ut/utils.cpp | 92 ++++++++++++++ ut/utils.h | 41 +++++- 19 files changed, 475 insertions(+), 297 deletions(-) create mode 100644 ut/connection_failed_client_ut.cpp create mode 100644 ut/connection_failed_client_ut.h create mode 100644 ut/readonly_client_ut.cpp create mode 100644 ut/readonly_client_ut.h create mode 100644 ut/ssl_ut.cpp diff --git a/.github/workflows/linux_ssl.yml b/.github/workflows/linux_ssl.yml index 5ed39c0d..a2e11d8e 100644 --- a/.github/workflows/linux_ssl.yml +++ b/.github/workflows/linux_ssl.yml @@ -12,5 +12,5 @@ jobs: uses: Enmk/clickhouse-cpp/.github/workflows/linux.yml@master with: extra_cmake_flags: -DWITH_OPENSSL=ON - extra_install: libssl1.1 libssl-dev - gtest_args: --gtest_filter="-*LocalhostTLS*" + extra_install: libssl-dev +# gtest_args: --gtest_filter="-*LocalhostTLS*" diff --git a/CMakeLists.txt b/CMakeLists.txt index 5dad9515..21baf6b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,9 +4,9 @@ INCLUDE (cmake/cpp17.cmake) INCLUDE (cmake/subdirs.cmake) INCLUDE (cmake/openssl.cmake) -OPTION(BUILD_BENCHMARK "Build benchmark" OFF) -OPTION(BUILD_TESTS "Build tests" OFF) -OPTION(WITH_OPENSSL "Use OpenSSL for TLS connections" OFF) +OPTION (BUILD_BENCHMARK "Build benchmark" OFF) +OPTION (BUILD_TESTS "Build tests" OFF) +OPTION (WITH_OPENSSL "Use OpenSSL for TLS connections" OFF) PROJECT (CLICKHOUSE-CLIENT) @@ -14,20 +14,20 @@ PROJECT (CLICKHOUSE-CLIENT) USE_OPENSSL() IF ("${CMAKE_BUILD_TYPE}" STREQUAL "") - set(CMAKE_BUILD_TYPE "Debug") + SET (CMAKE_BUILD_TYPE "Debug") ENDIF() IF (UNIX) IF (APPLE) SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -Wall -Wextra -Werror") ELSE () - SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2 -pthread -Wall -Wextra -Werror") + SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread -Wall -Wextra -Werror") ENDIF () SET (CMAKE_EXE_LINKER_FLAGS, "${CMAKE_EXE_LINKER_FLAGS} -lpthread") ENDIF () - INCLUDE_DIRECTORIES(.) - INCLUDE_DIRECTORIES(contrib) + INCLUDE_DIRECTORIES (.) + INCLUDE_DIRECTORIES (contrib) SUBDIRS ( clickhouse diff --git a/clickhouse/CMakeLists.txt b/clickhouse/CMakeLists.txt index e2a9f2f4..3bef4142 100644 --- a/clickhouse/CMakeLists.txt +++ b/clickhouse/CMakeLists.txt @@ -1,10 +1,10 @@ SET ( clickhouse-cpp-lib-src + base/coded.cpp base/compressed.cpp base/input.cpp base/output.cpp base/platform.cpp base/socket.cpp - base/coded.cpp columns/array.cpp columns/date.cpp @@ -31,9 +31,9 @@ SET ( clickhouse-cpp-lib-src query.cpp ) -if (WITH_OPENSSL) +IF (WITH_OPENSSL) LIST(APPEND clickhouse-cpp-lib-src base/sslsocket.cpp) -endif() +ENDIF () ADD_LIBRARY (clickhouse-cpp-lib SHARED ${clickhouse-cpp-lib-src}) SET_TARGET_PROPERTIES(clickhouse-cpp-lib PROPERTIES LINKER_LANGUAGE CXX) @@ -56,7 +56,7 @@ IF (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") TARGET_LINK_LIBRARIES (clickhouse-cpp-lib-static gcc_s) ENDIF () -INSTALL(TARGETS clickhouse-cpp-lib clickhouse-cpp-lib-static +INSTALL (TARGETS clickhouse-cpp-lib clickhouse-cpp-lib-static ARCHIVE DESTINATION lib LIBRARY DESTINATION lib ) @@ -71,6 +71,7 @@ INSTALL(FILES protocol.h DESTINATION include/clickhouse/) INSTALL(FILES query.h DESTINATION include/clickhouse/) # base +INSTALL(FILES base/coded.h DESTINATION include/clickhouse/base/) INSTALL(FILES base/buffer.h DESTINATION include/clickhouse/base/) INSTALL(FILES base/compressed.h DESTINATION include/clickhouse/base/) INSTALL(FILES base/input.h DESTINATION include/clickhouse/base/) @@ -104,7 +105,7 @@ INSTALL(FILES columns/uuid.h DESTINATION include/clickhouse/columns/) INSTALL(FILES types/type_parser.h DESTINATION include/clickhouse/types/) INSTALL(FILES types/types.h DESTINATION include/clickhouse/types/) -if (WITH_OPENSSL) - target_link_libraries(clickhouse-cpp-lib OpenSSL::SSL) - target_link_libraries(clickhouse-cpp-lib-static OpenSSL::SSL) -endif() +IF (WITH_OPENSSL) + TARGET_LINK_LIBRARIES (clickhouse-cpp-lib OpenSSL::SSL) + TARGET_LINK_LIBRARIES (clickhouse-cpp-lib-static OpenSSL::SSL) +ENDIF () diff --git a/clickhouse/base/socket.cpp b/clickhouse/base/socket.cpp index a61d89fd..285f98ba 100644 --- a/clickhouse/base/socket.cpp +++ b/clickhouse/base/socket.cpp @@ -131,8 +131,8 @@ SOCKET SocketConnect(const NetworkAddress& addr) { } // namespace NetworkAddress::NetworkAddress(const std::string& host, const std::string& port) - : host_(host), - info_(nullptr) + : host_(host) + , info_(nullptr) { struct addrinfo hints; memset(&hints, 0, sizeof(hints)); diff --git a/clickhouse/base/socket.h b/clickhouse/base/socket.h index b4da52fd..ac5cda5a 100644 --- a/clickhouse/base/socket.h +++ b/clickhouse/base/socket.h @@ -30,7 +30,7 @@ struct addrinfo; namespace clickhouse { -/** +/** Address of a host to establish connection to. * */ class NetworkAddress { diff --git a/clickhouse/base/sslsocket.cpp b/clickhouse/base/sslsocket.cpp index 120f861b..7c588cb6 100644 --- a/clickhouse/base/sslsocket.cpp +++ b/clickhouse/base/sslsocket.cpp @@ -7,37 +7,43 @@ #include #include -#include namespace { std::string getCertificateInfo(X509* cert) { + if (!cert) + return "No certificate"; + std::unique_ptr mem_bio(BIO_new(BIO_s_mem()), &BIO_free); X509_print(mem_bio.get(), cert); + char * data = nullptr; - size_t len = BIO_get_mem_data(mem_bio.get(), &data); + 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) { +void throwSSLError(SSL * ssl, int error, const char * /*location*/, const char * /*statement*/) { 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) { - if (auto ssl_session = SSL_get_session(ssl)) - if (auto server_certificate = SSL_SESSION_get0_peer(ssl_session)) - reason_str += "\nServer certificate: " + getCertificateInfo(server_certificate); + // 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)) { + 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; +// 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 std::runtime_error(std::string("OpenSSL error: ") + std::to_string(error) + " : " + reason_str); } @@ -64,8 +70,8 @@ SSL_CTX * prepareSSLContext(const clickhouse::SSLParams & context_params) { throw std::runtime_error("Failed to initialize SSL context"); #define HANDLE_SSL_CTX_ERROR(statement) do { \ - if (const auto ret_code = statement; !ret_code) \ - throwSSLError(nullptr, ERR_peek_error(), LOCATION, STRINGIFY(statement)); \ + if (const auto ret_code = (statement); !ret_code) \ + throwSSLError(nullptr, ERR_peek_error(), LOCATION, #statement); \ } while(false); if (context_params.use_default_ca_locations) @@ -91,38 +97,38 @@ SSL_CTX * prepareSSLContext(const clickhouse::SSLParams & context_params) { SSL_CTX_set_max_proto_version(ctx.get(), context_params.max_protocol_version)); return ctx.release(); +#undef HANDLE_SSL_CTX_ERROR } - - } -#define HANDLE_SSL_ERROR(statement) do { \ - if (const auto ret_code = statement; ret_code <= 0) \ - throwSSLError(ssl_, SSL_get_error(ssl_, ret_code), LOCATION, STRINGIFY(statement)); \ -} while(false); - namespace clickhouse { SSLContext::SSLContext(SSL_CTX & context) - : context_(&context) + : context_(&context, &SSL_CTX_free) { - SSL_CTX_up_ref(context_); + SSL_CTX_up_ref(context_.get()); } SSLContext::SSLContext(const SSLParams & context_params) - : context_(prepareSSLContext(context_params)) + : context_(prepareSSLContext(context_params), &SSL_CTX_free) { } -SSLContext::~SSLContext() { - SSL_CTX_free(context_); -} - SSL_CTX * SSLContext::getContext() { - return context_; + return context_.get(); } +// Allows caller to use returned value of `statement` if there was no error, throws exception otherwise. +#define HANDLE_SSL_ERROR(statement) [&] { \ + if (const auto ret_code = (statement); ret_code <= 0) { \ + throwSSLError(ssl_, SSL_get_error(ssl_, ret_code), LOCATION, #statement); \ + return static_cast(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_) \ @@ -130,8 +136,9 @@ SSL_CTX * SSLContext::getContext() { << std::endl */ SSLSocket::SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, SSLContext& context) - : Socket(addr), - ssl_(SSL_new(context.getContext())) + : Socket(addr) + , ssl_ptr_(SSL_new(context.getContext()), &SSL_free) + , ssl_(ssl_ptr_.get()) { if (!ssl_) throw std::runtime_error("Failed to create SSL instance"); @@ -143,20 +150,20 @@ SSLSocket::SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, S SSL_set_connect_state(ssl_); HANDLE_SSL_ERROR(SSL_connect(ssl_)); HANDLE_SSL_ERROR(SSL_set_mode(ssl_, SSL_MODE_AUTO_RETRY)); + auto peer_certificate = SSL_get_peer_certificate(ssl_); - if(const auto verify_result = SSL_get_verify_result(ssl_); verify_result != X509_V_OK) { - auto error_message = X509_verify_cert_error_string(verify_result); - auto ssl_session = SSL_get_session(ssl_); - auto cert = SSL_SESSION_get0_peer(ssl_session); + if (!peer_certificate) + throw std::runtime_error("Failed to verify SSL connection: server provided no ceritificate."); + if (const auto verify_result = SSL_get_verify_result(ssl_); 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 + "\n" + getCertificateInfo(cert)); + + std::to_string(verify_result) + + " " + error_message + + "\nServer certificate: " + getCertificateInfo(peer_certificate)); } if (ssl_params.use_SNI) { - auto ssl_session = SSL_get_session(ssl_); - auto peer_cert = SSL_SESSION_get0_peer(ssl_session); auto hostname = addr.Host(); char * out_name = nullptr; @@ -164,20 +171,16 @@ SSLSocket::SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, S if (addr) { // if hostname is actually an IP address HANDLE_SSL_ERROR(X509_check_ip( - peer_cert, + peer_certificate, ASN1_STRING_get0_data(addr.get()), ASN1_STRING_length(addr.get()), 0)); } else { - HANDLE_SSL_ERROR(X509_check_host(peer_cert, hostname.c_str(), hostname.length(), 0, &out_name)); + HANDLE_SSL_ERROR(X509_check_host(peer_certificate, hostname.c_str(), hostname.length(), 0, &out_name)); } } } -SSLSocket::~SSLSocket() { - SSL_free(ssl_); -} - std::unique_ptr SSLSocket::makeInputStream() const { return std::make_unique(ssl_); } @@ -190,8 +193,6 @@ SSLSocketInput::SSLSocketInput(SSL *ssl) : ssl_(ssl) {} -SSLSocketInput::~SSLSocketInput() = default; - size_t SSLSocketInput::DoRead(void* buf, size_t len) { size_t actually_read; HANDLE_SSL_ERROR(SSL_read_ex(ssl_, buf, len, &actually_read)); @@ -202,8 +203,6 @@ SSLSocketOutput::SSLSocketOutput(SSL *ssl) : ssl_(ssl) {} -SSLSocketOutput::~SSLSocketOutput() = default; - void SSLSocketOutput::DoWrite(const void* data, size_t len) { HANDLE_SSL_ERROR(SSL_write(ssl_, data, len)); } diff --git a/clickhouse/base/sslsocket.h b/clickhouse/base/sslsocket.h index 8a3b38f4..516a1677 100644 --- a/clickhouse/base/sslsocket.h +++ b/clickhouse/base/sslsocket.h @@ -2,6 +2,8 @@ #include "socket.h" +#include + typedef struct ssl_ctx_st SSL_CTX; typedef struct ssl_st SSL; @@ -23,7 +25,7 @@ class SSLContext public: explicit SSLContext(SSL_CTX & context); explicit SSLContext(const SSLParams & context_params); - ~SSLContext(); + ~SSLContext() = default; SSLContext(const SSLContext &) = delete; SSLContext& operator=(const SSLContext &) = delete; @@ -35,14 +37,14 @@ class SSLContext SSL_CTX * getContext(); private: - SSL_CTX * const context_; + std::unique_ptr context_; }; class SSLSocket : public Socket { public: explicit SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, SSLContext& context); SSLSocket(SSLSocket &&) = default; - ~SSLSocket(); + ~SSLSocket() = default; SSLSocket(const SSLSocket & ) = delete; SSLSocket& operator=(const SSLSocket & ) = delete; @@ -51,30 +53,33 @@ class SSLSocket : public Socket { std::unique_ptr makeOutputStream() const override; private: - SSL *ssl_; + std::unique_ptr ssl_ptr_; + SSL *ssl_; // for convinience with SSL API }; class SSLSocketInput : public InputStream { public: explicit SSLSocketInput(SSL *ssl); - ~SSLSocketInput(); + ~SSLSocketInput() = default; protected: size_t DoRead(void* buf, size_t len) override; private: + // Not owning SSL *ssl_; }; class SSLSocketOutput : public OutputStream { public: explicit SSLSocketOutput(SSL *ssl); - ~SSLSocketOutput(); + ~SSLSocketOutput() = default; protected: void DoWrite(const void* data, size_t len) override; private: + // Not owning SSL *ssl_; }; diff --git a/clickhouse/client.cpp b/clickhouse/client.cpp index f505c9ae..25b24938 100644 --- a/clickhouse/client.cpp +++ b/clickhouse/client.cpp @@ -18,7 +18,7 @@ #include #include -#if WITH_OPENSSL +#if defined(WITH_OPENSSL) #include "base/sslsocket.h" #endif @@ -65,7 +65,7 @@ std::ostream& operator<<(std::ostream& os, const ClientOptions& opt) { << " retry_timeout:" << opt.retry_timeout.count() << " compression_method:" << (opt.compression_method == CompressionMethod::LZ4 ? "LZ4" : "None"); -#if WITH_OPENSSL +#if defined(WITH_OPENSSL) if (opt.ssl_options.use_ssl) { const auto & ssl_options = opt.ssl_options; os << " SSL (" @@ -166,7 +166,7 @@ class Client::Impl { std::unique_ptr socket_; -#if WITH_OPENSSL +#if defined(WITH_OPENSSL) std::unique_ptr ssl_context_; #endif @@ -304,7 +304,7 @@ void Client::Impl::ResetConnection() { std::unique_ptr socket; const auto address = NetworkAddress(options_.host, std::to_string(options_.port)); -#if WITH_OPENSSL +#if defined(WITH_OPENSSL) // TODO: maybe do not re-create context multiple times upon reconnection - that doesn't make sense. std::unique_ptr ssl_context; if (options_.ssl_options.use_ssl) { @@ -355,7 +355,7 @@ void Client::Impl::ResetConnection() { std::swap(output, output_); std::swap(socket, socket_); -#if WITH_OPENSSL +#if defined(WITH_OPENSSL) std::swap(ssl_context_, ssl_context); #endif diff --git a/clickhouse/client.h b/clickhouse/client.h index 581a7748..58611f49 100644 --- a/clickhouse/client.h +++ b/clickhouse/client.h @@ -21,7 +21,7 @@ #include #include -#if WITH_OPENSSL +#if defined(WITH_OPENSSL) typedef struct ssl_ctx_st SSL_CTX; #endif @@ -95,7 +95,17 @@ struct ClientOptions { */ DECLARE_FIELD(backward_compatibility_lowcardinality_as_wrapped_column, bool, SetBakcwardCompatibilityFeatureLowCardinalityAsWrappedColumn, true); -#if WITH_OPENSSL + /** Set max size data to compress if compression enabled. + * + * Allows choosing tradeoff betwen RAM\CPU: + * - Lower value reduces RAM usage, but slightly increases CPU usage. + * - Higher value increases RAM usage but slightly decreases CPU usage. + * + * Default is 0, use natural implementation-defined chunk size. + */ + 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. @@ -110,7 +120,7 @@ struct ClientOptions { * other options, like path_to_ca_files, path_to_ca_directory, use_default_ca_locations, etc. */ SSL_CTX * ssl_context = nullptr; - auto & UseExternalSSLContext(SSL_CTX * new_ssl_context) { + auto & SetExternalSSLContext(SSL_CTX * new_ssl_context) { ssl_context = new_ssl_context; return *this; } @@ -120,31 +130,31 @@ struct ClientOptions { * See https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_default_verify_paths.html */ /// Load deafult CA certificates from deafult locations. - DECLARE_FIELD(use_default_ca_locations, bool, UseDefaultCaLocations, true); + DECLARE_FIELD(use_default_ca_locations, bool, SetUseDefaultCALocations, true); /// Path to the CA files to verify server certificate, may be empty. - DECLARE_FIELD(path_to_ca_files, std::vector, PathToCAFiles, {}); + DECLARE_FIELD(path_to_ca_files, std::vector, SetPathToCAFiles, {}); /// Path to the directory with CA files used to validate server certificate, may be empty. - DECLARE_FIELD(path_to_ca_directory, std::string, PathToCADirectory, ""); + DECLARE_FIELD(path_to_ca_directory, std::string, SetPathToCADirectory, ""); /** Min and max protocol versions to use, set with SSL_CTX_set_min_proto_version and SSL_CTX_set_max_proto_version * for details see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_min_proto_version.html */ - DECLARE_FIELD(min_protocol_version, int, MinProtocolVersion, DEFAULT_VALUE); - DECLARE_FIELD(max_protocol_version, int, MaxProtocolVersion, DEFAULT_VALUE); + DECLARE_FIELD(min_protocol_version, int, SetMinProtocolVersion, DEFAULT_VALUE); + DECLARE_FIELD(max_protocol_version, int, SetMaxProtocolVersion, DEFAULT_VALUE); /** Options to be set with SSL_CTX_set_options, * for details see https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_options.html */ - DECLARE_FIELD(context_options, int, ContextOptions, DEFAULT_VALUE); + 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 */ - DECLARE_FIELD(use_sni, bool, UseSNI, true); + DECLARE_FIELD(use_sni, bool, SetUseSNI, true); static const int DEFAULT_VALUE = -1; }; - // By default SSL is turned off. + // By default SSL is turned off, hence the `{false}` DECLARE_FIELD(ssl_options, SSLOptions, SetSSLOptions, {false}); #endif diff --git a/cmake/openssl.cmake b/cmake/openssl.cmake index 6241c037..05bd767e 100644 --- a/cmake/openssl.cmake +++ b/cmake/openssl.cmake @@ -1,9 +1,8 @@ MACRO (USE_OPENSSL) - if (WITH_OPENSSL) - find_package(OpenSSL REQUIRED) - message("Found OpenSSL version: ${OPENSSL_VERSION} at ${OPENSSL_INCLUDE_DIR}") - add_compile_definitions(WITH_OPENSSL=1) - endif() + IF (WITH_OPENSSL) + FIND_PACKAGE (OpenSSL REQUIRED) + ADD_COMPILE_DEFINITIONS (WITH_OPENSSL=1) + ENDIF () -ENDMACRO(USE_OPENSSL) +ENDMACRO () diff --git a/ut/CMakeLists.txt b/ut/CMakeLists.txt index 4968efe9..c87bf7ca 100644 --- a/ut/CMakeLists.txt +++ b/ut/CMakeLists.txt @@ -1,4 +1,4 @@ -ADD_EXECUTABLE (clickhouse-cpp-ut +SET ( clickhouse-cpp-ut-src main.cpp client_ut.cpp @@ -12,6 +12,16 @@ ADD_EXECUTABLE (clickhouse-cpp-ut performance_tests.cpp tcp_server.cpp utils.cpp + readonly_client_ut.cpp + connection_failed_client_ut.cpp +) + +IF (WITH_OPENSSL) + LIST (APPEND clickhouse-cpp-ut-src ssl_ut.cpp) +ENDIF () + +ADD_EXECUTABLE (clickhouse-cpp-ut + ${clickhouse-cpp-ut-src} ) TARGET_LINK_LIBRARIES (clickhouse-cpp-ut diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index dc0a163e..c6318282 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -889,201 +889,3 @@ INSTANTIATE_TEST_CASE_P( .SetPingBeforeQuery(false) .SetCompressionMethod(CompressionMethod::LZ4) )); - -#if WITH_OPENSSL - -namespace { - -struct DateTimeValue { - explicit DateTimeValue(const time_t & v) - : value(v) - {} - - template - explicit DateTimeValue(const T & v) - : value(v) - {} - - const time_t value; -}; - -std::ostream& operator<<(std::ostream & ostr, const DateTimeValue & time) { - const auto t = std::localtime(&time.value); - char buffer[] = "2015-05-18 07:40:12\0\0"; - std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", t); - - return ostr << buffer; -} - -template -bool doPrintValue(const ColumnRef & c, const size_t row, std::ostream & ostr) { - if (const auto & casted_c = c->As()) { - ostr << static_cast(casted_c->At(row)); - return true; - } - return false; -} - -std::ostream & printColumnValue(const ColumnRef& c, const size_t row, std::ostream & ostr) { - const auto r = false - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr) - /* || doPrintValue(c, row, ostr) - || doPrintValue(c, row, ostr)*/; - if (!r) - ostr << "Unable to print value of type " << c->GetType().GetName(); - - return ostr; -} - -std::ostream& operator<<(std::ostream & ostr, const Block & block) { - if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) - return ostr; - - for (size_t col = 0; col < block.GetColumnCount(); ++col) { - const auto & c = block[col]; - ostr << c->GetType().GetName() << " ["; - - for (size_t row = 0; row < block.GetRowCount(); ++row) { - printColumnValue(c, row, ostr); - if (row != block.GetRowCount() - 1) - ostr << ", "; - } - ostr << "]"; - - if (col != block.GetColumnCount() - 1) - ostr << "\n"; - } - - return ostr; -} - -} - -class ReadonlyClientTest : public testing::TestWithParam > /*queries*/> { -protected: - void SetUp() override { - client_ = std::make_unique(std::get<0>(GetParam())); - } - - void TearDown() override { - client_.reset(); - } - - std::unique_ptr client_; -}; - -TEST_P(ReadonlyClientTest, Select) { - - const auto & queries = std::get<1>(GetParam()); - for (const auto & query : queries) { - client_->Select(query, - [& query](const Block& block) { - if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) - return; - std::cout << query << " => " - << "\n\trows: " << block.GetRowCount() - << ", columns: " << block.GetColumnCount() - << ", data:\n\t" << block << std::endl; - } - ); - } -} - -#include - -const auto QUERIES = std::vector { - "SELECT version()", - "SELECT fqdn()", - "SELECT buildId()", - "SELECT uptime()", - "SELECT filesystemFree()", - "SELECT now()" -}; - -INSTANTIATE_TEST_CASE_P( - RemoteTLS, ReadonlyClientTest, - ::testing::Values(std::tuple > { - ClientOptions() - .SetHost("github.demo.trial.altinity.cloud") - .SetPort(9440) - .SetUser("demo") - .SetPassword("demo") - .SetDefaultDatabase("default") - .SetSendRetries(1) - .SetPingBeforeQuery(true) - // On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files - .SetCompressionMethod(CompressionMethod::None) - .SetSSLOptions(ClientOptions::SSLOptions() - .PathToCADirectory("/etc/ssl/certs")), - QUERIES - } -)); - -INSTANTIATE_TEST_CASE_P( - Remote_GH_API_TLS, ReadonlyClientTest, - ::testing::Values(std::tuple > { - ClientOptions() - .SetHost("gh-api.clickhouse.tech") - .SetPort(9440) - .SetUser("explorer") - .SetSendRetries(1) - .SetPingBeforeQuery(true) - // On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files - .SetCompressionMethod(CompressionMethod::None) - .SetSSLOptions(ClientOptions::SSLOptions() - .PathToCADirectory("/etc/ssl/certs")), - QUERIES - } -)); - -//// Special test that require properly configured TLS-enabled version of CH running locally -//INSTANTIATE_TEST_CASE_P( -// LocalhostTLS_None, ReadonlyClientTest, -// ::testing::Values(std::tuple > { -// ClientOptions() -// .SetHost("127.0.0.1") -// .SetPort(9440) -// .SetUser("default") -// .SetPingBeforeQuery(true) -// .SetCompressionMethod(CompressionMethod::None) -// .SetSSLOptions(ClientOptions::SSLOptions() -// .PathToCADirectory("./CA/") -// .UseSNI(false)), -// QUERIES -// } -//)); - -//INSTANTIATE_TEST_CASE_P( -// LocalhostTLS_LZ4, ReadonlyClientTest, -// ::testing::Values(std::tuple > { -// ClientOptions() -// .SetHost("127.0.0.1") -// .SetPort(9440) -// .SetUser("default") -// .SetPingBeforeQuery(true) -// .SetCompressionMethod(CompressionMethod::LZ4) -// .SetSSLOptions(ClientOptions::SSLOptions() -// .PathToCADirectory("./CA/") -// .UseSNI(false)), -// QUERIES -// } -//)); - -#endif diff --git a/ut/connection_failed_client_ut.cpp b/ut/connection_failed_client_ut.cpp new file mode 100644 index 00000000..068e4af7 --- /dev/null +++ b/ut/connection_failed_client_ut.cpp @@ -0,0 +1,28 @@ +#include "connection_failed_client_ut.h" +#include "utils.h" + +#include +#include + +#include +#include + +namespace { + using namespace clickhouse; +} + +TEST_P(ConnectionFailedClientTest, ValidateConnectionError) { + + const auto & client_options = std::get<0>(GetParam()); + const auto & exception_message = std::get<1>(GetParam()); + + std::unique_ptr client; + try { + client = std::make_unique(client_options); + 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; + } +} diff --git a/ut/connection_failed_client_ut.h b/ut/connection_failed_client_ut.h new file mode 100644 index 00000000..316b1dd5 --- /dev/null +++ b/ut/connection_failed_client_ut.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +#include + +#include +#include + +/// Verify that connection fails with some specific message. +class ConnectionFailedClientTest : public testing::TestWithParam< + std::tuple> {}; diff --git a/ut/readonly_client_ut.cpp b/ut/readonly_client_ut.cpp new file mode 100644 index 00000000..1847800a --- /dev/null +++ b/ut/readonly_client_ut.cpp @@ -0,0 +1,36 @@ +#include "readonly_client_ut.h" +#include "utils.h" + +#include +#include + +#include + +namespace { + using namespace clickhouse; +} + +void ReadonlyClientTest::SetUp() { + client_ = std::make_unique(std::get<0>(GetParam())); +} + +void ReadonlyClientTest::TearDown() { + client_.reset(); +} + +TEST_P(ReadonlyClientTest, Select) { + + const auto & queries = std::get<1>(GetParam()); + for (const auto & query : queries) { + client_->Select(query, + [& query](const Block& block) { + if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) + return; + std::cout << query << " => " + << "\n\trows: " << block.GetRowCount() + << ", columns: " << block.GetColumnCount() + << ", data:\n\t" << block << std::endl; + } + ); + } +} diff --git a/ut/readonly_client_ut.h b/ut/readonly_client_ut.h new file mode 100644 index 00000000..ebb5c6c4 --- /dev/null +++ b/ut/readonly_client_ut.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +class ReadonlyClientTest : public testing::TestWithParam< + std::tuple > /*queries*/> { +protected: + void SetUp() override; + void TearDown() override; + + std::unique_ptr client_; +}; diff --git a/ut/ssl_ut.cpp b/ut/ssl_ut.cpp new file mode 100644 index 00000000..d532b521 --- /dev/null +++ b/ut/ssl_ut.cpp @@ -0,0 +1,129 @@ +/** Collection of integration tests that validate TLS-connectivity to a CH server + */ +#include "readonly_client_ut.h" +#include "connection_failed_client_ut.h" + +#include +#include +#include + +namespace { + using namespace clickhouse; + + const auto QUERIES = std::vector { + "SELECT version()", + "SELECT fqdn()", + "SELECT buildId()", + "SELECT uptime()", + "SELECT filesystemFree()", + "SELECT now()" + }; +} + +INSTANTIATE_TEST_CASE_P( + RemoteTLS, ReadonlyClientTest, + ::testing::Values(ReadonlyClientTest::ParamType { + ClientOptions() + .SetHost("github.demo.trial.altinity.cloud") + .SetPort(9440) + .SetUser("demo") + .SetPassword("demo") + .SetDefaultDatabase("default") + .SetSendRetries(1) + .SetPingBeforeQuery(true) + // On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetPathToCADirectory("/etc/ssl/certs")), + QUERIES + } +)); + +#if defined(__linux__) +// On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files +const auto DEAFULT_CA_DIRECTORY_PATH = "/etc/ssl/certs"; +#elif defined(__APPLE__) +#elif defined(_win_) +#endif + +INSTANTIATE_TEST_CASE_P( + Remote_GH_API_TLS, ReadonlyClientTest, + ::testing::Values(ReadonlyClientTest::ParamType { + ClientOptions() + .SetHost("gh-api.clickhouse.tech") + .SetPort(9440) + .SetUser("explorer") + .SetSendRetries(1) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetPathToCADirectory(DEAFULT_CA_DIRECTORY_PATH)), + QUERIES + } +)); + +INSTANTIATE_TEST_CASE_P( + Remote_GH_API_TLS_no_CA, ConnectionFailedClientTest, + ::testing::Values(ConnectionFailedClientTest::ParamType { + ClientOptions() + .SetHost("gh-api.clickhouse.tech") + .SetPort(9440) + .SetUser("explorer") + .SetSendRetries(1) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetUseDefaultCALocations(false)), + "X509_v error: 20 unable to get local issuer certificate" + } +)); + +INSTANTIATE_TEST_CASE_P( + Remote_GH_API_TLS_wrong_TLS_version, ConnectionFailedClientTest, + ::testing::Values(ConnectionFailedClientTest::ParamType { + ClientOptions() + .SetHost("gh-api.clickhouse.tech") + .SetPort(9440) + .SetUser("explorer") + .SetSendRetries(1) + .SetPingBeforeQuery(true) + .SetCompressionMethod(CompressionMethod::None) + .SetSSLOptions(ClientOptions::SSLOptions() + .SetUseDefaultCALocations(false) + .SetMaxProtocolVersion(SSL3_VERSION)), + "no protocols available" + } +)); + +//// Special test that require properly configured TLS-enabled version of CH running locally +//INSTANTIATE_TEST_CASE_P( +// LocalhostTLS_None, ReadonlyClientTest, +// ::testing::Values(std::tuple > { +// ClientOptions() +// .SetHost("127.0.0.1") +// .SetPort(9440) +// .SetUser("default") +// .SetPingBeforeQuery(true) +// .SetCompressionMethod(CompressionMethod::None) +// .SetSSLOptions(ClientOptions::SSLOptions() +// .SetPathToCADirectory("./CA/") +// .SetUseSNI(false)), +// QUERIES +// } +//)); + +//INSTANTIATE_TEST_CASE_P( +// LocalhostTLS_LZ4, ReadonlyClientTest, +// ::testing::Values(std::tuple > { +// ClientOptions() +// .SetHost("127.0.0.1") +// .SetPort(9440) +// .SetUser("default") +// .SetPingBeforeQuery(true) +// .SetCompressionMethod(CompressionMethod::LZ4) +// .SetSSLOptions(ClientOptions::SSLOptions() +// .SetPathToCADirectory("./CA/") +// .SetUseSNI(false)), +// QUERIES +// } +//)); diff --git a/ut/utils.cpp b/ut/utils.cpp index 6a9dd984..a3f0ca21 100644 --- a/ut/utils.cpp +++ b/ut/utils.cpp @@ -1 +1,93 @@ #include "utils.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +using namespace clickhouse; +struct DateTimeValue { + explicit DateTimeValue(const time_t & v) + : value(v) + {} + + template + explicit DateTimeValue(const T & v) + : value(v) + {} + + const time_t value; +}; + +std::ostream& operator<<(std::ostream & ostr, const DateTimeValue & time) { + const auto t = std::localtime(&time.value); + char buffer[] = "2015-05-18 07:40:12\0\0"; + std::strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", t); + + return ostr << buffer; +} + +template +bool doPrintValue(const ColumnRef & c, const size_t row, std::ostream & ostr) { + if (const auto & casted_c = c->As()) { + ostr << static_cast(casted_c->At(row)); + return true; + } + return false; +} + +std::ostream & printColumnValue(const ColumnRef& c, const size_t row, std::ostream & ostr) { + const auto r = false + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr) + /* || doPrintValue(c, row, ostr) + || doPrintValue(c, row, ostr)*/; + if (!r) + ostr << "Unable to print value of type " << c->GetType().GetName(); + + return ostr; +} + +} + +std::ostream& operator<<(std::ostream & ostr, const Block & block) { + if (block.GetRowCount() == 0 || block.GetColumnCount() == 0) + return ostr; + + for (size_t col = 0; col < block.GetColumnCount(); ++col) { + const auto & c = block[col]; + ostr << c->GetType().GetName() << " ["; + + for (size_t row = 0; row < block.GetRowCount(); ++row) { + printColumnValue(c, row, ostr); + if (row != block.GetRowCount() - 1) + ostr << ", "; + } + ostr << "]"; + + if (col != block.GetColumnCount() - 1) + ostr << "\n"; + } + + return ostr; +} diff --git a/ut/utils.h b/ut/utils.h index 17cec27e..1e1a2100 100644 --- a/ut/utils.h +++ b/ut/utils.h @@ -3,9 +3,14 @@ #include #include #include +#include #include +namespace clickhouse { + class Block; +} + template struct Timer { @@ -43,7 +48,7 @@ struct Timer }; template -const char * getPrefix() { +inline const char * getPrefix() { const char * prefix = "?"; if constexpr (std::ratio_equal_v) { prefix = "n"; @@ -64,7 +69,39 @@ const char * getPrefix() { namespace std { template -ostream & operator<<(ostream & ostr, const chrono::duration & d) { +inline ostream & operator<<(ostream & ostr, const chrono::duration & d) { return ostr << d.count() << ::getPrefix

() << "s"; } } + +template +class MeasuresCollector { +public: + using Result = std::result_of_t; + explicit MeasuresCollector(MeasureFunc && measurment_func, const size_t preallocate_results = 10) + : measurment_func_(std::move(measurment_func)) + { + results_.reserve(preallocate_results); + } + + template + void Add(NameType && name) { + results_.emplace_back(name, measurment_func_()); + } + + const auto & GetResults() const { + return results_; + } + +private: + MeasureFunc measurment_func_; + std::vector> results_; +}; + +template +MeasuresCollector collect(MeasureFunc && f) +{ + return MeasuresCollector(std::forward(f)); +} + +std::ostream& operator<<(std::ostream & ostr, const clickhouse::Block & block); From 61923c1f845e4953cb281c7e5246c0188f051e90 Mon Sep 17 00:00:00 2001 From: Vasily Nemkov Date: Thu, 11 Nov 2021 10:18:09 +0200 Subject: [PATCH 4/4] Fixed non-functional issues from PR review minor renaming, comments and other grooming --- clickhouse/base/sslsocket.cpp | 38 ++++++++++--------- clickhouse/base/sslsocket.h | 3 +- clickhouse/client.h | 10 ----- ut/CMakeLists.txt | 4 +- ut/client_ut.cpp | 1 - ....cpp => connection_failed_client_test.cpp} | 2 +- ...t_ut.h => connection_failed_client_test.h} | 0 ...client_ut.cpp => readonly_client_test.cpp} | 2 +- ...nly_client_ut.h => readonly_client_test.h} | 0 ut/ssl_ut.cpp | 21 +++++----- 10 files changed, 35 insertions(+), 46 deletions(-) rename ut/{connection_failed_client_ut.cpp => connection_failed_client_test.cpp} (95%) rename ut/{connection_failed_client_ut.h => connection_failed_client_test.h} (100%) rename ut/{readonly_client_ut.cpp => readonly_client_test.cpp} (96%) rename ut/{readonly_client_ut.h => readonly_client_test.h} (100%) diff --git a/clickhouse/base/sslsocket.cpp b/clickhouse/base/sslsocket.cpp index 7c588cb6..d6301525 100644 --- a/clickhouse/base/sslsocket.cpp +++ b/clickhouse/base/sslsocket.cpp @@ -120,9 +120,9 @@ SSL_CTX * SSLContext::getContext() { } // Allows caller to use returned value of `statement` if there was no error, throws exception otherwise. -#define HANDLE_SSL_ERROR(statement) [&] { \ +#define HANDLE_SSL_ERROR(SSL_PTR, statement) [&] { \ if (const auto ret_code = (statement); ret_code <= 0) { \ - throwSSLError(ssl_, SSL_get_error(ssl_, ret_code), LOCATION, #statement); \ + throwSSLError(SSL_PTR, SSL_get_error(SSL_PTR, ret_code), LOCATION, #statement); \ return static_cast(0); \ } \ else \ @@ -137,25 +137,25 @@ SSL_CTX * SSLContext::getContext() { */ SSLSocket::SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, SSLContext& context) : Socket(addr) - , ssl_ptr_(SSL_new(context.getContext()), &SSL_free) - , ssl_(ssl_ptr_.get()) + , ssl_(SSL_new(context.getContext()), &SSL_free) { - if (!ssl_) + auto ssl = ssl_.get(); + if (!ssl) throw std::runtime_error("Failed to create SSL instance"); - HANDLE_SSL_ERROR(SSL_set_fd(ssl_, handle_)); + HANDLE_SSL_ERROR(ssl, SSL_set_fd(ssl, handle_)); if (ssl_params.use_SNI) - HANDLE_SSL_ERROR(SSL_set_tlsext_host_name(ssl_, addr.Host().c_str())); + HANDLE_SSL_ERROR(ssl, SSL_set_tlsext_host_name(ssl, addr.Host().c_str())); - SSL_set_connect_state(ssl_); - HANDLE_SSL_ERROR(SSL_connect(ssl_)); - HANDLE_SSL_ERROR(SSL_set_mode(ssl_, SSL_MODE_AUTO_RETRY)); - auto peer_certificate = SSL_get_peer_certificate(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 ceritificate."); - 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); 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) @@ -170,23 +170,23 @@ SSLSocket::SSLSocket(const NetworkAddress& addr, const SSLParams & ssl_params, S 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(X509_check_ip( + 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(X509_check_host(peer_certificate, hostname.c_str(), hostname.length(), 0, &out_name)); + HANDLE_SSL_ERROR(ssl, X509_check_host(peer_certificate, hostname.c_str(), hostname.length(), 0, &out_name)); } } } std::unique_ptr SSLSocket::makeInputStream() const { - return std::make_unique(ssl_); + return std::make_unique(ssl_.get()); } std::unique_ptr SSLSocket::makeOutputStream() const { - return std::make_unique(ssl_); + return std::make_unique(ssl_.get()); } SSLSocketInput::SSLSocketInput(SSL *ssl) @@ -195,7 +195,7 @@ SSLSocketInput::SSLSocketInput(SSL *ssl) size_t SSLSocketInput::DoRead(void* buf, size_t len) { size_t actually_read; - HANDLE_SSL_ERROR(SSL_read_ex(ssl_, buf, len, &actually_read)); + HANDLE_SSL_ERROR(ssl_, SSL_read_ex(ssl_, buf, len, &actually_read)); return actually_read; } @@ -204,7 +204,9 @@ SSLSocketOutput::SSLSocketOutput(SSL *ssl) {} void SSLSocketOutput::DoWrite(const void* data, size_t len) { - HANDLE_SSL_ERROR(SSL_write(ssl_, data, len)); + HANDLE_SSL_ERROR(ssl_, SSL_write(ssl_, data, len)); } +#undef HANDLE_SSL_ERROR + } diff --git a/clickhouse/base/sslsocket.h b/clickhouse/base/sslsocket.h index 516a1677..7e213d1b 100644 --- a/clickhouse/base/sslsocket.h +++ b/clickhouse/base/sslsocket.h @@ -53,8 +53,7 @@ class SSLSocket : public Socket { std::unique_ptr makeOutputStream() const override; private: - std::unique_ptr ssl_ptr_; - SSL *ssl_; // for convinience with SSL API + std::unique_ptr ssl_; }; class SSLSocketInput : public InputStream { diff --git a/clickhouse/client.h b/clickhouse/client.h index 58611f49..0e759319 100644 --- a/clickhouse/client.h +++ b/clickhouse/client.h @@ -95,16 +95,6 @@ struct ClientOptions { */ DECLARE_FIELD(backward_compatibility_lowcardinality_as_wrapped_column, bool, SetBakcwardCompatibilityFeatureLowCardinalityAsWrappedColumn, true); - /** Set max size data to compress if compression enabled. - * - * Allows choosing tradeoff betwen RAM\CPU: - * - Lower value reduces RAM usage, but slightly increases CPU usage. - * - Higher value increases RAM usage but slightly decreases CPU usage. - * - * Default is 0, use natural implementation-defined chunk size. - */ - 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. diff --git a/ut/CMakeLists.txt b/ut/CMakeLists.txt index c87bf7ca..9e4289d4 100644 --- a/ut/CMakeLists.txt +++ b/ut/CMakeLists.txt @@ -12,8 +12,8 @@ SET ( clickhouse-cpp-ut-src performance_tests.cpp tcp_server.cpp utils.cpp - readonly_client_ut.cpp - connection_failed_client_ut.cpp + readonly_client_test.cpp + connection_failed_client_test.cpp ) IF (WITH_OPENSSL) diff --git a/ut/client_ut.cpp b/ut/client_ut.cpp index c6318282..717cff82 100644 --- a/ut/client_ut.cpp +++ b/ut/client_ut.cpp @@ -2,7 +2,6 @@ #include #include -#include using namespace clickhouse; diff --git a/ut/connection_failed_client_ut.cpp b/ut/connection_failed_client_test.cpp similarity index 95% rename from ut/connection_failed_client_ut.cpp rename to ut/connection_failed_client_test.cpp index 068e4af7..697e9a93 100644 --- a/ut/connection_failed_client_ut.cpp +++ b/ut/connection_failed_client_test.cpp @@ -1,4 +1,4 @@ -#include "connection_failed_client_ut.h" +#include "connection_failed_client_test.h" #include "utils.h" #include diff --git a/ut/connection_failed_client_ut.h b/ut/connection_failed_client_test.h similarity index 100% rename from ut/connection_failed_client_ut.h rename to ut/connection_failed_client_test.h diff --git a/ut/readonly_client_ut.cpp b/ut/readonly_client_test.cpp similarity index 96% rename from ut/readonly_client_ut.cpp rename to ut/readonly_client_test.cpp index 1847800a..17bf7d36 100644 --- a/ut/readonly_client_ut.cpp +++ b/ut/readonly_client_test.cpp @@ -1,4 +1,4 @@ -#include "readonly_client_ut.h" +#include "readonly_client_test.h" #include "utils.h" #include diff --git a/ut/readonly_client_ut.h b/ut/readonly_client_test.h similarity index 100% rename from ut/readonly_client_ut.h rename to ut/readonly_client_test.h diff --git a/ut/ssl_ut.cpp b/ut/ssl_ut.cpp index d532b521..ef6772b8 100644 --- a/ut/ssl_ut.cpp +++ b/ut/ssl_ut.cpp @@ -1,7 +1,7 @@ /** Collection of integration tests that validate TLS-connectivity to a CH server */ -#include "readonly_client_ut.h" -#include "connection_failed_client_ut.h" +#include "readonly_client_test.h" +#include "connection_failed_client_test.h" #include #include @@ -20,6 +20,13 @@ namespace { }; } +#if defined(__linux__) +// On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files +const auto DEAFULT_CA_DIRECTORY_PATH = "/etc/ssl/certs"; +#elif defined(__APPLE__) +#elif defined(_win_) +#endif + INSTANTIATE_TEST_CASE_P( RemoteTLS, ReadonlyClientTest, ::testing::Values(ReadonlyClientTest::ParamType { @@ -31,21 +38,13 @@ INSTANTIATE_TEST_CASE_P( .SetDefaultDatabase("default") .SetSendRetries(1) .SetPingBeforeQuery(true) - // On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files .SetCompressionMethod(CompressionMethod::None) .SetSSLOptions(ClientOptions::SSLOptions() - .SetPathToCADirectory("/etc/ssl/certs")), + .SetPathToCADirectory(DEAFULT_CA_DIRECTORY_PATH)), QUERIES } )); -#if defined(__linux__) -// On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files -const auto DEAFULT_CA_DIRECTORY_PATH = "/etc/ssl/certs"; -#elif defined(__APPLE__) -#elif defined(_win_) -#endif - INSTANTIATE_TEST_CASE_P( Remote_GH_API_TLS, ReadonlyClientTest, ::testing::Values(ReadonlyClientTest::ParamType {