websocket: add WebSocket client module - #428
Conversation
Add a new websocket module providing WebSocket (RFC 6455) client connectivity to ucode scripts through an event driven, uloop based API. Follow the same integration pattern as the zlib module: the wslay library is discovered with find_library()/find_path() and linked externally, WEBSOCKET_SUPPORT automatically defaults on when the library is present. A corresponding libwslay package is proposed for the OpenWrt core tree. This is the initial scaffold: the connect() entry point is registered but raises a "not implemented" exception until the connection state machine, handshake and wslay event wiring land. Signed-off-by: Nicolò Veronese <nicveronese@gmail.com>
Implement the WebSocket (RFC 6455) client on top of the module skeleton: - URL parsing for ws:// (wss:// is rejected until TLS support lands), including IPv6 literals in bracket notation (stored unbracketed for getaddrinfo(), re-added for the Host header as required by RFC 7230), explicit ports, query strings; userinfo is rejected with a clear error - Synchronous getaddrinfo resolution followed by a fully asynchronous, uloop driven non-blocking connect, handshake and data exchange - RFC 6455 opening handshake: random nonce generation, locally implemented SHA-1 and base64, HTTP upgrade request assembly with optional custom headers (only the 'headers' option value is serialized; values containing line breaks are rejected) and Sec-WebSocket-Accept validation - wslay event integration wired to the socket fd with automatic ping/pong handling, close handshake tracking and EAGAIN aware read/write event management - Event callbacks (open, message, close, error) receive the connection resource as explicit first argument since ucode arrow functions have no lexical this binding - Frame reception bounded through wslay max_recv_msg_length, exposed as max_frame_size option (default 256 KiB, range 1 KiB to 16 MiB) - Bytes arriving pipelined behind the handshake response are handed to the wslay receive path instead of being discarded - The connection struct is explicitly zeroed since ucode resource data regions are not zero-initialized; all exception raising argument validation branches are properly guarded with returns Signed-off-by: Nicolò Veronese <nicveronese@gmail.com> Also fix bracketed IPv6 hosts with an explicit port: the URL parser did not advance past the port digits, so ws://[::1]:8080/path produced the request path /:8080/path (reported in review).
Harden the connection lifecycle: - Complete the close handshake when our close frame was sent and wslay disabled further reads (oversized message, protocol violation): surface the close code we sent instead of waiting for a peer reply that can never be processed, which previously wedged the connection until the timeout fired; propagate the close reason received from the peer - Enforce a 5 second close phase timeout so a peer never replying its close frame cannot hang a CLOSING connection indefinitely - Make close() idempotent while closing and turn close() during the connecting or handshake phase into an immediate abort (close event with status 1006, full teardown) - Defer wslay_event_send() when send(), ping() or close() are invoked from within event callbacks to avoid calling into wslay re-entrantly from its own receive context - Stop polling for readability once wslay disabled reads and handle the resulting empty poll set by deregistering the descriptor - Route in-session socket errors such as ECONNRESET through the abnormal closure path (error event, close code 1006) Fix defects uncovered while building the test suite: - Fix a reference counting error where the connection resource was shared between the C context and the script instead of being properly ucv_get() accounted, causing a premature free and heap corruption when the resource was used after teardown - Fix a poll registration regression that dropped readability events during the handshake response phase, making every connection time out - Fix the ws://host:port/ hostname off-by-one introduced with IPv6 literal support - Repair a mangled is-callable check in on() which caused event callbacks to be silently dropped Signed-off-by: Nicolò Veronese <nicveronese@gmail.com> Additional review fixes: - Never flush queued frames, and never tear the connection down, while still inside the wslay_event_recv() call stack: the deferred flush previously ran from the message callback epilogue which is still below wslay_event_recv(), so a peer sending a message and resetting the connection while the handler invoked send() or close() caused a use-after-free of the wslay context (remotely triggerable, found with ASAN) - Reject on() calls on torn down connections instead of dereferencing a NULL resource handle - Always emit a final close event with status 1006 after fatal send errors, matching the abrupt reset behaviour
Introduce tests/cram/test_websocket.t covering module loading, handshake header validation, text and binary echo, ping/pong, server and client initiated close handshakes (including close reason propagation), fragmented message reassembly, oversized frame rejection (close 1009), message flood processing without loss, abrupt TCP resets, wrong Sec-WebSocket-Accept and non-101 handshake replies as well as URL validation. Provide tests/cram/fixtures/ws-fixture.c, a small scripted RFC 6455 server (independent implementation, no wslay) exposing scenarios through the request path. The fixture target and the test file are only built and registered when WEBSOCKET_SUPPORT is enabled. Also add complete jsdoc for the connect() function. Signed-off-by: Nicolò Veronese <nicveronese@gmail.com> Add regression tests for the review findings: IPv6 literal with port request path, on() after connection teardown and send() from a message callback across a peer reset (fixture /msgreset scenario).
|
have a look at https://github.com/blogic/uwsc. it does nto depend on an external websocket library and is derived/based ont he websocket server code that @jow- wrote. |
I completly missed that repo! I searched for ucode prefix and havent seen that! Have to do some tests on my integration and see if it is indeed a perfect replacement. |
|
@hitech95 if anything is missing let me know we can extend uwsc. I have been using the uwsc code base for several projects over the last months and it is ultra stable for me. |
Hi,
this adds a new
websocketmodule so ucode scripts can talk toWebSocket (RFC 6455) servers. Its completly event driven and hooked
into uloop, so it plays nice with the usual event loop based scripts:
Some notes on the design descisions:
the caller drives the event loop, which fits our embeded use case nicely. It is linked the same way zlib is linked:
WEBSOCKET_SUPPORT automaticaly defaults on when the libray is found, no vendored code.
this, the callbacks recieve the connection as explicit first argument.recieved message sizes are capped thru max_frame_size (default 256 KiB, oversize messages get closed with 1009).
Tests: there is a cram suite (test_websocket.t) with a small scripted fixture server that speaks the protocol independantly of wslay, covering handshake validation, echo, fragmentation, oversized
frames, resets, floods etc.
Everything was also run thru ASan and cross compiled + runtime tested on aarch64/musl (qemu).
On the OpenWrt side I'm working on a PR adding libwslay to package/libs, plus the ucode-mod-websocket package defininition happy to send those once/if this is acceptted here.
Known limitations: no TLS (wss:// fails with a clear error for now) and no permessage-deflate (on purpose, memory).
Server role is not planned unless someone actualy needs it.
Real world user case why this exist:
Iḿ making a status bridge between CamillaDSP websocket interface and ubus.
Feedback very welcome, especialy on the API surface and the error
handling conventions.