use alloc::vec::Vec;
use core::{fmt, slice};
use crate::{
error::HttpError,
limits::Limits,
method::Method,
redact::{FieldName, FieldValue},
syntax::{
check_field_value, find_byte, is_host_value, is_ows, is_reason, is_target_chars, is_tchar,
is_token, list_elements, parse_decimal, trim_ows,
},
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) struct Span {
start: u32,
end: u32,
}
impl Span {
#[inline]
fn new(start: usize, end: usize) -> Self {
Self {
start: start as u32,
end: end as u32,
}
}
#[inline]
pub(crate) fn get<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
buf.get(self.start as usize..self.end as usize)
.unwrap_or(&[])
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Field {
name: Span,
value: Span,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum HeadKind {
Request,
Response,
Trailer,
}
pub(crate) enum Progress {
Partial,
Done(usize),
}
pub(crate) fn find_line(
msg: &[u8],
line_start: usize,
scan: &mut usize,
budget: usize,
too_long: HttpError,
) -> Result<Option<usize>, HttpError> {
let from = (*scan).max(line_start);
let hay = msg.get(from..).unwrap_or(&[]);
match find_byte(b'\n', hay) {
Some(i) => {
let lf = from + i;
if lf + 1 - line_start > budget {
return Err(too_long);
}
if lf == line_start || msg.get(lf - 1) != Some(&b'\r') {
return Err(HttpError::BareLf);
}
*scan = lf + 1;
Ok(Some(lf - 1))
}
None => {
*scan = msg.len();
if msg.len().saturating_sub(line_start) >= budget {
Err(too_long)
} else {
Ok(None)
}
}
}
}
pub(crate) struct HeadScanner {
scan: usize,
line_start: usize,
start_line: Option<Span>,
skipped: usize,
field_bytes: usize,
count: usize,
pub(crate) fields: Vec<Field>,
}
impl HeadScanner {
pub(crate) const fn new() -> Self {
Self {
scan: 0,
line_start: 0,
start_line: None,
skipped: 0,
field_bytes: 0,
count: 0,
fields: Vec::new(),
}
}
pub(crate) fn reset(&mut self) {
self.scan = 0;
self.line_start = 0;
self.start_line = None;
self.skipped = 0;
self.field_bytes = 0;
self.count = 0;
self.fields.clear();
}
pub(crate) fn reset_lines(&mut self) {
self.scan = 0;
self.line_start = 0;
self.skipped = 0;
self.field_bytes = 0;
self.count = 0;
}
pub(crate) fn start_line(&self) -> Span {
self.start_line.unwrap_or_default()
}
pub(crate) fn advance(
&mut self,
msg: &[u8],
limits: &Limits,
kind: HeadKind,
store: bool,
) -> Result<Progress, HttpError> {
loop {
let in_start = kind != HeadKind::Trailer && self.start_line.is_none();
let (budget, too_long) = if in_start {
(
limits.max_start_line().saturating_sub(self.skipped),
HttpError::StartLineTooLong,
)
} else {
(
limits.max_header_bytes().saturating_sub(self.field_bytes),
HttpError::HeadersTooLarge,
)
};
let Some(end) = find_line(msg, self.line_start, &mut self.scan, budget, too_long)?
else {
return Ok(Progress::Partial);
};
let start = self.line_start;
let line = msg.get(start..end).unwrap_or(&[]);
let line_len = self.scan - start;
self.line_start = self.scan;
if in_start {
if line.is_empty() {
if kind == HeadKind::Request {
self.skipped += line_len;
continue;
}
return Err(HttpError::InvalidStartLine);
}
if find_byte(b'\r', line).is_some() {
return Err(HttpError::BareCr);
}
self.start_line = Some(Span::new(start, end));
continue;
}
if line.is_empty() {
return Ok(Progress::Done(self.scan));
}
self.field_bytes += line_len;
if self.field_bytes + 2 > limits.max_header_bytes() {
return Err(HttpError::HeadersTooLarge);
}
if self.count >= limits.max_headers() {
return Err(HttpError::TooManyHeaders);
}
let field = parse_field(line, start)?;
self.count += 1;
if store {
self.fields.push(field);
}
}
}
}
fn parse_field(line: &[u8], base: usize) -> Result<Field, HttpError> {
if line.first().is_some_and(|&b| is_ows(b)) {
return Err(HttpError::ObsFold);
}
let Some(colon) = find_byte(b':', line) else {
return Err(if find_byte(b'\r', line).is_some() {
HttpError::BareCr
} else {
HttpError::InvalidHeaderName
});
};
let name = &line[..colon];
if name.last().is_some_and(|&b| is_ows(b)) {
return Err(HttpError::WhitespaceBeforeColon);
}
if !name.iter().all(|&b| is_tchar(b)) || name.is_empty() {
return Err(if find_byte(b'\r', name).is_some() {
HttpError::BareCr
} else {
HttpError::InvalidHeaderName
});
}
let raw = &line[colon + 1..];
let lead = raw.iter().take_while(|&&b| is_ows(b)).count();
let value = trim_ows(raw);
check_field_value(value)?;
let value_start = base + colon + 1 + lead;
Ok(Field {
name: Span::new(base, base + colon),
value: Span::new(value_start, value_start + value.len()),
})
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Version {
Http10,
Http11,
}
impl Version {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Http10 => "HTTP/1.0",
Self::Http11 => "HTTP/1.1",
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BodyFraming {
None,
Length(u64),
Chunked,
UntilClose,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TargetForm {
Origin,
Absolute,
Authority,
Asterisk,
}
pub(crate) fn is_h2c(protocol: &[u8]) -> bool {
let name = protocol
.iter()
.position(|&b| b == b'/')
.map_or(protocol, |i| &protocol[..i]);
name.eq_ignore_ascii_case(b"h2c")
}
pub(crate) fn protocol_matches(chosen: &[u8], element: &[u8]) -> bool {
if is_h2c(element) || is_h2c(chosen) {
return false;
}
if chosen.eq_ignore_ascii_case(element) {
return true;
}
!chosen.contains(&b'/')
&& element
.iter()
.position(|&b| b == b'/')
.is_some_and(|i| element[..i].eq_ignore_ascii_case(chosen))
}
fn parse_version(v: &[u8]) -> Result<Version, HttpError> {
match v {
[b'H', b'T', b'T', b'P', b'/', major, b'.', minor]
if major.is_ascii_digit() && minor.is_ascii_digit() =>
{
match (major, minor) {
(b'1', b'0') => Ok(Version::Http10),
(b'1', _) => Ok(Version::Http11),
_ => Err(HttpError::UnsupportedVersion),
}
}
_ => Err(HttpError::InvalidVersion),
}
}
#[derive(Default)]
struct FieldFacts {
content_length: Option<u64>,
te_seen: bool,
te_codings: usize,
te_chunked: usize,
te_last_chunked: bool,
te_other: bool,
close: bool,
keep_alive: bool,
upgrade_token: bool,
upgrade_field: bool,
upgrade_offer: bool,
expect_continue: bool,
hosts: usize,
host: Option<Span>,
}
fn scan_fields(head: &[u8], fields: &[Field]) -> Result<FieldFacts, HttpError> {
let mut facts = FieldFacts::default();
for field in fields {
let name = field.name.get(head);
let value = field.value.get(head);
if name.eq_ignore_ascii_case(b"content-length") {
if facts.content_length.is_some() {
return Err(HttpError::DuplicateContentLength);
}
facts.content_length =
Some(parse_decimal(value).ok_or(HttpError::InvalidContentLength)?);
} else if name.eq_ignore_ascii_case(b"transfer-encoding") {
facts.te_seen = true;
for element in list_elements(value) {
let (coding, params) = match find_byte(b';', element) {
Some(i) => (trim_ows(&element[..i]), true),
None => (element, false),
};
if !is_token(coding) {
return Err(HttpError::InvalidTransferEncoding);
}
facts.te_codings += 1;
if coding.eq_ignore_ascii_case(b"chunked") {
if params {
return Err(HttpError::InvalidTransferEncoding);
}
facts.te_chunked += 1;
facts.te_last_chunked = true;
} else {
facts.te_other = true;
facts.te_last_chunked = false;
}
}
} else if name.eq_ignore_ascii_case(b"connection") {
for token in list_elements(value) {
if token.eq_ignore_ascii_case(b"close") {
facts.close = true;
} else if token.eq_ignore_ascii_case(b"keep-alive") {
facts.keep_alive = true;
} else if token.eq_ignore_ascii_case(b"upgrade") {
facts.upgrade_token = true;
}
}
} else if name.eq_ignore_ascii_case(b"upgrade") {
facts.upgrade_field |= !value.is_empty();
facts.upgrade_offer |= list_elements(value).any(|e| !is_h2c(e));
} else if name.eq_ignore_ascii_case(b"host") {
facts.hosts += 1;
facts.host = Some(field.value);
} else if name.eq_ignore_ascii_case(b"expect") {
facts.expect_continue |=
list_elements(value).any(|e| e.eq_ignore_ascii_case(b"100-continue"));
}
}
Ok(facts)
}
fn transfer_framing(
facts: &FieldFacts,
version: Version,
) -> Result<Option<BodyFraming>, HttpError> {
if !facts.te_seen {
return Ok(None);
}
if version == Version::Http10 {
return Err(HttpError::TransferEncodingInHttp10);
}
if facts.content_length.is_some() {
return Err(HttpError::ConflictingFraming);
}
if facts.te_codings == 0 || facts.te_chunked > 1 || !facts.te_last_chunked {
return Err(HttpError::InvalidTransferEncoding);
}
if facts.te_other {
return Err(HttpError::UnsupportedTransferEncoding);
}
Ok(Some(BodyFraming::Chunked))
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct RequestInfo {
pub(crate) method: Method,
method_span: Span,
target: Span,
target_form: TargetForm,
pub(crate) version: Version,
pub(crate) framing: BodyFraming,
content_length: Option<u64>,
pub(crate) keep_alive: bool,
pub(crate) upgrade: bool,
pub(crate) expect_continue: bool,
host: Option<Span>,
authority: Option<Span>,
origin: Span,
}
impl RequestInfo {
pub(crate) const EMPTY: Self = Self {
method: Method::Get,
method_span: Span { start: 0, end: 0 },
target: Span { start: 0, end: 0 },
target_form: TargetForm::Origin,
version: Version::Http11,
framing: BodyFraming::None,
content_length: None,
keep_alive: false,
upgrade: false,
expect_continue: false,
host: None,
authority: None,
origin: Span { start: 0, end: 0 },
};
}
pub(crate) fn check_target(method: Method, target: &[u8]) -> Result<(), HttpError> {
let ok = match target.first() {
None => false,
Some(b'/') => method != Method::Connect && is_target_chars(target),
Some(b'*') if target.len() == 1 => method == Method::Options,
Some(_) if method == Method::Connect => {
is_authority(target)
&& target
.iter()
.rposition(|&b| b == b':')
.is_some_and(|c| c + 1 < target.len() && !target[c..].contains(&b']'))
}
Some(_) => split_absolute_form(target).is_some(),
};
if ok {
Ok(())
} else {
Err(HttpError::InvalidTarget)
}
}
fn is_authority(s: &[u8]) -> bool {
!s.is_empty() && s[0] != b':' && is_host_value(s)
}
fn split_absolute_form(target: &[u8]) -> Option<(usize, usize)> {
let colon = find_byte(b':', target)?;
let scheme = &target[..colon];
let scheme_ok = scheme.first().is_some_and(u8::is_ascii_alphabetic)
&& scheme
.iter()
.all(|&b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.'));
if !scheme_ok || !target[colon..].starts_with(b"://") {
return None;
}
let auth_start = colon + 3;
let after = &target[auth_start..];
let auth_len = after
.iter()
.position(|&b| b == b'/' || b == b'?')
.unwrap_or(after.len());
let authority = &after[..auth_len];
let rest = &after[auth_len..];
(is_authority(authority) && is_target_chars(rest))
.then_some((auth_start, auth_start + auth_len))
}
pub(crate) fn interpret_request(
head: &[u8],
start: Span,
fields: &[Field],
limits: &Limits,
) -> Result<RequestInfo, HttpError> {
let line = start.get(head);
let base = start.start as usize;
let sp1 = find_byte(b' ', line).ok_or(HttpError::InvalidStartLine)?;
let sp2 = line
.iter()
.rposition(|&b| b == b' ')
.ok_or(HttpError::InvalidStartLine)?;
if sp2 == sp1 {
return Err(HttpError::InvalidStartLine);
}
let method_bytes = &line[..sp1];
if !is_token(method_bytes) {
return Err(HttpError::InvalidMethod);
}
let version = parse_version(&line[sp2 + 1..])?;
let method = Method::from_bytes(method_bytes);
let target = &line[sp1 + 1..sp2];
check_target(method, target)?;
let target_start = base + sp1 + 1;
let target_span = Span::new(target_start, base + sp2);
let (target_form, authority, origin) = match target.first() {
Some(b'/') => (TargetForm::Origin, None, target_span),
Some(b'*') => (TargetForm::Asterisk, None, target_span),
_ if method == Method::Connect => (TargetForm::Authority, None, target_span),
_ => match split_absolute_form(target) {
Some((a, b)) => (
TargetForm::Absolute,
Some(Span::new(target_start + a, target_start + b)),
Span::new(target_start + b, base + sp2),
),
None => return Err(HttpError::InvalidTarget),
},
};
let facts = scan_fields(head, fields)?;
let framing = match transfer_framing(&facts, version)? {
Some(f) => f,
None => match facts.content_length {
Some(n) if n > limits.max_body() => return Err(HttpError::BodyTooLarge),
Some(0) | None => BodyFraming::None,
Some(n) => BodyFraming::Length(n),
},
};
if facts.hosts > 1 {
return Err(HttpError::InvalidHost);
}
if let Some(host) = facts.host {
if !is_host_value(host.get(head)) {
return Err(HttpError::InvalidHost);
}
} else if version == Version::Http11 {
return Err(HttpError::MissingHost);
}
let keep_alive = match version {
Version::Http11 => !facts.close,
Version::Http10 => facts.keep_alive && !facts.close,
};
Ok(RequestInfo {
method,
method_span: Span::new(base, base + sp1),
target: target_span,
target_form,
version,
framing,
content_length: facts.content_length,
keep_alive,
upgrade: version == Version::Http11 && facts.upgrade_token && facts.upgrade_offer,
expect_continue: version == Version::Http11
&& facts.expect_continue
&& framing != BodyFraming::None,
host: facts.host,
authority,
origin,
})
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ResponseInfo {
pub(crate) status: u16,
reason: Span,
pub(crate) version: Version,
pub(crate) framing: BodyFraming,
content_length: Option<u64>,
pub(crate) keep_alive: bool,
pub(crate) upgrade_field: bool,
}
impl ResponseInfo {
pub(crate) const EMPTY: Self = Self {
status: 0,
reason: Span { start: 0, end: 0 },
version: Version::Http11,
framing: BodyFraming::None,
content_length: None,
keep_alive: false,
upgrade_field: false,
};
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct Asked {
pub(crate) head: bool,
pub(crate) connect: bool,
}
pub(crate) fn interpret_response(
head: &[u8],
start: Span,
fields: &[Field],
limits: &Limits,
asked: Asked,
) -> Result<ResponseInfo, HttpError> {
let line = start.get(head);
let base = start.start as usize;
if line.len() < 12 || line[8] != b' ' || (line.len() > 12 && line[12] != b' ') {
return Err(HttpError::InvalidStartLine);
}
let version = parse_version(&line[..8])?;
let code = &line[9..12];
if !code.iter().all(u8::is_ascii_digit) {
return Err(HttpError::InvalidStatus);
}
let status = code
.iter()
.fold(0u16, |acc, &d| acc * 10 + u16::from(d - b'0'));
if !(100..=599).contains(&status) {
return Err(HttpError::InvalidStatus);
}
let reason_start = line.len().min(13);
let reason = &line[reason_start..];
if !is_reason(reason) {
return Err(HttpError::InvalidStartLine);
}
let facts = scan_fields(head, fields)?;
let bodiless = asked.head
|| (100..200).contains(&status)
|| status == 204
|| status == 304
|| (asked.connect && (200..300).contains(&status));
let framing = if bodiless {
BodyFraming::None
} else {
match transfer_framing(&facts, version)? {
Some(f) => f,
None => match facts.content_length {
Some(n) if n > limits.max_body() => return Err(HttpError::BodyTooLarge),
Some(0) => BodyFraming::None,
Some(n) => BodyFraming::Length(n),
None => BodyFraming::UntilClose,
},
}
};
let keep_alive = framing != BodyFraming::UntilClose
&& match version {
Version::Http11 => !facts.close,
Version::Http10 => facts.keep_alive && !facts.close,
};
Ok(ResponseInfo {
status,
reason: Span::new(base + reason_start, base + line.len()),
version,
framing,
content_length: facts.content_length,
keep_alive,
upgrade_field: facts.upgrade_field,
})
}
#[derive(Clone)]
pub struct Headers<'a> {
head: &'a [u8],
fields: slice::Iter<'a, Field>,
}
impl<'a> Iterator for Headers<'a> {
type Item = (&'a str, &'a [u8]);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let field = self.fields.next()?;
Some((
core::str::from_utf8(field.name.get(self.head)).unwrap_or(""),
field.value.get(self.head),
))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.fields.size_hint()
}
}
impl ExactSizeIterator for Headers<'_> {}
impl fmt::Debug for Headers<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = self.head;
f.debug_list()
.entries(self.fields.clone().map(|field| {
let name = field.name.get(head);
(
FieldName(name),
FieldValue {
name,
value: field.value.get(head),
},
)
}))
.finish()
}
}
#[derive(Clone)]
pub struct HeaderValues<'a, 'n> {
head: &'a [u8],
fields: slice::Iter<'a, Field>,
name: &'n [u8],
}
impl<'a> Iterator for HeaderValues<'a, '_> {
type Item = &'a [u8];
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (head, name) = (self.head, self.name);
self.fields
.find(|f| f.name.get(head).eq_ignore_ascii_case(name))
.map(|f| f.value.get(head))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
(0, self.fields.size_hint().1)
}
}
impl fmt::Debug for HeaderValues<'_, '_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = self.name;
f.debug_list()
.entries(self.clone().map(|value| FieldValue { name, value }))
.finish()
}
}
fn find_header<'a>(head: &'a [u8], fields: &[Field], name: &str) -> Option<&'a [u8]> {
fields
.iter()
.find(|f| f.name.get(head).eq_ignore_ascii_case(name.as_bytes()))
.map(|f| f.value.get(head))
}
fn header_values<'a, 'n>(
head: &'a [u8],
fields: &'a [Field],
name: &'n str,
) -> HeaderValues<'a, 'n> {
HeaderValues {
head,
fields: fields.iter(),
name: name.as_bytes(),
}
}
#[derive(Clone, Copy)]
pub struct RequestHead<'a> {
head: &'a [u8],
fields: &'a [Field],
info: &'a RequestInfo,
}
impl<'a> RequestHead<'a> {
pub(crate) const fn new(head: &'a [u8], fields: &'a [Field], info: &'a RequestInfo) -> Self {
Self { head, fields, info }
}
#[must_use]
pub fn method(&self) -> Method {
self.info.method
}
#[must_use]
pub fn method_str(&self) -> &'a str {
core::str::from_utf8(self.info.method_span.get(self.head)).unwrap_or("")
}
#[must_use]
pub fn target(&self) -> &'a str {
core::str::from_utf8(self.info.target.get(self.head)).unwrap_or("")
}
#[must_use]
pub fn target_form(&self) -> TargetForm {
self.info.target_form
}
#[must_use]
pub fn path(&self) -> &'a str {
let origin = self.origin();
let path = origin.split_once('?').map_or(origin, |(p, _)| p);
if path.is_empty() && self.info.authority.is_some() {
"/"
} else {
path
}
}
#[must_use]
pub fn query(&self) -> Option<&'a str> {
self.origin().split_once('?').map(|(_, q)| q)
}
fn origin(&self) -> &'a str {
core::str::from_utf8(self.info.origin.get(self.head)).unwrap_or("")
}
#[must_use]
pub fn version(&self) -> Version {
self.info.version
}
#[must_use]
pub fn headers(&self) -> Headers<'a> {
Headers {
head: self.head,
fields: self.fields.iter(),
}
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&'a [u8]> {
find_header(self.head, self.fields, name)
}
#[must_use]
pub fn header_str(&self, name: &str) -> Option<&'a str> {
self.header(name).and_then(|v| core::str::from_utf8(v).ok())
}
#[must_use]
pub fn header_all<'n>(&self, name: &'n str) -> HeaderValues<'a, 'n> {
header_values(self.head, self.fields, name)
}
#[must_use]
pub fn content_length(&self) -> Option<u64> {
self.info.content_length
}
#[must_use]
pub fn host(&self) -> Option<&'a str> {
self.info
.authority
.or(self.info.host)
.and_then(|s| core::str::from_utf8(s.get(self.head)).ok())
}
#[must_use]
pub fn expects_continue(&self) -> bool {
self.info.expect_continue
}
#[must_use]
pub fn framing(&self) -> BodyFraming {
self.info.framing
}
#[must_use]
pub fn keep_alive(&self) -> bool {
self.info.keep_alive
}
#[must_use]
pub fn is_upgrade(&self) -> bool {
self.info.upgrade
}
pub(crate) fn offers(&self, protocol: &[u8]) -> bool {
self.info.upgrade
&& self
.header_all("upgrade")
.any(|v| list_elements(v).any(|e| protocol_matches(protocol, e)))
}
}
impl fmt::Debug for RequestHead<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RequestHead")
.field("method", &self.method_str())
.field("target", &self.target())
.field("version", &self.version())
.field("headers", &self.headers())
.field("framing", &self.framing())
.finish()
}
}
#[derive(Clone, Copy)]
pub struct ResponseHead<'a> {
head: &'a [u8],
fields: &'a [Field],
info: &'a ResponseInfo,
}
impl<'a> ResponseHead<'a> {
pub(crate) const fn new(head: &'a [u8], fields: &'a [Field], info: &'a ResponseInfo) -> Self {
Self { head, fields, info }
}
#[must_use]
pub fn status(&self) -> u16 {
self.info.status
}
#[must_use]
pub fn reason(&self) -> &'a str {
core::str::from_utf8(self.info.reason.get(self.head)).unwrap_or("")
}
#[must_use]
pub fn version(&self) -> Version {
self.info.version
}
#[must_use]
pub fn headers(&self) -> Headers<'a> {
Headers {
head: self.head,
fields: self.fields.iter(),
}
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&'a [u8]> {
find_header(self.head, self.fields, name)
}
#[must_use]
pub fn header_str(&self, name: &str) -> Option<&'a str> {
self.header(name).and_then(|v| core::str::from_utf8(v).ok())
}
#[must_use]
pub fn header_all<'n>(&self, name: &'n str) -> HeaderValues<'a, 'n> {
header_values(self.head, self.fields, name)
}
#[must_use]
pub fn content_length(&self) -> Option<u64> {
self.info.content_length
}
#[must_use]
pub fn framing(&self) -> BodyFraming {
self.info.framing
}
#[must_use]
pub fn keep_alive(&self) -> bool {
self.info.keep_alive
}
}
impl fmt::Debug for ResponseHead<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResponseHead")
.field("status", &self.status())
.field("reason", &self.reason())
.field("version", &self.version())
.field("headers", &self.headers())
.field("framing", &self.framing())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(msg: &[u8], kind: HeadKind) -> Result<(HeadScanner, usize), HttpError> {
let mut s = HeadScanner::new();
match s.advance(msg, &Limits::new(), kind, true)? {
Progress::Done(n) => Ok((s, n)),
Progress::Partial => Err(HttpError::UnexpectedEof),
}
}
#[test]
fn test_scanner_resumes_without_rescanning() {
let msg = b"GET / HTTP/1.1\r\nHost: a\r\n\r\n";
let mut s = HeadScanner::new();
for end in 1..msg.len() {
assert!(matches!(
s.advance(&msg[..end], &Limits::new(), HeadKind::Request, true),
Ok(Progress::Partial)
));
}
assert!(matches!(
s.advance(msg, &Limits::new(), HeadKind::Request, true),
Ok(Progress::Done(n)) if n == msg.len()
));
assert_eq!(s.fields.len(), 1);
}
#[test]
fn test_scanner_rejects_line_shapes() {
let cases: &[(&[u8], HttpError)] = &[
(b"GET / HTTP/1.1\nHost: a\r\n\r\n", HttpError::BareLf),
(b"GET / HTTP/1.1\r\n Host: a\r\n\r\n", HttpError::ObsFold),
(
b"GET / HTTP/1.1\r\nHost : a\r\n\r\n",
HttpError::WhitespaceBeforeColon,
),
(
b"GET / HTTP/1.1\r\nHost\r\n\r\n",
HttpError::InvalidHeaderName,
),
(
b"GET / HTTP/1.1\r\n: a\r\n\r\n",
HttpError::InvalidHeaderName,
),
(b"GET / HTTP/1.1\r\nHo\rst: a\r\n\r\n", HttpError::BareCr),
(b"GET / HTTP/1.1\r\nHost: a\rb\r\n\r\n", HttpError::BareCr),
(b"GET /\r HTTP/1.1\r\n\r\n", HttpError::BareCr),
(
b"GET / HTTP/1.1\r\nHost: a\x00\r\n\r\n",
HttpError::InvalidHeaderValue,
),
];
for (msg, err) in cases {
assert_eq!(scan(msg, HeadKind::Request).err(), Some(*err), "{msg:?}");
}
}
#[test]
fn test_scanner_skips_leading_empty_lines_for_requests_only() {
assert!(scan(b"\r\n\r\nGET / HTTP/1.1\r\n\r\n", HeadKind::Request).is_ok());
assert_eq!(
scan(b"\r\nHTTP/1.1 200 OK\r\n\r\n", HeadKind::Response).err(),
Some(HttpError::InvalidStartLine)
);
}
#[test]
fn test_scanner_enforces_limits_at_boundaries() {
let limits = Limits::new().with_max_start_line(16);
let mut s = HeadScanner::new();
assert!(
s.advance(b"GET / HTTP/1.1\r\n\r\n", &limits, HeadKind::Request, true)
.is_ok()
);
let mut s = HeadScanner::new();
assert_eq!(
s.advance(b"GET /a HTTP/1.1\r\n\r\n", &limits, HeadKind::Request, true)
.err(),
Some(HttpError::StartLineTooLong)
);
let mut s = HeadScanner::new();
assert_eq!(
s.advance(b"GET /aaaaaaaaaaaa", &limits, HeadKind::Request, true)
.err(),
Some(HttpError::StartLineTooLong)
);
let limits = Limits::new().with_max_headers(1);
let mut s = HeadScanner::new();
assert_eq!(
s.advance(
b"GET / HTTP/1.1\r\nA: 1\r\nB: 2\r\n\r\n",
&limits,
HeadKind::Request,
true
)
.err(),
Some(HttpError::TooManyHeaders)
);
let limits = Limits::new().with_max_header_bytes(8);
let mut s = HeadScanner::new();
assert!(
s.advance(
b"GET / HTTP/1.1\r\nA: 1\r\n\r\n",
&limits,
HeadKind::Request,
true
)
.is_ok()
);
let mut s = HeadScanner::new();
assert_eq!(
s.advance(
b"GET / HTTP/1.1\r\nA: 12\r\nB: 1\r\n\r\n",
&limits,
HeadKind::Request,
true
)
.err(),
Some(HttpError::HeadersTooLarge)
);
}
fn response(line: &[u8]) -> Result<ResponseInfo, HttpError> {
let mut msg = line.to_vec();
msg.extend_from_slice(b"\r\nContent-Length: 0\r\n\r\n");
let (s, _) = scan(&msg, HeadKind::Response)?;
interpret_response(
&msg,
s.start_line(),
&s.fields,
&Limits::new(),
Asked {
head: false,
connect: false,
},
)
}
#[test]
fn test_status_line_without_space_after_code_is_accepted() {
let info = response(b"HTTP/1.1 200").unwrap();
assert_eq!(info.status, 200);
assert_eq!(info.reason, Span::new(12, 12));
assert_eq!(response(b"HTTP/1.1 200 ").unwrap().status, 200);
assert_eq!(response(b"HTTP/1.1 204 No Content").unwrap().status, 204);
assert_eq!(
response(b"HTTP/1.1 20").err(),
Some(HttpError::InvalidStartLine)
);
assert_eq!(
response(b"HTTP/1.1 2000").err(),
Some(HttpError::InvalidStartLine)
);
assert_eq!(
response(b"HTTP/1.1200 OK").err(),
Some(HttpError::InvalidStartLine)
);
assert_eq!(
response(b"HTTP/1.1 600").err(),
Some(HttpError::InvalidStatus)
);
}
#[test]
fn test_protocol_matching_rules() {
assert!(protocol_matches(b"websocket", b"websocket"));
assert!(protocol_matches(b"WebSocket", b"websocket"));
assert!(protocol_matches(b"websocket", b"websocket/13"));
assert!(protocol_matches(b"foo/2", b"FOO/2"));
assert!(!protocol_matches(b"foo/2", b"foo/3"));
assert!(!protocol_matches(b"foo/2", b"foo"));
assert!(!protocol_matches(b"ws", b"websocket"));
assert!(!protocol_matches(b"h2c", b"h2c"));
assert!(!protocol_matches(b"H2C", b"h2c"));
assert!(is_h2c(b"h2c/1") && is_h2c(b"H2c") && !is_h2c(b"h2"));
}
#[test]
fn test_authority_targets_refuse_empty_host_and_bad_escapes() {
assert!(check_target(Method::Connect, b"a%41:443").is_ok());
assert!(check_target(Method::Connect, b"a%4:443").is_err());
assert!(check_target(Method::Get, b"http://a%zz/").is_err());
assert!(check_target(Method::Get, b"http://a%41b/").is_ok());
assert!(check_target(Method::Get, b"ftp://h/").is_ok());
}
#[test]
fn test_versions() {
assert_eq!(parse_version(b"HTTP/1.1"), Ok(Version::Http11));
assert_eq!(parse_version(b"HTTP/1.0"), Ok(Version::Http10));
assert_eq!(parse_version(b"HTTP/1.9"), Ok(Version::Http11));
assert_eq!(
parse_version(b"HTTP/2.0"),
Err(HttpError::UnsupportedVersion)
);
assert_eq!(
parse_version(b"HTTP/0.9"),
Err(HttpError::UnsupportedVersion)
);
assert_eq!(parse_version(b"http/1.1"), Err(HttpError::InvalidVersion));
assert_eq!(parse_version(b"HTTP/1.10"), Err(HttpError::InvalidVersion));
assert_eq!(parse_version(b"HTTP/1"), Err(HttpError::InvalidVersion));
}
#[test]
fn test_target_forms_follow_method() {
assert!(check_target(Method::Get, b"/").is_ok());
assert!(check_target(Method::Options, b"*").is_ok());
assert!(check_target(Method::Get, b"*").is_err());
assert!(check_target(Method::Connect, b"example.com:443").is_ok());
assert!(check_target(Method::Connect, b"[::1]:443").is_ok());
assert!(check_target(Method::Connect, b"example.com").is_err());
assert!(check_target(Method::Connect, b"/").is_err());
assert!(check_target(Method::Get, b"http://example.com/a?b").is_ok());
assert!(check_target(Method::Get, b"http://[::1]:80/").is_ok());
assert!(check_target(Method::Get, b"http://x").is_ok());
assert!(check_target(Method::Get, b"http://x?q").is_ok());
assert!(check_target(Method::Get, b"http://u@x/").is_err());
assert!(check_target(Method::Get, b"http:x").is_err());
assert!(check_target(Method::Get, b"http://:80/").is_err());
assert!(check_target(Method::Get, b"http://x/[a]").is_err());
assert!(check_target(Method::Connect, b":443").is_err());
assert!(check_target(Method::Get, b"1http://x").is_err());
assert!(check_target(Method::Get, b"example.com").is_err());
assert!(check_target(Method::Get, b"").is_err());
}
}