///|
/// The end-of-head matcher shared by `scan_head` and `HeadDecoder`: it
/// refuses a bare CR or LF and reports the byte that completes CRLF CRLF.
/// While `leading`, CRLFs before the start line are skipped instead of
/// ending the head.
priv struct HeadScan {
mut matched : Int32
mut previous_cr : Bool
mut leading : Bool
}
///|
fn HeadScan::new(skip_leading_crlf : Bool) -> HeadScan {
{ matched: 0, previous_cr: false, leading: skip_leading_crlf, }
}
///|
fn HeadScan::step(self : HeadScan, byte : Byte) -> Bool raise ParseError {
if (self.previous_cr && byte != b'\n') || (byte == b'\n' && !self.previous_cr) {
raise malformed("head requires CRLF line endings")
}
self.previous_cr = byte == b'\r'
if self.leading {
if byte is (b'\r' | b'\n') {
return false
}
self.leading = false
}
self.matched = match (self.matched, byte) {
(0 | 2, b'\r') => self.matched + 1
(1 | 3, b'\n') => self.matched + 1
_ => 0
}
self.matched == 4
}
///|
/// Finds the first complete head in `buf` within `max` bytes and returns its
/// exclusive end, or `None` while the head is incomplete and within `max`.
/// Refuses a bare CR or LF. With `skip_leading_crlf`, empty lines (CRLF)
/// before the start line are part of the head instead of ending it, and
/// count against `max`; a server reading requests sets it (RFC 9112,
/// section 2.2). Each call scans `buf` from the start; feed arriving input
/// to a `HeadDecoder` instead, which scans each byte once.
///
/// ```mbt check
/// test {
/// let wire = b"HTTP/1.1 204\r\n\r\nnext"
/// assert_eq(@h1.scan_head(wire, max=16384), Some(16))
/// assert_eq(@h1.scan_head(wire[:10], max=16384), None)
/// let request = b"\r\nGET / HTTP/1.1\r\nHost: h\r\n\r\n"
/// assert_eq(
/// @h1.scan_head(request, max=16384, skip_leading_crlf=true),
/// Some(request.length()),
/// )
/// }
/// ```
pub fn scan_head(
buf : BytesView,
max~ : Int32,
skip_leading_crlf~ : Bool = false,
) -> Int32? raise ParseError {
if max < 0 {
raise malformed("negative head limit")
}
let scan = HeadScan::new(skip_leading_crlf)
for i, byte in buf {
if i >= max {
raise too_large("head exceeds \{max} bytes")
}
if scan.step(byte) {
return Some(i + 1)
}
}
None
}
///|
/// The result of feeding a `HeadDecoder`: the head once it is complete, and
/// how many of the fed bytes it took. The bytes after `consumed` belong to
/// the body or to a pipelined message; the decoder never keeps them.
#derive(Eq, Debug)
pub struct HeadResult {
/// The complete head, including its final CRLF CRLF.
head : Bytes?
/// How many bytes of this feed belong to the head.
consumed : Int32
}
///|
/// Formats head decoder results.
pub extend HeadResult with @debug.Debug::{to_repr}
///|
/// Compares head decoder results.
pub extend HeadResult with Eq::{equal, not_equal}
///|
/// A bounded head accumulator that scans each byte once, so feeding a head
/// in any number of pieces costs linear time. After completion, later feeds
/// return the same head with zero bytes consumed. Errors are sticky: every
/// later `feed` or `finish` raises the first error again.
pub struct HeadDecoder {
priv max : Int32
priv mut seen : Int32
priv buffer : Buffer
priv scan : HeadScan
priv mut complete : Bytes?
priv mut failure : ParseError?
}
///|
/// Starts a head accumulator that refuses heads longer than `max` bytes,
/// final CRLF CRLF included (default 16 KiB, as `Limits::default`). With
/// `skip_leading_crlf`, empty lines (CRLF) before the start line are
/// consumed, left out of the returned head and counted against `max`, as a
/// server reading requests should (RFC 9112, section 2.2); a client reading
/// responses leaves it off, so a leading empty line is refused.
///
/// ```mbt check
/// test {
/// let decoder = @h1.HeadDecoder::new()
/// assert_eq(decoder.feed(b"HTTP/1.1 200 OK\r\n").consumed, 17)
/// let result = decoder.feed(b"\r\nbody")
/// assert_eq(result.consumed, 2)
/// assert_eq(result.head, Some(b"HTTP/1.1 200 OK\r\n\r\n"))
/// let requests = @h1.HeadDecoder::new(skip_leading_crlf=true)
/// let result = requests.feed(b"\r\nGET / HTTP/1.1\r\nHost: h\r\n\r\n")
/// assert_eq(result.consumed, 29)
/// assert_eq(result.head, Some(b"GET / HTTP/1.1\r\nHost: h\r\n\r\n"))
/// }
/// ```
pub fn HeadDecoder::new(
max~ : Int32 = 16384,
skip_leading_crlf~ : Bool = false,
) -> HeadDecoder {
{
max,
seen: 0,
buffer: Buffer(),
scan: HeadScan::new(skip_leading_crlf),
complete: None,
failure: None,
}
}
///|
fn HeadDecoder::fail(self : HeadDecoder, error : ParseError) -> ParseError {
self.failure = Some(error)
self.buffer.reset()
error
}
///|
/// Takes the bytes of `bytes` that belong to the head, up to the decoder's
/// limit, and returns the head once its final CRLF CRLF arrived.
pub fn HeadDecoder::feed(
self : HeadDecoder,
bytes : BytesView,
) -> HeadResult raise ParseError {
if self.failure is Some(error) {
raise error
}
if self.complete is Some(head) {
return { head: Some(head), consumed: 0, }
}
if self.max < 0 {
raise self.fail(malformed("negative head limit"))
}
for i, byte in bytes {
// Skipped empty lines count too, so a peer cannot send them forever.
if self.seen >= self.max {
raise self.fail(too_large("head exceeds \{self.max} bytes"))
}
self.seen += 1
let done = self.scan.step(byte) catch {
ParseError(..) as error => raise self.fail(error)
}
if self.scan.leading {
continue
}
self.buffer.write_byte(byte)
if done {
let head = self.buffer.to_bytes()
self.buffer.reset()
self.complete = Some(head)
return { head: Some(head), consumed: i + 1, }
}
}
{ head: None, consumed: bytes.length(), }
}
///|
/// Returns the completed head at end of input, or refuses a truncated one.
pub fn HeadDecoder::finish(self : HeadDecoder) -> Bytes raise ParseError {
if self.failure is Some(error) {
raise error
}
match self.complete {
Some(head) => head
None => raise self.fail(malformed("truncated head"))
}
}
///|
/// Splits one complete head into its start line and its fields, after
/// any empty lines `skip_leading_crlf` allows.
fn head_lines(
head : BytesView,
limits : Limits,
skip_leading_crlf~ : Bool,
) -> (String, @mttp_core.Headers) raise ParseError {
guard scan_head(head, max=limits.max_head_bytes, skip_leading_crlf~)
is Some(end) &&
end == head.length() else {
raise malformed("expected exactly one complete head")
}
// Only skipped empty lines precede the start line, so this stops inside
// the head.
let mut start = 0
while skip_leading_crlf && head[start] is (b'\r' | b'\n') {
start += 1
}
if limits.max_headers < 0 {
raise malformed("negative field limit")
}
// scan_head proved every CR is followed by LF and every LF preceded by CR,
// so splitting on CRLF recovers the lines exactly.
let lines = latin1(head[start:head.length() - 4]).split("\r\n")
let first = lines.next().unwrap_or("")
let pairs = []
for line in lines {
if pairs.length() >= limits.max_headers {
raise too_large("more than \{limits.max_headers} fields")
}
pairs.push(field(line))
}
(first.to_owned(), @mttp_core.Headers::of(pairs))
}
///|
/// Reads `HTTP-version` (RFC 9112, section 2.3). The name is case
/// sensitive; a higher HTTP/1 minor version is read as HTTP/1.1, and another
/// major version is unsupported.
fn version(text : StringView) -> Version raise ParseError {
match text {
['H', 'T', 'T', 'P', '/', '1', '.', '0'] => Http10
['H', 'T', 'T', 'P', '/', '1', '.', '1'..<='9'] => Http11
['H', 'T', 'T', 'P', '/', '0'..<='9', '.', '0'..<='9'] =>
raise ParseError(kind=Unsupported, detail="unsupported HTTP version")
_ => raise malformed("invalid HTTP version")
}
}
///|
/// A recipient of HTTP/1.0 with Transfer-Encoding cannot trust its framing
/// (RFC 9112, section 6.1), and an HTTP/1.0 peer may frame it differently.
fn check_version_framing(
v : Version,
headers : @mttp_core.Headers,
) -> Unit raise ParseError {
if v == Http10 && headers.contains("transfer-encoding") {
raise ParseError(kind=Smuggling, detail="Transfer-Encoding on HTTP/1.0")
}
}
///|
/// Parses a strict request head: one complete head within `limits` (empty
/// lines before the request line are skipped and counted), a request line of three parts separated by single spaces, a target of the
/// form its method requires, and at most one valid `Host`, which HTTP/1.1
/// requires. Fields keep their order and duplicates. Body framing is checked
/// separately by `request_framing`, except that Transfer-Encoding on HTTP/1.0
/// is refused here as smuggling.
///
/// ```mbt check
/// test {
/// let request = @h1.parse_request_head(
/// b"GET /items?page=2 HTTP/1.1\r\nHost: example.com\r\nAccept: */*\r\n\r\n",
/// )
/// assert_eq(request.meth, Get)
/// assert_eq(request.target, "/items?page=2")
/// assert_eq(request.version, Http11)
/// assert_eq(request.headers.get("host"), Some("example.com"))
/// }
/// ```
pub fn parse_request_head(
head : BytesView,
limits~ : Limits = Limits::default(),
) -> ParsedRequest raise ParseError {
let (line, headers) = head_lines(head, limits, skip_leading_crlf=true)
let parts = line.split(" ").collect()
guard parts is [meth, target, v] && token(meth) else {
raise malformed("invalid request line")
}
if target.is_empty() ||
target.iter().any(c => c <= ' ' || c >= '\u{7f}' || c == '#') {
raise malformed("invalid request target")
}
let version = version(v)
let meth = @mttp_core.Method::parse(meth.to_owned())
let target = target.to_owned()
validate_target(meth, target)
check_version_framing(version, headers)
let hosts = headers.get_all("host")
if hosts.length() > 1 || (version == Http11 && hosts.is_empty()) {
raise malformed("expected one Host")
}
if hosts is [host] {
validate_host(host)
}
{ meth, target, version, headers, }
}
///|
/// A `Host` value is a bare authority (`uri-host [ ":" port ]`).
fn validate_host(host : String) -> Unit raise ParseError {
if host.is_empty() ||
host
.iter()
.any(c => c <= ' ' || c >= '\u{7f}' || "/\\@?#,".contains_char(c)) {
raise malformed("invalid Host")
}
let parsed = @mttp_core.Url::parse("http://" + host) catch {
_ => raise malformed("invalid Host")
}
if parsed.host is (None | Some("")) {
raise malformed("invalid Host")
}
}
///|
/// Checks the request-target form against the method (RFC 9112, section
/// 3.2): authority-form only for CONNECT, asterisk-form only for OPTIONS,
/// otherwise origin-form or an absolute http(s) URL without userinfo.
fn validate_target(
meth : @mttp_core.Method,
target : String,
) -> Unit raise ParseError {
if target.contains("\\") {
raise malformed("backslash in request target")
}
if meth == Connect {
if target.iter().any(c => "/?#@".contains_char(c)) {
raise malformed("CONNECT requires authority-form")
}
// An unknown scheme keeps every explicit port, including HTTP's 80.
let url = @mttp_core.Url::parse("connect://" + target) catch {
_ => raise malformed("invalid CONNECT authority")
}
if url.host is (None | Some("")) || url.port is None {
raise malformed("CONNECT requires host and port")
}
} else if target == "*" {
if meth != Options {
raise malformed("asterisk-form requires OPTIONS")
}
} else if !target.has_prefix("/") {
let url = @mttp_core.Url::parse(target) catch {
_ => raise malformed("invalid absolute target")
}
if !(url.scheme is Some("http" | "https")) ||
url.host is (None | Some("")) ||
url.userinfo is Some(_) {
raise malformed("expected origin-form or absolute HTTP target")
}
}
}
///|
/// Parses a strict response head: one complete head within `limits`, with
/// no empty line before it, and a status line of the version, a single space, three digits from 100 to 599
/// and an optional reason phrase after another space. A status line without
/// a reason phrase, with or without the space before it, is accepted
/// (RFC 9112, section 4). Fields keep their order, duplicates and obs-text.
///
/// ```mbt check
/// test {
/// let response = @h1.parse_response_head(
/// b"HTTP/1.1 200\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\n\r\n",
/// )
/// assert_eq(response.status, 200)
/// assert_eq(response.headers.get_all("set-cookie"), ["a=1", "b=2"])
/// }
/// ```
pub fn parse_response_head(
head : BytesView,
limits~ : Limits = Limits::default(),
) -> ParsedResponse raise ParseError {
let (line, headers) = head_lines(head, limits, skip_leading_crlf=false)
guard line.length() >= 12 &&
line[8] == ' ' &&
(line.length() == 12 || line[12] == ' ') else {
raise malformed("invalid status line")
}
let version = version(line[:8])
let mut status = 0
for c in line[9:12] {
guard c is ('0'..<='9') else { raise malformed("invalid status code") }
status = status * 10 + c.to_int() - '0'.to_int()
}
if status < 100 || status > 599 {
raise malformed("status code out of range")
}
if !line[12:].iter().all(is_field_char) {
raise malformed("invalid reason phrase")
}
check_version_framing(version, headers)
{ version, status, headers, }
}