1use crate::smuggling::SmugglingDetector;
6use crate::types::{HeaderEntry, HeaderList, Http1Config, Http1Error, HttpRequest, HttpRequestLine};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum ParserState {
11 WaitingRequest,
13 ReadingRequest,
15 ReadingHeaders,
17 ReadingBody,
19 ReadingChunkSize,
21 ReadingChunkData,
23 ReadingChunkTrailer,
25 HeadersComplete,
27 Error,
29}
30
31#[inline]
33fn find_crlf_at(buf: &[u8], start: usize) -> Option<usize> {
34 let mut i = start;
35 while i + 1 < buf.len() {
36 if buf[i] == b'\r' && buf[i + 1] == b'\n' {
37 return Some(i);
38 }
39 i += 1;
40 }
41 None
42}
43
44#[inline]
53fn scan_bare_lf(buf: &[u8], init_prev_cr: bool) -> (bool, bool) {
54 let mut prev_cr = init_prev_cr;
55 for &b in buf {
56 if b == b'\n' && !prev_cr {
57 return (true, false);
58 }
59 prev_cr = b == b'\r';
60 }
61 (false, prev_cr)
62}
63
64#[derive(Debug, Clone)]
66pub struct Http1Parser {
67 config: Http1Config,
68 state: ParserState,
69 headers: HeaderList,
71 buf: Vec<u8>,
73 scanned: usize,
75 request_line: Option<HttpRequestLine>,
77 smuggler: SmugglingDetector,
79 first_byte_ms: Option<u64>,
81 last_activity_ms: Option<u64>,
83 body_expected: Option<u64>,
85 partial_body: Vec<u8>,
87 chunk_remaining: usize,
89 last_byte_was_cr: bool,
92}
93
94impl Http1Parser {
95 #[inline]
97 pub fn new(config: Http1Config) -> Self {
98 Self {
99 config,
100 state: ParserState::WaitingRequest,
101 headers: HeaderList::new(),
102 buf: Vec::with_capacity(256),
103 scanned: 0,
104 request_line: None,
105 smuggler: SmugglingDetector::new(),
106 first_byte_ms: None,
107 last_activity_ms: None,
108 body_expected: None,
109 partial_body: Vec::new(),
110 chunk_remaining: 0,
111 last_byte_was_cr: false,
112 }
113 }
114
115 pub fn reset(&mut self) {
117 self.state = ParserState::WaitingRequest;
118 self.headers.clear();
119 self.buf.clear();
120 self.scanned = 0;
121 self.request_line = None;
122 self.first_byte_ms = None;
123 self.last_activity_ms = None;
124 self.body_expected = None;
125 self.partial_body.clear();
126 self.chunk_remaining = 0;
127 self.last_byte_was_cr = false;
128 }
129
130 #[inline]
132 pub fn state(&self) -> ParserState {
133 self.state
134 }
135
136 pub fn check_idle_timeout(&mut self, now_ms: u64) -> Result<(), Http1Error> {
147 if matches!(
149 self.state,
150 ParserState::HeadersComplete | ParserState::Error
151 ) {
152 return Ok(());
153 }
154 let deadline_base = self.last_activity_ms.or(self.first_byte_ms);
155 if let Some(base) = deadline_base
156 && now_ms.saturating_sub(base) > self.config.idle_timeout_ms
157 {
158 self.state = ParserState::Error;
159 return Err(Http1Error::IdleTimeout);
160 }
161 Ok(())
162 }
163
164 #[inline]
169 pub fn note_first_byte(&mut self, now_ms: u64) {
170 if self.first_byte_ms.is_none() {
171 self.first_byte_ms = Some(now_ms);
172 }
173 }
174
175 #[inline]
180 pub fn note_activity(&mut self, now_ms: u64) {
181 if self.first_byte_ms.is_none() {
182 self.first_byte_ms = Some(now_ms);
183 }
184 self.last_activity_ms = Some(now_ms);
185 }
186
187 pub fn feed(&mut self, input: &[u8]) -> Result<(Option<HttpRequest>, usize), Http1Error> {
189 if input.is_empty() {
190 if matches!(self.state, ParserState::HeadersComplete) {
192 let req = self.build_request()?;
193 return Ok((Some(req), 0));
194 }
195 if self.scanned >= self.buf.len() {
197 return Ok((None, 0));
198 }
199 }
201
202 let scanned_before = self.scanned;
204 let old_buf_len = self.buf.len();
205
206 let new_buf_len = match old_buf_len.checked_add(input.len()) {
209 Some(n) => n,
210 None => {
211 self.state = ParserState::Error;
212 return Err(Http1Error::BufferOverflow);
213 }
214 };
215 if new_buf_len > self.config.max_buffer_size {
216 self.state = ParserState::Error;
217 return Err(Http1Error::BufferOverflow);
218 }
219
220 self.buf.extend_from_slice(input);
221 let new_bytes = self.buf.len() - old_buf_len;
222
223 while !matches!(self.state, ParserState::HeadersComplete | ParserState::Error) {
225 match self.state {
226 ParserState::WaitingRequest | ParserState::ReadingRequest => {
227 if let Some(cp) = find_crlf_at(&self.buf, self.scanned) {
228 if cp - self.scanned > self.config.max_request_line_size {
231 self.state = ParserState::Error;
232 return Err(Http1Error::RequestLineTooLong);
233 }
234 self.request_line =
236 Some(parse_request_line(&self.buf[self.scanned..cp])?);
237 self.scanned = cp + 2;
238 self.state = ParserState::ReadingHeaders;
239 } else {
240 if self.check_bare_lf(self.scanned) {
243 self.state = ParserState::Error;
244 return Err(Http1Error::SyntaxError(
245 "bare LF in request line not allowed (RFC 7230 §3.5)".into(),
246 ));
247 }
248 if self.buf.len() - self.scanned > self.config.max_request_line_size {
250 self.state = ParserState::Error;
251 return Err(Http1Error::RequestLineTooLong);
252 }
253 break;
254 }
255 }
256 ParserState::ReadingHeaders => {
257 let mut line_start = self.scanned;
258 let mut progressed = false;
259 let mut headers_done = false;
260 'lines: loop {
261 if line_start >= self.buf.len() {
262 break 'lines;
263 }
264 let rest = &self.buf[line_start..];
265 if let Some(cp) = find_crlf(rest) {
266 let line_end = line_start + cp;
267 let line_slice = &self.buf[line_start..line_end];
269 line_start = line_end + 2;
270 progressed = true;
271 if line_slice.is_empty() {
272 self.scanned = line_start;
273 headers_done = true;
274 break 'lines;
275 }
276 if let Some(entry) = parse_header_line(line_slice, &self.config)? {
277 self.headers.push(entry);
278 }
279 if self.headers.len() > self.config.max_header_count {
280 self.state = ParserState::Error;
281 return Err(Http1Error::TooManyHeaders);
282 }
283 if self.current_header_size() > self.config.max_header_size {
284 self.state = ParserState::Error;
285 return Err(Http1Error::HeaderTooLarge);
286 }
287 } else {
288 if self.check_bare_lf(line_start) {
290 self.state = ParserState::Error;
291 return Err(Http1Error::SyntaxError(
292 "bare LF in header block not allowed (RFC 7230 §3.5)".into(),
293 ));
294 }
295 break 'lines;
296 }
297 }
298 if !progressed {
299 if self.buf.len() - self.scanned > self.config.max_header_size {
300 self.state = ParserState::Error;
301 return Err(Http1Error::HeaderTooLarge);
302 }
303 break;
304 }
305 self.scanned = line_start;
306
307 if headers_done {
308 if let Err(e) = self.smuggler.detect_err_already_lowercased(self.headers.as_slice()) {
310 self.state = ParserState::Error;
311 return Err(e);
312 }
313 if !self.has_host() {
315 self.state = ParserState::Error;
316 return Err(Http1Error::MissingHost);
317 }
318 self.check_absolute_form_host_consistency()?;
322
323 let content_length = match parse_content_length(self.headers.as_slice()) {
327 Ok(v) => v,
328 Err(e) => {
329 self.state = ParserState::Error;
330 return Err(e);
331 }
332 };
333 let te = analyze_transfer_encoding(self.headers.as_slice());
334
335 let is_http10 = self
338 .request_line
339 .as_ref()
340 .map(|l| l.version.as_ref() == "HTTP/1.0")
341 .unwrap_or(false);
342 if is_http10 && te != TransferEncodingKind::None {
343 self.state = ParserState::Error;
344 return Err(Http1Error::SmugglingDetected(
345 "Transfer-Encoding in HTTP/1.0 request".into(),
346 ));
347 }
348 if te == TransferEncodingKind::Invalid {
351 self.state = ParserState::Error;
352 return Err(Http1Error::SmugglingDetected(
353 "Transfer-Encoding without 'chunked'".into(),
354 ));
355 }
356 let chunked = te == TransferEncodingKind::Chunked;
358
359 if chunked {
360 self.body_expected = None;
364 self.partial_body.clear();
365 self.chunk_remaining = 0;
366 self.state = ParserState::ReadingChunkSize;
367 } else if let Some(cl) = content_length {
368 if cl == 0 {
369 self.body_expected = Some(0);
371 self.partial_body.clear();
372 self.state = ParserState::HeadersComplete;
373 } else {
374 self.body_expected = Some(cl);
376 self.partial_body.clear();
377 let cl_usize = match usize::try_from(cl) {
380 Ok(n) => n,
381 Err(_) => {
382 self.state = ParserState::Error;
383 return Err(Http1Error::BodyTooLarge);
384 }
385 };
386 if cl_usize > self.config.max_body_size {
387 self.state = ParserState::Error;
388 return Err(Http1Error::BodyTooLarge);
389 }
390 self.partial_body.reserve(core::cmp::min(cl_usize, 64 * 1024));
394 self.state = ParserState::ReadingBody;
395 let tail_avail = self.buf.len().saturating_sub(self.scanned);
397 if tail_avail > 0 {
398 let copy_n =
399 core::cmp::min(tail_avail, cl_usize);
400 self.partial_body
401 .extend_from_slice(&self.buf[self.scanned..self.scanned + copy_n]);
402 self.scanned += copy_n;
403 }
404 if self.partial_body.len() as u64 >= cl {
406 self.state = ParserState::HeadersComplete;
407 }
408 }
409 } else {
410 self.body_expected = None;
412 self.partial_body.clear();
413 self.state = ParserState::HeadersComplete;
414 }
415 }
416 }
417 ParserState::ReadingBody => {
418 let cl = match self.body_expected {
419 Some(c) => c,
420 None => {
421 self.state = ParserState::HeadersComplete;
423 break;
424 }
425 };
426 let remaining = cl.saturating_sub(self.partial_body.len() as u64);
429 if remaining == 0 {
430 self.state = ParserState::HeadersComplete;
431 break;
432 }
433 let tail_avail = self.buf.len().saturating_sub(self.scanned);
434 if tail_avail == 0 {
435 break;
437 }
438 let copy_n = core::cmp::min(remaining, tail_avail as u64) as usize;
439 self.partial_body
440 .extend_from_slice(&self.buf[self.scanned..self.scanned + copy_n]);
441 self.scanned += copy_n;
442 if self.partial_body.len() as u64 >= cl {
443 self.state = ParserState::HeadersComplete;
444 }
445 }
446 ParserState::ReadingChunkSize => {
451 if let Some(cp) = find_crlf_at(&self.buf, self.scanned) {
453 let chunk_size = parse_chunk_size(&self.buf[self.scanned..cp])?;
454 self.scanned = cp + 2;
455 if chunk_size == 0 {
456 self.state = ParserState::ReadingChunkTrailer;
458 } else {
459 if self
461 .partial_body
462 .len()
463 .checked_add(chunk_size)
464 .map(|sum| sum > self.config.max_body_size)
465 .unwrap_or(true)
466 {
467 self.state = ParserState::Error;
468 return Err(Http1Error::BodyTooLarge);
469 }
470 self.chunk_remaining = chunk_size;
471 self.state = ParserState::ReadingChunkData;
472 }
473 } else {
474 break;
476 }
477 }
478 ParserState::ReadingChunkData => {
479 if self.chunk_remaining > 0 {
480 let tail_avail = self.buf.len().saturating_sub(self.scanned);
482 if tail_avail == 0 {
483 break;
484 }
485 let copy_n = core::cmp::min(self.chunk_remaining, tail_avail);
486 self.partial_body
487 .extend_from_slice(&self.buf[self.scanned..self.scanned + copy_n]);
488 self.scanned += copy_n;
489 self.chunk_remaining -= copy_n;
490 }
491 if self.chunk_remaining == 0 {
492 let tail_avail = self.buf.len().saturating_sub(self.scanned);
494 if tail_avail < 2 {
495 break;
496 }
497 if self.buf[self.scanned] != b'\r'
498 || self.buf[self.scanned + 1] != b'\n'
499 {
500 self.state = ParserState::Error;
501 return Err(Http1Error::ChunkedError(
502 "missing CRLF after chunk data".into(),
503 ));
504 }
505 self.scanned += 2;
506 self.state = ParserState::ReadingChunkSize;
507 }
508 }
509 ParserState::ReadingChunkTrailer => {
510 if let Some(cp) = find_crlf_at(&self.buf, self.scanned) {
512 let line_slice = &self.buf[self.scanned..cp];
514 let trailer_entry = if line_slice.is_empty() {
515 None
516 } else {
517 parse_header_line(line_slice, &self.config)?
519 };
520 self.scanned = cp + 2;
521 if line_slice.is_empty() {
522 self.state = ParserState::HeadersComplete;
524 } else {
525 if let Some(entry) = trailer_entry {
527 self.headers.push(entry);
528 }
529 if self.headers.len() > self.config.max_header_count {
530 self.state = ParserState::Error;
531 return Err(Http1Error::TooManyHeaders);
532 }
533 if self.current_header_size() > self.config.max_header_size {
534 self.state = ParserState::Error;
535 return Err(Http1Error::HeaderTooLarge);
536 }
537 }
538 } else {
539 break;
540 }
541 }
542 ParserState::HeadersComplete | ParserState::Error => break,
543 }
544 }
545
546 let processed_after = self.scanned;
548 let processed_delta = processed_after.saturating_sub(scanned_before);
549
550 let consumed = if new_bytes == 0 {
556 0
557 } else if processed_delta >= new_bytes {
558 new_bytes
559 } else {
560 processed_delta
561 };
562
563 self.last_byte_was_cr = self.buf.last() == Some(&b'\r');
566
567 if !matches!(self.state, ParserState::HeadersComplete) && self.scanned > 0 {
569 self.buf.drain(..self.scanned);
570 self.scanned = 0;
571 }
572
573 if matches!(self.state, ParserState::HeadersComplete) {
574 let req = self.build_request()?;
575 Ok((Some(req), consumed))
576 } else {
577 Ok((None, consumed))
578 }
579 }
580
581 fn check_bare_lf(&mut self, region_start: usize) -> bool {
588 let init_prev_cr = if region_start == 0 {
589 self.last_byte_was_cr
590 } else {
591 self.buf[region_start - 1] == b'\r'
592 };
593 let (bare, last_cr) = scan_bare_lf(&self.buf[region_start..], init_prev_cr);
594 self.last_byte_was_cr = last_cr;
595 bare
596 }
597
598 fn build_request(&mut self) -> Result<HttpRequest, Http1Error> {
600 let line = self
601 .request_line
602 .take()
603 .ok_or_else(|| Http1Error::Internal("missing request line".into()))?;
604
605 self.smuggler.detect_err_already_lowercased(self.headers.as_slice())?;
606
607 if !self.has_host() {
608 return Err(Http1Error::MissingHost);
609 }
610
611 let content_length = parse_content_length(self.headers.as_slice())?;
613 let chunked =
615 analyze_transfer_encoding(self.headers.as_slice()) == TransferEncodingKind::Chunked;
616 let keep_alive = if line.version.as_ref() == "HTTP/1.0" {
622 connection_has_token(self.headers.as_slice(), "keep-alive")
623 } else {
624 !connection_has_token(self.headers.as_slice(), "close")
625 };
626
627 let body: Vec<u8> = std::mem::take(&mut self.partial_body);
629
630 if let Some(cl) = content_length
632 && !chunked
633 && (body.len() as u64) != cl
634 {
635 self.state = ParserState::Error;
636 return Err(Http1Error::SyntaxError(format!(
637 "request body length mismatch: declared Content-Length={cl}, actual bytes={}",
638 body.len()
639 )));
640 }
641
642 let req = HttpRequest {
643 line,
644 headers: std::mem::take(&mut self.headers),
645 body,
646 content_length,
647 chunked,
648 keep_alive,
649 };
650
651 self.state = ParserState::WaitingRequest;
654 if self.scanned > 0 && self.scanned <= self.buf.len() {
655 self.buf.drain(..self.scanned);
656 }
657 self.scanned = 0;
658 self.body_expected = None;
659 self.chunk_remaining = 0;
660 self.last_byte_was_cr = false;
662 self.first_byte_ms = None;
664 self.last_activity_ms = None;
665 Ok(req)
668 }
669
670}
671
672fn parse_content_length(headers: &[HeaderEntry]) -> Result<Option<u64>, Http1Error> {
681 match headers.iter().find(|(k, _)| k.as_ref() == "content-length") {
682 None => Ok(None),
683 Some((_, v)) => {
684 let cl = v.parse::<u64>().map_err(|_| {
685 Http1Error::SmugglingDetected(format!(
686 "Content-Length value not parseable as u64: '{}'",
687 v.as_ref()
688 ))
689 })?;
690 Ok(Some(cl))
691 }
692 }
693}
694
695fn connection_has_token(headers: &[HeaderEntry], token: &str) -> bool {
700 headers
701 .iter()
702 .filter(|(k, _)| k.as_ref() == "connection")
703 .flat_map(|(_, v)| v.split(','))
704 .map(str::trim)
705 .any(|t| t.eq_ignore_ascii_case(token))
706}
707
708fn absolute_form_authority(target: &str) -> Option<&str> {
714 let after_scheme = if target.len() >= 7 && target[..7].eq_ignore_ascii_case("http://") {
715 &target[7..]
716 } else if target.len() >= 8 && target[..8].eq_ignore_ascii_case("https://") {
717 &target[8..]
718 } else {
719 return None;
720 };
721 let end = after_scheme
722 .find(['/', '?', '#'])
723 .unwrap_or(after_scheme.len());
724 Some(&after_scheme[..end])
725}
726
727fn parse_request_line(line: &[u8]) -> Result<HttpRequestLine, Http1Error> {
731 let s = std::str::from_utf8(line)
732 .map_err(|_| Http1Error::SyntaxError("request line not UTF-8".into()))?;
733 let s = s.trim_end_matches('\r');
734
735 let mut parts = s.splitn(3, ' ');
736 let method = parts
737 .next()
738 .ok_or_else(|| Http1Error::SyntaxError("missing method".into()))?;
739 let target = parts
740 .next()
741 .ok_or_else(|| Http1Error::SyntaxError("missing target".into()))?;
742 let version = parts
743 .next()
744 .ok_or_else(|| Http1Error::SyntaxError("missing version".into()))?;
745
746 if !is_valid_method(method) {
747 return Err(Http1Error::UnsupportedMethod(method.to_string()));
748 }
749 if version != "HTTP/1.0" && version != "HTTP/1.1" {
750 return Err(Http1Error::UnsupportedVersion(version.to_string()));
751 }
752 if target.chars().any(|c| (c as u32) < 0x20) {
753 return Err(Http1Error::SyntaxError(
754 "target contains control chars".into(),
755 ));
756 }
757
758 Ok(HttpRequestLine {
759 method: method.into(),
760 target: target.into(),
761 version: version.into(),
762 })
763}
764
765fn parse_header_line(line: &[u8], config: &Http1Config) -> Result<Option<HeaderEntry>, Http1Error> {
771 if line.is_empty() {
772 return Ok(None);
773 }
774 if line.iter().any(|&b| b == b'\r' || b == b'\n') {
775 return Err(Http1Error::SyntaxError(
776 "header line contains CR/LF".into(),
777 ));
778 }
779 if line.first() == Some(&b' ') || line.first() == Some(&b'\t') {
781 return Err(Http1Error::SyntaxError(
782 "header line folding is forbidden".into(),
783 ));
784 }
785
786 let colon = line
787 .iter()
788 .position(|&b| b == b':')
789 .ok_or_else(|| Http1Error::SyntaxError("missing colon in header".into()))?;
790
791 let name = std::str::from_utf8(&line[..colon])
792 .map_err(|_| Http1Error::SyntaxError("header name not UTF-8".into()))?;
793 let value = std::str::from_utf8(&line[colon + 1..])
795 .map_err(|_| Http1Error::SyntaxError("header value not UTF-8".into()))?;
796
797 if !is_valid_header_name(name) {
798 return Err(Http1Error::SyntaxError(format!(
799 "invalid header name: {name}"
800 )));
801 }
802 if name.len() > config.max_header_name_len {
803 return Err(Http1Error::HeaderTooLarge);
804 }
805
806 let val_trim = value.trim_matches(|c| c == ' ' || c == '\t');
809 if val_trim.len() > config.max_header_value_len {
810 return Err(Http1Error::HeaderTooLarge);
811 }
812 if val_trim.contains('\r') || val_trim.contains('\n') {
813 return Err(Http1Error::SyntaxError(
814 "header value contains CR/LF".into(),
815 ));
816 }
817
818 let name = name.to_ascii_lowercase();
819 Ok(Some((name.into_boxed_str(), val_trim.into())))
820}
821
822impl Http1Parser {
823 fn current_header_size(&self) -> usize {
825 self.headers
826 .iter()
827 .map(|(k, v)| k.len() + v.len() + 2)
828 .sum()
829 }
830
831 #[inline]
833 fn has_host(&self) -> bool {
834 self.headers.iter().any(|(k, _)| k.as_ref() == "host")
835 }
836
837 #[inline]
839 fn host_value(&self) -> Option<&str> {
840 self.headers
841 .iter()
842 .find(|(k, _)| k.as_ref() == "host")
843 .map(|(_, v)| v.as_ref())
844 }
845
846 fn check_absolute_form_host_consistency(&mut self) -> Result<(), Http1Error> {
852 let target = match self.request_line.as_ref() {
853 Some(line) => line.target.as_ref(),
854 None => return Ok(()),
855 };
856 let authority = match absolute_form_authority(target) {
857 Some(a) => a,
858 None => return Ok(()),
859 };
860 let host = self.host_value().unwrap_or("");
862 if authority.is_empty() || !authority.eq_ignore_ascii_case(host) {
863 self.state = ParserState::Error;
864 return Err(Http1Error::ProtocolInconsistency(
865 "absolute-form authority does not match Host header".into(),
866 ));
867 }
868 Ok(())
869 }
870}
871
872#[inline]
874fn find_crlf(buf: &[u8]) -> Option<usize> {
875 find_crlf_at(buf, 0)
876}
877
878pub(crate) fn parse_chunk_size(line: &[u8]) -> Result<usize, Http1Error> {
894 let line_str = std::str::from_utf8(line)
895 .map_err(|_| Http1Error::ChunkedError("chunk size line not UTF-8".into()))?;
896 let size_part = line_str.split(';').next().unwrap_or("");
898 let size_part = size_part.trim();
899 if size_part.is_empty() {
900 return Err(Http1Error::ChunkedError("empty chunk size".into()));
901 }
902 if !size_part.chars().all(|c| c.is_ascii_hexdigit()) {
903 return Err(Http1Error::ChunkedError(format!(
904 "invalid chunk size: non-hex digit in '{size_part}'"
905 )));
906 }
907 usize::from_str_radix(size_part, 16)
908 .map_err(|_| Http1Error::ChunkedError(format!("chunk size overflow: {size_part}")))
909}
910
911#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
913enum TransferEncodingKind {
914 None,
916 Chunked,
918 Invalid,
920}
921
922fn analyze_transfer_encoding(headers: &[HeaderEntry]) -> TransferEncodingKind {
929 let mut saw_te = false;
930 let mut saw_chunked = false;
931 for (k, v) in headers {
932 if k.as_ref() != "transfer-encoding" {
933 continue;
934 }
935 saw_te = true;
936 for tok in v.split(',') {
937 let tok = tok.trim();
938 if tok.is_empty() {
939 continue;
940 }
941 if tok.eq_ignore_ascii_case("chunked") {
942 saw_chunked = true;
943 }
944 }
945 }
946 if !saw_te {
947 TransferEncodingKind::None
948 } else if saw_chunked {
949 TransferEncodingKind::Chunked
950 } else {
951 TransferEncodingKind::Invalid
952 }
953}
954
955#[inline]
961fn is_valid_method(m: &str) -> bool {
962 m.parse::<zenith_api::Method>().is_ok()
963}
964
965#[inline]
969fn is_valid_header_name(s: &str) -> bool {
970 zenith_api::normalize::is_valid_header_name(s)
971}
972
973#[cfg(test)]
974mod tests {
975 use super::*;
976
977 fn make_config() -> Http1Config {
978 Http1Config::new()
979 }
980
981 #[test]
982 fn test_parse_get_request() {
983 let mut p = Http1Parser::new(make_config());
984 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
985 let (req, _) = p.feed(input).unwrap();
986 let req = req.unwrap();
987 assert_eq!(req.line.method.as_ref(), "GET");
988 assert_eq!(req.line.target.as_ref(), "/");
989 assert_eq!(req.line.version.as_ref(), "HTTP/1.1");
990 assert_eq!(req.get_header("host"), Some("example.com"));
991 assert!(req.keep_alive);
992 }
993
994 #[test]
995 fn test_parse_post_with_content_length() {
996 let mut p = Http1Parser::new(make_config());
997 let input =
999 b"POST /api HTTP/1.1\r\nHost: example.com\r\nContent-Length: 13\r\n\r\nHello, World!";
1000 let (req, _) = p.feed(input).unwrap();
1001 let req = req.unwrap();
1002 assert_eq!(req.content_length, Some(13));
1003 assert_eq!(req.body, b"Hello, World!");
1004 assert!(!req.chunked);
1005 }
1006
1007 #[test]
1008 fn test_parse_chunked_request() {
1009 let mut p = Http1Parser::new(make_config());
1010 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";
1013 let (req, _) = p.feed(input).unwrap();
1014 let req = req.unwrap();
1015 assert!(req.chunked);
1016 assert_eq!(req.content_length, None);
1017 assert_eq!(req.body, b"Hello World");
1018 }
1019
1020 #[test]
1021 fn test_parse_chunked_empty_body() {
1022 let mut p = Http1Parser::new(make_config());
1023 let input = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
1025 let (req, _) = p.feed(input).unwrap();
1026 let req = req.unwrap();
1027 assert!(req.chunked);
1028 assert!(req.body.is_empty());
1029 }
1030
1031 #[test]
1032 fn test_parse_chunked_with_chunk_ext() {
1033 let mut p = Http1Parser::new(make_config());
1034 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";
1036 let (req, _) = p.feed(input).unwrap();
1037 let req = req.unwrap();
1038 assert_eq!(req.body, b"Hello");
1039 }
1040
1041 #[test]
1042 fn test_parse_chunked_with_trailer() {
1043 let mut p = Http1Parser::new(make_config());
1044 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";
1046 let (req, _) = p.feed(input).unwrap();
1047 let req = req.unwrap();
1048 assert_eq!(req.body, b"Hello");
1049 assert_eq!(req.get_header("x-checksum"), Some("abc123"));
1050 }
1051
1052 #[test]
1053 fn test_parse_chunked_multiple_chunks() {
1054 let mut p = Http1Parser::new(make_config());
1055 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";
1057 let (req, _) = p.feed(input).unwrap();
1058 let req = req.unwrap();
1059 assert_eq!(req.body, b"abcdefghi");
1060 }
1061
1062 #[test]
1063 fn test_parse_chunked_incremental_byte_by_byte() {
1064 let mut p = Http1Parser::new(make_config());
1066 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";
1067 let mut req_opt: Option<HttpRequest> = None;
1068 for i in 0..input.len() {
1069 let (r, _) = p.feed(&input[i..i + 1]).unwrap();
1070 if r.is_some() {
1071 req_opt = r;
1072 }
1073 }
1074 let req = req_opt.expect("should parse complete request after byte-by-byte feed");
1075 assert!(req.chunked);
1076 assert_eq!(req.body, b"Hello");
1077 }
1078
1079 #[test]
1080 fn test_parse_chunked_incremental_split_at_chunks() {
1081 let mut p = Http1Parser::new(make_config());
1083 let part1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHel";
1084 let part2 = b"lo\r\n0\r\n\r\n";
1085
1086 let (r1, _) = p.feed(part1).unwrap();
1087 assert!(r1.is_none(), "first part should not complete");
1088 let (r2, _) = p.feed(part2).unwrap();
1089 let req = r2.expect("second part should complete");
1090 assert_eq!(req.body, b"Hello");
1091 }
1092
1093 #[test]
1094 fn test_parse_chunked_invalid_hex_size() {
1095 let mut p = Http1Parser::new(make_config());
1096 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";
1097 let r = p.feed(input);
1098 assert!(matches!(r, Err(Http1Error::ChunkedError(_))));
1099 }
1100
1101 #[test]
1102 fn test_parse_chunked_empty_size_line() {
1103 let mut p = Http1Parser::new(make_config());
1104 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";
1106 let r = p.feed(input);
1107 assert!(matches!(r, Err(Http1Error::ChunkedError(_))));
1108 }
1109
1110 #[test]
1111 fn test_parse_chunked_missing_crlf_after_data() {
1112 let mut p = Http1Parser::new(make_config());
1113 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";
1115 let r = p.feed(input);
1116 assert!(matches!(r, Err(Http1Error::ChunkedError(_))));
1117 }
1118
1119 #[test]
1120 fn test_parse_chunked_body_too_large() {
1121 let config = Http1Config::new().with_max_body_size(10);
1122 let mut p = Http1Parser::new(config);
1123 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";
1125 let r = p.feed(input);
1126 assert!(matches!(r, Err(Http1Error::BodyTooLarge)));
1127 }
1128
1129 #[test]
1130 fn test_parse_chunked_large_hex_size() {
1131 let mut p = Http1Parser::new(make_config());
1132 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";
1134 let (req, _) = p.feed(input).unwrap();
1135 let req = req.unwrap();
1136 assert_eq!(req.body, b"0123456789");
1137 }
1138
1139 #[test]
1140 fn test_parse_chunked_partial_last_chunk_crlf() {
1141 let mut p = Http1Parser::new(make_config());
1143 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";
1144 let part2 = b"\r\n";
1145
1146 let (r1, _) = p.feed(part1).unwrap();
1147 assert!(r1.is_none(), "should not complete before trailer CRLF");
1148 let (r2, _) = p.feed(part2).unwrap();
1149 assert!(r2.is_some(), "should complete after trailer CRLF");
1150 }
1151
1152 #[test]
1153 fn test_parse_chunked_split_at_chunk_data_crlf() {
1154 let mut p = Http1Parser::new(make_config());
1156 let part1 = b"POST /upload HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello";
1157 let part2 = b"\r\n0\r\n\r\n";
1158
1159 let (r1, _) = p.feed(part1).unwrap();
1160 assert!(r1.is_none());
1161 let (r2, _) = p.feed(part2).unwrap();
1162 let req = r2.unwrap();
1163 assert_eq!(req.body, b"Hello");
1164 }
1165
1166 #[test]
1167 fn test_bare_lf_rejected_in_request_line() {
1168 let mut p = Http1Parser::new(Http1Config::new());
1170 let result = p.feed(b"GET /status HTTP/1.1\nHost: example.com\n\n");
1171 assert!(matches!(result, Err(Http1Error::SyntaxError(_))), "bare LF 必须拒绝: {:?}", result);
1172 }
1173
1174 #[test]
1175 fn test_bare_lf_rejected_in_headers() {
1176 let mut p = Http1Parser::new(Http1Config::new());
1177 let result = p.feed(b"GET /status HTTP/1.1\r\nHost: example.com\nX-Other: yes\r\n\r\n");
1178 assert!(matches!(result, Err(Http1Error::SyntaxError(_))), "header 区块裸 LF 必须拒绝: {:?}", result);
1179 }
1180
1181 #[test]
1182 fn test_crlf_lf_only_edge_accept() {
1183 let mut p = Http1Parser::new(Http1Config::new());
1185 let result = p.feed(b"GET /status HTTP/1.1\r\nHost: example.com\r\n\r\n");
1186 assert!(result.is_ok(), "合法 CRLF 请求不得被拒绝: {:?}", result);
1187 }
1188
1189 #[test]
1192 fn test_crlf_split_across_feeds_accepted() {
1193 let mut p = Http1Parser::new(Http1Config::new());
1196 let r1 = p.feed(b"GET /status HTTP/1.1\r");
1198 assert!(r1.is_ok(), "以 CR 结尾的分段不得报错: {:?}", r1);
1199 let (req, _) = p
1201 .feed(b"\nHost: example.com\r\n\r\n")
1202 .expect("补全 CRLF 后应解析成功");
1203 assert!(req.is_some(), "跨 feed 拆分的 CRLF 必须被接受");
1204 }
1205
1206 #[test]
1207 fn test_bare_lf_split_across_feeds_rejected() {
1208 let mut p = Http1Parser::new(Http1Config::new());
1211 let r1 = p.feed(b"GET /status HTTP/1.1");
1212 assert!(r1.is_ok());
1213 let r2 = p.feed(b"\n");
1214 assert!(
1215 matches!(r2, Err(Http1Error::SyntaxError(_))),
1216 "跨 feed 裸 LF 必须拒绝,实际 {r2:?}"
1217 );
1218 }
1219
1220 #[test]
1221 fn test_bare_lf_split_across_feeds_rejected_full() {
1222 let mut p = Http1Parser::new(Http1Config::new());
1225 let r1 = p.feed(b"GET /status HTTP/1.1");
1226 assert!(r1.is_ok());
1227 let r2 = p.feed(b"\nHost: x\r\n\r\n");
1228 assert!(
1229 r2.is_err(),
1230 "请求行内跨 feed 裸 LF 必须拒绝,实际 {r2:?}"
1231 );
1232 }
1233
1234 #[test]
1235 fn test_crlf_split_across_feeds_in_headers_accepted() {
1236 let mut p = Http1Parser::new(Http1Config::new());
1238 let r1 = p.feed(b"GET / HTTP/1.1\r\nHost: example.com\r");
1239 assert!(r1.is_ok());
1240 let (req, _) = p
1241 .feed(b"\nX-Test: v\r\n\r\n")
1242 .expect("头部 CRLF 跨 feed 拆分应解析成功");
1243 assert!(req.is_some());
1244 }
1245
1246 #[test]
1247 fn test_missing_host() {
1248 let mut p = Http1Parser::new(make_config());
1249 let input = b"GET / HTTP/1.1\r\n\r\n";
1250 let r = p.feed(input);
1251 assert!(matches!(r, Err(Http1Error::MissingHost)));
1252 }
1253
1254 #[test]
1255 fn test_invalid_method() {
1256 let mut p = Http1Parser::new(make_config());
1257 let input = b"FOOBAR / HTTP/1.1\r\nHost: x\r\n\r\n";
1258 let r = p.feed(input);
1259 assert!(matches!(r, Err(Http1Error::UnsupportedMethod(_))));
1260 }
1261
1262 #[test]
1263 fn test_invalid_version() {
1264 let mut p = Http1Parser::new(make_config());
1265 let input = b"GET / HTTP/2.0\r\nHost: x\r\n\r\n";
1266 let r = p.feed(input);
1267 assert!(matches!(r, Err(Http1Error::UnsupportedVersion(_))));
1268 }
1269
1270 #[test]
1271 fn test_cl_te_smuggling_rejected() {
1272 let mut p = Http1Parser::new(make_config());
1273 let input = b"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n";
1274 let r = p.feed(input);
1275 assert!(matches!(r, Err(Http1Error::SmugglingDetected(_))));
1276 }
1277
1278 #[test]
1279 fn test_incremental_parsing() {
1280 let mut p = Http1Parser::new(make_config());
1281 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1282 let (req1, consumed1) = p.feed(&input[..10]).unwrap();
1283 assert!(req1.is_none());
1284 assert_eq!(consumed1, 0, "partial input should not be fully consumed");
1285 let (req2, consumed2) = p.feed(&input[10..]).unwrap();
1286 assert!(req2.is_some(), "should parse full request on second call");
1287 assert_eq!(consumed2, input.len() - 10);
1288 }
1289
1290 #[test]
1291 fn test_reset() {
1292 let mut p = Http1Parser::new(make_config());
1293 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1294 p.feed(input).unwrap();
1295 p.reset();
1296 assert_eq!(p.state(), ParserState::WaitingRequest);
1297 }
1298
1299 #[test]
1300 fn test_invalid_header_name_rejected() {
1301 let mut p = Http1Parser::new(make_config());
1302 let input = b"GET / HTTP/1.1\r\nHost: x\r\nBad\x00: value\r\n\r\n";
1303 let r = p.feed(input);
1304 assert!(r.is_err());
1305 }
1306
1307 #[test]
1308 fn test_smuggling_detected_by_parser() {
1309 let mut p = Http1Parser::new(make_config());
1310 let input = b"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 0\r\nTransfer-Encoding: chunked\r\n\r\n";
1311 let r = p.feed(input);
1312 assert!(r.is_err(), "expected CL.TE smuggling error, got {r:?}");
1313 }
1314
1315 #[test]
1316 fn test_double_content_length_rejected() {
1317 let mut p = Http1Parser::new(make_config());
1318 let input = b"GET / HTTP/1.1\r\nHost: x\r\nContent-Length: 10\r\nContent-Length: 20\r\n\r\n";
1319 let r = p.feed(input);
1320 assert!(r.is_err(), "expected double CL error, got {r:?}");
1321 }
1322
1323 #[test]
1324 fn test_parser_state_variants() {
1325 let states = [
1326 ParserState::WaitingRequest,
1327 ParserState::ReadingRequest,
1328 ParserState::ReadingHeaders,
1329 ParserState::ReadingBody,
1330 ParserState::ReadingChunkSize,
1331 ParserState::ReadingChunkData,
1332 ParserState::ReadingChunkTrailer,
1333 ParserState::HeadersComplete,
1334 ParserState::Error,
1335 ];
1336 for (i, s) in states.iter().enumerate() {
1337 assert_eq!(*s, states[i]);
1338 }
1339 assert_ne!(ParserState::WaitingRequest, ParserState::Error);
1340 }
1341
1342 #[test]
1343 fn test_parse_http10_request() {
1344 let mut p = Http1Parser::new(make_config());
1345 let input = b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n";
1346 let (req, _) = p.feed(input).unwrap();
1347 let req = req.unwrap();
1348 assert_eq!(req.line.version.as_ref(), "HTTP/1.0");
1349 }
1350
1351 #[test]
1352 fn test_header_name_lowercase_normalization() {
1353 let mut p = Http1Parser::new(make_config());
1354 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";
1355 let (req, _) = p.feed(input).unwrap();
1356 let req = req.unwrap();
1357 assert_eq!(req.get_header("host"), Some("example.com"));
1358 assert_eq!(req.get_header("content-type"), Some("text/plain"));
1359 assert_eq!(req.get_header("x-custom-header"), Some("value"));
1360 }
1361
1362 #[test]
1363 fn test_header_value_leading_whitespace_trimmed() {
1364 let mut p = Http1Parser::new(make_config());
1365 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Test:\tvalue\r\n\r\n";
1366 let (req, _) = p.feed(input).unwrap();
1367 let req = req.unwrap();
1368 assert_eq!(req.get_header("host"), Some("example.com"));
1369 assert_eq!(req.get_header("x-test"), Some("value"));
1370 }
1371
1372 #[test]
1373 fn test_missing_method_in_request_line() {
1374 let mut p = Http1Parser::new(make_config());
1375 let input = b" / HTTP/1.1\r\nHost: x\r\n\r\n";
1376 let r = p.feed(input);
1377 assert!(r.is_err());
1378 }
1379
1380 #[test]
1381 fn test_missing_version_in_request_line() {
1382 let mut p = Http1Parser::new(make_config());
1383 let input = b"GET /\r\nHost: x\r\n\r\n";
1384 let r = p.feed(input);
1385 assert!(r.is_err());
1386 }
1387
1388 #[test]
1389 fn test_only_method_in_request_line() {
1390 let mut p = Http1Parser::new(make_config());
1391 let input = b"GET\r\nHost: x\r\n\r\n";
1392 let r = p.feed(input);
1393 assert!(r.is_err());
1394 }
1395
1396 #[test]
1397 fn test_header_line_folding_rejected() {
1398 let mut p = Http1Parser::new(make_config());
1399 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n continued\r\n\r\n";
1400 let r = p.feed(input);
1401 assert!(r.is_err());
1402 }
1403
1404 #[test]
1405 fn test_header_line_folding_tab_rejected() {
1406 let mut p = Http1Parser::new(make_config());
1407 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\tcontinued\r\n\r\n";
1408 let r = p.feed(input);
1409 assert!(r.is_err());
1410 }
1411
1412 #[test]
1413 fn test_header_missing_colon() {
1414 let mut p = Http1Parser::new(make_config());
1415 let input = b"GET / HTTP/1.1\r\nHost example.com\r\n\r\n";
1416 let r = p.feed(input);
1417 assert!(r.is_err());
1418 }
1419
1420 #[test]
1421 fn test_empty_input() {
1422 let mut p = Http1Parser::new(make_config());
1423 let (req, consumed) = p.feed(b"").unwrap();
1424 assert!(req.is_none());
1425 assert_eq!(consumed, 0);
1426 }
1427
1428 #[test]
1429 fn test_multiple_headers_same_name() {
1430 let mut p = Http1Parser::new(make_config());
1431 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Test: first\r\nX-Test: second\r\n\r\n";
1432 let (req, _) = p.feed(input).unwrap();
1433 let req = req.unwrap();
1434 let test_headers: Vec<_> = req.headers.iter().filter(|(k, _)| k.as_ref() == "x-test").collect();
1435 assert_eq!(test_headers.len(), 2);
1436 }
1437
1438 #[test]
1439 fn test_request_line_with_query_string() {
1440 let mut p = Http1Parser::new(make_config());
1441 let input = b"GET /path?query=value&foo=bar HTTP/1.1\r\nHost: example.com\r\n\r\n";
1442 let (req, _) = p.feed(input).unwrap();
1443 let req = req.unwrap();
1444 assert_eq!(req.line.target.as_ref(), "/path?query=value&foo=bar");
1445 }
1446
1447 #[test]
1448 fn test_all_valid_methods() {
1449 let methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "CONNECT", "TRACE"];
1450 for method in methods.iter() {
1451 let mut p = Http1Parser::new(make_config());
1452 let input = format!("{} / HTTP/1.1\r\nHost: example.com\r\n\r\n", method);
1453 let (req, _) = p.feed(input.as_bytes()).unwrap();
1454 let req = req.unwrap();
1455 assert_eq!(req.line.method.as_ref(), *method);
1456 }
1457 }
1458
1459 #[test]
1460 fn test_lowercase_method_rejected() {
1461 let mut p = Http1Parser::new(make_config());
1463 let input = b"get / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1464 let r = p.feed(input);
1465 assert!(
1466 matches!(r, Err(Http1Error::UnsupportedMethod(_))),
1467 "小写方法必须拒绝,实际 {r:?}"
1468 );
1469 let mut p = Http1Parser::new(make_config());
1471 let input = b"Get / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1472 assert!(matches!(p.feed(input), Err(Http1Error::UnsupportedMethod(_))));
1473 }
1474
1475 #[test]
1476 fn test_http10_default_connection_close() {
1477 let mut p = Http1Parser::new(make_config());
1479 let input = b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n";
1480 let (req, _) = p.feed(input).unwrap();
1481 let req = req.unwrap();
1482 assert!(!req.keep_alive, "HTTP/1.0 无 Connection 头应默认关闭");
1483 }
1484
1485 #[test]
1486 fn test_http10_explicit_keep_alive() {
1487 let mut p = Http1Parser::new(make_config());
1489 let input = b"GET / HTTP/1.0\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n";
1490 let (req, _) = p.feed(input).unwrap();
1491 let req = req.unwrap();
1492 assert!(req.keep_alive);
1493 }
1494
1495 #[test]
1496 fn test_http11_default_keep_alive() {
1497 let mut p = Http1Parser::new(make_config());
1499 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
1500 let (req, _) = p.feed(input).unwrap();
1501 assert!(req.unwrap().keep_alive);
1502 let mut p = Http1Parser::new(make_config());
1503 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
1504 let (req, _) = p.feed(input).unwrap();
1505 assert!(!req.unwrap().keep_alive);
1506 }
1507
1508 #[test]
1509 fn test_http10_transfer_encoding_rejected() {
1510 let mut p = Http1Parser::new(make_config());
1512 let input = b"POST / HTTP/1.0\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
1513 let r = p.feed(input);
1514 assert!(
1515 matches!(r, Err(Http1Error::SmugglingDetected(_))),
1516 "HTTP/1.0 + TE 必须拒绝,实际 {r:?}"
1517 );
1518 }
1519
1520 #[test]
1521 fn test_transfer_encoding_without_chunked_rejected() {
1522 let mut p = Http1Parser::new(make_config());
1524 let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: gzip\r\n\r\n";
1525 let r = p.feed(input);
1526 assert!(
1527 matches!(r, Err(Http1Error::SmugglingDetected(_))),
1528 "TE 无 chunked 必须拒绝,实际 {r:?}"
1529 );
1530 }
1531
1532 #[test]
1533 fn test_transfer_encoding_identity_rejected() {
1534 let mut p = Http1Parser::new(make_config());
1536 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: identity\r\n\r\n";
1537 let r = p.feed(input);
1538 assert!(
1539 matches!(r, Err(Http1Error::SmugglingDetected(_))),
1540 "TE: identity 必须走私拒绝,实际 {r:?}"
1541 );
1542 }
1543
1544 #[test]
1545 fn test_transfer_encoding_gzip_chunked_parsed() {
1546 let mut p = Http1Parser::new(make_config());
1548 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";
1549 let (req, _) = p.feed(input).unwrap();
1550 let req = req.unwrap();
1551 assert!(req.chunked);
1552 assert_eq!(req.body, b"Hello");
1553 }
1554
1555 #[test]
1556 fn test_header_name_too_long() {
1557 let config = Http1Config::new().with_max_header_size(1024);
1558 let mut p = Http1Parser::new(config);
1559 let long_name = "X-".to_string() + &"A".repeat(100);
1560 let input = format!("GET / HTTP/1.1\r\nHost: x\r\n{}: value\r\n\r\n", long_name);
1561 let r = p.feed(input.as_bytes());
1562 let _ = r;
1563 }
1564
1565 #[test]
1566 fn test_control_char_in_target_rejected() {
1567 let mut p = Http1Parser::new(make_config());
1568 let input = b"GET /path\x01 HTTP/1.1\r\nHost: x\r\n\r\n";
1569 let r = p.feed(input);
1570 assert!(r.is_err());
1571 }
1572
1573 #[test]
1574 fn test_parser_clone() {
1575 let p = Http1Parser::new(make_config());
1576 let p2 = p.clone();
1577 assert_eq!(p.state(), p2.state());
1578 }
1579
1580 #[test]
1581 fn test_parser_debug_format() {
1582 let p = Http1Parser::new(make_config());
1583 let s = format!("{:?}", p);
1584 assert!(!s.is_empty());
1585 }
1586
1587 #[test]
1588 fn test_connection_close_header() {
1589 let mut p = Http1Parser::new(make_config());
1590 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n";
1591 let (req, _) = p.feed(input).unwrap();
1592 let req = req.unwrap();
1593 assert!(!req.keep_alive);
1594 }
1595
1596 #[test]
1599 fn test_content_length_overflow_u64_rejected() {
1600 let mut p = Http1Parser::new(make_config());
1602 let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 99999999999999999999999\r\n\r\n";
1603 let r = p.feed(input);
1604 assert!(
1605 matches!(r, Err(Http1Error::SmugglingDetected(_))),
1606 "超 u64 的 CL 必须 fail-closed 拒绝,实际 {r:?}"
1607 );
1608 }
1609
1610 #[test]
1611 fn test_content_length_valid_still_accepted() {
1612 let mut p = Http1Parser::new(make_config());
1614 let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";
1615 let (req, _) = p.feed(input).unwrap();
1616 let req = req.unwrap();
1617 assert_eq!(req.content_length, Some(5));
1618 assert_eq!(req.body, b"hello");
1619 }
1620
1621 #[test]
1624 fn test_complete_request_line_too_long_rejected() {
1625 let config = Http1Config::new();
1627 let line_limit = config.max_request_line_size;
1628 let mut p = Http1Parser::new(config);
1629 let long_path = "a".repeat(line_limit + 1);
1630 let input = format!("GET /{long_path} HTTP/1.1\r\nHost: x\r\n\r\n");
1631 let r = p.feed(input.as_bytes());
1632 assert!(
1633 matches!(r, Err(Http1Error::RequestLineTooLong)),
1634 "完整超长请求行必须拒绝,实际 {r:?}"
1635 );
1636 assert_eq!(p.state(), ParserState::Error);
1637 }
1638
1639 #[test]
1640 fn test_incomplete_request_line_too_long_rejected() {
1641 let config = Http1Config::new();
1643 let line_limit = config.max_request_line_size;
1644 let mut p = Http1Parser::new(config);
1645 let junk = "a".repeat(line_limit + 1);
1646 let r = p.feed(junk.as_bytes());
1647 assert!(matches!(r, Err(Http1Error::RequestLineTooLong)), "实际 {r:?}");
1648 }
1649
1650 #[test]
1651 fn test_request_line_at_limit_accepted() {
1652 let config = Http1Config::new();
1654 let line_limit = config.max_request_line_size;
1655 let pad = line_limit - "GET /".len() - " HTTP/1.1".len();
1657 let mut p = Http1Parser::new(config);
1658 let input = format!("GET /{} HTTP/1.1\r\nHost: x\r\n\r\n", "a".repeat(pad));
1659 let (req, _) = p.feed(input.as_bytes()).unwrap();
1660 assert!(req.is_some(), "恰在上限内的请求行必须放行");
1661 }
1662
1663 #[test]
1666 fn test_absolute_form_matching_host_accepted() {
1667 let mut p = Http1Parser::new(make_config());
1669 let input = b"GET http://example.com/path?q=1 HTTP/1.1\r\nHost: EXAMPLE.com\r\n\r\n";
1670 let (req, _) = p.feed(input).unwrap();
1671 let req = req.unwrap();
1672 assert_eq!(req.line.target.as_ref(), "http://example.com/path?q=1");
1673 }
1674
1675 #[test]
1676 fn test_absolute_form_with_port_matching_host_accepted() {
1677 let mut p = Http1Parser::new(make_config());
1679 let input = b"GET https://example.com:8443/x HTTP/1.1\r\nHost: example.com:8443\r\n\r\n";
1680 let (req, _) = p.feed(input).unwrap();
1681 assert!(req.is_some());
1682 }
1683
1684 #[test]
1685 fn test_absolute_form_host_mismatch_rejected() {
1686 let mut p = Http1Parser::new(make_config());
1688 let input = b"GET http://evil.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1689 let r = p.feed(input);
1690 assert!(
1691 matches!(r, Err(Http1Error::ProtocolInconsistency(_))),
1692 "authority 与 Host 不一致必须拒绝,实际 {r:?}"
1693 );
1694 }
1695
1696 #[test]
1697 fn test_absolute_form_port_mismatch_rejected() {
1698 let mut p = Http1Parser::new(make_config());
1700 let input = b"GET http://example.com:8080/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1701 let r = p.feed(input);
1702 assert!(matches!(r, Err(Http1Error::ProtocolInconsistency(_))), "实际 {r:?}");
1703 }
1704
1705 #[test]
1706 fn test_origin_form_unaffected_by_host_check() {
1707 let mut p = Http1Parser::new(make_config());
1709 let input = b"GET /path HTTP/1.1\r\nHost: example.com\r\n\r\n";
1710 let (req, _) = p.feed(input).unwrap();
1711 assert!(req.is_some());
1712 }
1713
1714 #[test]
1717 fn test_connection_close_among_tokens() {
1718 let mut p = Http1Parser::new(make_config());
1720 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close, upgrade\r\n\r\n";
1721 let (req, _) = p.feed(input).unwrap();
1722 let req = req.unwrap();
1723 assert!(!req.keep_alive, "token 列表中的 close 必须生效");
1724 }
1725
1726 #[test]
1727 fn test_connection_keep_alive_among_tokens_http10() {
1728 let mut p = Http1Parser::new(make_config());
1730 let input =
1731 b"GET / HTTP/1.0\r\nHost: example.com\r\nConnection: keep-alive, timeout=5\r\n\r\n";
1732 let (req, _) = p.feed(input).unwrap();
1733 let req = req.unwrap();
1734 assert!(req.keep_alive, "token 列表中的 keep-alive 必须生效");
1735 }
1736
1737 #[test]
1738 fn test_connection_prefixed_token_not_close() {
1739 let mut p = Http1Parser::new(make_config());
1741 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nConnection: x-close\r\n\r\n";
1742 let (req, _) = p.feed(input).unwrap();
1743 let req = req.unwrap();
1744 assert!(req.keep_alive);
1745 }
1746
1747 #[test]
1750 fn test_header_value_trailing_ows_trimmed() {
1751 let mut p = Http1Parser::new(make_config());
1753 let input = b"GET / HTTP/1.1\r\nHost: example.com\r\nX-Test: value \t\r\n\r\n";
1754 let (req, _) = p.feed(input).unwrap();
1755 let req = req.unwrap();
1756 assert_eq!(req.get_header("x-test"), Some("value"));
1757 }
1758
1759 #[test]
1762 fn test_idle_timeout_during_body_read() {
1763 let config = Http1Config::new().with_idle_timeout_ms(30_000);
1765 let mut p = Http1Parser::new(config);
1766 p.note_activity(1_000);
1767 let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\n\r\n";
1769 let (req, _) = p.feed(input).unwrap();
1770 assert!(req.is_none());
1771 assert_eq!(p.state(), ParserState::ReadingBody);
1772 let r = p.check_idle_timeout(1_000 + 30_001);
1774 assert!(matches!(r, Err(Http1Error::IdleTimeout)), "实际 {r:?}");
1775 assert_eq!(p.state(), ParserState::Error);
1776 }
1777
1778 #[test]
1779 fn test_idle_timeout_refreshed_by_activity() {
1780 let config = Http1Config::new().with_idle_timeout_ms(30_000);
1782 let mut p = Http1Parser::new(config);
1783 p.note_activity(0);
1784 let part1 = b"POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 10\r\n\r\nhe";
1785 let (r1, _) = p.feed(part1).unwrap();
1786 assert!(r1.is_none());
1787 assert_eq!(p.state(), ParserState::ReadingBody);
1788 p.note_activity(20_000);
1790 assert!(p.check_idle_timeout(45_000).is_ok(), "活跃后 25s 不得超时");
1791 let r = p.check_idle_timeout(20_000 + 30_001);
1793 assert!(matches!(r, Err(Http1Error::IdleTimeout)));
1794 }
1795
1796 #[test]
1797 fn test_idle_timeout_chunked_phase() {
1798 let config = Http1Config::new().with_idle_timeout_ms(30_000);
1800 let mut p = Http1Parser::new(config);
1801 p.note_activity(500);
1802 let input = b"POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHel";
1803 let (r1, _) = p.feed(input).unwrap();
1804 assert!(r1.is_none());
1805 assert_eq!(p.state(), ParserState::ReadingChunkData);
1806 let r = p.check_idle_timeout(500 + 30_001);
1807 assert!(matches!(r, Err(Http1Error::IdleTimeout)));
1808 }
1809
1810 #[test]
1813 fn test_pipelining_two_requests_single_feed() {
1814 let mut p = Http1Parser::new(make_config());
1816 let req1 = b"GET /a HTTP/1.1\r\nHost: example.com\r\n\r\n";
1817 let req2 = b"GET /b HTTP/1.1\r\nHost: example.com\r\n\r\n";
1818 let mut input = Vec::with_capacity(req1.len() + req2.len());
1819 input.extend_from_slice(req1);
1820 input.extend_from_slice(req2);
1821
1822 let (r1, c1) = p.feed(&input).unwrap();
1823 let r1 = r1.expect("first request must complete");
1824 assert_eq!(r1.line.target.as_ref(), "/a");
1825 assert_eq!(c1, req1.len(), "consumed 必须恰好等于第一个请求字节数");
1826
1827 let (r2, c2) = p.feed(&[]).unwrap();
1829 let r2 = r2.expect("second request must be buffered internally");
1830 assert_eq!(r2.line.target.as_ref(), "/b");
1831 assert_eq!(c2, 0, "空输入不消耗字节");
1832 }
1833
1834 #[test]
1835 fn test_pipelining_body_then_next_request() {
1836 let mut p = Http1Parser::new(make_config());
1838 let req1 = b"POST /x HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\n\r\nhello";
1839 let req2 = b"GET /y HTTP/1.1\r\nHost: example.com\r\n\r\n";
1840 let mut input = Vec::with_capacity(req1.len() + req2.len());
1841 input.extend_from_slice(req1);
1842 input.extend_from_slice(req2);
1843
1844 let (r1, c1) = p.feed(&input).unwrap();
1845 let r1 = r1.expect("first request must complete");
1846 assert_eq!(r1.body, b"hello");
1847 assert_eq!(c1, req1.len());
1848
1849 let (r2, _) = p.feed(&[]).unwrap();
1850 let r2 = r2.expect("second request must follow");
1851 assert_eq!(r2.line.target.as_ref(), "/y");
1852 }
1853}