|
@@ -3,12 +3,17 @@
|
|
|
#include <database.grpc.pb.h>
|
|
#include <database.grpc.pb.h>
|
|
|
|
|
|
|
|
#include <grpcpp/grpcpp.h>
|
|
#include <grpcpp/grpcpp.h>
|
|
|
|
|
+#include <grpcpp/security/credentials.h>
|
|
|
|
|
+#include <grpcpp/security/tls_certificate_verifier.h>
|
|
|
|
|
+#include <grpcpp/security/tls_credentials_options.h>
|
|
|
#include <spdlog/spdlog.h>
|
|
#include <spdlog/spdlog.h>
|
|
|
|
|
|
|
|
#include <atomic>
|
|
#include <atomic>
|
|
|
#include <chrono>
|
|
#include <chrono>
|
|
|
#include <cstdlib>
|
|
#include <cstdlib>
|
|
|
|
|
+#include <fstream>
|
|
|
#include <mutex>
|
|
#include <mutex>
|
|
|
|
|
+#include <sstream>
|
|
|
#include <thread>
|
|
#include <thread>
|
|
|
|
|
|
|
|
namespace smartbotic::database {
|
|
namespace smartbotic::database {
|
|
@@ -38,6 +43,16 @@ uint32_t computeBackoffMs(uint32_t baseMs, uint32_t attempt, uint32_t capMs, dou
|
|
|
if (jitter < 0.1) jitter = 0.1;
|
|
if (jitter < 0.1) jitter = 0.1;
|
|
|
return static_cast<uint32_t>(exp * jitter);
|
|
return static_cast<uint32_t>(exp * jitter);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+// v2.4 — slurp a PEM-encoded CA cert file into a string. Returns empty
|
|
|
|
|
+// string on read failure; caller logs the surrounding context.
|
|
|
|
|
+std::string readCaCertFile(const std::string& path) {
|
|
|
|
|
+ std::ifstream in(path, std::ios::binary);
|
|
|
|
|
+ if (!in.is_open()) return {};
|
|
|
|
|
+ std::ostringstream buf;
|
|
|
|
|
+ buf << in.rdbuf();
|
|
|
|
|
+ return buf.str();
|
|
|
|
|
+}
|
|
|
} // anonymous namespace
|
|
} // anonymous namespace
|
|
|
|
|
|
|
|
// ===== PIMPL Implementation =====
|
|
// ===== PIMPL Implementation =====
|
|
@@ -80,16 +95,25 @@ public:
|
|
|
channelArgs.SetMaxReceiveMessageSize(100 * 1024 * 1024);
|
|
channelArgs.SetMaxReceiveMessageSize(100 * 1024 * 1024);
|
|
|
channelArgs.SetMaxSendMessageSize(100 * 1024 * 1024);
|
|
channelArgs.SetMaxSendMessageSize(100 * 1024 * 1024);
|
|
|
|
|
|
|
|
|
|
+ std::shared_ptr<grpc::ChannelCredentials> creds = buildChannelCredentials();
|
|
|
|
|
+ if (!creds) {
|
|
|
|
|
+ spdlog::error("Database client connection failed: could not build channel credentials");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
channel_ = grpc::CreateCustomChannel(
|
|
channel_ = grpc::CreateCustomChannel(
|
|
|
config_.address,
|
|
config_.address,
|
|
|
- grpc::InsecureChannelCredentials(),
|
|
|
|
|
|
|
+ creds,
|
|
|
channelArgs
|
|
channelArgs
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
stub_ = smartbotic::databasepb::DatabaseService::NewStub(channel_);
|
|
stub_ = smartbotic::databasepb::DatabaseService::NewStub(channel_);
|
|
|
connected_ = true;
|
|
connected_ = true;
|
|
|
|
|
|
|
|
- spdlog::info("Database client connected to {}", config_.address);
|
|
|
|
|
|
|
+ const char* scheme = config_.tls_enabled ? "tls" : "plaintext";
|
|
|
|
|
+ const char* authed = config_.auth_token.empty() ? "no-auth" : "bearer-auth";
|
|
|
|
|
+ spdlog::info("Database client connected to {} ({}, {})",
|
|
|
|
|
+ config_.address, scheme, authed);
|
|
|
return true;
|
|
return true;
|
|
|
} catch (const std::exception& e) {
|
|
} catch (const std::exception& e) {
|
|
|
spdlog::error("Database client connection failed: {}", e.what());
|
|
spdlog::error("Database client connection failed: {}", e.what());
|
|
@@ -97,6 +121,70 @@ public:
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // v2.4 — build the ChannelCredentials matching Config's TLS settings.
|
|
|
|
|
+ //
|
|
|
|
|
+ // Three cases:
|
|
|
|
|
+ // 1. !tls_enabled → InsecureChannelCredentials
|
|
|
|
|
+ // (v2.3 back-compat default).
|
|
|
|
|
+ // 2. tls_enabled && !insecure_skip_verify → SslCredentials with
|
|
|
|
|
+ // pem_root_certs read from tls_ca_cert_path, or empty (= system
|
|
|
|
|
+ // trust roots) when the path is unset.
|
|
|
|
|
+ // 3. tls_enabled && insecure_skip_verify → experimental
|
|
|
|
|
+ // TlsChannelCredentialsOptions with NoOpCertificateVerifier and
|
|
|
|
|
+ // set_verify_server_certs(false). Loud WARN on every connect.
|
|
|
|
|
+ std::shared_ptr<grpc::ChannelCredentials> buildChannelCredentials() {
|
|
|
|
|
+ if (!config_.tls_enabled) {
|
|
|
|
|
+ return grpc::InsecureChannelCredentials();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (config_.tls_insecure_skip_verify) {
|
|
|
|
|
+ spdlog::warn(
|
|
|
|
|
+ "Database client: connecting with tls_insecure_skip_verify — "
|
|
|
|
|
+ "DEV ONLY. Server certificate will NOT be validated against any "
|
|
|
|
|
+ "trust root. Drop the server's self-signed cert at "
|
|
|
|
|
+ "tls_ca_cert_path for any production-shaped use.");
|
|
|
|
|
+
|
|
|
|
|
+ // Route through gRPC's experimental TLS API. This is the path
|
|
|
|
|
+ // gRPC documents for "accept any cert" — combine
|
|
|
|
|
+ // NoOpCertificateVerifier with set_verify_server_certs(false).
|
|
|
|
|
+ grpc::experimental::TlsChannelCredentialsOptions tlsOpts;
|
|
|
|
|
+ tlsOpts.set_certificate_verifier(
|
|
|
|
|
+ std::make_shared<grpc::experimental::NoOpCertificateVerifier>());
|
|
|
|
|
+ tlsOpts.set_verify_server_certs(false);
|
|
|
|
|
+ tlsOpts.set_check_call_host(false);
|
|
|
|
|
+ auto creds = grpc::experimental::TlsCredentials(tlsOpts);
|
|
|
|
|
+ if (!creds) {
|
|
|
|
|
+ // No fallback that "trusts everything" without
|
|
|
|
|
+ // experimental TLS — if the build/runtime can't produce
|
|
|
|
|
+ // experimental TlsCredentials, refuse to silently degrade
|
|
|
|
|
+ // to system-trust mode (which would reject the
|
|
|
|
|
+ // self-signed cert anyway). The operator should bake the
|
|
|
|
|
+ // server's self-signed cert into tls_ca_cert_path.
|
|
|
|
|
+ spdlog::error(
|
|
|
|
|
+ "Database client: tls_insecure_skip_verify requested but "
|
|
|
|
|
+ "experimental TlsCredentials returned null. Set "
|
|
|
|
|
+ "tls_ca_cert_path to the server's self-signed PEM "
|
|
|
|
|
+ "instead.");
|
|
|
|
|
+ return nullptr;
|
|
|
|
|
+ }
|
|
|
|
|
+ return creds;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ grpc::SslCredentialsOptions sslOpts;
|
|
|
|
|
+ if (!config_.tls_ca_cert_path.empty()) {
|
|
|
|
|
+ sslOpts.pem_root_certs = readCaCertFile(config_.tls_ca_cert_path);
|
|
|
|
|
+ if (sslOpts.pem_root_certs.empty()) {
|
|
|
|
|
+ spdlog::error(
|
|
|
|
|
+ "Database client: could not read tls_ca_cert_path '{}'",
|
|
|
|
|
+ config_.tls_ca_cert_path);
|
|
|
|
|
+ return nullptr;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // Else leave pem_root_certs empty → gRPC uses system trust roots
|
|
|
|
|
+ // (or GRPC_DEFAULT_SSL_ROOTS_FILE_PATH env var, per gRPC docs).
|
|
|
|
|
+ return grpc::SslCredentials(sslOpts);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void disconnect() {
|
|
void disconnect() {
|
|
|
connected_ = false;
|
|
connected_ = false;
|
|
|
stub_.reset();
|
|
stub_.reset();
|
|
@@ -882,6 +970,7 @@ public:
|
|
|
// Longer deadline — can iterate many rows
|
|
// Longer deadline — can iterate many rows
|
|
|
auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(10);
|
|
auto deadline = std::chrono::system_clock::now() + std::chrono::minutes(10);
|
|
|
context.set_deadline(deadline);
|
|
context.set_deadline(deadline);
|
|
|
|
|
+ attachAuth(context);
|
|
|
|
|
|
|
|
Client::TimestampMigrationResult out;
|
|
Client::TimestampMigrationResult out;
|
|
|
auto status = stub_->MigrateCollectionTimestamps(&context, request, &response);
|
|
auto status = stub_->MigrateCollectionTimestamps(&context, request, &response);
|
|
@@ -1064,6 +1153,9 @@ public:
|
|
|
std::shared_ptr<void> subscribe(const std::vector<std::string>& collections,
|
|
std::shared_ptr<void> subscribe(const std::vector<std::string>& collections,
|
|
|
Client::EventCallback callback) {
|
|
Client::EventCallback callback) {
|
|
|
auto context = std::make_shared<grpc::ClientContext>();
|
|
auto context = std::make_shared<grpc::ClientContext>();
|
|
|
|
|
+ // Subscribe is a long-running stream — no deadline — but the auth
|
|
|
|
|
+ // metadata still has to land on the initial request headers.
|
|
|
|
|
+ attachAuth(*context);
|
|
|
|
|
|
|
|
smartbotic::databasepb::SubscribeRequest request;
|
|
smartbotic::databasepb::SubscribeRequest request;
|
|
|
for (const auto& coll : collections) {
|
|
for (const auto& coll : collections) {
|
|
@@ -1403,6 +1495,7 @@ public:
|
|
|
auto timeout_ms = config_.timeoutMs + (data.size() / (1024 * 1024)) * 1000;
|
|
auto timeout_ms = config_.timeoutMs + (data.size() / (1024 * 1024)) * 1000;
|
|
|
context.set_deadline(
|
|
context.set_deadline(
|
|
|
std::chrono::system_clock::now() + std::chrono::milliseconds(timeout_ms));
|
|
std::chrono::system_clock::now() + std::chrono::milliseconds(timeout_ms));
|
|
|
|
|
+ attachAuth(context);
|
|
|
|
|
|
|
|
smartbotic::databasepb::UploadFileResponse response;
|
|
smartbotic::databasepb::UploadFileResponse response;
|
|
|
auto writer = stub_->UploadFile(&context, &response);
|
|
auto writer = stub_->UploadFile(&context, &response);
|
|
@@ -1446,6 +1539,7 @@ public:
|
|
|
grpc::ClientContext context;
|
|
grpc::ClientContext context;
|
|
|
context.set_deadline(
|
|
context.set_deadline(
|
|
|
std::chrono::system_clock::now() + std::chrono::milliseconds(config_.timeoutMs * 10));
|
|
std::chrono::system_clock::now() + std::chrono::milliseconds(config_.timeoutMs * 10));
|
|
|
|
|
+ attachAuth(context);
|
|
|
|
|
|
|
|
auto reader = stub_->DownloadFile(&context, request);
|
|
auto reader = stub_->DownloadFile(&context, request);
|
|
|
|
|
|
|
@@ -1560,10 +1654,33 @@ public:
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private:
|
|
private:
|
|
|
|
|
+ // Per-context setup: deadline + (v2.4) bearer-token auth metadata.
|
|
|
|
|
+ //
|
|
|
|
|
+ // Most RPC call sites in this library funnel through this helper —
|
|
|
|
|
+ // unary (insert, get, update, ...) plus the unary file ops that need
|
|
|
|
|
+ // the default deadline. Long-running streams (Subscribe) and the
|
|
|
|
|
+ // file-streaming RPCs (UploadFile/DownloadFile) compute their own
|
|
|
|
|
+ // deadlines and call `attachAuth()` separately so the auth metadata
|
|
|
|
|
+ // still lands on every outbound RPC. Centralising both concerns here
|
|
|
|
|
+ // means we don't need a gRPC ClientInterceptorFactoryInterface — the
|
|
|
|
|
+ // interceptor route is also offered by gRPC++ but it sits in
|
|
|
|
|
+ // grpc::experimental:: and would need to be re-validated on every
|
|
|
|
|
+ // grpc minor; the per-context style keeps Stage E independent of
|
|
|
|
|
+ // gRPC's TLS/interceptor experimental flux.
|
|
|
void setDeadline(grpc::ClientContext& context) {
|
|
void setDeadline(grpc::ClientContext& context) {
|
|
|
context.set_deadline(
|
|
context.set_deadline(
|
|
|
std::chrono::system_clock::now() + std::chrono::milliseconds(config_.timeoutMs)
|
|
std::chrono::system_clock::now() + std::chrono::milliseconds(config_.timeoutMs)
|
|
|
);
|
|
);
|
|
|
|
|
+ attachAuth(context);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // v2.4 — attach the bearer-token authorization metadata if configured.
|
|
|
|
|
+ // Use this on contexts that set their own deadline (streams,
|
|
|
|
|
+ // long-running ops) so the auth metadata still lands.
|
|
|
|
|
+ void attachAuth(grpc::ClientContext& context) {
|
|
|
|
|
+ if (!config_.auth_token.empty()) {
|
|
|
|
|
+ context.AddMetadata("authorization", "Bearer " + config_.auth_token);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
Config config_;
|
|
Config config_;
|