code wiki / (root) / nx_websocket.nx

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}