1use std::net::SocketAddr;
7
8use bytes::{Bytes, BytesMut};
9use ringline::{ConnCtx, ParseResult};
10
11use crate::error::HttpError;
12use crate::response::Response;
13
14pub const DEFAULT_MAX_HEADER_SECTION: usize = 64 * 1024;
18
19pub const DEFAULT_MAX_CHUNK_SIZE: usize = 16 * 1024 * 1024;
23
24pub const DEFAULT_MAX_TRAILER_SECTION: usize = 4 * 1024;
27
28pub const DEFAULT_MAX_BODY_SIZE: usize = 16 * 1024 * 1024;
31
32pub struct H1Conn {
34 conn: ConnCtx,
35 host: String,
36 peer_will_close: bool,
39 max_header_section: usize,
40 max_chunk_size: usize,
41 max_trailer_section: usize,
42 max_body_size: usize,
43 max_decompressed_size: usize,
44}
45
46impl H1Conn {
47 fn new(conn: ConnCtx, host: &str) -> Self {
48 Self {
49 conn,
50 host: host.to_string(),
51 peer_will_close: false,
52 max_header_section: DEFAULT_MAX_HEADER_SECTION,
53 max_chunk_size: DEFAULT_MAX_CHUNK_SIZE,
54 max_trailer_section: DEFAULT_MAX_TRAILER_SECTION,
55 max_body_size: DEFAULT_MAX_BODY_SIZE,
56 max_decompressed_size: crate::compress::DEFAULT_MAX_DECOMPRESSED_SIZE,
57 }
58 }
59
60 pub async fn connect_tls(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
62 let conn = ringline::connect_tls(addr, host)?.await?;
63 Ok(Self::new(conn, host))
64 }
65
66 pub async fn connect_plain(addr: SocketAddr, host: &str) -> Result<Self, HttpError> {
68 let conn = ringline::connect(addr)?.await?;
69 Ok(Self::new(conn, host))
70 }
71
72 pub fn set_max_header_section(&mut self, n: usize) {
76 self.max_header_section = n;
77 }
78
79 pub fn set_max_chunk_size(&mut self, n: usize) {
82 self.max_chunk_size = n;
83 }
84
85 pub fn set_max_trailer_section(&mut self, n: usize) {
88 self.max_trailer_section = n;
89 }
90
91 pub fn set_max_body_size(&mut self, n: usize) {
94 self.max_body_size = n;
95 }
96
97 pub fn set_max_decompressed_size(&mut self, n: usize) {
101 self.max_decompressed_size = n;
102 }
103
104 pub fn peer_will_close(&self) -> bool {
109 self.peer_will_close
110 }
111
112 pub fn conn(&self) -> ConnCtx {
114 self.conn
115 }
116
117 pub fn close(&self) {
119 self.conn.close();
120 }
121
122 pub fn host(&self) -> &str {
123 &self.host
124 }
125
126 pub async fn send_request(
128 &mut self,
129 method: &str,
130 path: &str,
131 extra_headers: &[(&str, &str)],
132 body: Option<&[u8]>,
133 ) -> Result<Response, HttpError> {
134 let hdr = self
135 .send_and_parse_headers(method, path, extra_headers, body)
136 .await?;
137
138 let mut body_buf = hdr.body_leftover;
139
140 let no_body = response_has_no_body(method, hdr.status);
145
146 if no_body {
147 } else if let Some(cl) = hdr.content_length {
149 if cl > self.max_body_size {
150 return Err(HttpError::MaxSizeExceeded(format!(
151 "Content-Length {cl} exceeds {} body cap",
152 self.max_body_size
153 )));
154 }
155 while body_buf.len() < cl {
157 let target_len = cl;
158 let n = self
159 .conn
160 .with_data(|data| {
161 body_buf.extend_from_slice(data);
162 if body_buf.len() >= target_len {
163 ParseResult::Consumed(data.len())
164 } else {
165 ParseResult::Consumed(data.len())
167 }
168 })
169 .await;
170
171 if n == 0 {
172 return Err(HttpError::ConnectionClosed);
173 }
174 }
175 body_buf.truncate(cl);
176 } else if hdr.chunked {
177 let mut decoded = BytesMut::new();
179 let mut leftover = body_buf.to_vec();
180 body_buf.clear();
181 let max_body = self.max_body_size;
182 let max_chunk = self.max_chunk_size;
183 let max_trailer = self.max_trailer_section;
184
185 loop {
186 match decode_chunk(&leftover, max_chunk, max_trailer) {
187 ChunkResult::Complete {
188 data,
189 consumed,
190 is_last,
191 } => {
192 if decoded.len() + data.len() > max_body {
193 return Err(HttpError::MaxSizeExceeded(format!(
194 "chunked body exceeds {max_body} byte cap"
195 )));
196 }
197 decoded.extend_from_slice(data);
198 leftover = leftover[consumed..].to_vec();
199 if is_last {
200 break;
201 }
202 }
203 ChunkResult::NeedMore => {
204 let n = self
206 .conn
207 .with_data(|data| {
208 leftover.extend_from_slice(data);
209 ParseResult::Consumed(data.len())
210 })
211 .await;
212
213 if n == 0 {
214 return Err(HttpError::ConnectionClosed);
215 }
216 }
217 ChunkResult::Invalid(reason) => {
218 return Err(HttpError::InvalidMessage(format!(
219 "chunked decoding: {reason}"
220 )));
221 }
222 }
223 }
224
225 body_buf = decoded;
226 } else if hdr.version == HttpVersion::Http10 || self.peer_will_close {
227 let max_body = self.max_body_size;
231 loop {
232 if body_buf.len() > max_body {
233 return Err(HttpError::MaxSizeExceeded(format!(
234 "close-delimited body exceeds {max_body} byte cap"
235 )));
236 }
237 let n = self
238 .conn
239 .with_data(|data| {
240 body_buf.extend_from_slice(data);
241 ParseResult::Consumed(data.len())
242 })
243 .await;
244 if n == 0 {
245 break; }
247 }
248 }
249 #[cfg(any(feature = "gzip", feature = "zstd", feature = "brotli"))]
253 if let Some(ref encoding) = hdr.content_encoding {
254 let decompressed =
255 crate::compress::decompress(encoding, &body_buf, self.max_decompressed_size)?;
256 return Ok(Response::new(
257 hdr.status,
258 hdr.headers,
259 bytes::Bytes::from(decompressed),
260 ));
261 }
262
263 Ok(Response::new(hdr.status, hdr.headers, body_buf.freeze()))
264 }
265
266 pub async fn send_request_streaming(
271 &mut self,
272 method: &str,
273 path: &str,
274 extra_headers: &[(&str, &str)],
275 body: Option<&[u8]>,
276 ) -> Result<H1StreamingResponse<'_>, HttpError> {
277 let hdr = self
278 .send_and_parse_headers(method, path, extra_headers, body)
279 .await?;
280
281 let max_chunk = self.max_chunk_size;
282 let max_trailer = self.max_trailer_section;
283 let no_body = response_has_no_body(method, hdr.status);
284
285 let state = if no_body {
286 H1StreamState::Done
287 } else if let Some(cl) = hdr.content_length {
288 if cl > self.max_body_size {
289 return Err(HttpError::MaxSizeExceeded(format!(
290 "Content-Length {cl} exceeds {} body cap",
291 self.max_body_size
292 )));
293 }
294 H1StreamState::ContentLength {
295 remaining: cl.saturating_sub(hdr.body_leftover.len()),
296 leftover: hdr.body_leftover,
297 }
298 } else if hdr.chunked {
299 H1StreamState::Chunked {
300 leftover: hdr.body_leftover.to_vec(),
301 max_chunk,
302 max_trailer,
303 }
304 } else {
305 H1StreamState::Done
306 };
307
308 Ok(H1StreamingResponse {
309 conn: &mut self.conn,
310 status: hdr.status,
311 headers: hdr.headers,
312 state,
313 })
314 }
315
316 async fn send_and_parse_headers(
318 &mut self,
319 method: &str,
320 path: &str,
321 extra_headers: &[(&str, &str)],
322 body: Option<&[u8]>,
323 ) -> Result<H1HeaderResult, HttpError> {
324 validate_method(method)?;
329 validate_request_target(path)?;
330 validate_token(&self.host)
331 .map_err(|_| HttpError::InvalidMessage("host contains forbidden bytes".into()))?;
332 for (name, value) in extra_headers {
333 validate_field_name(name)?;
334 validate_field_value(value)?;
335 }
336
337 let mut req = Vec::with_capacity(256);
339 req.extend_from_slice(method.as_bytes());
340 req.push(b' ');
341 req.extend_from_slice(path.as_bytes());
342 req.extend_from_slice(b" HTTP/1.1\r\n");
343 req.extend_from_slice(b"host: ");
344 req.extend_from_slice(self.host.as_bytes());
345 req.extend_from_slice(b"\r\n");
346
347 let has_accept_encoding = extra_headers
348 .iter()
349 .any(|(name, _)| name.eq_ignore_ascii_case("accept-encoding"));
350
351 for (name, value) in extra_headers {
352 req.extend_from_slice(name.as_bytes());
353 req.extend_from_slice(b": ");
354 req.extend_from_slice(value.as_bytes());
355 req.extend_from_slice(b"\r\n");
356 }
357
358 if !has_accept_encoding && let Some(ae) = crate::compress::accept_encoding_value() {
361 req.extend_from_slice(b"accept-encoding: ");
362 req.extend_from_slice(ae.as_bytes());
363 req.extend_from_slice(b"\r\n");
364 }
365
366 if let Some(b) = body
367 && !b.is_empty()
368 {
369 req.extend_from_slice(b"content-length: ");
370 req.extend_from_slice(b.len().to_string().as_bytes());
371 req.extend_from_slice(b"\r\n");
372 }
373
374 req.extend_from_slice(b"\r\n");
375
376 if let Some(b) = body
377 && !b.is_empty()
378 {
379 req.extend_from_slice(b);
380 }
381
382 self.conn.send_nowait(&req)?;
383
384 let max_section = self.max_header_section;
387 let mut bytes_seen = 0usize;
388 let mut parse_error: Option<HttpError> = None;
389 let mut status: u16 = 0;
390 let mut version: HttpVersion = HttpVersion::Http10;
391 let mut headers: Vec<(String, String)> = Vec::new();
392 let mut content_length: Option<usize> = None;
393 let mut chunked = false;
394 let mut content_encoding: Option<String> = None;
395 let mut connection_close = false;
396 let mut headers_done = false;
397 let mut body_leftover = BytesMut::new();
398 let mut scanned: usize = 0;
402
403 while !headers_done {
404 let n = self
405 .conn
406 .with_data(|data| {
407 bytes_seen = data.len();
408 if bytes_seen > max_section {
409 parse_error = Some(HttpError::MaxSizeExceeded(format!(
410 "response header section exceeds {max_section} bytes"
411 )));
412 return ParseResult::Consumed(data.len());
413 }
414 let scan_from = scanned.saturating_sub(3);
415 if let Some(end) = find_header_end(data, scan_from) {
416 let header_bytes = &data[..end];
417 match parse_response_headers(header_bytes) {
418 Ok(parsed) => {
419 status = parsed.status;
420 version = parsed.version;
421 headers = parsed.headers;
422 content_length = parsed.content_length;
423 chunked = parsed.chunked;
424 content_encoding = parsed.content_encoding;
425 connection_close = parsed.connection_close;
426 headers_done = true;
427 let header_consumed = end + 4;
428
429 let remaining = &data[header_consumed..];
430 if !remaining.is_empty() {
431 body_leftover.extend_from_slice(remaining);
432 }
433 }
434 Err(e) => parse_error = Some(e),
435 }
436 ParseResult::Consumed(data.len())
437 } else {
438 scanned = data.len();
443 ParseResult::Consumed(0)
444 }
445 })
446 .await;
447
448 if let Some(e) = parse_error.take() {
449 return Err(e);
450 }
451 if n == 0 {
452 return Err(HttpError::ConnectionClosed);
453 }
454 }
455
456 self.peer_will_close = match version {
461 HttpVersion::Http10 => !headers.iter().any(|(k, v)| {
462 k.eq_ignore_ascii_case("connection") && contains_token(v, "keep-alive")
463 }),
464 HttpVersion::Http11 => connection_close,
465 };
466
467 Ok(H1HeaderResult {
468 status,
469 version,
470 headers,
471 content_length,
472 chunked,
473 content_encoding,
474 body_leftover,
475 })
476 }
477}
478
479struct H1HeaderResult {
481 status: u16,
482 #[allow(dead_code)] version: HttpVersion,
484 headers: Vec<(String, String)>,
485 content_length: Option<usize>,
486 chunked: bool,
487 #[cfg_attr(
488 not(any(feature = "gzip", feature = "zstd", feature = "brotli")),
489 allow(dead_code)
490 )]
491 content_encoding: Option<String>,
492 body_leftover: BytesMut,
493}
494
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
497enum HttpVersion {
498 Http10,
499 Http11,
500}
501
502enum H1StreamState {
504 ContentLength {
505 remaining: usize,
506 leftover: BytesMut,
507 },
508 Chunked {
509 leftover: Vec<u8>,
510 max_chunk: usize,
511 max_trailer: usize,
512 },
513 Done,
514}
515
516pub struct H1StreamingResponse<'a> {
520 conn: &'a mut ConnCtx,
521 status: u16,
522 headers: Vec<(String, String)>,
523 state: H1StreamState,
524}
525
526impl<'a> H1StreamingResponse<'a> {
527 pub fn status(&self) -> u16 {
529 self.status
530 }
531
532 pub fn headers(&self) -> &[(String, String)] {
534 &self.headers
535 }
536
537 pub fn header(&self, name: &str) -> Option<&str> {
539 let lower = name.to_ascii_lowercase();
540 self.headers
541 .iter()
542 .find(|(k, _)| k.to_ascii_lowercase() == lower)
543 .map(|(_, v)| v.as_str())
544 }
545
546 pub async fn next_chunk(&mut self) -> Result<Option<Bytes>, HttpError> {
548 loop {
549 match &mut self.state {
550 H1StreamState::ContentLength {
551 remaining,
552 leftover,
553 } => {
554 if !leftover.is_empty() {
556 let chunk = leftover.split().freeze();
557 *remaining = remaining.saturating_sub(chunk.len());
558 return Ok(Some(chunk));
559 }
560 if *remaining == 0 {
561 self.state = H1StreamState::Done;
562 return Ok(None);
563 }
564 let rem = *remaining;
566 let mut got = BytesMut::new();
567 let n = self
568 .conn
569 .with_data(|data| {
570 let take = data.len().min(rem);
571 got.extend_from_slice(&data[..take]);
572 ParseResult::Consumed(take)
573 })
574 .await;
575 if n == 0 {
576 self.state = H1StreamState::Done;
577 return Err(HttpError::ConnectionClosed);
578 }
579 *remaining -= got.len();
580 return Ok(Some(got.freeze()));
581 }
582 H1StreamState::Chunked {
583 leftover,
584 max_chunk,
585 max_trailer,
586 } => {
587 match decode_chunk(leftover, *max_chunk, *max_trailer) {
588 ChunkResult::Complete {
589 data,
590 consumed,
591 is_last,
592 } => {
593 let chunk = Bytes::copy_from_slice(data);
594 *leftover = leftover[consumed..].to_vec();
595 if is_last {
596 self.state = H1StreamState::Done;
597 if chunk.is_empty() {
598 return Ok(None);
599 }
600 return Ok(Some(chunk));
601 }
602 return Ok(Some(chunk));
603 }
604 ChunkResult::NeedMore => {
605 let n = self
607 .conn
608 .with_data(|data| {
609 leftover.extend_from_slice(data);
610 ParseResult::Consumed(data.len())
611 })
612 .await;
613 if n == 0 {
614 self.state = H1StreamState::Done;
615 return Err(HttpError::ConnectionClosed);
616 }
617 }
619 ChunkResult::Invalid(reason) => {
620 self.state = H1StreamState::Done;
621 return Err(HttpError::InvalidMessage(format!(
622 "chunked decoding: {reason}"
623 )));
624 }
625 }
626 }
627 H1StreamState::Done => return Ok(None),
628 }
629 }
630 }
631}
632
633fn find_header_end(data: &[u8], start: usize) -> Option<usize> {
640 let end = data.len().saturating_sub(3);
641 (start..end).find(|&i| {
642 data[i] == b'\r' && data[i + 1] == b'\n' && data[i + 2] == b'\r' && data[i + 3] == b'\n'
643 })
644}
645
646struct ParsedHeaders {
647 status: u16,
648 version: HttpVersion,
649 headers: Vec<(String, String)>,
650 content_length: Option<usize>,
651 chunked: bool,
652 content_encoding: Option<String>,
653 connection_close: bool,
654}
655
656fn parse_response_headers(data: &[u8]) -> Result<ParsedHeaders, HttpError> {
665 let text = std::str::from_utf8(data).map_err(|_| HttpError::Parse)?;
666 let mut lines = text.split("\r\n");
667
668 let status_line = lines.next().ok_or(HttpError::Parse)?;
670 let mut parts = status_line.splitn(3, ' ');
671 let version_str = parts.next().ok_or(HttpError::Parse)?;
672 let version = match version_str {
673 "HTTP/1.0" => HttpVersion::Http10,
674 "HTTP/1.1" => HttpVersion::Http11,
675 _ => return Err(HttpError::Parse),
676 };
677 let status_str = parts.next().ok_or(HttpError::Parse)?;
678 if status_str.len() != 3 || !status_str.chars().all(|c| c.is_ascii_digit()) {
679 return Err(HttpError::Parse);
680 }
681 let status: u16 = status_str.parse().map_err(|_| HttpError::Parse)?;
682
683 let mut headers = Vec::new();
684 let mut content_lengths: Vec<usize> = Vec::new();
685 let mut transfer_encodings: Vec<String> = Vec::new();
686 let mut content_encoding = None;
687 let mut connection_close = false;
688
689 for line in lines {
690 if line.is_empty() {
691 break;
692 }
693 let (name, value) = line.split_once(':').ok_or(HttpError::Parse)?;
694 let name = name.trim().to_string();
695 let value = value.trim().to_string();
696
697 if value.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
702 return Err(HttpError::InvalidMessage(format!(
703 "header `{name}` contains CR/LF/NUL"
704 )));
705 }
706 if name.is_empty() || name.bytes().any(|b| !is_token_char(b)) {
709 return Err(HttpError::Parse);
710 }
711
712 let name_lc = name.to_ascii_lowercase();
713 match name_lc.as_str() {
714 "content-length" => {
715 for piece in value.split(',') {
720 let n: usize = piece.trim().parse().map_err(|_| {
721 HttpError::InvalidMessage("malformed Content-Length".into())
722 })?;
723 content_lengths.push(n);
724 }
725 }
726 "transfer-encoding" => {
727 for piece in value.split(',') {
728 transfer_encodings.push(piece.trim().to_ascii_lowercase());
729 }
730 }
731 "content-encoding" => content_encoding = Some(value.clone()),
732 "connection" if contains_token(&value, "close") => {
733 connection_close = true;
734 }
735 _ => {}
736 }
737
738 headers.push((name, value));
739 }
740
741 let chunked = if transfer_encodings.is_empty() {
745 false
746 } else {
747 if transfer_encodings.iter().any(|c| c != "chunked") {
751 return Err(HttpError::InvalidMessage(format!(
752 "unsupported Transfer-Encoding: {transfer_encodings:?}"
753 )));
754 }
755 if transfer_encodings.last().map(String::as_str) != Some("chunked") {
756 return Err(HttpError::InvalidMessage(
757 "Transfer-Encoding: chunked must be the final coding".into(),
758 ));
759 }
760 true
761 };
762
763 if chunked && !content_lengths.is_empty() {
766 return Err(HttpError::InvalidMessage(
767 "Transfer-Encoding and Content-Length both present".into(),
768 ));
769 }
770
771 let content_length = match content_lengths.as_slice() {
773 [] => None,
774 values => {
775 let first = values[0];
776 if values.iter().any(|&v| v != first) {
777 return Err(HttpError::InvalidMessage(
778 "conflicting Content-Length values".into(),
779 ));
780 }
781 Some(first)
782 }
783 };
784
785 Ok(ParsedHeaders {
786 status,
787 version,
788 headers,
789 content_length,
790 chunked,
791 content_encoding,
792 connection_close,
793 })
794}
795
796fn contains_token(field: &str, token: &str) -> bool {
799 field
800 .split(',')
801 .any(|p| p.trim().eq_ignore_ascii_case(token))
802}
803
804fn is_token_char(b: u8) -> bool {
806 matches!(b,
807 b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
808 b'^' | b'_' | b'`' | b'|' | b'~' |
809 b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z')
810}
811
812fn validate_method(method: &str) -> Result<(), HttpError> {
813 if method.is_empty() || method.bytes().any(|b| !is_token_char(b)) {
814 return Err(HttpError::InvalidMessage(format!(
815 "invalid method `{method}`"
816 )));
817 }
818 Ok(())
819}
820
821fn validate_request_target(target: &str) -> Result<(), HttpError> {
822 if target.is_empty() {
823 return Err(HttpError::InvalidMessage("empty request target".into()));
824 }
825 for b in target.bytes() {
830 if b <= 0x20 || b == 0x7f || b == b'\r' || b == b'\n' {
831 return Err(HttpError::InvalidMessage(
832 "request target contains control or whitespace bytes".into(),
833 ));
834 }
835 }
836 Ok(())
837}
838
839fn validate_field_name(name: &str) -> Result<(), HttpError> {
840 if name.is_empty() || name.bytes().any(|b| !is_token_char(b)) {
841 return Err(HttpError::InvalidMessage(format!(
842 "invalid header name `{name}`"
843 )));
844 }
845 Ok(())
846}
847
848fn validate_field_value(value: &str) -> Result<(), HttpError> {
849 for b in value.bytes() {
853 if b == b'\r' || b == b'\n' || b == 0 {
854 return Err(HttpError::InvalidMessage(
855 "header value contains CR/LF/NUL".into(),
856 ));
857 }
858 }
859 Ok(())
860}
861
862fn response_has_no_body(method: &str, status: u16) -> bool {
866 method.eq_ignore_ascii_case("HEAD") || matches!(status, 100..=199 | 204 | 304)
867}
868
869fn validate_token(s: &str) -> Result<(), ()> {
870 if s.is_empty() || s.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
871 return Err(());
872 }
873 Ok(())
874}
875
876fn validate_trailer_line(line: &[u8]) -> Result<(), &'static str> {
883 if matches!(line.first(), Some(b' ' | b'\t')) {
886 return Err("trailer contains obs-fold continuation");
887 }
888 let text = std::str::from_utf8(line).map_err(|_| "non-UTF-8 trailer line")?;
889 let (name, value) = text.split_once(':').ok_or("trailer missing colon")?;
890 let name = name.trim();
891 if name.is_empty() || name.bytes().any(|b| !is_token_char(b)) {
892 return Err("trailer field-name is not a token");
893 }
894 let value = value.trim();
898 if value.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
899 return Err("trailer value contains CR/LF/NUL");
900 }
901 Ok(())
902}
903
904enum ChunkResult<'a> {
905 Complete {
906 data: &'a [u8],
907 consumed: usize,
908 is_last: bool,
909 },
910 NeedMore,
911 Invalid(&'static str),
914}
915
916fn decode_chunk(data: &[u8], max_chunk_size: usize, max_trailer_section: usize) -> ChunkResult<'_> {
921 let crlf = match find_crlf(data) {
923 Some(pos) => pos,
924 None => return ChunkResult::NeedMore,
925 };
926
927 let size_str = match std::str::from_utf8(&data[..crlf]) {
928 Ok(s) => s.trim(),
929 Err(_) => return ChunkResult::Invalid("non-UTF-8 chunk header"),
931 };
932
933 let size_hex = size_str.split(';').next().unwrap_or("").trim();
935 if size_hex.is_empty() || !size_hex.chars().all(|c| c.is_ascii_hexdigit()) {
936 return ChunkResult::Invalid("malformed chunk size");
937 }
938 let size = match usize::from_str_radix(size_hex, 16) {
939 Ok(s) => s,
940 Err(_) => return ChunkResult::Invalid("chunk size overflows usize"),
941 };
942 if size > max_chunk_size {
943 return ChunkResult::Invalid("chunk size exceeds configured cap");
944 }
945
946 if size == 0 {
947 let after_zero = crlf + 2;
951 let mut pos = after_zero;
952 loop {
953 if pos - after_zero > max_trailer_section {
954 return ChunkResult::Invalid("trailer section exceeds cap");
955 }
956 match find_crlf(&data[pos..]) {
957 Some(0) => {
958 return ChunkResult::Complete {
960 data: &[],
961 consumed: pos + 2,
962 is_last: true,
963 };
964 }
965 Some(next_crlf) => {
966 let line = &data[pos..pos + next_crlf];
967 if let Err(reason) = validate_trailer_line(line) {
968 return ChunkResult::Invalid(reason);
969 }
970 pos += next_crlf + 2;
971 }
972 None => return ChunkResult::NeedMore,
973 }
974 }
975 }
976
977 let chunk_start = crlf + 2;
978 let chunk_end = chunk_start + size;
979 let total = chunk_end + 2; if data.len() < total {
982 return ChunkResult::NeedMore;
983 }
984
985 if data[chunk_end] != b'\r' || data[chunk_end + 1] != b'\n' {
988 return ChunkResult::Invalid("missing CRLF after chunk data");
989 }
990
991 ChunkResult::Complete {
992 data: &data[chunk_start..chunk_end],
993 consumed: total,
994 is_last: false,
995 }
996}
997
998fn find_crlf(data: &[u8]) -> Option<usize> {
999 (0..data.len().saturating_sub(1)).find(|&i| data[i] == b'\r' && data[i + 1] == b'\n')
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004 use super::*;
1005
1006 #[test]
1007 fn parse_simple_response() {
1008 let data = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\n";
1009 let parsed = parse_response_headers(data).unwrap();
1010 assert_eq!(parsed.status, 200);
1011 assert_eq!(parsed.content_length, Some(5));
1012 assert!(!parsed.chunked);
1013 assert_eq!(parsed.headers.len(), 1);
1014 }
1015
1016 #[test]
1017 fn parse_chunked_response() {
1018 let data = b"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n";
1019 let parsed = parse_response_headers(data).unwrap();
1020 assert_eq!(parsed.status, 200);
1021 assert!(parsed.chunked);
1022 assert_eq!(parsed.content_length, None);
1023 }
1024
1025 #[test]
1026 fn find_header_end_found() {
1027 let data = b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\nbody";
1028 assert_eq!(find_header_end(data, 0), Some(34));
1029 }
1030
1031 #[test]
1032 fn find_header_end_not_found() {
1033 let data = b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n";
1034 assert_eq!(find_header_end(data, 0), None);
1035 }
1036
1037 #[test]
1038 fn find_header_end_resumes_from_cursor_with_boundary_overlap() {
1039 let mut buf: Vec<u8> = b"GET / HTTP/1.1\r\nhost: x\r".to_vec();
1044 assert_eq!(find_header_end(&buf, 0), None);
1046 let scanned = buf.len();
1047 buf.extend_from_slice(b"\n\r\nbody");
1049 let start = scanned.saturating_sub(3);
1050 let pos = find_header_end(&buf, start).expect("must find boundary");
1051 assert_eq!(&buf[pos..pos + 4], b"\r\n\r\n");
1052 }
1053
1054 #[test]
1055 fn find_header_end_cursor_skips_already_scanned_region() {
1056 let data = b"HTTP/1.1 200 OK\r\n\r\nbody-with-\r\n\r\n-inside";
1060 assert_eq!(find_header_end(data, 0), Some(15));
1062 let after = 19; assert_eq!(find_header_end(data, after), Some(29));
1067 }
1068
1069 #[test]
1070 fn decode_chunk_simple() {
1071 let data = b"5\r\nhello\r\n";
1072 match decode_chunk(data, usize::MAX, usize::MAX) {
1073 ChunkResult::Complete {
1074 data,
1075 consumed,
1076 is_last,
1077 } => {
1078 assert_eq!(data, b"hello");
1079 assert_eq!(consumed, 10);
1080 assert!(!is_last);
1081 }
1082 ChunkResult::NeedMore | ChunkResult::Invalid(_) => panic!("expected Complete"),
1083 }
1084 }
1085
1086 #[test]
1087 fn decode_chunk_last_with_empty_trailers() {
1088 let data = b"0\r\n\r\n";
1090 match decode_chunk(data, usize::MAX, usize::MAX) {
1091 ChunkResult::Complete {
1092 is_last, consumed, ..
1093 } => {
1094 assert!(is_last);
1095 assert_eq!(consumed, 5, "should consume 0\\r\\n\\r\\n");
1096 }
1097 ChunkResult::NeedMore | ChunkResult::Invalid(_) => panic!("expected Complete"),
1098 }
1099 }
1100
1101 #[test]
1102 fn decode_chunk_last_needs_trailing_crlf() {
1103 let data = b"0\r\n";
1105 match decode_chunk(data, usize::MAX, usize::MAX) {
1106 ChunkResult::NeedMore => {}
1107 ChunkResult::Complete { .. } | ChunkResult::Invalid(_) => panic!("expected NeedMore"),
1108 }
1109 }
1110
1111 #[test]
1112 fn decode_chunk_last_with_trailers() {
1113 let data = b"0\r\nTrailer: value\r\n\r\n";
1115 match decode_chunk(data, usize::MAX, usize::MAX) {
1116 ChunkResult::Complete {
1117 is_last, consumed, ..
1118 } => {
1119 assert!(is_last);
1120 assert_eq!(
1121 consumed,
1122 data.len(),
1123 "should consume entire trailer section"
1124 );
1125 }
1126 ChunkResult::NeedMore | ChunkResult::Invalid(_) => panic!("expected Complete"),
1127 }
1128 }
1129
1130 #[test]
1131 fn decode_chunk_last_does_not_consume_next_response() {
1132 let data = b"0\r\n\r\nHTTP/1.1 200 OK\r\n";
1134 match decode_chunk(data, usize::MAX, usize::MAX) {
1135 ChunkResult::Complete {
1136 is_last, consumed, ..
1137 } => {
1138 assert!(is_last);
1139 assert_eq!(consumed, 5, "should only consume 0\\r\\n\\r\\n");
1140 assert_eq!(&data[consumed..], b"HTTP/1.1 200 OK\r\n");
1141 }
1142 ChunkResult::NeedMore | ChunkResult::Invalid(_) => panic!("expected Complete"),
1143 }
1144 }
1145
1146 #[test]
1147 fn decode_chunk_need_more() {
1148 let data = b"5\r\nhel";
1149 match decode_chunk(data, usize::MAX, usize::MAX) {
1150 ChunkResult::NeedMore => {}
1151 ChunkResult::Complete { .. } | ChunkResult::Invalid(_) => panic!("expected NeedMore"),
1152 }
1153 }
1154
1155 #[test]
1156 fn parse_malformed_status_line_returns_err() {
1157 assert!(parse_response_headers(b"HTTP/1.1 \r\n").is_err());
1159 assert!(parse_response_headers(b"NOT-HTTP\r\n").is_err());
1161 assert!(parse_response_headers(b"").is_err());
1163 assert!(parse_response_headers(b"HTTP/1.1\r\n").is_err());
1165 assert!(parse_response_headers(b"HTTP/2.0 200 OK\r\n").is_err());
1167 assert!(parse_response_headers(b"HTTP/1.1 20X OK\r\n").is_err());
1169 }
1170
1171 #[test]
1174 fn parse_rejects_te_and_cl_together() {
1175 let data = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\ntransfer-encoding: chunked\r\n";
1177 let err = parse_response_headers(data).err().unwrap();
1178 assert!(matches!(err, HttpError::InvalidMessage(_)));
1179 }
1180
1181 #[test]
1182 fn parse_rejects_multiple_conflicting_content_length() {
1183 let data = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\ncontent-length: 7\r\n";
1185 let err = parse_response_headers(data).err().unwrap();
1186 assert!(matches!(err, HttpError::InvalidMessage(_)));
1187 }
1188
1189 #[test]
1190 fn parse_accepts_multiple_identical_content_length() {
1191 let data = b"HTTP/1.1 200 OK\r\ncontent-length: 5\r\ncontent-length: 5\r\n";
1193 let parsed = parse_response_headers(data).unwrap();
1194 assert_eq!(parsed.content_length, Some(5));
1195 }
1196
1197 #[test]
1198 fn parse_rejects_unsupported_transfer_encoding() {
1199 let data = b"HTTP/1.1 200 OK\r\ntransfer-encoding: gzip\r\n";
1201 let err = parse_response_headers(data).err().unwrap();
1202 assert!(matches!(err, HttpError::InvalidMessage(_)));
1203 }
1204
1205 #[test]
1206 fn parse_rejects_chunked_not_final_coding() {
1207 let data = b"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked, gzip\r\n";
1209 let err = parse_response_headers(data).err().unwrap();
1210 assert!(matches!(err, HttpError::InvalidMessage(_)));
1211 }
1212
1213 #[test]
1214 fn parse_rejects_header_value_with_embedded_lf() {
1215 let data = b"HTTP/1.1 200 OK\r\nx-evil: value\nset-cookie: pwned\r\n";
1217 let err = parse_response_headers(data).err().unwrap();
1218 assert!(matches!(err, HttpError::InvalidMessage(_)));
1219 }
1220
1221 #[test]
1222 fn parse_honors_connection_close() {
1223 let data = b"HTTP/1.1 200 OK\r\nconnection: close\r\n";
1224 let parsed = parse_response_headers(data).unwrap();
1225 assert!(parsed.connection_close);
1226 }
1227
1228 #[test]
1229 fn parse_http10_version() {
1230 let data = b"HTTP/1.0 200 OK\r\n";
1231 let parsed = parse_response_headers(data).unwrap();
1232 assert_eq!(parsed.version, HttpVersion::Http10);
1233 }
1234
1235 #[test]
1236 fn decode_chunk_rejects_malformed_size() {
1237 let data = b"xyz\r\n";
1239 assert!(matches!(
1240 decode_chunk(data, usize::MAX, usize::MAX),
1241 ChunkResult::Invalid(_)
1242 ));
1243 }
1244
1245 #[test]
1246 fn decode_chunk_rejects_oversize() {
1247 let data = b"1000\r\n"; assert!(matches!(
1250 decode_chunk(data, 100, usize::MAX),
1251 ChunkResult::Invalid(_)
1252 ));
1253 }
1254
1255 #[test]
1256 fn decode_chunk_rejects_missing_trailing_crlf() {
1257 let data = b"5\r\nhello\x00\x00";
1259 assert!(matches!(
1260 decode_chunk(data, usize::MAX, usize::MAX),
1261 ChunkResult::Invalid(_)
1262 ));
1263 }
1264
1265 #[test]
1266 fn decode_chunk_rejects_oversize_trailers() {
1267 let trailer: Vec<u8> = b"0\r\n"
1269 .iter()
1270 .copied()
1271 .chain(std::iter::repeat_n(b'a', 200))
1272 .chain(b"\r\n\r\n".iter().copied())
1273 .collect();
1274 assert!(matches!(
1275 decode_chunk(&trailer, usize::MAX, 100),
1276 ChunkResult::Invalid(_)
1277 ));
1278 }
1279
1280 #[test]
1281 fn validate_field_value_rejects_crlf() {
1282 assert!(validate_field_value("value\r\nx: y").is_err());
1283 assert!(validate_field_value("value\nx: y").is_err());
1284 assert!(validate_field_value("normal").is_ok());
1285 }
1286
1287 #[test]
1288 fn decode_chunk_accepts_valid_trailer() {
1289 let data = b"0\r\nx-checksum: abc123\r\n\r\n";
1291 match decode_chunk(data, usize::MAX, usize::MAX) {
1292 ChunkResult::Complete { is_last, .. } => assert!(is_last),
1293 other => panic!("expected Complete, got {:?}", chunk_kind(&other)),
1294 }
1295 }
1296
1297 #[test]
1298 fn decode_chunk_rejects_trailer_without_colon() {
1299 let data = b"0\r\nno-colon-here\r\n\r\n";
1300 assert!(matches!(
1301 decode_chunk(data, usize::MAX, usize::MAX),
1302 ChunkResult::Invalid(_)
1303 ));
1304 }
1305
1306 #[test]
1307 fn decode_chunk_rejects_trailer_with_invalid_field_name() {
1308 let data = b"0\r\nbad name: value\r\n\r\n";
1310 assert!(matches!(
1311 decode_chunk(data, usize::MAX, usize::MAX),
1312 ChunkResult::Invalid(_)
1313 ));
1314 }
1315
1316 #[test]
1317 fn decode_chunk_rejects_trailer_value_with_embedded_nul() {
1318 let data = b"0\r\nx-evil: bad\x00stuff\r\n\r\n";
1322 assert!(matches!(
1323 decode_chunk(data, usize::MAX, usize::MAX),
1324 ChunkResult::Invalid(_)
1325 ));
1326 }
1327
1328 #[test]
1329 fn decode_chunk_rejects_trailer_obs_fold_continuation() {
1330 let data = b"0\r\n trailer-continuation\r\n\r\n";
1333 assert!(matches!(
1334 decode_chunk(data, usize::MAX, usize::MAX),
1335 ChunkResult::Invalid(_)
1336 ));
1337 }
1338
1339 fn chunk_kind(r: &ChunkResult<'_>) -> &'static str {
1340 match r {
1341 ChunkResult::Complete { .. } => "Complete",
1342 ChunkResult::NeedMore => "NeedMore",
1343 ChunkResult::Invalid(_) => "Invalid",
1344 }
1345 }
1346
1347 #[test]
1348 fn validate_request_target_rejects_whitespace() {
1349 assert!(validate_request_target("/path with space").is_err());
1350 assert!(validate_request_target("/path\r\nGET").is_err());
1351 assert!(validate_request_target("/normal").is_ok());
1352 }
1353
1354 #[test]
1355 fn response_has_no_body_for_head_and_status_codes() {
1356 assert!(response_has_no_body("HEAD", 200));
1357 assert!(response_has_no_body("GET", 100));
1358 assert!(response_has_no_body("GET", 204));
1359 assert!(response_has_no_body("GET", 304));
1360 assert!(!response_has_no_body("GET", 200));
1361 assert!(!response_has_no_body("POST", 201));
1362 }
1363}