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}