use crate::smuggling::SmugglingDetector;
use crate::types::{HeaderEntry, HeaderList, Http1Config, Http1Error, HttpRequest, HttpRequestLine};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ParserState {
WaitingRequest,
ReadingRequest,
ReadingHeaders,
ReadingBody,
ReadingChunkSize,
ReadingChunkData,
ReadingChunkTrailer,
HeadersComplete,
Error,
}
#[inline]
fn find_crlf_at(buf: &[u8], start: usize) -> Option<usize> {
let mut i = start;
while i + 1 < buf.len() {
if buf[i] == b'\r' && buf[i + 1] == b'\n' {
return Some(i);
}
i += 1;
}
None
}
#[inline]
fn scan_bare_lf(buf: &[u8], init_prev_cr: bool) -> (bool, bool) {
let mut prev_cr = init_prev_cr;
for &b in buf {
if b == b'\n' && !prev_cr {
return (true, false);
}
prev_cr = b == b'\r';
}
(false, prev_cr)
}
#[derive(Debug, Clone)]
pub struct Http1Parser {
config: Http1Config,
state: ParserState,
headers: HeaderList,
buf: Vec<u8>,
scanned: usize,
request_line: Option<HttpRequestLine>,
smuggler: SmugglingDetector,
first_byte_ms: Option<u64>,
last_activity_ms: Option<u64>,
body_expected: Option<u64>,
partial_body: Vec<u8>,
chunk_remaining: usize,
last_byte_was_cr: bool,
}
impl Http1Parser {
#[inline]
pub fn new(config: Http1Config) -> Self {
Self {
config,
state: ParserState::WaitingRequest,
headers: HeaderList::new(),
buf: Vec::with_capacity(256),
scanned: 0,
request_line: None,
smuggler: SmugglingDetector::new(),
first_byte_ms: None,
last_activity_ms: None,
body_expected: None,
partial_body: Vec::new(),
chunk_remaining: 0,
last_byte_was_cr: false,
}
}
pub fn reset(&mut self) {
self.state = ParserState::WaitingRequest;
self.headers.clear();
self.buf.clear();
self.scanned = 0;
self.request_line = None;
self.first_byte_ms = None;
self.last_activity_ms = None;
self.body_expected = None;
self.partial_body.clear();
self.chunk_remaining = 0;
self.last_byte_was_cr = false;
}
#[inline]
pub fn state(&self) -> ParserState {
self.state
}
pub fn check_idle_timeout(&mut self, now_ms: u64) -> Result<(), Http1Error> {
if matches!(
self.state,
ParserState::HeadersComplete | ParserState::Error
) {
return Ok(());
}
let deadline_base = self.last_activity_ms.or(self.first_byte_ms);
if let Some(base) = deadline_base
&& now_ms.saturating_sub(base) > self.config.idle_timeout_ms
{
self.state = ParserState::Error;
return Err(Http1Error::IdleTimeout);
}
Ok(())
}
#[inline]
pub fn note_first_byte(&mut self, now_ms: u64) {
if self.first_byte_ms.is_none() {
self.first_byte_ms = Some(now_ms);
}
}
#[inline]
pub fn note_activity(&mut self, now_ms: u64) {
if self.first_byte_ms.is_none() {
self.first_byte_ms = Some(now_ms);
}
self.last_activity_ms = Some(now_ms);
}
pub fn feed(&mut self, input: &[u8]) -> Result<(Option<HttpRequest>, usize), Http1Error> {
if input.is_empty() {
if matches!(self.state, ParserState::HeadersComplete) {
let req = self.build_request()?;
return Ok((Some(req), 0));
}
if self.scanned >= self.buf.len() {
return Ok((None, 0));
}
}
let scanned_before = self.scanned;
let old_buf_len = self.buf.len();
let new_buf_len = match old_buf_len.checked_add(input.len()) {
Some(n) => n,
None => {
self.state = ParserState::Error;
return Err(Http1Error::BufferOverflow);
}
};
if new_buf_len > self.config.max_buffer_size {
self.state = ParserState::Error;
return Err(Http1Error::BufferOverflow);
}
self.buf.extend_from_slice(input);
let new_bytes = self.buf.len() - old_buf_len;
while !matches!(self.state, ParserState::HeadersComplete | ParserState::Error) {
match self.state {
ParserState::WaitingRequest | ParserState::ReadingRequest => {
if let Some(cp) = find_crlf_at(&self.buf, self.scanned) {
if cp - self.scanned > self.config.max_request_line_size {
self.state = ParserState::Error;
return Err(Http1Error::RequestLineTooLong);
}
self.request_line =
Some(parse_request_line(&self.buf[self.scanned..cp])?);
self.scanned = cp + 2;
self.state = ParserState::ReadingHeaders;
} else {
if self.check_bare_lf(self.scanned) {
self.state = ParserState::Error;
return Err(Http1Error::SyntaxError(
"bare LF in request line not allowed (RFC 7230 §3.5)".into(),
));
}
if self.buf.len() - self.scanned > self.config.max_request_line_size {
self.state = ParserState::Error;
return Err(Http1Error::RequestLineTooLong);
}
break;
}
}
ParserState::ReadingHeaders => {
let mut line_start = self.scanned;
let mut progressed = false;
let mut headers_done = false;
'lines: loop {
if line_start >= self.buf.len() {
break 'lines;
}
let rest = &self.buf[line_start..];
if let Some(cp) = find_crlf(rest) {
let line_end = line_start + cp;
let line_slice = &self.buf[line_start..line_end];
line_start = line_end + 2;
progressed = true;
if line_slice.is_empty() {
self.scanned = line_start;
headers_done = true;
break 'lines;
}
if let Some(entry) = parse_header_line(line_slice, &self.config)? {
self.headers.push(entry);
}
if self.headers.len() > self.config.max_header_count {
self.state = ParserState::Error;
return Err(Http1Error::TooManyHeaders);
}
if self.current_header_size() > self.config.max_header_size {
self.state = ParserState::Error;
return Err(Http1Error::HeaderTooLarge);
}
} else {
if self.check_bare_lf(line_start) {
self.state = ParserState::Error;
return Err(Http1Error::SyntaxError(
"bare LF in header block not allowed (RFC 7230 §3.5)".into(),
));
}
break 'lines;
}
}
if !progressed {
if self.buf.len() - self.scanned > self.config.max_header_size {
self.state = ParserState::Error;
return Err(Http1Error::HeaderTooLarge);
}
break;
}
self.scanned = line_start;
if headers_done {
if let Err(e) = self.smuggler.detect_err_already_lowercased(self.headers.as_slice()) {
self.state = ParserState::Error;
return Err(e);
}
if !self.has_host() {
self.state = ParserState::Error;
return Err(Http1Error::MissingHost);
}
self.check_absolute_form_host_consistency()?;
let content_length = match parse_content_length(self.headers.as_slice()) {
Ok(v) => v,
Err(e) => {
self.state = ParserState::Error;
return Err(e);
}
};
let te = analyze_transfer_encoding(self.headers.as_slice());
let is_http10 = self
.request_line
.as_ref()
.map(|l| l.version.as_ref() == "HTTP/1.0")
.unwrap_or(false);
if is_http10 && te != TransferEncodingKind::None {
self.state = ParserState::Error;
return Err(Http1Error::SmugglingDetected(
"Transfer-Encoding in HTTP/1.0 request".into(),
));
}
if te == TransferEncodingKind::Invalid {
self.state = ParserState::Error;
return Err(Http1Error::SmugglingDetected(
"Transfer-Encoding without 'chunked'".into(),
));
}
let chunked = te == TransferEncodingKind::Chunked;
if chunked {
self.body_expected = None;
self.partial_body.clear();
self.chunk_remaining = 0;
self.state = ParserState::ReadingChunkSize;
} else if let Some(cl) = content_length {
if cl == 0 {
self.body_expected = Some(0);
self.partial_body.clear();
self.state = ParserState::HeadersComplete;
} else {
self.body_expected = Some(cl);
self.partial_body.clear();
let cl_usize = match usize::try_from(cl) {
Ok(n) => n,
Err(_) => {
self.state = ParserState::Error;
return Err(Http1Error::BodyTooLarge);
}
};
if cl_usize > self.config.max_body_size {
self.state = ParserState::Error;
return Err(Http1Error::BodyTooLarge);
}
self.partial_body.reserve(core::cmp::min(cl_usize, 64 * 1024));
self.state = ParserState::ReadingBody;
let tail_avail = self.buf.len().saturating_sub(self.scanned);
if tail_avail > 0 {
let copy_n =
core::cmp::min(tail_avail, cl_usize);
self.partial_body
.extend_from_slice(&self.buf[self.scanned..self.scanned + copy_n]);
self.scanned += copy_n;
}
if self.partial_body.len() as u64 >= cl {
self.state = ParserState::HeadersComplete;
}
}
} else {
self.body_expected = None;
self.partial_body.clear();
self.state = ParserState::HeadersComplete;
}
}
}
ParserState::ReadingBody => {
let cl = match self.body_expected {
Some(c) => c,
None => {
self.state = ParserState::HeadersComplete;
break;
}
};
let remaining = cl.saturating_sub(self.partial_body.len() as u64);
if remaining == 0 {
self.state = ParserState::HeadersComplete;
break;
}
let tail_avail = self.buf.len().saturating_sub(self.scanned);
if tail_avail == 0 {
break;
}
let copy_n = core::cmp::min(remaining, tail_avail as u64) as usize;
self.partial_body
.extend_from_slice(&self.buf[self.scanned..self.scanned + copy_n]);
self.scanned += copy_n;
if self.partial_body.len() as u64 >= cl {
self.state = ParserState::HeadersComplete;
}
}
ParserState::ReadingChunkSize => {
if let Some(cp) = find_crlf_at(&self.buf, self.scanned) {
let chunk_size = parse_chunk_size(&self.buf[self.scanned..cp])?;
self.scanned = cp + 2;
if chunk_size == 0 {
self.state = ParserState::ReadingChunkTrailer;
} else {
if self
.partial_body
.len()
.checked_add(chunk_size)
.map(|sum| sum > self.config.max_body_size)
.unwrap_or(true)
{
self.state = ParserState::Error;
return Err(Http1Error::BodyTooLarge);
}
self.chunk_remaining = chunk_size;
self.state = ParserState::ReadingChunkData;
}
} else {
break;
}
}
ParserState::ReadingChunkData => {
if self.chunk_remaining > 0 {
let tail_avail = self.buf.len().saturating_sub(self.scanned);
if tail_avail == 0 {
break;
}
let copy_n = core::cmp::min(self.chunk_remaining, tail_avail);
self.partial_body
.extend_from_slice(&self.buf[self.scanned..self.scanned + copy_n]);
self.scanned += copy_n;
self.chunk_remaining -= copy_n;
}
if self.chunk_remaining == 0 {
let tail_avail = self.buf.len().saturating_sub(self.scanned);
if tail_avail < 2 {
break;
}
if self.buf[self.scanned] != b'\r'
|| self.buf[self.scanned + 1] != b'\n'
{
self.state = ParserState::Error;
return Err(Http1Error::ChunkedError(
"missing CRLF after chunk data".into(),
));
}
self.scanned += 2;
self.state = ParserState::ReadingChunkSize;
}
}
ParserState::ReadingChunkTrailer => {
if let Some(cp) = find_crlf_at(&self.buf, self.scanned) {
let line_slice = &self.buf[self.scanned..cp];
let trailer_entry = if line_slice.is_empty() {
None
} else {
parse_header_line(line_slice, &self.config)?
};
self.scanned = cp + 2;
if line_slice.is_empty() {
self.state = ParserState::HeadersComplete;
} else {
if let Some(entry) = trailer_entry {
self.headers.push(entry);
}
if self.headers.len() > self.config.max_header_count {
self.state = ParserState::Error;
return Err(Http1Error::TooManyHeaders);
}
if self.current_header_size() > self.config.max_header_size {
self.state = ParserState::Error;
return Err(Http1Error::HeaderTooLarge);
}
}
} else {
break;
}
}
ParserState::HeadersComplete | ParserState::Error => break,
}
}
let processed_after = self.scanned;
let processed_delta = processed_after.saturating_sub(scanned_before);
let consumed = if new_bytes == 0 {
0
} else if processed_delta >= new_bytes {
new_bytes
} else {
processed_delta
};
self.last_byte_was_cr = self.buf.last() == Some(&b'\r');
if !matches!(self.state, ParserState::HeadersComplete) && self.scanned > 0 {
self.buf.drain(..self.scanned);
self.scanned = 0;
}
if matches!(self.state, ParserState::HeadersComplete) {
let req = self.build_request()?;
Ok((Some(req), consumed))
} else {
Ok((None, consumed))
}
}
fn check_bare_lf(&mut self, region_start: usize) -> bool {
let init_prev_cr = if region_start == 0 {
self.last_byte_was_cr
} else {
self.buf[region_start - 1] == b'\r'
};
let (bare, last_cr) = scan_bare_lf(&self.buf[region_start..], init_prev_cr);
self.last_byte_was_cr = last_cr;
bare
}
fn build_request(&mut self) -> Result<HttpRequest, Http1Error> {
let line = self
.request_line
.take()
.ok_or_else(|| Http1Error::Internal("missing request line".into()))?;
self.smuggler.detect_err_already_lowercased(self.headers.as_slice())?;
if !self.has_host() {
return Err(Http1Error::MissingHost);
}
let content_length = parse_content_length(self.headers.as_slice())?;
let chunked =
analyze_transfer_encoding(self.headers.as_slice()) == TransferEncodingKind::Chunked;
let keep_alive = if line.version.as_ref() == "HTTP/1.0" {
connection_has_token(self.headers.as_slice(), "keep-alive")
} else {
!connection_has_token(self.headers.as_slice(), "close")
};
let body: Vec<u8> = std::mem::take(&mut self.partial_body);
if let Some(cl) = content_length
&& !chunked
&& (body.len() as u64) != cl
{
self.state = ParserState::Error;
return Err(Http1Error::SyntaxError(format!(
"request body length mismatch: declared Content-Length={cl}, actual bytes={}",
body.len()
)));
}
let req = HttpRequest {
line,
headers: std::mem::take(&mut self.headers),
body,
content_length,
chunked,
keep_alive,
};
self.state = ParserState::WaitingRequest;
if self.scanned > 0 && self.scanned <= self.buf.len() {
self.buf.drain(..self.scanned);
}
self.scanned = 0;
self.body_expected = None;
self.chunk_remaining = 0;
self.last_byte_was_cr = false;
self.first_byte_ms = None;
self.last_activity_ms = None;
Ok(req)
}
}
fn parse_content_length(headers: &[HeaderEntry]) -> Result<Option<u64>, Http1Error> {
match headers.iter().find(|(k, _)| k.as_ref() == "content-length") {
None => Ok(None),
Some((_, v)) => {
let cl = v.parse::<u64>().map_err(|_| {
Http1Error::SmugglingDetected(format!(
"Content-Length value not parseable as u64: '{}'",
v.as_ref()
))
})?;
Ok(Some(cl))
}
}
}
fn connection_has_token(headers: &[HeaderEntry], token: &str) -> bool {
headers
.iter()
.filter(|(k, _)| k.as_ref() == "connection")
.flat_map(|(_, v)| v.split(','))
.map(str::trim)
.any(|t| t.eq_ignore_ascii_case(token))
}
fn absolute_form_authority(target: &str) -> Option<&str> {
let after_scheme = if target.len() >= 7 && target[..7].eq_ignore_ascii_case("http://") {
&target[7..]
} else if target.len() >= 8 && target[..8].eq_ignore_ascii_case("https://") {
&target[8..]
} else {
return None;
};
let end = after_scheme
.find(['/', '?', '#'])
.unwrap_or(after_scheme.len());
Some(&after_scheme[..end])
}
fn parse_request_line(line: &[u8]) -> Result<HttpRequestLine, Http1Error> {
let s = std::str::from_utf8(line)
.map_err(|_| Http1Error::SyntaxError("request line not UTF-8".into()))?;
let s = s.trim_end_matches('\r');
let mut parts = s.splitn(3, ' ');
let method = parts
.next()
.ok_or_else(|| Http1Error::SyntaxError("missing method".into()))?;
let target = parts
.next()
.ok_or_else(|| Http1Error::SyntaxError("missing target".into()))?;
let version = parts
.next()
.ok_or_else(|| Http1Error::SyntaxError("missing version".into()))?;
if !is_valid_method(method) {
return Err(Http1Error::UnsupportedMethod(method.to_string()));
}
if version != "HTTP/1.0" && version != "HTTP/1.1" {
return Err(Http1Error::UnsupportedVersion(version.to_string()));
}
if target.chars().any(|c| (c as u32) < 0x20) {
return Err(Http1Error::SyntaxError(
"target contains control chars".into(),
));
}
Ok(HttpRequestLine {
method: method.into(),
target: target.into(),
version: version.into(),
})
}
fn parse_header_line(line: &[u8], config: &Http1Config) -> Result<Option<HeaderEntry>, Http1Error> {
if line.is_empty() {
return Ok(None);
}
if line.iter().any(|&b| b == b'\r' || b == b'\n') {
return Err(Http1Error::SyntaxError(
"header line contains CR/LF".into(),
));
}
if line.first() == Some(&b' ') || line.first() == Some(&b'\t') {
return Err(Http1Error::SyntaxError(
"header line folding is forbidden".into(),
));
}
let colon = line
.iter()
.position(|&b| b == b':')
.ok_or_else(|| Http1Error::SyntaxError("missing colon in header".into()))?;
let name = std::str::from_utf8(&line[..colon])
.map_err(|_| Http1Error::SyntaxError("header name not UTF-8".into()))?;
let value = std::str::from_utf8(&line[colon + 1..])
.map_err(|_| Http1Error::SyntaxError("header value not UTF-8".into()))?;
if !is_valid_header_name(name) {
return Err(Http1Error::SyntaxError(format!(
"invalid header name: {name}"
)));
}
if name.len() > config.max_header_name_len {
return Err(Http1Error::HeaderTooLarge);
}
let val_trim = value.trim_matches(|c| c == ' ' || c == '\t');
if val_trim.len() > config.max_header_value_len {
return Err(Http1Error::HeaderTooLarge);
}
if val_trim.contains('\r') || val_trim.contains('\n') {
return Err(Http1Error::SyntaxError(
"header value contains CR/LF".into(),
));
}
let name = name.to_ascii_lowercase();
Ok(Some((name.into_boxed_str(), val_trim.into())))
}
impl Http1Parser {
fn current_header_size(&self) -> usize {
self.headers
.iter()
.map(|(k, v)| k.len() + v.len() + 2)
.sum()
}
#[inline]
fn has_host(&self) -> bool {
self.headers.iter().any(|(k, _)| k.as_ref() == "host")
}
#[inline]
fn host_value(&self) -> Option<&str> {
self.headers
.iter()
.find(|(k, _)| k.as_ref() == "host")
.map(|(_, v)| v.as_ref())
}
fn check_absolute_form_host_consistency(&mut self) -> Result<(), Http1Error> {
let target = match self.request_line.as_ref() {
Some(line) => line.target.as_ref(),
None => return Ok(()),
};
let authority = match absolute_form_authority(target) {
Some(a) => a,
None => return Ok(()),
};
let host = self.host_value().unwrap_or("");
if authority.is_empty() || !authority.eq_ignore_ascii_case(host) {
self.state = ParserState::Error;
return Err(Http1Error::ProtocolInconsistency(
"absolute-form authority does not match Host header".into(),
));
}
Ok(())
}
}
#[inline]
fn find_crlf(buf: &[u8]) -> Option<usize> {
find_crlf_at(buf, 0)
}
pub(crate) fn parse_chunk_size(line: &[u8]) -> Result<usize, Http1Error> {
let line_str = std::str::from_utf8(line)
.map_err(|_| Http1Error::ChunkedError("chunk size line not UTF-8".into()))?;
let size_part = line_str.split(';').next().unwrap_or("");
let size_part = size_part.trim();
if size_part.is_empty() {
return Err(Http1Error::ChunkedError("empty chunk size".into()));
}
if !size_part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Http1Error::ChunkedError(format!(
"invalid chunk size: non-hex digit in '{size_part}'"
)));
}
usize::from_str_radix(size_part, 16)
.map_err(|_| Http1Error::ChunkedError(format!("chunk size overflow: {size_part}")))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum TransferEncodingKind {
None,
Chunked,
Invalid,
}
fn analyze_transfer_encoding(headers: &[HeaderEntry]) -> TransferEncodingKind {
let mut saw_te = false;
let mut saw_chunked = false;
for (k, v) in headers {
if k.as_ref() != "transfer-encoding" {
continue;
}
saw_te = true;
for tok in v.split(',') {
let tok = tok.trim();
if tok.is_empty() {
continue;
}
if tok.eq_ignore_ascii_case("chunked") {
saw_chunked = true;
}
}
}
if !saw_te {
TransferEncodingKind::None
} else if saw_chunked {
TransferEncodingKind::Chunked
} else {
TransferEncodingKind::Invalid
}
}
#[inline]
fn is_valid_method(m: &str) -> bool {
m.parse::<zenith_api::Method>().is_ok()
}
#[inline]
fn is_valid_header_name(s: &str) -> bool {
zenith_api::normalize::is_valid_header_name(s)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_config() -> Http1Config {
Http1Config::new()
}
#[test]
fn test_parse_get_request() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.line.method.as_ref(), "GET");
assert_eq!(req.line.target.as_ref(), "/");
assert_eq!(req.line.version.as_ref(), "HTTP/1.1");
assert_eq!(req.get_header("host"), Some("example.com"));
assert!(req.keep_alive);
}
#[test]
fn test_parse_post_with_content_length() {
let mut p = Http1Parser::new(make_config());
let input =
b"POST /api HTTP/1.1\r\nHost: example.com\r\nContent-Length: 13\r\n\r\nHello, World!";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.content_length, Some(13));
assert_eq!(req.body, b"Hello, World!");
assert!(!req.chunked);
}
#[test]
fn test_parse_chunked_request() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n6\r\n World\r\n0\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(req.chunked);
assert_eq!(req.content_length, None);
assert_eq!(req.body, b"Hello World");
}
#[test]
fn test_parse_chunked_empty_body() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(req.chunked);
assert!(req.body.is_empty());
}
#[test]
fn test_parse_chunked_with_chunk_ext() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5;name=value\r\nHello\r\n0\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.body, b"Hello");
}
#[test]
fn test_parse_chunked_with_trailer() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\nX-Checksum: abc123\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.body, b"Hello");
assert_eq!(req.get_header("x-checksum"), Some("abc123"));
}
#[test]
fn test_parse_chunked_multiple_chunks() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nabc\r\n3\r\ndef\r\n3\r\nghi\r\n0\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.body, b"abcdefghi");
}
#[test]
fn test_parse_chunked_incremental_byte_by_byte() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n";
let mut req_opt: Option<HttpRequest> = None;
for i in 0..input.len() {
let (r, _) = p.feed(&input[i..i + 1]).unwrap();
if r.is_some() {
req_opt = r;
}
}
let req = req_opt.expect("should parse complete request after byte-by-byte feed");
assert!(req.chunked);
assert_eq!(req.body, b"Hello");
}
#[test]
fn test_parse_chunked_incremental_split_at_chunks() {
let mut p = Http1Parser::new(make_config());
let part1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHel";
let part2 = b"lo\r\n0\r\n\r\n";
let (r1, _) = p.feed(part1).unwrap();
assert!(r1.is_none(), "first part should not complete");
let (r2, _) = p.feed(part2).unwrap();
let req = r2.expect("second part should complete");
assert_eq!(req.body, b"Hello");
}
#[test]
fn test_parse_chunked_invalid_hex_size() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\nXY\r\nHello\r\n0\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::ChunkedError(_))));
}
#[test]
fn test_parse_chunked_empty_size_line() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n\r\nHello\r\n0\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::ChunkedError(_))));
}
#[test]
fn test_parse_chunked_missing_crlf_after_data() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHelloXX\r\n0\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::ChunkedError(_))));
}
#[test]
fn test_parse_chunked_body_too_large() {
let config = Http1Config::new().with_max_body_size(10);
let mut p = Http1Parser::new(config);
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\nB\r\nHello World\r\n0\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::BodyTooLarge)));
}
#[test]
fn test_parse_chunked_large_hex_size() {
let mut p = Http1Parser::new(make_config());
let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\nA\r\n0123456789\r\n0\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.body, b"0123456789");
}
#[test]
fn test_parse_chunked_partial_last_chunk_crlf() {
let mut p = Http1Parser::new(make_config());
let part1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n";
let part2 = b"\r\n";
let (r1, _) = p.feed(part1).unwrap();
assert!(r1.is_none(), "should not complete before trailer CRLF");
let (r2, _) = p.feed(part2).unwrap();
assert!(r2.is_some(), "should complete after trailer CRLF");
}
#[test]
fn test_parse_chunked_split_at_chunk_data_crlf() {
let mut p = Http1Parser::new(make_config());
let part1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello";
let part2 = b"\r\n0\r\n\r\n";
let (r1, _) = p.feed(part1).unwrap();
assert!(r1.is_none());
let (r2, _) = p.feed(part2).unwrap();
let req = r2.unwrap();
assert_eq!(req.body, b"Hello");
}
#[test]
fn test_bare_lf_rejected_in_request_line() {
let mut p = Http1Parser::new(Http1Config::new());
let result = p.feed(b"GET /status HTTP/1.1\nHost: example.com\n\n");
assert!(matches!(result, Err(Http1Error::SyntaxError(_))), "bare LF 必须拒绝: {:?}", result);
}
#[test]
fn test_bare_lf_rejected_in_headers() {
let mut p = Http1Parser::new(Http1Config::new());
let result = p.feed(b"GET /status HTTP/1.1\r\nHost: example.com\nX-Other: yes\r\n\r\n");
assert!(matches!(result, Err(Http1Error::SyntaxError(_))), "header 区块裸 LF 必须拒绝: {:?}", result);
}
#[test]
fn test_crlf_lf_only_edge_accept() {
let mut p = Http1Parser::new(Http1Config::new());
let result = p.feed(b"GET /status HTTP/1.1\r\nHost: example.com\r\n\r\n");
assert!(result.is_ok(), "合法 CRLF 请求不得被拒绝: {:?}", result);
}
#[test]
fn test_crlf_split_across_feeds_accepted() {
let mut p = Http1Parser::new(Http1Config::new());
let r1 = p.feed(b"GET /status HTTP/1.1\r");
assert!(r1.is_ok(), "以 CR 结尾的分段不得报错: {:?}", r1);
let (req, _) = p
.feed(b"\nHost: example.com\r\n\r\n")
.expect("补全 CRLF 后应解析成功");
assert!(req.is_some(), "跨 feed 拆分的 CRLF 必须被接受");
}
#[test]
fn test_bare_lf_split_across_feeds_rejected() {
let mut p = Http1Parser::new(Http1Config::new());
let r1 = p.feed(b"GET /status HTTP/1.1");
assert!(r1.is_ok());
let r2 = p.feed(b"\n");
assert!(
matches!(r2, Err(Http1Error::SyntaxError(_))),
"跨 feed 裸 LF 必须拒绝,实际 {r2:?}"
);
}
#[test]
fn test_bare_lf_split_across_feeds_rejected_full() {
let mut p = Http1Parser::new(Http1Config::new());
let r1 = p.feed(b"GET /status HTTP/1.1");
assert!(r1.is_ok());
let r2 = p.feed(b"\nHost: x\r\n\r\n");
assert!(
r2.is_err(),
"请求行内跨 feed 裸 LF 必须拒绝,实际 {r2:?}"
);
}
#[test]
fn test_crlf_split_across_feeds_in_headers_accepted() {
let mut p = Http1Parser::new(Http1Config::new());
let r1 = p.feed(b"GET / HTTP/1.1\r\nHost: example.com\r");
assert!(r1.is_ok());
let (req, _) = p
.feed(b"\nX-Test: v\r\n\r\n")
.expect("头部 CRLF 跨 feed 拆分应解析成功");
assert!(req.is_some());
}
#[test]
fn test_missing_host() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::MissingHost)));
}
#[test]
fn test_invalid_method() {
let mut p = Http1Parser::new(make_config());
let input = b"FOOBAR / HTTP/1.1\r\nHost: x\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::UnsupportedMethod(_))));
}
#[test]
fn test_invalid_version() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/2.0\r\nHost: x\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::UnsupportedVersion(_))));
}
#[test]
fn test_cl_te_smuggling_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::SmugglingDetected(_))));
}
#[test]
fn test_incremental_parsing() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
let (req1, consumed1) = p.feed(&input[..10]).unwrap();
assert!(req1.is_none());
assert_eq!(consumed1, 0, "partial input should not be fully consumed");
let (req2, consumed2) = p.feed(&input[10..]).unwrap();
assert!(req2.is_some(), "should parse full request on second call");
assert_eq!(consumed2, input.len() - 10);
}
#[test]
fn test_reset() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
p.feed(input).unwrap();
p.reset();
assert_eq!(p.state(), ParserState::WaitingRequest);
}
#[test]
fn test_invalid_header_name_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: x\r\nBad\x00: value\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_smuggling_detected_by_parser() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err(), "expected CL.TE smuggling error, got {r:?}");
}
#[test]
fn test_double_content_length_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 10\r\nContent-Length: 20\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err(), "expected double CL error, got {r:?}");
}
#[test]
fn test_parser_state_variants() {
let states = [
ParserState::WaitingRequest,
ParserState::ReadingRequest,
ParserState::ReadingHeaders,
ParserState::ReadingBody,
ParserState::ReadingChunkSize,
ParserState::ReadingChunkData,
ParserState::ReadingChunkTrailer,
ParserState::HeadersComplete,
ParserState::Error,
];
for (i, s) in states.iter().enumerate() {
assert_eq!(*s, states[i]);
}
assert_ne!(ParserState::WaitingRequest, ParserState::Error);
}
#[test]
fn test_parse_http10_request() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.line.version.as_ref(), "HTTP/1.0");
}
#[test]
fn test_header_name_lowercase_normalization() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/plain\r\nX-Custom-Header: value\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.get_header("host"), Some("example.com"));
assert_eq!(req.get_header("content-type"), Some("text/plain"));
assert_eq!(req.get_header("x-custom-header"), Some("value"));
}
#[test]
fn test_header_value_leading_whitespace_trimmed() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Test:\tvalue\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.get_header("host"), Some("example.com"));
assert_eq!(req.get_header("x-test"), Some("value"));
}
#[test]
fn test_missing_method_in_request_line() {
let mut p = Http1Parser::new(make_config());
let input = b" / HTTP/1.1\r\nHost: x\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_missing_version_in_request_line() {
let mut p = Http1Parser::new(make_config());
let input = b"GET /\r\nHost: x\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_only_method_in_request_line() {
let mut p = Http1Parser::new(make_config());
let input = b"GET\r\nHost: x\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_header_line_folding_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n continued\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_header_line_folding_tab_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\tcontinued\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_header_missing_colon() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost example.com\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_empty_input() {
let mut p = Http1Parser::new(make_config());
let (req, consumed) = p.feed(b"").unwrap();
assert!(req.is_none());
assert_eq!(consumed, 0);
}
#[test]
fn test_multiple_headers_same_name() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Test: first\r\nX-Test: second\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
let test_headers: Vec<_> = req.headers.iter().filter(|(k, _)| k.as_ref() == "x-test").collect();
assert_eq!(test_headers.len(), 2);
}
#[test]
fn test_request_line_with_query_string() {
let mut p = Http1Parser::new(make_config());
let input = b"GET /path?query=value&foo=bar HTTP/1.1\r\nHost: example.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.line.target.as_ref(), "/path?query=value&foo=bar");
}
#[test]
fn test_all_valid_methods() {
let methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"];
for method in methods.iter() {
let mut p = Http1Parser::new(make_config());
let input = format!("{} / HTTP/1.1\r\nHost: example.com\r\n\r\n", method);
let (req, _) = p.feed(input.as_bytes()).unwrap();
let req = req.unwrap();
assert_eq!(req.line.method.as_ref(), *method);
}
}
#[test]
fn test_lowercase_method_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"get / HTTP/1.1\r\nHost: example.com\r\n\r\n";
let r = p.feed(input);
assert!(
matches!(r, Err(Http1Error::UnsupportedMethod(_))),
"小写方法必须拒绝,实际 {r:?}"
);
let mut p = Http1Parser::new(make_config());
let input = b"Get / HTTP/1.1\r\nHost: example.com\r\n\r\n";
assert!(matches!(p.feed(input), Err(Http1Error::UnsupportedMethod(_))));
}
#[test]
fn test_http10_default_connection_close() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(!req.keep_alive, "HTTP/1.0 无 Connection 头应默认关闭");
}
#[test]
fn test_http10_explicit_keep_alive() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.0\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(req.keep_alive);
}
#[test]
fn test_http11_default_keep_alive() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
assert!(req.unwrap().keep_alive);
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
assert!(!req.unwrap().keep_alive);
}
#[test]
fn test_http10_transfer_encoding_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"POST / HTTP/1.0\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
let r = p.feed(input);
assert!(
matches!(r, Err(Http1Error::SmugglingDetected(_))),
"HTTP/1.0 + TE 必须拒绝,实际 {r:?}"
);
}
#[test]
fn test_transfer_encoding_without_chunked_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: gzip\r\n\r\n";
let r = p.feed(input);
assert!(
matches!(r, Err(Http1Error::SmugglingDetected(_))),
"TE 无 chunked 必须拒绝,实际 {r:?}"
);
}
#[test]
fn test_transfer_encoding_identity_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: identity\r\n\r\n";
let r = p.feed(input);
assert!(
matches!(r, Err(Http1Error::SmugglingDetected(_))),
"TE: identity 必须走私拒绝,实际 {r:?}"
);
}
#[test]
fn test_transfer_encoding_gzip_chunked_parsed() {
let mut p = Http1Parser::new(make_config());
let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: gzip, chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(req.chunked);
assert_eq!(req.body, b"Hello");
}
#[test]
fn test_header_name_too_long() {
let config = Http1Config::new().with_max_header_size(1024);
let mut p = Http1Parser::new(config);
let long_name = "X-".to_string() + &"A".repeat(100);
let input = format!("GET / HTTP/1.1\r\nHost: x\r\n{}: value\r\n\r\n", long_name);
let r = p.feed(input.as_bytes());
let _ = r;
}
#[test]
fn test_control_char_in_target_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET /path\x01 HTTP/1.1\r\nHost: x\r\n\r\n";
let r = p.feed(input);
assert!(r.is_err());
}
#[test]
fn test_parser_clone() {
let p = Http1Parser::new(make_config());
let p2 = p.clone();
assert_eq!(p.state(), p2.state());
}
#[test]
fn test_parser_debug_format() {
let p = Http1Parser::new(make_config());
let s = format!("{:?}", p);
assert!(!s.is_empty());
}
#[test]
fn test_connection_close_header() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(!req.keep_alive);
}
#[test]
fn test_content_length_overflow_u64_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 99999999999999999999999\r\n\r\n";
let r = p.feed(input);
assert!(
matches!(r, Err(Http1Error::SmugglingDetected(_))),
"超 u64 的 CL 必须 fail-closed 拒绝,实际 {r:?}"
);
}
#[test]
fn test_content_length_valid_still_accepted() {
let mut p = Http1Parser::new(make_config());
let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.content_length, Some(5));
assert_eq!(req.body, b"hello");
}
#[test]
fn test_complete_request_line_too_long_rejected() {
let config = Http1Config::new();
let line_limit = config.max_request_line_size;
let mut p = Http1Parser::new(config);
let long_path = "a".repeat(line_limit + 1);
let input = format!("GET /{long_path} HTTP/1.1\r\nHost: x\r\n\r\n");
let r = p.feed(input.as_bytes());
assert!(
matches!(r, Err(Http1Error::RequestLineTooLong)),
"完整超长请求行必须拒绝,实际 {r:?}"
);
assert_eq!(p.state(), ParserState::Error);
}
#[test]
fn test_incomplete_request_line_too_long_rejected() {
let config = Http1Config::new();
let line_limit = config.max_request_line_size;
let mut p = Http1Parser::new(config);
let junk = "a".repeat(line_limit + 1);
let r = p.feed(junk.as_bytes());
assert!(matches!(r, Err(Http1Error::RequestLineTooLong)), "实际 {r:?}");
}
#[test]
fn test_request_line_at_limit_accepted() {
let config = Http1Config::new();
let line_limit = config.max_request_line_size;
let pad = line_limit - "GET /".len() - " HTTP/1.1".len();
let mut p = Http1Parser::new(config);
let input = format!("GET /{} HTTP/1.1\r\nHost: x\r\n\r\n", "a".repeat(pad));
let (req, _) = p.feed(input.as_bytes()).unwrap();
assert!(req.is_some(), "恰在上限内的请求行必须放行");
}
#[test]
fn test_absolute_form_matching_host_accepted() {
let mut p = Http1Parser::new(make_config());
let input = b"GET http://example.com/path?q=1 HTTP/1.1\r\nHost: EXAMPLE.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.line.target.as_ref(), "http://example.com/path?q=1");
}
#[test]
fn test_absolute_form_with_port_matching_host_accepted() {
let mut p = Http1Parser::new(make_config());
let input = b"GET https://example.com:8443/x HTTP/1.1\r\nHost: example.com:8443\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
assert!(req.is_some());
}
#[test]
fn test_absolute_form_host_mismatch_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET http://evil.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
let r = p.feed(input);
assert!(
matches!(r, Err(Http1Error::ProtocolInconsistency(_))),
"authority 与 Host 不一致必须拒绝,实际 {r:?}"
);
}
#[test]
fn test_absolute_form_port_mismatch_rejected() {
let mut p = Http1Parser::new(make_config());
let input = b"GET http://example.com:8080/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
let r = p.feed(input);
assert!(matches!(r, Err(Http1Error::ProtocolInconsistency(_))), "实际 {r:?}");
}
#[test]
fn test_origin_form_unaffected_by_host_check() {
let mut p = Http1Parser::new(make_config());
let input = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
assert!(req.is_some());
}
#[test]
fn test_connection_close_among_tokens() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close, upgrade\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(!req.keep_alive, "token 列表中的 close 必须生效");
}
#[test]
fn test_connection_keep_alive_among_tokens_http10() {
let mut p = Http1Parser::new(make_config());
let input =
b"GET / HTTP/1.0\r\nHost: example.com\r\nConnection: keep-alive, timeout=5\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(req.keep_alive, "token 列表中的 keep-alive 必须生效");
}
#[test]
fn test_connection_prefixed_token_not_close() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: x-close\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert!(req.keep_alive);
}
#[test]
fn test_header_value_trailing_ows_trimmed() {
let mut p = Http1Parser::new(make_config());
let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Test: value \t\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
let req = req.unwrap();
assert_eq!(req.get_header("x-test"), Some("value"));
}
#[test]
fn test_idle_timeout_during_body_read() {
let config = Http1Config::new().with_idle_timeout_ms(30_000);
let mut p = Http1Parser::new(config);
p.note_activity(1_000);
let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\n\r\n";
let (req, _) = p.feed(input).unwrap();
assert!(req.is_none());
assert_eq!(p.state(), ParserState::ReadingBody);
let r = p.check_idle_timeout(1_000 + 30_001);
assert!(matches!(r, Err(Http1Error::IdleTimeout)), "实际 {r:?}");
assert_eq!(p.state(), ParserState::Error);
}
#[test]
fn test_idle_timeout_refreshed_by_activity() {
let config = Http1Config::new().with_idle_timeout_ms(30_000);
let mut p = Http1Parser::new(config);
p.note_activity(0);
let part1 = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\n\r\nhe";
let (r1, _) = p.feed(part1).unwrap();
assert!(r1.is_none());
assert_eq!(p.state(), ParserState::ReadingBody);
p.note_activity(20_000);
assert!(p.check_idle_timeout(45_000).is_ok(), "活跃后 25s 不得超时");
let r = p.check_idle_timeout(20_000 + 30_001);
assert!(matches!(r, Err(Http1Error::IdleTimeout)));
}
#[test]
fn test_idle_timeout_chunked_phase() {
let config = Http1Config::new().with_idle_timeout_ms(30_000);
let mut p = Http1Parser::new(config);
p.note_activity(500);
let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHel";
let (r1, _) = p.feed(input).unwrap();
assert!(r1.is_none());
assert_eq!(p.state(), ParserState::ReadingChunkData);
let r = p.check_idle_timeout(500 + 30_001);
assert!(matches!(r, Err(Http1Error::IdleTimeout)));
}
#[test]
fn test_pipelining_two_requests_single_feed() {
let mut p = Http1Parser::new(make_config());
let req1 = b"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n";
let req2 = b"GET /b HTTP/1.1\r\nHost: example.com\r\n\r\n";
let mut input = Vec::with_capacity(req1.len() + req2.len());
input.extend_from_slice(req1);
input.extend_from_slice(req2);
let (r1, c1) = p.feed(&input).unwrap();
let r1 = r1.expect("first request must complete");
assert_eq!(r1.line.target.as_ref(), "/a");
assert_eq!(c1, req1.len(), "consumed 必须恰好等于第一个请求字节数");
let (r2, c2) = p.feed(&[]).unwrap();
let r2 = r2.expect("second request must be buffered internally");
assert_eq!(r2.line.target.as_ref(), "/b");
assert_eq!(c2, 0, "空输入不消耗字节");
}
#[test]
fn test_pipelining_body_then_next_request() {
let mut p = Http1Parser::new(make_config());
let req1 = b"POST /x HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";
let req2 = b"GET /y HTTP/1.1\r\nHost: example.com\r\n\r\n";
let mut input = Vec::with_capacity(req1.len() + req2.len());
input.extend_from_slice(req1);
input.extend_from_slice(req2);
let (r1, c1) = p.feed(&input).unwrap();
let r1 = r1.expect("first request must complete");
assert_eq!(r1.body, b"hello");
assert_eq!(c1, req1.len());
let (r2, _) = p.feed(&[]).unwrap();
let r2 = r2.expect("second request must follow");
assert_eq!(r2.line.target.as_ref(), "/y");
}
}