From a0ab09fc0aa1df97c9cba1c9ebae4b7af734058b Mon Sep 17 00:00:00 2001 From: "Kim, Hyuk" Date: Thu, 29 Oct 2026 05:15:17 +0900 Subject: [PATCH] Add set_basic_auth and set_bearer_token_auth to WebSocketClient (#2652) ClientImpl and SSLClient let callers set origin credentials with set_basic_auth()/set_bearer_token_auth(), but WebSocketClient could only take a fixed Authorization header through its constructor. Add both setters: each connect() adds the Authorization header to the upgrade request from the current credentials (Basic over Bearer), unless the constructor headers already carry one, and never to the proxy's CONNECT request. --- README-websocket.md | 20 ++++++++ httplib.h | 20 ++++++++ test/test.cc | 117 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+) diff --git a/README-websocket.md b/README-websocket.md index b787118f..621237e9 100644 --- a/README-websocket.md +++ b/README-websocket.md @@ -200,6 +200,10 @@ void close(CloseStatus status = CloseStatus::Normal, const std::string &reason = ""); bool is_open() const; +// Authentication - the same setters Client has (Basic wins over Bearer). +void set_basic_auth(const std::string &username, const std::string &password); +void set_bearer_token_auth(const std::string &token); + // Proxy - the same setters Client has. connect() opens a CONNECT tunnel // through the proxy for ws:// as well as wss:// (RFC 6455 4.1); the proxy // credentials are sent only on the CONNECT request, never through the tunnel. @@ -404,6 +408,22 @@ svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &r The server accepts a handshake from any `Origin`. A browser sends its cookies with a handshake another site starts, so if cookies authenticate the connection, also check `req.get_header_value("Origin")` against your own origins in one of these handlers (cross-site WebSocket hijacking). +### Authentication + +```cpp +httplib::ws::WebSocketClient ws("wss://example.com/ws"); +ws.set_bearer_token_auth("token123"); // Authorization: Bearer token123 +// or +ws.set_basic_auth("user", "pass"); // Authorization: Basic dXNlcjpwYXNz + +if (ws.connect()) { /* ... */ } +``` + +The credentials are read on every `connect()`, so a new token takes effect on +the next reconnect. An `Authorization` header passed to the constructor wins +over them. Through a proxy they are sent only inside the tunnel, never on +`CONNECT`. + ### Custom Headers and Timeouts ```cpp diff --git a/httplib.h b/httplib.h index d223fd29..26c455b0 100644 --- a/httplib.h +++ b/httplib.h @@ -4768,6 +4768,9 @@ public: void set_tunnel_hostname_addr_map(std::map addr_map); + void set_basic_auth(const std::string &username, const std::string &password); + void set_bearer_token_auth(const std::string &token); + void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, const std::string &password); @@ -4841,6 +4844,10 @@ private: // Hostname to CONNECT target map, used only through a proxy. std::map tunnel_addr_map_; + std::string basic_auth_username_; + std::string basic_auth_password_; + std::string bearer_token_auth_token_; + std::string proxy_host_; int proxy_port_ = -1; std::string proxy_basic_auth_username_; @@ -23990,6 +23997,9 @@ inline void WebSocketClient::prepare_default_headers(Request &req) { } detail::add_default_user_agent_header(req); + detail::add_authorization_header(req, basic_auth_username_, + basic_auth_password_, + bearer_token_auth_token_, false); } inline Result WebSocketClient::connect() { @@ -24212,6 +24222,16 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res, return true; } +inline void WebSocketClient::set_basic_auth(const std::string &username, + const std::string &password) { + basic_auth_username_ = username; + basic_auth_password_ = password; +} + +inline void WebSocketClient::set_bearer_token_auth(const std::string &token) { + bearer_token_auth_token_ = token; +} + inline void WebSocketClient::set_proxy(const std::string &host, int port) { proxy_host_ = host; proxy_port_ = port; diff --git a/test/test.cc b/test/test.cc index a1a7e262..fd51d76f 100644 --- a/test/test.cc +++ b/test/test.cc @@ -32199,8 +32199,106 @@ void WebSocketCredentialsStayWithTheirHop( << "origin credentials must not be sent to the proxy"; } +// Like WebSocketCredentialsStayWithTheirHop(), but with the setters. +void WebSocketSetterCredentialsStayWithTheirHop( + const std::function &set_credentials, + const std::string &proxy_authz, const std::string &origin_authz) { + ScopedWebSocketOrigin origin; + ScopedConnectProxy proxy( + origin.port(), require_proxy_auth(proxy_authz, "Basic realm=\"proxy\"")); + ASSERT_NE(0, proxy.port()); + + ws::WebSocketClient client(origin.url()); + client.set_proxy("127.0.0.1", proxy.port()); + set_credentials(client); + auto res = client.connect(); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + + std::string msg; + ASSERT_TRUE(client.read(msg)); + EXPECT_EQ("authorization:" + origin_authz + "|proxy-authorization:", msg); + client.close(); + + EXPECT_EQ(1, proxy.connect_hits()); + EXPECT_EQ(std::string::npos, + proxy.connect_request().find("\r\nAuthorization: ")) + << "origin credentials must not be sent to the proxy"; +} + } // namespace proxy_tunnel_test +namespace { + +// Connects `client` and returns the Authorization header its upgrade +// request carried, or "" when the connection could not be opened. +std::string websocket_authorization(ws::WebSocketClient &client) { + if (!client.connect()) { return ""; } + std::string msg; + if (!client.read(msg)) { return ""; } + client.close(); + auto prefix = std::string("authorization:"); + auto end = msg.find("|proxy-authorization:"); + return msg.substr(prefix.size(), end - prefix.size()); +} + +} // namespace + +TEST(WebSocketClientAuthTest, BasicAuth) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + ws::WebSocketClient client(origin.url()); + client.set_basic_auth("user", "pass"); + EXPECT_EQ("Basic " + detail::base64_encode("user:pass"), + websocket_authorization(client)); +} + +TEST(WebSocketClientAuthTest, BasicAuthWithEmptyPasswordIsSent) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + ws::WebSocketClient client(origin.url()); + client.set_basic_auth("user", ""); + EXPECT_EQ("Basic " + detail::base64_encode("user:"), + websocket_authorization(client)); +} + +TEST(WebSocketClientAuthTest, BearerTokenAuth) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + ws::WebSocketClient client(origin.url()); + client.set_bearer_token_auth("token123"); + EXPECT_EQ("Bearer token123", websocket_authorization(client)); +} + +TEST(WebSocketClientAuthTest, BasicAuthWinsOverBearerToken) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + ws::WebSocketClient client(origin.url()); + client.set_bearer_token_auth("token123"); + client.set_basic_auth("user", "pass"); + EXPECT_EQ("Basic " + detail::base64_encode("user:pass"), + websocket_authorization(client)); +} + +TEST(WebSocketClientAuthTest, ConstructorHeaderWinsOverSetter) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + ws::WebSocketClient client(origin.url(), + {{"Authorization", "Bearer from-header"}}); + client.set_bearer_token_auth("from-setter"); + client.set_basic_auth("user", "pass"); + EXPECT_EQ("Bearer from-header", websocket_authorization(client)); +} + +TEST(WebSocketClientAuthTest, CredentialsApplyOnReconnect) { + proxy_tunnel_test::ScopedWebSocketOrigin origin; + ws::WebSocketClient client(origin.url()); + EXPECT_EQ("", websocket_authorization(client)); + + client.set_bearer_token_auth("first"); + EXPECT_EQ("Bearer first", websocket_authorization(client)); + + client.set_bearer_token_auth("second"); + EXPECT_EQ("Bearer second", websocket_authorization(client)); + + client.set_bearer_token_auth(""); + EXPECT_EQ("", websocket_authorization(client)); +} + TEST(WebSocketProxyTest, TunnelsThroughConnect) { proxy_tunnel_test::ScopedWebSocketOrigin origin; proxy_tunnel_test::ScopedConnectProxy proxy(origin.port()); @@ -32243,6 +32341,25 @@ TEST(WebSocketProxyTest, BearerCredentialsStayWithTheirHop) { "Bearer proxy-token", "Bearer origin-token"); } +TEST(WebSocketProxyTest, BasicAuthSetterStaysWithItsHop) { + proxy_tunnel_test::WebSocketSetterCredentialsStayWithTheirHop( + [](ws::WebSocketClient &cli) { + cli.set_proxy_basic_auth("proxy-user", "proxy-pass"); + cli.set_basic_auth("origin-user", "origin-pass"); + }, + "Basic " + detail::base64_encode("proxy-user:proxy-pass"), + "Basic " + detail::base64_encode("origin-user:origin-pass")); +} + +TEST(WebSocketProxyTest, BearerTokenAuthSetterStaysWithItsHop) { + proxy_tunnel_test::WebSocketSetterCredentialsStayWithTheirHop( + [](ws::WebSocketClient &cli) { + cli.set_proxy_bearer_token_auth("proxy-token"); + cli.set_bearer_token_auth("origin-token"); + }, + "Bearer proxy-token", "Bearer origin-token"); +} + TEST(WebSocketProxyTest, BasicCredentialsWithEmptyPasswordAreSent) { proxy_tunnel_test::ScopedWebSocketOrigin origin; proxy_tunnel_test::ScopedConnectProxy proxy(