Skip to content

Commit 86abc9a

Browse files
committed
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.
1 parent bd02a50 commit 86abc9a

5 files changed

Lines changed: 155 additions & 0 deletions

File tree

README-websocket.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ bool is_open() const;
135135
explicit WebSocketClient(const std::string &scheme_host_port_path,
136136
const Headers &headers = {});
137137
138+
// Constructor with a client certificate for mutual TLS (wss:// only,
139+
// requires CPPHTTPLIB_OPENSSL_SUPPORT). The certificate is ignored for
140+
// ws:// URLs.
141+
struct PemMemory {
142+
const char *cert_pem;
143+
size_t cert_pem_len;
144+
const char *key_pem;
145+
size_t key_pem_len;
146+
const char *private_key_password;
147+
};
148+
explicit WebSocketClient(const std::string &scheme_host_port_path,
149+
const PemMemory &pem, const Headers &headers = {});
150+
138151
// Check if the URL was parsed successfully
139152
bool is_valid() const;
140153

docs-src/pages/en/cookbook/t04-mtls.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ auto res = cli.Get("/");
5757

5858
Note you're using `SSLClient` directly, not `Client`. If the private key has a password, pass it as the fifth argument.
5959

60+
## WebSocket clients
61+
62+
`ws::WebSocketClient` has the same `PemMemory` struct, so `wss://` connections can present a client certificate too.
63+
64+
```cpp
65+
httplib::ws::WebSocketClient::PemMemory pem{};
66+
pem.cert_pem = client_cert.data();
67+
pem.cert_pem_len = client_cert.size();
68+
pem.key_pem = client_key.data();
69+
pem.key_pem_len = client_key.size();
70+
71+
httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem);
72+
73+
if (ws.connect()) {
74+
ws.send("hello");
75+
}
76+
```
77+
78+
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.
79+
6080
## Read client info from a handler
6181
6282
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).

docs-src/pages/ja/cookbook/t04-mtls.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,26 @@ auto res = cli.Get("/");
5757

5858
`Client`ではなく`SSLClient`を直接使う点に注意してください。秘密鍵にパスワードがある場合は第5引数で渡せます。
5959

60+
## WebSocketクライアントの場合
61+
62+
`ws::WebSocketClient`にも同じ`PemMemory`構造体があり、`wss://`接続でクライアント証明書を使えます。
63+
64+
```cpp
65+
httplib::ws::WebSocketClient::PemMemory pem{};
66+
pem.cert_pem = client_cert.data();
67+
pem.cert_pem_len = client_cert.size();
68+
pem.key_pem = client_key.data();
69+
pem.key_pem_len = client_key.size();
70+
71+
httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem);
72+
73+
if (ws.connect()) {
74+
ws.send("hello");
75+
}
76+
```
77+
78+
`ws://`(非TLS)のURLに`PemMemory`を渡した場合は黙って無視されます。ファイルパスから直接読み込むコンストラクタは用意されていないので、`SSLClient`と違いPEMをメモリ上に読み込んでから渡す必要があります。
79+
6080
## ハンドラからクライアント情報を取得する
6181
6282
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくは[T05. サーバー側でピア証明書を参照する](../t05-peer-cert)を参照してください。

httplib.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4300,6 +4300,16 @@ class WebSocketClient {
43004300
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
43014301

43024302
#ifdef CPPHTTPLIB_SSL_ENABLED
4303+
struct PemMemory {
4304+
const char *cert_pem;
4305+
size_t cert_pem_len;
4306+
const char *key_pem;
4307+
size_t key_pem_len;
4308+
const char *private_key_password;
4309+
};
4310+
explicit WebSocketClient(const std::string &scheme_host_port_path,
4311+
const PemMemory &pem, const Headers &headers = {});
4312+
43034313
void set_ca_cert_path(const std::string &ca_cert_file_path,
43044314
const std::string &ca_cert_dir_path = std::string());
43054315
void set_ca_cert_store(tls::ca_store_t store);
@@ -21289,6 +21299,24 @@ inline WebSocketClient::WebSocketClient(
2128921299
}
2129021300
}
2129121301

21302+
#ifdef CPPHTTPLIB_SSL_ENABLED
21303+
inline WebSocketClient::WebSocketClient(
21304+
const std::string &scheme_host_port_path, const PemMemory &pem,
21305+
const Headers &headers)
21306+
: WebSocketClient(scheme_host_port_path, headers) {
21307+
// For ws:// URLs the client certificate is silently ignored, consistent
21308+
// with the TLS-only setters such as set_ca_cert_path().
21309+
if (is_valid_ && is_ssl_ && pem.cert_pem && pem.key_pem) {
21310+
if (!tls::set_client_cert_pem(tls_ctx_, pem.cert_pem, pem.key_pem,
21311+
pem.private_key_password)) {
21312+
tls::free_context(tls_ctx_);
21313+
tls_ctx_ = nullptr;
21314+
is_valid_ = false;
21315+
}
21316+
}
21317+
}
21318+
#endif
21319+
2129221320
inline WebSocketClient::~WebSocketClient() {
2129321321
shutdown_and_close();
2129421322
#ifdef CPPHTTPLIB_SSL_ENABLED

test/test.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21118,6 +21118,80 @@ TEST_F(WebSocketSSLDnsHostTest, VerificationDisabledAcceptsAnyName) {
2111821118
EXPECT_EQ("hello", msg);
2111921119
client.close();
2112021120
}
21121+
21122+
class WebSocketSSLPemMemoryTest : public ::testing::Test {
21123+
protected:
21124+
void SetUp() override {
21125+
server_ = httplib::detail::make_unique<SSLServer>(
21126+
SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
21127+
ASSERT_TRUE(server_->is_valid());
21128+
server_->WebSocket("/ws-echo", [](const Request &, ws::WebSocket &ws) {
21129+
std::string msg;
21130+
while (ws.read(msg)) {
21131+
ws.send(msg);
21132+
}
21133+
});
21134+
port_ = server_->bind_to_any_port("localhost");
21135+
server_thread_ = std::thread([this]() { server_->listen_after_bind(); });
21136+
server_->wait_until_ready();
21137+
}
21138+
21139+
void TearDown() override {
21140+
server_->stop();
21141+
if (server_thread_.joinable()) { server_thread_.join(); }
21142+
}
21143+
21144+
std::string url() const {
21145+
return "wss://localhost:" + std::to_string(port_) + "/ws-echo";
21146+
}
21147+
21148+
void ConnectWithClientCert(const std::string &client_cert_file,
21149+
const std::string &client_private_key_file,
21150+
const char *private_key_password) {
21151+
std::string cert_pem, key_pem;
21152+
read_file(client_cert_file, cert_pem);
21153+
read_file(client_private_key_file, key_pem);
21154+
21155+
ws::WebSocketClient::PemMemory pem = {cert_pem.c_str(), cert_pem.size(),
21156+
key_pem.c_str(), key_pem.size(),
21157+
private_key_password};
21158+
ws::WebSocketClient client(url(), pem);
21159+
ASSERT_TRUE(client.is_valid());
21160+
client.enable_server_certificate_verification(false);
21161+
21162+
ASSERT_TRUE(client.connect());
21163+
ASSERT_TRUE(client.send("hello"));
21164+
std::string msg;
21165+
EXPECT_EQ(ws::Text, client.read(msg));
21166+
EXPECT_EQ("hello", msg);
21167+
client.close();
21168+
}
21169+
21170+
std::unique_ptr<SSLServer> server_;
21171+
std::thread server_thread_;
21172+
int port_ = 0;
21173+
};
21174+
21175+
TEST_F(WebSocketSSLPemMemoryTest, ClientCertAccepted) {
21176+
ConnectWithClientCert(CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE, nullptr);
21177+
}
21178+
21179+
// Control for the tests above: the fixture's server really does require a
21180+
// client certificate, so it is the PEM the constructor installed that decides
21181+
// the outcome.
21182+
TEST_F(WebSocketSSLPemMemoryTest, NoClientCertRejected) {
21183+
ws::WebSocketClient client(url());
21184+
ASSERT_TRUE(client.is_valid());
21185+
client.enable_server_certificate_verification(false);
21186+
21187+
EXPECT_FALSE(client.connect());
21188+
}
21189+
21190+
TEST_F(WebSocketSSLPemMemoryTest, EncryptedClientCertAccepted) {
21191+
ConnectWithClientCert(CLIENT_ENCRYPTED_CERT_FILE,
21192+
CLIENT_ENCRYPTED_PRIVATE_KEY_FILE,
21193+
CLIENT_ENCRYPTED_PRIVATE_KEY_PASS);
21194+
}
2112121195
#endif
2112221196

2112321197
#if !defined(_WIN32)

0 commit comments

Comments
 (0)