ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
ws_client.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file ws_client.cpp
6 * @brief WebSocket (RFC 6455) client codec (host-testable) + the raw-lwIP /
7 * mbedTLS persistent transport (ESP32 only).
8 */
9
11#include "services/system/clock.h" // pcdelay
12#include "shared_primitives/strbuf.h" // pc_sb frame builder
13
14#if PC_ENABLE_WS_CLIENT
15
16#include "crypto/hash/sha1.h"
18#include <stdio.h>
19#include <string.h>
20
21// ---------------------------------------------------------------------------
22// Pure codec (host-testable)
23// ---------------------------------------------------------------------------
24
25#if defined(ARDUINO)
26#include "network_drivers/transport/client.h" // shared outbound TCP client (L4)
27#include <Arduino.h>
28#include <esp_system.h> // esp_fill_random (per-frame masking key)
29#endif
30#if defined(ARDUINO) && PC_ENABLE_WS_CLIENT_TLS
32#include <mbedtls/ssl.h>
33#endif
34static const char WS_MAGIC[] = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
35
36void ws_client_accept_for_key(const char *key_b64, char *out, size_t out_cap)
37{
38 if (!out || out_cap == 0)
39 {
40 return;
41 }
42 out[0] = '\0';
43 if (!key_b64)
44 {
45 return;
46 }
47 char concat[64];
48 size_t klen = strnlen(key_b64, sizeof(concat));
49 size_t mlen = sizeof(WS_MAGIC) - 1;
50 if (klen + mlen >= sizeof(concat))
51 {
52 return;
53 }
54 memcpy(concat, key_b64, klen);
55 memcpy(concat + klen, WS_MAGIC, mlen);
56 uint8_t digest[PC_SHA1_DIGEST_LEN];
57 pc_sha1((const uint8_t *)concat, klen + mlen, digest);
58 if (out_cap < 29) // 28 base64 chars + NUL
59 {
60 return;
61 }
63}
64
65size_t ws_client_build_handshake(uint8_t *out, size_t cap, const char *host, const char *path, const char *key_b64,
66 const char *subprotocol)
67{
68 if (!out || !host || !path || !key_b64)
69 {
70 return 0;
71 }
72 // A Sec-WebSocket-Protocol offer is emitted only when a subprotocol is requested (e.g. "wamp.2.json" for
73 // WAMP-over-WebSocket); the server echoes the one it selected. Null/empty omits the header entirely.
74 // The two forms differ only by the optional Protocol header, so one builder emits both rather
75 // than two near-identical copies of the same handshake.
76 pc_sb sb = {(char *)out, cap, 0, true};
77 pc_sb_put(&sb, "GET ");
78 pc_sb_put(&sb, path);
79 pc_sb_put(&sb, " HTTP/1.1\r\nHost: ");
80 pc_sb_put(&sb, host);
81 pc_sb_put(&sb, "\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ");
82 pc_sb_put(&sb, key_b64);
83 if (subprotocol && subprotocol[0])
84 {
85 pc_sb_put(&sb, "\r\nSec-WebSocket-Protocol: ");
86 pc_sb_put(&sb, subprotocol);
87 }
88 pc_sb_put(&sb, "\r\nSec-WebSocket-Version: 13\r\n\r\n");
89 size_t n = pc_sb_finish(&sb);
90 if (!sb.ok)
91 {
92 return 0;
93 }
94 return n;
95}
96
97// Case-insensitive header-value lookup within [buf, buf+len); returns a pointer
98// to the value (past "name:" and OWS) and its length via *vlen, or nullptr.
99static const char *find_header(const uint8_t *buf, size_t len, const char *name, size_t *vlen)
100{
101 size_t nlen = strnlen(name, len + 1);
102 const uint8_t *p = buf;
103 const uint8_t *end = buf + len;
104 while (p + nlen + 1 < end)
105 {
106 if (strncasecmp((const char *)p, name, nlen) == 0 && p[nlen] == ':')
107 {
108 const uint8_t *v = p + nlen + 1;
109 while (v < end && (*v == ' ' || *v == '\t'))
110 {
111 v++;
112 }
113 const uint8_t *e = v;
114 while (e < end && *e != '\r' && *e != '\n')
115 {
116 e++;
117 }
118 *vlen = (size_t)(e - v);
119 return (const char *)v;
120 }
121 while (p < end && *p != '\n')
122 {
123 p++;
124 }
125 if (p < end)
126 {
127 p++;
128 }
129 }
130 return nullptr;
131}
132
133bool ws_client_check_response(const uint8_t *buf, size_t len, const char *expected_accept)
134{
135 if (!buf || len < 12 || !expected_accept)
136 {
137 return false;
138 }
139 // Status line must be an HTTP 101.
140 const uint8_t *eol = (const uint8_t *)memchr(buf, '\n', len);
141 if (!eol)
142 {
143 return false;
144 }
145 bool ok101 = false;
146 for (const uint8_t *q = buf; q + 3 < eol; q++)
147 {
148 if (q[0] == '1' && q[1] == '0' && q[2] == '1')
149 {
150 ok101 = true;
151 break;
152 }
153 }
154 if (!ok101)
155 {
156 return false;
157 }
158 size_t vlen = 0;
159 const char *acc = find_header(buf, len, "Sec-WebSocket-Accept", &vlen);
160 if (!acc)
161 {
162 return false;
163 }
164 return vlen == strnlen(expected_accept, vlen + 1) && memcmp(acc, expected_accept, vlen) == 0;
165}
166
167size_t ws_client_build_frame(uint8_t *out, size_t cap, WsClientOpcode opcode, const uint8_t *payload, size_t len,
168 const uint8_t mask[4])
169{
170 if (!out || !mask)
171 {
172 return 0;
173 }
174 size_t hdr = 2 + 4; // byte0 + len-byte + 4-byte mask (short form)
175 if (len >= 126 && len < 65536)
176 {
177 hdr += 2;
178 }
179 else if (len >= 65536)
180 {
181 hdr += 8;
182 }
183 if (hdr + len > cap)
184 {
185 return 0;
186 }
187
188 size_t i = 0;
189 out[i++] = (uint8_t)(0x80 | static_cast<uint8_t>(opcode)); // FIN + opcode (all opcodes <= 0x0A)
190 if (len < 126)
191 {
192 out[i++] = (uint8_t)(0x80 | len);
193 }
194 else if (len < 65536)
195 {
196 out[i++] = 0x80 | 126;
197 out[i++] = (uint8_t)(len >> 8);
198 out[i++] = (uint8_t)(len & 0xFF);
199 }
200 else
201 {
202 out[i++] = 0x80 | 127;
203 for (int s = 56; s >= 0; s -= 8)
204 {
205 out[i++] = (uint8_t)((uint64_t)len >> s);
206 }
207 }
208 memcpy(out + i, mask, 4);
209 i += 4;
210 for (size_t j = 0; j < len; j++)
211 {
212 out[i + j] = (uint8_t)(payload[j] ^ mask[j & 3]);
213 }
214 return i + len;
215}
216
217bool ws_client_parse_frame(const uint8_t *buf, size_t avail, uint8_t *opcode, bool *fin, size_t *payload_off,
218 size_t *payload_len, size_t *consumed)
219{
220 if (!buf || avail < 2)
221 {
222 return false;
223 }
224 uint8_t b0 = buf[0];
225 uint8_t b1 = buf[1];
226 bool masked = (b1 & 0x80) != 0;
227 uint64_t len = b1 & 0x7F;
228 size_t off = 2;
229 if (len == 126)
230 {
231 if (avail < off + 2)
232 {
233 return false;
234 }
235 len = ((uint64_t)buf[off] << 8) | buf[off + 1];
236 off += 2;
237 }
238 else if (len == 127)
239 {
240 if (avail < off + 8)
241 {
242 return false;
243 }
244 uint64_t v = 0;
245 for (int s = 0; s < 8; s++)
246 {
247 v = (v << 8) | buf[off + s];
248 }
249 if (v > 0xFFFFFFFFu) // absurd frame length on a constrained device
250 {
251 return false;
252 }
253 len = v;
254 off += 8;
255 }
256 if (masked)
257 {
258 off += 4; // server frames should not be masked, but stay aligned if they are
259 }
260 if (avail < off + (size_t)len)
261 {
262 return false;
263 }
264 *opcode = (uint8_t)(b0 & 0x0F);
265 *fin = (b0 & 0x80) != 0;
266 *payload_off = off;
267 *payload_len = (size_t)len;
268 *consumed = off + (size_t)len;
269 return true;
270}
271
272// ---------------------------------------------------------------------------
273// Transport (ESP32 only): persistent raw-lwIP client + RFC 6455 framing,
274// with wss:// over a persistent client TLS session (pc_tls csess).
275// ---------------------------------------------------------------------------
276#if defined(ARDUINO)
277
278#ifdef PC_WS_CLIENT_DEBUG
279#define WSC_DBG(...) printf(__VA_ARGS__)
280#else
281#define WSC_DBG(...) ((void)0)
282#endif
283
284// All WebSocket-client connection state, owned by one instance (internal linkage): one server
285// at a time, all static / no heap. Grouped so it is one named owner, unreachable cross-TU.
286struct WsClientCtx
287{
288 WsClientMessageCb cb;
289 int cid = -1; // outbound connection id (pc_client pool)
290 volatile bool closed; // peer closed / error (set when the pump sees it)
291 bool ws_up;
292 bool use_tls;
293
294 // Inbound plaintext ring, fed by a pump in the loop: from pc_client_read for
295 // plain ws, from the TLS session (pc_tls_client_session_read) for wss.
296 uint8_t rx[PC_WS_CLIENT_BUF_SIZE];
297 volatile size_t rx_head;
298 volatile size_t rx_tail;
299 uint8_t pkt[PC_WS_CLIENT_BUF_SIZE]; // a frame copied out to parse
300 uint8_t tx[PC_WS_CLIENT_BUF_SIZE]; // outgoing frame scratch
301
302 // Fragmented-message reassembly (continuation frames -> one delivered message).
303 uint8_t msg[PC_WS_CLIENT_BUF_SIZE];
304 size_t msg_len;
305 uint8_t msg_op;
306};
307static WsClientCtx s_wsc;
308
309static inline size_t ring_avail()
310{
311 return (s_wsc.rx_head + sizeof(s_wsc.rx) - s_wsc.rx_tail) % sizeof(s_wsc.rx);
312}
313static inline uint8_t ring_peek(size_t i)
314{
315 return s_wsc.rx[(s_wsc.rx_tail + i) % sizeof(s_wsc.rx)];
316}
317static inline void ring_advance(size_t n)
318{
319 s_wsc.rx_tail = (s_wsc.rx_tail + n) % sizeof(s_wsc.rx);
320}
321static void ring_copy(uint8_t *dst, size_t n)
322{
323 for (size_t i = 0; i < n; i++)
324 {
325 dst[i] = s_wsc.rx[(s_wsc.rx_tail + i) % sizeof(s_wsc.rx)];
326 }
327}
328
329// --- transport over the shared outbound client (pc_client) ---
330
331static bool ws_tx_plain(const uint8_t *data, size_t len)
332{
333 return pc_client_send(s_wsc.cid, data, len);
334}
335
336// Drain plaintext wire bytes from the client into the s_wsc.rx ring (plain ws).
337static void ws_pump_plain()
338{
339 uint8_t tmp[256];
340 for (;;)
341 {
342 size_t freey = (sizeof(s_wsc.rx) - 1) - ring_avail();
343 if (freey == 0)
344 {
345 break;
346 }
347 size_t want = freey < sizeof(tmp) ? freey : sizeof(tmp);
348 size_t n = pc_client_read(s_wsc.cid, tmp, want);
349 if (n == 0)
350 {
351 if (pc_client_is_closed(s_wsc.cid))
352 {
353 s_wsc.closed = true;
354 }
355 break;
356 }
357 for (size_t i = 0; i < n; i++)
358 {
359 s_wsc.rx[s_wsc.rx_head] = tmp[i];
360 s_wsc.rx_head = (s_wsc.rx_head + 1) % sizeof(s_wsc.rx);
361 }
362 }
363}
364
365#if PC_ENABLE_WS_CLIENT_TLS
366// TLS BIO over the shared client: write ciphertext through the pool, read
367// ciphertext by draining the client's wire ring.
368static int ws_tls_send(void *ctx, const unsigned char *buf, size_t len)
369{
370 (void)ctx;
371 size_t cap = len > 0xFFFF ? 0xFFFF : len;
372 return pc_client_send(s_wsc.cid, buf, cap) ? (int)cap : MBEDTLS_ERR_SSL_WANT_WRITE;
373}
374static int ws_tls_recv(void *ctx, unsigned char *buf, size_t len)
375{
376 (void)ctx;
377 size_t n = pc_client_read(s_wsc.cid, buf, len);
378 if (n == 0)
379 {
380 return pc_client_is_closed(s_wsc.cid) ? 0 : MBEDTLS_ERR_SSL_WANT_READ;
381 }
382 return (int)n;
383}
384static void ws_pump_tls()
385{
386 uint8_t tmp[256];
387 for (;;)
388 {
389 size_t freey = (sizeof(s_wsc.rx) - 1) - ring_avail();
390 if (freey == 0)
391 {
392 break;
393 }
394 size_t want = freey < sizeof(tmp) ? freey : sizeof(tmp);
395 int n = pc_tls_client_session_read(tmp, want);
396 if (n <= 0)
397 {
398 if (n < 0)
399 {
400 s_wsc.closed = true;
401 }
402 break;
403 }
404 for (int i = 0; i < n; i++)
405 {
406 s_wsc.rx[s_wsc.rx_head] = tmp[i];
407 s_wsc.rx_head = (s_wsc.rx_head + 1) % sizeof(s_wsc.rx);
408 }
409 }
410}
411#endif // PC_ENABLE_WS_CLIENT_TLS
412
413// Send already-framed bytes (plaintext or TLS-encrypted per the mode).
414static bool ws_tx(const uint8_t *data, size_t len)
415{
416#if PC_ENABLE_WS_CLIENT_TLS
417 if (s_wsc.use_tls)
418 {
419 return pc_tls_client_session_write(data, len) == (int)len;
420 }
421#endif
422 return ws_tx_plain(data, len);
423}
424
425// Frame and send a message with a fresh random masking key (RFC 6455 client rule).
426static bool ws_send_frame(WsClientOpcode opcode, const uint8_t *payload, size_t len)
427{
428 if (!s_wsc.ws_up)
429 {
430 return false;
431 }
432 uint8_t mask[4];
433 esp_fill_random(mask, 4);
434 size_t n = ws_client_build_frame(s_wsc.tx, sizeof(s_wsc.tx), opcode, payload, len, mask);
435 return n && ws_tx(s_wsc.tx, n);
436}
437
438static void ws_close_tcp()
439{
440#if PC_ENABLE_WS_CLIENT_TLS
441 if (s_wsc.use_tls)
442 {
443 pc_tls_client_session_end();
444 }
445#endif
446 if (s_wsc.cid >= 0)
447 {
448 pc_client_close(s_wsc.cid);
449 }
450 s_wsc.cid = -1;
451 s_wsc.ws_up = false;
452}
453
454static void deliver(uint8_t op, const uint8_t *payload, size_t len)
455{
456 if (s_wsc.cb && (op == (uint8_t)WsClientOpcode::WSC_OP_TEXT || op == (uint8_t)WsClientOpcode::WSC_OP_BINARY))
457 {
458 s_wsc.cb(op, payload, len);
459 }
460}
461
462// Dispatch one parsed frame (handles fragmentation, ping/pong, close).
463static void handle_frame(uint8_t op, bool fin, const uint8_t *payload, size_t len)
464{
465 switch ((WsClientOpcode)op)
466 {
467 case WsClientOpcode::WSC_OP_TEXT:
468 case WsClientOpcode::WSC_OP_BINARY:
469 if (fin)
470 {
471 deliver(op, payload, len); // common case: unfragmented
472 }
473 else
474 {
475 s_wsc.msg_op = op; // first fragment
476 s_wsc.msg_len = len < sizeof(s_wsc.msg) ? len : sizeof(s_wsc.msg);
477 memcpy(s_wsc.msg, payload, s_wsc.msg_len);
478 }
479 break;
480 case WsClientOpcode::WSC_OP_CONT:
481 if (s_wsc.msg_len + len <= sizeof(s_wsc.msg))
482 {
483 memcpy(s_wsc.msg + s_wsc.msg_len, payload, len);
484 s_wsc.msg_len += len;
485 }
486 if (fin)
487 {
488 deliver(s_wsc.msg_op, s_wsc.msg, s_wsc.msg_len);
489 s_wsc.msg_len = 0;
490 }
491 break;
492 case WsClientOpcode::WSC_OP_PING:
493 ws_send_frame(WsClientOpcode::WSC_OP_PONG, payload, len); // echo the application data
494 break;
495 case WsClientOpcode::WSC_OP_CLOSE:
496 ws_send_frame(WsClientOpcode::WSC_OP_CLOSE, nullptr, 0);
497 s_wsc.closed = true;
498 break;
499 case WsClientOpcode::WSC_OP_PONG:
500 default:
501 break;
502 }
503}
504
505static void process_rx()
506{
507#if PC_ENABLE_WS_CLIENT_TLS
508 if (s_wsc.use_tls)
509 {
510 ws_pump_tls();
511 }
512 else
513#endif
514 ws_pump_plain();
515 for (;;)
516 {
517 size_t avail = ring_avail();
518 if (avail < 2)
519 {
520 return;
521 }
522 // Peek the header bytes (a frame header is at most 14 bytes); parse_frame
523 // reads only the header and uses the real ring count to test completeness.
524 uint8_t hdr[14];
525 size_t hn = avail < sizeof(hdr) ? avail : sizeof(hdr);
526 for (size_t i = 0; i < hn; i++)
527 {
528 hdr[i] = ring_peek(i);
529 }
530 uint8_t op;
531 bool fin;
532 size_t off;
533 size_t plen;
534 size_t consumed;
535 if (!ws_client_parse_frame(hdr, avail, &op, &fin, &off, &plen, &consumed))
536 {
537 return; // header incomplete or full frame not yet arrived
538 }
539 if (consumed > sizeof(s_wsc.pkt))
540 {
541 ring_advance(consumed); // oversized frame: drop it
542 continue;
543 }
544 ring_copy(s_wsc.pkt, consumed);
545 ring_advance(consumed);
546 handle_frame(op, fin, s_wsc.pkt + off, plen);
547 }
548}
549
550void ws_client_on_message(WsClientMessageCb cb)
551{
552 s_wsc.cb = cb;
553}
554
555bool ws_client_connect(const char *host, uint16_t port, bool use_tls, const char *path)
556{
557 if (!host || !path)
558 {
559 return false;
560 }
561#if !PC_ENABLE_WS_CLIENT_TLS
562 if (use_tls)
563 {
564 return false;
565 }
566#endif
567 s_wsc.rx_head = s_wsc.rx_tail = 0;
568 s_wsc.closed = s_wsc.ws_up = false;
569 s_wsc.msg_len = 0;
570 s_wsc.use_tls = use_tls;
571
572 uint32_t deadline = millis() + 8000;
573
574 // Open the TCP connection (DNS + connect) via the shared client transport.
575 s_wsc.cid = pc_client_open(host, port, 8000);
576 if (s_wsc.cid < 0)
577 {
578 WSC_DBG("[wsc] pc_client_open failed (%d)\n", s_wsc.cid);
579 return false;
580 }
581
582#if PC_ENABLE_WS_CLIENT_TLS
583 if (s_wsc.use_tls)
584 {
585 if (!pc_tls_client_session_begin(host, ws_tls_send, ws_tls_recv))
586 {
587 WSC_DBG("[wsc] csess_begin failed\n");
588 ws_close_tcp();
589 return false;
590 }
591 int h;
592 while ((h = pc_tls_client_session_handshake()) == 0 && !s_wsc.closed && (int32_t)(deadline - millis()) > 0)
593 {
594 pcdelay(5);
595 }
596 if (h != 1)
597 {
598 WSC_DBG("[wsc] TLS handshake h=%d closed=%d\n", h, (int)s_wsc.closed);
599 ws_close_tcp();
600 return false;
601 }
602 WSC_DBG("[wsc] TLS handshake ok\n");
603 }
604#endif
605
606 // Generate a random 16-byte key, base64 it, send the opening handshake.
607 uint8_t keyraw[16];
608 esp_fill_random(keyraw, sizeof(keyraw));
609 char key_b64[25];
610 pc_base64_encode(keyraw, sizeof(keyraw), key_b64);
611 char expect[32];
612 ws_client_accept_for_key(key_b64, expect, sizeof(expect));
613
614 size_t n = ws_client_build_handshake(s_wsc.tx, sizeof(s_wsc.tx), host, path, key_b64, nullptr);
615 if (n == 0 || !ws_tx(s_wsc.tx, n))
616 {
617 ws_close_tcp();
618 return false;
619 }
620
621 // Read the response header (up to "\r\n\r\n") out of the rx ring.
622 uint8_t hs[512];
623 size_t hl = 0;
624 bool done = false;
625 while (!done && !s_wsc.closed && (int32_t)(deadline - millis()) > 0)
626 {
627#if PC_ENABLE_WS_CLIENT_TLS
628 if (s_wsc.use_tls)
629 {
630 ws_pump_tls();
631 }
632 else
633#endif
634 ws_pump_plain();
635 while (ring_avail() > 0 && hl < sizeof(hs))
636 {
637 hs[hl++] = ring_peek(0);
638 ring_advance(1);
639 if (hl >= 4 && hs[hl - 4] == '\r' && hs[hl - 3] == '\n' && hs[hl - 2] == '\r' && hs[hl - 1] == '\n')
640 {
641 done = true;
642 break;
643 }
644 }
645 if (!done)
646 {
647 pcdelay(5);
648 }
649 }
650 if (!done || !ws_client_check_response(hs, hl, expect))
651 {
652 WSC_DBG("[wsc] handshake fail done=%d hl=%u resp:\n%.*s\n", (int)done, (unsigned)hl, (int)hl, (const char *)hs);
653 ws_close_tcp();
654 return false;
655 }
656 s_wsc.ws_up = true;
657 return true;
658}
659
660bool ws_client_send_text(const char *text)
661{
662 return ws_send_frame(WsClientOpcode::WSC_OP_TEXT, (const uint8_t *)text,
663 text ? strnlen(text, PC_WS_CLIENT_BUF_SIZE) : 0);
664}
665bool ws_client_send_binary(const uint8_t *data, size_t len)
666{
667 return ws_send_frame(WsClientOpcode::WSC_OP_BINARY, data, len);
668}
669
670bool ws_client_loop()
671{
672 if (!s_wsc.ws_up)
673 {
674 return false;
675 }
676 process_rx();
677 if (s_wsc.closed)
678 {
679 ws_close_tcp();
680 return false;
681 }
682 return true;
683}
684
685bool ws_client_connected()
686{
687 return s_wsc.ws_up;
688}
689
690void ws_client_close()
691{
692 if (s_wsc.ws_up)
693 {
694 ws_send_frame(WsClientOpcode::WSC_OP_CLOSE, nullptr, 0);
695 }
696 ws_close_tcp();
697}
698
699#else // host build: transport is a stub
700
701void ws_client_on_message(WsClientMessageCb)
702{
703}
704bool ws_client_connect(const char *, uint16_t, bool, const char *)
705{
706 return false;
707}
708bool ws_client_send_text(const char *)
709{
710 return false;
711}
712bool ws_client_send_binary(const uint8_t *, size_t)
713{
714 return false;
715}
716bool ws_client_loop()
717{
718 return false;
719}
720bool ws_client_connected()
721{
722 return false;
723}
724void ws_client_close()
725{
726}
727
728#endif // ARDUINO
729
730#endif // PC_ENABLE_WS_CLIENT
void pc_base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
Definition base64.cpp:29
Base64 encoder/decoder.
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
Definition client.cpp:366
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
Definition client.cpp:374
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
Definition client.cpp:362
int pc_client_open(const char *, uint16_t, uint32_t)
Resolve host (dotted-quad fast path, else DNS) and connect to host : port, blocking up to timeout_ms.
Definition client.cpp:354
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
Definition client.cpp:378
Layer 4 outbound TCP client transport - the client-side peer of the (server) transport in tcp....
Pluggable monotonic clock for all library timing.
void pcdelay(uint32_t ms)
Block for at least ms milliseconds - the library's single delay primitive.
Definition clock.h:89
uint32_t mask(uint8_t width)
Mask of width low bits (width 32 handled without a 32-bit shift, which is UB).
Definition crc.h:61
#define PC_WS_CLIENT_BUF_SIZE
WebSocket client send/receive buffer size in bytes (bounds one frame).
PC_CRYPTO_HOT void pc_sha1(const uint8_t *data, size_t len, uint8_t digest[PC_SHA1_DIGEST_LEN])
Compute a SHA-1 digest over an arbitrary byte buffer.
Definition sha1.cpp:26
SHA-1 (FIPS 180-4) - one-shot digest.
#define PC_SHA1_DIGEST_LEN
SHA-1 digest length in bytes.
Definition sha1.h:23
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
bool ok
Definition strbuf.h:34
Deterministic TLS engine: mbedTLS over a static memory pool (PC_ENABLE_TLS).
bool ws_send_frame(WsConn *ws, WsOpcode opcode, const uint8_t *payload, uint16_t len)
Send a WebSocket frame to the client.
Zero-heap outbound WebSocket client, RFC 6455 (PC_ENABLE_WS_CLIENT).