Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README-websocket.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -4768,6 +4768,9 @@ class WebSocketClient {
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> 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);
Expand Down Expand Up @@ -4841,6 +4844,10 @@ class WebSocketClient {
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> 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_;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down
117 changes: 117 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(ws::WebSocketClient &)> &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 "<failed>" when the connection could not be opened.
std::string websocket_authorization(ws::WebSocketClient &client) {
if (!client.connect()) { return "<failed>"; }
std::string msg;
if (!client.read(msg)) { return "<failed>"; }
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());
Expand Down Expand Up @@ -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(
Expand Down
Loading