From d2ef193b9c1327b2404a171973b8871c46e22cec Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 7 Aug 2026 07:01:33 -0400 Subject: [PATCH] Let WebSocketClient take a CA directory the way ClientImpl does WebSocketClient::set_ca_cert_path took a single path and create_stream() hardcoded an empty directory when calling detail::load_client_ca_config, while ClientImpl has always accepted (ca_cert_file_path, ca_cert_dir_path = ""). Give WebSocketClient the same signature and store the directory, so both clients configure CA loading identically. The one-argument form is unchanged for callers. Also note at both call sites why the "load the CA config once" guard differs: SSLClient needs call_once because one client serves concurrent requests, and WebSocketClient does not because connect() is not safe to call concurrently anyway. --- README-websocket.md | 7 ++++++- httplib.h | 22 ++++++++++++++++------ test/test.cc | 22 ++++++++++++++++++++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/README-websocket.md b/README-websocket.md index 7517048..3a09082 100644 --- a/README-websocket.md +++ b/README-websocket.md @@ -155,9 +155,14 @@ bool is_open() const; // Timeouts void set_read_timeout(time_t sec, time_t usec = 0); void set_write_timeout(time_t sec, time_t usec = 0); +template +void set_read_timeout(const std::chrono::duration &duration); +template +void set_write_timeout(const std::chrono::duration &duration); // SSL configuration (wss:// only, requires CPPHTTPLIB_OPENSSL_SUPPORT) -void set_ca_cert_path(const std::string &path); +void set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(tls::ca_store_t store); void enable_server_certificate_verification(bool enabled); ``` diff --git a/httplib.h b/httplib.h index 12a58b9..990d62c 100644 --- a/httplib.h +++ b/httplib.h @@ -4302,7 +4302,8 @@ public: void set_hostname_addr_map(std::map addr_map); #ifdef CPPHTTPLIB_SSL_ENABLED - void set_ca_cert_path(const std::string &path); + void set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(tls::ca_store_t store); void load_ca_cert_store(const char *ca_cert, std::size_t size); void enable_server_certificate_verification(bool enabled); @@ -4346,6 +4347,7 @@ private: tls::ctx_t tls_ctx_ = nullptr; tls::session_t tls_session_ = nullptr; std::string ca_cert_file_path_; + std::string ca_cert_dir_path_; bool custom_ca_loaded_ = false; bool certs_loaded_ = false; SystemCAMode system_ca_mode_ = SystemCAMode::Auto; @@ -17077,6 +17079,8 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert, inline bool SSLClient::load_certs() { auto ret = true; + // call_once rather than the plain flag WebSocketClient::create_stream() uses: + // one client is shared across concurrent requests here. std::call_once(initialize_cert_, [&]() { std::lock_guard guard(ctx_mutex_); @@ -21352,11 +21356,14 @@ inline void WebSocketClient::shutdown_and_close() { inline bool WebSocketClient::create_stream(std::unique_ptr &strm) { #ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl_) { + // A plain flag rather than SSLClient::load_certs()'s call_once: connect() + // is not safe to call concurrently on one client to begin with, since + // nothing else here is guarded either. if (server_certificate_verification_ && !certs_loaded_) { uint64_t backend_error = 0; - detail::load_client_ca_config(tls_ctx_, ca_cert_file_path_, std::string(), - custom_ca_loaded_, system_ca_mode_, - backend_error); + detail::load_client_ca_config(tls_ctx_, ca_cert_file_path_, + ca_cert_dir_path_, custom_ca_loaded_, + system_ca_mode_, backend_error); certs_loaded_ = true; } @@ -21518,8 +21525,11 @@ inline void WebSocketClient::set_hostname_addr_map( #ifdef CPPHTTPLIB_SSL_ENABLED -inline void WebSocketClient::set_ca_cert_path(const std::string &path) { - ca_cert_file_path_ = path; +inline void +WebSocketClient::set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path) { + ca_cert_file_path_ = ca_cert_file_path; + ca_cert_dir_path_ = ca_cert_dir_path; } inline void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) { diff --git a/test/test.cc b/test/test.cc index fa988ff..4db19ec 100644 --- a/test/test.cc +++ b/test/test.cc @@ -20958,6 +20958,28 @@ TEST_F(WebSocketSSLCATest, WrongCustomCaFailsVerification) { ASSERT_FALSE(client.connect()); } +// The same CA as a file path rather than PEM in memory +TEST_F(WebSocketSSLCATest, SetCaCertPathVerifiesServer) { + ws::WebSocketClient client(url()); + client.set_ca_cert_path(SERVER_CERT2_FILE); + + ASSERT_TRUE(client.connect()); + ASSERT_TRUE(client.send("hello")); + std::string msg; + EXPECT_EQ(ws::Text, client.read(msg)); + EXPECT_EQ("hello", msg); + client.close(); +} + +// ...and a CA file that does not cover the server still fails, so it is the +// path above that decides the outcome +TEST_F(WebSocketSSLCATest, WrongCaCertPathFailsVerification) { + ws::WebSocketClient client(url()); + client.set_ca_cert_path(CLIENT_CA_CERT_FILE); + + ASSERT_FALSE(client.connect()); +} + // Regression test: reconnecting with a native custom CA store used to reuse // a store handle the previous TLS context had already freed (use-after-free // under the OpenSSL backend). The context now lives as long as the client.