From 86abc9a0eaa531308d82d5e72d32b94f828302a1 Mon Sep 17 00:00:00 2001 From: yhirose Date: Fri, 7 Aug 2026 16:27:35 -0400 Subject: [PATCH] Give WebSocketClient the PemMemory client certificate constructor SSLClient has Adds ws::WebSocketClient::PemMemory and a constructor overload that installs an in-memory client certificate on the TLS context, enabling mutual TLS for wss:// connections. The certificate is silently ignored for ws:// URLs, consistent with the existing TLS-only setters such as set_ca_cert_path(). Part of the interface alignment discussed in #2531. --- README-websocket.md | 13 +++++ docs-src/pages/en/cookbook/t04-mtls.md | 20 +++++++ docs-src/pages/ja/cookbook/t04-mtls.md | 20 +++++++ httplib.h | 28 ++++++++++ test/test.cc | 74 ++++++++++++++++++++++++++ 5 files changed, 155 insertions(+) diff --git a/README-websocket.md b/README-websocket.md index 3a09082..c15fa58 100644 --- a/README-websocket.md +++ b/README-websocket.md @@ -135,6 +135,19 @@ bool is_open() const; explicit WebSocketClient(const std::string &scheme_host_port_path, const Headers &headers = {}); +// Constructor with a client certificate for mutual TLS (wss:// only, +// requires CPPHTTPLIB_OPENSSL_SUPPORT). The certificate is ignored for +// ws:// URLs. +struct PemMemory { + const char *cert_pem; + size_t cert_pem_len; + const char *key_pem; + size_t key_pem_len; + const char *private_key_password; +}; +explicit WebSocketClient(const std::string &scheme_host_port_path, + const PemMemory &pem, const Headers &headers = {}); + // Check if the URL was parsed successfully bool is_valid() const; diff --git a/docs-src/pages/en/cookbook/t04-mtls.md b/docs-src/pages/en/cookbook/t04-mtls.md index 10497ac..1a57fd7 100644 --- a/docs-src/pages/en/cookbook/t04-mtls.md +++ b/docs-src/pages/en/cookbook/t04-mtls.md @@ -57,6 +57,26 @@ auto res = cli.Get("/"); Note you're using `SSLClient` directly, not `Client`. If the private key has a password, pass it as the fifth argument. +## WebSocket clients + +`ws::WebSocketClient` has the same `PemMemory` struct, so `wss://` connections can present a client certificate too. + +```cpp +httplib::ws::WebSocketClient::PemMemory pem{}; +pem.cert_pem = client_cert.data(); +pem.cert_pem_len = client_cert.size(); +pem.key_pem = client_key.data(); +pem.key_pem_len = client_key.size(); + +httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem); + +if (ws.connect()) { + ws.send("hello"); +} +``` + +Passing `PemMemory` to a `ws://` (non-TLS) URL is silently ignored. There's no constructor that reads the cert files directly, so unlike `SSLClient` you always load the PEM into memory yourself before passing it in. + ## Read client info from a handler To see which client connected from inside a handler, use `req.peer_cert()`. Details in [T05. Access the peer certificate on the server](../t05-peer-cert). diff --git a/docs-src/pages/ja/cookbook/t04-mtls.md b/docs-src/pages/ja/cookbook/t04-mtls.md index 199acc1..ba30965 100644 --- a/docs-src/pages/ja/cookbook/t04-mtls.md +++ b/docs-src/pages/ja/cookbook/t04-mtls.md @@ -57,6 +57,26 @@ auto res = cli.Get("/"); `Client`ではなく`SSLClient`を直接使う点に注意してください。秘密鍵にパスワードがある場合は第5引数で渡せます。 +## WebSocketクライアントの場合 + +`ws::WebSocketClient`にも同じ`PemMemory`構造体があり、`wss://`接続でクライアント証明書を使えます。 + +```cpp +httplib::ws::WebSocketClient::PemMemory pem{}; +pem.cert_pem = client_cert.data(); +pem.cert_pem_len = client_cert.size(); +pem.key_pem = client_key.data(); +pem.key_pem_len = client_key.size(); + +httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem); + +if (ws.connect()) { + ws.send("hello"); +} +``` + +`ws://`(非TLS)のURLに`PemMemory`を渡した場合は黙って無視されます。ファイルパスから直接読み込むコンストラクタは用意されていないので、`SSLClient`と違いPEMをメモリ上に読み込んでから渡す必要があります。 + ## ハンドラからクライアント情報を取得する ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくは[T05. サーバー側でピア証明書を参照する](../t05-peer-cert)を参照してください。 diff --git a/httplib.h b/httplib.h index db2c5c9..231c37c 100644 --- a/httplib.h +++ b/httplib.h @@ -4300,6 +4300,16 @@ public: void set_hostname_addr_map(std::map addr_map); #ifdef CPPHTTPLIB_SSL_ENABLED + struct PemMemory { + const char *cert_pem; + size_t cert_pem_len; + const char *key_pem; + size_t key_pem_len; + const char *private_key_password; + }; + explicit WebSocketClient(const std::string &scheme_host_port_path, + const PemMemory &pem, const Headers &headers = {}); + 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); @@ -21289,6 +21299,24 @@ inline WebSocketClient::WebSocketClient( } } +#ifdef CPPHTTPLIB_SSL_ENABLED +inline WebSocketClient::WebSocketClient( + const std::string &scheme_host_port_path, const PemMemory &pem, + const Headers &headers) + : WebSocketClient(scheme_host_port_path, headers) { + // For ws:// URLs the client certificate is silently ignored, consistent + // with the TLS-only setters such as set_ca_cert_path(). + if (is_valid_ && is_ssl_ && pem.cert_pem && pem.key_pem) { + if (!tls::set_client_cert_pem(tls_ctx_, pem.cert_pem, pem.key_pem, + pem.private_key_password)) { + tls::free_context(tls_ctx_); + tls_ctx_ = nullptr; + is_valid_ = false; + } + } +} +#endif + inline WebSocketClient::~WebSocketClient() { shutdown_and_close(); #ifdef CPPHTTPLIB_SSL_ENABLED diff --git a/test/test.cc b/test/test.cc index e50b12d..689973c 100644 --- a/test/test.cc +++ b/test/test.cc @@ -21118,6 +21118,80 @@ TEST_F(WebSocketSSLDnsHostTest, VerificationDisabledAcceptsAnyName) { EXPECT_EQ("hello", msg); client.close(); } + +class WebSocketSSLPemMemoryTest : public ::testing::Test { +protected: + void SetUp() override { + server_ = httplib::detail::make_unique( + SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); + ASSERT_TRUE(server_->is_valid()); + server_->WebSocket("/ws-echo", [](const Request &, ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send(msg); + } + }); + port_ = server_->bind_to_any_port("localhost"); + server_thread_ = std::thread([this]() { server_->listen_after_bind(); }); + server_->wait_until_ready(); + } + + void TearDown() override { + server_->stop(); + if (server_thread_.joinable()) { server_thread_.join(); } + } + + std::string url() const { + return "wss://localhost:" + std::to_string(port_) + "/ws-echo"; + } + + void ConnectWithClientCert(const std::string &client_cert_file, + const std::string &client_private_key_file, + const char *private_key_password) { + std::string cert_pem, key_pem; + read_file(client_cert_file, cert_pem); + read_file(client_private_key_file, key_pem); + + ws::WebSocketClient::PemMemory pem = {cert_pem.c_str(), cert_pem.size(), + key_pem.c_str(), key_pem.size(), + private_key_password}; + ws::WebSocketClient client(url(), pem); + ASSERT_TRUE(client.is_valid()); + client.enable_server_certificate_verification(false); + + 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(); + } + + std::unique_ptr server_; + std::thread server_thread_; + int port_ = 0; +}; + +TEST_F(WebSocketSSLPemMemoryTest, ClientCertAccepted) { + ConnectWithClientCert(CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE, nullptr); +} + +// Control for the tests above: the fixture's server really does require a +// client certificate, so it is the PEM the constructor installed that decides +// the outcome. +TEST_F(WebSocketSSLPemMemoryTest, NoClientCertRejected) { + ws::WebSocketClient client(url()); + ASSERT_TRUE(client.is_valid()); + client.enable_server_certificate_verification(false); + + EXPECT_FALSE(client.connect()); +} + +TEST_F(WebSocketSSLPemMemoryTest, EncryptedClientCertAccepted) { + ConnectWithClientCert(CLIENT_ENCRYPTED_CERT_FILE, + CLIENT_ENCRYPTED_PRIVATE_KEY_FILE, + CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); +} #endif #if !defined(_WIN32)