const HEAD_BYTES: usize = 1_024;
const ROUTE_BYTES: usize = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ParserError {
HeadTooLarge,
InvalidSyntax,
Unsupported,
InvalidRoute,
InvalidHost,
InvalidContentLength,
AmbiguousFraming,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct FrozenHead {
route: [u8; ROUTE_BYTES],
route_len: usize,
content_length: usize,
}
impl FrozenHead {
pub(crate) fn route(&self) -> &[u8] {
&self.route[..self.route_len]
}
pub(crate) const fn content_length(&self) -> usize {
self.content_length
}
}
pub(crate) struct HeadParser {
bytes: [u8; HEAD_BYTES],
length: usize,
complete: bool,
}
impl HeadParser {
pub(crate) const fn new() -> Self {
Self {
bytes: [0; HEAD_BYTES],
length: 0,
complete: false,
}
}
pub(crate) fn push_byte(&mut self, byte: u8) -> Result<Option<FrozenHead>, ParserError> {
if self.complete {
return Err(ParserError::InvalidSyntax);
}
let target = self
.bytes
.get_mut(self.length)
.ok_or(ParserError::HeadTooLarge)?;
*target = byte;
self.length += 1;
if !self.bytes[..self.length].ends_with(b"\r\n\r\n") {
return Ok(None);
}
self.complete = true;
parse_frozen(&self.bytes[..self.length]).map(Some)
}
}
fn parse_frozen(head: &[u8]) -> Result<FrozenHead, ParserError> {
if head.iter().enumerate().any(|(index, byte)| {
(*byte == b'\n' && (index == 0 || head[index - 1] != b'\r'))
|| (*byte == b'\r' && head.get(index + 1) != Some(&b'\n'))
}) {
return Err(ParserError::InvalidSyntax);
}
let mut lines = head.split(|byte| *byte == b'\n');
let request = trim_cr(lines.next().ok_or(ParserError::InvalidSyntax)?)?;
let route = parse_request_line(request)?;
let mut host_seen = false;
let mut length = None;
let mut ended = false;
for raw in lines {
let line = trim_cr(raw)?;
if line.is_empty() {
ended = true;
break;
}
if line[0] == b' ' || line[0] == b'\t' {
return Err(ParserError::InvalidSyntax);
}
let colon = line
.iter()
.position(|byte| *byte == b':')
.ok_or(ParserError::InvalidSyntax)?;
let name = &line[..colon];
let value = &line[colon + 1..];
if name.eq_ignore_ascii_case(b"host") {
if host_seen || !canonical_field_value(value) || !canonical_host(&value[1..]) {
return Err(ParserError::InvalidHost);
}
host_seen = true;
} else if name.eq_ignore_ascii_case(b"content-length") {
if length.is_some() || !canonical_field_value(value) {
return Err(ParserError::InvalidContentLength);
}
length = Some(parse_decimal(&value[1..])?);
} else if name.eq_ignore_ascii_case(b"transfer-encoding")
|| name.eq_ignore_ascii_case(b"upgrade")
|| name.eq_ignore_ascii_case(b"expect")
|| name.eq_ignore_ascii_case(b"trailer")
|| name.eq_ignore_ascii_case(b"proxy-connection")
{
return Err(ParserError::AmbiguousFraming);
} else if name.eq_ignore_ascii_case(b"connection")
&& (!canonical_field_value(value) || &value[1..] != b"close")
{
return Err(ParserError::Unsupported);
}
}
if !ended || !host_seen {
return Err(ParserError::InvalidHost);
}
let content_length = length.ok_or(ParserError::InvalidContentLength)?;
let mut frozen_route = [0; ROUTE_BYTES];
frozen_route[..route.len()].copy_from_slice(route);
Ok(FrozenHead {
route: frozen_route,
route_len: route.len(),
content_length,
})
}
fn trim_cr(line: &[u8]) -> Result<&[u8], ParserError> {
line.strip_suffix(b"\r").ok_or(ParserError::InvalidSyntax)
}
fn parse_request_line(line: &[u8]) -> Result<&[u8], ParserError> {
let prefix = b"POST ";
let suffix = b" HTTP/1.1";
if !line.starts_with(prefix) || !line.ends_with(suffix) {
return Err(ParserError::Unsupported);
}
let route = &line[prefix.len()..line.len() - suffix.len()];
if route.len() < 2 || route.len() > ROUTE_BYTES || route[0] != b'/' {
return Err(ParserError::InvalidRoute);
}
let mut segment = 0;
for byte in &route[1..] {
match byte {
b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' => segment += 1,
b'/' if segment > 0 => segment = 0,
_ => return Err(ParserError::InvalidRoute),
}
}
if segment == 0 {
return Err(ParserError::InvalidRoute);
}
Ok(route)
}
fn canonical_field_value(value: &[u8]) -> bool {
value.len() > 1
&& value[0] == b' '
&& !value[1..]
.iter()
.any(|byte| matches!(byte, b' ' | b'\t' | b'\r' | b'\n' | 0))
}
fn canonical_host(value: &[u8]) -> bool {
if value.is_empty() || value.len() > 128 {
return false;
}
let (host, port) = match value.iter().rposition(|byte| *byte == b':') {
Some(colon) if !value[..colon].contains(&b':') => {
(&value[..colon], Some(&value[colon + 1..]))
}
Some(_) => return false,
None => (value, None),
};
if host.is_empty()
|| host.split(|byte| *byte == b'.').any(|label| {
label.is_empty()
|| label.len() > 63
|| !(label[0].is_ascii_lowercase() || label[0].is_ascii_digit())
|| !(label[label.len() - 1].is_ascii_lowercase()
|| label[label.len() - 1].is_ascii_digit())
|| label.iter().any(|byte| {
!byte.is_ascii_lowercase() && !byte.is_ascii_digit() && *byte != b'-'
})
})
{
return false;
}
match port {
None => true,
Some(port)
if !port.is_empty()
&& port.iter().all(u8::is_ascii_digit)
&& (port.len() == 1 || port[0] != b'0') =>
{
port.iter()
.try_fold(0_u32, |value, digit| {
value.checked_mul(10)?.checked_add(u32::from(digit - b'0'))
})
.is_some_and(|port| (1..=u32::from(u16::MAX)).contains(&port))
}
Some(_) => false,
}
}
fn parse_decimal(value: &[u8]) -> Result<usize, ParserError> {
if value.is_empty()
|| !value.iter().all(u8::is_ascii_digit)
|| (value.len() > 1 && value[0] == b'0')
{
return Err(ParserError::InvalidContentLength);
}
value.iter().try_fold(0_usize, |total, digit| {
total
.checked_mul(10)
.and_then(|value| value.checked_add(usize::from(digit - b'0')))
.ok_or(ParserError::InvalidContentLength)
})
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(value: &[u8]) -> Result<FrozenHead, ParserError> {
let mut parser = HeadParser::new();
for byte in value {
if let Some(head) = parser.push_byte(*byte)? {
return Ok(head);
}
}
Err(ParserError::InvalidSyntax)
}
#[test]
fn freezes_only_after_canonical_head() {
let head = parse(
b"POST /orders/create HTTP/1.1\r\nHost: api.example\r\nContent-Length: 5\r\nConnection: close\r\n\r\n",
)
.unwrap();
assert_eq!(head.route(), b"/orders/create");
assert_eq!(head.content_length(), 5);
}
#[test]
fn rejects_ambiguous_framing_and_normalization() {
assert_eq!(
parse(b"POST /x HTTP/1.1\r\nHost: api\r\nContent-Length: 1\r\nTransfer-Encoding: chunked\r\n\r\n")
.unwrap_err(),
ParserError::AmbiguousFraming
);
assert_eq!(
parse(b"POST /X HTTP/1.1\r\nHost: api\r\nContent-Length: 1\r\n\r\n").unwrap_err(),
ParserError::InvalidRoute
);
assert_eq!(
parse(b"POST /x HTTP/1.1\r\nHost: api\r\nContent-Length: 01\r\n\r\n").unwrap_err(),
ParserError::InvalidContentLength
);
}
#[test]
fn host_matches_the_approved_authority_subset() {
for host in [
b"Api.example".as_slice(),
b"-api.example".as_slice(),
b"api-.example".as_slice(),
b"api..example".as_slice(),
b"api.example:0".as_slice(),
b"api.example:080".as_slice(),
b"api.example:65536".as_slice(),
b"api.example:1:2".as_slice(),
] {
assert!(!canonical_host(host), "{host:?}");
}
for host in [
b"api.example".as_slice(),
b"127.0.0.1".as_slice(),
b"api-1.example:443".as_slice(),
] {
assert!(canonical_host(host), "{host:?}");
}
}
}