code wiki / (root) / nx_ws_client.nx

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}