v0.10.0: server-side WebSocket support
Adds the listener-side surface paired with the existing client API:
- (sigil websocket server): parse-handshake-request, build-handshake-response, build-handshake-error-response, compute-accept-key. Handshake parsing surfaces ws-handshake-request (method, path, host, key, version, subprotocols, headers, x-forwarded-for) or a structured ws-handshake-error (status + reason) for non-upgrade or malformed requests. - (sigil websocket frame): encode-server-{text,binary,close, ping,pong}-frame variants that omit the client mask per RFC 6455 5.1. - Re-exports from (sigil websocket) so server applications can import a single module. - 14 new tests including the RFC 6455 4.2.2 spec test vector for Sec-WebSocket-Accept ("dGhlIHNhbXBsZSBub25jZQ==" -> "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=").
Also rebases dependencies on the post-monorepo split: sigil-stdlib ^0.14, sigil-socket ^0.14, sigil-tls ^0.14.1, sigil-crypto ^0.15.
Used to power enclave-server's IRCv3-WebSocket transport (Phase 2.1 of the personal comm hub).
.gitignore | 2 +
CHANGELOG.md | 12 +++++
dev-redirects.sgl | 15 +++++--
package.sgl | 21 +++++----
sigil.lock | 31 +++++++------
src/sigil/websocket.sgl | 43 +++++++++++++++++-
src/sigil/websocket/frame.sgl | 50 ++++++++++++++++++++-
src/sigil/websocket/server.sgl | 386 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
test/test-server.sgl | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 726 insertions(+), 28 deletions(-).gitignoremodified
build/.sigil/*.sgbCHANGELOG.mdmodified
The format is based on [Keep a Changelog](https://keepachangelog.com/),and this project adheres to [Semantic Versioning](https://semver.org/).## [0.10.0] - 2026-04-29### Added- Server-side WebSocket support: new `(sigil websocket server)` module with `parse-handshake-request`, `build-handshake-response`, `build-handshake-error-response`, and `compute-accept-key` for accepting WebSocket upgrades on a TCP listener.- Server-side frame encoders that omit the client mask per RFC 6455 §5.1: `encode-server-text-frame`, `encode-server-binary-frame`, `encode-server-close-frame`, `encode-server-ping-frame`, `encode-server-pong-frame`.- Re-exported frame primitives (`encode-frame`, `decode-frame`, `ws-frame*`) and handshake helpers from the top-level `(sigil websocket)` module so server applications can use one import.### Changed- Rebased dependencies on the post-monorepo split: `sigil-stdlib` `^0.14`, `sigil-socket` `^0.14`, `sigil-tls` `^0.14.1`, `sigil-crypto` `^0.15`. Top-level `sigil: "^0.14"` declared so the resolver picks a compatible compiler.## [0.7.0] - 2026-03-01### Addeddev-redirects.sglmodified
;; Development redirects — point dependencies at local checkouts;; Development redirects — point dependencies at local sibling checkouts.(redirects repos: (list (for-repo url: "codeberg:sigil/sigil" use: (from-path dir: "../sigil")))) url: "codeberg:sigil/sigil-lang" use: (from-path dir: "../sigil-lang")) (for-repo url: "codeberg:sigil/sigil-socket" use: (from-path dir: "../sigil-socket")) (for-repo url: "codeberg:sigil/sigil-tls" use: (from-path dir: "../sigil-tls")) (for-repo url: "codeberg:sigil/sigil-crypto" use: (from-path dir: "../sigil-crypto"))))package.sglmodified
;;; sigil-websocket - WebSocket client library;;; sigil-websocket - WebSocket protocol library;;;;;; Provides WebSocket client functionality for real-time bidirectional;;; communication. Supports both ws:// and wss:// (TLS) connections.;;; Provides RFC 6455 framing plus both client and server surfaces.;;; Client side: ws-connect for ws:// or wss:// connections. Server;;; side: HTTP-Upgrade handshake helpers + unmasked frame encoders so;;; a listener can accept WebSocket connections and relay text/binary;;; messages to its own connection-loop.(package name: "sigil-websocket" version: "0.9.1" description: "WebSocket client library with TLS support" version: "0.10.0" sigil: "^0.14" description: "WebSocket protocol library — client + server, with TLS" url: "https://codeberg.org/sigil/sigil-websocket" license: "BSD-3-Clause" authors: (list "David Wilson <[email protected]>") optimize: 2)) dependencies: (list (from-git url: "codeberg:sigil/sigil" package: "sigil-stdlib" version: "^0.9.0") (from-git url: "codeberg:sigil/sigil" package: "sigil-socket" version: "^0.9.0") (from-git url: "codeberg:sigil/sigil" package: "sigil-crypto" version: "^0.9.0")) (from-git url: "codeberg:sigil/sigil-lang" package: "sigil-stdlib" version: "^0.14") (from-git url: "codeberg:sigil/sigil-socket" version: "^0.14.0") (from-git url: "codeberg:sigil/sigil-tls" version: "^0.14.1") (from-git url: "codeberg:sigil/sigil-crypto" version: "^0.15.0")) tasks: (list (tasksigil.lockmodified
;; Auto-generated by sigil deps install. Do not edit.(lock (package name: "sigil-stdlib" url: "codeberg:sigil/sigil" ref: "^0.9.0" sha: "54f66e8ec446b65b75ab6f12454d2c0aa269f2c0" url: "codeberg:sigil/sigil-lang" ref: "^0.14" sha: "152ea26c05b73c5d003b246a008d0a79ed0c42d0" package-selector: "sigil-stdlib" version: "0.9.1") version: "0.14.17") (package name: "sigil-socket" url: "codeberg:sigil/sigil" ref: "^0.9.0" sha: "54f66e8ec446b65b75ab6f12454d2c0aa269f2c0" package-selector: "sigil-socket" version: "0.9.1") url: "codeberg:sigil/sigil-socket" ref: "^0.14.0" sha: "cfc8005ebf26b8234f933be11514a78b96c325e0" version: "0.14.0") (package name: "sigil-tls" url: "codeberg:sigil/sigil-tls" ref: "^0.14.1" sha: "722f1ae58644ffe2d129f78fabbbe1e517cdf29a" version: "0.14.1") (package name: "sigil-crypto" url: "codeberg:sigil/sigil" ref: "^0.9.0" sha: "54f66e8ec446b65b75ab6f12454d2c0aa269f2c0" package-selector: "sigil-crypto" version: "0.9.1") url: "codeberg:sigil/sigil-crypto" ref: "^0.15.0" sha: "71e41e7320566bf057828a586f1239041d08d525" version: "0.15.0"))src/sigil/websocket.sglmodified
(define-library (sigil websocket) (import (sigil websocket frame) (sigil websocket connection)) (sigil websocket connection) (sigil websocket server)) (export ;; Connection management opcode-binary opcode-close opcode-ping opcode-pong)) opcode-pong ;; Frame encoding / decoding (re-exported for server use) ws-frame ws-frame? ws-frame-fin? ws-frame-opcode ws-frame-payload encode-frame decode-frame frame-decode-result? frame-decode-result-frame frame-decode-result-bytes-consumed encode-server-text-frame encode-server-binary-frame encode-server-close-frame encode-server-ping-frame encode-server-pong-frame ;; Server handshake ws-handshake-request ws-handshake-request? ws-handshake-request-method ws-handshake-request-path ws-handshake-request-host ws-handshake-request-key ws-handshake-request-version ws-handshake-request-subprotocols ws-handshake-request-headers ws-handshake-request-x-forwarded-for ws-handshake-error ws-handshake-error? ws-handshake-error-status ws-handshake-error-reason parse-handshake-request handshake-request-bytes-needed? build-handshake-response build-handshake-error-response compute-accept-key))src/sigil/websocket/frame.sglmodified
ws-frame-opcode ws-frame-payload ;; Encoding ;; Encoding (client-side: masked per RFC 6455 §5.3) encode-frame encode-text-frame encode-binary-frame encode-ping-frame encode-pong-frame ;; Encoding (server-side: unmasked per RFC 6455 §5.1 — server-to- ;; client frames MUST NOT be masked) encode-server-text-frame encode-server-binary-frame encode-server-close-frame encode-server-ping-frame encode-server-pong-frame ;; Decoding decode-frame frame-decode-result? (: bytevector? -> bytevector?) (encode-frame opcode-pong payload #t #t)) ;; ============================================================ ;; Server-side encoders (no masking — server-to-client frames ;; MUST NOT be masked per RFC 6455 §5.1) ;; ============================================================ ;;; Encode a text frame for server-to-client delivery (unmasked). (define (encode-server-text-frame text) (: string? -> bytevector?) (encode-frame opcode-text text #t #f)) ;;; Encode a binary frame for server-to-client delivery (unmasked). (define (encode-server-binary-frame data) (: bytevector? -> bytevector?) (encode-frame opcode-binary data #t #f)) ;;; Encode a close frame from the server (unmasked). Optional ;;; close code per RFC 6455 §7.4. (define (encode-server-close-frame . args) (: integer? ... -> bytevector?) (let ((payload (if (null? args) (empty-bytevector) (let ((code (car args))) (bytes->bytevector (list (bitwise-and (arithmetic-shift code -8) #xFF) (bitwise-and code #xFF))))))) (encode-frame opcode-close payload #t #f))) ;;; Encode a ping frame from the server (unmasked). (define (encode-server-ping-frame . args) (: bytevector? ... -> bytevector?) (let ((payload (if (null? args) (empty-bytevector) (car args)))) (encode-frame opcode-ping payload #t #f))) ;;; Encode a pong frame from the server (unmasked). Pong payload ;;; must equal the originating ping's payload. (define (encode-server-pong-frame payload) (: bytevector? -> bytevector?) (encode-frame opcode-pong payload #t #f)) ;; ============================================================ ;; Frame Decoding ;; ============================================================src/sigil/websocket/server.sgladded
;;; (sigil websocket server) - Server-side WebSocket handshake helpers.;;;;;; Pairs with `(sigil websocket frame)` for full server support.;;; The handshake flow on the listener:;;;;;; 1. Read bytes from the client until you have the full HTTP;;; request header block (terminated by CRLF CRLF).;;; 2. Pass the header block to `parse-handshake-request`. It;;; returns either a `ws-handshake-request` record or a;;; `ws-handshake-error` record describing what was wrong;;; (which the listener turns into a 400/426 response).;;; 3. Pass the request to `build-handshake-response` to obtain;;; the 101 Switching Protocols response. Optionally pass a;;; chosen subprotocol (one of the values from;;; `ws-handshake-request-subprotocols`).;;; 4. Write the response to the socket. Subsequent reads are;;; raw WebSocket frames; pass them to `decode-frame` and;;; reply with the unmasked `encode-server-*-frame` encoders.;;;;;; This module deliberately does not own a connection record or a;;; read loop — every server has its own per-connection abstractions.;;; What this module owns is the wire-format-correct handshake.(define-library (sigil websocket server) (import (sigil core) (sigil string) (sigil math) (sigil struct) (sigil crypto)) (export ws-guid compute-accept-key ws-handshake-request ws-handshake-request? ws-handshake-request-method ws-handshake-request-path ws-handshake-request-host ws-handshake-request-key ws-handshake-request-version ws-handshake-request-subprotocols ws-handshake-request-headers ws-handshake-request-x-forwarded-for ws-handshake-error ws-handshake-error? ws-handshake-error-status ws-handshake-error-reason parse-handshake-request handshake-request-bytes-needed? build-handshake-response build-handshake-error-response) (begin ;; ============================================================ ;; WebSocket GUID (RFC 6455 §1.3) ;; ============================================================ (define ws-guid "258EAFA5-E914-47DA-95CA-C5AB0DC85B11") ;;; Compute the Sec-WebSocket-Accept value from the client's ;;; Sec-WebSocket-Key per RFC 6455 §4.2.2: ;;; base64(SHA-1(key ++ guid)) ;;; ;;; sigil-crypto's `sha1` already returns the raw 20-byte digest ;;; as a bytevector (despite the older docstring suggesting hex — ;;; the runtime returns a bytevector for SHA family hashes that ;;; `base64-encode` accepts directly). (define (compute-accept-key key) (: string? -> string?) (base64-encode (sha1 (string-append key ws-guid)))) ;; ============================================================ ;; Handshake records ;; ============================================================ (define-struct ws-handshake-request (method) ;; string, e.g. "GET" (path) ;; string, e.g. "/ws" (host) ;; string from Host: header (key) ;; raw Sec-WebSocket-Key value (version) ;; integer (Sec-WebSocket-Version) (subprotocols) ;; list of strings (Sec-WebSocket-Protocol values) (headers) ;; alist of (lowercase-name . raw-value) (x-forwarded-for)) ;; string or #f — first value of XFF if present (define-struct ws-handshake-error (status) ;; integer HTTP status (reason)) ;; string reason phrase / message body ;; ============================================================ ;; Request parsing ;; ============================================================ ;;; Has the client sent a complete header block yet? ;;; Looks for CRLF CRLF or LF LF in the buffer. (define (handshake-request-bytes-needed? buffer) (: string? -> boolean?) (not (header-end-index buffer))) (define (header-end-index buffer) ;; Returns the index of the first byte AFTER the header block, ;; or #f if the block is not yet complete. (let ((len (string-length buffer))) (or (find-substring buffer 0 len "\r\n\r\n" 4) (find-substring buffer 0 len "\n\n" 2)))) (define (find-substring str start end needle nlen) (let loop ((i start)) (cond ((> (+ i nlen) end) #f) ((string-substring=? str i needle nlen) (+ i nlen)) (else (loop (+ i 1)))))) (define (string-substring=? haystack pos needle nlen) (let loop ((i 0)) (cond ((>= i nlen) #t) ((char=? (string-ref haystack (+ pos i)) (string-ref needle i)) (loop (+ i 1))) (else #f)))) ;;; Parse a complete handshake request. Returns either a ;;; ws-handshake-request or a ws-handshake-error. ;;; ;;; Caller is responsible for first checking that the header ;;; block is complete with `handshake-request-bytes-needed?`. (define (parse-handshake-request buffer) (: string? -> any?) (let* ((len (string-length buffer)) (end (header-end-index buffer))) (cond ((not end) (ws-handshake-error status: 400 reason: "incomplete handshake header")) (else (let* ((header-block (substring buffer 0 (header-block-end-trim buffer end))) (lines (split-header-lines header-block))) (cond ((null? lines) (ws-handshake-error status: 400 reason: "empty request")) (else (parse-request-from-lines lines)))))))) (define (header-block-end-trim buffer end) ;; `end` is the index after the terminator. Strip the ;; terminator itself so it doesn't end up in the parsed line. (cond ((and (>= end 4) (char=? (string-ref buffer (- end 4)) #\return)) (- end 4)) (else (- end 2)))) (define (split-header-lines block) (let* ((len (string-length block)) (lines '())) (let loop ((start 0) (acc '())) (let ((nl (find-newline-from block start len))) (cond ((not nl) (reverse (cond ((< start len) (cons (trim-cr (substring block start len)) acc)) (else acc)))) (else (loop (+ nl 1) (cons (trim-cr (substring block start nl)) acc)))))))) (define (find-newline-from str start end) (let loop ((i start)) (cond ((>= i end) #f) ((char=? (string-ref str i) #\newline) i) (else (loop (+ i 1)))))) (define (trim-cr s) (let ((len (string-length s))) (cond ((and (> len 0) (char=? (string-ref s (- len 1)) #\return)) (substring s 0 (- len 1))) (else s)))) (define (parse-request-from-lines lines) (let* ((request-line (car lines)) (parts (split-by-space request-line)) (method (car parts)) (path (and (pair? (cdr parts)) (cadr parts))) (proto (and (pair? (cdr parts)) (pair? (cddr parts)) (caddr parts)))) (cond ((not (and method path proto)) (ws-handshake-error status: 400 reason: "malformed request line")) ((not (string=? method "GET")) (ws-handshake-error status: 405 reason: "method not allowed; expected GET")) ((not (or (string=? proto "HTTP/1.1") (string=? proto "HTTP/1.0"))) (ws-handshake-error status: 505 reason: "HTTP version not supported")) (else (let ((headers (parse-headers (cdr lines) '()))) (validate-and-build method path headers)))))) (define (parse-headers lines acc) (cond ((null? lines) (reverse acc)) (else (let* ((line (car lines)) (colon (find-char line 0 (string-length line) #\:))) (cond ((not colon) (parse-headers (cdr lines) acc)) (else (let* ((name (string-downcase (string-trim (substring line 0 colon)))) (value (string-trim (substring line (+ colon 1) (string-length line))))) (parse-headers (cdr lines) (cons (cons name value) acc))))))))) (define (find-char s start end ch) (let loop ((i start)) (cond ((>= i end) #f) ((char=? (string-ref s i) ch) i) (else (loop (+ i 1)))))) (define (split-by-space s) (let* ((len (string-length s)) (acc '())) (let loop ((start 0) (i 0) (acc acc)) (cond ((>= i len) (reverse (if (> i start) (cons (substring s start i) acc) acc))) ((char=? (string-ref s i) #\space) (loop (+ i 1) (+ i 1) (if (> i start) (cons (substring s start i) acc) acc))) (else (loop start (+ i 1) acc)))))) (define (split-by-comma s) (let* ((len (string-length s)) (acc '())) (let loop ((start 0) (i 0) (acc acc)) (cond ((>= i len) (reverse (cond ((> i start) (cons (string-trim (substring s start i)) acc)) (else acc)))) ((char=? (string-ref s i) #\,) (loop (+ i 1) (+ i 1) (cond ((> i start) (cons (string-trim (substring s start i)) acc)) (else acc)))) (else (loop start (+ i 1) acc)))))) (define (header-value headers name) (cond ((null? headers) #f) ((string=? (caar headers) name) (cdar headers)) (else (header-value (cdr headers) name)))) (define (string-contains-ci? haystack needle) ;; Case-insensitive substring check. (let* ((hlow (string-downcase haystack)) (nlow (string-downcase needle)) (hlen (string-length hlow)) (nlen (string-length nlow))) (and (find-substring hlow 0 hlen nlow nlen) #t))) (define (validate-and-build method path headers) (let ((host (header-value headers "host")) (upgrade (header-value headers "upgrade")) (connection (header-value headers "connection")) (key (header-value headers "sec-websocket-key")) (version-str (header-value headers "sec-websocket-version")) (proto-str (header-value headers "sec-websocket-protocol")) (xff (header-value headers "x-forwarded-for"))) (cond ((not (and upgrade (string=? (string-downcase upgrade) "websocket"))) (ws-handshake-error status: 426 reason: "upgrade required: websocket")) ((not (and connection (string-contains-ci? connection "upgrade"))) (ws-handshake-error status: 400 reason: "missing Connection: Upgrade")) ((not key) (ws-handshake-error status: 400 reason: "missing Sec-WebSocket-Key")) ((or (not version-str) (not (string=? (string-trim version-str) "13"))) (ws-handshake-error status: 426 reason: "unsupported Sec-WebSocket-Version (need 13)")) (else (ws-handshake-request method: method path: path host: (or host "") key: (string-trim key) version: 13 subprotocols: (cond (proto-str (split-by-comma proto-str)) (else '())) headers: headers x-forwarded-for: (cond (xff (xff-first xff)) (else #f))))))) (define (xff-first value) ;; Take the first hop in a comma-separated XFF chain. (let* ((commas (split-by-comma value))) (cond ((null? commas) value) (else (car commas))))) ;; ============================================================ ;; Response building ;; ============================================================ ;;; Build the 101 Switching Protocols response for a successfully- ;;; validated handshake. `subprotocol`, when supplied, is echoed ;;; back in the Sec-WebSocket-Protocol header. (define (build-handshake-response request (keys: (subprotocol #f))) (: ws-handshake-request? (subprotocol: any?) -> string?) (let ((accept (compute-accept-key (ws-handshake-request-key request)))) (string-append "HTTP/1.1 101 Switching Protocols\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Accept: " accept "\r\n" (cond (subprotocol (string-append "Sec-WebSocket-Protocol: " subprotocol "\r\n")) (else "")) "\r\n"))) ;;; Build a short HTTP error response. Used when the request ;;; isn't a valid WebSocket upgrade — listeners should send the ;;; corresponding status (e.g. 400, 426) and close. (define (build-handshake-error-response error) (: ws-handshake-error? -> string?) (let* ((status (ws-handshake-error-status error)) (reason (ws-handshake-error-reason error)) (status-str (number->string status)) (body (string-append reason "\n"))) (string-append "HTTP/1.1 " status-str " " (status-phrase status) "\r\n" "Content-Type: text/plain; charset=utf-8\r\n" "Content-Length: " (number->string (string-length body)) "\r\n" "Connection: close\r\n" (cond ((= status 426) "Upgrade: websocket\r\nSec-WebSocket-Version: 13\r\n") (else "")) "\r\n" body))) (define (status-phrase status) (cond ((= status 400) "Bad Request") ((= status 405) "Method Not Allowed") ((= status 426) "Upgrade Required") ((= status 505) "HTTP Version Not Supported") (else "Error"))) ))test/test-server.sgladded
(import (sigil test) (sigil string) (sigil math) (sigil websocket frame) (sigil websocket server));; ============================================================;; compute-accept-key — RFC 6455 §1.3 test vector;; ============================================================(test-group "compute-accept-key" (test "matches RFC 6455 example" ;; Spec test vector: key "dGhlIHNhbXBsZSBub25jZQ==" produces ;; accept "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" (assert-equal "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" (compute-accept-key "dGhlIHNhbXBsZSBub25jZQ=="))));; ============================================================;; handshake-request-bytes-needed?;; ============================================================(test-group "handshake-request-bytes-needed?" (test "incomplete request needs more bytes" (assert-true (handshake-request-bytes-needed? "GET /ws HTTP/1.1\r\n")) (assert-true (handshake-request-bytes-needed? "GET /ws HTTP/1.1\r\nHost: x\r\n"))) (test "complete request does not need more" (assert-false (handshake-request-bytes-needed? (string-append "GET /ws HTTP/1.1\r\n" "Host: x\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n")))));; ============================================================;; parse-handshake-request;; ============================================================(define (good-request) (string-append "GET /ws HTTP/1.1\r\n" "Host: enclave.example\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 13\r\n" "Sec-WebSocket-Protocol: text.ircv3.net, binary.ircv3.net\r\n" "X-Forwarded-For: 198.51.100.7, 10.0.0.1\r\n" "\r\n"))(test-group "parse-handshake-request" (test "parses a valid request" (let ((req (parse-handshake-request (good-request)))) (assert-true (ws-handshake-request? req)) (assert-equal "GET" (ws-handshake-request-method req)) (assert-equal "/ws" (ws-handshake-request-path req)) (assert-equal "enclave.example" (ws-handshake-request-host req)) (assert-equal "dGhlIHNhbXBsZSBub25jZQ==" (ws-handshake-request-key req)) (assert-equal 13 (ws-handshake-request-version req)) (assert-equal '("text.ircv3.net" "binary.ircv3.net") (ws-handshake-request-subprotocols req)) (assert-equal "198.51.100.7" (ws-handshake-request-x-forwarded-for req)))) (test "missing Upgrade -> 426" (let ((req (parse-handshake-request (string-append "GET / HTTP/1.1\r\n" "Host: x\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n")))) (assert-true (ws-handshake-error? req)) (assert-equal 426 (ws-handshake-error-status req)))) (test "wrong version -> 426" (let ((req (parse-handshake-request (string-append "GET / HTTP/1.1\r\n" "Host: x\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 8\r\n" "\r\n")))) (assert-true (ws-handshake-error? req)) (assert-equal 426 (ws-handshake-error-status req)))) (test "POST -> 405" (let ((req (parse-handshake-request (string-append "POST /ws HTTP/1.1\r\n" "Host: x\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n")))) (assert-true (ws-handshake-error? req)) (assert-equal 405 (ws-handshake-error-status req)))) (test "missing key -> 400" (let ((req (parse-handshake-request (string-append "GET / HTTP/1.1\r\n" "Host: x\r\n" "Upgrade: websocket\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n")))) (assert-true (ws-handshake-error? req)) (assert-equal 400 (ws-handshake-error-status req)))));; ============================================================;; build-handshake-response;; ============================================================(test-group "build-handshake-response" (test "writes 101 with correct accept" (let* ((req (parse-handshake-request (good-request))) (resp (build-handshake-response req))) (assert-true (string-contains? resp "HTTP/1.1 101 Switching Protocols\r\n")) (assert-true (string-contains? resp "Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n")) (assert-true (string-contains? resp "Upgrade: websocket\r\n")) (assert-true (string-contains? resp "Connection: Upgrade\r\n")))) (test "echoes negotiated subprotocol" (let* ((req (parse-handshake-request (good-request))) (resp (build-handshake-response req subprotocol: "text.ircv3.net"))) (assert-true (string-contains? resp "Sec-WebSocket-Protocol: text.ircv3.net\r\n")))));; ============================================================;; build-handshake-error-response;; ============================================================(test-group "build-handshake-error-response" (test "426 includes Upgrade hint" (let* ((err (parse-handshake-request (string-append "GET / HTTP/1.1\r\n" "Host: x\r\n" "Connection: Upgrade\r\n" "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" "Sec-WebSocket-Version: 13\r\n" "\r\n"))) (resp (build-handshake-error-response err))) (assert-true (string-contains? resp "HTTP/1.1 426")) (assert-true (string-contains? resp "Upgrade: websocket\r\n")) (assert-true (string-contains? resp "Sec-WebSocket-Version: 13\r\n")))));; ============================================================;; Server-side frame encoders are unmasked;; ============================================================(test-group "server-side frame encoders" (test "encode-server-text-frame is unmasked and round-trips" (let* ((encoded (encode-server-text-frame "PRIVMSG #hive :hi")) (result (decode-frame encoded)) (frame (frame-decode-result-frame result))) (assert-true (ws-frame? frame)) (assert-equal opcode-text (ws-frame-opcode frame)) (assert-true (ws-frame-fin? frame)) ;; Mask bit must be 0 — verify by checking byte 1's high bit. (assert-equal 0 (bitwise-and (bytevector-u8-ref encoded 1) #x80)))) (test "encode-server-pong-frame is unmasked" (let ((encoded (encode-server-pong-frame (make-bytevector 0)))) (assert-equal opcode-pong (bitwise-and (bytevector-u8-ref encoded 0) #x0F)) (assert-equal 0 (bitwise-and (bytevector-u8-ref encoded 1) #x80)))) (test "encode-server-close-frame includes status code" (let* ((encoded (encode-server-close-frame 1000)) (result (decode-frame encoded)) (frame (frame-decode-result-frame result)) (payload (ws-frame-payload frame))) (assert-equal opcode-close (ws-frame-opcode frame)) (assert-equal 2 (bytevector-length payload)) (assert-equal #x03 (bytevector-u8-ref payload 0)) ; 1000 >> 8 (assert-equal #xE8 (bytevector-u8-ref payload 1))))) ; 1000 & 0xFF(run-tests)