code wiki / (root) / nx_websocket_client_upgrade.nx

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}