nx_ws_client.nx source
↩ module page · 151 lines · 7297 B
1// nx_ws_client.nx -- THE CLIENT HALF OF RFC 6455, SO THE ESTATE CAN DRIVE A BROWSER WITHOUT PYTHON.
2//
3// WHY THIS EXISTS (operator standing order 2026-09-03: "i dont want py or js or sh or anything else in the
4// build lanes i want us nishilang soverign"). The estate drives headless Chromium over the DevTools Protocol
5// through nishi-ops/ac_cdp.py -- a Python file whose own header concedes the WebSocket layer is "the ~40
6// lines below". That is the last non-sovereign link in the render-measurement lane, and this file is the
7// first half of removing it.
8//
9// IT IS A SIBLING, NOT A SECOND RULER. nx_websocket.nx already implements the framing, the opcodes, the
10// payload-length variants and the accept-key derivation over nx_sha1 and nx_base64. Every constant and the
11// accept-key function are COMPOSED from it; nothing here re-implements a byte of that. What is added is only
12// the mirror image, because RFC 6455 is asymmetric by design:
13// nx_ws_build_frame builds UNMASKED (server -> client) <- incumbent
14// nx_ws_build_frame_masked builds MASKED (client -> server) <- here, RFC 6455 s5.3
15// nx_ws_parse_frame_inplace requires MASKED (server reading) <- incumbent
16// nx_ws_parse_frame_server requires UNMASKED (client reading) <- here, the mirror rule
17// It lives beside the server lib rather than inside it because nx_websocket.nx is load-bearing for the video
18// /signal lane, and the client half has no business widening a rule the server depends on.
19//
20// THE MASK KEY IS AN ARGUMENT, NOT AN INTERNAL. RFC 6455 wants the key unpredictable, but a lib that reaches
21// for entropy is a lib no gate can test deterministically. The caller supplies four bytes; the gate supplies
22// a fixed key and proves the round trip exactly. Where the key comes from is the caller's contract, and the
23// header of any production caller must say so.
24//
25// PROVEN 9/9 GREEN 2026-09-03, and the tooth that carries it needs no fixture of mine: a frame built here is
26// handed to the INCUMBENT server parser untouched and comes back byte-for-byte (39 bytes in, opcode 1, 33
27// payload), including the extended-length path at 300 bytes. The incumbent's own refusal of an unmasked
28// frame is re-checked in the same run and is unchanged.
29//
30// SCOPE, STATED SO NOBODY READS THIS AS MORE THAN IT IS: this is the WebSocket CODEC half. A full sovereign
31// replacement for ac_cdp.py additionally needs a plain TCP connect, an HTTP GET of /json/list to discover the
32// page target, the upgrade request itself, and the CDP request/response loop. Those are named rungs, not
33// done work.
34// license_tier: ORIGINAL No hw writes (Rule 26). LIB (no main).
35import "nx_syscalls_x86_64.nx"
36import "nx_websocket.nx"
37
38const WSC_MASK_BYTES: i64 = 4
39const WSC_LEN16: i64 = 126
40const WSC_LEN64: i64 = 127
41const WSC_MAX16: i64 = 65535
42const WSC_BYTE: i64 = 0xff
43const WSC_LEN7: i64 = 0x7f
44const WSC_OP: i64 = 0x0f
45const WSC_EIGHT: i64 = 8
46const WSC_SEVEN: i64 = 7
47// base64 of a 20-byte SHA-1 is always 28 characters, and nx_ws_accept_key writes exactly that many.
48const WSC_ACCEPT_LEN: i64 = 28
49
50// Build a MASKED client -> server frame. mask4 supplies the four masking bytes.
51// Returns total bytes written, or a negative sealed verdict from nx_websocket's enum.
52func nx_ws_build_frame_masked(out_buf: *u8, out_cap: i64,
53 opcode: i64,
54 payload: *u8, payload_len: i64,
55 mask4: *u8) -> i64 {
56 if payload_len < 0 { return 0 - NX_WS_VERDICT_BAD_PARAMS }
57 var hdr_len: i64 = 2
58 if payload_len >= WSC_LEN16 { hdr_len = 4 }
59 if payload_len > WSC_MAX16 { hdr_len = 10 }
60 let total: i64 = hdr_len + WSC_MASK_BYTES + payload_len
61 if total > out_cap { return 0 - NX_WS_VERDICT_BUF_TOO_SMALL }
62
63 out_buf[0] = NX_WS_FIN_BIT | (opcode & WSC_OP)
64 if payload_len < WSC_LEN16 {
65 out_buf[1] = NX_WS_MASK_BIT | (payload_len & WSC_LEN7)
66 } else {
67 if payload_len <= WSC_MAX16 {
68 out_buf[1] = NX_WS_MASK_BIT | WSC_LEN16
69 out_buf[2] = (payload_len >> WSC_EIGHT) & WSC_BYTE
70 out_buf[3] = payload_len & WSC_BYTE
71 } else {
72 out_buf[1] = NX_WS_MASK_BIT | WSC_LEN64
73 var k: i64 = 0
74 while k < WSC_EIGHT {
75 out_buf[2 + k] = (payload_len >> ((WSC_SEVEN - k) * WSC_EIGHT)) & WSC_BYTE
76 k = k + 1
77 }
78 }
79 }
80 var m: i64 = 0
81 while m < WSC_MASK_BYTES {
82 out_buf[hdr_len + m] = mask4[m]
83 m = m + 1
84 }
85 let poff: i64 = hdr_len + WSC_MASK_BYTES
86 var i: i64 = 0
87 while i < payload_len {
88 out_buf[poff + i] = payload[i] ^ mask4[i & 3]
89 i = i + 1
90 }
91 return total
92}
93
94// Parse a server -> client frame. RFC 6455 says a server MUST NOT mask, so a masked frame is REFUSED here --
95// the exact mirror of nx_ws_parse_frame_inplace, which refuses an UNmasked one. Neither is a widening of the
96// other, and a client that quietly accepted a masked frame would be accepting a frame no conforming server
97// sends.
98func nx_ws_parse_frame_server(in_buf: *u8, in_len: i64,
99 out_opcode: *i64,
100 out_payload_off: *i64, out_payload_len: *i64) -> i64 {
101 if in_len < 2 { return 0 - NX_WS_VERDICT_TRUNCATED }
102 let b0: i64 = in_buf[0]
103 let b1: i64 = in_buf[1]
104 let opcode: i64 = b0 & WSC_OP
105 let masked: i64 = b1 & NX_WS_MASK_BIT
106 let len7: i64 = b1 & WSC_LEN7
107 var off: i64 = 2
108 var plen: i64 = len7
109 if len7 == WSC_LEN16 {
110 if in_len < 4 { return 0 - NX_WS_VERDICT_TRUNCATED }
111 plen = (in_buf[2] << WSC_EIGHT) | in_buf[3]
112 off = 4
113 } else {
114 if len7 == WSC_LEN64 {
115 if in_len < 10 { return 0 - NX_WS_VERDICT_TRUNCATED }
116 plen = 0
117 var k: i64 = 0
118 while k < WSC_EIGHT {
119 plen = (plen << WSC_EIGHT) | in_buf[2 + k]
120 k = k + 1
121 }
122 off = 10
123 }
124 }
125 if masked != 0 { return 0 - NX_WS_VERDICT_UNSUPPORTED_FRAME }
126 if off + plen > in_len { return 0 - NX_WS_VERDICT_TRUNCATED }
127 *out_opcode = opcode
128 *out_payload_off = off
129 *out_payload_len = plen
130 return off + plen
131}
132
133// Verify a server's Sec-WebSocket-Accept against the key we sent, by COMPOSING the incumbent's derivation.
134// Returns 1 on match, 0 on mismatch, negative on a parameter fault. A client that skips this check will
135// happily talk to anything that answers on the port.
136func nx_ws_verify_accept(client_key: *u8, key_len: i64,
137 server_accept: *u8, accept_len: i64,
138 scratch: *u8) -> i64 {
139 if key_len <= 0 { return 0 - NX_WS_VERDICT_BAD_PARAMS }
140 if accept_len != WSC_ACCEPT_LEN { return 0 }
141 // the incumbent returns a sealed VERDICT, not a length, and always writes exactly 28 base64 characters
142 // (20 SHA-1 bytes). Reading its return as a length is the mistake this comment exists to stop.
143 let v: i64 = nx_ws_accept_key(client_key, key_len, scratch)
144 if v != NX_WS_VERDICT_OK { return 0 - NX_WS_VERDICT_BAD_HANDSHAKE }
145 var i: i64 = 0
146 while i < WSC_ACCEPT_LEN {
147 if scratch[i] != server_accept[i] { return 0 }
148 i = i + 1
149 }
150 return 1
151}