code wiki / (root) / nx_websocket_frame.nx

nx_websocket_frame.nx source

↩ module page · 256 lines · 8926 B

1// websocket_frame.nx -- RFC 6455 WebSocket frame parser + builder. 2// 3// Parses and emits individual frames. The handshake (HTTP 4// Upgrade + Sec-WebSocket-Key + Sec-WebSocket-Accept) goes in a 5// separate websocket_handshake.nx that will build on sha1.nx + 6// base64.nx + this module. 7// 8// Frame layout (RFC 6455 ยง5.2): 9// 10// 0 1 2 3 11// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 12// +-+-+-+-+-------+-+-------------+-------------------------------+ 13// |F|R|R|R| opcode|M| Payload len | Extended payload length | 14// |I|S|S|S| (4) |A| (7) | (16/64) | 15// |N|V|V|V| |S| | (if payload len==126/127) | 16// | |1|2|3| |K| | | 17// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 18// | Extended payload length continued, if payload len == 127 | 19// + - - - - - - - - - - - - - - - +-------------------------------+ 20// | |Masking-key, if MASK set to 1 | 21// +-------------------------------+-------------------------------+ 22// | Masking-key (continued) | Payload Data | 23// +-------------------------------- - - - - - - - - - - - - - - - + 24// 25// Opcodes: 26// 0x0 continuation frame 27// 0x1 text frame (UTF-8) 28// 0x2 binary frame 29// 0x8 close 30// 0x9 ping 31// 0xA pong 32// 33// Invariants: 34// WS1 Client -> server frames MUST be masked (RFC). We set 35// this flag on encode per the `masked` arg; callers MUST 36// supply a random 4-byte mask when sending from a client. 37// WS2 Server -> client frames MUST NOT be masked. 38// WS3 Payload length encoding: 7-bit inline (0..125), or 39// 126 + 16-bit extended, or 127 + 64-bit extended. 40// WS4 Masking is reversible -- apply mask twice = identity. 41 42// nx_safety_envelope: 43// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 44// sil_target: SIL1 45// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 46// verdict: NOT_YET_EVALUATED 47 48import "nx_syscalls.nx" 49const WS_MAGIC_65536: i64 = 65536 50 51const WS_ERR_SHORT: i64 = -1 52const WS_ERR_LONG: i64 = -2 53 54const WS_OP_CONT: i64 = 0x0 55const WS_OP_TEXT: i64 = 0x1 56const WS_OP_BINARY: i64 = 0x2 57const WS_OP_CLOSE: i64 = 0x8 58const WS_OP_PING: i64 = 0x9 59const WS_OP_PONG: i64 = 0xA 60 61struct WsFrame { 62 fin: i64, 63 rsv1: i64, 64 rsv2: i64, 65 rsv3: i64, 66 opcode: i64, 67 masked: i64, 68 mask_key: i64, // 32-bit mask (host byte order) 69 payload_off: i64, // offset into original buffer 70 payload_len: i64, 71 // Total byte length of this frame in the buffer (header + payload) 72 // so caller can advance to the next frame. 73 frame_len: i64, 74} 75 76// Parse one frame starting at buf[off]. Returns 0 on success, 77// WS_ERR_SHORT if caller needs to read more bytes. 78func ws_parse_frame(buf: *u8, n: i64, off: i64, f: *WsFrame) -> i64 { 79 if n < off + 2 { return WS_ERR_SHORT } 80 let b0: i64 = buf[off] 81 let b1: i64 = buf[off + 1] 82 83 f.fin = (b0 >> 7) & 0x1 84 f.rsv1 = (b0 >> 6) & 0x1 85 f.rsv2 = (b0 >> 5) & 0x1 86 f.rsv3 = (b0 >> 4) & 0x1 87 f.opcode = b0 & 0xF 88 f.masked = (b1 >> 7) & 0x1 89 90 let len7: i64 = b1 & 0x7F 91 var cur: i64 = off + 2 92 var payload_len: i64 = len7 93 94 if len7 == 126 { 95 if n < cur + 2 { return WS_ERR_SHORT } 96 payload_len = (buf[cur] << 8) | buf[cur + 1] 97 cur = cur + 2 98 } 99 if len7 == 127 { 100 if n < cur + 8 { return WS_ERR_SHORT } 101 // 64-bit BE length. We assemble high-to-low. 102 let b2: i64 = buf[cur] 103 let b3: i64 = buf[cur + 1] 104 let b4: i64 = buf[cur + 2] 105 let b5: i64 = buf[cur + 3] 106 let b6: i64 = buf[cur + 4] 107 let b7: i64 = buf[cur + 5] 108 let b8: i64 = buf[cur + 6] 109 let b9: i64 = buf[cur + 7] 110 payload_len = (b2 << 56) | (b3 << 48) | (b4 << 40) | (b5 << 32) 111 | (b6 << 24) | (b7 << 16) | (b8 << 8) | b9 112 cur = cur + 8 113 } 114 f.payload_len = payload_len 115 116 if f.masked == 1 { 117 if n < cur + 4 { return WS_ERR_SHORT } 118 let m0: i64 = buf[cur] 119 let m1: i64 = buf[cur + 1] 120 let m2: i64 = buf[cur + 2] 121 let m3: i64 = buf[cur + 3] 122 f.mask_key = (m0 << 24) | (m1 << 16) | (m2 << 8) | m3 123 cur = cur + 4 124 } else { 125 f.mask_key = 0 126 } 127 128 f.payload_off = cur 129 f.frame_len = cur + payload_len - off 130 131 if n < off + f.frame_len { return WS_ERR_SHORT } 132 return 0 133} 134 135// Apply the 4-byte mask to payload in place. Same operation 136// masks or unmasks (XOR is self-inverse). 137func ws_apply_mask(buf: *u8, off: i64, n: i64, mask_key: i64) -> i64 { 138 let m0: i64 = (mask_key >> 24) & 0xFF 139 let m1: i64 = (mask_key >> 16) & 0xFF 140 let m2: i64 = (mask_key >> 8) & 0xFF 141 let m3: i64 = mask_key & 0xFF 142 var i: i64 = 0 143 while i < n { 144 let idx: i64 = i % 4 145 var m: i64 = 0 146 if idx == 0 { m = m0 } 147 if idx == 1 { m = m1 } 148 if idx == 2 { m = m2 } 149 if idx == 3 { m = m3 } 150 buf[off + i] = buf[off + i] ^ m 151 i = i + 1 152 } 153 return 0 154} 155 156// Build a frame header at out[0]. Payload is the caller's job to 157// append immediately after (plus mask application if masked=1). 158// Returns header length or WS_ERR_SHORT/WS_ERR_LONG. 159func ws_build_header(out: *u8, cap: i64, 160 fin: i64, opcode: i64, 161 masked: i64, mask_key: i64, 162 payload_len: i64) -> i64 { 163 if cap < 2 { return WS_ERR_SHORT } 164 if payload_len < 0 { return WS_ERR_LONG } 165 var off: i64 = 0 166 var b0: i64 = opcode & 0xF 167 if fin == 1 { b0 = b0 | 0x80 } 168 out[off] = b0 169 off = off + 1 170 171 var b1: i64 = 0 172 if masked == 1 { b1 = b1 | 0x80 } 173 if payload_len < 126 { 174 b1 = b1 | (payload_len & 0x7F) 175 out[off] = b1 176 off = off + 1 177 } else { 178 if payload_len < WS_MAGIC_65536 { 179 b1 = b1 | 126 180 out[off] = b1 181 off = off + 1 182 if cap < off + 2 { return WS_ERR_SHORT } 183 out[off] = (payload_len >> 8) & 0xFF 184 out[off + 1] = payload_len & 0xFF 185 off = off + 2 186 } else { 187 b1 = b1 | 127 188 out[off] = b1 189 off = off + 1 190 if cap < off + 8 { return WS_ERR_SHORT } 191 out[off] = (payload_len >> 56) & 0xFF 192 out[off + 1] = (payload_len >> 48) & 0xFF 193 out[off + 2] = (payload_len >> 40) & 0xFF 194 out[off + 3] = (payload_len >> 32) & 0xFF 195 out[off + 4] = (payload_len >> 24) & 0xFF 196 out[off + 5] = (payload_len >> 16) & 0xFF 197 out[off + 6] = (payload_len >> 8) & 0xFF 198 out[off + 7] = payload_len & 0xFF 199 off = off + 8 200 } 201 } 202 if masked == 1 { 203 if cap < off + 4 { return WS_ERR_SHORT } 204 out[off] = (mask_key >> 24) & 0xFF 205 out[off + 1] = (mask_key >> 16) & 0xFF 206 out[off + 2] = (mask_key >> 8) & 0xFF 207 out[off + 3] = mask_key & 0xFF 208 off = off + 4 209 } 210 return off 211} 212 213// Compile-only smoke. 214func main() -> i64 { 215 // Parse a minimal server-sent text frame: FIN=1, opcode=1, 216 // masked=0, payload_len=5, payload="hello". 217 let raw: *u8 = sys_mmap(64) 218 raw[0] = 0x81 // FIN + opcode=1 219 raw[1] = 5 // len, no mask 220 raw[2] = 0x68; raw[3] = 0x65; raw[4] = 0x6C 221 raw[5] = 0x6C; raw[6] = 0x6F // 'hello' 222 223 let f_raw: *u8 = sys_mmap(128) 224 let f: *WsFrame = f_raw as *WsFrame 225 if ws_parse_frame(raw, 7, 0, f) != 0 { return 1 } 226 if f.fin != 1 { return 2 } 227 if f.opcode != WS_OP_TEXT { return 3 } 228 if f.masked != 0 { return 4 } 229 if f.payload_len != 5 { return 5 } 230 if f.payload_off != 2 { return 6 } 231 if f.frame_len != 7 { return 7 } 232 233 // Build an equivalent frame header. 234 let out: *u8 = sys_mmap(64) 235 let hlen: i64 = ws_build_header(out, 64, 1, WS_OP_TEXT, 0, 0, 5) 236 if hlen != 2 { return 8 } 237 if out[0] != 0x81 { return 9 } 238 if out[1] != 5 { return 10 } 239 240 // Masked client frame: apply + unapply mask. 241 let pl: *u8 = sys_mmap(16) 242 pl[0] = 0x68; pl[1] = 0x65; pl[2] = 0x6C; pl[3] = 0x6C; pl[4] = 0x6F 243 let key: i64 = 0x12345678 244 ws_apply_mask(pl, 0, 5, key) 245 ws_apply_mask(pl, 0, 5, key) // self-inverse 246 if pl[0] != 0x68 { return 11 } 247 if pl[4] != 0x6F { return 12 } 248 249 // 16-bit extended length. 250 let hlen2: i64 = ws_build_header(out, 64, 1, WS_OP_BINARY, 0, 0, 1000) 251 if hlen2 != 4 { return 13 } 252 if out[1] != 126 { return 14 } 253 if out[2] != 0x03 { return 15 } // 1000 >> 8 = 3 254 if out[3] != 0xE8 { return 16 } // 1000 & 0xFF = 232 255 return 0 256}