use crate::{Error, parse};
use base_http::{Fields, Method, Protocol, RequestMetadata, ResponseMetadata, Status, Version};
use smallvec::SmallVec;
pub(crate) fn token(bytes: &[u8]) -> bool {
!bytes.is_empty()
&& bytes.iter().all(|b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
})
}
pub(crate) fn has_token(fields: &Fields, name: &str, token: &[u8]) -> bool {
fields
.pairs()
.iter()
.filter(|f| f.name().eq_ignore_ascii_case(name))
.flat_map(|f| f.value().split(|b| *b == b','))
.any(|part| trim(part).eq_ignore_ascii_case(token))
}
pub(crate) fn trim(mut value: &[u8]) -> &[u8] {
while value.first().is_some_and(|b| matches!(b, b' ' | b'\t')) {
value = &value[1..];
}
while value.last().is_some_and(|b| matches!(b, b' ' | b'\t')) {
value = &value[..value.len() - 1];
}
value
}
pub(crate) fn singleton<'a>(fields: &'a Fields, name: &str) -> Result<Option<&'a [u8]>, Error> {
let mut values = fields
.pairs()
.iter()
.filter(|f| f.name().eq_ignore_ascii_case(name));
let value = values.next().map(|f| trim(f.value()));
if values.next().is_some() {
return Err(Error::Invalid("duplicate singleton field"));
}
Ok(value)
}
pub(crate) fn framing(fields: &Fields) -> Result<(Option<u64>, bool), Error> {
parse_framing(
singleton(fields, "content-length")?,
singleton(fields, "transfer-encoding")?,
)
}
pub(crate) fn parse_framing(
length: Option<&[u8]>,
transfer: Option<&[u8]>,
) -> Result<(Option<u64>, bool), Error> {
let length = length
.map(|bytes| {
if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
return Err(Error::Invalid("invalid content-length"));
}
std::str::from_utf8(bytes)
.unwrap()
.parse::<u64>()
.map_err(|_| Error::Invalid("content-length overflow"))
})
.transpose()?;
if transfer.is_some() && length.is_some() {
return Err(Error::Invalid("content-length with transfer-encoding"));
}
if transfer.is_some_and(|v| !v.eq_ignore_ascii_case(b"chunked")) {
return Err(Error::Invalid("unsupported transfer coding"));
}
Ok((length, transfer.is_some()))
}
pub(crate) fn output_fields(fields: &[base_http::Field]) -> Result<(), Error> {
if fields.iter().any(base_http::Field::has_dangerous_bytes) {
return Err(Error::Invalid("field value contains CR, LF or NUL"));
}
Ok(())
}
const REG_NAME: [bool; 256] = {
let mut table = [false; 256];
let mut byte = 0;
while byte < 256 {
let b = byte as u8;
table[byte] = b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'.'
| b'_'
| b'~'
| b'%'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b';'
| b'='
);
byte += 1;
}
table
};
#[inline]
fn simple_authority(value: &[u8], require_port: bool) -> bool {
let (host, port) = match value.iter().rposition(|b| *b == b':') {
Some(colon) => (&value[..colon], Some(&value[colon + 1..])),
None => (value, None),
};
if host.is_empty()
|| !host
.iter()
.fold(true, |valid, b| valid & REG_NAME[usize::from(*b)])
{
return false;
}
match port {
None => !require_port,
Some(port) => {
if port.is_empty() || port.len() > 5 {
return false;
}
let (number, digits) = port.iter().fold((0u32, true), |(number, digits), b| {
(
number * 10 + u32::from(b.wrapping_sub(b'0')),
digits & b.is_ascii_digit(),
)
});
digits && number <= u32::from(u16::MAX)
}
}
}
pub(crate) fn authority(value: &str, require_port: bool) -> Result<(), Error> {
if simple_authority(value.as_bytes(), require_port) {
return Ok(());
}
if value.is_empty()
|| !value.is_ascii()
|| value
.bytes()
.any(|b| b <= 32 || b == 127 || matches!(b, b'/' | b'?' | b'#' | b'@' | b'\\' | b','))
{
return Err(Error::Invalid("invalid authority"));
}
let port = if let Some(value) = value.strip_prefix('[') {
let (host, rest) = value
.split_once(']')
.ok_or(Error::Invalid("invalid IPv6 authority"))?;
host.parse::<std::net::Ipv6Addr>()
.map_err(|_| Error::Invalid("invalid IPv6 authority"))?;
if rest.is_empty() {
None
} else {
Some(
rest.strip_prefix(':')
.ok_or(Error::Invalid("invalid port"))?,
)
}
} else {
let (host, port) = value
.split_once(':')
.map_or((value, None), |(h, p)| (h, Some(p)));
if !host.bytes().all(|b| REG_NAME[usize::from(b)]) {
return Err(Error::Invalid("invalid authority host"));
}
if host.is_empty() {
return Err(Error::Invalid("missing authority host"));
}
port
};
if let Some(port) = port {
if port.is_empty()
|| !port.bytes().all(|b| b.is_ascii_digit())
|| port.parse::<u16>().is_err()
{
return Err(Error::Invalid("invalid port"));
}
} else if require_port {
return Err(Error::Invalid("CONNECT requires a port"));
}
Ok(())
}
pub(crate) fn request_target(method: &Method, target: &str) -> Result<(), Error> {
if target.is_empty()
|| !target.is_ascii()
|| target
.bytes()
.any(|b| b <= 32 || b == 127 || matches!(b, b'#' | b'\\'))
{
return Err(Error::Invalid("invalid request target"));
}
target_form(method, target)
}
pub(crate) fn target_form(method: &Method, target: &str) -> Result<(), Error> {
if *method == Method::CONNECT {
authority(target, true)
} else if target.starts_with('/') || (target == "*" && *method == Method::OPTIONS) {
Ok(())
} else if let Some((scheme, rest)) = target.split_once("://") {
if !scheme.eq_ignore_ascii_case("http") && !scheme.eq_ignore_ascii_case("https") {
return Err(Error::Invalid("unsupported target scheme"));
}
let end = rest.find(['/', '?']).unwrap_or(rest.len());
authority(&rest[..end], false)
} else {
Err(Error::Invalid("invalid request target form"))
}
}
pub(crate) fn connection_options(fields: &Fields) -> Result<(), Error> {
for value in fields.values("connection") {
for option in value
.split(|b| *b == b',')
.map(trim)
.filter(|v| !v.is_empty())
{
if !token(option) {
return Err(Error::Invalid("invalid connection option"));
}
}
}
Ok(())
}
pub(crate) struct ResponseFraming<'a> {
pub length: Option<u64>,
pub chunked: bool,
pub close: bool,
pub keep_alive: bool,
pub connection_upgrade: bool,
pub trailer: bool,
pub upgrade: Option<&'a [u8]>,
pub duplicate_upgrade: bool,
}
pub(crate) fn response_framing(fields: &Fields) -> Result<ResponseFraming<'_>, Error> {
let mut length = None;
let mut transfer = None;
let mut facts = ResponseFraming {
length: None,
chunked: false,
close: false,
keep_alive: false,
connection_upgrade: false,
trailer: false,
upgrade: None,
duplicate_upgrade: false,
};
for field in fields.pairs() {
let name = field.name();
if name.eq_ignore_ascii_case("content-length") {
set_singleton(&mut length, field.value())?;
} else if name.eq_ignore_ascii_case("transfer-encoding") {
set_singleton(&mut transfer, field.value())?;
} else if name.eq_ignore_ascii_case("connection") {
for option in field.value().split(|b| *b == b',').map(trim) {
facts.close |= option.eq_ignore_ascii_case(b"close");
facts.keep_alive |= option.eq_ignore_ascii_case(b"keep-alive");
facts.connection_upgrade |= option.eq_ignore_ascii_case(b"upgrade");
}
} else if name.eq_ignore_ascii_case("trailer") {
facts.trailer = true;
} else if name.eq_ignore_ascii_case("upgrade") {
facts.duplicate_upgrade |= facts.upgrade.replace(trim(field.value())).is_some();
}
}
(facts.length, facts.chunked) = parse_framing(length, transfer)?;
Ok(facts)
}
#[derive(Debug, PartialEq)]
pub(crate) struct RequestFraming {
pub length: Option<u64>,
pub chunked: bool,
pub persistent: bool,
pub expect_continue: bool,
pub trailer_options: Vec<Vec<u8>>,
pub upgrade: Option<Box<[u8]>>,
}
pub(crate) fn set_singleton<'a>(slot: &mut Option<&'a [u8]>, value: &'a [u8]) -> Result<(), Error> {
if slot.replace(trim(value)).is_some() {
return Err(Error::Invalid("duplicate singleton field"));
}
Ok(())
}
#[derive(Clone, Copy)]
pub(crate) enum Control {
Host,
ContentLength,
TransferEncoding,
Connection,
Upgrade,
Removed,
}
pub(crate) fn request_control(name: &[u8]) -> Option<Control> {
let is = |control: &[u8]| name.eq_ignore_ascii_case(control);
match name.len() {
2 if is(b"te") => Some(Control::Removed),
4 if is(b"host") => Some(Control::Host),
7 if is(b"upgrade") => Some(Control::Upgrade),
10 if is(b"connection") => Some(Control::Connection),
10 if is(b"keep-alive") => Some(Control::Removed),
14 if is(b"content-length") => Some(Control::ContentLength),
16 if is(b"proxy-connection") => Some(Control::Removed),
17 if is(b"transfer-encoding") => Some(Control::TransferEncoding),
_ => None,
}
}
pub(crate) fn request(
bytes: &[u8],
max_fields: usize,
) -> Result<Option<(RequestMetadata, RequestFraming, usize)>, Error> {
let Some((line, at)) = parse::request_line(bytes)? else {
return Ok(None);
};
let method = match line.method {
b"GET" => Method::GET,
b"HEAD" => Method::HEAD,
b"POST" => Method::POST,
b"PUT" => Method::PUT,
b"DELETE" => Method::DELETE,
b"CONNECT" => Method::CONNECT,
b"OPTIONS" => Method::OPTIONS,
b"TRACE" => Method::TRACE,
b"PATCH" => Method::PATCH,
b"QUERY" => Method::QUERY,
other => Method::new(std::str::from_utf8(other).unwrap())
.expect("the parser accepts only token methods"),
};
let mut lines = parse::Lines::new(bytes, at, max_fields);
let mut fields: SmallVec<[parse::Field; 16]> = SmallVec::new();
let length_of_head = loop {
match lines.next()? {
parse::Line::Field(field) => fields.push(field),
parse::Line::End(n) => break n,
parse::Line::Incomplete => return Ok(None),
}
};
let text = std::str::from_utf8(&bytes[..length_of_head]).ok();
let target = line.target.ascii(text, bytes);
target_form(&method, target)?;
let version = line.version;
let mut head = RequestMetadata::new(method);
head.version = Some(version);
if fields.len() > 3 {
head.fields.reserve(fields.len());
}
let mut host = None;
let mut length = None;
let mut transfer = None;
let mut close = false;
let mut keep_alive = false;
let mut connection_upgrade = false;
let mut expect_continue = false;
let mut first_upgrade = None;
let mut upgrade_count = 0;
let mut options: Vec<Vec<u8>> = Vec::new();
let mut offers = Vec::new();
for field in &fields {
let name = field.name.of(bytes);
let value = field.value.of(bytes);
match request_control(name) {
Some(Control::Host) => {
if host.replace(field.value).is_some() {
return Err(Error::Invalid("duplicate singleton field"));
}
}
Some(Control::ContentLength) => set_singleton(&mut length, value)?,
Some(Control::TransferEncoding) => set_singleton(&mut transfer, value)?,
Some(Control::Connection) => {
for option in value.split(|b| *b == b',').map(trim) {
close |= option.eq_ignore_ascii_case(b"close");
keep_alive |= option.eq_ignore_ascii_case(b"keep-alive");
connection_upgrade |= option.eq_ignore_ascii_case(b"upgrade");
if !option.is_empty() {
if !token(option) {
return Err(Error::Invalid("invalid connection option"));
}
options.push(option.to_vec());
}
}
}
Some(Control::Upgrade) => {
first_upgrade.get_or_insert(value);
upgrade_count += 1;
if !offers.is_empty() {
offers.push(b',');
}
offers.extend_from_slice(value);
}
Some(Control::Removed) => {}
None => {
if name.eq_ignore_ascii_case(b"expect") {
expect_continue |= value
.split(|b| *b == b',')
.any(|v| trim(v).eq_ignore_ascii_case(b"100-continue"));
}
head.fields
.add(field.name.ascii(text, bytes), value)
.expect("the parser accepts only token field names");
}
}
}
if !options.is_empty() {
head.fields.retain(|field| {
!options
.iter()
.any(|option| field.name().as_bytes().eq_ignore_ascii_case(option))
});
}
let host = host
.map(|host| host.text(text, bytes).ok_or(Error::Invalid("invalid host")))
.transpose()?;
if version == Version::Http11 && host.is_none() {
return Err(Error::Invalid("missing host"));
}
if let Some(host) = host {
authority(host, false)?;
head.authority = Some(host.into());
}
if head.method == Method::CONNECT {
head.authority = Some(target.into());
} else if target.starts_with('/') || (target == "*" && head.method == Method::OPTIONS) {
head.path = Some(target.into());
} else {
let (scheme, rest) = target
.split_once("://")
.expect("other accepted targets are absolute");
let end = rest.find(['/', '?']).unwrap_or(rest.len());
head.authority = Some(rest[..end].into());
head.scheme = Some(scheme.into());
let suffix = &rest[end..];
head.path = Some(if suffix.is_empty() {
"/".into()
} else if suffix.starts_with('?') {
format!("/{suffix}").into()
} else {
suffix.into()
});
}
let upgrade = if connection_upgrade {
if version == Version::Http11 {
let first = first_upgrade.ok_or(Error::Invalid("missing upgrade protocol"))?;
if upgrade_count == 1 {
if let Ok(offer) = std::str::from_utf8(trim(first)) {
head.protocol = Protocol::new(offer).ok();
}
}
}
Some(offers.into_boxed_slice())
} else {
None
};
let (length, chunked) = parse_framing(length, transfer)?;
head.content_length = length;
let framing = RequestFraming {
length,
chunked,
persistent: !close && (version == Version::Http11 || keep_alive),
expect_continue,
trailer_options: options,
upgrade,
};
Ok(Some((head, framing, length_of_head)))
}
fn field_section(
bytes: &[u8],
at: usize,
max_fields: usize,
) -> Result<Option<(Fields, usize)>, Error> {
let mut lines = parse::Lines::new(bytes, at, max_fields);
let mut spans: SmallVec<[parse::Field; 16]> = SmallVec::new();
let end = loop {
match lines.next()? {
parse::Line::Field(field) => spans.push(field),
parse::Line::End(n) => break n,
parse::Line::Incomplete => return Ok(None),
}
};
let text = std::str::from_utf8(&bytes[..end]).ok();
let mut fields = Fields::with_capacity(spans.len());
for field in &spans {
fields
.add(field.name.ascii(text, bytes), field.value.of(bytes))
.expect("the parser accepts only token field names");
}
Ok(Some((fields, end)))
}
pub(crate) fn response(
bytes: &[u8],
max_fields: usize,
) -> Result<Option<(ResponseMetadata, usize)>, Error> {
let Some((line, at)) = parse::status_line(bytes)? else {
return Ok(None);
};
let Some((fields, end)) = field_section(bytes, at, max_fields)? else {
return Ok(None);
};
let status = Status::new(line.code).map_err(|_| Error::Invalid("invalid status"))?;
let mut head = ResponseMetadata::new(status);
head.version = Some(line.version);
head.fields = fields;
Ok(Some((head, end)))
}
pub(crate) fn trailers(bytes: &[u8], max_fields: usize) -> Result<Option<(Fields, usize)>, Error> {
field_section(bytes, 0, max_fields).map_err(|error| match error {
Error::Limit("field count") => Error::Limit("trailer count"),
error => error,
})
}
pub(crate) fn application_fields(mut fields: Fields) -> Result<(Fields, Vec<Vec<u8>>), Error> {
let options: Vec<Vec<u8>> = fields
.values("connection")
.flat_map(|value| value.split(|b| *b == b','))
.map(trim)
.filter(|option| !option.is_empty())
.map(|option| {
if !token(option) {
return Err(Error::Invalid("invalid connection option"));
}
Ok(option.to_vec())
})
.collect::<Result<_, _>>()?;
fields.retain(|field| {
let name = field.name();
!([
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"content-length",
"upgrade",
"te",
]
.iter()
.any(|control| name.eq_ignore_ascii_case(control))
|| options
.iter()
.any(|option| name.as_bytes().eq_ignore_ascii_case(option)))
});
Ok((fields, options))
}
pub(crate) fn declared_length(typed: Option<u64>, raw: Option<u64>) -> Result<Option<u64>, Error> {
if typed.is_some() && raw.is_some() && typed != raw {
return Err(Error::Invalid("conflicting content-length"));
}
Ok(typed.or(raw))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_authority_only_accepts_valid_authorities() {
for (value, require_port) in [
("example", false),
("example:80", false),
("127.0.0.1:65535", true),
("example:0", false),
("example:65535", false),
("example:00080", false),
("example.", false),
("a-b.c_d~e%20!$&'()*+;=", false),
] {
assert!(simple_authority(value.as_bytes(), require_port), "{value}");
assert!(authority(value, require_port).is_ok(), "{value}");
}
for (value, require_port) in [
("", false),
("example", true),
("example:", false),
("example:65536", false),
("example:123456", false),
("example:8a", false),
("example:80:80", false),
("exa mple", false),
("user@example", false),
("example,other", false),
("example/path", false),
("[::1]:80", false),
(":80", false),
("ex\u{e9}mple", false),
] {
assert!(!simple_authority(value.as_bytes(), require_port), "{value}");
}
assert!(!simple_authority(b"example:000080", false));
assert!(authority("example:000080", false).is_ok());
assert!(authority("[::1]:80", true).is_ok());
}
}