use std::ops::Range;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Head {
pub(crate) method: Range<usize>,
pub(crate) target: Range<usize>,
pub(crate) minor: u8,
pub(crate) headers: Range<usize>,
pub(crate) len: usize,
pub(crate) content_length: Option<u64>,
pub(crate) chunked: bool,
pub(crate) close: bool,
pub(crate) keep_alive: bool,
pub(crate) expect_continue: bool,
}
pub(crate) fn find_head_end(buf: &[u8], from: usize) -> Option<usize> {
let mut i = from;
while let Some(off) = buf.get(i..)?.iter().position(|&b| b == b'\n') {
let nl = i + off;
if nl >= 3 && &buf[nl - 3..nl] == b"\r\n\r" {
return Some(nl + 1);
}
i = nl + 1;
}
None
}
fn is_token(b: u8) -> bool {
matches!(b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
b'^' | b'_' | b'`' | b'|' | b'~' | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')
}
pub(crate) fn is_token_str(s: &[u8]) -> bool {
!s.is_empty() && s.iter().all(|&b| is_token(b))
}
pub(crate) fn is_field_value(s: &[u8]) -> bool {
s.iter().all(|&b| b == b'\t' || (b >= 0x20 && b != 0x7f))
}
pub(crate) fn trim(mut s: &[u8]) -> &[u8] {
while let [b' ' | b'\t', rest @ ..] = s {
s = rest;
}
while let [rest @ .., b' ' | b'\t'] = s {
s = rest;
}
s
}
pub(crate) fn header_lines(block: &[u8]) -> impl Iterator<Item = (&[u8], &[u8])> {
block
.split(|&b| b == b'\n')
.filter(|line| !line.is_empty())
.filter_map(|line| {
let line = line.strip_suffix(b"\r").unwrap_or(line);
let colon = line.iter().position(|&b| b == b':')?;
Some((&line[..colon], trim(&line[colon + 1..])))
})
}
pub(crate) fn parse_head(head: &[u8]) -> Result<Head, u16> {
debug_assert!(head.ends_with(b"\r\n\r\n"));
let line_end = head
.iter()
.position(|&b| b == b'\n')
.filter(|&i| i > 0 && head[i - 1] == b'\r')
.ok_or(400u16)?;
let line = &head[..line_end - 1];
let sp1 = line.iter().position(|&b| b == b' ').ok_or(400u16)?;
let sp2 = sp1
+ 1
+ line[sp1 + 1..]
.iter()
.position(|&b| b == b' ')
.ok_or(400u16)?;
let (method, target, version) = (&line[..sp1], &line[sp1 + 1..sp2], &line[sp2 + 1..]);
if !is_token_str(method) || method.len() > 32 {
return Err(400);
}
if target.is_empty() || !target.iter().all(|&b| (0x21..=0x7e).contains(&b)) {
return Err(400);
}
let minor = match version {
b"HTTP/1.1" => 1,
b"HTTP/1.0" => 0,
[b'H', b'T', b'T', b'P', b'/', major, b'.', minor]
if major.is_ascii_digit() && minor.is_ascii_digit() =>
{
return Err(505)
}
_ => return Err(400),
};
let headers = line_end + 1..head.len() - 2;
let mut parsed = Head {
method: 0..sp1,
target: sp1 + 1..sp2,
minor,
headers: headers.clone(),
len: head.len(),
content_length: None,
chunked: false,
close: false,
keep_alive: false,
expect_continue: false,
};
let mut hosts = 0;
let mut transfer_encoding = false;
let mut rest = &head[headers];
while !rest.is_empty() {
let nl = rest.iter().position(|&b| b == b'\n').ok_or(400u16)?;
if nl == 0 || rest[nl - 1] != b'\r' {
return Err(400);
}
let line = &rest[..nl - 1];
rest = &rest[nl + 1..];
let colon = line.iter().position(|&b| b == b':').ok_or(400u16)?;
let (name, value) = (&line[..colon], trim(&line[colon + 1..]));
if !is_token_str(name) || !is_field_value(value) {
return Err(400);
}
if name.eq_ignore_ascii_case(b"content-length") {
if value.is_empty() || value.len() > 19 || !value.iter().all(u8::is_ascii_digit) {
return Err(400);
}
let n = value
.iter()
.fold(0u64, |acc, &b| acc * 10 + (b - b'0') as u64);
if parsed.content_length.is_some_and(|prev| prev != n) {
return Err(400);
}
parsed.content_length = Some(n);
} else if name.eq_ignore_ascii_case(b"transfer-encoding") {
if transfer_encoding || minor == 0 {
return Err(400);
}
if !value.eq_ignore_ascii_case(b"chunked") {
return Err(501);
}
transfer_encoding = true;
parsed.chunked = true;
} else if name.eq_ignore_ascii_case(b"connection") {
for token in value.split(|&b| b == b',').map(trim) {
if token.eq_ignore_ascii_case(b"close") {
parsed.close = true;
} else if token.eq_ignore_ascii_case(b"keep-alive") {
parsed.keep_alive = true;
}
}
} else if name.eq_ignore_ascii_case(b"expect") {
if !value.eq_ignore_ascii_case(b"100-continue") {
return Err(417);
}
parsed.expect_continue = true;
} else if name.eq_ignore_ascii_case(b"host") {
hosts += 1;
}
}
if parsed.chunked && parsed.content_length.is_some() {
return Err(400);
}
if hosts > 1 || (minor == 1 && hosts == 0) {
return Err(400);
}
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(s: &str) -> Result<Head, u16> {
parse_head(s.as_bytes())
}
#[test]
fn finds_the_end_of_a_head_from_any_offset() {
let buf = b"GET / HTTP/1.1\r\nHost: x\r\n\r\nrest";
assert_eq!(find_head_end(buf, 0), Some(27));
for from in 0..=26 {
assert_eq!(find_head_end(buf, from.min(23)), Some(27), "from {from}");
}
assert_eq!(find_head_end(&buf[..26], 0), None);
assert_eq!(find_head_end(b"\n\n\r\n", 0), None);
assert_eq!(find_head_end(b"", 0), None);
assert_eq!(find_head_end(b"abc", 7), None);
}
#[test]
fn parses_a_typical_request() {
let s = "POST /a/b?x=1 HTTP/1.1\r\nHost: example.com\r\nContent-Length: 12\r\nConnection: Keep-Alive, Upgrade\r\n\r\n";
let h = parse(s).unwrap();
assert_eq!(&s[h.method.clone()], "POST");
assert_eq!(&s[h.target.clone()], "/a/b?x=1");
assert_eq!(h.minor, 1);
assert_eq!(h.len, s.len());
assert_eq!(h.content_length, Some(12));
assert!(h.keep_alive && !h.close && !h.chunked && !h.expect_continue);
let lines: Vec<_> = header_lines(&s.as_bytes()[h.headers.clone()]).collect();
assert_eq!(lines.len(), 3);
assert_eq!(lines[0], (&b"Host"[..], &b"example.com"[..]));
}
#[test]
fn recognises_framing_and_connection_headers() {
let h = parse("PUT / HTTP/1.1\r\nhost: x\r\nTRANSFER-ENCODING: Chunked\r\nconnection: close\r\nExpect: 100-Continue\r\n\r\n").unwrap();
assert!(h.chunked && h.close && h.expect_continue);
assert_eq!(h.content_length, None);
let h = parse("GET / HTTP/1.0\r\n\r\n").unwrap();
assert_eq!(h.minor, 0);
assert!(parse(
"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 3\r\nContent-Length: 3\r\n\r\n"
)
.is_ok());
}
#[test]
fn rejects_malformed_and_ambiguous_heads() {
let cases: &[(&str, u16)] = &[
("GET /\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\n\r\n", 400),
("GET / HTTP/1.1 \r\nHost: x\r\n\r\n", 400),
("GET / HTTP/2.0\r\nHost: x\r\n\r\n", 505),
("GET / FTP/1.1\r\nHost: x\r\n\r\n", 400),
("G@T / HTTP/1.1\r\nHost: x\r\n\r\n", 400),
("GET /a\tb HTTP/1.1\r\nHost: x\r\n\r\n", 400),
("GET /\u{e9} HTTP/1.1\r\nHost: x\r\n\r\n", 400),
("GET / HTTP/1.1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nNoColon\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nBad Name: 1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nName : 1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\n folded: 1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\n: empty\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nA: b\0c\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\nA: b\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nA: b\rc\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 1, 1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: -1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: +1\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: \r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 99999999999999999999\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: chunked\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: gzip, chunked\r\n\r\n", 501),
("GET / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n", 400),
("GET / HTTP/1.1\r\nHost: x\r\nExpect: something\r\n\r\n", 417),
];
for (input, status) in cases {
assert_eq!(parse(input), Err(*status), "{input:?}");
}
}
}