Files
zeekay 286f257aae De-brand the client: clickhouse-cpp -> datastore-cpp
Pure rename applied on top of upstream tag v2.6.2 (8d2415d6), not HEAD.
The fork was cut at 737145d = v2.6.2-57, which still declared version 2.6.2
in version.h; renaming there would have shipped 57 unreleased commits wearing
a 2.6.2 label. Reset to the tag first so this commit changes names only.

  clickhouse/            -> datastore/            (public include root)
  namespace clickhouse   -> namespace datastore
  CLICKHOUSE_CPP_VERSION -> DATASTORE_CPP_VERSION
  clickhouse-cpp-lib/-ut -> datastore-cpp-lib/-ut (CMake, Bazel)
  CH_MAP_BOOL_TO_UINT8   -> DS_MAP_BOOL_TO_UINT8
  CLICKHOUSE_*           -> DATASTORE_*           (test env vars)
  doc links              -> docs.hanzo.ai/datastore

This client speaks the NATIVE binary protocol only - there is no HTTP path
and no X-ClickHouse-* header anywhere in it, so the header rename that the
HTTP clients required does not apply and nothing here was broken.

The one string that reaches the wire is CLIENT_NAME, sent by both SendHello
and SendQuery's ClientInfo from a single macro, and it lands in
system.query_log.client_name. Both sites still read the one macro, so they
stay consistent and the server's Hello/Query consistency check is satisfied.
Old rows keep the old name; that is a reporting discontinuity, not a
migration, since nothing keys on it.

CI is repointed at our own server: ghcr.io/hanzoai/datastore:26.2.3.2, the
image production runs. Its entrypoint honours DATASTORE_SKIP_USER_SETUP, not
the CLICKHOUSE_ spelling the compose file used, and its config lives in
/etc/hanzo-datastore-server. ci/docker-compose/config.xml is now layered in
as a config.d drop-in so the image keeps its own white-labelling. That file
and users.xml deliberately keep a <clickhouse> root: it must match the roots
of the drop-ins the image ships, and a mismatch fails silently - the server
logs "Merging bypassed" and starts with every drop-in discarded.

The remote-TLS suites used to default to the third-party host
play.clickhouse.com. They now read DATASTORE_SECURE*_HOST and skip when it is
unset rather than point at a host we do not run.

Dropped as upstream infrastructure with no meaning here: AI_POLICY.md
(upstream's policy about their codebase), .travis.yml (dead CI referencing a
third-party apt repo), cross-repo-bug-relay.yml (relays our issues into an
upstream triage repo). LICENSE keeps its copyright notice, as Apache-2.0
requires.

Verified: cmake configure + 115-target build clean; the full suite is green
against ghcr.io/hanzoai/datastore:26.2.3.2 (894 passed, 0 failed); the
pristine v2.6.2 build run against that same server passes the identical set,
and both logged the same 3914 queries under their respective client names.
2026-07-26 11:44:39 -07:00

205 lines
8.3 KiB
C++

/** Collection of integration tests that validate TLS-connectivity to a CH server
*/
#include "readonly_client_test.h"
#include "connection_failed_client_test.h"
#include "utils.h"
#include <openssl/tls1.h>
#include <openssl/ssl2.h>
#include <openssl/ssl3.h>
namespace {
using namespace datastore;
const auto QUERIES = std::vector<std::string> {
"SELECT version()",
"SELECT fqdn()",
"SELECT blockSize()",
"SELECT uptime()",
"SELECT now()"
};
}
#if defined(__linux__)
// On Ubuntu 20.04 /etc/ssl/certs is a default directory with the CA files
const auto DEFAULT_CA_DIRECTORY_PATH = "/etc/ssl/certs";
#elif defined(__APPLE__)
// On macOS we will rely on Homebrew's OpenSSL installation
const auto DEFAULT_CA_DIRECTORY_PATH = "/usr/local/etc/[email protected]/cert.pem";
#elif defined(_win_)
// DEBUG ME: mingw - was not able to make it work. Every time it ends with exception:
// "Failed to verify SSL connection, X509_v error: 20 unable to get local issuer certificate"
const auto DEFAULT_CA_DIRECTORY_PATH = "/mingw64/ssl/cert.pem";
// const auto DEFAULT_CA_DIRECTORY_PATH = "/mingw64/ssl/certs";
// const auto DEFAULT_CA_DIRECTORY_PATH = "/mingw64/ssl/certs/ca-bundle.crt";
#endif
INSTANTIATE_TEST_SUITE_P(
RemoteTLS, ReadonlyClientTest,
::testing::Values(ReadonlyClientTest::ParamType {
ClientOptions()
.SetHost( getEnvOrDefault("DATASTORE_SECURE_HOST", ""))
.SetPort( getEnvOrDefault<size_t>("DATASTORE_SECURE_PORT", "9440"))
.SetUser( getEnvOrDefault("DATASTORE_SECURE_USER", "explorer"))
.SetPassword( getEnvOrDefault("DATASTORE_SECURE_PASSWORD", ""))
.SetDefaultDatabase(getEnvOrDefault("DATASTORE_SECURE_DB", "default"))
.SetSendRetries(1)
.SetPingBeforeQuery(true)
.SetCompressionMethod(CompressionMethod::None)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetPathToCADirectory(DEFAULT_CA_DIRECTORY_PATH)),
QUERIES
}
));
const auto DatastoreExplorerConfig = ClientOptions()
.SetHost( getEnvOrDefault("DATASTORE_SECURE2_HOST", ""))
.SetPort( getEnvOrDefault<size_t>("DATASTORE_SECURE2_PORT", "9440"))
.SetUser( getEnvOrDefault("DATASTORE_SECURE2_USER", "explorer"))
.SetPassword( getEnvOrDefault("DATASTORE_SECURE2_PASSWORD", ""))
.SetDefaultDatabase(getEnvOrDefault("DATASTORE_SECURE2_DB", "default"))
.SetSendRetries(1)
.SetPingBeforeQuery(true)
.SetCompressionMethod(CompressionMethod::None);
INSTANTIATE_TEST_SUITE_P(
Remote_GH_API_TLS, ReadonlyClientTest,
::testing::Values(ReadonlyClientTest::ParamType {
ClientOptions(DatastoreExplorerConfig)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetPathToCADirectory(DEFAULT_CA_DIRECTORY_PATH)),
QUERIES
}
));
// For some reasons doesn't work on MacOS.
// Looks like `VerifyCAPath` has no effect, while parsing and setting value works.
// Also for some reason SetPathToCADirectory() + SSL_CTX_load_verify_locations() works.
#if !defined(__APPLE__)
TEST(OpenSSLConfiguration, DISABLED_ValidValues) {
// Verify that Client with valid configuration set via SetConfiguration is able to connect.
EXPECT_NO_THROW(
Client(ClientOptions(DatastoreExplorerConfig)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetConfiguration({
{"VerifyCAPath", DEFAULT_CA_DIRECTORY_PATH},
{"MinProtocol", "TLSv1.3"},
{"VerifyMode", "Peer"},
{"no_comp"} // shorthand for command with no value
})
))
);
}
#endif
TEST(OpenSSLConfiguration, InValidValues) {
if (DatastoreExplorerConfig.host.empty())
GTEST_SKIP() << "No server configured: set DATASTORE_SECURE2_HOST.";
// Verify that invalid options cause throwing exception.
std::vector<ClientOptions::SSLOptions::CommandAndValue> configurations = {
{"VerifyCAPath", std::nullopt}, // missing required value
{"VerifyCAPath"}, // missing required value, shorthand
{"MinProtocol", "NOT A STRING"}, // invalid value
{"MinProtocol", ""}, // invalid value
{"unknownCommand"}, // invalid command, shorthand
{"unknownCommand", "with unexpected value"}, // invalid command + some value
{"", std::nullopt}, // empty command with no value
{""}, // empty command with no value
{"no_comp", "unexpected value"} // value for command that doesn't require a value.
};
// Unfortunately, there is no way of checking configuration validity without
// creating a Client and pointing that client to a working CH server.
// So we mix valid and invalid configurations by providing the correct
// server config but incorrect single Command-Value pair individually.
for (const auto & cv : configurations) {
EXPECT_ANY_THROW(
Client(ClientOptions(DatastoreExplorerConfig)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetConfiguration({cv})
));
) << "On command \'" << cv.command << "\' and value: " << (cv.value ? *cv.value : "<EMPTY>");
}
}
//INSTANTIATE_TEST_SUITE_P(
// Remote_GH_API_TLS_WithStringConfig, ReadonlyClientTest,
// ::testing::Values(ReadonlyClientTest::ParamType {
// ClientOptions(DatastoreExplorerConfig)
// .SetSSLOptions(ClientOptions::SSLOptions()
// .SetConfiguration({{"", DEFAULT_CA_DIRECTORY_PATH}})
// QUERIES
// }
//));
INSTANTIATE_TEST_SUITE_P(
Remote_GH_API_TLS_no_CA, ReadonlyClientTest,
::testing::Values(ReadonlyClientTest::ParamType {
ClientOptions(DatastoreExplorerConfig)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetUseDefaultCALocations(false)
.SetSkipVerification(true)), // No CA loaded, but verification is skipped
{"SELECT 1;"}
}
));
INSTANTIATE_TEST_SUITE_P(
Remote_GH_API_TLS_no_CA, ConnectionFailedClientTest,
::testing::Values(ConnectionFailedClientTest::ParamType {
ClientOptions(DatastoreExplorerConfig)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetUseDefaultCALocations(false)),
ExpectingException{"X509_v error: 20 unable to get local issuer certificate"}
}
));
INSTANTIATE_TEST_SUITE_P(
Remote_GH_API_TLS_wrong_TLS_version, ConnectionFailedClientTest,
::testing::Values(ConnectionFailedClientTest::ParamType {
ClientOptions(DatastoreExplorerConfig)
.SetSSLOptions(ClientOptions::SSLOptions()
.SetUseDefaultCALocations(false)
.SetMaxProtocolVersion(SSL3_VERSION)),
ExpectingException{"no protocols available"}
}
));
//// Special test that require properly configured TLS-enabled version of CH running locally
//INSTANTIATE_TEST_SUITE_P(
// LocalhostTLS_None, ReadonlyClientTest,
// ::testing::Values(std::tuple<ClientOptions, std::vector<std::string> > {
// 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_SUITE_P(
// LocalhostTLS_LZ4, ReadonlyClientTest,
// ::testing::Values(std::tuple<ClientOptions, std::vector<std::string> > {
// ClientOptions()
// .SetHost("127.0.0.1")
// .SetPort(9440)
// .SetUser("default")
// .SetPingBeforeQuery(true)
// .SetCompressionMethod(CompressionMethod::LZ4)
// .SetSSLOptions(ClientOptions::SSLOptions()
// .SetPathToCADirectory("./CA/")
// .SetUseSNI(false)),
// QUERIES
// }
//));