code wiki / (root) / nx_http.nx

nx_http.nx source

↩ module page · 300 lines · 11022 B

1// http.nx -- HTTP/1.1 request parser (RFC 7230/9112 subset). 2// 3// Parses an HTTP request from a byte buffer (typically accumulated 4// from socket reads). Reader-only subset: 5// - Request line: METHOD SP path SP HTTP/1.1 CRLF 6// - Headers: Name ": " Value CRLF (until empty line) 7// - Body: remaining bytes per Content-Length 8// 9// Response generation is the caller's job; we provide a typed 10// view of the request + a Content-Length header lookup helper. 11// Real-world HTTP servers add: chunked transfer, keep-alive, 12// HTTP/2 framing, pipelining. Those are future work. 13// 14// Invariants: 15// H1 All reads bounds-check against caller-supplied buffer end. 16// H2 Header parsing stops at CRLF CRLF (empty line) per 17// RFC 7230 ยง3. 18// H3 Returns offsets into the caller's buffer; nothing copied. 19// Parser struct owns only position metadata. 20// H4 Malformed input returns negative HTTP_ERR_*; no silent 21// acceptance of weird forms (e.g. bare LF line ends). 22 23// nx_safety_envelope: 24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 25// sil_target: SIL1 26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 27// verdict: NOT_YET_EVALUATED 28 29import "nx_syscalls.nx" 30 31// Error codes. 32const HTTP_ERR_TRUNCATED: i64 = -1 33const HTTP_ERR_BAD_METHOD: i64 = -2 34const HTTP_ERR_BAD_VERSION: i64 = -3 35const HTTP_ERR_BAD_HEADER: i64 = -4 36const HTTP_ERR_TOO_MANY_HDR: i64 = -5 37 38const HTTP_MAX_HEADERS: i64 = 64 39 40// Parsed request descriptor. Offsets + lengths point into the 41// caller's buffer. Headers stored as parallel offset/length 42// arrays (memory-friendly, no struct-of-structs). 43struct HttpRequest { 44 method_off: i64, method_len: i64, 45 path_off: i64, path_len: i64, 46 version_off: i64, version_len: i64, 47 n_headers: i64, 48 body_off: i64, body_len: i64, // body_len = 0 if absent 49 // parallel header arrays; allocated externally by the caller 50 // via http_request_init below. 51 hdr_name_off: *i64, 52 hdr_name_len: *i64, 53 hdr_val_off: *i64, 54 hdr_val_len: *i64, 55} 56 57// Allocate header slot arrays + wire into the struct. 58func http_request_init(req: *HttpRequest) -> i64 { 59 let raw1: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 60 let raw2: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 61 let raw3: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 62 let raw4: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 63 req.hdr_name_off = raw1 as *i64 64 req.hdr_name_len = raw2 as *i64 65 req.hdr_val_off = raw3 as *i64 66 req.hdr_val_len = raw4 as *i64 67 req.n_headers = 0 68 return 0 69} 70 71// Scan until any of: SP (0x20), TAB (0x09), CR (0x0D), LF (0x0A). 72// Returns position of the whitespace byte, or `end` if not found. 73func http_scan_token_end(buf: *u8, pos: i64, end: i64) -> i64 { 74 var p: i64 = pos 75 while p < end { 76 let b: i64 = buf[p] 77 if b == 0x20 { return p } 78 if b == 0x09 { return p } 79 if b == 0x0D { return p } 80 if b == 0x0A { return p } 81 p = p + 1 82 } 83 return end 84} 85 86// Skip consecutive SP / TAB bytes. Returns new position. 87func http_skip_hspace(buf: *u8, pos: i64, end: i64) -> i64 { 88 var p: i64 = pos 89 var done: i64 = 0 90 // Loop on `done` flag, not `p`, so when we hit a non-whitespace byte we 91 // stop without overwriting p. Earlier sentinel-exit pattern set 92 // p = end + 1 and tried to restore via p - 1, which gave p = end -- 93 // wrong position, broke every caller of http_parse_request_line. 94 while done == 0 { 95 if p >= end { done = 1 } 96 if done == 0 { 97 let b: i64 = buf[p] as i64 98 if b == 0x20 { p = p + 1 } 99 if b == 0x09 { p = p + 1 } 100 if b != 0x20 { if b != 0x09 { done = 1 } } 101 } 102 } 103 return p 104} 105 106// Find next CRLF starting at `pos`. Returns position of CR (first 107// byte of the CRLF pair), or -1 if not found / malformed (bare CR 108// or bare LF). 109func http_find_crlf(buf: *u8, pos: i64, end: i64) -> i64 { 110 var p: i64 = pos 111 while p + 1 < end { 112 if buf[p] == 0x0D { 113 if buf[p + 1] == 0x0A { return p } 114 return HTTP_ERR_BAD_HEADER 115 } 116 if buf[p] == 0x0A { return HTTP_ERR_BAD_HEADER } // bare LF 117 p = p + 1 118 } 119 return HTTP_ERR_TRUNCATED 120} 121 122// Parse the request line: METHOD SP path SP version CRLF. 123// Returns position after the CRLF on success. 124func http_parse_request_line(buf: *u8, end: i64, req: *HttpRequest) -> i64 { 125 var pos: i64 = 0 126 127 // METHOD. 128 req.method_off = pos 129 let after_method: i64 = http_scan_token_end(buf, pos, end) 130 req.method_len = after_method - pos 131 if req.method_len == 0 { return HTTP_ERR_BAD_METHOD } 132 pos = http_skip_hspace(buf, after_method, end) 133 134 // Path. 135 req.path_off = pos 136 let after_path: i64 = http_scan_token_end(buf, pos, end) 137 req.path_len = after_path - pos 138 if req.path_len == 0 { return HTTP_ERR_BAD_METHOD } 139 pos = http_skip_hspace(buf, after_path, end) 140 141 // Version "HTTP/1.1". 142 req.version_off = pos 143 let crlf_pos: i64 = http_find_crlf(buf, pos, end) 144 if crlf_pos < 0 { return crlf_pos } 145 req.version_len = crlf_pos - pos 146 if req.version_len < 8 { return HTTP_ERR_BAD_VERSION } 147 // Minimum check: prefix must be "HTTP/". 148 if buf[pos] != 0x48 { return HTTP_ERR_BAD_VERSION } // 'H' 149 if buf[pos + 1] != 0x54 { return HTTP_ERR_BAD_VERSION } 150 if buf[pos + 2] != 0x54 { return HTTP_ERR_BAD_VERSION } 151 if buf[pos + 3] != 0x50 { return HTTP_ERR_BAD_VERSION } // 'P' 152 if buf[pos + 4] != 0x2F { return HTTP_ERR_BAD_VERSION } // '/' 153 154 return crlf_pos + 2 // past CRLF 155} 156 157// Parse headers starting at pos. Populates req.hdr_* arrays and 158// n_headers. Returns position after the empty-line CRLF. 159func http_parse_headers(buf: *u8, start: i64, end: i64, 160 req: *HttpRequest) -> i64 { 161 var pos: i64 = start 162 while pos < end { 163 // Empty line (CRLF) marks end of headers. 164 if pos + 1 < end { 165 if buf[pos] == 0x0D { 166 if buf[pos + 1] == 0x0A { return pos + 2 } 167 } 168 } 169 if req.n_headers >= HTTP_MAX_HEADERS { 170 return HTTP_ERR_TOO_MANY_HDR 171 } 172 // Header name: token up to ':'. Capture the colon position 173 // in `colon_pos`; without the capture the sentinel-exit pattern 174 // (p = end + 1) would clobber the position we need below. 175 let name_start: i64 = pos 176 var p: i64 = pos 177 var colon_pos: i64 = -1 178 while p < end { 179 if colon_pos < 0 { 180 let b: i64 = buf[p] as i64 181 if b == 0x3A { colon_pos = p; p = end } // exit 182 if b == 0x0D { return HTTP_ERR_BAD_HEADER } 183 if colon_pos < 0 { p = p + 1 } 184 } 185 } 186 if colon_pos < 0 { return HTTP_ERR_TRUNCATED } 187 let name_off: i64 = name_start 188 let name_len: i64 = colon_pos - name_start 189 if name_len == 0 { return HTTP_ERR_BAD_HEADER } 190 191 // Skip ':' + optional whitespace. 192 pos = colon_pos + 1 193 pos = http_skip_hspace(buf, pos, end) 194 195 // Header value: up to CRLF. 196 let val_start: i64 = pos 197 let crlf: i64 = http_find_crlf(buf, pos, end) 198 if crlf < 0 { return crlf } 199 // Trim trailing hspace from value. Loop on `done` so non-whitespace 200 // exit doesn't clobber val_end (the original sentinel-exit set 201 // val_end = val_start - 1 then forced val_end = val_start, which made 202 // every value parse as length 0). 203 var val_end: i64 = crlf 204 var trim_done: i64 = 0 205 while trim_done == 0 { 206 if val_end <= val_start { trim_done = 1 } 207 if trim_done == 0 { 208 let b: i64 = buf[val_end - 1] as i64 209 if b == 0x20 { val_end = val_end - 1 } 210 if b == 0x09 { val_end = val_end - 1 } 211 if b != 0x20 { if b != 0x09 { trim_done = 1 } } 212 } 213 } 214 215 let idx: i64 = req.n_headers 216 req.hdr_name_off[idx] = name_off 217 req.hdr_name_len[idx] = name_len 218 req.hdr_val_off[idx] = val_start 219 req.hdr_val_len[idx] = val_end - val_start 220 req.n_headers = idx + 1 221 222 pos = crlf + 2 223 } 224 return HTTP_ERR_TRUNCATED 225} 226 227// Full parse. Returns 0 on success; negative HTTP_ERR_* on failure. 228// req.body_off is set to the byte after the empty-line CRLF; body_len 229// is NOT populated here (requires Content-Length / chunked decode). 230func http_parse(buf: *u8, n: i64, req: *HttpRequest) -> i64 { 231 http_request_init(req) 232 let after_line: i64 = http_parse_request_line(buf, n, req) 233 if after_line < 0 { return after_line } 234 let after_hdrs: i64 = http_parse_headers(buf, after_line, n, req) 235 if after_hdrs < 0 { return after_hdrs } 236 req.body_off = after_hdrs 237 req.body_len = 0 // caller sets via Content-Length lookup 238 return 0 239} 240 241// Case-insensitive ASCII compare of a byte range against a null- 242// terminated cstring. Used for header-name matching where the 243// client's casing is arbitrary ("Content-Length" vs "content-length"). 244func http_header_name_eq(buf: *u8, off: i64, len: i64, 245 cstr: *u8) -> i64 { 246 var i: i64 = 0 247 while i < len { 248 if cstr[i] == 0 { return 0 } 249 let a: i64 = buf[off + i] 250 let b: i64 = cstr[i] 251 var an: i64 = a 252 var bn: i64 = b 253 // ASCII lowercase. 254 if an >= 0x41 { if an <= 0x5A { an = an + 32 } } 255 if bn >= 0x41 { if bn <= 0x5A { bn = bn + 32 } } 256 if an != bn { return 0 } 257 i = i + 1 258 } 259 if cstr[i] != 0 { return 0 } 260 return 1 261} 262 263// Find a header by name (case-insensitive). Writes value offset 264// and length to out slots. Returns 1 if found, 0 otherwise. 265func http_get_header(buf: *u8, req: *HttpRequest, name: *u8, 266 val_off_out: *i64, val_len_out: *i64) -> i64 { 267 var i: i64 = 0 268 while i < req.n_headers { 269 if http_header_name_eq(buf, req.hdr_name_off[i], 270 req.hdr_name_len[i], name) == 1 { 271 *val_off_out = req.hdr_val_off[i] 272 *val_len_out = req.hdr_val_len[i] 273 return 1 274 } 275 i = i + 1 276 } 277 return 0 278} 279 280// Compile-only smoke. 281func main() -> i64 { 282 let raw: *u8 = "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n" 283 var n: i64 = 0 284 while raw[n] != 0 { n = n + 1 } 285 286 let req_raw: *u8 = sys_mmap(512) 287 let req: *HttpRequest = req_raw as *HttpRequest 288 let rc: i64 = http_parse(raw, n, req) 289 if rc != 0 { return 1 } 290 if req.method_len != 3 { return 2 } // "GET" 291 if req.path_len != 11 { return 3 } // "/index.html" 292 if req.n_headers != 1 { return 4 } 293 294 // Look up Host header. 295 let vo: *i64 = sys_mmap(16) as *i64 296 let vl: *i64 = sys_mmap(16) as *i64 297 if http_get_header(raw, req, "host", vo, vl) != 1 { return 5 } 298 if *vl != 11 { return 6 } // "example.com" 299 return 0 300}