- rename the two libraries: writeonce-framework -> writeonce-serve
(`use serve`), wo-html -> writeonce-view (`use view`). Names say the
ROLE now; every sample, script, gate and live doc follows
- stories/specs/plans keep the old names: they are dated records, and
both library READMEs carry a "renamed 2026-08-25" note
- serve/http/files.wo: StaticFiles { dir, max_bytes } — traversal
refused not normalised, extension content types, attachment
disposition for archives. Lifted out of the shop, which had said in
a comment that it belonged in the framework
- shop drops its private copy and mounts the framework's
- site: /dl/*path over $WO_DIST (default ./dist), 16 MiB ceiling
- /install gains supported systems — Linux x86-64, glibc >= 2.38,
not musl — read off `file` and the binaries' GLIBC_ symbol
versions, not off a wish list; plus GitHub release as primary,
/dl as mirror, and the sha256 verify step
- site-accept: 17 -> 21 checks (supported systems, gzip download with
a binary-safe probe, checksum, /dl traversal 404)
Verified on 192.168.0.165: the real 960,820-byte tarball downloads
as application/gzip and its sha256 matches the published digest.
Gates: oop-accept MET, site 21/0, web-app 46/0, fibers 10/0,
db-actor 8/0; shop rebuilt and its /assets served by the framework.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 lines
3.7 KiB
Text
112 lines
3.7 KiB
Text
-- http/wsframe.wo — the RFC 6455 frame codec (iteration 24), pure
|
|
-- functions over Text (net.read/net.write speak Text; a wo Text is
|
|
-- binary-safe bytes). No fd, no actor — the reader owns a carry buffer
|
|
-- exactly like internal/parse.wo's keep-alive carry: append what
|
|
-- net.read returned, call ws_parse, act on the frame, keep `rest`.
|
|
--
|
|
-- v1 bounds, all deliberate (spec 2026-08-23): client frames MUST be
|
|
-- masked (RFC), fragmentation is refused (kind -1 — answer a close),
|
|
-- 64-bit payload lengths are refused, payloads cap at 1 MiB (the
|
|
-- BODY_MAX doctrine). Binary frames parse fine; what an app does with
|
|
-- them is its business (chat echoes).
|
|
--
|
|
-- Frame.kind: 0 = incomplete (feed more bytes), 1 = text, 2 = binary,
|
|
-- 8 = close, 9 = ping, 10 = pong, 0 - 1 = protocol error (close the
|
|
-- peer). kind mirrors the wire opcode for real frames.
|
|
|
|
pub typedef Frame = {
|
|
kind: Int,
|
|
payload: Text,
|
|
rest: Text
|
|
}
|
|
|
|
const WSF_MAX = 1048576
|
|
|
|
fn incomplete() -> Frame {
|
|
return Frame { kind: 0, payload: "", rest: "" };
|
|
}
|
|
|
|
fn protocol_error() -> Frame {
|
|
return Frame { kind: 0 - 1, payload: "", rest: "" };
|
|
}
|
|
|
|
-- Parse ONE complete client frame off the front of buf. Anything short
|
|
-- is `incomplete` — never an error, the bytes just have not arrived.
|
|
pub fn ws_parse(buf: Text) -> Frame {
|
|
if len(buf) < 2 { return incomplete(); }
|
|
let b0 = byte_at(buf, 0);
|
|
let b1 = byte_at(buf, 1);
|
|
let fin = b0 & 0x80;
|
|
let op = b0 & 0x0F;
|
|
if b0 & 0x70 != 0 { return protocol_error(); } -- RSV bits: no extension negotiated
|
|
if op == 0 or fin == 0 { return protocol_error(); } -- fragmentation refused, v1
|
|
if op != 1 and op != 2 and op != 8 and op != 9 and op != 10 {
|
|
return protocol_error();
|
|
}
|
|
if b1 & 0x80 == 0 { return protocol_error(); } -- a client frame must be masked
|
|
let plen = b1 & 0x7F;
|
|
let off = 2;
|
|
if plen == 127 { return protocol_error(); } -- 64-bit lengths refused, v1
|
|
if plen == 126 {
|
|
if len(buf) < 4 { return incomplete(); }
|
|
plen = (byte_at(buf, 2) << 8) | byte_at(buf, 3);
|
|
off = 4;
|
|
}
|
|
if plen > WSF_MAX { return protocol_error(); }
|
|
-- control frames are short by RFC (§5.5): <= 125 and never fragmented
|
|
if op >= 8 and plen > 125 { return protocol_error(); }
|
|
if len(buf) < off + 4 + plen { return incomplete(); }
|
|
let k0 = byte_at(buf, off);
|
|
let k1 = byte_at(buf, off + 1);
|
|
let k2 = byte_at(buf, off + 2);
|
|
let k3 = byte_at(buf, off + 3);
|
|
let data = off + 4;
|
|
-- unmask: XOR each payload byte with key[i % 4]; parts + one join
|
|
-- keeps the build linear instead of concat-quadratic
|
|
let parts: multi Text = [];
|
|
let i = 0;
|
|
while i < plen {
|
|
let k = k0;
|
|
let m = i % 4;
|
|
if m == 1 { k = k1; }
|
|
if m == 2 { k = k2; }
|
|
if m == 3 { k = k3; }
|
|
push(parts, char_of(byte_at(buf, data + i) ^ k));
|
|
i = i + 1;
|
|
}
|
|
let payload = join(parts, "");
|
|
let end = data + plen;
|
|
return Frame { kind: op, payload: payload,
|
|
rest: substr(buf, end, len(buf) - end) };
|
|
}
|
|
|
|
-- Serialize a SERVER frame: unmasked by RFC (§5.1 — only clients mask).
|
|
-- Payloads above 64 KiB are refused with "" — the framework never sends
|
|
-- them (chat lines are short; the 16-bit length form is the v1 ceiling).
|
|
fn ws_ser(op: Int, payload: Text) -> Text {
|
|
let n = len(payload);
|
|
if n > 65535 { return ""; }
|
|
let head = char_of(0x80 | op);
|
|
if n < 126 {
|
|
head = head .. char_of(n);
|
|
} else {
|
|
head = head .. char_of(126) .. char_of(n >> 8) .. char_of(n & 0xFF);
|
|
}
|
|
return head .. payload;
|
|
}
|
|
|
|
pub fn ws_text(payload: Text) -> Text {
|
|
return ws_ser(1, payload);
|
|
}
|
|
|
|
pub fn ws_close() -> Text {
|
|
return ws_ser(8, "");
|
|
}
|
|
|
|
pub fn ws_ping() -> Text {
|
|
return ws_ser(9, "");
|
|
}
|
|
|
|
pub fn ws_pong(payload: Text) -> Text {
|
|
return ws_ser(10, payload);
|
|
}
|