1use std::fmt;
28
29use crate::chunked::ChunkedDecoder;
30use crate::types::Http1Error;
31
32pub const MAX_HEADER_SECTION: usize = 64 * 1024;
34
35pub const MAX_HEADER_COUNT: usize = 256;
37
38pub const MAX_HEADER_NAME_LEN: usize = 64;
40
41pub const MAX_HEADER_VALUE_LEN: usize = 8192;
43
44pub const MAX_BODY_BYTES: u64 = 64 * 1024 * 1024;
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ClientResponse {
54 pub status: u16,
56 pub headers: Vec<(String, String)>,
58 pub body: Vec<u8>,
60}
61
62#[derive(Debug)]
64pub enum ClientError {
65 InvalidStatusLine,
67 InvalidHeader(String),
69 HeaderTooLarge,
71 BodyTooLarge,
73 InvalidChunked(String),
75 ProtocolInconsistency(String),
77 Truncated,
79}
80
81impl fmt::Display for ClientError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::InvalidStatusLine => write!(f, "upstream status line invalid"),
85 Self::InvalidHeader(m) => write!(f, "upstream header invalid: {m}"),
86 Self::HeaderTooLarge => write!(f, "upstream header section too large"),
87 Self::BodyTooLarge => write!(f, "upstream body too large"),
88 Self::InvalidChunked(m) => write!(f, "upstream chunked encoding error: {m}"),
89 Self::ProtocolInconsistency(m) => write!(f, "upstream protocol inconsistency: {m}"),
90 Self::Truncated => write!(f, "upstream response truncated"),
91 }
92 }
93}
94
95impl std::error::Error for ClientError {}
96
97#[inline]
99fn is_valid_token(s: &str) -> bool {
100 !s.is_empty()
101 && s.bytes().all(|b| {
102 b.is_ascii_alphanumeric()
103 || matches!(
104 b,
105 b'!' | b'#'
106 | b'$'
107 | b'%'
108 | b'&'
109 | b'\''
110 | b'*'
111 | b'+'
112 | b'-'
113 | b'.'
114 | b'^'
115 | b'_'
116 | b'`'
117 | b'|'
118 | b'~'
119 )
120 })
121}
122
123#[inline]
125fn contains_crlf(s: &str) -> bool {
126 s.contains('\r') || s.contains('\n')
127}
128
129pub fn encode_request(
145 method: &str,
146 path_with_query: &str,
147 host: &str,
148 headers: &[(String, String)],
149 body: &[u8],
150) -> Option<Vec<u8>> {
151 if !is_valid_token(method)
153 || path_with_query.is_empty()
154 || contains_crlf(path_with_query)
155 || path_with_query.contains(' ')
156 || host.is_empty()
157 || contains_crlf(host)
158 {
159 return None;
160 }
161 for (name, value) in headers {
163 if !is_valid_token(name) || contains_crlf(value) {
164 return None;
165 }
166 }
167
168 let mut out = Vec::with_capacity(method.len() + path_with_query.len() + host.len() + body.len() + 256);
170 out.extend_from_slice(method.as_bytes());
171 out.push(b' ');
172 out.extend_from_slice(path_with_query.as_bytes());
173 out.extend_from_slice(b" HTTP/1.1\r\nHost: ");
174 out.extend_from_slice(host.as_bytes());
175 out.extend_from_slice(b"\r\n");
176
177 for (name, value) in headers {
178 out.extend_from_slice(name.as_bytes());
179 out.extend_from_slice(b": ");
180 out.extend_from_slice(value.as_bytes());
181 out.extend_from_slice(b"\r\n");
182 }
183
184 out.extend_from_slice(b"Content-Length: ");
185 out.extend_from_slice(body.len().to_string().as_bytes());
186 out.extend_from_slice(b"\r\nConnection: close\r\n\r\n");
188 out.extend_from_slice(body);
189 Some(out)
190}
191
192#[inline]
194fn find_header_end(buf: &[u8]) -> Option<usize> {
195 buf.windows(4).position(|w| w == b"\r\n\r\n")
196}
197
198fn parse_status_line(line: &[u8]) -> Result<u16, ClientError> {
200 let s = std::str::from_utf8(line).map_err(|_| ClientError::InvalidStatusLine)?;
201 let mut parts = s.splitn(3, ' ');
202 let version = parts.next().ok_or(ClientError::InvalidStatusLine)?;
203 let ver_ok = version.len() == 8
205 && version.starts_with("HTTP/1.")
206 && version.as_bytes()[7].is_ascii_digit();
207 if !ver_ok {
208 return Err(ClientError::InvalidStatusLine);
209 }
210 let status_str = parts.next().ok_or(ClientError::InvalidStatusLine)?;
211 if status_str.len() != 3 || !status_str.bytes().all(|b| b.is_ascii_digit()) {
212 return Err(ClientError::InvalidStatusLine);
213 }
214 status_str
215 .parse::<u16>()
216 .map_err(|_| ClientError::InvalidStatusLine)
217}
218
219fn parse_head(head: &[u8]) -> Result<(u16, Vec<(String, String)>), ClientError> {
225 let lines: Vec<&[u8]> = head.split(|&b| b == b'\n').collect();
226 let total = lines.len();
227 if total == 0 {
228 return Err(ClientError::InvalidStatusLine);
229 }
230 let status_line = lines[0];
231 let status_line = if total > 1 {
232 match status_line.last() {
233 Some(b'\r') => &status_line[..status_line.len() - 1],
234 _ => {
235 return Err(ClientError::InvalidHeader(
236 "bare LF in status line (protocol violation)".into(),
237 ))
238 }
239 }
240 } else {
241 match status_line.last() {
242 Some(b'\r') => &status_line[..status_line.len() - 1],
243 _ => status_line,
244 }
245 };
246 let status = parse_status_line(status_line)?;
247
248 let mut headers: Vec<(String, String)> = Vec::new();
249 for (i, raw) in lines.iter().enumerate().skip(1) {
250 let line = if i < total - 1 {
251 match raw.last() {
252 Some(b'\r') => &raw[..raw.len() - 1],
253 _ => {
254 return Err(ClientError::InvalidHeader(
255 "bare LF in header line (protocol violation)".into(),
256 ))
257 }
258 }
259 } else {
260 match raw.last() {
261 Some(b'\r') => &raw[..raw.len() - 1],
262 _ => raw,
263 }
264 };
265 if line.is_empty() {
266 continue;
267 }
268 let colon = line
269 .iter()
270 .position(|&b| b == b':')
271 .ok_or_else(|| ClientError::InvalidHeader("missing colon".into()))?;
272 let name = std::str::from_utf8(&line[..colon])
273 .map_err(|_| ClientError::InvalidHeader("name not UTF-8".into()))?;
274 if !is_valid_token(name) {
275 return Err(ClientError::InvalidHeader(format!(
276 "invalid header name: {name:?}"
277 )));
278 }
279 if name.len() > MAX_HEADER_NAME_LEN {
281 return Err(ClientError::HeaderTooLarge);
282 }
283 let value_raw = &line[colon + 1..];
284 let mut start = 0;
286 let mut end = value_raw.len();
287 while start < end && matches!(value_raw[start], b' ' | b'\t') {
288 start += 1;
289 }
290 while end > start && matches!(value_raw[end - 1], b' ' | b'\t') {
291 end -= 1;
292 }
293 if end - start > MAX_HEADER_VALUE_LEN {
295 return Err(ClientError::HeaderTooLarge);
296 }
297 let value = std::str::from_utf8(&value_raw[start..end])
298 .map_err(|_| ClientError::InvalidHeader("value not UTF-8".into()))?;
299 headers.push((name.to_string(), value.to_string()));
300 if headers.len() > MAX_HEADER_COUNT {
302 return Err(ClientError::HeaderTooLarge);
303 }
304 }
305 Ok((status, headers))
306}
307
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
310enum BodyFraming {
311 Bodiless,
313 Chunked,
315 Length(usize),
317 UntilEof,
319}
320
321fn determine_framing(
323 status: u16,
324 headers: &[(String, String)],
325) -> Result<BodyFraming, ClientError> {
326 if matches!(status, 100..=199) || status == 204 || status == 304 {
328 return Ok(BodyFraming::Bodiless);
329 }
330
331 let mut content_lengths: Vec<usize> = Vec::new();
332 let mut chunked = false;
333 for (name, value) in headers {
334 if name.eq_ignore_ascii_case("content-length") {
335 let n: u64 = value
337 .trim()
338 .parse()
339 .map_err(|_| ClientError::InvalidHeader("content-length not a number".into()))?;
340 let n = usize::try_from(n)
341 .map_err(|_| ClientError::BodyTooLarge)?;
342 content_lengths.push(n);
343 } else if name.eq_ignore_ascii_case("transfer-encoding") {
344 for token in value.split(',') {
346 if token.trim().eq_ignore_ascii_case("chunked") {
347 chunked = true;
348 }
349 }
350 }
351 }
352
353 if chunked && !content_lengths.is_empty() {
355 return Err(ClientError::ProtocolInconsistency(
356 "content-length with transfer-encoding".into(),
357 ));
358 }
359 if let Some(first) = content_lengths.first()
361 && content_lengths.iter().any(|n| n != first)
362 {
363 return Err(ClientError::ProtocolInconsistency(
364 "conflicting content-length headers".into(),
365 ));
366 }
367
368 if chunked {
369 Ok(BodyFraming::Chunked)
370 } else if let Some(&len) = content_lengths.first() {
371 Ok(BodyFraming::Length(len))
372 } else {
373 Ok(BodyFraming::UntilEof)
374 }
375}
376
377pub fn parse_response(buf: &[u8]) -> Result<Option<(ClientResponse, usize)>, ClientError> {
388 let head_end = match find_header_end(buf) {
389 Some(pos) => pos,
390 None => {
391 if buf.len() > MAX_HEADER_SECTION {
393 return Err(ClientError::HeaderTooLarge);
394 }
395 return Ok(None);
396 }
397 };
398 if head_end.saturating_add(4) > MAX_HEADER_SECTION {
400 return Err(ClientError::HeaderTooLarge);
401 }
402 let (status, headers) = parse_head(&buf[..head_end])?;
403 let body_start = head_end.saturating_add(4);
404 let body_bytes = &buf[body_start..];
405
406 match determine_framing(status, &headers)? {
407 BodyFraming::Bodiless => Ok(Some((
408 ClientResponse {
409 status,
410 headers,
411 body: Vec::new(),
412 },
413 body_start,
414 ))),
415 BodyFraming::Length(len) => {
416 if len as u64 > MAX_BODY_BYTES {
418 return Err(ClientError::BodyTooLarge);
419 }
420 let need = body_start.checked_add(len).ok_or(ClientError::BodyTooLarge)?;
422 if buf.len() < need {
423 return Ok(None);
424 }
425 Ok(Some((
426 ClientResponse {
427 status,
428 headers,
429 body: buf[body_start..need].to_vec(),
430 },
431 need,
432 )))
433 }
434 BodyFraming::Chunked => {
435 let mut dec = ChunkedDecoder::new(MAX_BODY_BYTES);
439 let (out, consumed) = dec.feed(body_bytes).map_err(|e| match e {
440 Http1Error::BodyTooLarge => ClientError::BodyTooLarge,
441 other => ClientError::InvalidChunked(other.to_string()),
442 })?;
443 if dec.is_done() {
444 Ok(Some((
445 ClientResponse {
446 status,
447 headers,
448 body: out,
449 },
450 body_start.saturating_add(consumed),
451 )))
452 } else {
453 Ok(None)
454 }
455 }
456 BodyFraming::UntilEof => Ok(None),
458 }
459}
460
461#[derive(Debug)]
479pub struct ResponseParser {
480 head: Option<ParsedHead>,
482 chunked_decoder: Option<ChunkedDecoder>,
484 body_processed: usize,
486 chunked_output: Vec<u8>,
488}
489
490#[derive(Debug, Clone)]
491struct ParsedHead {
492 status: u16,
493 headers: Vec<(String, String)>,
494 body_start: usize,
495 framing: BodyFraming,
496}
497
498impl ResponseParser {
499 #[inline]
501 pub fn new() -> Self {
502 Self {
503 head: None,
504 chunked_decoder: None,
505 body_processed: 0,
506 chunked_output: Vec::new(),
507 }
508 }
509
510 pub fn feed(&mut self, buf: &[u8]) -> Result<Option<(ClientResponse, usize)>, ClientError> {
516 if self.head.is_none() {
518 let head_end = match find_header_end(buf) {
519 Some(pos) => pos,
520 None => {
521 if buf.len() > MAX_HEADER_SECTION {
522 return Err(ClientError::HeaderTooLarge);
523 }
524 return Ok(None);
525 }
526 };
527 if head_end.saturating_add(4) > MAX_HEADER_SECTION {
528 return Err(ClientError::HeaderTooLarge);
529 }
530 let (status, headers) = parse_head(&buf[..head_end])?;
531 let body_start = head_end.saturating_add(4);
532 let framing = determine_framing(status, &headers)?;
533 self.head = Some(ParsedHead {
534 status,
535 headers,
536 body_start,
537 framing,
538 });
539 }
540
541 let head = self.head.as_ref().expect("head just parsed");
542 let body_bytes = &buf[head.body_start..];
543
544 match head.framing {
545 BodyFraming::Bodiless => Ok(Some((
546 ClientResponse {
547 status: head.status,
548 headers: head.headers.clone(),
549 body: Vec::new(),
550 },
551 head.body_start,
552 ))),
553 BodyFraming::Length(len) => {
554 if len as u64 > MAX_BODY_BYTES {
555 return Err(ClientError::BodyTooLarge);
556 }
557 let need = head
558 .body_start
559 .checked_add(len)
560 .ok_or(ClientError::BodyTooLarge)?;
561 if buf.len() < need {
562 return Ok(None);
563 }
564 Ok(Some((
565 ClientResponse {
566 status: head.status,
567 headers: head.headers.clone(),
568 body: buf[head.body_start..need].to_vec(),
569 },
570 need,
571 )))
572 }
573 BodyFraming::Chunked => {
574 if self.chunked_decoder.is_none() {
576 self.chunked_decoder = Some(ChunkedDecoder::new(MAX_BODY_BYTES));
577 }
578 let decoder = self.chunked_decoder.as_mut().expect("just initialized");
579
580 let new_data = &body_bytes[self.body_processed..];
582 if !new_data.is_empty() {
583 let (out, consumed) = decoder
584 .feed(new_data)
585 .map_err(|e| match e {
586 Http1Error::BodyTooLarge => ClientError::BodyTooLarge,
587 other => ClientError::InvalidChunked(other.to_string()),
588 })?;
589 self.body_processed += consumed;
590 self.chunked_output.extend_from_slice(&out);
591
592 if decoder.is_done() {
593 let total_consumed = head.body_start + self.body_processed;
594 let body = std::mem::take(&mut self.chunked_output);
595 return Ok(Some((
596 ClientResponse {
597 status: head.status,
598 headers: head.headers.clone(),
599 body,
600 },
601 total_consumed,
602 )));
603 }
604 }
605 Ok(None)
606 }
607 BodyFraming::UntilEof => Ok(None),
608 }
609 }
610}
611
612impl Default for ResponseParser {
613 #[inline]
614 fn default() -> Self {
615 Self::new()
616 }
617}
618
619pub fn parse_response_eof(buf: &[u8]) -> Result<ClientResponse, ClientError> {
628 let head_end = find_header_end(buf).ok_or(ClientError::Truncated)?;
629 let (status, headers) = parse_head(&buf[..head_end])?;
630 let body_start = head_end.saturating_add(4);
631
632 match determine_framing(status, &headers)? {
633 BodyFraming::UntilEof => Ok(ClientResponse {
634 status,
635 headers,
636 body: buf[body_start..].to_vec(),
637 }),
638 _ => match parse_response(buf)? {
640 Some((resp, _)) => Ok(resp),
641 None => Err(ClientError::Truncated),
642 },
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
653 fn encode_basic_get() {
654 let out = encode_request("GET", "/api/x?a=1", "127.0.0.1:8080", &[], b"");
655 let out = match out {
656 Some(v) => v,
657 None => panic!("encode should succeed"),
658 };
659 let s = String::from_utf8_lossy(&out);
660 assert!(s.starts_with("GET /api/x?a=1 HTTP/1.1\r\n"));
661 assert!(s.contains("Host: 127.0.0.1:8080\r\n"));
662 assert!(s.contains("Content-Length: 0\r\n"));
663 assert!(s.contains("Connection: close\r\n"));
664 assert!(s.ends_with("\r\n\r\n"));
665 }
666
667 #[test]
668 fn encode_post_with_body_and_headers() {
669 let headers = vec![
670 ("content-type".to_string(), "application/json".to_string()),
671 ("x-token".to_string(), "abc".to_string()),
672 ];
673 let out = encode_request("POST", "/echo", "up:9000", &headers, b"hello");
674 let out = match out {
675 Some(v) => v,
676 None => panic!("encode should succeed"),
677 };
678 let s = String::from_utf8_lossy(&out);
679 assert!(s.starts_with("POST /echo HTTP/1.1\r\n"));
680 assert!(s.contains("content-type: application/json\r\n"));
681 assert!(s.contains("x-token: abc\r\n"));
682 assert!(s.contains("Content-Length: 5\r\n"));
683 assert!(s.ends_with("\r\n\r\nhello"));
684 }
685
686 #[test]
687 fn encode_rejects_crlf_injection_fail_closed() {
688 assert!(encode_request("GET\r\nEvil: x", "/", "h", &[], b"").is_none());
690 assert!(encode_request("GET", "/a\r\nb", "h", &[], b"").is_none());
691 assert!(encode_request("GET", "/", "ho\r\nst", &[], b"").is_none());
692 assert!(encode_request("GET", "/a\nb", "h", &[], b"").is_none());
693 let bad_value = vec![("x-bad".to_string(), "evil\r\nInjected: yes".to_string())];
695 assert!(encode_request("GET", "/", "h", &bad_value, b"").is_none());
696 let bad_name = vec![("x-bad\r\nInjected".to_string(), "v".to_string())];
697 assert!(encode_request("GET", "/", "h", &bad_name, b"").is_none());
698 assert!(encode_request("GE T", "/", "h", &[], b"").is_none());
700 assert!(encode_request("", "/", "h", &[], b"").is_none());
701 assert!(encode_request("GET", "", "h", &[], b"").is_none());
702 assert!(encode_request("GET", "/", "", &[], b"").is_none());
703 let empty_name = vec![("".to_string(), "v".to_string())];
704 assert!(encode_request("GET", "/", "h", &empty_name, b"").is_none());
705 }
706
707 #[test]
710 fn parse_content_length_response() {
711 let raw = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nhello";
712 let r = parse_response(raw);
713 let (resp, consumed) = match r {
714 Ok(Some(v)) => v,
715 other => panic!("expected complete response, got {other:?}"),
716 };
717 assert_eq!(resp.status, 200);
718 assert_eq!(resp.body, b"hello");
719 assert_eq!(consumed, raw.len());
720 assert_eq!(
721 resp.headers,
722 vec![
723 ("Content-Type".to_string(), "text/plain".to_string()),
724 ("Content-Length".to_string(), "5".to_string()),
725 ]
726 );
727 }
728
729 #[test]
730 fn parse_content_length_incremental_feed() {
731 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 11\r\n\r\nhello world";
733 for i in 0..raw.len() {
734 match parse_response(&raw[..i]) {
735 Ok(None) => {}
736 other => panic!("prefix {i} should be incomplete, got {other:?}"),
737 }
738 }
739 match parse_response(raw) {
740 Ok(Some((resp, consumed))) => {
741 assert_eq!(resp.body, b"hello world");
742 assert_eq!(consumed, raw.len());
743 }
744 other => panic!("full input should complete, got {other:?}"),
745 }
746 }
747
748 #[test]
749 fn parse_content_length_sticky_packet_consumed() {
750 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nhiNEXT-REQUEST-BYTES";
752 match parse_response(raw) {
753 Ok(Some((resp, consumed))) => {
754 assert_eq!(resp.body, b"hi");
755 assert_eq!(&raw[consumed..], b"NEXT-REQUEST-BYTES");
756 }
757 other => panic!("expected complete, got {other:?}"),
758 }
759 }
760
761 #[test]
762 fn parse_status_without_reason_phrase() {
763 let raw = b"HTTP/1.1 200\r\nContent-Length: 0\r\n\r\n";
764 match parse_response(raw) {
765 Ok(Some((resp, _))) => assert_eq!(resp.status, 200),
766 other => panic!("expected complete, got {other:?}"),
767 }
768 }
769
770 #[test]
771 fn parse_http10_response() {
772 let raw = b"HTTP/1.0 302 Found\r\nContent-Length: 2\r\n\r\nok";
773 match parse_response(raw) {
774 Ok(Some((resp, _))) => {
775 assert_eq!(resp.status, 302);
776 assert_eq!(resp.body, b"ok");
777 }
778 other => panic!("expected complete, got {other:?}"),
779 }
780 }
781
782 #[test]
783 fn parse_bodiless_status_complete_without_body() {
784 let raw = b"HTTP/1.1 204 No Content\r\nX-A: b\r\n\r\n";
786 match parse_response(raw) {
787 Ok(Some((resp, consumed))) => {
788 assert_eq!(resp.status, 204);
789 assert!(resp.body.is_empty());
790 assert_eq!(consumed, raw.len());
791 }
792 other => panic!("204 should complete immediately, got {other:?}"),
793 }
794 let raw304 = b"HTTP/1.1 304 Not Modified\r\n\r\n";
796 match parse_response(raw304) {
797 Ok(Some((resp, _))) => assert_eq!(resp.status, 304),
798 other => panic!("304 should complete immediately, got {other:?}"),
799 }
800 }
801
802 #[test]
803 fn parse_duplicate_identical_content_length_ok() {
804 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nContent-Length: 3\r\n\r\nabc";
805 match parse_response(raw) {
806 Ok(Some((resp, _))) => assert_eq!(resp.body, b"abc"),
807 other => panic!("expected complete, got {other:?}"),
808 }
809 }
810
811 #[test]
814 fn parse_chunked_response() {
815 let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
816 match parse_response(raw) {
817 Ok(Some((resp, consumed))) => {
818 assert_eq!(resp.status, 200);
819 assert_eq!(resp.body, b"hello world");
820 assert_eq!(consumed, raw.len());
821 }
822 other => panic!("expected complete, got {other:?}"),
823 }
824 }
825
826 #[test]
827 fn parse_chunked_with_trailer_ignored() {
828 let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n2\r\nhi\r\n0\r\nX-Trailer: v\r\n\r\n";
829 match parse_response(raw) {
830 Ok(Some((resp, _))) => assert_eq!(resp.body, b"hi"),
831 other => panic!("expected complete, got {other:?}"),
832 }
833 }
834
835 #[test]
836 fn parse_chunked_incremental_across_chunks() {
837 let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
839 let mut saw_complete_before_full = false;
840 for i in 0..raw.len() {
841 if let Ok(Some(_)) = parse_response(&raw[..i]) {
842 saw_complete_before_full = true;
843 }
844 }
845 assert!(!saw_complete_before_full, "chunked must not complete before terminator");
846 match parse_response(raw) {
847 Ok(Some((resp, _))) => assert_eq!(resp.body, b"hello world"),
848 other => panic!("expected complete, got {other:?}"),
849 }
850 }
851
852 #[test]
853 fn parse_chunked_uppercase_hex_and_extension() {
854 let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nA;ext=1\r\n0123456789\r\n0\r\n\r\n";
855 match parse_response(raw) {
856 Ok(Some((resp, _))) => assert_eq!(resp.body, b"0123456789"),
857 other => panic!("expected complete, got {other:?}"),
858 }
859 }
860
861 #[test]
864 fn parse_until_eof_waits_then_eof_completes() {
865 let raw = b"HTTP/1.1 200 OK\r\nX-A: b\r\n\r\nstreamed-body-until-close";
867 match parse_response(raw) {
868 Ok(None) => {}
869 other => panic!("until-eof framing must wait for EOF, got {other:?}"),
870 }
871 let resp = match parse_response_eof(raw) {
872 Ok(r) => r,
873 Err(e) => panic!("eof parse should succeed: {e}"),
874 };
875 assert_eq!(resp.status, 200);
876 assert_eq!(resp.body, b"streamed-body-until-close");
877 }
878
879 #[test]
880 fn parse_response_eof_rejects_truncated() {
881 assert!(matches!(
883 parse_response_eof(b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n"),
884 Err(ClientError::Truncated)
885 ));
886 assert!(matches!(
888 parse_response_eof(b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nshort"),
889 Err(ClientError::Truncated)
890 ));
891 assert!(matches!(
893 parse_response_eof(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhel"),
894 Err(ClientError::Truncated)
895 ));
896 assert!(matches!(parse_response_eof(b""), Err(ClientError::Truncated)));
898 }
899
900 #[test]
903 fn parse_rejects_bad_status_line() {
904 for raw in [
905 &b"NOTHTTP 200 OK\r\n\r\n"[..],
906 b"HTTP/2 200 OK\r\n\r\n",
907 b"HTTP/1.1 20 OK\r\n\r\n",
908 b"HTTP/1.1 abc OK\r\n\r\n",
909 b"HTTP/1.1 \r\n\r\n",
910 ] {
911 match parse_response(raw) {
912 Err(ClientError::InvalidStatusLine) => {}
913 other => panic!(
914 "should reject {:?}, got {other:?}",
915 String::from_utf8_lossy(raw)
916 ),
917 }
918 }
919 }
920
921 #[test]
922 fn parse_rejects_header_without_colon() {
923 let raw = b"HTTP/1.1 200 OK\r\nBadHeaderLine\r\n\r\n";
924 assert!(matches!(
925 parse_response(raw),
926 Err(ClientError::InvalidHeader(_))
927 ));
928 }
929
930 #[test]
931 fn parse_rejects_invalid_header_name() {
932 let raw = b"HTTP/1.1 200 OK\r\nBad Name: v\r\n\r\n";
933 assert!(matches!(
934 parse_response(raw),
935 Err(ClientError::InvalidHeader(_))
936 ));
937 }
938
939 #[test]
940 fn parse_rejects_bare_lf_in_status_line() {
941 let raw = b"HTTP/1.1 200 OK\nInjected: evil\r\nContent-Length: 0\r\n\r\n";
943 assert!(matches!(
944 parse_response(raw),
945 Err(ClientError::InvalidHeader(_))
946 ));
947 }
948
949 #[test]
950 fn parse_rejects_bare_lf_in_header_line() {
951 let raw = b"HTTP/1.1 200 OK\r\nX-A: b\nInjected: evil\r\nContent-Length: 0\r\n\r\n";
953 assert!(matches!(
954 parse_response(raw),
955 Err(ClientError::InvalidHeader(_))
956 ));
957 }
958
959 #[test]
960 fn parse_rejects_cl_and_te_coexist() {
961 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n\r\n";
962 assert!(matches!(
963 parse_response(raw),
964 Err(ClientError::ProtocolInconsistency(_))
965 ));
966 }
967
968 #[test]
969 fn parse_rejects_conflicting_content_length() {
970 let raw = b"HTTP/1.1 200 OK\r\nContent-Length: 3\r\nContent-Length: 4\r\n\r\nabcd";
971 assert!(matches!(
972 parse_response(raw),
973 Err(ClientError::ProtocolInconsistency(_))
974 ));
975 }
976
977 #[test]
978 fn parse_rejects_invalid_chunk_size() {
979 let raw = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nZZ\r\nx\r\n0\r\n\r\n";
980 assert!(matches!(
981 parse_response(raw),
982 Err(ClientError::InvalidChunked(_))
983 ));
984 }
985
986 #[test]
987 fn parse_rejects_oversized_header_section() {
988 let mut raw = Vec::new();
989 raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
990 while raw.len() <= MAX_HEADER_SECTION {
992 raw.extend_from_slice(b"X-Pad: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
993 }
994 assert!(matches!(
995 parse_response(&raw),
996 Err(ClientError::HeaderTooLarge)
997 ));
998 }
999
1000 #[test]
1003 fn parse_rejects_oversized_header_section_with_terminator() {
1004 let mut raw = Vec::new();
1007 raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
1008 while raw.len() <= MAX_HEADER_SECTION {
1009 raw.extend_from_slice(b"X-Pad: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\r\n");
1010 }
1011 raw.extend_from_slice(b"\r\n"); raw.extend_from_slice(b"ok");
1013 assert!(matches!(
1014 parse_response(&raw),
1015 Err(ClientError::HeaderTooLarge)
1016 ));
1017 assert!(matches!(
1019 parse_response_eof(&raw),
1020 Err(ClientError::HeaderTooLarge)
1021 ));
1022 }
1023
1024 #[test]
1025 fn parse_rejects_too_many_headers() {
1026 let mut raw = Vec::new();
1028 raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
1029 for i in 0..=MAX_HEADER_COUNT {
1030 raw.extend_from_slice(format!("X-H{i}: v\r\n").as_bytes());
1031 }
1032 raw.extend_from_slice(b"Content-Length: 0\r\n\r\n");
1033 assert!(matches!(
1034 parse_response(&raw),
1035 Err(ClientError::HeaderTooLarge)
1036 ));
1037 }
1038
1039 #[test]
1040 fn parse_accepts_header_count_at_limit() {
1041 let mut raw = Vec::new();
1043 raw.extend_from_slice(b"HTTP/1.1 200 OK\r\n");
1044 for i in 1..MAX_HEADER_COUNT {
1045 raw.extend_from_slice(format!("X-H{i}: v\r\n").as_bytes());
1046 }
1047 raw.extend_from_slice(b"Content-Length: 0\r\n\r\n");
1048 assert!(
1049 matches!(parse_response(&raw), Ok(Some(_))),
1050 "恰在上限内的条数必须放行"
1051 );
1052 }
1053
1054 #[test]
1055 fn parse_rejects_oversized_header_name() {
1056 let name = format!("X-{}", "A".repeat(MAX_HEADER_NAME_LEN));
1058 let raw = format!("HTTP/1.1 200 OK\r\n{name}: v\r\nContent-Length: 0\r\n\r\n");
1059 assert!(matches!(
1060 parse_response(raw.as_bytes()),
1061 Err(ClientError::HeaderTooLarge)
1062 ));
1063 }
1064
1065 #[test]
1066 fn parse_rejects_oversized_header_value() {
1067 let value = "v".repeat(MAX_HEADER_VALUE_LEN + 1);
1069 let raw = format!("HTTP/1.1 200 OK\r\nX-Pad: {value}\r\nContent-Length: 0\r\n\r\n");
1070 assert!(matches!(
1071 parse_response(raw.as_bytes()),
1072 Err(ClientError::HeaderTooLarge)
1073 ));
1074 }
1075
1076 #[test]
1077 fn parse_header_ows_trimmed() {
1078 let raw = b"HTTP/1.1 200 OK\r\nX-Pad: value \r\nContent-Length: 1\r\n\r\nx";
1079 match parse_response(raw) {
1080 Ok(Some((resp, _))) => {
1081 assert_eq!(resp.headers[0], ("X-Pad".to_string(), "value".to_string()))
1082 }
1083 other => panic!("expected complete, got {other:?}"),
1084 }
1085 }
1086
1087 #[test]
1090 fn error_display_impl() {
1091 let e = ClientError::InvalidStatusLine;
1092 assert!(!format!("{e}").is_empty());
1093 let e = ClientError::Truncated;
1094 assert!(format!("{e}").contains("truncated"));
1095 let _: &dyn std::error::Error = &ClientError::HeaderTooLarge;
1097 }
1098}