nx_http_response_parse.nx source
↩ module page · 469 lines · 17092 B
1// nx_http_response_parse.nx -- client-side HTTP/1.1 response parser.
2//
3// Parses the bytes that come back over a TLS record stream into:
4// - status_code (integer, e.g. 200)
5// - reason_offset (byte offset of reason phrase in input)
6// - reason_len
7// - headers_offset (byte offset of first header line)
8// - headers_len (bytes covering all headers, EXCLUDING the
9// blank line terminator)
10// - body_offset (byte offset of body start in input, after
11// the CRLF CRLF boundary)
12// - body_len (Content-Length value if present and decoded,
13// else 0 -- callers can dechunk explicitly via
14// nx_http_dechunk)
15// - transfer_kind (NX_HTTP_BODY_CONTENT_LENGTH | NX_HTTP_BODY_
16// CHUNKED | NX_HTTP_BODY_UNTIL_CLOSE | NX_HTTP
17// _BODY_EMPTY)
18//
19// Field accessors (case-insensitive header name match):
20// nx_http_response_header(parsed, name, name_len, val_off, val_len)
21//
22// Chunked decoder (RFC 7230 §4.1):
23// nx_http_dechunk(src, src_len, out, out_cap) -> decoded_len
24//
25// Built per F7 post-order DFS: callees defined before callers.
26// Built per F6 size discipline: <12 lets per function.
27//
28// expect_exit: 0
29// license_tier: ORIGINAL
30
31import "nx_syscalls.nx"
32
33// ===== Verdicts ===================================================
34
35const NX_HTTP_RESP_OK: i64 = 0
36const NX_HTTP_RESP_ERR_TOO_SHORT: i64 = 1
37const NX_HTTP_RESP_ERR_BAD_STATUS_LINE: i64 = 2
38const NX_HTTP_RESP_ERR_BAD_VERSION: i64 = 3
39const NX_HTTP_RESP_ERR_BAD_CODE: i64 = 4
40const NX_HTTP_RESP_ERR_NO_BODY_SEP: i64 = 5
41const NX_HTTP_RESP_ERR_HEADER_OVERFLOW: i64 = 6
42const NX_HTTP_RESP_ERR_BAD_CHUNK_SIZE: i64 = 7
43const NX_HTTP_RESP_ERR_TRUNCATED: i64 = 8
44
45const NX_HTTP_BODY_EMPTY: i64 = 0
46const NX_HTTP_BODY_CONTENT_LENGTH: i64 = 1
47const NX_HTTP_BODY_CHUNKED: i64 = 2
48const NX_HTTP_BODY_UNTIL_CLOSE: i64 = 3
49
50// CAPABILITY_COMPLETENESS: FULL for HTTP/1.1 status + headers +
51// Content-Length + chunked. MISSING_CAPABILITIES: trailer headers
52// after the last chunk (RFC 7230 §4.1.2), gzip/deflate decode
53// (separate nx_inflate primitive). These are explicit follow-on
54// primitives, NOT skip-paths.
55
56// ===== Helpers (LEAF) =============================================
57
58func nx_http_resp_is_digit(c: i64) -> i64 {
59 if c >= 0x30 { if c <= 0x39 { return 1 } }
60 return 0
61}
62
63func nx_http_resp_is_hex(c: i64) -> i64 {
64 if nx_http_resp_is_digit(c) == 1 { return 1 }
65 if c >= 0x41 { if c <= 0x46 { return 1 } } // A-F
66 if c >= 0x61 { if c <= 0x66 { return 1 } } // a-f
67 return 0
68}
69
70// ASCII case-fold to lowercase
71func nx_http_resp_lower(c: i64) -> i64 {
72 if c >= 0x41 { if c <= 0x5a { return c + 0x20 } }
73 return c
74}
75
76// hex digit (0-9, A-F, a-f) → integer value 0..15; returns -1 if not hex
77func nx_http_resp_hex_val(c: i64) -> i64 {
78 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } }
79 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } }
80 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } }
81 return 0 - 1
82}
83
84// ASCII bytes-equal case-insensitive
85func nx_http_resp_cieq(a: *u8, a_len: i64, b: *u8, b_len: i64) -> i64 {
86 if a_len != b_len { return 0 }
87 var i: i64 = 0
88 while i < a_len {
89 let ca: i64 = nx_http_resp_lower(a[i] & 0xff)
90 let cb: i64 = nx_http_resp_lower(b[i] & 0xff)
91 if ca != cb { return 0 }
92 i = i + 1
93 }
94 return 1
95}
96
97// Find next byte == c starting at off, up to src_len. Returns offset
98// or -1 if not found.
99func nx_http_resp_find_byte(src: *u8, src_len: i64, off: i64, c: i64) -> i64 {
100 var i: i64 = off
101 while i < src_len {
102 if (src[i] & 0xff) == c { return i }
103 i = i + 1
104 }
105 return 0 - 1
106}
107
108// Find CRLF starting at off. Returns offset of CR or -1.
109func nx_http_resp_find_crlf(src: *u8, src_len: i64, off: i64) -> i64 {
110 var i: i64 = off
111 while i + 1 < src_len {
112 if (src[i] & 0xff) == 0x0d {
113 if (src[i + 1] & 0xff) == 0x0a { return i }
114 }
115 i = i + 1
116 }
117 return 0 - 1
118}
119
120// Find double-CRLF (end of headers). Returns offset of first CR.
121func nx_http_resp_find_crlfcrlf(src: *u8, src_len: i64, off: i64) -> i64 {
122 var i: i64 = off
123 while i + 3 < src_len {
124 if (src[i] & 0xff) == 0x0d {
125 if (src[i + 1] & 0xff) == 0x0a {
126 if (src[i + 2] & 0xff) == 0x0d {
127 if (src[i + 3] & 0xff) == 0x0a { return i }
128 }
129 }
130 }
131 i = i + 1
132 }
133 return 0 - 1
134}
135
136// Parse decimal integer at off until non-digit. Returns value, sets
137// *out_end to offset of first non-digit byte. Returns -1 if no
138// digits seen.
139func nx_http_resp_parse_dec(src: *u8, src_len: i64, off: i64, out_end: *i64) -> i64 {
140 var i: i64 = off
141 var val: i64 = 0
142 var n: i64 = 0
143 while i < src_len {
144 let c: i64 = src[i] & 0xff
145 if nx_http_resp_is_digit(c) != 1 {
146 out_end[0] = i
147 if n == 0 { return 0 - 1 }
148 return val
149 }
150 val = val * 10 + (c - 0x30)
151 n = n + 1
152 i = i + 1
153 }
154 out_end[0] = i
155 if n == 0 { return 0 - 1 }
156 return val
157}
158
159// Parse hex integer at off until non-hex. Returns value, sets
160// *out_end. Returns -1 if no hex digits.
161func nx_http_resp_parse_hex(src: *u8, src_len: i64, off: i64, out_end: *i64) -> i64 {
162 var i: i64 = off
163 var val: i64 = 0
164 var n: i64 = 0
165 while i < src_len {
166 let c: i64 = src[i] & 0xff
167 let hv: i64 = nx_http_resp_hex_val(c)
168 if hv < 0 {
169 out_end[0] = i
170 if n == 0 { return 0 - 1 }
171 return val
172 }
173 val = (val * 16) + hv
174 n = n + 1
175 i = i + 1
176 }
177 out_end[0] = i
178 if n == 0 { return 0 - 1 }
179 return val
180}
181
182// Skip horizontal whitespace (space + htab) starting at off.
183func nx_http_resp_skip_ows(src: *u8, src_len: i64, off: i64) -> i64 {
184 var i: i64 = off
185 while i < src_len {
186 let c: i64 = src[i] & 0xff
187 if c != 0x20 { if c != 0x09 { return i } }
188 i = i + 1
189 }
190 return i
191}
192
193// ===== Mid-level: status line + one header ========================
194
195// Parse "HTTP/1.1 <code> <reason>\r\n" starting at offset 0.
196// Returns verdict (NX_HTTP_RESP_OK or error). On OK, populates:
197// *out_code = status code
198// *out_reason_off = byte offset of reason phrase
199// *out_reason_len = length of reason phrase (excluding CRLF)
200// *out_next = offset of first header byte (after CRLF)
201func nx_http_resp_parse_status_line(
202 src: *u8,
203 src_len: i64,
204 out_code: *i64,
205 out_reason_off: *i64,
206 out_reason_len: *i64,
207 out_next: *i64
208) -> i64 {
209 if src_len < 12 { return NX_HTTP_RESP_ERR_TOO_SHORT }
210 // Accept HTTP/1.0 and HTTP/1.1
211 if (src[0] & 0xff) != 0x48 { return NX_HTTP_RESP_ERR_BAD_VERSION } // H
212 if (src[1] & 0xff) != 0x54 { return NX_HTTP_RESP_ERR_BAD_VERSION }
213 if (src[2] & 0xff) != 0x54 { return NX_HTTP_RESP_ERR_BAD_VERSION }
214 if (src[3] & 0xff) != 0x50 { return NX_HTTP_RESP_ERR_BAD_VERSION } // P
215 if (src[4] & 0xff) != 0x2f { return NX_HTTP_RESP_ERR_BAD_VERSION } // /
216 if (src[5] & 0xff) != 0x31 { return NX_HTTP_RESP_ERR_BAD_VERSION } // 1
217 if (src[6] & 0xff) != 0x2e { return NX_HTTP_RESP_ERR_BAD_VERSION } // .
218 let v_minor: i64 = src[7] & 0xff
219 if v_minor != 0x30 { if v_minor != 0x31 { return NX_HTTP_RESP_ERR_BAD_VERSION } }
220 if (src[8] & 0xff) != 0x20 { return NX_HTTP_RESP_ERR_BAD_STATUS_LINE }
221
222 let end_slot: *i64 = sys_mmap(8) as *i64
223 let code: i64 = nx_http_resp_parse_dec(src, src_len, 9, end_slot)
224 if code < 100 { return NX_HTTP_RESP_ERR_BAD_CODE }
225 if code > 599 { return NX_HTTP_RESP_ERR_BAD_CODE }
226 out_code[0] = code
227
228 let after_code: i64 = end_slot[0]
229 // Optional reason: zero or more bytes after a single SP up to CRLF
230 let reason_off: i64 = nx_http_resp_skip_ows(src, src_len, after_code)
231 let crlf: i64 = nx_http_resp_find_crlf(src, src_len, reason_off)
232 if crlf < 0 { return NX_HTTP_RESP_ERR_BAD_STATUS_LINE }
233 out_reason_off[0] = reason_off
234 out_reason_len[0] = crlf - reason_off
235 out_next[0] = crlf + 2
236 return NX_HTTP_RESP_OK
237}
238
239// Parse one "Name: Value\r\n" header line starting at off.
240// Returns NX_HTTP_RESP_OK on success, populating slots and *out_next
241// = offset after the trailing CRLF. Returns NX_HTTP_RESP_OK with
242// *out_name_len == 0 to signal end-of-headers (the blank line at
243// CRLF CRLF).
244func nx_http_resp_parse_header_line(
245 src: *u8,
246 src_len: i64,
247 off: i64,
248 out_name_off: *i64,
249 out_name_len: *i64,
250 out_val_off: *i64,
251 out_val_len: *i64,
252 out_next: *i64
253) -> i64 {
254 if off + 1 >= src_len { return NX_HTTP_RESP_ERR_TRUNCATED }
255 if (src[off] & 0xff) == 0x0d {
256 if (src[off + 1] & 0xff) == 0x0a {
257 out_name_len[0] = 0
258 out_next[0] = off + 2
259 return NX_HTTP_RESP_OK
260 }
261 }
262 let colon: i64 = nx_http_resp_find_byte(src, src_len, off, 0x3a)
263 if colon < 0 { return NX_HTTP_RESP_ERR_HEADER_OVERFLOW }
264 out_name_off[0] = off
265 out_name_len[0] = colon - off
266
267 let val_start: i64 = nx_http_resp_skip_ows(src, src_len, colon + 1)
268 let line_end: i64 = nx_http_resp_find_crlf(src, src_len, val_start)
269 if line_end < 0 { return NX_HTTP_RESP_ERR_HEADER_OVERFLOW }
270 out_val_off[0] = val_start
271 // Trim trailing OWS (RFC 7230 §3.2.4) -- bounded-loop pattern.
272 var v_end: i64 = line_end
273 var trimming: i64 = 1
274 while trimming == 1 {
275 if v_end <= val_start { trimming = 0 }
276 if trimming == 1 {
277 let c: i64 = src[v_end - 1] & 0xff
278 if c == 0x20 { v_end = v_end - 1 }
279 if c == 0x09 { v_end = v_end - 1 }
280 if c != 0x20 { if c != 0x09 { trimming = 0 } }
281 }
282 }
283 out_val_len[0] = v_end - val_start
284 out_next[0] = line_end + 2
285 return NX_HTTP_RESP_OK
286}
287
288// ===== High-level: full message parse =============================
289
290// NxHttpResp packed layout (we use a flat i64 array to dodge struct
291// types for now):
292// slot 0: verdict
293// slot 1: status_code
294// slot 2: reason_off
295// slot 3: reason_len
296// slot 4: headers_off
297// slot 5: headers_len
298// slot 6: body_off
299// slot 7: body_len (declared if Content-Length seen)
300// slot 8: transfer_kind
301// slot 9: content_length (parsed value, or -1 if absent)
302// slot 10: te_chunked_seen (1 if Transfer-Encoding: chunked)
303
304func nx_http_resp_alloc() -> *i64 { return sys_mmap(128) as *i64 }
305
306// Walk all headers from headers_off, classify transfer model.
307// Caller passes a pre-allocated NxHttpResp slot ptr.
308func nx_http_resp_classify_body(src: *u8, src_len: i64, r: *i64) -> i64 {
309 let no: *i64 = sys_mmap(8) as *i64
310 let nl: *i64 = sys_mmap(8) as *i64
311 let vo: *i64 = sys_mmap(8) as *i64
312 let vl: *i64 = sys_mmap(8) as *i64
313 let nx: *i64 = sys_mmap(8) as *i64
314 var pos: i64 = r[4]
315 var done: i64 = 0
316 var verdict: i64 = NX_HTTP_RESP_OK
317 while done == 0 {
318 let v: i64 = nx_http_resp_parse_header_line(src, src_len, pos, no, nl, vo, vl, nx)
319 if v != NX_HTTP_RESP_OK { verdict = v; done = 1 }
320 if done == 0 {
321 if nl[0] == 0 {
322 r[5] = pos - r[4]
323 r[6] = nx[0]
324 done = 1
325 }
326 if done == 0 {
327 // Compare name CI against "Content-Length" and "Transfer-Encoding"
328 let cl_name: *u8 = sys_mmap(16)
329 cl_name[0]=0x43; cl_name[1]=0x6f; cl_name[2]=0x6e; cl_name[3]=0x74
330 cl_name[4]=0x65; cl_name[5]=0x6e; cl_name[6]=0x74; cl_name[7]=0x2d
331 cl_name[8]=0x4c; cl_name[9]=0x65; cl_name[10]=0x6e; cl_name[11]=0x67
332 cl_name[12]=0x74; cl_name[13]=0x68
333 if nx_http_resp_cieq(src + no[0], nl[0], cl_name, 14) == 1 {
334 let end_slot: *i64 = sys_mmap(8) as *i64
335 let n: i64 = nx_http_resp_parse_dec(src, src_len, vo[0], end_slot)
336 if n >= 0 { r[9] = n }
337 }
338 let te_name: *u8 = sys_mmap(32)
339 te_name[0]=0x54; te_name[1]=0x72; te_name[2]=0x61; te_name[3]=0x6e
340 te_name[4]=0x73; te_name[5]=0x66; te_name[6]=0x65; te_name[7]=0x72
341 te_name[8]=0x2d; te_name[9]=0x45; te_name[10]=0x6e; te_name[11]=0x63
342 te_name[12]=0x6f; te_name[13]=0x64; te_name[14]=0x69; te_name[15]=0x6e
343 te_name[16]=0x67
344 if nx_http_resp_cieq(src + no[0], nl[0], te_name, 17) == 1 {
345 let chunked: *u8 = sys_mmap(16)
346 chunked[0]=0x63; chunked[1]=0x68; chunked[2]=0x75; chunked[3]=0x6e
347 chunked[4]=0x6b; chunked[5]=0x65; chunked[6]=0x64
348 if nx_http_resp_cieq(src + vo[0], vl[0], chunked, 7) == 1 { r[10] = 1 }
349 }
350 pos = nx[0]
351 }
352 }
353 }
354 return verdict
355}
356
357// Parse full HTTP response. Returns verdict; populates *r.
358func nx_http_response_parse(src: *u8, src_len: i64, r: *i64) -> i64 {
359 var i: i64 = 0
360 while i < 16 { r[i] = 0; i = i + 1 }
361 r[9] = 0 - 1 // content_length absent sentinel
362
363 let code: *i64 = sys_mmap(8) as *i64
364 let ro: *i64 = sys_mmap(8) as *i64
365 let rl: *i64 = sys_mmap(8) as *i64
366 let nx: *i64 = sys_mmap(8) as *i64
367 let v: i64 = nx_http_resp_parse_status_line(src, src_len, code, ro, rl, nx)
368 if v != NX_HTTP_RESP_OK { r[0] = v; return v }
369 r[1] = code[0]
370 r[2] = ro[0]
371 r[3] = rl[0]
372 r[4] = nx[0]
373
374 let cb: i64 = nx_http_resp_classify_body(src, src_len, r)
375 if cb != NX_HTTP_RESP_OK { r[0] = cb; return cb }
376
377 // Decide transfer kind
378 if r[10] == 1 {
379 r[8] = NX_HTTP_BODY_CHUNKED
380 } else {
381 if r[9] >= 0 {
382 r[8] = NX_HTTP_BODY_CONTENT_LENGTH
383 r[7] = r[9]
384 } else {
385 // No Content-Length, no chunked: HTTP/1.0-style read-until-close
386 r[8] = NX_HTTP_BODY_UNTIL_CLOSE
387 r[7] = src_len - r[6]
388 }
389 }
390 r[0] = NX_HTTP_RESP_OK
391 return NX_HTTP_RESP_OK
392}
393
394// Case-insensitive header lookup. Returns 1 if found, sets *val_off,
395// *val_len; 0 if not found.
396func nx_http_response_header(
397 src: *u8,
398 src_len: i64,
399 r: *i64,
400 name: *u8,
401 name_len: i64,
402 val_off: *i64,
403 val_len: *i64
404) -> i64 {
405 let no: *i64 = sys_mmap(8) as *i64
406 let nl: *i64 = sys_mmap(8) as *i64
407 let vo: *i64 = sys_mmap(8) as *i64
408 let vl: *i64 = sys_mmap(8) as *i64
409 let nx: *i64 = sys_mmap(8) as *i64
410 var pos: i64 = r[4]
411 var found: i64 = 0
412 var done: i64 = 0
413 while done == 0 {
414 let v: i64 = nx_http_resp_parse_header_line(src, src_len, pos, no, nl, vo, vl, nx)
415 if v != NX_HTTP_RESP_OK { done = 1 }
416 if done == 0 {
417 if nl[0] == 0 { done = 1 }
418 if done == 0 {
419 if nx_http_resp_cieq(src + no[0], nl[0], name, name_len) == 1 {
420 val_off[0] = vo[0]
421 val_len[0] = vl[0]
422 found = 1
423 done = 1
424 }
425 pos = nx[0]
426 }
427 }
428 }
429 return found
430}
431
432// Decode RFC 7230 §4.1 chunked body. Returns decoded length, or
433// negative verdict on malformed input.
434func nx_http_dechunk(src: *u8, src_len: i64, out: *u8, out_cap: i64) -> i64 {
435 var pos: i64 = 0
436 var out_pos: i64 = 0
437 var done: i64 = 0
438 var verdict: i64 = 0
439 while done == 0 {
440 let end_slot: *i64 = sys_mmap(8) as *i64
441 let chunk_size: i64 = nx_http_resp_parse_hex(src, src_len, pos, end_slot)
442 if chunk_size < 0 { verdict = 0 - NX_HTTP_RESP_ERR_BAD_CHUNK_SIZE; done = 1 }
443 if done == 0 {
444 let crlf: i64 = nx_http_resp_find_crlf(src, src_len, end_slot[0])
445 if crlf < 0 { verdict = 0 - NX_HTTP_RESP_ERR_TRUNCATED; done = 1 }
446 if done == 0 {
447 let data_off: i64 = crlf + 2
448 if chunk_size == 0 {
449 // last chunk; skip optional trailers via CRLF CRLF
450 verdict = out_pos
451 done = 1
452 }
453 if done == 0 {
454 if data_off + chunk_size + 2 > src_len { verdict = 0 - NX_HTTP_RESP_ERR_TRUNCATED; done = 1 }
455 if done == 0 {
456 if out_pos + chunk_size > out_cap { verdict = 0 - NX_HTTP_RESP_ERR_HEADER_OVERFLOW; done = 1 }
457 if done == 0 {
458 var k: i64 = 0
459 while k < chunk_size { out[out_pos + k] = src[data_off + k]; k = k + 1 }
460 out_pos = out_pos + chunk_size
461 pos = data_off + chunk_size + 2
462 }
463 }
464 }
465 }
466 }
467 }
468 return verdict
469}