code wiki / (root) / nx_http_response_parse.nx

nx_http_response_parse.nx source

↩ module page · 487 lines · 18695 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// Coverage: strict HTTP/1.0 and HTTP/1.1 status-line grammar has a dedicated boundary gate. 51// Existing full-parser tests cover selected headers, Content-Length and chunk decoding. 52// This is not full HTTP framing conformance: header numeric overflow, duplicate framing, 53// trailer exposure and allocation cleanup need independent qualification. Compression decode 54// belongs to its existing separate primitive; no whole-response validity follows from status alone. 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) 201// Strict RFC 9112 section 4 grammar, within this owner's HTTP/1.0 and 1.1 support. 202// Returns status or a negative existing verdict. No allocation and no scanning for a later start line. 203func nx_http_resp_status_code(src:*u8,src_len:i64)->i64{ 204 if (src as i64)==0||src_len<15{return 0-NX_HTTP_RESP_ERR_TOO_SHORT} 205 let prefix:*u8="HTTP/1.";var i:i64=0 206 while i<7{if src[i]!=prefix[i]{return 0-NX_HTTP_RESP_ERR_BAD_VERSION};i=i+1} 207 if src[7]!=48 as u8&&src[7]!=49 as u8{return 0-NX_HTTP_RESP_ERR_BAD_VERSION} 208 if src[8]!=32 as u8{return 0-NX_HTTP_RESP_ERR_BAD_STATUS_LINE} 209 i=9;var code:i64=0 210 while i<12{let c:i64=src[i] as i64;if nx_http_resp_is_digit(c)!=1{return 0-NX_HTTP_RESP_ERR_BAD_CODE};code=code*10+c-48;i=i+1} 211 if code<100||code>599{return 0-NX_HTTP_RESP_ERR_BAD_CODE} 212 if src[12]!=32 as u8{return 0-NX_HTTP_RESP_ERR_BAD_STATUS_LINE} 213 i=13 214 while i<src_len{ 215 let c:i64=src[i] as i64 216 if c==13{if i+1<src_len{if src[i+1]==10 as u8{return code}};return 0-NX_HTTP_RESP_ERR_BAD_STATUS_LINE} 217 // reason-phrase admits HTAB, SP, VCHAR and obs-text, excluding DEL and other controls. 218 if c!=9{if c<32||c==127{return 0-NX_HTTP_RESP_ERR_BAD_STATUS_LINE}} 219 i=i+1 220 } 221 return 0-NX_HTTP_RESP_ERR_BAD_STATUS_LINE 222} 223func nx_http_resp_parse_status_line(src:*u8,src_len:i64,out_code:*i64,out_reason_off:*i64,out_reason_len:*i64,out_next:*i64)->i64{ 224 out_code[0]=0;out_reason_off[0]=0;out_reason_len[0]=0;out_next[0]=0 225 let code:i64=nx_http_resp_status_code(src,src_len);if code<0{return 0-code} 226 // Preserve exact reason bytes, including legal whitespace after the required separator. 227 let end:i64=nx_http_resp_find_crlf(src,src_len,13) 228 out_code[0]=code;out_reason_off[0]=13;out_reason_len[0]=end-13;out_next[0]=end+2 229 return NX_HTTP_RESP_OK 230} 231 232// Parse one "Name: Value\r\n" header line starting at off. 233// Returns NX_HTTP_RESP_OK on success, populating slots and *out_next 234// = offset after the trailing CRLF. Returns NX_HTTP_RESP_OK with 235// *out_name_len == 0 to signal end-of-headers (the blank line at 236// CRLF CRLF). 237func nx_http_resp_parse_header_line( 238 src: *u8, 239 src_len: i64, 240 off: i64, 241 out_name_off: *i64, 242 out_name_len: *i64, 243 out_val_off: *i64, 244 out_val_len: *i64, 245 out_next: *i64 246) -> i64 { 247 if off + 1 >= src_len { return NX_HTTP_RESP_ERR_TRUNCATED } 248 if (src[off] & 0xff) == 0x0d { 249 if (src[off + 1] & 0xff) == 0x0a { 250 out_name_len[0] = 0 251 out_next[0] = off + 2 252 return NX_HTTP_RESP_OK 253 } 254 } 255 let colon: i64 = nx_http_resp_find_byte(src, src_len, off, 0x3a) 256 if colon < 0 { return NX_HTTP_RESP_ERR_HEADER_OVERFLOW } 257 out_name_off[0] = off 258 out_name_len[0] = colon - off 259 260 let val_start: i64 = nx_http_resp_skip_ows(src, src_len, colon + 1) 261 let line_end: i64 = nx_http_resp_find_crlf(src, src_len, val_start) 262 if line_end < 0 { return NX_HTTP_RESP_ERR_HEADER_OVERFLOW } 263 out_val_off[0] = val_start 264 // Trim trailing OWS (RFC 7230 §3.2.4) -- bounded-loop pattern. 265 var v_end: i64 = line_end 266 var trimming: i64 = 1 267 while trimming == 1 { 268 if v_end <= val_start { trimming = 0 } 269 if trimming == 1 { 270 let c: i64 = src[v_end - 1] & 0xff 271 if c == 0x20 { v_end = v_end - 1 } 272 if c == 0x09 { v_end = v_end - 1 } 273 if c != 0x20 { if c != 0x09 { trimming = 0 } } 274 } 275 } 276 out_val_len[0] = v_end - val_start 277 out_next[0] = line_end + 2 278 return NX_HTTP_RESP_OK 279} 280 281// ===== High-level: full message parse ============================= 282 283// NxHttpResp packed layout (we use a flat i64 array to dodge struct 284// types for now): 285// slot 0: verdict 286// slot 1: status_code 287// slot 2: reason_off 288// slot 3: reason_len 289// slot 4: headers_off 290// slot 5: headers_len 291// slot 6: body_off 292// slot 7: body_len (declared if Content-Length seen) 293// slot 8: transfer_kind 294// slot 9: content_length (parsed value, or -1 if absent) 295// slot 10: te_chunked_seen (1 if Transfer-Encoding: chunked) 296 297func nx_http_resp_alloc() -> *i64 { return sys_mmap(128) as *i64 } 298 299// Walk all headers from headers_off, classify transfer model. 300// Caller passes a pre-allocated NxHttpResp slot ptr. 301func nx_http_resp_classify_body(src: *u8, src_len: i64, r: *i64) -> i64 { 302 let no: *i64 = sys_mmap(8) as *i64 303 let nl: *i64 = sys_mmap(8) as *i64 304 let vo: *i64 = sys_mmap(8) as *i64 305 let vl: *i64 = sys_mmap(8) as *i64 306 let nx: *i64 = sys_mmap(8) as *i64 307 var pos: i64 = r[4] 308 var done: i64 = 0 309 var verdict: i64 = NX_HTTP_RESP_OK 310 while done == 0 { 311 let v: i64 = nx_http_resp_parse_header_line(src, src_len, pos, no, nl, vo, vl, nx) 312 if v != NX_HTTP_RESP_OK { verdict = v; done = 1 } 313 if done == 0 { 314 if nl[0] == 0 { 315 r[5] = pos - r[4] 316 r[6] = nx[0] 317 done = 1 318 } 319 if done == 0 { 320 // Compare name CI against "Content-Length" and "Transfer-Encoding" 321 let cl_name: *u8 = sys_mmap(16) 322 cl_name[0]=0x43; cl_name[1]=0x6f; cl_name[2]=0x6e; cl_name[3]=0x74 323 cl_name[4]=0x65; cl_name[5]=0x6e; cl_name[6]=0x74; cl_name[7]=0x2d 324 cl_name[8]=0x4c; cl_name[9]=0x65; cl_name[10]=0x6e; cl_name[11]=0x67 325 cl_name[12]=0x74; cl_name[13]=0x68 326 if nx_http_resp_cieq(src + no[0], nl[0], cl_name, 14) == 1 { 327 let end_slot: *i64 = sys_mmap(8) as *i64 328 let n: i64 = nx_http_resp_parse_dec(src, src_len, vo[0], end_slot) 329 if n >= 0 { r[9] = n } 330 } 331 let te_name: *u8 = sys_mmap(32) 332 te_name[0]=0x54; te_name[1]=0x72; te_name[2]=0x61; te_name[3]=0x6e 333 te_name[4]=0x73; te_name[5]=0x66; te_name[6]=0x65; te_name[7]=0x72 334 te_name[8]=0x2d; te_name[9]=0x45; te_name[10]=0x6e; te_name[11]=0x63 335 te_name[12]=0x6f; te_name[13]=0x64; te_name[14]=0x69; te_name[15]=0x6e 336 te_name[16]=0x67 337 if nx_http_resp_cieq(src + no[0], nl[0], te_name, 17) == 1 { 338 let chunked: *u8 = sys_mmap(16) 339 chunked[0]=0x63; chunked[1]=0x68; chunked[2]=0x75; chunked[3]=0x6e 340 chunked[4]=0x6b; chunked[5]=0x65; chunked[6]=0x64 341 if nx_http_resp_cieq(src + vo[0], vl[0], chunked, 7) == 1 { r[10] = 1 } 342 } 343 pos = nx[0] 344 } 345 } 346 } 347 return verdict 348} 349 350// Parse full HTTP response. Returns verdict; populates *r. 351func nx_http_response_parse(src: *u8, src_len: i64, r: *i64) -> i64 { 352 var i: i64 = 0 353 while i < 16 { r[i] = 0; i = i + 1 } 354 r[9] = 0 - 1 // content_length absent sentinel 355 356 let code: *i64 = sys_mmap(8) as *i64 357 let ro: *i64 = sys_mmap(8) as *i64 358 let rl: *i64 = sys_mmap(8) as *i64 359 let nx: *i64 = sys_mmap(8) as *i64 360 let v: i64 = nx_http_resp_parse_status_line(src, src_len, code, ro, rl, nx) 361 if v != NX_HTTP_RESP_OK { r[0] = v; return v } 362 r[1] = code[0] 363 r[2] = ro[0] 364 r[3] = rl[0] 365 r[4] = nx[0] 366 367 let cb: i64 = nx_http_resp_classify_body(src, src_len, r) 368 if cb != NX_HTTP_RESP_OK { r[0] = cb; return cb } 369 370 // Decide transfer kind 371 if r[10] == 1 { 372 r[8] = NX_HTTP_BODY_CHUNKED 373 } else { 374 if r[9] >= 0 { 375 r[8] = NX_HTTP_BODY_CONTENT_LENGTH 376 r[7] = r[9] 377 } else { 378 // No Content-Length, no chunked: HTTP/1.0-style read-until-close 379 r[8] = NX_HTTP_BODY_UNTIL_CLOSE 380 r[7] = src_len - r[6] 381 } 382 } 383 r[0] = NX_HTTP_RESP_OK 384 return NX_HTTP_RESP_OK 385} 386 387// Case-insensitive header lookup. Returns 1 if found, sets *val_off, 388// *val_len; 0 if not found. 389func nx_http_response_header( 390 src: *u8, 391 src_len: i64, 392 r: *i64, 393 name: *u8, 394 name_len: i64, 395 val_off: *i64, 396 val_len: *i64 397) -> i64 { 398 let no: *i64 = sys_mmap(8) as *i64 399 let nl: *i64 = sys_mmap(8) as *i64 400 let vo: *i64 = sys_mmap(8) as *i64 401 let vl: *i64 = sys_mmap(8) as *i64 402 let nx: *i64 = sys_mmap(8) as *i64 403 var pos: i64 = r[4] 404 var found: i64 = 0 405 var done: i64 = 0 406 while done == 0 { 407 let v: i64 = nx_http_resp_parse_header_line(src, src_len, pos, no, nl, vo, vl, nx) 408 if v != NX_HTTP_RESP_OK { done = 1 } 409 if done == 0 { 410 if nl[0] == 0 { done = 1 } 411 if done == 0 { 412 if nx_http_resp_cieq(src + no[0], nl[0], name, name_len) == 1 { 413 val_off[0] = vo[0] 414 val_len[0] = vl[0] 415 found = 1 416 done = 1 417 } 418 pos = nx[0] 419 } 420 } 421 } 422 return found 423} 424 425// Decode RFC 7230 §4.1 chunked body. Returns decoded length, or 426// negative verdict on malformed input. 427func nx_http_dechunk(src: *u8, src_len: i64, out: *u8, out_cap: i64) -> i64 { 428 if src_len < 0 || out_cap < 0 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 429 var pos: i64 = 0 430 var written: i64 = 0 431 while pos < src_len { 432 var size: i64 = 0 433 var digits: i64 = 0 434 while pos < src_len { 435 let digit: i64 = nx_http_resp_hex_val(src[pos] as i64) 436 if digit < 0 { break } 437 // A chunk cannot exceed the entire supplied representation. Bound BEFORE multiplication. 438 if digit > src_len { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 439 if size > (src_len-digit)/16 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 440 size=size*16+digit; digits=digits+1; pos=pos+1 441 } 442 if digits == 0 { return 0 - NX_HTTP_RESP_ERR_BAD_CHUNK_SIZE } 443 if pos >= src_len { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 444 // Extensions remain opaque; require their separator and reject control bytes in the size line. 445 if src[pos] == 59 as u8 { 446 pos=pos+1 447 while pos < src_len { 448 let c: i64=src[pos] as i64 449 if c == 13 { break } 450 if c < 32 && c != 9 { return 0 - NX_HTTP_RESP_ERR_BAD_CHUNK_SIZE } 451 if c == 127 { return 0 - NX_HTTP_RESP_ERR_BAD_CHUNK_SIZE } 452 pos=pos+1 453 } 454 } 455 if src_len-pos < 2 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 456 if src[pos] != 13 as u8 || src[pos+1] != 10 as u8 { return 0 - NX_HTTP_RESP_ERR_BAD_CHUNK_SIZE } 457 pos=pos+2 458 if size == 0 { 459 // Last-chunk alone is incomplete; consume the trailer section through its final empty line. 460 while pos < src_len { 461 let end: i64=nx_http_resp_find_crlf(src,src_len,pos) 462 if end < 0 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 463 if end == pos { return written } 464 var colon: i64=0 465 var ti: i64=pos 466 while ti < end { 467 let c: i64=src[ti] as i64 468 if c == 58 && ti > pos { colon=1 } 469 if c < 32 && c != 9 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 470 if c == 127 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 471 ti=ti+1 472 } 473 if colon == 0 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 474 pos=end+2 475 } 476 return 0 - NX_HTTP_RESP_ERR_TRUNCATED 477 } 478 if src_len-pos < 2 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 479 if size > src_len-pos-2 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 480 if size > out_cap-written { return 0 - NX_HTTP_RESP_ERR_HEADER_OVERFLOW } 481 if src[pos+size] != 13 as u8 || src[pos+size+1] != 10 as u8 { return 0 - NX_HTTP_RESP_ERR_TRUNCATED } 482 var i: i64=0 483 while i < size { out[written+i]=src[pos+i]; i=i+1 } 484 written=written+size; pos=pos+size+2 485 } 486 return 0 - NX_HTTP_RESP_ERR_TRUNCATED 487}