code wiki / (root) / nx_http_response_parse.nx

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}