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}