code wiki / (root) / http.nx

http.nx source

↩ module page · 276 lines · 10067 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 23import "syscalls.nx" 24 25// Error codes. 26const HTTP_ERR_TRUNCATED: i64 = -1 27const HTTP_ERR_BAD_METHOD: i64 = -2 28const HTTP_ERR_BAD_VERSION: i64 = -3 29const HTTP_ERR_BAD_HEADER: i64 = -4 30const HTTP_ERR_TOO_MANY_HDR: i64 = -5 31 32const HTTP_MAX_HEADERS: i64 = 64 33 34// Parsed request descriptor. Offsets + lengths point into the 35// caller's buffer. Headers stored as parallel offset/length 36// arrays (memory-friendly, no struct-of-structs). 37struct HttpRequest { 38 method_off: i64, method_len: i64, 39 path_off: i64, path_len: i64, 40 version_off: i64, version_len: i64, 41 n_headers: i64, 42 body_off: i64, body_len: i64, // body_len = 0 if absent 43 // parallel header arrays; allocated externally by the caller 44 // via http_request_init below. 45 hdr_name_off: *i64, 46 hdr_name_len: *i64, 47 hdr_val_off: *i64, 48 hdr_val_len: *i64, 49} 50 51// Allocate header slot arrays + wire into the struct. 52func http_request_init(req: *HttpRequest) -> i64 { 53 let raw1: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 54 let raw2: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 55 let raw3: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 56 let raw4: *u8 = sys_mmap(HTTP_MAX_HEADERS * 8) 57 req.hdr_name_off = raw1 as *i64 58 req.hdr_name_len = raw2 as *i64 59 req.hdr_val_off = raw3 as *i64 60 req.hdr_val_len = raw4 as *i64 61 req.n_headers = 0 62 return 0 63} 64 65// Scan until any of: SP (0x20), TAB (0x09), CR (0x0D), LF (0x0A). 66// Returns position of the whitespace byte, or `end` if not found. 67func http_scan_token_end(buf: *u8, pos: i64, end: i64) -> i64 { 68 var p: i64 = pos 69 while p < end { 70 let b: i64 = buf[p] 71 if b == 0x20 { return p } 72 if b == 0x09 { return p } 73 if b == 0x0D { return p } 74 if b == 0x0A { return p } 75 p = p + 1 76 } 77 return end 78} 79 80// Skip consecutive SP / TAB bytes. Returns new position. 81func http_skip_hspace(buf: *u8, pos: i64, end: i64) -> i64 { 82 var p: i64 = pos 83 while p < end { 84 let b: i64 = buf[p] 85 if b == 0x20 { p = p + 1 } 86 else { if b == 0x09 { p = p + 1 } else { p = end + 1 } } 87 } 88 if p > end { p = p - 1 } 89 return p 90} 91 92// Find next CRLF starting at `pos`. Returns position of CR (first 93// byte of the CRLF pair), or -1 if not found / malformed (bare CR 94// or bare LF). 95func http_find_crlf(buf: *u8, pos: i64, end: i64) -> i64 { 96 var p: i64 = pos 97 while p + 1 < end { 98 if buf[p] == 0x0D { 99 if buf[p + 1] == 0x0A { return p } 100 return HTTP_ERR_BAD_HEADER 101 } 102 if buf[p] == 0x0A { return HTTP_ERR_BAD_HEADER } // bare LF 103 p = p + 1 104 } 105 return HTTP_ERR_TRUNCATED 106} 107 108// Parse the request line: METHOD SP path SP version CRLF. 109// Returns position after the CRLF on success. 110func http_parse_request_line(buf: *u8, end: i64, req: *HttpRequest) -> i64 { 111 var pos: i64 = 0 112 113 // METHOD. 114 req.method_off = pos 115 let after_method: i64 = http_scan_token_end(buf, pos, end) 116 req.method_len = after_method - pos 117 if req.method_len == 0 { return HTTP_ERR_BAD_METHOD } 118 pos = http_skip_hspace(buf, after_method, end) 119 120 // Path. 121 req.path_off = pos 122 let after_path: i64 = http_scan_token_end(buf, pos, end) 123 req.path_len = after_path - pos 124 if req.path_len == 0 { return HTTP_ERR_BAD_METHOD } 125 pos = http_skip_hspace(buf, after_path, end) 126 127 // Version "HTTP/1.1". 128 req.version_off = pos 129 let crlf_pos: i64 = http_find_crlf(buf, pos, end) 130 if crlf_pos < 0 { return crlf_pos } 131 req.version_len = crlf_pos - pos 132 if req.version_len < 8 { return HTTP_ERR_BAD_VERSION } 133 // Minimum check: prefix must be "HTTP/". 134 if buf[pos] != 0x48 { return HTTP_ERR_BAD_VERSION } // 'H' 135 if buf[pos + 1] != 0x54 { return HTTP_ERR_BAD_VERSION } 136 if buf[pos + 2] != 0x54 { return HTTP_ERR_BAD_VERSION } 137 if buf[pos + 3] != 0x50 { return HTTP_ERR_BAD_VERSION } // 'P' 138 if buf[pos + 4] != 0x2F { return HTTP_ERR_BAD_VERSION } // '/' 139 140 return crlf_pos + 2 // past CRLF 141} 142 143// Parse headers starting at pos. Populates req.hdr_* arrays and 144// n_headers. Returns position after the empty-line CRLF. 145func http_parse_headers(buf: *u8, start: i64, end: i64, 146 req: *HttpRequest) -> i64 { 147 var pos: i64 = start 148 while pos < end { 149 // Empty line (CRLF) marks end of headers. 150 if pos + 1 < end { 151 if buf[pos] == 0x0D { 152 if buf[pos + 1] == 0x0A { return pos + 2 } 153 } 154 } 155 if req.n_headers >= HTTP_MAX_HEADERS { 156 return HTTP_ERR_TOO_MANY_HDR 157 } 158 // Header name: token up to ':'. 159 let name_start: i64 = pos 160 var p: i64 = pos 161 while p < end { 162 if buf[p] == 0x3A { p = end + 1 } // ':' sentinel exit 163 else { 164 if buf[p] == 0x0D { return HTTP_ERR_BAD_HEADER } 165 p = p + 1 166 } 167 } 168 if p <= end { return HTTP_ERR_TRUNCATED } 169 let name_end: i64 = p - 1 170 let name_off: i64 = name_start 171 let name_len: i64 = name_end - name_start 172 if name_len == 0 { return HTTP_ERR_BAD_HEADER } 173 174 // Skip ':' + optional whitespace. 175 pos = name_end + 1 176 pos = http_skip_hspace(buf, pos, end) 177 178 // Header value: up to CRLF. 179 let val_start: i64 = pos 180 let crlf: i64 = http_find_crlf(buf, pos, end) 181 if crlf < 0 { return crlf } 182 // Trim trailing hspace from value. 183 var val_end: i64 = crlf 184 while val_end > val_start { 185 let b: i64 = buf[val_end - 1] 186 if b == 0x20 { val_end = val_end - 1 } 187 else { if b == 0x09 { val_end = val_end - 1 } else { val_end = val_start - 1 } } 188 } 189 if val_end < val_start { val_end = val_start } 190 191 let idx: i64 = req.n_headers 192 req.hdr_name_off[idx] = name_off 193 req.hdr_name_len[idx] = name_len 194 req.hdr_val_off[idx] = val_start 195 req.hdr_val_len[idx] = val_end - val_start 196 req.n_headers = idx + 1 197 198 pos = crlf + 2 199 } 200 return HTTP_ERR_TRUNCATED 201} 202 203// Full parse. Returns 0 on success; negative HTTP_ERR_* on failure. 204// req.body_off is set to the byte after the empty-line CRLF; body_len 205// is NOT populated here (requires Content-Length / chunked decode). 206func http_parse(buf: *u8, n: i64, req: *HttpRequest) -> i64 { 207 http_request_init(req) 208 let after_line: i64 = http_parse_request_line(buf, n, req) 209 if after_line < 0 { return after_line } 210 let after_hdrs: i64 = http_parse_headers(buf, after_line, n, req) 211 if after_hdrs < 0 { return after_hdrs } 212 req.body_off = after_hdrs 213 req.body_len = 0 // caller sets via Content-Length lookup 214 return 0 215} 216 217// Case-insensitive ASCII compare of a byte range against a null- 218// terminated cstring. Used for header-name matching where the 219// client's casing is arbitrary ("Content-Length" vs "content-length"). 220func http_header_name_eq(buf: *u8, off: i64, len: i64, 221 cstr: *u8) -> i64 { 222 var i: i64 = 0 223 while i < len { 224 if cstr[i] == 0 { return 0 } 225 let a: i64 = buf[off + i] 226 let b: i64 = cstr[i] 227 var an: i64 = a 228 var bn: i64 = b 229 // ASCII lowercase. 230 if an >= 0x41 { if an <= 0x5A { an = an + 32 } } 231 if bn >= 0x41 { if bn <= 0x5A { bn = bn + 32 } } 232 if an != bn { return 0 } 233 i = i + 1 234 } 235 if cstr[i] != 0 { return 0 } 236 return 1 237} 238 239// Find a header by name (case-insensitive). Writes value offset 240// and length to out slots. Returns 1 if found, 0 otherwise. 241func http_get_header(buf: *u8, req: *HttpRequest, name: *u8, 242 val_off_out: *i64, val_len_out: *i64) -> i64 { 243 var i: i64 = 0 244 while i < req.n_headers { 245 if http_header_name_eq(buf, req.hdr_name_off[i], 246 req.hdr_name_len[i], name) == 1 { 247 *val_off_out = req.hdr_val_off[i] 248 *val_len_out = req.hdr_val_len[i] 249 return 1 250 } 251 i = i + 1 252 } 253 return 0 254} 255 256// Compile-only smoke. 257func main() -> i64 { 258 let raw: *u8 = "GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n" 259 var n: i64 = 0 260 while raw[n] != 0 { n = n + 1 } 261 262 let req_raw: *u8 = sys_mmap(512) 263 let req: *HttpRequest = req_raw as *HttpRequest 264 let rc: i64 = http_parse(raw, n, req) 265 if rc != 0 { return 1 } 266 if req.method_len != 3 { return 2 } // "GET" 267 if req.path_len != 11 { return 3 } // "/index.html" 268 if req.n_headers != 1 { return 4 } 269 270 // Look up Host header. 271 let vo: *i64 = sys_mmap(16) as *i64 272 let vl: *i64 = sys_mmap(16) as *i64 273 if http_get_header(raw, req, "host", vo, vl) != 1 { return 5 } 274 if *vl != 11 { return 6 } // "example.com" 275 return 0 276}