code wiki / (root) / nx_websocket_upgrade.nx

nx_websocket_upgrade.nx source

↩ module page · 332 lines · 12087 B

1// nx_websocket_upgrade.nx -- server-side WebSocket HTTP upgrade driver. 2// 3// RFC 6455 §4.1 + §4.2. Reads the client's HTTP/1.1 GET + 4// Upgrade: websocket request from a TCP fd, validates the required 5// headers, computes Sec-WebSocket-Accept via the KAT'd 6// nx_websocket_handshake module, and writes the 101 Switching 7// Protocols response back to the fd. After this returns OK, the 8// caller can use nx_websocket_stream's framer for the binary 9// channel. 10// 11// Required client headers (RFC 6455 §4.1): 12// GET <path> HTTP/1.1 13// Host: <host> (RFC allows any host; we don't gate) 14// Upgrade: websocket (case-insensitive match required) 15// Connection: Upgrade (case-insensitive; may also be a list) 16// Sec-WebSocket-Key: <24-char base64 of 16 random bytes> 17// Sec-WebSocket-Version: 13 (we only support 13 per RFC 6455) 18// 19// Response (RFC 6455 §4.2.2): 20// HTTP/1.1 101 Switching Protocols 21// Upgrade: websocket 22// Connection: Upgrade 23// Sec-WebSocket-Accept: base64(sha1(key || GUID)) 24// 25// Per Cardinals 9 (single-responsibility: ONE upgrade per call), 12 26// (defensive at boundary: every header validated), 22 (composes 27// nx_websocket_handshake + nx_websocket_stream's write helper). 28// 29// Caller writes the path bytes out via out_path_buf (caller-supplied) 30// so the signaling daemon can route by URL. 31// 32// genealogy_id: rfc_6455 + nx_websocket_handshake + nx_edge_v0_read_request_q10 33// lineage_id: nishi_websocket_upgrade_q1 34 35import "nx_syscalls.nx" 36import "nx_websocket_handshake.nx" 37 38const NX_WSU_OK: i64 = 1 39const NX_WSU_READ_FAIL: i64 = 2 40const NX_WSU_BAD_METHOD: i64 = 3 41const NX_WSU_BAD_HTTP: i64 = 4 // request line malformed 42const NX_WSU_BAD_UPGRADE: i64 = 5 // missing/wrong Upgrade header 43const NX_WSU_BAD_CONNECTION: i64 = 6 // missing/wrong Connection header 44const NX_WSU_BAD_VERSION: i64 = 7 // missing/wrong Sec-WebSocket-Version 45const NX_WSU_BAD_KEY: i64 = 8 // missing/malformed Sec-WebSocket-Key 46const NX_WSU_WRITE_FAIL: i64 = 9 47const NX_WSU_HEADERS_TOO_BIG: i64 = 10 48const NX_WSU_VERDICT_N: i64 = 11 49 50func nx_wsu_verdict_is_valid(v: i64) -> i64 { 51 if v < NX_WSU_OK { return 0 } 52 if v >= NX_WSU_VERDICT_N { return 0 } 53 return 1 54} 55 56// ASCII tolower for [A-Z]; passes everything else through. 57func _wsu_tolower(c: u8) -> u8 { 58 let ci: i64 = c as i64 59 if ci >= 65 { 60 if ci <= 90 { return (ci + 32) as u8 } 61 } 62 return c 63} 64 65// Case-insensitive prefix-match: does buf[off..] start with the 66// nul-terminated literal `lit`? Returns 1 if yes, 0 if no. 67func _wsu_eq_ci(buf: *u8, off: i64, end: i64, lit: *u8, lit_n: i64) -> i64 { 68 if off + lit_n > end { return 0 } 69 var i: i64 = 0 70 while i < lit_n { 71 let a: u8 = _wsu_tolower(buf[off + i]) 72 let b: u8 = _wsu_tolower(lit[i]) 73 if a != b { return 0 } 74 i = i + 1 75 } 76 return 1 77} 78 79// Case-sensitive prefix-match (used for known-case literals like "GET "). 80func _wsu_eq_cs(buf: *u8, off: i64, end: i64, lit: *u8, lit_n: i64) -> i64 { 81 if off + lit_n > end { return 0 } 82 var i: i64 = 0 83 while i < lit_n { 84 if buf[off + i] != lit[i] { return 0 } 85 i = i + 1 86 } 87 return 1 88} 89 90// Scan for "\r\n\r\n" end-of-headers in buf[0..n). Returns the 91// offset of the byte AFTER \r\n\r\n on success, or -1 if not found. 92func _wsu_find_header_end(buf: *u8, n: i64) -> i64 { 93 if n < 4 { return 0 - 1 } 94 var i: i64 = 0 95 while i + 3 < n { 96 if buf[i] == 13 as u8 { 97 if buf[i + 1] == 10 as u8 { 98 if buf[i + 2] == 13 as u8 { 99 if buf[i + 3] == 10 as u8 { return i + 4 } 100 } 101 } 102 } 103 i = i + 1 104 } 105 return 0 - 1 106} 107 108// Read HTTP headers (up to \r\n\r\n) into buf, bounded by cap. 109// Returns total bytes read on success, or -1 on EOF/error/cap overflow. 110func _wsu_read_headers(fd: i64, buf: *u8, cap: i64) -> i64 { 111 var off: i64 = 0 112 while off < cap { 113 let r: i64 = sys_read(fd, (buf as i64 + off) as *u8, cap - off) 114 if r <= 0 { return 0 - 1 } 115 off = off + r 116 if _wsu_find_header_end(buf, off) >= 0 { return off } 117 } 118 return 0 - 1 119} 120 121// Write all bytes of buf[0..n) to fd via looping sys_write. 122// Returns 0 on success, -1 on any sys_write error. 123func _wsu_write_n(fd: i64, buf: *u8, n: i64) -> i64 { 124 var off: i64 = 0 125 while off < n { 126 let w: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off) 127 if w <= 0 { return 0 - 1 } 128 off = off + w 129 } 130 return 0 131} 132 133// Advance `start` to the byte AFTER the next \n in buf[start..end). 134// Returns the new offset, or `end` if no more lines. 135func _wsu_next_line(buf: *u8, start: i64, end: i64) -> i64 { 136 var i: i64 = start 137 while i < end { 138 if buf[i] == 10 as u8 { return i + 1 } 139 i = i + 1 140 } 141 return end 142} 143 144// Skip leading SP/HT at `start` (bounded by `end`); return the 145// offset of the first non-whitespace byte. 146func _wsu_skip_ws(buf: *u8, start: i64, end: i64) -> i64 { 147 var i: i64 = start 148 while i < end { 149 let c: u8 = buf[i] 150 if c == 32 as u8 { i = i + 1 } 151 else { 152 if c == 9 as u8 { i = i + 1 } 153 else { return i } 154 } 155 } 156 return i 157} 158 159// Find the offset of the next \r OR \n in buf[start..end); returns 160// `end` if neither found. 161func _wsu_eol(buf: *u8, start: i64, end: i64) -> i64 { 162 var i: i64 = start 163 while i < end { 164 let c: u8 = buf[i] 165 if c == 13 as u8 { return i } 166 if c == 10 as u8 { return i } 167 i = i + 1 168 } 169 return end 170} 171 172// Find the offset of the next single byte `target` in buf[start..end); 173// returns `end` if not found. 174func _wsu_find_byte(buf: *u8, start: i64, end: i64, target: u8) -> i64 { 175 var i: i64 = start 176 while i < end { 177 if buf[i] == target { return i } 178 i = i + 1 179 } 180 return end 181} 182 183// Find a header in buf[hdr_start..hdr_end) whose name (case-insensitive) 184// matches `name`. Returns the offset into buf of the VALUE start 185// (whitespace skipped, end-of-line stripped) on success with 186// *val_len_p set to the value length. Returns -1 if not found. 187func _wsu_find_header(buf: *u8, hdr_start: i64, hdr_end: i64, 188 name: *u8, name_n: i64, 189 val_len_p: *i64) -> i64 { 190 var line_start: i64 = hdr_start 191 while line_start < hdr_end { 192 if _wsu_eq_ci(buf, line_start, hdr_end, name, name_n) == 1 { 193 if line_start + name_n < hdr_end { 194 if buf[line_start + name_n] == 58 as u8 { // ':' 195 let val_start: i64 = _wsu_skip_ws(buf, line_start + name_n + 1, hdr_end) 196 let val_end: i64 = _wsu_eol(buf, val_start, hdr_end) 197 *val_len_p = val_end - val_start 198 return val_start 199 } 200 } 201 } 202 line_start = _wsu_next_line(buf, line_start, hdr_end) 203 } 204 return 0 - 1 205} 206 207// Server-side WebSocket upgrade: read client request, validate 208// required headers, send 101 reply. On OK populates: 209// out_path[0..*out_path_len_p) -- the request URL path 210// out_key[0..*out_key_len_p) -- the client's Sec-WebSocket-Key 211// (24 ASCII chars typical) 212// On NON-OK the out_* are undefined. 213func nx_ws_upgrade_handshake(fd: i64, 214 hdr_buf: *u8, hdr_cap: i64, 215 out_path: *u8, out_path_cap: i64, 216 out_path_len_p: *i64, 217 out_key: *u8, out_key_cap: i64, 218 out_key_len_p: *i64) -> i64 { 219 // Read headers. 220 let hdr_n: i64 = _wsu_read_headers(fd, hdr_buf, hdr_cap) 221 if hdr_n < 0 { 222 if hdr_n == 0 - 1 { return NX_WSU_READ_FAIL } 223 return NX_WSU_HEADERS_TOO_BIG 224 } 225 226 // Parse the request line: "GET <path> HTTP/1.1\r\n". 227 if _wsu_eq_cs(hdr_buf, 0, hdr_n, "GET " as *u8, 4) != 1 { 228 return NX_WSU_BAD_METHOD 229 } 230 let path_start: i64 = 4 231 let path_end: i64 = _wsu_find_byte(hdr_buf, path_start, hdr_n, 32 as u8) 232 if path_end >= hdr_n { return NX_WSU_BAD_HTTP } 233 let path_len: i64 = path_end - path_start 234 if path_len <= 0 { return NX_WSU_BAD_HTTP } 235 if path_len > out_path_cap { return NX_WSU_BAD_HTTP } 236 var k: i64 = 0 237 while k < path_len { 238 out_path[k] = hdr_buf[path_start + k] 239 k = k + 1 240 } 241 *out_path_len_p = path_len 242 243 // Headers start AFTER the request line's \r\n. 244 let req_eol: i64 = _wsu_eol(hdr_buf, path_end, hdr_n) 245 if req_eol >= hdr_n { return NX_WSU_BAD_HTTP } 246 var hdr_start: i64 = req_eol + 1 247 if hdr_buf[req_eol] == 13 as u8 { hdr_start = req_eol + 2 } // \r\n 248 if hdr_start >= hdr_n { return NX_WSU_BAD_HTTP } 249 250 // Validate Upgrade: websocket 251 let v_p: *i64 = sys_mmap(16) as *i64 252 let upg_off: i64 = _wsu_find_header(hdr_buf, hdr_start, hdr_n, 253 "Upgrade" as *u8, 7, v_p) 254 if upg_off < 0 { return NX_WSU_BAD_UPGRADE } 255 if _wsu_eq_ci(hdr_buf, upg_off, hdr_n, "websocket" as *u8, 9) != 1 { 256 return NX_WSU_BAD_UPGRADE 257 } 258 259 // Validate Connection: contains "upgrade" (case-insensitive) 260 let con_off: i64 = _wsu_find_header(hdr_buf, hdr_start, hdr_n, 261 "Connection" as *u8, 10, v_p) 262 if con_off < 0 { return NX_WSU_BAD_CONNECTION } 263 // The Connection header may be a comma-list; we look for "upgrade" 264 // anywhere in the value (case-insensitive). 265 let con_val_len: i64 = *v_p 266 var found_upg: i64 = 0 267 var ci: i64 = 0 268 while ci + 7 <= con_val_len { 269 if _wsu_eq_ci(hdr_buf, con_off + ci, con_off + con_val_len, 270 "upgrade" as *u8, 7) == 1 { 271 found_upg = 1 272 ci = con_val_len 273 } 274 ci = ci + 1 275 } 276 if found_upg != 1 { return NX_WSU_BAD_CONNECTION } 277 278 // Validate Sec-WebSocket-Version: 13 279 let ver_off: i64 = _wsu_find_header(hdr_buf, hdr_start, hdr_n, 280 "Sec-WebSocket-Version" as *u8, 21, v_p) 281 if ver_off < 0 { return NX_WSU_BAD_VERSION } 282 if *v_p != 2 { return NX_WSU_BAD_VERSION } 283 if hdr_buf[ver_off] != 49 as u8 { return NX_WSU_BAD_VERSION } // '1' 284 if hdr_buf[ver_off + 1] != 51 as u8 { return NX_WSU_BAD_VERSION } // '3' 285 286 // Extract Sec-WebSocket-Key 287 let key_off: i64 = _wsu_find_header(hdr_buf, hdr_start, hdr_n, 288 "Sec-WebSocket-Key" as *u8, 17, v_p) 289 if key_off < 0 { return NX_WSU_BAD_KEY } 290 let key_n: i64 = *v_p 291 if key_n < 16 { return NX_WSU_BAD_KEY } 292 if key_n > 64 { return NX_WSU_BAD_KEY } 293 if key_n > out_key_cap { return NX_WSU_BAD_KEY } 294 var ki: i64 = 0 295 while ki < key_n { 296 out_key[ki] = hdr_buf[key_off + ki] 297 ki = ki + 1 298 } 299 *out_key_len_p = key_n 300 301 // Compute Sec-WebSocket-Accept. 302 let accept_buf: *u8 = sys_mmap(32) 303 let accept_n: i64 = ws_accept(out_key, key_n, accept_buf) 304 if accept_n != WS_ACCEPT_LEN { return NX_WSU_BAD_KEY } 305 306 // Build 101 response. 307 // HTTP/1.1 101 Switching Protocols\r\n 308 // Upgrade: websocket\r\n 309 // Connection: Upgrade\r\n 310 // Sec-WebSocket-Accept: <28 bytes>\r\n 311 // \r\n 312 let resp: *u8 = sys_mmap(256) 313 let prefix: *u8 = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " as *u8 314 let prefix_n: i64 = 97 315 var ri: i64 = 0 316 while ri < prefix_n { resp[ri] = prefix[ri]; ri = ri + 1 } 317 var aj: i64 = 0 318 while aj < WS_ACCEPT_LEN { resp[ri + aj] = accept_buf[aj]; aj = aj + 1 } 319 ri = ri + WS_ACCEPT_LEN 320 resp[ri] = 13 as u8 // \r 321 resp[ri + 1] = 10 as u8 // \n 322 resp[ri + 2] = 13 as u8 // \r 323 resp[ri + 3] = 10 as u8 // \n 324 let total: i64 = ri + 4 325 326 if _wsu_write_n(fd, resp, total) < 0 { return NX_WSU_WRITE_FAIL } 327 return NX_WSU_OK 328} 329 330func main() -> i64 { 331 return 0 332}