nx_websocket_client_upgrade.nx source
↩ module page · 444 lines · 14860 B
1// nx_websocket_client_upgrade.nx -- CLIENT-side WebSocket HTTP upgrade.
2//
3// Sibling to nx_websocket_upgrade.nx (server-side). Sends the
4// HTTP/1.1 GET + Upgrade: websocket request on an already-connected
5// TCP fd, reads the server's response headers, validates the 101
6// status, the Upgrade/Connection headers, and the Sec-WebSocket-Accept
7// value against the SHA-1 + Base64 transform of the key we sent.
8//
9// RFC 6455 §1.3 + §4.1 + §4.2 client side. After this returns OK,
10// the caller can use nx_websocket_stream's framer (just like the
11// server side does post-upgrade).
12//
13// Request bytes built (CRLF line endings):
14// GET <path> HTTP/1.1\r\n
15// Host: <host>\r\n
16// Upgrade: websocket\r\n
17// Connection: Upgrade\r\n
18// Sec-WebSocket-Key: <24-char base64 of 16 random bytes>\r\n
19// Sec-WebSocket-Version: 13\r\n
20// \r\n
21//
22// Response validation (RFC 6455 §4.2.2 + §4.1):
23// Status-line MUST begin "HTTP/1.1 101"
24// Upgrade: websocket (case-insensitive)
25// Connection: Upgrade (case-insensitive, may be comma-list)
26// Sec-WebSocket-Accept: <expected> (matches our key per §1.3 algo)
27//
28// Composes:
29// nx_syscalls (sys_read / sys_write)
30// nx_websocket_handshake (ws_accept = SHA-1 + Base64 of key||GUID)
31// nx_rand (rand_bytes for 16-byte client key entropy)
32// nx_base64 (b64_encode for the 16-byte key)
33//
34// Per Cardinals 9 (single-responsibility: ONE upgrade per call), 12
35// (defensive at boundary: every required header validated, status
36// strictly == 101), 22 (composition over re-implementation: ws_accept
37// is the authoritative SHA-1+Base64 transform), 14 (graceful
38// degradation: distinct sealed-enum verdicts for each failure mode
39// so caller can act differently on TIMEOUT vs BAD_STATUS).
40//
41// genealogy_id: rfc_6455_client + nx_websocket_handshake + nx_websocket_upgrade
42// lineage_id: nishi_websocket_client_upgrade_q1
43
44// nx_safety_envelope:
45// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
46// sil_target: SIL1
47// evidence: [bulk_applied_2026-05-20, client-side-ws-upgrade]
48// verdict: NOT_YET_EVALUATED
49
50import "nx_syscalls.nx"
51import "nx_websocket_handshake.nx"
52import "nx_base64.nx"
53import "nx_rand.nx"
54
55const NX_WSCU_OK: i64 = 1
56const NX_WSCU_WRITE_FAIL: i64 = 2
57const NX_WSCU_READ_FAIL: i64 = 3
58const NX_WSCU_HEADERS_TOO_BIG: i64 = 4
59const NX_WSCU_BAD_STATUS: i64 = 5 // not "HTTP/1.1 101"
60const NX_WSCU_BAD_UPGRADE: i64 = 6
61const NX_WSCU_BAD_CONNECTION: i64 = 7
62const NX_WSCU_BAD_ACCEPT: i64 = 8 // Sec-WebSocket-Accept mismatch
63const NX_WSCU_KEY_TOO_BIG: i64 = 9 // host/path overflow scratch
64const NX_WSCU_VERDICT_N: i64 = 10
65
66func nx_wscu_verdict_is_valid(v: i64) -> i64 {
67 if v < NX_WSCU_OK { return 0 }
68 if v >= NX_WSCU_VERDICT_N { return 0 }
69 return 1
70}
71
72// ASCII tolower for [A-Z]; passes everything else through.
73func _wscu_tolower(c: u8) -> u8 {
74 let ci: i64 = c as i64
75 if ci >= 65 {
76 if ci <= 90 { return (ci + 32) as u8 }
77 }
78 return c
79}
80
81// Case-insensitive prefix-match.
82func _wscu_eq_ci(buf: *u8, off: i64, end: i64, lit: *u8, lit_n: i64) -> i64 {
83 if off + lit_n > end { return 0 }
84 var i: i64 = 0
85 while i < lit_n {
86 let a: u8 = _wscu_tolower(buf[off + i])
87 let b: u8 = _wscu_tolower(lit[i])
88 if a != b { return 0 }
89 i = i + 1
90 }
91 return 1
92}
93
94// Case-sensitive prefix-match.
95func _wscu_eq_cs(buf: *u8, off: i64, end: i64, lit: *u8, lit_n: i64) -> i64 {
96 if off + lit_n > end { return 0 }
97 var i: i64 = 0
98 while i < lit_n {
99 if buf[off + i] != lit[i] { return 0 }
100 i = i + 1
101 }
102 return 1
103}
104
105// Strict bytewise equality.
106func _wscu_bytes_eq(a: *u8, b: *u8, n: i64) -> i64 {
107 var i: i64 = 0
108 while i < n {
109 if a[i] != b[i] { return 0 }
110 i = i + 1
111 }
112 return 1
113}
114
115// Find header-block terminator "\r\n\r\n" in buf[0..n). Returns
116// offset of byte AFTER the terminator, or -1.
117func _wscu_find_header_end(buf: *u8, n: i64) -> i64 {
118 if n < 4 { return 0 - 1 }
119 var i: i64 = 0
120 while i + 3 < n {
121 if buf[i] == 13 as u8 {
122 if buf[i + 1] == 10 as u8 {
123 if buf[i + 2] == 13 as u8 {
124 if buf[i + 3] == 10 as u8 { return i + 4 }
125 }
126 }
127 }
128 i = i + 1
129 }
130 return 0 - 1
131}
132
133// Read response headers (until "\r\n\r\n") into buf, bounded by cap.
134// Returns bytes read or -1.
135func _wscu_read_headers(fd: i64, buf: *u8, cap: i64) -> i64 {
136 var off: i64 = 0
137 while off < cap {
138 let r: i64 = sys_read(fd, (buf as i64 + off) as *u8, cap - off)
139 if r <= 0 { return 0 - 1 }
140 off = off + r
141 if _wscu_find_header_end(buf, off) >= 0 { return off }
142 }
143 return 0 - 1
144}
145
146// Write all bytes via looping sys_write. Returns 0 / -1.
147func _wscu_write_n(fd: i64, buf: *u8, n: i64) -> i64 {
148 var off: i64 = 0
149 while off < n {
150 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off)
151 if w <= 0 { return 0 - 1 }
152 off = off + w
153 }
154 return 0
155}
156
157// Advance past next \n in buf[start..end).
158func _wscu_next_line(buf: *u8, start: i64, end: i64) -> i64 {
159 var i: i64 = start
160 while i < end {
161 if buf[i] == 10 as u8 { return i + 1 }
162 i = i + 1
163 }
164 return end
165}
166
167// Skip SP/HT at start.
168func _wscu_skip_ws(buf: *u8, start: i64, end: i64) -> i64 {
169 var i: i64 = start
170 while i < end {
171 let c: u8 = buf[i]
172 if c == 32 as u8 { i = i + 1 }
173 else {
174 if c == 9 as u8 { i = i + 1 }
175 else { return i }
176 }
177 }
178 return i
179}
180
181// Find next \r or \n; return end if absent.
182func _wscu_eol(buf: *u8, start: i64, end: i64) -> i64 {
183 var i: i64 = start
184 while i < end {
185 let c: u8 = buf[i]
186 if c == 13 as u8 { return i }
187 if c == 10 as u8 { return i }
188 i = i + 1
189 }
190 return end
191}
192
193// Find a header by case-insensitive name; returns offset of VALUE
194// start (whitespace skipped, EOL stripped) or -1. Mirrors
195// nx_websocket_upgrade._wsu_find_header (server side).
196func _wscu_find_header(buf: *u8, hdr_start: i64, hdr_end: i64,
197 name: *u8, name_n: i64,
198 val_len_p: *i64) -> i64 {
199 var line_start: i64 = hdr_start
200 while line_start < hdr_end {
201 if _wscu_eq_ci(buf, line_start, hdr_end, name, name_n) == 1 {
202 if line_start + name_n < hdr_end {
203 if buf[line_start + name_n] == 58 as u8 { // ':'
204 let val_start: i64 = _wscu_skip_ws(buf, line_start + name_n + 1, hdr_end)
205 let val_end: i64 = _wscu_eol(buf, val_start, hdr_end)
206 *val_len_p = val_end - val_start
207 return val_start
208 }
209 }
210 }
211 line_start = _wscu_next_line(buf, line_start, hdr_end)
212 }
213 return 0 - 1
214}
215
216// i64 -> 3-digit ASCII (e.g. 8445 -> "8445"). Writes at out[0..n);
217// returns n. Caller ensures cap is large enough; we cap at 10 digits.
218func _wscu_itoa(n: i64, out: *u8, cap: i64) -> i64 {
219 if n <= 0 { out[0] = 48 as u8; return 1 }
220 let tmp: *u8 = sys_mmap(16)
221 var x: i64 = n
222 var i: i64 = 0
223 while x > 0 {
224 tmp[i] = ((x % 10) + 48) as u8
225 x = x / 10
226 i = i + 1
227 }
228 if i > cap { return 0 - 1 }
229 var j: i64 = 0
230 while j < i {
231 out[j] = tmp[i - 1 - j]
232 j = j + 1
233 }
234 return i
235}
236
237// Build the client request bytes into req_buf. Returns total length
238// or -1 on overflow. host_str is optional (when host_len == 0 the
239// Host header is omitted; daemons MAY tolerate that for ws:// but
240// RFC requires it -- caller should always pass a host).
241//
242// out_key MUST be 24 bytes; we write the base64 of 16 random bytes
243// there so the caller can compute the expected Sec-WebSocket-Accept.
244func nx_ws_client_build_request(
245 req_buf: *u8, req_cap: i64,
246 host: *u8, host_len: i64,
247 path: *u8, path_len: i64,
248 port: i64,
249 include_port_in_host: i64,
250 out_key24: *u8
251) -> i64 {
252 // Generate 16-byte random key, base64-encode -> 24 chars.
253 let rk: *u8 = sys_mmap(32)
254 rand_bytes(rk, 16)
255 b64_encode(rk, 16, out_key24)
256
257 var off: i64 = 0
258
259 // "GET "
260 if off + 4 > req_cap { return 0 - 1 }
261 req_buf[off] = 71 as u8; req_buf[off+1] = 69 as u8
262 req_buf[off+2] = 84 as u8; req_buf[off+3] = 32 as u8
263 off = off + 4
264
265 // path
266 if off + path_len > req_cap { return 0 - 1 }
267 var i: i64 = 0
268 while i < path_len { req_buf[off + i] = path[i]; i = i + 1 }
269 off = off + path_len
270
271 // " HTTP/1.1\r\n"
272 let httpv: *u8 = " HTTP/1.1\r\n" as *u8
273 if off + 11 > req_cap { return 0 - 1 }
274 i = 0
275 while i < 11 { req_buf[off + i] = httpv[i]; i = i + 1 }
276 off = off + 11
277
278 // "Host: " + host[:port] + "\r\n"
279 if host_len > 0 {
280 let h: *u8 = "Host: " as *u8
281 if off + 6 > req_cap { return 0 - 1 }
282 i = 0; while i < 6 { req_buf[off + i] = h[i]; i = i + 1 }
283 off = off + 6
284
285 if off + host_len > req_cap { return 0 - 1 }
286 i = 0; while i < host_len { req_buf[off + i] = host[i]; i = i + 1 }
287 off = off + host_len
288
289 if include_port_in_host == 1 {
290 if off + 1 > req_cap { return 0 - 1 }
291 req_buf[off] = 58 as u8 // ':'
292 off = off + 1
293 let pn: i64 = _wscu_itoa(port, (req_buf as i64 + off) as *u8, req_cap - off)
294 if pn < 0 { return 0 - 1 }
295 off = off + pn
296 }
297 if off + 2 > req_cap { return 0 - 1 }
298 req_buf[off] = 13 as u8; req_buf[off + 1] = 10 as u8
299 off = off + 2
300 }
301
302 // "Upgrade: websocket\r\n"
303 let upg: *u8 = "Upgrade: websocket\r\n" as *u8
304 if off + 20 > req_cap { return 0 - 1 }
305 i = 0; while i < 20 { req_buf[off + i] = upg[i]; i = i + 1 }
306 off = off + 20
307
308 // "Connection: Upgrade\r\n"
309 let con: *u8 = "Connection: Upgrade\r\n" as *u8
310 if off + 21 > req_cap { return 0 - 1 }
311 i = 0; while i < 21 { req_buf[off + i] = con[i]; i = i + 1 }
312 off = off + 21
313
314 // "Sec-WebSocket-Key: " + key24 + "\r\n"
315 let swk: *u8 = "Sec-WebSocket-Key: " as *u8
316 if off + 19 > req_cap { return 0 - 1 }
317 i = 0; while i < 19 { req_buf[off + i] = swk[i]; i = i + 1 }
318 off = off + 19
319 if off + 24 > req_cap { return 0 - 1 }
320 i = 0; while i < 24 { req_buf[off + i] = out_key24[i]; i = i + 1 }
321 off = off + 24
322 if off + 2 > req_cap { return 0 - 1 }
323 req_buf[off] = 13 as u8; req_buf[off + 1] = 10 as u8
324 off = off + 2
325
326 // "Sec-WebSocket-Version: 13\r\n"
327 let swv: *u8 = "Sec-WebSocket-Version: 13\r\n" as *u8
328 if off + 27 > req_cap { return 0 - 1 }
329 i = 0; while i < 27 { req_buf[off + i] = swv[i]; i = i + 1 }
330 off = off + 27
331
332 // "\r\n" (end of headers)
333 if off + 2 > req_cap { return 0 - 1 }
334 req_buf[off] = 13 as u8; req_buf[off + 1] = 10 as u8
335 off = off + 2
336
337 return off
338}
339
340// Validate a server response in resp_buf[0..n) given the client key
341// we sent (key24 -- 24 base64 ASCII chars). Returns NX_WSCU_OK or
342// a specific failure verdict.
343func nx_ws_client_validate_response(
344 resp_buf: *u8, n: i64,
345 key24: *u8
346) -> i64 {
347 if n < 12 { return NX_WSCU_BAD_STATUS }
348
349 // Status line MUST start with "HTTP/1.1 101"
350 let prefix: *u8 = "HTTP/1.1 101" as *u8
351 if _wscu_eq_cs(resp_buf, 0, n, prefix, 12) != 1 {
352 return NX_WSCU_BAD_STATUS
353 }
354
355 // Find end-of-status-line.
356 let sl_eol: i64 = _wscu_eol(resp_buf, 0, n)
357 if sl_eol >= n { return NX_WSCU_BAD_STATUS }
358 var hdr_start: i64 = sl_eol + 1
359 if resp_buf[sl_eol] == 13 as u8 { hdr_start = sl_eol + 2 }
360 if hdr_start >= n { return NX_WSCU_BAD_STATUS }
361
362 let hdr_end_off: i64 = _wscu_find_header_end(resp_buf, n)
363 if hdr_end_off < 0 { return NX_WSCU_HEADERS_TOO_BIG }
364 let hdr_end: i64 = hdr_end_off
365
366 let v_p: *i64 = sys_mmap(16) as *i64
367
368 // Upgrade: websocket
369 let upg_off: i64 = _wscu_find_header(resp_buf, hdr_start, hdr_end,
370 "Upgrade" as *u8, 7, v_p)
371 if upg_off < 0 { return NX_WSCU_BAD_UPGRADE }
372 if _wscu_eq_ci(resp_buf, upg_off, hdr_end, "websocket" as *u8, 9) != 1 {
373 return NX_WSCU_BAD_UPGRADE
374 }
375
376 // Connection contains "upgrade"
377 let con_off: i64 = _wscu_find_header(resp_buf, hdr_start, hdr_end,
378 "Connection" as *u8, 10, v_p)
379 if con_off < 0 { return NX_WSCU_BAD_CONNECTION }
380 let con_val_len: i64 = *v_p
381 var found: i64 = 0
382 var ci: i64 = 0
383 while ci + 7 <= con_val_len {
384 if _wscu_eq_ci(resp_buf, con_off + ci, con_off + con_val_len,
385 "upgrade" as *u8, 7) == 1 {
386 found = 1
387 ci = con_val_len
388 }
389 ci = ci + 1
390 }
391 if found != 1 { return NX_WSCU_BAD_CONNECTION }
392
393 // Sec-WebSocket-Accept matches SHA-1+Base64(key||GUID)
394 let acc_off: i64 = _wscu_find_header(resp_buf, hdr_start, hdr_end,
395 "Sec-WebSocket-Accept" as *u8, 20, v_p)
396 if acc_off < 0 { return NX_WSCU_BAD_ACCEPT }
397 if *v_p != 28 { return NX_WSCU_BAD_ACCEPT }
398
399 let expected: *u8 = sys_mmap(32)
400 let en: i64 = ws_accept(key24, 24, expected)
401 if en != 28 { return NX_WSCU_BAD_ACCEPT }
402
403 if _wscu_bytes_eq((resp_buf as i64 + acc_off) as *u8, expected, 28) != 1 {
404 return NX_WSCU_BAD_ACCEPT
405 }
406
407 return NX_WSCU_OK
408}
409
410// Top-level orchestrator: assumes fd is an already-connected TCP
411// socket to host:port. Writes the client request, reads the
412// response, validates. On OK any bytes already received PAST the
413// header terminator are NOT preserved -- callers wanting that case
414// must use the lower-level build/validate primitives directly. v0
415// signaling daemons send the 101 alone (no body) so this is fine.
416func nx_ws_client_upgrade(
417 fd: i64,
418 host: *u8, host_len: i64,
419 path: *u8, path_len: i64,
420 port: i64,
421 include_port_in_host: i64,
422 req_buf: *u8, req_cap: i64,
423 resp_buf: *u8, resp_cap: i64
424) -> i64 {
425 let key24: *u8 = sys_mmap(32)
426
427 let rn: i64 = nx_ws_client_build_request(req_buf, req_cap,
428 host, host_len,
429 path, path_len,
430 port, include_port_in_host,
431 key24)
432 if rn < 0 { return NX_WSCU_KEY_TOO_BIG }
433
434 if _wscu_write_n(fd, req_buf, rn) < 0 { return NX_WSCU_WRITE_FAIL }
435
436 let hn: i64 = _wscu_read_headers(fd, resp_buf, resp_cap)
437 if hn < 0 { return NX_WSCU_READ_FAIL }
438
439 return nx_ws_client_validate_response(resp_buf, hn, key24)
440}
441
442func main() -> i64 {
443 return 0
444}