ProtoCore v1.0.16
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ws_client.h
Go to the documentation of this file.
1// ProtoCore v1.0.16 - Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ws_client.h
6 * @brief The WebSocket Protocol (RFC 6455), client end: the opening handshake and the base framing
7 * protocol (PROTOCORE_ENABLE_WS_CLIENT).
8 *
9 * RFC 6455 sec 3 gives the connection its four terms: /host/, /port/, /secure/ and the
10 * /resource name/ the request-line targets. The opening handshake rides on HTTP/1.1 - a GET
11 * request-line (RFC 9112 sec 3) carrying Host (RFC 9110 sec 7.2) and Upgrade (RFC 9110 sec 7.8),
12 * answered by 101 Switching Protocols (RFC 9110 sec 15.2.2). After that the connection carries
13 * frames, not messages, and every frame this end sends is masked (RFC 6455 sec 5.3).
14 *
15 * Two halves behind one handle:
16 *
17 * - The codec (RFC 6455 sec 4 and sec 5) reads and writes octets in the caller's buffer and holds
18 * nothing, so it is unit-tested on the host (env:native_ws_client).
19 * - The transport drives one connection over the outbound TCP client, and wss:// over the shared
20 * persistent client TLS session. No heap; one connection at a time.
21 *
22 * Only a message that fits PROTOCORE_WS_CLIENT_BUF_SIZE is delivered.
23 *
24 * The module exports one symbol, @ref WsClient. Everything in ws_client.c has internal linkage.
25 *
26 * @author Douglas Quigg (dstroy0)
27 * @date 2026
28 */
29
30#ifndef PROTOCORE_WS_CLIENT_H
31#define PROTOCORE_WS_CLIENT_H
32
33#include "protocore_config.h" // the entry point: protocore_types.h for the widths
34
35#if PROTOCORE_ENABLE_WS_CLIENT
36
38
39// ---------------------------------------------------------------------------
40// Literals
41// ---------------------------------------------------------------------------
42
43/** @brief |Sec-WebSocket-Key| room: base64 of 16 octets is 24 characters plus NUL (RFC 6455 sec 4.1). */
44#define PROTOCORE_WS_KEY_CAP 25
45
46/** @brief |Sec-WebSocket-Accept| room: base64 of the 20-octet SHA-1 is 28 characters plus NUL (RFC 6455 sec 4.2.2). */
47#define PROTOCORE_WS_ACCEPT_CAP 29
48
49// ---------------------------------------------------------------------------
50// Typedefs
51// ---------------------------------------------------------------------------
52
53/** @brief The 4-bit opcode a frame carries (RFC 6455 sec 5.2); sec 5.6 data, sec 5.5 control. */
54typedef enum PROTO_ENUM_PACKED
55{
56 WSC_OP_CONT = 0x0, ///< continuation frame (RFC 6455 sec 5.4)
57 WSC_OP_TEXT = 0x1, ///< text frame, UTF-8 Application data (RFC 6455 sec 5.6)
58 WSC_OP_BINARY = 0x2, ///< binary frame (RFC 6455 sec 5.6)
59 WSC_OP_CLOSE = 0x8, ///< connection close (RFC 6455 sec 5.5.1)
60 WSC_OP_PING = 0x9, ///< ping (RFC 6455 sec 5.5.2)
61 WSC_OP_PONG = 0xA, ///< pong (RFC 6455 sec 5.5.3)
62} WsClientOpcode;
63
64/** @brief Where a reassembled Text or Binary message is delivered (RFC 6455 sec 5.6). */
65typedef void (*WsClientMessageCb)(uint8_t opcode, const uint8_t *payload, size_t len);
66
67/**
68 * @brief RFC 6455 sec 3 and sec 4.1: the four URI terms and the three fields the handshake names.
69 *
70 * @c accept is both ends of one value: the accept computation writes it, and the server-handshake
71 * check compares the field that arrived against it.
72 */
73typedef struct
74{
75 const char *host; ///< /host/, the Host field's value (RFC 6455 sec 4.1, RFC 9110 sec 7.2)
76 uint16_t port; ///< /port/, the port dialed (RFC 6455 sec 3)
77 proto_bool secure; ///< /secure/, the connection runs over TLS (RFC 6455 sec 3)
78 const char *resource_name; ///< /resource name/, the request-line's target (RFC 6455 sec 3, RFC 9112 sec 3)
79 const char *key; ///< |Sec-WebSocket-Key|, base64 of 16 random octets (RFC 6455 sec 11.3.1)
80 const char *subprotocol; ///< |Sec-WebSocket-Protocol| offered; null or empty omits it (RFC 6455 sec 11.3.4)
81 char *accept; ///< |Sec-WebSocket-Accept|, written by one call and compared by another (sec 11.3.3)
82 size_t accept_cap; ///< its room, at least ::PROTOCORE_WS_ACCEPT_CAP
83} WsHandshakeArgs;
84
85/** @brief RFC 6455 sec 5.2: the base framing protocol's fields, set by a build and filled by a parse. */
86typedef struct
87{
88 uint8_t opcode; ///< the 4-bit opcode (RFC 6455 sec 5.2)
89 proto_bool fin; ///< FIN, the final fragment of a message (RFC 6455 sec 5.2, sec 5.4)
90 const uint8_t *payload; ///< the Payload data a build masks into the frame
91 size_t payload_len; ///< its octet count; a parse reports the Payload data length it found
92 size_t payload_off; ///< where a parse found Payload data inside the frame
93 size_t consumed; ///< the whole frame a parse read: header plus Payload data
94 const uint8_t *masking_key; ///< the 4-octet Masking-key a build applies (RFC 6455 sec 5.3)
95} WsFrameArgs;
96
97/** @brief The octets a codec call moves: one buffer it writes, one it reads. */
98typedef struct
99{
100 uint8_t *out; ///< where a build writes
101 size_t cap; ///< how much room it has
102 const uint8_t *in; ///< the octets a parse or a check reads
103 size_t avail; ///< how many are readable there
104} WsBufArgs;
105
106/** @brief RFC 6455 sec 5.6: the Application data a Data frame carries, and where an inbound one lands. */
107typedef struct
108{
109 const char *text; ///< the UTF-8 Application data a Text frame carries
110 const uint8_t *data; ///< the Application data a Binary frame carries
111 size_t len; ///< its octet count
112 WsClientMessageCb on_message; ///< where a reassembled Text or Binary message is delivered
113} WsMessageArgs;
114
115/**
116 * @brief The client end of the WebSocket Protocol (RFC 6455).
117 *
118 * A caller sets the members a call takes, invokes it through ::WsClient, and reads the outcome off
119 * the same handle. The connection and its buffers are behind @ref internal.
120 *
121 * No slot member: this end drives one connection at a time, so no call names one.
122 *
123 * @var WsClientNs::handshake the four URI terms and the three handshake fields (RFC 6455 sec 3, sec 4.1)
124 * @var WsClientNs::frame the base framing protocol's fields (RFC 6455 sec 5.2)
125 * @var WsClientNs::buf the octets a codec call writes or reads
126 * @var WsClientNs::msg the Application data a Data frame carries (RFC 6455 sec 5.6)
127 * @var WsClientNs::ok a call's true/false outcome
128 * @var WsClientNs::n the octets a build wrote, 0 when they would not fit @ref WsBufArgs::cap
129 * @var WsClientNs::accept_for_key
130 * Write base64(SHA-1(@c key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")) into @c accept, the value the
131 * server's |Sec-WebSocket-Accept| must carry (RFC 6455 sec 1.3, sec 4.2.2 step 5).
132 * @var WsClientNs::build_opening_handshake
133 * Build the client's opening handshake into @c buf: a GET request-line naming @c resource_name
134 * (RFC 9112 sec 3) with Host, Upgrade, Connection, |Sec-WebSocket-Key|, an optional
135 * |Sec-WebSocket-Protocol|, and |Sec-WebSocket-Version| 13 (RFC 6455 sec 4.1).
136 * @var WsClientNs::check_server_handshake
137 * True when the octets in @c buf are a 101 Switching Protocols status-line (RFC 9110 sec 15.2.2)
138 * carrying a |Sec-WebSocket-Accept| field equal to @c accept (RFC 6455 sec 4.1).
139 * @var WsClientNs::build_frame
140 * Build one FIN frame for @c opcode into @c buf, its Payload data masked with @c masking_key
141 * (RFC 6455 sec 5.2, sec 5.3).
142 * @var WsClientNs::parse_frame
143 * Read one inbound frame from @c buf, filling @c opcode, @c fin, @c payload_off, @c payload_len and
144 * @c consumed. False when @c avail holds less than the whole frame (RFC 6455 sec 5.2).
145 * @var WsClientNs::on_message record @c on_message; call it before @ref WsClientNs::connect
146 * @var WsClientNs::connect
147 * Dial /host/ and /port/, run TLS when /secure/ is set, then exchange the opening handshake and
148 * verify the response. True once the WebSocket connection is established (RFC 6455 sec 4.1).
149 * @var WsClientNs::send_text send @c text as a masked Text frame (RFC 6455 sec 5.6)
150 * @var WsClientNs::send_binary send @c data as a masked Binary frame (RFC 6455 sec 5.6)
151 * @var WsClientNs::loop
152 * Read inbound frames, reassemble fragments, answer Ping with Pong (RFC 6455 sec 5.5.2) and echo a
153 * Close (RFC 6455 sec 5.5.1). False once the connection is gone. Call once per loop().
154 * @var WsClientNs::connected true while the WebSocket connection is established
155 * @var WsClientNs::close
156 * Send a Close frame and then Close the WebSocket Connection (RFC 6455 sec 5.5.1, sec 7.1.1).
157 */
158typedef struct
159{
160 WsHandshakeArgs handshake; ///< the URI terms and the handshake fields
161 WsFrameArgs frame; ///< the base framing protocol's fields
162 WsBufArgs buf; ///< the octets a codec call moves
163 WsMessageArgs msg; ///< the Application data a Data frame carries
164 proto_bool ok;
165 size_t n;
166} WsClientVars;
167
168/** @brief The operands and the outcome. */
169extern WsClientVars WsClientV;
170
171/** @brief The entries. */
172typedef struct
173{
174 void (*const accept_for_key)(uint8_t *work);
175 void (*const build_opening_handshake)(uint8_t *work);
176 void (*const check_server_handshake)(uint8_t *work);
177 void (*const build_frame)(uint8_t *work);
178 void (*const parse_frame)(uint8_t *work);
179 void (*const on_message)(uint8_t *work);
180 void (*const connect)(uint8_t *work);
181 void (*const send_text)(uint8_t *work);
182 void (*const send_binary)(uint8_t *work);
183 void (*const loop)(uint8_t *work);
184 void (*const connected)(uint8_t *work);
185 void (*const close)(uint8_t *work);
186} WsClientNs;
187
188// What the table binds, defined once in the .c and taking one parameter each: everything
189// else an entry needs is an operand in WsClientV or a region of the borrow at a fixed offset.
190void protocore_ws_client_accept_for_key(uint8_t *work);
191void protocore_ws_client_build_opening_handshake(uint8_t *work);
192void protocore_ws_client_check_server_handshake(uint8_t *work);
193void protocore_ws_client_build_frame(uint8_t *work);
194void protocore_ws_client_parse_frame(uint8_t *work);
195void protocore_ws_client_on_message(uint8_t *work);
196void protocore_ws_client_connect(uint8_t *work);
197void protocore_ws_client_send_text(uint8_t *work);
198void protocore_ws_client_send_binary(uint8_t *work);
199void protocore_ws_client_loop(uint8_t *work);
200void protocore_ws_client_connected(uint8_t *work);
201void protocore_ws_client_close(uint8_t *work);
202
203// `static const`, initialised HERE rather than `extern` against a definition in the .c: a
204// const object whose initializer every translation unit can see is a COMPILE-TIME FACT, so
205// `WsClient.accept_for_key(work)` resolves to a named function and becomes a DIRECT call. An extern table
206// leaves the call indirect and the symbol live at every level, -O2 -flto included.
207static const WsClientNs WsClient __attribute__((unused)) = {
208 .accept_for_key = protocore_ws_client_accept_for_key,
209 .build_opening_handshake = protocore_ws_client_build_opening_handshake,
210 .check_server_handshake = protocore_ws_client_check_server_handshake,
211 .build_frame = protocore_ws_client_build_frame,
212 .parse_frame = protocore_ws_client_parse_frame,
213 .on_message = protocore_ws_client_on_message,
214 .connect = protocore_ws_client_connect,
215 .send_text = protocore_ws_client_send_text,
216 .send_binary = protocore_ws_client_send_binary,
217 .loop = protocore_ws_client_loop,
218 .connected = protocore_ws_client_connected,
219 .close = protocore_ws_client_close,
220};
221
222/**
223 * @brief The PROTOCORE_WS_CLIENT_BORROW bytes this module's state lives in.
224 *
225 * Stated beside the namespace rather than on it: an entry takes a borrow, and this is where
226 * that borrow comes from. Taken once from the end of the pool, which no mark and no release
227 * walks, so the state lasts the life of the program.
228 *
229 * @return the span.
230 */
231#if PROTOCORE_HAS_NET_STACK
232uint8_t *protocore_ws_client_span(void);
233#endif // PROTOCORE_HAS_NET_STACK
234
236
237#endif // PROTOCORE_ENABLE_WS_CLIENT
238
239#endif // PROTOCORE_WS_CLIENT_H
PROTO_ENUM_PACKED
Application protocol spoken on a listener port or connection slot.
#define PROTOCORE_BEGIN_DECLS
Give a header's declarations C linkage, so their symbol names carry no parameter types.
Definition types.h:96
_Bool proto_bool
The truth value.
Definition types.h:64
#define PROTOCORE_END_DECLS
Definition types.h:97