nx_websocket.nx source
↩ module page · 303 lines · 11469 B
1// nx_websocket.nx -- RFC 6455 server-side WebSocket primitive.
2// Pure NishiLang. No external library. Composes nx_sha1 and
3// nx_base64 (both already shipped) for the handshake; implements
4// the binary framing (opcodes, masking, payload-length variants)
5// from the RFC text.
6//
7// Server-side flow:
8// 1. Client sends:
9// GET /ws HTTP/1.1
10// Upgrade: websocket
11// Connection: Upgrade
12// Sec-WebSocket-Key: <base64 16-byte nonce>
13// Sec-WebSocket-Version: 13
14// We compute Sec-WebSocket-Accept = base64(sha1(key + GUID))
15// where GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455
16// section 1.3).
17// 2. Reply:
18// HTTP/1.1 101 Switching Protocols
19// Upgrade: websocket
20// Connection: Upgrade
21// Sec-WebSocket-Accept: <accept>
22// 3. Subsequent traffic is binary frames.
23//
24// Frame layout (most-common case; mask MUST be present for client->server):
25// [byte 0] FIN (1) | RSV (3) | OPCODE (4)
26// [byte 1] MASK (1) | LEN7 (7)
27// LEN7 in 0..125: payload length is LEN7
28// LEN7 == 126: next 2 bytes (big-endian) are payload length
29// LEN7 == 127: next 8 bytes (big-endian) are payload length
30// [4 bytes] masking-key (when MASK=1)
31// [N bytes] payload (XORed with masking-key when MASK=1)
32//
33// Common opcodes:
34// 0x0 continuation
35// 0x1 text
36// 0x2 binary
37// 0x8 close
38// 0x9 ping
39// 0xA pong
40//
41// Server -> client frames omit the mask (MASK=0; no key bytes).
42//
43// genealogy_id: rfc_6455_websocket + nx_sha1 + nx_base64 + nx_http_io_q10
44// lineage_id: nishi_websocket_q10
45
46// nx_safety_envelope:
47// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
48// sil_target: SIL1
49// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
50// verdict: NOT_YET_EVALUATED
51
52import "nx_syscalls_x86_64.nx"
53import "nx_ws_crypto.nx"
54
55// Sealed verdicts.
56const NX_WS_VERDICT_UNKNOWN: i64 = 0
57const NX_WS_VERDICT_OK: i64 = 1
58const NX_WS_VERDICT_BAD_HANDSHAKE: i64 = 2
59const NX_WS_VERDICT_BUF_TOO_SMALL: i64 = 3
60const NX_WS_VERDICT_TRUNCATED: i64 = 4
61const NX_WS_VERDICT_UNSUPPORTED_FRAME: i64 = 5
62const NX_WS_VERDICT_BAD_PARAMS: i64 = 6
63const NX_WS_VERDICT_N: i64 = 7
64
65const NX_WS_OPCODE_CONT: i64 = 0
66const NX_WS_OPCODE_TEXT: i64 = 1
67const NX_WS_OPCODE_BINARY: i64 = 2
68const NX_WS_OPCODE_CLOSE: i64 = 8
69const NX_WS_OPCODE_PING: i64 = 9
70const NX_WS_OPCODE_PONG: i64 = 10
71
72const NX_WS_FIN_BIT: i64 = 128
73const NX_WS_MASK_BIT: i64 = 128
74
75// RFC 6455 GUID (36 ASCII chars). Appended to client key before SHA-1.
76func _ws_guid(buf: *u8) -> i64 {
77 buf[0]=50; buf[1]=53; buf[2]=56; buf[3]=69; buf[4]=65 // "258EA"
78 buf[5]=70; buf[6]=65; buf[7]=53 // "FA5"
79 buf[8]=45 // "-"
80 buf[9]=69; buf[10]=57; buf[11]=49; buf[12]=52 // "E914"
81 buf[13]=45 // "-"
82 buf[14]=52; buf[15]=55; buf[16]=68; buf[17]=65 // "47DA"
83 buf[18]=45 // "-"
84 buf[19]=57; buf[20]=53; buf[21]=67; buf[22]=65 // "95CA"
85 buf[23]=45 // "-"
86 buf[24]=67; buf[25]=53; buf[26]=65; buf[27]=66 // "C5AB"
87 buf[28]=48; buf[29]=68; buf[30]=67; buf[31]=56 // "0DC8"
88 buf[32]=53; buf[33]=66; buf[34]=49; buf[35]=49 // "5B11"
89 return 36
90}
91
92// Write the 36-byte GUID literal into combined[k..k+36]. Inlined
93// constant stores -- workaround for an x86_64 peephole that
94// miscompiles `combined[key_len + j] = guid_buf[j]` (variable+variable
95// indexing on an alloca pointer through a loop).
96func _ws_write_guid_at(combined: *u8, k: i64) -> i64 {
97 combined[k+ 0]=50; combined[k+ 1]=53; combined[k+ 2]=56
98 combined[k+ 3]=69; combined[k+ 4]=65; combined[k+ 5]=70
99 combined[k+ 6]=65; combined[k+ 7]=53
100 combined[k+ 8]=45
101 combined[k+ 9]=69; combined[k+10]=57; combined[k+11]=49; combined[k+12]=52
102 combined[k+13]=45
103 combined[k+14]=52; combined[k+15]=55; combined[k+16]=68; combined[k+17]=65
104 combined[k+18]=45
105 combined[k+19]=57; combined[k+20]=53; combined[k+21]=67; combined[k+22]=65
106 combined[k+23]=45
107 combined[k+24]=67; combined[k+25]=53; combined[k+26]=65; combined[k+27]=66
108 combined[k+28]=48; combined[k+29]=68; combined[k+30]=67; combined[k+31]=56
109 combined[k+32]=53; combined[k+33]=66; combined[k+34]=49; combined[k+35]=49
110 return 36
111}
112
113// Byte-copy helper. Isolated so the alloca-pointer peephole can't
114// fold it into a destination caller's frame.
115func _ws_copy(dst: *u8, src: *u8, n: i64) -> i64 {
116 var i: i64 = 0
117 while i < n {
118 dst[i] = src[i]
119 i = i + 1
120 }
121 return n
122}
123
124// Compute the Sec-WebSocket-Accept value given the client's
125// Sec-WebSocket-Key string. Writes the base64 result into accept_out
126// (28 bytes always). Returns sealed verdict.
127func nx_ws_accept_key(client_key: *u8, key_len: i64,
128 accept_out: *u8) -> i64 {
129 if key_len < 16 { return NX_WS_VERDICT_BAD_HANDSHAKE }
130 if key_len > 64 { return NX_WS_VERDICT_BAD_HANDSHAKE }
131 let combined: *u8 = sys_mmap(128)
132 _ws_copy(combined, client_key, key_len)
133 _ws_write_guid_at(combined, key_len)
134 let total: i64 = key_len + 36
135
136 let digest: *u8 = sys_mmap(32)
137 nx_wsx_sha1(combined, total, digest)
138
139 // base64-encode 20 raw bytes -> 28 ASCII chars (including "=" pad).
140 nx_wsx_b64_encode(digest, 20, accept_out)
141 return NX_WS_VERDICT_OK
142}
143
144// Build an HTTP/1.1 101 handshake response body into out_buf.
145// Caller passes the already-computed accept value (28 bytes).
146// Returns total bytes written, or -1 on overflow.
147func nx_ws_build_handshake_response(out_buf: *u8, out_cap: i64,
148 accept: *u8) -> i64 {
149 let needed: i64 = 130
150 if out_cap < needed { return -1 }
151 var off: i64 = 0
152 // "HTTP/1.1 101 Switching Protocols\r\n"
153 let s1: *u8 = sys_mmap(64)
154 s1[0]=72; s1[1]=84; s1[2]=84; s1[3]=80; s1[4]=47; s1[5]=49; s1[6]=46; s1[7]=49
155 s1[8]=32; s1[9]=49; s1[10]=48; s1[11]=49; s1[12]=32
156 s1[13]=83; s1[14]=119; s1[15]=105; s1[16]=116; s1[17]=99; s1[18]=104; s1[19]=105; s1[20]=110; s1[21]=103
157 s1[22]=32; s1[23]=80; s1[24]=114; s1[25]=111; s1[26]=116; s1[27]=111; s1[28]=99; s1[29]=111; s1[30]=108; s1[31]=115
158 s1[32]=13; s1[33]=10
159 var i: i64 = 0
160 while i < 34 { out_buf[off + i] = s1[i]; i = i + 1 }
161 off = off + 34
162 // "Upgrade: websocket\r\n"
163 let s2: *u8 = sys_mmap(32)
164 s2[0]=85; s2[1]=112; s2[2]=103; s2[3]=114; s2[4]=97; s2[5]=100; s2[6]=101
165 s2[7]=58; s2[8]=32
166 s2[9]=119; s2[10]=101; s2[11]=98; s2[12]=115; s2[13]=111; s2[14]=99; s2[15]=107; s2[16]=101; s2[17]=116
167 s2[18]=13; s2[19]=10
168 i = 0
169 while i < 20 { out_buf[off + i] = s2[i]; i = i + 1 }
170 off = off + 20
171 // "Connection: Upgrade\r\n"
172 let s3: *u8 = sys_mmap(32)
173 s3[0]=67; s3[1]=111; s3[2]=110; s3[3]=110; s3[4]=101; s3[5]=99; s3[6]=116; s3[7]=105; s3[8]=111; s3[9]=110
174 s3[10]=58; s3[11]=32
175 s3[12]=85; s3[13]=112; s3[14]=103; s3[15]=114; s3[16]=97; s3[17]=100; s3[18]=101
176 s3[19]=13; s3[20]=10
177 i = 0
178 while i < 21 { out_buf[off + i] = s3[i]; i = i + 1 }
179 off = off + 21
180 // "Sec-WebSocket-Accept: "
181 let s4: *u8 = sys_mmap(32)
182 s4[0]=83; s4[1]=101; s4[2]=99; s4[3]=45 // "Sec-"
183 s4[4]=87; s4[5]=101; s4[6]=98; s4[7]=83; s4[8]=111; s4[9]=99; s4[10]=107; s4[11]=101; s4[12]=116 // "WebSocket"
184 s4[13]=45; s4[14]=65; s4[15]=99; s4[16]=99; s4[17]=101; s4[18]=112; s4[19]=116 // "-Accept"
185 s4[20]=58; s4[21]=32 // ": "
186 i = 0
187 while i < 22 { out_buf[off + i] = s4[i]; i = i + 1 }
188 off = off + 22
189 i = 0
190 while i < 28 { out_buf[off + i] = accept[i]; i = i + 1 }
191 off = off + 28
192 out_buf[off] = 13; out_buf[off + 1] = 10 // CRLF
193 out_buf[off + 2] = 13; out_buf[off + 3] = 10 // CRLF (end of headers)
194 off = off + 4
195 return off
196}
197
198// Build a server -> client frame (no mask) carrying `payload` of
199// `payload_len` bytes with the given opcode. Returns total bytes
200// written into out_buf, or -1 on overflow.
201//
202// Frame size = header (2..10 bytes) + payload.
203func nx_ws_build_frame(out_buf: *u8, out_cap: i64,
204 opcode: i64,
205 payload: *u8, payload_len: i64) -> i64 {
206 if payload_len < 0 { return -1 }
207 var hdr_len: i64 = 2
208 if payload_len >= 126 { hdr_len = 4 }
209 if payload_len > 65535 { hdr_len = 10 }
210 if hdr_len + payload_len > out_cap { return -1 }
211
212 out_buf[0] = NX_WS_FIN_BIT | (opcode & 0x0f)
213 if payload_len < 126 {
214 out_buf[1] = payload_len & 0x7f
215 } else {
216 if payload_len <= 65535 {
217 out_buf[1] = 126
218 out_buf[2] = (payload_len >> 8) & 0xff
219 out_buf[3] = payload_len & 0xff
220 } else {
221 out_buf[1] = 127
222 // 8-byte big-endian length. We cap at i63.
223 var k: i64 = 0
224 while k < 8 {
225 out_buf[2 + k] = (payload_len >> ((7 - k) * 8)) & 0xff
226 k = k + 1
227 }
228 }
229 }
230 var i: i64 = 0
231 while i < payload_len {
232 out_buf[hdr_len + i] = payload[i]
233 i = i + 1
234 }
235 return hdr_len + payload_len
236}
237
238// Parse a single client -> server frame from in_buf[..in_len).
239// Writes:
240// *out_opcode = the 4-bit opcode
241// *out_payload_off = byte offset of the (unmasked) payload within in_buf
242// *out_payload_len = payload byte count
243// AND unmasks the payload IN PLACE (modifies in_buf).
244// Returns total bytes consumed (incl. header + payload), or sealed verdict <0.
245//
246// Caller verifies it has enough bytes (at least 2 + maybe more for length
247// + 4 for mask + payload_len).
248func nx_ws_parse_frame_inplace(
249 in_buf: *u8, in_len: i64,
250 out_opcode: *i64,
251 out_payload_off: *i64, out_payload_len: *i64
252) -> i64 {
253 if in_len < 2 { return -NX_WS_VERDICT_TRUNCATED }
254 let b0: i64 = in_buf[0]
255 let b1: i64 = in_buf[1]
256 let opcode: i64 = b0 & 0x0f
257 let masked: i64 = b1 & NX_WS_MASK_BIT
258 let len7: i64 = b1 & 0x7f
259 var off: i64 = 2
260 var plen: i64 = len7
261 if len7 == 126 {
262 if in_len < 4 { return -NX_WS_VERDICT_TRUNCATED }
263 plen = (in_buf[2] << 8) | in_buf[3]
264 off = 4
265 } else {
266 if len7 == 127 {
267 if in_len < 10 { return -NX_WS_VERDICT_TRUNCATED }
268 plen = 0
269 var k: i64 = 0
270 while k < 8 {
271 plen = (plen << 8) | in_buf[2 + k]
272 k = k + 1
273 }
274 off = 10
275 }
276 }
277
278 // RFC 6455: client->server MUST mask. Reject unmasked.
279 if masked == 0 { return -NX_WS_VERDICT_UNSUPPORTED_FRAME }
280 if off + 4 > in_len { return -NX_WS_VERDICT_TRUNCATED }
281 let mask: *u8 = (in_buf as i64 + off) as *u8
282 off = off + 4
283 if off + plen > in_len { return -NX_WS_VERDICT_TRUNCATED }
284
285 // Unmask in place.
286 var i: i64 = 0
287 while i < plen {
288 in_buf[off + i] = in_buf[off + i] ^ mask[i & 3]
289 i = i + 1
290 }
291
292 *out_opcode = opcode
293 *out_payload_off = off
294 *out_payload_len = plen
295 return off + plen
296}
297
298// Sealed-enum gate.
299func nx_ws_verdict_is_valid(v: i64) -> i64 {
300 if v < 0 { return 0 }
301 if v >= NX_WS_VERDICT_N { return 0 }
302 return 1
303}