use crate::header::HeaderId;
use crate::{Head, Limits, Version};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BodyKind {
None,
Length(u64),
Chunked,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum FramingError {
#[error("both Content-Length and Transfer-Encoding present")]
LengthAndTransferEncoding,
#[error("conflicting Content-Length values")]
DuplicateContentLength,
#[error("malformed Content-Length")]
InvalidContentLength,
#[error("chunked is not the final transfer coding")]
ChunkedNotFinal,
#[error("unsupported transfer coding")]
UnsupportedTransferEncoding,
#[error("Transfer-Encoding on an HTTP/1.0 request")]
TransferEncodingOnHttp10,
#[error("missing Host")]
MissingHost,
#[error("multiple Host fields")]
MultipleHost,
#[error("declared body too large")]
BodyTooLarge,
}
impl FramingError {
#[inline]
pub fn status(&self) -> u16 {
match self {
FramingError::BodyTooLarge => 413,
FramingError::UnsupportedTransferEncoding => 501,
_ => 400,
}
}
}
pub fn decide(head: &Head, limits: &Limits) -> Result<BodyKind, FramingError> {
let mut host_count = 0usize;
let mut has_len = false;
let mut has_te = false;
for (id, _) in head.headers.iter() {
match id {
HeaderId::Host => host_count += 1,
HeaderId::ContentLength => has_len = true,
HeaderId::TransferEncoding => has_te = true,
_ => {}
}
}
match host_count {
0 if head.version == Version::Http11 => return Err(FramingError::MissingHost),
n if n > 1 => return Err(FramingError::MultipleHost),
_ => {}
}
if has_len && has_te {
return Err(FramingError::LengthAndTransferEncoding);
}
if has_te {
if head.version == Version::Http10 {
return Err(FramingError::TransferEncodingOnHttp10);
}
let mut codings: smallvec::SmallVec<[&str; 4]> = smallvec::SmallVec::new();
for value in head.all(&HeaderId::TransferEncoding) {
let s = std::str::from_utf8(value).map_err(|_| FramingError::ChunkedNotFinal)?;
for coding in s.split(',') {
let coding = coding.trim();
if !coding.is_empty() {
codings.push(coding);
}
}
}
let chunked_count = codings
.iter()
.filter(|c| c.eq_ignore_ascii_case("chunked"))
.count();
return match chunked_count {
1 if codings
.last()
.is_some_and(|c| c.eq_ignore_ascii_case("chunked")) =>
{
if codings.len() == 1 {
Ok(BodyKind::Chunked)
} else {
Err(FramingError::UnsupportedTransferEncoding)
}
}
n if n >= 1 => Err(FramingError::ChunkedNotFinal),
_ => Err(FramingError::UnsupportedTransferEncoding),
};
}
if has_len {
let mut agreed: Option<u64> = None;
for value in head.all(&HeaderId::ContentLength) {
let s = std::str::from_utf8(value).map_err(|_| FramingError::InvalidContentLength)?;
for element in s.split(',') {
let n = parse_content_length(element.trim())?;
match agreed {
None => agreed = Some(n),
Some(prev) if prev == n => {}
Some(_) => return Err(FramingError::DuplicateContentLength),
}
}
}
let len = agreed.ok_or(FramingError::InvalidContentLength)?;
if len > limits.max_body_bytes {
return Err(FramingError::BodyTooLarge);
}
return Ok(BodyKind::Length(len));
}
Ok(BodyKind::None)
}
#[inline]
fn parse_content_length(s: &str) -> Result<u64, FramingError> {
let bytes = s.as_bytes();
if bytes.is_empty() || !bytes.iter().all(|b| b.is_ascii_digit()) {
return Err(FramingError::InvalidContentLength);
}
s.parse::<u64>()
.map_err(|_| FramingError::InvalidContentLength)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Limits, parse_head};
use bytes::Bytes;
fn head(raw: &'static [u8]) -> crate::Head {
parse_head(&Bytes::from_static(raw), &Limits::default())
.expect("must parse")
.expect("must be complete")
.0
}
fn decide_raw(raw: &'static [u8]) -> Result<BodyKind, FramingError> {
decide(&head(raw), &Limits::default())
}
#[test]
fn no_framing_headers_means_no_body() {
assert_eq!(
decide_raw(b"GET / HTTP/1.1\r\nHost: a\r\n\r\n"),
Ok(BodyKind::None)
);
}
#[test]
fn content_length_gives_a_fixed_body() {
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\n"),
Ok(BodyKind::Length(5))
);
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 0\r\n\r\n"),
Ok(BodyKind::Length(0))
);
}
#[test]
fn transfer_encoding_chunked_gives_a_chunked_body() {
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"),
Ok(BodyKind::Chunked)
);
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: CHUNKED\r\n\r\n"),
Ok(BodyKind::Chunked),
"transfer codings are case-insensitive"
);
}
#[test]
fn identical_duplicate_content_length_is_accepted() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\n"
),
Ok(BodyKind::Length(5))
);
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5, 5\r\n\r\n"),
Ok(BodyKind::Length(5)),
"a comma list of identical values is the same case"
);
}
#[test]
fn http_10_needs_no_host() {
assert_eq!(decide_raw(b"GET / HTTP/1.0\r\n\r\n"), Ok(BodyKind::None));
}
#[test]
fn rejects_content_length_with_transfer_encoding() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n"
),
Err(FramingError::LengthAndTransferEncoding)
);
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\n"
),
Err(FramingError::LengthAndTransferEncoding)
);
}
#[test]
fn rejects_conflicting_content_length() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\n"
),
Err(FramingError::DuplicateContentLength)
);
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5, 6\r\n\r\n"),
Err(FramingError::DuplicateContentLength)
);
}
#[test]
fn rejects_malformed_content_length() {
for raw in [
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: abc\r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: \r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: +5\r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: -5\r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5x\r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 0x5\r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5 5\r\n\r\n"[..],
&b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 99999999999999999999999\r\n\r\n"[..],
] {
let h = parse_head(&Bytes::copy_from_slice(raw), &Limits::default())
.unwrap()
.unwrap()
.0;
assert_eq!(
decide(&h, &Limits::default()),
Err(FramingError::InvalidContentLength),
"should have rejected: {}",
String::from_utf8_lossy(raw)
);
}
}
#[test]
fn rejects_chunked_not_final() {
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked, gzip\r\n\r\n"),
Err(FramingError::ChunkedNotFinal)
);
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\nTransfer-Encoding: gzip\r\n\r\n"
),
Err(FramingError::ChunkedNotFinal),
"codings accumulate across repeated fields"
);
}
#[test]
fn rejects_repeated_chunked() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked, chunked\r\n\r\n"
),
Err(FramingError::ChunkedNotFinal)
);
}
#[test]
fn rejects_unsupported_transfer_coding() {
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: gzip\r\n\r\n"),
Err(FramingError::UnsupportedTransferEncoding)
);
assert_eq!(
decide_raw(b"POST / HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: identity\r\n\r\n"),
Err(FramingError::UnsupportedTransferEncoding)
);
}
#[test]
fn rejects_missing_or_multiple_host_on_http_11() {
assert_eq!(
decide_raw(b"GET / HTTP/1.1\r\n\r\n"),
Err(FramingError::MissingHost)
);
assert_eq!(
decide_raw(b"GET / HTTP/1.1\r\nHost: a\r\nHost: b\r\n\r\n"),
Err(FramingError::MultipleHost)
);
}
#[test]
fn rejects_multiple_host_on_http_10_too() {
assert_eq!(
decide_raw(b"GET / HTTP/1.0\r\nHost: a\r\nHost: b\r\n\r\n"),
Err(FramingError::MultipleHost)
);
}
#[test]
fn rejects_transfer_encoding_on_http_10() {
assert_eq!(
decide_raw(b"POST / HTTP/1.0\r\nTransfer-Encoding: chunked\r\n\r\n"),
Err(FramingError::TransferEncodingOnHttp10)
);
assert_eq!(
decide_raw(b"POST / HTTP/1.0\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"),
Err(FramingError::TransferEncodingOnHttp10)
);
assert_eq!(
decide_raw(b"POST / HTTP/1.0\r\nTransfer-Encoding: gzip\r\n\r\n"),
Err(FramingError::TransferEncodingOnHttp10)
);
}
#[test]
fn conflict_precedes_the_http_10_transfer_encoding_rule() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.0\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n"
),
Err(FramingError::LengthAndTransferEncoding)
);
}
#[test]
fn rejects_oversized_declared_body() {
let limits = Limits {
max_body_bytes: 4,
..Default::default()
};
let h = head(b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\n\r\n");
assert_eq!(decide(&h, &limits), Err(FramingError::BodyTooLarge));
}
#[test]
fn conflict_check_precedes_individual_analysis() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n"
),
Err(FramingError::LengthAndTransferEncoding),
"must not report ChunkedNotFinal or a Length body"
);
}
#[test]
fn conflict_reported_even_when_content_length_is_garbage() {
assert_eq!(
decide_raw(
b"POST / HTTP/1.1\r\nHost: a\r\nContent-Length: abc\r\nTransfer-Encoding: chunked\r\n\r\n"
),
Err(FramingError::LengthAndTransferEncoding)
);
}
#[test]
fn status_codes_map_correctly() {
assert_eq!(FramingError::LengthAndTransferEncoding.status(), 400);
assert_eq!(FramingError::DuplicateContentLength.status(), 400);
assert_eq!(FramingError::InvalidContentLength.status(), 400);
assert_eq!(FramingError::ChunkedNotFinal.status(), 400);
assert_eq!(FramingError::MissingHost.status(), 400);
assert_eq!(FramingError::MultipleHost.status(), 400);
assert_eq!(FramingError::UnsupportedTransferEncoding.status(), 501);
assert_eq!(FramingError::TransferEncodingOnHttp10.status(), 400);
assert_eq!(FramingError::BodyTooLarge.status(), 413);
}
}