use std::collections::VecDeque;
use bytes::{Bytes, BytesMut};
use super::poison::HttpPoison;
use super::types::{BodyFraming, HttpVersion, Normalization, SmugglingPolicy, SwitchKind};
use crate::error::{Error, Module};
const HEADER_STACK_SLOTS: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Dir {
Request,
Response,
}
#[derive(Debug, Clone)]
pub(crate) struct EngineLimits {
pub max_head_bytes: usize,
pub max_headers: usize,
pub max_chunk_line_bytes: usize,
pub max_trailer_bytes: usize,
pub max_pipelined: usize,
pub policy: SmugglingPolicy,
}
impl Default for EngineLimits {
fn default() -> Self {
Self {
max_head_bytes: 64 * 1024,
max_headers: 64,
max_chunk_line_bytes: 1024,
max_trailer_bytes: 8 * 1024,
max_pipelined: 64,
policy: SmugglingPolicy::Strict,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) struct Head {
pub dir: Dir,
pub method: Bytes,
pub path: Bytes,
pub status: u16,
pub reason: Bytes,
pub version: HttpVersion,
pub headers: Vec<(Bytes, Bytes)>,
pub framing: BodyFraming,
pub interim: bool,
pub applied: Vec<Normalization>,
pub raw: Bytes,
}
#[derive(Debug, Clone)]
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) enum EngineEvent {
Head(Head),
Body {
decoded: Bytes,
raw: Bytes,
},
Trailers {
fields: Vec<(Bytes, Bytes)>,
raw: Bytes,
},
End,
Switch(SwitchKind),
}
#[derive(Debug, Clone)]
enum BodyState {
Length { remaining: u64 },
Chunked(ChunkState),
UntilClose,
}
#[derive(Debug, Clone)]
enum ChunkState {
Size,
Data { remaining: u64 },
DataCrlf,
Trailer,
}
#[derive(Debug, Clone)]
enum DirState {
Head,
Body(BodyState),
Tunnel,
Closed,
Desynced,
}
#[derive(Debug, Clone, Copy)]
struct ReqCtx {
is_head: bool,
is_connect: bool,
}
#[derive(Debug, Clone)]
struct DirMachine {
buf: BytesMut,
state: DirState,
poison: Option<HttpPoison>,
close_after_message: bool,
scanned: usize,
}
impl DirMachine {
fn new() -> Self {
Self {
buf: BytesMut::new(),
state: DirState::Head,
poison: None,
close_after_message: false,
scanned: 0,
}
}
fn reset(&mut self) {
self.buf.clear();
self.state = DirState::Head;
self.poison = None;
self.close_after_message = false;
self.scanned = 0;
}
fn take(&mut self, n: usize) -> Bytes {
self.scanned = self.scanned.saturating_sub(n);
self.buf.split_to(n).freeze()
}
}
#[derive(Debug, Clone)]
pub(crate) struct Engine {
limits: EngineLimits,
request: DirMachine,
response: DirMachine,
pending: VecDeque<ReqCtx>,
pending_switch: Option<SwitchKind>,
}
impl Engine {
pub(crate) fn new(limits: EngineLimits) -> Self {
Self {
limits,
request: DirMachine::new(),
response: DirMachine::new(),
pending: VecDeque::new(),
pending_switch: None,
}
}
fn dir_mut(&mut self, dir: Dir) -> &mut DirMachine {
match dir {
Dir::Request => &mut self.request,
Dir::Response => &mut self.response,
}
}
fn dir(&self, dir: Dir) -> &DirMachine {
match dir {
Dir::Request => &self.request,
Dir::Response => &self.response,
}
}
pub(crate) fn push(&mut self, dir: Dir, bytes: &[u8]) {
if bytes.is_empty() || !self.can_consume(dir) {
return;
}
self.dir_mut(dir).buf.extend_from_slice(bytes);
}
fn can_consume(&self, dir: Dir) -> bool {
matches!(self.dir(dir).state, DirState::Head | DirState::Body(_))
}
pub(crate) fn is_desynced(&self, dir: Dir) -> bool {
matches!(self.dir(dir).state, DirState::Desynced)
}
pub(crate) fn reset(&mut self, dir: Dir) {
self.dir_mut(dir).reset();
if dir == Dir::Request {
self.pending.clear();
}
}
pub(crate) fn poll(&mut self, dir: Dir) -> crate::Result<Option<EngineEvent>> {
loop {
let state = self.dir(dir).state.clone();
match state {
DirState::Desynced | DirState::Closed | DirState::Tunnel => return Ok(None),
DirState::Head => match self.poll_head(dir)? {
Some(ev) => return Ok(Some(ev)),
None => return Ok(None),
},
DirState::Body(body) => match self.poll_body(dir, body)? {
Progress::Event(ev) => return Ok(Some(ev)),
Progress::NeedMore => return Ok(None),
Progress::Again => continue,
},
}
}
}
pub(crate) fn fin(&mut self, dir: Dir) -> Option<EngineEvent> {
let m = self.dir_mut(dir);
match std::mem::replace(&mut m.state, DirState::Closed) {
DirState::Body(BodyState::UntilClose) => {
let n = m.buf.len();
let raw = m.take(n);
Some(EngineEvent::Body {
decoded: raw.clone(),
raw,
})
}
DirState::Desynced => {
m.state = DirState::Desynced;
None
}
DirState::Tunnel => {
m.state = DirState::Tunnel;
None
}
_ => None,
}
}
pub(crate) fn is_closed(&self, dir: Dir) -> bool {
matches!(self.dir(dir).state, DirState::Closed)
}
pub(crate) fn is_tunnelled(&self) -> bool {
matches!(self.request.state, DirState::Tunnel)
}
fn poll_head(&mut self, dir: Dir) -> crate::Result<Option<EngineEvent>> {
let limits = self.limits.clone();
if dir == Dir::Request {
match preface_match(&self.request.buf) {
PrefaceMatch::Yes => {
self.switch_to_tunnel();
return Ok(Some(EngineEvent::Switch(SwitchKind::Http2PriorKnowledge)));
}
PrefaceMatch::Partial => return Ok(None),
PrefaceMatch::No => {}
}
}
let m = self.dir_mut(dir);
let Some(hlen) = scan_blank_line(&m.buf, &mut m.scanned) else {
if m.buf.len() > limits.max_head_bytes {
return Err(Self::desync(m, HttpPoison::HeadOverflow));
}
return Ok(None);
};
if hlen > limits.max_head_bytes {
return Err(Self::desync(m, HttpPoison::HeadOverflow));
}
if limits.policy != SmugglingPolicy::Observe
&& let Err(reason) = check_head_bytes(&m.buf[..hlen])
{
return Err(Self::desync(m, reason));
}
let parts = match parse_head_offsets(&m.buf[..hlen], dir, limits.max_headers) {
Ok(p) => p,
Err(_) => return Err(Self::desync(m, HttpPoison::MalformedHead)),
};
let raw = m.take(hlen);
let head = parts.into_head(dir, &raw, raw.clone());
let wants_close = signals_close(&head);
match dir {
Dir::Request => {
if self.limits.policy != SmugglingPolicy::Observe
&& let Err(reason) = check_single_host(&head.headers)
{
let m = self.dir_mut(dir);
return Err(Self::desync(m, reason));
}
let (framing, applied) = match request_framing(&head.headers, self.limits.policy) {
Ok(v) => v,
Err(reason) => {
let m = self.dir_mut(dir);
return Err(Self::desync(m, reason));
}
};
if self.pending.len() >= self.limits.max_pipelined {
let m = self.dir_mut(dir);
return Err(Self::desync(m, HttpPoison::PipelineOverflow));
}
self.pending.push_back(ReqCtx {
is_head: head.method.as_ref().eq_ignore_ascii_case(b"HEAD"),
is_connect: head.method.as_ref().eq_ignore_ascii_case(b"CONNECT"),
});
let head = Head {
framing,
applied,
..head
};
let m = self.dir_mut(dir);
m.close_after_message = wants_close;
m.state = body_state(framing);
Ok(Some(EngineEvent::Head(head)))
}
Dir::Response => self.finish_response_head(head, wants_close),
}
}
fn finish_response_head(
&mut self,
head: Head,
wants_close: bool,
) -> crate::Result<Option<EngineEvent>> {
let status = head.status;
if status == 101 {
let protocol = header_value(&head.headers, b"upgrade").unwrap_or_default();
let head = Head {
framing: BodyFraming::None,
interim: false,
..head
};
self.pending.pop_front();
self.pending_switch = Some(SwitchKind::Upgrade { protocol });
self.response.state = DirState::Body(BodyState::Length { remaining: 0 });
return Ok(Some(EngineEvent::Head(head)));
}
if (100..=199).contains(&status) {
let head = Head {
framing: BodyFraming::None,
interim: true,
..head
};
self.response.state = DirState::Head;
return Ok(Some(EngineEvent::Head(head)));
}
let ctx = self.pending.pop_front();
if ctx.is_none() && self.limits.policy != SmugglingPolicy::Observe {
let m = self.dir_mut(Dir::Response);
return Err(Self::desync(m, HttpPoison::UnexpectedResponse));
}
let is_head_request = ctx.is_some_and(|c| c.is_head);
if ctx.is_some_and(|c| c.is_connect) && (200..=299).contains(&status) {
let head = Head {
framing: BodyFraming::None,
interim: false,
..head
};
self.pending_switch = Some(SwitchKind::ConnectTunnel);
self.response.state = DirState::Body(BodyState::Length { remaining: 0 });
return Ok(Some(EngineEvent::Head(head)));
}
let (framing, applied) =
match response_framing(status, is_head_request, &head.headers, self.limits.policy) {
Ok(v) => v,
Err(reason) => {
let m = self.dir_mut(Dir::Response);
return Err(Self::desync(m, reason));
}
};
let head = Head {
framing,
interim: false,
applied,
..head
};
self.response.close_after_message = wants_close;
self.response.state = body_state(framing);
Ok(Some(EngineEvent::Head(head)))
}
fn switch_to_tunnel(&mut self) {
self.request.state = DirState::Tunnel;
self.response.state = DirState::Tunnel;
self.pending.clear();
}
pub(crate) fn take_residue(&mut self, dir: Dir) -> Bytes {
if !self.is_tunnelled() {
return Bytes::new();
}
let m = self.dir_mut(dir);
let n = m.buf.len();
m.take(n)
}
fn poll_body(&mut self, dir: Dir, body: BodyState) -> crate::Result<Progress> {
match body {
BodyState::Length { remaining: 0 } => {
if let Some(kind) = self.pending_switch.take() {
self.switch_to_tunnel();
return Ok(Progress::Event(EngineEvent::Switch(kind)));
}
let m = self.dir_mut(dir);
m.state = if m.close_after_message {
DirState::Closed
} else {
DirState::Head
};
Ok(Progress::Event(EngineEvent::End))
}
BodyState::Length { remaining } => {
let m = self.dir_mut(dir);
let take = remaining.min(m.buf.len() as u64) as usize;
if take == 0 {
return Ok(Progress::NeedMore);
}
let raw = m.take(take);
m.state = DirState::Body(BodyState::Length {
remaining: remaining - take as u64,
});
Ok(Progress::Event(EngineEvent::Body {
decoded: raw.clone(),
raw,
}))
}
BodyState::UntilClose => {
let m = self.dir_mut(dir);
let n = m.buf.len();
if n == 0 {
return Ok(Progress::NeedMore);
}
let raw = m.take(n);
Ok(Progress::Event(EngineEvent::Body {
decoded: raw.clone(),
raw,
}))
}
BodyState::Chunked(cs) => self.poll_chunked(dir, cs),
}
}
fn poll_chunked(&mut self, dir: Dir, cs: ChunkState) -> crate::Result<Progress> {
let limits = self.limits.clone();
let m = self.dir_mut(dir);
match cs {
ChunkState::Size => {
let Some(eol) = scan_crlf(&m.buf, &mut m.scanned) else {
if m.buf.len() > limits.max_chunk_line_bytes {
return Err(Self::desync(m, HttpPoison::ChunkLineOverflow));
}
return Ok(Progress::NeedMore);
};
if eol > limits.max_chunk_line_bytes {
return Err(Self::desync(m, HttpPoison::ChunkLineOverflow));
}
let line = &m.buf[..eol];
let hex_end = line.iter().position(|&b| b == b';').unwrap_or(line.len());
let Some(size) = parse_hex(line[..hex_end].trim_ascii()) else {
return Err(Self::desync(m, HttpPoison::InvalidChunkSize));
};
if size == 0 {
m.state = DirState::Body(BodyState::Chunked(ChunkState::Trailer));
return Ok(Progress::Again);
}
let raw = m.take(eol + 2);
m.state = DirState::Body(BodyState::Chunked(ChunkState::Data { remaining: size }));
Ok(Progress::Event(EngineEvent::Body {
decoded: Bytes::new(),
raw,
}))
}
ChunkState::Data { remaining } => {
let take = remaining.min(m.buf.len() as u64) as usize;
if take == 0 {
return Ok(Progress::NeedMore);
}
let raw = m.take(take);
let left = remaining - take as u64;
m.state = DirState::Body(BodyState::Chunked(if left == 0 {
ChunkState::DataCrlf
} else {
ChunkState::Data { remaining: left }
}));
Ok(Progress::Event(EngineEvent::Body {
decoded: raw.clone(),
raw,
}))
}
ChunkState::DataCrlf => {
if m.buf.len() < 2 {
return Ok(Progress::NeedMore);
}
if &m.buf[..2] != b"\r\n" {
return Err(Self::desync(m, HttpPoison::MalformedChunkTerminator));
}
let raw = m.take(2);
m.state = DirState::Body(BodyState::Chunked(ChunkState::Size));
Ok(Progress::Event(EngineEvent::Body {
decoded: Bytes::new(),
raw,
}))
}
ChunkState::Trailer => {
let Some(end) = scan_trailer_end(&m.buf, &mut m.scanned) else {
if m.buf.len() > limits.max_trailer_bytes {
return Err(Self::desync(m, HttpPoison::TrailerOverflow));
}
return Ok(Progress::NeedMore);
};
if end > limits.max_trailer_bytes {
return Err(Self::desync(m, HttpPoison::TrailerOverflow));
}
let raw = m.take(end);
let fields = parse_trailer_fields(&raw);
m.state = DirState::Body(BodyState::Length { remaining: 0 });
Ok(Progress::Event(EngineEvent::Trailers { fields, raw }))
}
}
}
fn desync(m: &mut DirMachine, reason: HttpPoison) -> Error {
m.state = DirState::Desynced;
m.poison = Some(reason);
m.buf.clear();
m.scanned = 0;
match reason {
HttpPoison::HeadOverflow
| HttpPoison::ChunkLineOverflow
| HttpPoison::TrailerOverflow => Error::buffer_overflow(Module::Http, 0),
other => Error::parse(Module::Http, other.as_str()),
}
}
pub(crate) fn poison(&self, dir: Dir) -> Option<HttpPoison> {
self.dir(dir).poison
}
pub(crate) fn buffered(&self, dir: Dir) -> usize {
self.dir(dir).buf.len()
}
}
enum Progress {
Event(EngineEvent),
NeedMore,
Again,
}
fn request_framing(
headers: &[(Bytes, Bytes)],
policy: SmugglingPolicy,
) -> Result<(BodyFraming, Vec<Normalization>), HttpPoison> {
let (te, cl, applied) = framing_headers(headers, policy)?;
if te {
return Ok((BodyFraming::Chunked, applied));
}
Ok(match cl {
Some(0) | None => (BodyFraming::None, applied),
Some(n) => (BodyFraming::ContentLength(n), applied),
})
}
fn framing_headers(
headers: &[(Bytes, Bytes)],
policy: SmugglingPolicy,
) -> Result<(bool, Option<u64>, Vec<Normalization>), HttpPoison> {
let observe = policy == SmugglingPolicy::Observe;
let mut applied = Vec::new();
let te_values: Vec<&Bytes> = headers
.iter()
.filter(|(k, _)| k.as_ref().eq_ignore_ascii_case(b"transfer-encoding"))
.map(|(_, v)| v)
.collect();
let mut chunked = false;
if !te_values.is_empty() {
if te_values.len() > 1 && !observe {
return Err(HttpPoison::DuplicateTransferEncoding);
}
let codings: Vec<&[u8]> = te_values
.iter()
.flat_map(|v| v.split(|&b| b == b','))
.map(|t| t.trim_ascii())
.filter(|t| !t.is_empty())
.collect();
chunked = codings
.last()
.is_some_and(|t| t.eq_ignore_ascii_case(b"chunked"));
if !observe {
for (i, coding) in codings.iter().enumerate() {
let is_chunked = coding.eq_ignore_ascii_case(b"chunked");
if !is_chunked && !coding.eq_ignore_ascii_case(b"identity") {
return Err(HttpPoison::UnknownTransferCoding);
}
if is_chunked && i != codings.len() - 1 {
return Err(HttpPoison::NonFinalChunked);
}
}
if !chunked {
return Err(HttpPoison::NonFinalChunked);
}
} else {
chunked = codings.iter().any(|t| t.eq_ignore_ascii_case(b"chunked"));
}
}
let mut lengths: Vec<u64> = Vec::new();
let mut saw_cl = false;
for (_, v) in headers
.iter()
.filter(|(k, _)| k.as_ref().eq_ignore_ascii_case(b"content-length"))
{
saw_cl = true;
for part in v.split(|&b| b == b',') {
let t = part.trim_ascii();
if t.is_empty() {
continue;
}
match parse_decimal(t) {
Some(n) => lengths.push(n),
None if observe => {}
None => return Err(HttpPoison::InvalidContentLength),
}
}
}
let content_length = if lengths.is_empty() {
if saw_cl && !observe && !chunked {
return Err(HttpPoison::InvalidContentLength);
}
None
} else {
let first = lengths[0];
if lengths.iter().any(|n| *n != first) {
if !observe {
return Err(HttpPoison::ConflictingContentLength);
}
} else if lengths.len() > 1 {
applied.push(Normalization::CollapsedContentLength);
}
Some(first)
};
if chunked && content_length.is_some() {
match policy {
SmugglingPolicy::Strict => {
return Err(HttpPoison::ContentLengthWithTransferEncoding);
}
SmugglingPolicy::Normalize => applied.push(Normalization::StrippedContentLength),
SmugglingPolicy::Observe => {}
}
return Ok((true, None, applied));
}
Ok((chunked, content_length, applied))
}
fn parse_decimal(bytes: &[u8]) -> Option<u64> {
if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
return None;
}
std::str::from_utf8(bytes).ok()?.parse().ok()
}
fn check_head_bytes(head: &[u8]) -> Result<(), HttpPoison> {
let mut i = 0;
while i < head.len() {
match head[i] {
b'\r' => {
if head.get(i + 1) != Some(&b'\n') {
return Err(HttpPoison::BareCr);
}
if i > 0
&& matches!(head.get(i + 2), Some(b' ') | Some(b'\t'))
&& head.get(i + 3).is_some()
{
return Err(HttpPoison::ObsFold);
}
i += 2;
}
_ => i += 1,
}
}
Ok(())
}
fn check_single_host(headers: &[(Bytes, Bytes)]) -> Result<(), HttpPoison> {
let n = headers
.iter()
.filter(|(k, _)| k.as_ref().eq_ignore_ascii_case(b"host"))
.count();
if n > 1 {
return Err(HttpPoison::DuplicateHost);
}
Ok(())
}
fn response_framing(
status: u16,
request_was_head: bool,
headers: &[(Bytes, Bytes)],
policy: SmugglingPolicy,
) -> Result<(BodyFraming, Vec<Normalization>), HttpPoison> {
if request_was_head || matches!(status, 100..=199 | 204 | 304) {
return Ok((BodyFraming::None, Vec::new()));
}
let (te, cl, applied) = framing_headers(headers, policy)?;
if te {
return Ok((BodyFraming::Chunked, applied));
}
Ok(match cl {
Some(0) => (BodyFraming::None, applied),
Some(n) => (BodyFraming::ContentLength(n), applied),
None => (BodyFraming::UntilClose, applied),
})
}
fn body_state(framing: BodyFraming) -> DirState {
match framing {
BodyFraming::None => DirState::Body(BodyState::Length { remaining: 0 }),
BodyFraming::ContentLength(n) => DirState::Body(BodyState::Length { remaining: n }),
BodyFraming::Chunked => DirState::Body(BodyState::Chunked(ChunkState::Size)),
BodyFraming::UntilClose => DirState::Body(BodyState::UntilClose),
}
}
const H2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
enum PrefaceMatch {
Yes,
Partial,
No,
}
fn preface_match(buf: &[u8]) -> PrefaceMatch {
let n = buf.len().min(H2_PREFACE.len());
if buf[..n] != H2_PREFACE[..n] {
return PrefaceMatch::No;
}
if buf.len() >= H2_PREFACE.len() {
PrefaceMatch::Yes
} else {
PrefaceMatch::Partial
}
}
fn header_value(headers: &[(Bytes, Bytes)], name: &[u8]) -> Option<Bytes> {
headers
.iter()
.find(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
.map(|(_, v)| v.clone())
}
fn signals_close(head: &Head) -> bool {
let tokens = |name: &[u8], want: &[u8]| {
head.headers
.iter()
.filter(|(k, _)| k.as_ref().eq_ignore_ascii_case(name))
.any(|(_, v)| {
v.split(|&b| b == b',')
.any(|t| t.trim_ascii().eq_ignore_ascii_case(want))
})
};
if tokens(b"connection", b"close") {
return true;
}
head.version == HttpVersion::Http1_0 && !tokens(b"connection", b"keep-alive")
}
type Span = (usize, usize);
struct HeadOffsets {
method: Span,
path: Span,
status: u16,
reason: Span,
version: HttpVersion,
headers: Vec<(Span, Span)>,
}
impl HeadOffsets {
fn into_head(self, dir: Dir, region: &Bytes, raw: Bytes) -> Head {
let cut = |(off, len): Span| -> Bytes {
if len == 0 {
Bytes::new()
} else {
region.slice(off..off + len)
}
};
Head {
dir,
method: cut(self.method),
path: cut(self.path),
status: self.status,
reason: cut(self.reason),
version: self.version,
headers: self
.headers
.into_iter()
.map(|(n, v)| (cut(n), cut(v)))
.collect(),
framing: BodyFraming::None,
interim: false,
applied: Vec::new(),
raw,
}
}
}
#[inline]
fn span_of(base: &[u8], sub: &[u8]) -> Span {
let off = (sub.as_ptr() as usize).saturating_sub(base.as_ptr() as usize);
debug_assert!(
off + sub.len() <= base.len(),
"sub-slice must lie within the head region"
);
(off.min(base.len()), sub.len())
}
fn parse_head_offsets(head: &[u8], dir: Dir, max_headers: usize) -> crate::Result<HeadOffsets> {
let mut stack = [httparse::EMPTY_HEADER; HEADER_STACK_SLOTS];
let mut heap;
let storage: &mut [httparse::Header<'_>] = if max_headers <= HEADER_STACK_SLOTS {
&mut stack[..max_headers.max(1)]
} else {
heap = vec![httparse::EMPTY_HEADER; max_headers];
&mut heap[..]
};
match dir {
Dir::Request => {
let mut req = httparse::Request::new(storage);
match req.parse(head) {
Ok(httparse::Status::Complete(_)) => {}
Ok(httparse::Status::Partial) => {
return Err(Error::parse(Module::Http, "incomplete request head"));
}
Err(e) => return Err(Error::parse_with(Module::Http, "httparse failed", e)),
}
let method = req
.method
.ok_or_else(|| Error::parse(Module::Http, "missing method"))?;
let path = req
.path
.ok_or_else(|| Error::parse(Module::Http, "missing path"))?;
Ok(HeadOffsets {
method: span_of(head, method.as_bytes()),
path: span_of(head, path.as_bytes()),
status: 0,
reason: (0, 0),
version: version_of(req.version)?,
headers: header_spans(head, req.headers),
})
}
Dir::Response => {
let mut resp = httparse::Response::new(storage);
match resp.parse(head) {
Ok(httparse::Status::Complete(_)) => {}
Ok(httparse::Status::Partial) => {
return Err(Error::parse(Module::Http, "incomplete response head"));
}
Err(e) => return Err(Error::parse_with(Module::Http, "httparse failed", e)),
}
let status = resp
.code
.ok_or_else(|| Error::parse(Module::Http, "missing status code"))?;
let reason = resp.reason.unwrap_or("");
Ok(HeadOffsets {
method: (0, 0),
path: (0, 0),
status,
reason: if reason.is_empty() {
(0, 0)
} else {
span_of(head, reason.as_bytes())
},
version: version_of(resp.version)?,
headers: header_spans(head, resp.headers),
})
}
}
}
fn version_of(v: Option<u8>) -> crate::Result<HttpVersion> {
match v.ok_or_else(|| Error::parse(Module::Http, "missing version"))? {
0 => Ok(HttpVersion::Http1_0),
1 => Ok(HttpVersion::Http1_1),
other => Err(Error::parse(
Module::Http,
format!("unknown version: {other}"),
)),
}
}
fn header_spans(head: &[u8], hs: &[httparse::Header<'_>]) -> Vec<(Span, Span)> {
let n = hs.iter().take_while(|h| !h.name.is_empty()).count();
let mut out = Vec::with_capacity(n);
for h in hs.iter().take(n) {
out.push((span_of(head, h.name.as_bytes()), span_of(head, h.value)));
}
out
}
fn parse_trailer_fields(block: &Bytes) -> Vec<(Bytes, Bytes)> {
let mut out = Vec::new();
let mut pos = 0usize;
if let Some(eol) = find_crlf_from(block, 0) {
pos = eol + 2;
}
while pos < block.len() {
let Some(eol) = find_crlf_from(block, pos) else {
break;
};
if eol == pos {
break; }
let line = &block[pos..eol];
if let Some(colon) = line.iter().position(|&b| b == b':') {
let name = (pos, colon);
let value_start = pos + colon + 1;
let trimmed = block[value_start..eol].len()
- block[value_start..eol]
.iter()
.rev()
.take_while(|b| b.is_ascii_whitespace())
.count();
let lead = block[value_start..eol]
.iter()
.take_while(|b| **b == b' ' || **b == b'\t')
.count();
out.push((
block.slice(name.0..name.0 + name.1),
block.slice(value_start + lead..value_start + trimmed.max(lead)),
));
}
pos = eol + 2;
}
out
}
fn find_crlf_from(buf: &[u8], from: usize) -> Option<usize> {
if from >= buf.len() {
return None;
}
buf[from..]
.windows(2)
.position(|w| w == b"\r\n")
.map(|p| p + from)
}
fn scan_blank_line(buf: &[u8], scanned: &mut usize) -> Option<usize> {
let start = (*scanned).saturating_sub(3);
let mut i = start;
while i < buf.len() {
if buf[i] == b'\n' {
if i >= 1 && buf[i - 1] == b'\n' {
*scanned = i + 1;
return Some(i + 1);
}
if i >= 3 && buf[i - 1] == b'\r' && buf[i - 2] == b'\n' && buf[i - 3] == b'\r' {
*scanned = i + 1;
return Some(i + 1);
}
}
i += 1;
}
*scanned = buf.len();
None
}
fn scan_crlf(buf: &[u8], scanned: &mut usize) -> Option<usize> {
let start = (*scanned).saturating_sub(1);
if let Some(pos) = find_crlf_from(buf, start) {
*scanned = pos;
return Some(pos);
}
*scanned = buf.len();
None
}
fn scan_trailer_end(buf: &[u8], scanned: &mut usize) -> Option<usize> {
let Some(first) = find_crlf_from(buf, 0) else {
*scanned = buf.len();
return None;
};
let mut pos = first + 2;
loop {
match find_crlf_from(buf, pos) {
Some(eol) if eol == pos => {
*scanned = 0;
return Some(eol + 2);
}
Some(eol) => pos = eol + 2,
None => {
*scanned = buf.len();
return None;
}
}
}
}
fn parse_hex(bytes: &[u8]) -> Option<u64> {
if bytes.is_empty() {
return None;
}
let s = std::str::from_utf8(bytes).ok()?;
u64::from_str_radix(s, 16).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn engine() -> Engine {
Engine::new(EngineLimits::default())
}
fn drain(e: &mut Engine, dir: Dir) -> Vec<EngineEvent> {
let mut out = Vec::new();
while let Ok(Some(ev)) = e.poll(dir) {
out.push(ev);
}
out
}
fn framing_of(e: &mut Engine, dir: Dir) -> BodyFraming {
loop {
match e.poll(dir) {
Ok(Some(EngineEvent::Head(h))) => return h.framing,
Ok(Some(_)) => continue,
other => panic!("expected a head, got {other:?}"),
}
}
}
#[test]
fn framing_follows_rfc_9112_section_6_3() {
let mut e = engine();
e.push(Dir::Request, b"POST /a HTTP/1.1\r\nHost: h\r\n\r\n");
assert_eq!(framing_of(&mut e, Dir::Request), BodyFraming::None);
let mut e = engine();
e.push(
Dir::Request,
b"POST /a HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n",
);
assert_eq!(framing_of(&mut e, Dir::Request), BodyFraming::Chunked);
let mut e = engine();
e.push(
Dir::Request,
b"POST /a HTTP/1.1\r\nContent-Length: 7\r\n\r\n",
);
assert_eq!(
framing_of(&mut e, Dir::Request),
BodyFraming::ContentLength(7)
);
let mut e = engine();
e.push(Dir::Request, b"HEAD /a HTTP/1.1\r\n\r\n");
let _ = drain(&mut e, Dir::Request);
e.push(
Dir::Response,
b"HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\n",
);
assert_eq!(framing_of(&mut e, Dir::Response), BodyFraming::None);
let mut e = engine();
e.push(Dir::Request, b"GET /a HTTP/1.1\r\n\r\n");
let _ = drain(&mut e, Dir::Request);
e.push(Dir::Response, b"HTTP/1.1 200 OK\r\n\r\n");
assert_eq!(framing_of(&mut e, Dir::Response), BodyFraming::UntilClose);
}
#[test]
fn request_head_then_end_for_bodyless() {
let mut e = engine();
e.push(Dir::Request, b"GET /a HTTP/1.1\r\nHost: x\r\n\r\n");
let evs = drain(&mut e, Dir::Request);
assert!(matches!(evs[0], EngineEvent::Head(_)));
assert!(matches!(evs[1], EngineEvent::End));
assert_eq!(evs.len(), 2);
}
#[test]
fn chunked_body_is_decoded_and_reassembles_wire_bytes() {
let mut e = engine();
let wire: &[u8] = b"POST /u HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n\
5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
e.push(Dir::Request, wire);
let evs = drain(&mut e, Dir::Request);
let mut decoded = Vec::new();
let mut raw = Vec::new();
for ev in &evs {
match ev {
EngineEvent::Head(h) => raw.extend_from_slice(&h.raw),
EngineEvent::Body { decoded: d, raw: r } => {
decoded.extend_from_slice(d);
raw.extend_from_slice(r);
}
EngineEvent::Trailers { raw: r, .. } => raw.extend_from_slice(r),
EngineEvent::End | EngineEvent::Switch(_) => {}
}
}
assert_eq!(decoded, b"hello world");
assert_eq!(raw, wire, "raw spans must reproduce the wire bytes");
assert!(matches!(evs.last(), Some(EngineEvent::End)));
}
#[test]
fn head_response_has_no_body_despite_content_length() {
let mut e = engine();
e.push(
Dir::Request,
b"HEAD /x HTTP/1.1\r\n\r\nGET /y HTTP/1.1\r\n\r\n",
);
let _ = drain(&mut e, Dir::Request);
e.push(
Dir::Response,
b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\nHTTP/1.1 204 No Content\r\n\r\n",
);
let evs = drain(&mut e, Dir::Response);
let heads: Vec<u16> = evs
.iter()
.filter_map(|ev| match ev {
EngineEvent::Head(h) => Some(h.status),
_ => None,
})
.collect();
assert_eq!(heads, vec![200, 204]);
}
#[test]
fn clean_fin_does_not_desync() {
let mut e = engine();
e.push(Dir::Request, b"GET / HTTP/1.1\r\n\r\n");
let _ = drain(&mut e, Dir::Request);
assert!(e.fin(Dir::Request).is_none());
assert!(!e.is_desynced(Dir::Request), "clean FIN must not desync");
assert!(e.is_closed(Dir::Request));
}
#[test]
fn until_close_body_flushes_at_fin() {
let mut e = engine();
e.push(Dir::Request, b"GET / HTTP/1.1\r\n\r\n");
let _ = drain(&mut e, Dir::Request);
e.push(Dir::Response, b"HTTP/1.1 200 OK\r\n\r\nhel");
let evs = drain(&mut e, Dir::Response);
assert!(matches!(evs[0], EngineEvent::Head(_)));
e.push(Dir::Response, b"lo");
let _ = drain(&mut e, Dir::Response);
let flushed = e.fin(Dir::Response);
assert!(matches!(flushed, Some(EngineEvent::Body { .. })));
}
#[test]
fn byte_at_a_time_matches_one_shot() {
let wire: &[u8] = b"POST /u HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n\
3\r\nabc\r\n0\r\n\r\n";
let mut whole = engine();
whole.push(Dir::Request, wire);
let a = drain(&mut whole, Dir::Request);
let mut split = engine();
let mut b = Vec::new();
for byte in wire {
split.push(Dir::Request, std::slice::from_ref(byte));
b.extend(drain(&mut split, Dir::Request));
}
let decoded = |evs: &[EngineEvent]| -> Vec<u8> {
let mut v = Vec::new();
for ev in evs {
if let EngineEvent::Body { decoded, .. } = ev {
v.extend_from_slice(decoded);
}
}
v
};
assert_eq!(decoded(&a), decoded(&b));
assert_eq!(decoded(&b), b"abc");
}
#[test]
fn oversized_head_desyncs_instead_of_growing() {
let limits = EngineLimits {
max_head_bytes: 64,
..EngineLimits::default()
};
let mut e = Engine::new(limits);
e.push(Dir::Request, &[b'A'; 200]);
assert!(e.poll(Dir::Request).is_err());
assert!(e.is_desynced(Dir::Request));
}
}