-- internal/parse.wo — HTTP/1.1 request parsing over a net connection. -- -- INTERNAL (iteration 17): a consumer cannot `use` this module — the -- `internal/` segment makes that WO-E108. The connection-level parser, the -- carry-state record, and the %XX/query decoders are the framework's own -- business; `media_type`/`form_values` are the public half and live in -- `http/form.wo`. -- -- Bounded reads only (`net.read`), so requests are buffered to the header -- terminator, then the body to exactly Content-Length. Keep-alive means -- bytes past this request belong to the NEXT one: the caller passes the -- carry-over in and gets the new remainder back in Parsed.rest. -- -- Parsed is a three-state answer (no tuples in the language): -- closed=true peer ended the connection cleanly between requests -- ok=false malformed request — answer 400 and close -- ok=true, req non-nil one complete request use net use http -- Req lives in the public module now const BODY_MAX = 1048576 pub typedef Parsed = { closed: Bool, ok: Bool, ?req: Req, rest: Text } -- %XX decoding, '+' as space when plus_space (query strings only). -- Malformed escapes pass through verbatim — parsing stays total. fn hex_val(b: Int) -> Int { if b >= 48 and b <= 57 { return b - 48; } -- 0-9 if b >= 97 and b <= 102 { return b - 87; } -- a-f if b >= 65 and b <= 70 { return b - 55; } -- A-F return -1; } pub fn url_decode(t: Text, plus_space: Bool) -> Text { let out = ""; let i = 0; let n = len(t); while i < n { let b = byte_at(t, i); if b == 37 and i + 2 < n { -- '%' let hi = hex_val(byte_at(t, i + 1)); let lo = hex_val(byte_at(t, i + 2)); if hi >= 0 and lo >= 0 { out = out .. char_of(hi * 16 + lo); i = i + 3; continue; } } if plus_space and b == 43 { -- '+' out = out .. " "; i = i + 1; continue; } out = out .. substr(t, i, 1); i = i + 1; } return out; } -- "a=1&b=hello+world" -> decoded pairs; a bare key maps to "" pub fn parse_query(qs: Text) -> map { let q: map = {}; if qs == "" { return q; } for pair in split(qs, "&") { if pair == "" { continue; } let eq = index_of(pair, "="); if eq < 0 { q[url_decode(pair, true)] = ""; } else { q[url_decode(substr(pair, 0, eq), true)] = url_decode(substr(pair, eq + 1, len(pair) - eq - 1), true); } } return q; } fn malformed(rest: Text) -> Parsed { return Parsed { closed: false, ok: false, req: nil, rest: rest }; } -- One request off the connection. `carry` = leftover bytes from the same -- connection's previous request (keep-alive). Deadlines (iteration 35): -- `first_ms` bounds the wait for a request's FIRST bytes (the keep-alive -- idle window — expiry is a CLEAN close, not an error), `read_ms` bounds -- every later read (a slow-loris mid-request is torn = 400-and-close). -- ms <= 0 = wait forever, the pre-35 behavior bit for bit. pub fn parse_request(c: net.Conn, carry: Text, first_ms: Int, read_ms: Int) -> Parsed { let buf = carry; let header_end = index_of(buf, "\r\n\r\n"); while header_end == -1 { let dl = read_ms; if buf == "" { dl = first_ms; } let r = net.read_dl(c, 8192, dl); if r == nil { -- deadline expired: idle (nothing arrived) closes clean; a stalled -- peer MID-request is torn if trim(buf) == "" { return Parsed { closed: true, ok: true, req: nil, rest: "" }; } return malformed(""); } let got = "${r}"; if len(got) == 0 { -- peer closed: clean between requests (empty buffer), torn otherwise if trim(buf) == "" { return Parsed { closed: true, ok: true, req: nil, rest: "" }; } return malformed(""); } buf = buf .. got; header_end = index_of(buf, "\r\n\r\n"); if header_end == -1 and len(buf) > BODY_MAX { return malformed(""); } } let lines = split(substr(buf, 0, header_end), "\r\n"); let req_line = split_ws(trim(lines[0])); if len(req_line) < 3 { return malformed(""); } let method = req_line[0]; let target = req_line[1]; -- path / query split, both %-decoded ('+' is a space only in the query) let path = target; let query: map = {}; let qm = index_of(target, "?"); if qm >= 0 { path = substr(target, 0, qm); query = parse_query(substr(target, qm + 1, len(target) - qm - 1)); } path = url_decode(path, false); let headers: map = {}; let cl_seen = 0; let i = 1; while i < len(lines) { let line = trim(lines[i]); i = i + 1; if line == "" { continue; } let colon = index_of(line, ":"); if colon > 0 { let hname = to_lower(substr(line, 0, colon)); -- v1 slice 2, the strict-ambiguity audit: a SECOND Content-Length -- header is request smuggling's favorite tool — reject the request -- outright instead of letting last-one-wins pick a body length -- (RFC 9112 §6.3: such a message MUST be treated as an error). if hname == "content-length" { cl_seen = cl_seen + 1; if cl_seen > 1 { return malformed(""); } } headers[hname] = trim(substr(line, colon + 1, len(line) - colon - 1)); } } -- Transfer-Encoding is NOT implemented — and silently treating a -- chunked request as body-less is request smuggling's other favorite -- door (RFC 9112 §6.1: a server that cannot handle TE on a request -- MUST respond 400 and close). Reject ANY TE header outright. let te = headers["transfer-encoding"]; if te != nil { return malformed(""); } let want = 0; let cl = headers["content-length"]; if cl != nil { let n = parse_int(cl); if n == nil { return malformed(""); } if n < 0 or n > BODY_MAX { return malformed(""); } want = n; } let body = substr(buf, header_end + 4, len(buf) - header_end - 4); while len(body) < want { let r2 = net.read_dl(c, 8192, read_ms); if r2 == nil { return malformed(""); } -- stalled mid-body let got = "${r2}"; if len(got) == 0 { return malformed(""); } -- peer died mid-body body = body .. got; } -- bytes past the declared body belong to the next request on this conn let rest = ""; if len(body) > want { rest = substr(body, want, len(body) - want); body = substr(body, 0, want); } let req = Req { method: method, path: path, params: {}, query: query, headers: headers, body: body, principal: "", ctx: {}, conn: c }; return Parsed { closed: false, ok: true, req: req, rest: rest }; }