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}