Source
stdlib/websocket/websocket.hpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
#pragma once5
/**6
* @file websocket.hpp7
* @brief cheatah `websocket` — a from-scratch, low-latency WebSocket CLIENT8
* (RFC 6455) over the cheatah `tls` 1.3 client and `socket`. `import9
* websocket` to use it. No external libraries.10
*11
* Built for SPEED. The receive path is the hot path and is allocation-quiet:12
* - one read buffer per session, REUSED across every frame (no per-frame heap);13
* - server-to-client frames are unmasked by the protocol (RFC 6455 §5.1), so14
* recv does ZERO unmasking work — it slices the payload straight out of the15
* buffer;16
* - frame headers are parsed in place (no header object is materialized);17
* - the TLS layer is drained in large chunks, so many frames are decoded per18
* underlying read/decrypt.19
* The send path masks (clients MUST, §5.3) with a 64-bit-word XOR (8 bytes per20
* step), but sends are rare (subscribe/control) so they are off the hot path.21
*22
* The cheatah-facing API is the owning `websocket::Client` guard, created by23
* `websocket.open(...)` / `websocket.open_url("wss://...")`: it sends a close frame and tears24
* down the TLS session + socket automatically when it goes out of scope, so a cheatah program25
* cannot leak the heap Session. wss:// only (WebSocket over TLS) — the transport is always26
* encrypted, like the rest of cheatah's net stack. The flat handle-based calls (an integer27
* session id) live in websocket_lowlevel.hpp (C++ only).28
*29
* Threading: a session is single-owner; do not call recv and send for the same30
* session from two threads at once. Separate sessions are independent.31
*/33
#include <string>35
namespace cheatah::websocket {37
// The low-level, handle-based API (connect / connect_url / send_text / recv / close / shutdown,38
// keyed by an integer session id backed by a heap Session) is C++-only and lives in39
// websocket_lowlevel.hpp. It is intentionally NOT part of this cheatah-facing header: a cheatah40
// program cannot reach it, so it cannot leak the heap Session — it uses the owning41
// `websocket::Client` guard + `websocket.open()`/`open_url()` below, which free the Session42
// automatically at scope exit. `websocket::Client` is implemented on top of that low-level API.44
// ---- owning RAII client (the `with`-friendly, leak-proof API) ----46
/**47
* @brief An owning WebSocket client — closes the connection (frame + TLS + socket) on destruction.48
*49
* The RAII counterpart to the handle-based calls above. A `Client` owns one session; when it is50
* destroyed (scope exit out of a `with` body, including via exception) or explicitly close()d, the51
* close frame is sent and the TLS session and TCP socket are torn down, so52
* `with websocket.open_url(url) as ws { … }` cannot leak the session, its `tls` session, or its53
* fd. Move-only: the copy operations are deleted and a moved-from `Client` is left closed.54
*/55
class Client {56
public:57
/**58
* Construct a closed client (owns nothing).59
* @complexity O(1).60
* @alloc none.61
* @test CheatahWebSocket.ClientDefaultIsClosed62
*/63
Client() = default;64
/**65
* Adopt an existing session handle (e.g. from connect()); the `Client` now owns it.66
* @param session a session handle to take ownership of (0 for a closed client).67
* @complexity O(1).68
* @alloc none.69
* @systest WebSocketSys.ClientGuardRoundTrip70
*/71
explicit Client(long long session) : session_(session) {}72
Client(const Client&) = delete;73
Client& operator=(const Client&) = delete;74
/**75
* Move-construct, taking over @p other's session (the moved-from `Client` becomes closed).76
* @param other the client to move from.77
* @complexity O(1).78
* @alloc none.79
* @systest WebSocketSys.ClientGuardRoundTrip80
*/81
Client(Client&& other) noexcept : session_(other.session_) { other.session_ = 0; }82
/**83
* Move-assign, closing this session first, then taking over @p other's (which becomes closed).84
* @param other the client to move from.85
* @return reference to this client.86
* @complexity O(1).87
* @alloc none.88
* @systest WebSocketSys.ClientGuardRoundTrip89
* @systest WebSocketSys.ClientOpenAndMoveAssignClosesOpen90
*/91
Client& operator=(Client&& other) noexcept;92
/**93
* Close the connection if still open (close frame + TLS + socket teardown).94
* @complexity one TLS write + teardown.95
* @alloc a small close frame (when still open).96
* @test CheatahWebSocket.ClientDefaultIsClosed97
* @systest WebSocketSys.ClientOpenAndMoveAssignClosesOpen98
*/99
~Client();101
/**102
* Is a connection open?103
* @return true iff this owns an open session.104
* @complexity O(1).105
* @alloc none.106
* @test CheatahWebSocket.ClientDefaultIsClosed107
*/108
bool is_open() const { return session_ != 0; }109
/**110
* The raw session handle (for the low-level calls).111
* @return the owned handle, or 0 when closed.112
* @complexity O(1).113
* @alloc none.114
* @systest WebSocketSys.ClientGuardRoundTrip115
*/116
long long id() const { return session_; }117
/**118
* Send one application TEXT message as a single masked frame (see the free send_text()).119
* @param message the UTF-8 payload.120
* @return the number of payload bytes sent.121
* @throws std::runtime_error on a transport error.122
* @complexity O(message length).123
* @alloc one frame buffer sized to the message.124
* @concurrency a session is single-owner — do not send and recv on one session from125
* two threads at once (see the file-level threading note).126
* @systest WebSocketSys.ClientGuardRoundTrip127
*/128
long long send_text(const std::string& message);129
/**130
* Receive the next application message (see the free recv()).131
* @return the message payload; "" once the peer has closed.132
* @throws std::runtime_error on a transport/protocol error.133
* @complexity O(message length).134
* @alloc the returned payload.135
* @concurrency blocks until a full message arrives; a session has ONE reader —136
* shutdown() is the cross-thread wake-up.137
* @systest WebSocketSys.ClientGuardRoundTrip138
*/139
std::string recv();140
/**141
* Wake a reader blocked in recv() WITHOUT freeing the session (see the free shutdown()).142
* @return 0 on success, -1 on error.143
* @complexity O(1) + one syscall.144
* @alloc none.145
* @concurrency safe to call from another thread while the owner's recv() blocks —146
* that wake-up is its purpose; then join the reader before close().147
* @systest WebSocketSys.ClientGuardRoundTrip148
*/149
long long shutdown();150
/**151
* Close the connection now (idempotent — the destructor will not close it again).152
* @return 0 on success, -1 if already closed.153
* @complexity one TLS write + teardown.154
* @alloc a small close frame (when still open).155
* @test CheatahWebSocket.ClientDefaultIsClosed156
* @systest WebSocketSys.ClientGuardRoundTrip157
*/158
long long close();160
private:161
long long session_ = 0;162
};164
/**165
* Open a secure WebSocket connection and return it as an owning Client (the RAII,166
* `with`-friendly form of connect()). The TLS server is AUTHENTICATED by default.167
* @param host the server host, e.g. "echo.websocket.org".168
* @param port the TLS port, normally 443.169
* @param path the request path, e.g. "/".170
* @param server_name the TLS SNI / Host (usually == @p host); matched against the certificate SAN.171
* @param insecure skip certificate validation (pinned/controlled peer only). Default false.172
* @param ca_file a PEM CA bundle to trust instead of the system store (empty = system default).173
* @param secure whether to run the connection over TLS. Default TRUE; false selects a174
* PLAINTEXT WebSocket and is refused unless @p host is loopback.175
* @return an owning Client.176
* @throws std::runtime_error on connect/TLS/validation/upgrade failure.177
* TLS IS THE DEFAULT AND STAYS THE DEFAULT. @p secure = false selects a PLAINTEXT WebSocket,178
* and connect() then refuses any host that is not loopback (127.0.0.1, ::1, localhost). It179
* exists for a local control plane — Chrome's DevTools endpoint speaks ws:// on loopback and180
* offers no TLS at all — so cleartext here can never reach the network. Every existing caller181
* is unchanged: omit the parameter and you get TLS.182
*183
* @warning @p insecure = true drops the MITM protection: ANY peer that holds its own184
* certificate's key is accepted. Pinned/controlled peers only.185
* @complexity one TCP + one TLS handshake + one HTTP round trip.186
* @alloc the session.187
* @concurrency blocks for the TCP/TLS/upgrade round trips.188
* @systest WebSocketSys.ClientOpenAndMoveAssignClosesOpen189
*/190
Client open(const std::string& host, long long port, const std::string& path,191
const std::string& server_name, bool insecure = false, const std::string& ca_file = "",192
bool secure = true);194
/**195
* Open a secure WebSocket connection from a `wss://host[:port]/path` URL and return it as an196
* owning Client (the RAII, `with`-friendly form of connect_url()). Server AUTHENTICATED by default.197
* @param url the wss URL.198
* @param insecure skip certificate validation (pinned/controlled peer only). Default false.199
* @param ca_file a PEM CA bundle to trust instead of the system store (empty = system default).200
* @return an owning Client.201
* @throws std::runtime_error on a non-wss scheme or a connect/validation failure.202
* @warning @p insecure = true drops the MITM protection (see open()).203
* @complexity as open().204
* @alloc the session.205
* @concurrency blocks for the TCP/TLS/upgrade round trips.206
* @test CheatahWebSocket.ConnectUrlRejectsNonWss207
* @systest WebSocketSys.ClientGuardRoundTrip208
*/209
Client open_url(const std::string& url, bool insecure = false, const std::string& ca_file = "");211
} // namespace cheatah::websocket