14#if PC_ENABLE_WS_CLIENT
28#include <esp_system.h>
30#if defined(ARDUINO) && PC_ENABLE_WS_CLIENT_TLS
32#include <mbedtls/ssl.h>
34static const char WS_MAGIC[] =
"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
36void ws_client_accept_for_key(
const char *key_b64,
char *out,
size_t out_cap)
38 if (!out || out_cap == 0)
48 size_t klen = strnlen(key_b64,
sizeof(concat));
49 size_t mlen =
sizeof(WS_MAGIC) - 1;
50 if (klen + mlen >=
sizeof(concat))
54 memcpy(concat, key_b64, klen);
55 memcpy(concat + klen, WS_MAGIC, mlen);
57 pc_sha1((
const uint8_t *)concat, klen + mlen, digest);
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)
68 if (!out || !host || !path || !key_b64)
76 pc_sb sb = {(
char *)out, cap, 0,
true};
81 pc_sb_put(&sb,
"\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: ");
83 if (subprotocol && subprotocol[0])
85 pc_sb_put(&sb,
"\r\nSec-WebSocket-Protocol: ");
88 pc_sb_put(&sb,
"\r\nSec-WebSocket-Version: 13\r\n\r\n");
99static const char *find_header(
const uint8_t *buf,
size_t len,
const char *name,
size_t *vlen)
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)
106 if (strncasecmp((
const char *)p, name, nlen) == 0 && p[nlen] ==
':')
108 const uint8_t *v = p + nlen + 1;
109 while (v < end && (*v ==
' ' || *v ==
'\t'))
113 const uint8_t *e = v;
114 while (e < end && *e !=
'\r' && *e !=
'\n')
118 *vlen = (size_t)(e - v);
119 return (
const char *)v;
121 while (p < end && *p !=
'\n')
133bool ws_client_check_response(
const uint8_t *buf,
size_t len,
const char *expected_accept)
135 if (!buf || len < 12 || !expected_accept)
140 const uint8_t *eol = (
const uint8_t *)memchr(buf,
'\n', len);
146 for (
const uint8_t *q = buf; q + 3 < eol; q++)
148 if (q[0] ==
'1' && q[1] ==
'0' && q[2] ==
'1')
159 const char *acc = find_header(buf, len,
"Sec-WebSocket-Accept", &vlen);
164 return vlen == strnlen(expected_accept, vlen + 1) && memcmp(acc, expected_accept, vlen) == 0;
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])
175 if (len >= 126 && len < 65536)
179 else if (len >= 65536)
189 out[i++] = (uint8_t)(0x80 |
static_cast<uint8_t
>(opcode));
192 out[i++] = (uint8_t)(0x80 | len);
194 else if (len < 65536)
196 out[i++] = 0x80 | 126;
197 out[i++] = (uint8_t)(len >> 8);
198 out[i++] = (uint8_t)(len & 0xFF);
202 out[i++] = 0x80 | 127;
203 for (
int s = 56; s >= 0; s -= 8)
205 out[i++] = (uint8_t)((uint64_t)len >> s);
208 memcpy(out + i, mask, 4);
210 for (
size_t j = 0; j < len; j++)
212 out[i + j] = (uint8_t)(payload[j] ^ mask[j & 3]);
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)
220 if (!buf || avail < 2)
226 bool masked = (b1 & 0x80) != 0;
227 uint64_t len = b1 & 0x7F;
235 len = ((uint64_t)buf[off] << 8) | buf[off + 1];
245 for (
int s = 0; s < 8; s++)
247 v = (v << 8) | buf[off + s];
260 if (avail < off + (
size_t)len)
264 *opcode = (uint8_t)(b0 & 0x0F);
265 *fin = (b0 & 0x80) != 0;
267 *payload_len = (size_t)len;
268 *consumed = off + (size_t)len;
278#ifdef PC_WS_CLIENT_DEBUG
279#define WSC_DBG(...) printf(__VA_ARGS__)
281#define WSC_DBG(...) ((void)0)
288 WsClientMessageCb cb;
290 volatile bool closed;
297 volatile size_t rx_head;
298 volatile size_t rx_tail;
307static WsClientCtx s_wsc;
309static inline size_t ring_avail()
311 return (s_wsc.rx_head +
sizeof(s_wsc.rx) - s_wsc.rx_tail) %
sizeof(s_wsc.rx);
313static inline uint8_t ring_peek(
size_t i)
315 return s_wsc.rx[(s_wsc.rx_tail + i) %
sizeof(s_wsc.rx)];
317static inline void ring_advance(
size_t n)
319 s_wsc.rx_tail = (s_wsc.rx_tail + n) %
sizeof(s_wsc.rx);
321static void ring_copy(uint8_t *dst,
size_t n)
323 for (
size_t i = 0; i < n; i++)
325 dst[i] = s_wsc.rx[(s_wsc.rx_tail + i) %
sizeof(s_wsc.rx)];
331static bool ws_tx_plain(
const uint8_t *data,
size_t len)
337static void ws_pump_plain()
342 size_t freey = (
sizeof(s_wsc.rx) - 1) - ring_avail();
347 size_t want = freey <
sizeof(tmp) ? freey : sizeof(tmp);
357 for (
size_t i = 0; i < n; i++)
359 s_wsc.rx[s_wsc.rx_head] = tmp[i];
360 s_wsc.rx_head = (s_wsc.rx_head + 1) %
sizeof(s_wsc.rx);
365#if PC_ENABLE_WS_CLIENT_TLS
368static int ws_tls_send(
void *ctx,
const unsigned char *buf,
size_t len)
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;
374static int ws_tls_recv(
void *ctx,
unsigned char *buf,
size_t len)
384static void ws_pump_tls()
389 size_t freey = (
sizeof(s_wsc.rx) - 1) - ring_avail();
394 size_t want = freey <
sizeof(tmp) ? freey : sizeof(tmp);
395 int n = pc_tls_client_session_read(tmp, want);
404 for (
int i = 0; i < n; i++)
406 s_wsc.rx[s_wsc.rx_head] = tmp[i];
407 s_wsc.rx_head = (s_wsc.rx_head + 1) %
sizeof(s_wsc.rx);
414static bool ws_tx(
const uint8_t *data,
size_t len)
416#if PC_ENABLE_WS_CLIENT_TLS
419 return pc_tls_client_session_write(data, len) == (int)len;
422 return ws_tx_plain(data, len);
426static bool ws_send_frame(WsClientOpcode opcode,
const uint8_t *payload,
size_t len)
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);
438static void ws_close_tcp()
440#if PC_ENABLE_WS_CLIENT_TLS
443 pc_tls_client_session_end();
454static void deliver(uint8_t op,
const uint8_t *payload,
size_t len)
456 if (s_wsc.cb && (op == (uint8_t)WsClientOpcode::WSC_OP_TEXT || op == (uint8_t)WsClientOpcode::WSC_OP_BINARY))
458 s_wsc.cb(op, payload, len);
463static void handle_frame(uint8_t op,
bool fin,
const uint8_t *payload,
size_t len)
465 switch ((WsClientOpcode)op)
467 case WsClientOpcode::WSC_OP_TEXT:
468 case WsClientOpcode::WSC_OP_BINARY:
471 deliver(op, payload, len);
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);
480 case WsClientOpcode::WSC_OP_CONT:
481 if (s_wsc.msg_len + len <=
sizeof(s_wsc.msg))
483 memcpy(s_wsc.msg + s_wsc.msg_len, payload, len);
484 s_wsc.msg_len += len;
488 deliver(s_wsc.msg_op, s_wsc.msg, s_wsc.msg_len);
492 case WsClientOpcode::WSC_OP_PING:
495 case WsClientOpcode::WSC_OP_CLOSE:
499 case WsClientOpcode::WSC_OP_PONG:
505static void process_rx()
507#if PC_ENABLE_WS_CLIENT_TLS
517 size_t avail = ring_avail();
525 size_t hn = avail <
sizeof(hdr) ? avail : sizeof(hdr);
526 for (
size_t i = 0; i < hn; i++)
528 hdr[i] = ring_peek(i);
535 if (!ws_client_parse_frame(hdr, avail, &op, &fin, &off, &plen, &consumed))
539 if (consumed >
sizeof(s_wsc.pkt))
541 ring_advance(consumed);
544 ring_copy(s_wsc.pkt, consumed);
545 ring_advance(consumed);
546 handle_frame(op, fin, s_wsc.pkt + off, plen);
550void ws_client_on_message(WsClientMessageCb cb)
555bool ws_client_connect(
const char *host, uint16_t port,
bool use_tls,
const char *path)
561#if !PC_ENABLE_WS_CLIENT_TLS
567 s_wsc.rx_head = s_wsc.rx_tail = 0;
568 s_wsc.closed = s_wsc.ws_up =
false;
570 s_wsc.use_tls = use_tls;
572 uint32_t deadline = millis() + 8000;
578 WSC_DBG(
"[wsc] pc_client_open failed (%d)\n", s_wsc.cid);
582#if PC_ENABLE_WS_CLIENT_TLS
585 if (!pc_tls_client_session_begin(host, ws_tls_send, ws_tls_recv))
587 WSC_DBG(
"[wsc] csess_begin failed\n");
592 while ((h = pc_tls_client_session_handshake()) == 0 && !s_wsc.closed && (int32_t)(deadline - millis()) > 0)
598 WSC_DBG(
"[wsc] TLS handshake h=%d closed=%d\n", h, (
int)s_wsc.closed);
602 WSC_DBG(
"[wsc] TLS handshake ok\n");
608 esp_fill_random(keyraw,
sizeof(keyraw));
612 ws_client_accept_for_key(key_b64, expect,
sizeof(expect));
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))
625 while (!done && !s_wsc.closed && (int32_t)(deadline - millis()) > 0)
627#if PC_ENABLE_WS_CLIENT_TLS
635 while (ring_avail() > 0 && hl <
sizeof(hs))
637 hs[hl++] = ring_peek(0);
639 if (hl >= 4 && hs[hl - 4] ==
'\r' && hs[hl - 3] ==
'\n' && hs[hl - 2] ==
'\r' && hs[hl - 1] ==
'\n')
650 if (!done || !ws_client_check_response(hs, hl, expect))
652 WSC_DBG(
"[wsc] handshake fail done=%d hl=%u resp:\n%.*s\n", (
int)done, (
unsigned)hl, (
int)hl, (
const char *)hs);
660bool ws_client_send_text(
const char *text)
662 return ws_send_frame(WsClientOpcode::WSC_OP_TEXT, (
const uint8_t *)text,
665bool ws_client_send_binary(
const uint8_t *data,
size_t len)
667 return ws_send_frame(WsClientOpcode::WSC_OP_BINARY, data, len);
685bool ws_client_connected()
690void ws_client_close()
701void ws_client_on_message(WsClientMessageCb)
704bool ws_client_connect(
const char *, uint16_t,
bool,
const char *)
708bool ws_client_send_text(
const char *)
712bool ws_client_send_binary(
const uint8_t *,
size_t)
720bool ws_client_connected()
724void ws_client_close()
void pc_base64_encode(const uint8_t *src, size_t src_len, char *dst)
Encode src_len bytes of src as Base64.
bool pc_client_send(int, const void *, size_t)
Queue len wire bytes for transmission (marshaled tcp_write + output).
size_t pc_client_read(int, uint8_t *, size_t)
Drain up to cap buffered wire bytes into buf; returns the count.
bool pc_client_is_closed(int)
True once the peer closed (FIN) or the connection errored.
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.
void pc_client_close(int)
Tear down the connection (marshaled) and return the slot to the pool.
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.
uint32_t mask(uint8_t width)
Mask of width low bits (width 32 handled without a 32-bit shift, which is UB).
#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.
SHA-1 (FIPS 180-4) - one-shot digest.
#define PC_SHA1_DIGEST_LEN
SHA-1 digest length in bytes.
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.
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.
Bump-append target; ok latches false once an append would overflow cap.
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).