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(