cheatah
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 once
5/**
6 * @file websocket.hpp
7 * @brief cheatah `websocket` — a from-scratch, low-latency WebSocket CLIENT
8 * (RFC 6455) over the cheatah `tls` 1.3 client and `socket`. `import
9 * 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), so
14 * recv does ZERO unmasking work — it slices the payload straight out of the
15 * 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 per
18 * underlying read/decrypt.
19 * The send path masks (clients MUST, §5.3) with a 64-bit-word XOR (8 bytes per
20 * 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 by
23 * `websocket.open(...)` / `websocket.open_url("wss://...")`: it sends a close frame and tears
24 * down the TLS session + socket automatically when it goes out of scope, so a cheatah program
25 * cannot leak the heap Session. wss:// only (WebSocket over TLS) — the transport is always
26 * encrypted, like the rest of cheatah's net stack. The flat handle-based calls (an integer
27 * 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 same
30 * session from two threads at once. Separate sessions are independent.
31 */
33#include <string>
35namespace 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 in
39// websocket_lowlevel.hpp. It is intentionally NOT part of this cheatah-facing header: a cheatah
40// program cannot reach it, so it cannot leak the heap Session — it uses the owning
41// `websocket::Client` guard + `websocket.open()`/`open_url()` below, which free the Session
42// 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 is
50 * destroyed (scope exit out of a `with` body, including via exception) or explicitly close()d, the
51 * close frame is sent and the TLS session and TCP socket are torn down, so
52 * `with websocket.open_url(url) as ws { … }` cannot leak the session, its `tls` session, or its
53 * fd. Move-only: the copy operations are deleted and a moved-from `Client` is left closed.
54 */
55class Client {
56public:
57 /**
58 * Construct a closed client (owns nothing).
59 * @complexity O(1).
60 * @alloc none.
61 * @test CheatahWebSocket.ClientDefaultIsClosed
62 */
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.ClientGuardRoundTrip
70 */
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.ClientGuardRoundTrip
80 */
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.ClientGuardRoundTrip
89 * @systest WebSocketSys.ClientOpenAndMoveAssignClosesOpen
90 */
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.ClientDefaultIsClosed
97 * @systest WebSocketSys.ClientOpenAndMoveAssignClosesOpen
98 */
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.ClientDefaultIsClosed
107 */
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.ClientGuardRoundTrip
115 */
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 from
125 * two threads at once (see the file-level threading note).
126 * @systest WebSocketSys.ClientGuardRoundTrip
127 */
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.ClientGuardRoundTrip
138 */
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.ClientGuardRoundTrip
148 */
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.ClientDefaultIsClosed
156 * @systest WebSocketSys.ClientGuardRoundTrip
157 */
158 long long close();
160private:
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 a
174 * 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). It
179 * exists for a local control plane — Chrome's DevTools endpoint speaks ws:// on loopback and
180 * offers no TLS at all — so cleartext here can never reach the network. Every existing caller
181 * is unchanged: omit the parameter and you get TLS.
182 *
183 * @warning @p insecure = true drops the MITM protection: ANY peer that holds its own
184 * 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.ClientOpenAndMoveAssignClosesOpen
189 */
190Client 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 an
196 * 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.ConnectUrlRejectsNonWss
207 * @systest WebSocketSys.ClientGuardRoundTrip
208 */
209Client open_url(const std::string& url, bool insecure = false, const std::string& ca_file = "");
211} // namespace cheatah::websocket