1use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
2
3use crate::error::HttpError;
4use eggress_core::{BoxStream, TargetAddr, TargetHost};
5
6pub struct BodyCopyLimits {
8 pub max_chunk_size_line: usize,
9 pub max_chunk_size: u64,
10 pub max_decoded_body: u64,
11 pub max_trailer_line: usize,
12 pub max_trailer_bytes: usize,
13}
14
15impl Default for BodyCopyLimits {
16 fn default() -> Self {
17 Self {
18 max_chunk_size_line: 1024,
19 max_chunk_size: 64 * 1024 * 1024,
20 max_decoded_body: 64 * 1024 * 1024,
21 max_trailer_line: 8192,
22 max_trailer_bytes: 32 * 1024,
23 }
24 }
25}
26
27#[derive(Debug, Default)]
29pub struct BodyCopyReport {
30 pub wire_bytes: u64,
31 pub decoded_bytes: u64,
32}
33
34#[derive(Debug, Default)]
36pub struct ForwardResponseReport {
37 pub bytes_forwarded: u64,
38}
39
40pub async fn copy_request_body<R, W>(
46 reader: &mut R,
47 writer: &mut W,
48 kind: RequestBodyKind,
49 limits: &BodyCopyLimits,
50) -> Result<BodyCopyReport, HttpError>
51where
52 R: AsyncRead + Unpin,
53 W: AsyncWrite + Unpin,
54{
55 match kind {
56 RequestBodyKind::None => Ok(BodyCopyReport::default()),
57 RequestBodyKind::ContentLength(len) => {
58 if len > limits.max_decoded_body {
59 return Err(HttpError::MalformedRequest("decoded body too large".into()));
60 }
61 copy_content_length_body(reader, writer, len).await
62 }
63 RequestBodyKind::Chunked => copy_chunked_body(reader, writer, limits).await,
64 }
65}
66
67async fn copy_content_length_body<R, W>(
68 reader: &mut R,
69 writer: &mut W,
70 len: u64,
71) -> Result<BodyCopyReport, HttpError>
72where
73 R: AsyncRead + Unpin,
74 W: AsyncWrite + Unpin,
75{
76 let mut remaining = len;
77 let mut buf = [0u8; 8192];
78 while remaining > 0 {
79 let to_read = (remaining as usize).min(buf.len());
80 let n = reader.read(&mut buf[..to_read]).await?;
81 if n == 0 {
82 return Err(HttpError::MalformedRequest("unexpected EOF in body".into()));
83 }
84 writer.write_all(&buf[..n]).await?;
85 remaining -= n as u64;
86 }
87 Ok(BodyCopyReport {
88 wire_bytes: len,
89 decoded_bytes: len,
90 })
91}
92
93async fn copy_chunked_body<R, W>(
94 reader: &mut R,
95 writer: &mut W,
96 limits: &BodyCopyLimits,
97) -> Result<BodyCopyReport, HttpError>
98where
99 R: AsyncRead + Unpin,
100 W: AsyncWrite + Unpin,
101{
102 let mut wire_bytes: u64 = 0;
103 let mut decoded_bytes: u64 = 0;
104
105 loop {
106 let size_line = read_bounded_line(reader, limits.max_chunk_size_line).await?;
108 wire_bytes += size_line.len() as u64;
109
110 let chunk_size = parse_chunk_size(&size_line)?;
112
113 writer.write_all(&size_line).await?;
115
116 if chunk_size == 0 {
117 let mut trailer_bytes: u64 = 0;
119 loop {
120 let trailer = read_bounded_line(reader, limits.max_trailer_line).await?;
121 wire_bytes += trailer.len() as u64;
122 trailer_bytes += trailer.len() as u64;
123
124 if trailer_bytes > limits.max_trailer_bytes as u64 {
125 return Err(HttpError::MalformedRequest("trailers too large".into()));
126 }
127
128 writer.write_all(&trailer).await?;
129
130 if trailer == b"\r\n" {
131 break;
132 }
133 }
134 break;
135 }
136
137 if chunk_size > limits.max_chunk_size {
139 return Err(HttpError::MalformedRequest("chunk too large".into()));
140 }
141
142 decoded_bytes = decoded_bytes
144 .checked_add(chunk_size)
145 .ok_or_else(|| HttpError::MalformedRequest("decoded body too large".into()))?;
146 if decoded_bytes > limits.max_decoded_body {
147 return Err(HttpError::MalformedRequest("decoded body too large".into()));
148 }
149
150 let mut remaining = chunk_size;
152 let mut buf = [0u8; 8192];
153 while remaining > 0 {
154 let to_read = (remaining as usize).min(buf.len());
155 let n = reader.read(&mut buf[..to_read]).await?;
156 if n == 0 {
157 return Err(HttpError::MalformedRequest(
158 "unexpected EOF in chunk data".into(),
159 ));
160 }
161 writer.write_all(&buf[..n]).await?;
162 remaining -= n as u64;
163 wire_bytes += n as u64;
164 }
165
166 let mut crlf = [0u8; 2];
168 reader.read_exact(&mut crlf).await?;
169 wire_bytes += 2;
170 if crlf != *b"\r\n" {
171 return Err(HttpError::MalformedRequest(
172 "missing CRLF after chunk data".into(),
173 ));
174 }
175 writer.write_all(&crlf).await?;
176 }
177
178 Ok(BodyCopyReport {
179 wire_bytes,
180 decoded_bytes,
181 })
182}
183
184async fn read_bounded_line<R: AsyncRead + Unpin>(
186 reader: &mut R,
187 max_len: usize,
188) -> Result<Vec<u8>, HttpError> {
189 let mut line = Vec::new();
190 let mut temp = [0u8; 1];
191 loop {
192 if line.len() >= max_len {
193 return Err(HttpError::MalformedRequest("line too long".into()));
194 }
195 let n = reader.read(&mut temp).await?;
196 if n == 0 {
197 if line.is_empty() {
198 return Err(HttpError::MalformedRequest("unexpected EOF".into()));
199 }
200 return Err(HttpError::MalformedRequest("incomplete line".into()));
201 }
202 line.push(temp[0]);
203 if line.len() >= 2 && &line[line.len() - 2..] == b"\r\n" {
204 break;
205 }
206 }
207 Ok(line)
208}
209
210async fn read_bounded_line_into<R: AsyncRead + Unpin>(
211 reader: &mut R,
212 line: &mut Vec<u8>,
213 max_len: usize,
214) -> Result<(), HttpError> {
215 let mut temp = [0u8; 1];
216 loop {
217 if line.len() >= max_len {
218 return Err(HttpError::MalformedResponse("line too long".into()));
219 }
220 let n = reader.read(&mut temp).await?;
221 if n == 0 {
222 if line.is_empty() {
223 return Err(HttpError::MalformedResponse("unexpected EOF".into()));
224 }
225 return Err(HttpError::MalformedResponse("incomplete line".into()));
226 }
227 line.push(temp[0]);
228 if line.len() >= 2 && &line[line.len() - 2..] == b"\r\n" {
229 break;
230 }
231 }
232 Ok(())
233}
234
235fn parse_chunk_size(line_without_crlf: &[u8]) -> Result<u64, HttpError> {
238 let size_field = line_without_crlf
239 .split(|b| *b == b';')
240 .next()
241 .ok_or_else(|| HttpError::MalformedRequest("empty chunk size".into()))?;
242
243 if size_field.is_empty() {
244 return Err(HttpError::MalformedRequest("empty chunk size".into()));
245 }
246
247 let size_str = std::str::from_utf8(size_field)
248 .map_err(|_| HttpError::MalformedRequest("invalid chunk size encoding".into()))?;
249 let size_str = size_str.trim();
250
251 u64::from_str_radix(size_str, 16)
252 .map_err(|_| HttpError::MalformedRequest("invalid chunk size".into()))
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum RequestBodyKind {
258 None,
259 ContentLength(u64),
260 Chunked,
261}
262
263pub fn determine_request_body_kind(
270 headers: &[(String, String)],
271) -> Result<RequestBodyKind, HttpError> {
272 let mut content_lengths: Vec<u64> = Vec::new();
273 let mut transfer_encodings: Vec<String> = Vec::new();
274
275 for (name, value) in headers {
276 if name.eq_ignore_ascii_case("Content-Length") {
277 let len = value
279 .trim()
280 .parse::<u64>()
281 .map_err(|_| HttpError::InvalidContentLength)?;
282 content_lengths.push(len);
283 } else if name.eq_ignore_ascii_case("Transfer-Encoding") {
284 for coding in value.split(',') {
286 let coding = coding.trim().to_string();
287 if !coding.is_empty() {
288 transfer_encodings.push(coding);
289 }
290 }
291 }
292 }
293
294 if !content_lengths.is_empty() {
296 let first = content_lengths[0];
298 if content_lengths.iter().any(|&cl| cl != first) {
299 return Err(HttpError::ConflictingContentLength);
300 }
301 }
302
303 if !transfer_encodings.is_empty() {
305 if !content_lengths.is_empty() {
307 return Err(HttpError::TransferEncodingWithContentLength);
308 }
309
310 let has_chunked = transfer_encodings
312 .iter()
313 .any(|c| c.eq_ignore_ascii_case("chunked"));
314 if has_chunked {
315 let last = transfer_encodings.last().unwrap();
316 if !last.eq_ignore_ascii_case("chunked") {
317 return Err(HttpError::ChunkedNotFinal);
318 }
319 }
320
321 for coding in &transfer_encodings {
323 if !coding.eq_ignore_ascii_case("chunked") {
324 return Err(HttpError::UnsupportedTransferEncoding(coding.clone()));
325 }
326 }
327
328 return Ok(RequestBodyKind::Chunked);
329 }
330
331 if let Some(len) = content_lengths.first() {
332 Ok(RequestBodyKind::ContentLength(*len))
333 } else {
334 Ok(RequestBodyKind::None)
335 }
336}
337
338const MAX_HEAD_SIZE: usize = 32 * 1024;
340
341const MAX_RESPONSE_HEAD_SIZE: usize = 32 * 1024;
343
344const MAX_INFORMATIONAL_RESPONSES: usize = 8;
347
348const MAX_HEADER_LINES: usize = 128;
350
351const MAX_RESPONSE_CHUNK_SIZE: u64 = 64 * 1024 * 1024;
353
354const MAX_TRAILER_BYTES: usize = 64 * 1024;
357
358fn is_hop_by_hop_header(name: &str, value: &str) -> bool {
363 let lower = name.to_ascii_lowercase();
364 match lower.as_str() {
365 "transfer-encoding" => !value.eq_ignore_ascii_case("chunked"),
366 _ => matches!(
367 lower.as_str(),
368 "connection"
369 | "keep-alive"
370 | "proxy-authenticate"
371 | "proxy-authorization"
372 | "te"
373 | "trailers"
374 | "upgrade"
375 | "proxy-connection"
376 ),
377 }
378}
379
380fn connection_tokens(headers: &[(String, String)]) -> std::collections::HashSet<String> {
385 headers
386 .iter()
387 .filter(|(name, _)| name.eq_ignore_ascii_case("connection"))
388 .flat_map(|(_, value)| value.split(','))
389 .map(|token| token.trim().to_ascii_lowercase())
390 .filter(|token| !token.is_empty())
391 .collect()
392}
393
394pub fn filter_hop_by_hop(headers: &[(String, String)]) -> Vec<(String, String)> {
399 let nominated = connection_tokens(headers);
400 headers
401 .iter()
402 .filter(|(name, value)| {
403 let lower = name.to_ascii_lowercase();
404 !is_hop_by_hop_header(&lower, value) && !nominated.contains(&lower)
405 })
406 .cloned()
407 .collect()
408}
409
410pub fn has_unsupported_expectation(headers: &[(String, String)]) -> bool {
414 headers.iter().any(|(name, value)| {
415 name.eq_ignore_ascii_case("Expect")
416 && value
417 .split(',')
418 .any(|expectation| !expectation.trim().is_empty())
419 })
420}
421
422pub fn build_origin_request(request: &ForwardRequest) -> String {
429 let filtered = filter_hop_by_hop(&request.headers);
430
431 let mut req = format!(
432 "{} {} {}\r\n",
433 request.method, request.path, request.version
434 );
435
436 for (name, value) in &filtered {
437 req.push_str(&format!("{}: {}\r\n", name, value));
438 }
439
440 if !filtered
442 .iter()
443 .any(|(n, _)| n.eq_ignore_ascii_case("Connection"))
444 {
445 req.push_str("Connection: close\r\n");
446 }
447
448 req.push_str("\r\n");
449 req
450}
451
452#[derive(Debug)]
454pub struct ForwardResponse {
455 pub version: String,
456 pub status: u16,
457 pub reason: String,
458 pub headers: Vec<(String, String)>,
459 pub content_length: Option<u64>,
460 pub is_chunked: bool,
461 pub connection_close: bool,
463}
464
465async fn read_response_head<R: AsyncRead + Unpin>(
471 stream: &mut R,
472) -> Result<ForwardResponse, HttpError> {
473 let mut head_buf = Vec::with_capacity(1024);
474 let mut temp = [0u8; 1];
475 let mut crlf_count: usize = 0;
479
480 loop {
481 if head_buf.len() >= MAX_RESPONSE_HEAD_SIZE {
482 return Err(HttpError::HeaderTooLarge);
483 }
484
485 let n = stream.read(&mut temp).await?;
486 if n == 0 {
487 return Err(HttpError::MalformedResponse(
488 "unexpected EOF reading response".into(),
489 ));
490 }
491
492 head_buf.push(temp[0]);
493
494 if head_buf.len() >= 2 {
495 let len = head_buf.len();
496 if &head_buf[len - 2..] == b"\r\n" {
497 crlf_count += 1;
498 if crlf_count > MAX_HEADER_LINES + 2 {
501 return Err(HttpError::TooManyHeaders);
502 }
503 }
504 }
505
506 if head_buf.len() >= 4 {
507 let len = head_buf.len();
508 if &head_buf[len - 4..] == b"\r\n\r\n" {
509 break;
510 }
511 }
512 }
513
514 let head_str = String::from_utf8_lossy(&head_buf);
515 let mut lines = head_str.split("\r\n");
516
517 let status_line = lines
519 .next()
520 .ok_or_else(|| HttpError::MalformedResponse("empty response".into()))?;
521
522 let parts: Vec<&str> = status_line.split_whitespace().collect();
523 if parts.len() < 2 {
524 return Err(HttpError::MalformedResponse(format!(
525 "invalid status line: {}",
526 status_line
527 )));
528 }
529
530 let version = parts[0].to_string();
531 let status: u16 = parts[1]
532 .parse()
533 .map_err(|e| HttpError::MalformedResponse(format!("invalid status code: {}", e)))?;
534 let reason = parts.get(2).unwrap_or(&"").to_string();
535
536 let mut headers = Vec::new();
538 let mut content_length = None;
539 let mut is_chunked = false;
540 let mut connection_close = false;
541
542 let mut header_count = 0;
543 for line in lines {
544 if line.is_empty() {
545 break;
546 }
547 header_count += 1;
548 if header_count > MAX_HEADER_LINES {
549 return Err(HttpError::TooManyHeaders);
550 }
551 if let Some((name, value)) = parse_header_line(line) {
552 if name.eq_ignore_ascii_case("Content-Length") {
553 let parsed = value
554 .parse::<u64>()
555 .map_err(|_| HttpError::InvalidContentLength)?;
556 if content_length.is_some_and(|previous| previous != parsed) {
557 return Err(HttpError::ConflictingContentLength);
558 }
559 content_length = Some(parsed);
560 } else if name.eq_ignore_ascii_case("Transfer-Encoding") {
561 for coding in value.split(',') {
562 let coding_name = coding.trim().split(';').next().unwrap_or("").trim();
563 if coding_name.eq_ignore_ascii_case("chunked") {
564 is_chunked = true;
565 }
566 }
567 } else if name.eq_ignore_ascii_case("Connection") {
568 connection_close = value
570 .split(',')
571 .any(|t| t.trim().eq_ignore_ascii_case("close"));
572 }
573 headers.push((name, value));
574 }
575 }
576
577 if is_chunked {
581 content_length = None;
582 }
583
584 Ok(ForwardResponse {
585 version,
586 status,
587 reason,
588 headers,
589 content_length,
590 is_chunked,
591 connection_close,
592 })
593}
594
595fn format_response_head(
596 response: &ForwardResponse,
597 force_close: bool,
598) -> Result<String, HttpError> {
599 let filtered = filter_hop_by_hop(&response.headers);
600 if !response
601 .reason
602 .bytes()
603 .all(|byte| (0x20..=0x7e).contains(&byte))
604 {
605 return Err(HttpError::MalformedResponse(
606 "response reason contains non-printable bytes".into(),
607 ));
608 }
609 let mut head = format!("HTTP/1.1 {} {}\r\n", response.status, response.reason);
610
611 for (name, value) in &filtered {
612 if name.contains(['\r', '\n']) || value.contains(['\r', '\n']) {
613 return Err(HttpError::MalformedResponse(
614 "response header contains a line break".into(),
615 ));
616 }
617 head.push_str(&format!("{}: {}\r\n", name, value));
618 }
619
620 if force_close
621 && !filtered
622 .iter()
623 .any(|(n, _)| n.eq_ignore_ascii_case("Connection"))
624 {
625 head.push_str("Connection: close\r\n");
626 }
627
628 head.push_str("\r\n");
629 Ok(head)
630}
631
632pub struct ForwardResult {
634 pub report: ForwardResponseReport,
635 pub status: u16,
637 pub upstream_alive: bool,
639 pub client_should_close: bool,
641}
642
643pub async fn forward_response(
648 upstream: &mut BoxStream,
649 client: &mut BoxStream,
650) -> Result<ForwardResult, HttpError> {
651 let mut upstream_buf = tokio::io::BufReader::new(&mut *upstream);
655 let mut informational_responses = 0;
656 let mut bytes_forwarded: u64 = 0;
657 let response = loop {
658 let response = read_response_head(&mut upstream_buf).await?;
659 if response.status == 101 {
660 return Err(HttpError::UpgradeUnsupported);
661 }
662 if (100..200).contains(&response.status) {
663 informational_responses += 1;
664 if informational_responses > MAX_INFORMATIONAL_RESPONSES {
665 return Err(HttpError::TooManyInformationalResponses);
666 }
667 let head = format_response_head(&response, false)?;
668 client.write_all(head.as_bytes()).await?;
669 bytes_forwarded += head.len() as u64;
670 continue;
671 }
672 break response;
673 };
674 let head = format_response_head(&response, true)?;
675 client.write_all(head.as_bytes()).await?;
676 bytes_forwarded += head.len() as u64;
677
678 let mut eof_framing = false;
680 match (response.content_length, response.is_chunked) {
681 (Some(len), _) => {
682 let mut remaining = len;
683 let mut buf = [0u8; 8192];
684 while remaining > 0 {
685 let to_read = (remaining as usize).min(buf.len());
686 let n = upstream_buf.read(&mut buf[..to_read]).await?;
687 if n == 0 {
688 return Err(HttpError::MalformedResponse(
689 "unexpected EOF in response body".into(),
690 ));
691 }
692 client.write_all(&buf[..n]).await?;
693 bytes_forwarded += n as u64;
694 remaining -= n as u64;
695 }
696 }
697 (None, true) => {
698 let mut size_line_buf = Vec::new();
699 loop {
700 size_line_buf.clear();
701 read_bounded_line_into(&mut upstream_buf, &mut size_line_buf, 1024).await?;
702
703 let size_str = String::from_utf8_lossy(&size_line_buf);
704 let size_str = size_str.trim_end_matches("\r\n");
705 let size_str = size_str.split(';').next().unwrap_or("").trim();
706 let chunk_size = u64::from_str_radix(size_str, 16).map_err(|e| {
707 HttpError::MalformedResponse(format!("invalid chunk size: {}", e))
708 })?;
709 if chunk_size > MAX_RESPONSE_CHUNK_SIZE {
710 return Err(HttpError::MalformedResponse(
711 "response chunk too large".into(),
712 ));
713 }
714
715 client.write_all(&size_line_buf).await?;
716 bytes_forwarded += size_line_buf.len() as u64;
717
718 if chunk_size == 0 {
719 let mut trailer_total = 0usize;
720 loop {
721 let mut trailer = Vec::new();
722 read_bounded_line_into(&mut upstream_buf, &mut trailer, 8192).await?;
723 trailer_total += trailer.len();
724 if trailer_total > MAX_TRAILER_BYTES {
725 return Err(HttpError::MalformedResponse(
726 "response trailers exceed maximum total size".into(),
727 ));
728 }
729 client.write_all(&trailer).await?;
730 bytes_forwarded += trailer.len() as u64;
731 if trailer == b"\r\n" {
732 break;
733 }
734 if !trailer.ends_with(b"\r\n") {
735 break;
736 }
737 }
738 break;
739 }
740
741 let mut remaining =
742 usize::try_from(chunk_size.checked_add(2).ok_or_else(|| {
743 HttpError::MalformedResponse("response chunk size overflow".into())
744 })?)
745 .map_err(|_| HttpError::MalformedResponse("response chunk too large".into()))?;
746 let mut buf = [0u8; 8192];
747 while remaining > 0 {
748 let to_read = remaining.min(buf.len());
749 let n = upstream_buf.read(&mut buf[..to_read]).await?;
750 if n == 0 {
751 return Ok(ForwardResult {
752 report: ForwardResponseReport { bytes_forwarded },
753 status: response.status,
754 upstream_alive: false,
755 client_should_close: true,
756 });
757 }
758 client.write_all(&buf[..n]).await?;
759 bytes_forwarded += n as u64;
760 remaining -= n;
761 }
762 }
763 }
764 (None, false) => {
765 eof_framing = true;
768 let mut buf = [0u8; 8192];
769 loop {
770 let n = upstream_buf.read(&mut buf).await?;
771 if n == 0 {
772 break;
773 }
774 client.write_all(&buf[..n]).await?;
775 bytes_forwarded += n as u64;
776 }
777 }
778 }
779
780 let mut upstream_alive = if response.connection_close {
782 false
783 } else if response.version.contains("1.1") {
784 true
785 } else {
786 response
788 .headers
789 .iter()
790 .any(|(n, v)| n.eq_ignore_ascii_case("Keep-Alive") && !v.is_empty())
791 };
792 if eof_framing {
793 upstream_alive = false;
795 }
796
797 let client_should_close = response.connection_close;
799
800 Ok(ForwardResult {
801 report: ForwardResponseReport { bytes_forwarded },
802 status: response.status,
803 upstream_alive,
804 client_should_close,
805 })
806}
807
808#[derive(Debug, Clone)]
810pub struct ForwardRequest {
811 pub method: String,
812 pub path: String,
813 pub version: String,
814 pub headers: Vec<(String, String)>,
815 pub target: TargetAddr,
816 pub has_body: bool,
817 pub content_length: Option<u64>,
818 pub is_chunked: bool,
819 pub connection_close: bool,
821}
822
823impl ForwardRequest {
824 pub fn body_kind(&self) -> RequestBodyKind {
826 if self.is_chunked {
827 RequestBodyKind::Chunked
828 } else if let Some(len) = self.content_length {
829 RequestBodyKind::ContentLength(len)
830 } else {
831 RequestBodyKind::None
832 }
833 }
834}
835
836pub async fn forward_request(stream: BoxStream) -> Result<(ForwardRequest, BoxStream), HttpError> {
847 let mut stream: BoxStream = Box::new(tokio::io::BufReader::new(stream));
851 let request = read_forward_request(&mut stream).await?;
852 Ok((request, stream))
853}
854
855pub async fn forward_request_stream(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
860 read_forward_request(stream).await
861}
862
863async fn read_forward_request(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
865 let mut head_buf = Vec::with_capacity(1024);
866 let mut temp = [0u8; 1];
867 let mut header_count = 0;
868 let mut saw_request_line = false;
869
870 loop {
871 if head_buf.len() >= MAX_HEAD_SIZE {
872 return Err(HttpError::HeaderTooLarge);
873 }
874
875 let n = stream.read(&mut temp).await?;
876 if n == 0 {
877 return Err(HttpError::MalformedRequest(
878 "unexpected EOF reading request".into(),
879 ));
880 }
881
882 head_buf.push(temp[0]);
883
884 if head_buf.len() >= 4 {
886 let len = head_buf.len();
887 if &head_buf[len - 4..] == b"\r\n\r\n" {
888 break;
889 }
890 if head_buf.len() >= 2 && &head_buf[len - 2..] == b"\r\n" {
891 if saw_request_line {
895 header_count += 1;
896 } else {
897 saw_request_line = true;
898 }
899 if header_count > MAX_HEADER_LINES {
900 return Err(HttpError::TooManyHeaders);
901 }
902 }
903 }
904 }
905
906 let head_str = String::from_utf8_lossy(&head_buf);
907 let mut lines = head_str.split("\r\n");
908
909 let request_line = lines
911 .next()
912 .ok_or_else(|| HttpError::MalformedRequest("empty request".into()))?;
913
914 let parts: Vec<&str> = request_line.split_whitespace().collect();
915 if parts.len() != 3 {
916 return Err(HttpError::MalformedRequest(format!(
917 "expected 3 parts in request line, got {}",
918 parts.len()
919 )));
920 }
921
922 let method = parts[0].to_string();
923 let raw_target = parts[1].to_string();
924 let version = parts[2].to_string();
925 if version != "HTTP/1.0" && version != "HTTP/1.1" {
926 return Err(HttpError::MalformedRequest(format!(
927 "unsupported HTTP version: {version}"
928 )));
929 }
930
931 let (target, path) = parse_absolute_uri(&raw_target)?;
933
934 let mut headers = Vec::new();
936
937 for line in lines {
938 if line.is_empty() {
939 break;
940 }
941 if let Some((name, value)) = parse_header_line(line) {
942 if name.eq_ignore_ascii_case("Proxy-Authorization") {
944 continue;
945 }
946
947 headers.push((name, value));
948 }
949 }
950
951 let body_kind = determine_request_body_kind(&headers)?;
953 let (has_body, content_length, is_chunked) = match body_kind {
954 RequestBodyKind::None => (false, None, false),
955 RequestBodyKind::ContentLength(len) => (len > 0, Some(len), false),
956 RequestBodyKind::Chunked => (true, None, true),
957 };
958
959 let connection_close = headers.iter().any(|(n, v)| {
961 n.eq_ignore_ascii_case("Connection")
962 && v.split(',').any(|t| t.trim().eq_ignore_ascii_case("close"))
963 });
964
965 Ok(ForwardRequest {
966 method,
967 path,
968 version,
969 headers,
970 target,
971 has_body,
972 content_length,
973 is_chunked,
974 connection_close,
975 })
976}
977
978fn parse_absolute_uri(uri: &str) -> Result<(TargetAddr, String), HttpError> {
982 let (rest, default_port) = if let Some(stripped) = uri.strip_prefix("http://") {
984 (stripped, 80)
985 } else if let Some(stripped) = uri.strip_prefix("https://") {
986 (stripped, 443)
988 } else {
989 return Err(HttpError::MalformedRequest(format!(
990 "absolute URI required, got: {}",
991 uri
992 )));
993 };
994
995 let path_start = rest.find('/').unwrap_or(rest.len());
997 let authority = &rest[..path_start];
998 let path = if path_start < rest.len() {
999 &rest[path_start..]
1000 } else {
1001 "/"
1002 };
1003
1004 let target = parse_authority_with_default(authority, default_port)?;
1006
1007 Ok((target, path.to_string()))
1008}
1009
1010fn parse_authority_with_default(
1012 authority: &str,
1013 default_port: u16,
1014) -> Result<TargetAddr, HttpError> {
1015 if authority.starts_with('[') {
1017 let bracket_end = authority.find(']').ok_or_else(|| {
1018 HttpError::TargetParseError("unclosed bracket in IPv6 address".into())
1019 })?;
1020
1021 let ip_str = &authority[1..bracket_end];
1022 let ip: std::net::IpAddr = ip_str
1023 .parse()
1024 .map_err(|e| HttpError::TargetParseError(format!("invalid IPv6 address: {}", e)))?;
1025
1026 let port = if authority
1028 .as_bytes()
1029 .get(bracket_end + 1)
1030 .is_some_and(|&b| b == b':')
1031 {
1032 let port_str = authority.get(bracket_end + 2..).ok_or_else(|| {
1033 HttpError::TargetParseError("missing port after IPv6 address".into())
1034 })?;
1035 port_str
1036 .parse()
1037 .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?
1038 } else {
1039 default_port
1040 };
1041
1042 return Ok(TargetAddr {
1043 host: TargetHost::Ip(ip),
1044 port,
1045 });
1046 }
1047
1048 let colon_pos = authority.rfind(':');
1050
1051 let (host_str, port) = if let Some(colon_pos) = colon_pos {
1052 let host_str = &authority[..colon_pos];
1053 let port_str = &authority[colon_pos + 1..];
1054 let port: u16 = port_str
1055 .parse()
1056 .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?;
1057 (host_str, port)
1058 } else {
1059 (authority, default_port)
1060 };
1061
1062 if let Ok(ip) = host_str.parse::<std::net::IpAddr>() {
1064 return Ok(TargetAddr {
1065 host: TargetHost::Ip(ip),
1066 port,
1067 });
1068 }
1069
1070 if host_str.is_empty() {
1072 return Err(HttpError::TargetParseError("empty host".into()));
1073 }
1074
1075 Ok(TargetAddr {
1076 host: TargetHost::Domain(host_str.to_string()),
1077 port,
1078 })
1079}
1080
1081fn parse_header_line(line: &str) -> Option<(String, String)> {
1086 let colon_pos = line.find(':')?;
1087 let name = line[..colon_pos].trim().to_string();
1088 let value = line[colon_pos + 1..].trim().to_string();
1089
1090 if name.is_empty() {
1091 return None;
1092 }
1093 if name.bytes().any(|b| b == b'\0' || b == b'\r' || b == b'\n') {
1094 return None;
1095 }
1096 if value
1097 .bytes()
1098 .any(|b| b == b'\0' || b == b'\r' || b == b'\n')
1099 {
1100 return None;
1101 }
1102
1103 Some((name, value))
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108 use super::*;
1109
1110 #[test]
1111 fn test_parse_absolute_uri() {
1112 let (target, path) = parse_absolute_uri("http://example.com:8080/path").unwrap();
1113 assert_eq!(
1114 target,
1115 TargetAddr {
1116 host: TargetHost::Domain("example.com".to_string()),
1117 port: 8080,
1118 }
1119 );
1120 assert_eq!(path, "/path");
1121 }
1122
1123 #[test]
1124 fn test_parse_absolute_uri_no_path() {
1125 let (target, path) = parse_absolute_uri("http://example.com:80").unwrap();
1126 assert_eq!(
1127 target,
1128 TargetAddr {
1129 host: TargetHost::Domain("example.com".to_string()),
1130 port: 80,
1131 }
1132 );
1133 assert_eq!(path, "/");
1134 }
1135
1136 #[test]
1137 fn test_parse_absolute_uri_ipv4() {
1138 let (target, path) = parse_absolute_uri("http://192.168.1.1:3000/api").unwrap();
1139 assert_eq!(
1140 target,
1141 TargetAddr {
1142 host: TargetHost::Ip("192.168.1.1".parse().unwrap()),
1143 port: 3000,
1144 }
1145 );
1146 assert_eq!(path, "/api");
1147 }
1148
1149 #[test]
1150 fn test_parse_absolute_uri_no_scheme() {
1151 assert!(parse_absolute_uri("example.com/path").is_err());
1152 }
1153
1154 #[test]
1155 fn test_parse_header_line() {
1156 let (name, value) = parse_header_line("Content-Type: text/html").unwrap();
1157 assert_eq!(name, "Content-Type");
1158 assert_eq!(value, "text/html");
1159 }
1160
1161 #[test]
1162 fn test_parse_header_line_no_colon() {
1163 assert!(parse_header_line("NoColon").is_none());
1164 }
1165
1166 #[test]
1167 fn test_filter_hop_by_hop_connection_nominated() {
1168 let headers = vec![
1169 ("Connection".into(), "X-Custom, Keep-Alive".into()),
1170 ("X-Custom".into(), "value".into()),
1171 ("Keep-Alive".into(), "timeout=5".into()),
1172 ("Content-Type".into(), "text/html".into()),
1173 ];
1174 let filtered = filter_hop_by_hop(&headers);
1175 assert_eq!(filtered.len(), 1);
1178 assert_eq!(filtered[0].0, "Content-Type");
1179 }
1180
1181 #[test]
1182 fn test_filter_hop_by_hop_preserves_transfer_encoding_chunked() {
1183 let headers = vec![
1184 ("Transfer-Encoding".into(), "chunked".into()),
1185 ("Content-Type".into(), "application/json".into()),
1186 ];
1187 let filtered = filter_hop_by_hop(&headers);
1188 assert_eq!(filtered.len(), 2);
1189 assert!(filtered.iter().any(|(n, _)| n == "Transfer-Encoding"));
1190 }
1191
1192 #[test]
1193 fn test_filter_hop_by_hop_removes_transfer_encoding_non_chunked() {
1194 let headers = vec![
1195 ("Transfer-Encoding".into(), "gzip".into()),
1196 ("Content-Type".into(), "text/html".into()),
1197 ];
1198 let filtered = filter_hop_by_hop(&headers);
1199 assert_eq!(filtered.len(), 1);
1200 assert_eq!(filtered[0].0, "Content-Type");
1201 }
1202
1203 #[test]
1204 fn test_filter_connection_tokens_empty() {
1205 let headers = vec![("Content-Type".into(), "text/html".into())];
1206 let tokens = connection_tokens(&headers);
1207 assert!(tokens.is_empty());
1208 }
1209
1210 #[test]
1211 fn test_filter_connection_tokens_multiple() {
1212 let headers = vec![("Connection".into(), "close, Upgrade".into())];
1213 let tokens = connection_tokens(&headers);
1214 assert!(tokens.contains("close"));
1215 assert!(tokens.contains("upgrade"));
1216 }
1217
1218 #[test]
1219 fn test_determine_body_none() {
1220 let headers = vec![("Host".into(), "example.com".into())];
1221 assert_eq!(
1222 determine_request_body_kind(&headers).unwrap(),
1223 RequestBodyKind::None
1224 );
1225 }
1226
1227 #[test]
1228 fn test_determine_body_content_length() {
1229 let headers = vec![("Content-Length".into(), "42".into())];
1230 assert_eq!(
1231 determine_request_body_kind(&headers).unwrap(),
1232 RequestBodyKind::ContentLength(42)
1233 );
1234 }
1235
1236 #[test]
1237 fn test_determine_body_duplicate_equal_cl() {
1238 let headers = vec![
1239 ("Content-Length".into(), "42".into()),
1240 ("Content-Length".into(), "42".into()),
1241 ];
1242 assert_eq!(
1243 determine_request_body_kind(&headers).unwrap(),
1244 RequestBodyKind::ContentLength(42)
1245 );
1246 }
1247
1248 #[test]
1249 fn test_determine_body_conflicting_cl() {
1250 let headers = vec![
1251 ("Content-Length".into(), "42".into()),
1252 ("Content-Length".into(), "100".into()),
1253 ];
1254 assert!(matches!(
1255 determine_request_body_kind(&headers),
1256 Err(HttpError::ConflictingContentLength)
1257 ));
1258 }
1259
1260 #[test]
1261 fn test_determine_body_invalid_cl() {
1262 let headers = vec![("Content-Length".into(), "abc".into())];
1263 assert!(matches!(
1264 determine_request_body_kind(&headers),
1265 Err(HttpError::InvalidContentLength)
1266 ));
1267 }
1268
1269 #[test]
1270 fn test_determine_body_chunked() {
1271 let headers = vec![("Transfer-Encoding".into(), "chunked".into())];
1272 assert_eq!(
1273 determine_request_body_kind(&headers).unwrap(),
1274 RequestBodyKind::Chunked
1275 );
1276 }
1277
1278 #[test]
1279 fn test_determine_body_te_plus_cl() {
1280 let headers = vec![
1281 ("Transfer-Encoding".into(), "chunked".into()),
1282 ("Content-Length".into(), "42".into()),
1283 ];
1284 assert!(matches!(
1285 determine_request_body_kind(&headers),
1286 Err(HttpError::TransferEncodingWithContentLength)
1287 ));
1288 }
1289
1290 #[test]
1291 fn test_determine_body_unsupported_te() {
1292 let headers = vec![("Transfer-Encoding".into(), "gzip".into())];
1293 assert!(matches!(
1294 determine_request_body_kind(&headers),
1295 Err(HttpError::UnsupportedTransferEncoding(_))
1296 ));
1297 }
1298
1299 #[test]
1300 fn test_determine_body_chunked_not_final() {
1301 let headers = vec![("Transfer-Encoding".into(), "chunked, gzip".into())];
1302 assert!(matches!(
1303 determine_request_body_kind(&headers),
1304 Err(HttpError::ChunkedNotFinal)
1305 ));
1306 }
1307
1308 #[test]
1309 fn test_determine_body_mixed_header_casing() {
1310 let headers = vec![
1311 ("content-length".into(), "42".into()),
1312 ("CONTENT-LENGTH".into(), "42".into()),
1313 ];
1314 assert_eq!(
1315 determine_request_body_kind(&headers).unwrap(),
1316 RequestBodyKind::ContentLength(42)
1317 );
1318 }
1319
1320 #[tokio::test]
1323 async fn test_copy_chunked_body_simple() {
1324 let input = b"5\r\nhello\r\n0\r\n\r\n";
1325 let mut reader = &input[..];
1326 let mut writer = Vec::new();
1327 let limits = BodyCopyLimits::default();
1328
1329 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1330 .await
1331 .unwrap();
1332 assert_eq!(report.decoded_bytes, 5);
1333 assert_eq!(writer, input);
1334 }
1335
1336 #[tokio::test]
1337 async fn test_copy_chunked_body_multiple_chunks() {
1338 let input = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
1339 let mut reader = &input[..];
1340 let mut writer = Vec::new();
1341 let limits = BodyCopyLimits::default();
1342
1343 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1344 .await
1345 .unwrap();
1346 assert_eq!(report.decoded_bytes, 11);
1347 assert_eq!(writer, input);
1348 }
1349
1350 #[tokio::test]
1351 async fn test_copy_chunked_body_uppercase_hex() {
1352 let input = b"5\r\nhello\r\n0\r\n\r\n";
1353 let mut reader = &input[..];
1354 let mut writer = Vec::new();
1355 let limits = BodyCopyLimits::default();
1356
1357 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1358 .await
1359 .unwrap();
1360 assert_eq!(report.decoded_bytes, 5);
1361 }
1362
1363 #[tokio::test]
1364 async fn test_copy_chunked_body_with_extension() {
1365 let input = b"5;ext=value\r\nhello\r\n0\r\n\r\n";
1366 let mut reader = &input[..];
1367 let mut writer = Vec::new();
1368 let limits = BodyCopyLimits::default();
1369
1370 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1371 .await
1372 .unwrap();
1373 assert_eq!(report.decoded_bytes, 5);
1374 }
1375
1376 #[tokio::test]
1377 async fn test_copy_chunked_body_with_trailer() {
1378 let input = b"5\r\nhello\r\n0\r\nTrailer: value\r\n\r\n";
1379 let mut reader = &input[..];
1380 let mut writer = Vec::new();
1381 let limits = BodyCopyLimits::default();
1382
1383 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1384 .await
1385 .unwrap();
1386 assert_eq!(report.decoded_bytes, 5);
1387 }
1388
1389 #[tokio::test]
1390 async fn test_copy_chunked_body_malformed_hex() {
1391 let input = b"ZZ\r\nhello\r\n0\r\n\r\n";
1392 let mut reader = &input[..];
1393 let mut writer = Vec::new();
1394 let limits = BodyCopyLimits::default();
1395
1396 let result =
1397 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1398 assert!(result.is_err());
1399 }
1400
1401 #[tokio::test]
1402 async fn test_copy_chunked_body_empty_size() {
1403 let input = b"\r\nhello\r\n0\r\n\r\n";
1404 let mut reader = &input[..];
1405 let mut writer = Vec::new();
1406 let limits = BodyCopyLimits::default();
1407
1408 let result =
1409 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1410 assert!(result.is_err());
1411 }
1412
1413 #[tokio::test]
1414 async fn test_copy_chunked_body_missing_crlf() {
1415 let input = b"5\r\nhelloX\r\n0\r\n\r\n";
1416 let mut reader = &input[..];
1417 let mut writer = Vec::new();
1418 let limits = BodyCopyLimits::default();
1419
1420 let result =
1421 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1422 assert!(result.is_err());
1423 }
1424
1425 #[tokio::test]
1426 async fn test_copy_chunked_body_oversized_chunk() {
1427 let input = b"FFFFFFFFFFFFFFFF\r\nhello\r\n0\r\n\r\n";
1428 let mut reader = &input[..];
1429 let mut writer = Vec::new();
1430 let limits = BodyCopyLimits {
1431 max_chunk_size: 1024,
1432 ..Default::default()
1433 };
1434
1435 let result =
1436 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1437 assert!(result.is_err());
1438 }
1439
1440 #[tokio::test]
1441 async fn test_copy_content_length_body() {
1442 let input = b"hello world";
1443 let mut reader = &input[..];
1444 let mut writer = Vec::new();
1445 let limits = BodyCopyLimits::default();
1446
1447 let report = copy_request_body(
1448 &mut reader,
1449 &mut writer,
1450 RequestBodyKind::ContentLength(11),
1451 &limits,
1452 )
1453 .await
1454 .unwrap();
1455 assert_eq!(report.wire_bytes, 11);
1456 assert_eq!(report.decoded_bytes, 11);
1457 assert_eq!(writer, input);
1458 }
1459
1460 #[tokio::test]
1461 async fn test_copy_none_body() {
1462 let mut reader = &b""[..];
1463 let mut writer = Vec::new();
1464 let limits = BodyCopyLimits::default();
1465
1466 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::None, &limits)
1467 .await
1468 .unwrap();
1469 assert_eq!(report.wire_bytes, 0);
1470 assert_eq!(report.decoded_bytes, 0);
1471 }
1472
1473 #[tokio::test]
1474 async fn test_copy_content_length_body_premature_eof() {
1475 let input = b"hel"; let mut reader = &input[..];
1477 let mut writer = Vec::new();
1478 let limits = BodyCopyLimits::default();
1479
1480 let result = copy_request_body(
1481 &mut reader,
1482 &mut writer,
1483 RequestBodyKind::ContentLength(11),
1484 &limits,
1485 )
1486 .await;
1487 assert!(result.is_err());
1488 let err = result.unwrap_err();
1489 let msg = format!("{}", err);
1490 assert!(
1491 msg.contains("unexpected EOF"),
1492 "error should mention EOF: {}",
1493 msg
1494 );
1495 }
1496
1497 #[tokio::test]
1498 async fn test_copy_content_length_body_zero_length() {
1499 let input = b"";
1500 let mut reader = &input[..];
1501 let mut writer = Vec::new();
1502 let limits = BodyCopyLimits::default();
1503
1504 let report = copy_request_body(
1505 &mut reader,
1506 &mut writer,
1507 RequestBodyKind::ContentLength(0),
1508 &limits,
1509 )
1510 .await
1511 .unwrap();
1512 assert_eq!(report.wire_bytes, 0);
1513 assert_eq!(report.decoded_bytes, 0);
1514 }
1515
1516 #[tokio::test]
1517 async fn test_copy_chunked_body_decoded_limit_exceeded() {
1518 let chunk_data = "x".repeat(100);
1520 let input = format!("64\r\n{}\r\n0\r\n\r\n", chunk_data);
1521 let mut reader = input.as_bytes();
1522 let mut writer = Vec::new();
1523 let limits = BodyCopyLimits {
1524 max_decoded_body: 10,
1525 ..Default::default()
1526 };
1527
1528 let result =
1529 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1530 assert!(result.is_err());
1531 let msg = format!("{}", result.unwrap_err());
1532 assert!(
1533 msg.contains("decoded body too large"),
1534 "error should mention decoded body limit: {}",
1535 msg
1536 );
1537 }
1538
1539 #[test]
1542 fn test_te_plus_cl_rejected_not_forwarded() {
1543 let headers = vec![
1544 ("Transfer-Encoding".into(), "chunked".into()),
1545 ("Content-Length".into(), "0".into()),
1546 ];
1547 let result = determine_request_body_kind(&headers);
1548 assert!(
1549 matches!(result, Err(HttpError::TransferEncodingWithContentLength)),
1550 "TE+CL must be rejected to prevent ambiguous framing: {:?}",
1551 result
1552 );
1553 }
1554
1555 #[test]
1556 fn test_conflicting_cl_values_rejected() {
1557 let headers = vec![
1558 ("Content-Length".into(), "10".into()),
1559 ("Content-Length".into(), "20".into()),
1560 ];
1561 let result = determine_request_body_kind(&headers);
1562 assert!(
1563 matches!(result, Err(HttpError::ConflictingContentLength)),
1564 "conflicting CL values must be rejected: {:?}",
1565 result
1566 );
1567 }
1568
1569 #[test]
1570 fn test_equal_duplicate_cl_deterministic() {
1571 let headers = vec![
1572 ("Content-Length".into(), "42".into()),
1573 ("Content-Length".into(), "42".into()),
1574 ];
1575 let kind = determine_request_body_kind(&headers).unwrap();
1576 assert_eq!(kind, RequestBodyKind::ContentLength(42));
1577 }
1578
1579 #[test]
1580 fn test_connection_nominated_headers_removed() {
1581 let headers = vec![
1582 ("Connection".into(), "X-Foo, X-Bar".into()),
1583 ("X-Foo".into(), "a".into()),
1584 ("X-Bar".into(), "b".into()),
1585 ("Content-Type".into(), "text/html".into()),
1586 ];
1587 let filtered = filter_hop_by_hop(&headers);
1588 let names: Vec<_> = filtered.iter().map(|(n, _)| n.as_str()).collect();
1589 assert_eq!(names, vec!["Content-Type"]);
1590 }
1591
1592 #[test]
1593 fn test_ipv6_literal_authority_roundtrip() {
1594 let (target, path) = parse_absolute_uri("http://[::1]:8080/api").unwrap();
1595 assert_eq!(
1596 target,
1597 TargetAddr {
1598 host: TargetHost::Ip("::1".parse().unwrap()),
1599 port: 8080,
1600 }
1601 );
1602 assert_eq!(path, "/api");
1603 }
1604
1605 #[test]
1606 fn test_ipv6_literal_no_port() {
1607 let (target, _path) = parse_absolute_uri("http://[::1]/path").unwrap();
1608 assert_eq!(target.port, 80);
1609 assert_eq!(target.host, TargetHost::Ip("::1".parse().unwrap()));
1610 }
1611
1612 #[test]
1613 fn test_chunked_not_final_rejected() {
1614 let headers = vec![("Transfer-Encoding".into(), "gzip, chunked".into())];
1617 let result = determine_request_body_kind(&headers);
1618 assert!(
1619 matches!(
1620 result,
1621 Err(HttpError::UnsupportedTransferEncoding(_)) | Err(HttpError::ChunkedNotFinal)
1622 ),
1623 "chunked not final with unsupported coding must be rejected: {:?}",
1624 result
1625 );
1626 }
1627
1628 #[test]
1629 fn test_unsupported_transfer_encoding_rejected() {
1630 let headers = vec![("Transfer-Encoding".into(), "deflate".into())];
1631 let result = determine_request_body_kind(&headers);
1632 assert!(
1633 matches!(result, Err(HttpError::UnsupportedTransferEncoding(_))),
1634 "unsupported TE must be rejected: {:?}",
1635 result
1636 );
1637 }
1638
1639 #[tokio::test]
1640 async fn test_upstream_connection_close_detected() {
1641 let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello";
1642 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1644 tokio::spawn(async move {
1645 upstream_write.write_all(response).await.unwrap();
1646 upstream_write.shutdown().await.ok();
1647 });
1648 let (mut client_read, client_write) = tokio::io::duplex(4096);
1650
1651 let mut upstream: BoxStream = Box::new(upstream_read);
1652 let mut client: BoxStream = Box::new(client_write);
1653 let result = forward_response(&mut upstream, &mut client).await;
1654 assert!(result.is_ok());
1655 let fwd = result.unwrap();
1656 assert!(
1657 !fwd.upstream_alive,
1658 "Connection: close should make upstream not alive"
1659 );
1660 assert!(
1661 fwd.client_should_close,
1662 "client should close when upstream says close"
1663 );
1664 let mut buf = Vec::new();
1666 let _ = tokio::time::timeout(
1667 std::time::Duration::from_secs(1),
1668 client_read.read_to_end(&mut buf),
1669 )
1670 .await;
1671 let resp = String::from_utf8_lossy(&buf);
1672 assert!(
1673 resp.contains("200 OK"),
1674 "client should receive response: {resp}"
1675 );
1676 assert!(resp.contains("hello"), "client should receive body: {resp}");
1677 }
1678
1679 #[tokio::test]
1680 async fn test_upstream_http11_keepalive_default() {
1681 let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
1682 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1683 tokio::spawn(async move {
1684 upstream_write.write_all(response).await.unwrap();
1685 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1686 });
1687 let (mut client_read, client_write) = tokio::io::duplex(4096);
1688
1689 let mut upstream: BoxStream = Box::new(upstream_read);
1690 let mut client: BoxStream = Box::new(client_write);
1691 let result = forward_response(&mut upstream, &mut client).await;
1692 assert!(result.is_ok());
1693 let fwd = result.unwrap();
1694 assert!(
1695 fwd.upstream_alive,
1696 "HTTP/1.1 without Connection: close should be alive"
1697 );
1698 assert!(!fwd.client_should_close);
1699 let mut buf = Vec::new();
1700 let _ = tokio::time::timeout(
1701 std::time::Duration::from_secs(1),
1702 client_read.read_to_end(&mut buf),
1703 )
1704 .await;
1705 let resp = String::from_utf8_lossy(&buf);
1706 assert!(
1707 resp.contains("200 OK"),
1708 "client should receive response: {resp}"
1709 );
1710 }
1711
1712 #[test]
1713 fn test_filter_hop_by_hop_removes_upgrade() {
1714 let headers = vec![
1715 ("Upgrade".into(), "websocket".into()),
1716 ("Content-Type".into(), "text/html".into()),
1717 ];
1718 let filtered = filter_hop_by_hop(&headers);
1719 assert_eq!(filtered.len(), 1);
1720 assert_eq!(filtered[0].0, "Content-Type");
1721 }
1722
1723 #[test]
1724 fn test_filter_hop_by_hop_removes_proxy_connection() {
1725 let headers = vec![
1726 ("Proxy-Connection".into(), "keep-alive".into()),
1727 ("Content-Type".into(), "text/html".into()),
1728 ];
1729 let filtered = filter_hop_by_hop(&headers);
1730 assert_eq!(filtered.len(), 1);
1731 assert_eq!(filtered[0].0, "Content-Type");
1732 }
1733
1734 #[tokio::test]
1735 async fn test_request_body_kind_none_has_no_body() {
1736 let headers = vec![("Host".into(), "example.com".into())];
1737 let kind = determine_request_body_kind(&headers).unwrap();
1738 assert_eq!(kind, RequestBodyKind::None);
1739 assert!(!matches!(kind, RequestBodyKind::ContentLength(0)));
1740 }
1741
1742 #[test]
1743 fn test_forward_request_body_kind_dispatches_correctly() {
1744 let req_none = ForwardRequest {
1745 method: "GET".into(),
1746 path: "/".into(),
1747 version: "HTTP/1.1".into(),
1748 headers: vec![],
1749 target: TargetAddr {
1750 host: TargetHost::Domain("example.com".into()),
1751 port: 80,
1752 },
1753 has_body: false,
1754 content_length: None,
1755 is_chunked: false,
1756 connection_close: false,
1757 };
1758 assert_eq!(req_none.body_kind(), RequestBodyKind::None);
1759
1760 let req_cl = ForwardRequest {
1761 content_length: Some(100),
1762 has_body: true,
1763 ..req_none.clone()
1764 };
1765 assert_eq!(req_cl.body_kind(), RequestBodyKind::ContentLength(100));
1766
1767 let req_chunked = ForwardRequest {
1768 is_chunked: true,
1769 has_body: true,
1770 ..req_none.clone()
1771 };
1772 assert_eq!(req_chunked.body_kind(), RequestBodyKind::Chunked);
1773 }
1774
1775 #[tokio::test]
1778 async fn test_copy_request_body_premature_eof() {
1779 let input = b"short";
1780 let mut reader = &input[..];
1781 let mut writer = Vec::new();
1782 let limits = BodyCopyLimits::default();
1783
1784 let result = copy_request_body(
1785 &mut reader,
1786 &mut writer,
1787 RequestBodyKind::ContentLength(100),
1788 &limits,
1789 )
1790 .await;
1791 assert!(
1792 result.is_err(),
1793 "Content-Length body with premature EOF must fail"
1794 );
1795 let msg = format!("{}", result.unwrap_err());
1796 assert!(
1797 msg.contains("unexpected EOF"),
1798 "error should mention unexpected EOF: {msg}"
1799 );
1800 }
1801
1802 #[tokio::test]
1803 async fn test_forward_request_stream_after_failure() {
1804 use tokio::io::AsyncWriteExt;
1805
1806 let (client_read, mut client_write) = tokio::io::duplex(4096);
1807 let mut stream: BoxStream = Box::new(client_read);
1808
1809 let bad_request = b"INVALID\r\n\r\n";
1810 client_write.write_all(bad_request).await.unwrap();
1811
1812 let result = forward_request_stream(&mut stream).await;
1813 assert!(result.is_err(), "malformed request must fail");
1814
1815 let good_request = b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1816 client_write.write_all(good_request).await.unwrap();
1817
1818 let result2 = forward_request_stream(&mut stream).await;
1819 assert!(
1820 result2.is_ok(),
1821 "valid request after failure must succeed: {:?}",
1822 result2.err()
1823 );
1824 let req = result2.unwrap();
1825 assert_eq!(req.method, "GET");
1826 assert_eq!(req.path, "/");
1827 }
1828
1829 #[tokio::test]
1830 async fn test_forward_request_rejects_unsupported_http_version() {
1831 let (client_read, mut client_write) = tokio::io::duplex(4096);
1832 let mut stream: BoxStream = Box::new(client_read);
1833 client_write
1834 .write_all(b"GET http://example.com/ HTTP/9.9\r\n\r\n")
1835 .await
1836 .unwrap();
1837
1838 let error = forward_request_stream(&mut stream).await.unwrap_err();
1839 assert!(
1840 matches!(error, HttpError::MalformedRequest(message) if message.contains("HTTP/9.9"))
1841 );
1842 }
1843
1844 #[tokio::test]
1845 async fn test_response_header_limit_allows_maximum_header_count() {
1846 let (client_read, mut client_write) = tokio::io::duplex(32 * 1024);
1847 let mut stream: BoxStream = Box::new(client_read);
1848 let mut response = String::from("HTTP/1.1 200 OK\r\n");
1849 for index in 0..MAX_HEADER_LINES {
1850 response.push_str(&format!("X-Test-{index}: value\r\n"));
1851 }
1852 response.push_str("\r\n");
1853 client_write.write_all(response.as_bytes()).await.unwrap();
1854
1855 let parsed = read_response_head(&mut stream).await.unwrap();
1856 assert_eq!(parsed.headers.len(), MAX_HEADER_LINES);
1857 }
1858
1859 #[test]
1860 fn test_build_origin_request_strips_upgrade() {
1861 let req = ForwardRequest {
1862 method: "GET".into(),
1863 path: "/".into(),
1864 version: "HTTP/1.1".into(),
1865 headers: vec![
1866 ("Host".into(), "example.com".into()),
1867 ("Upgrade".into(), "websocket".into()),
1868 ("Connection".into(), "Upgrade".into()),
1869 ],
1870 target: TargetAddr {
1871 host: TargetHost::Domain("example.com".into()),
1872 port: 80,
1873 },
1874 has_body: false,
1875 content_length: None,
1876 is_chunked: false,
1877 connection_close: false,
1878 };
1879 let origin = build_origin_request(&req);
1880 assert!(
1881 !origin.to_lowercase().contains("upgrade"),
1882 "Upgrade header must be stripped from forwarded request: {origin}"
1883 );
1884 assert!(
1885 !origin.to_lowercase().contains("connection: upgrade"),
1886 "Connection: Upgrade must be stripped: {origin}"
1887 );
1888 assert!(
1889 origin.contains("Connection: close"),
1890 "proxy must add Connection: close: {origin}"
1891 );
1892 }
1893
1894 #[test]
1895 fn test_expectation_detection_is_case_insensitive_and_comma_aware() {
1896 assert!(has_unsupported_expectation(&[(
1897 "eXpEcT".into(),
1898 "foo, 100-continue".into()
1899 ),]));
1900 assert!(!has_unsupported_expectation(&[(
1901 "Expect".into(),
1902 " , ".into()
1903 )]));
1904 }
1905
1906 #[tokio::test]
1907 async fn test_forward_response_forwards_informational_responses_before_final() {
1908 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1909
1910 let response = b"HTTP/1.1 103 Early Hints\r\nLink: </style.css>\r\n\r\nHTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
1911 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1912 tokio::spawn(async move {
1913 upstream_write.write_all(response).await.unwrap();
1914 });
1915 let (mut client_read, client_write) = tokio::io::duplex(4096);
1916
1917 let mut upstream: BoxStream = Box::new(upstream_read);
1918 let mut client: BoxStream = Box::new(client_write);
1919
1920 let result = forward_response(&mut upstream, &mut client).await.unwrap();
1921 assert_eq!(result.status, 200);
1922 client.shutdown().await.unwrap();
1923
1924 let mut buf = Vec::new();
1925 client_read.read_to_end(&mut buf).await.unwrap();
1926 let resp = String::from_utf8_lossy(&buf);
1927 assert!(
1928 resp.starts_with("HTTP/1.1 103 Early"),
1929 "unexpected forwarded response: {resp:?}"
1930 );
1931 assert!(resp.contains("HTTP/1.1 100 Continue"));
1932 assert!(resp.contains("HTTP/1.1 200 OK"));
1933 assert!(resp.ends_with("hello"));
1934 assert!(resp.find("103").unwrap() < resp.find("100").unwrap());
1935 assert!(resp.find("100").unwrap() < resp.find("200").unwrap());
1936 }
1937
1938 #[tokio::test]
1939 async fn test_forward_response_rejects_switching_protocols() {
1940 use tokio::io::AsyncWriteExt;
1941
1942 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1943 tokio::spawn(async move {
1944 upstream_write
1945 .write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n")
1946 .await
1947 .unwrap();
1948 });
1949 let (_client_read, client_write) = tokio::io::duplex(1024);
1950 let mut upstream: BoxStream = Box::new(upstream_read);
1951 let mut client: BoxStream = Box::new(client_write);
1952
1953 assert!(matches!(
1954 forward_response(&mut upstream, &mut client).await,
1955 Err(HttpError::UpgradeUnsupported)
1956 ));
1957 }
1958
1959 #[tokio::test]
1960 async fn test_forward_response_rejects_invalid_content_length() {
1961 use tokio::io::AsyncWriteExt;
1962
1963 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1964 tokio::spawn(async move {
1965 upstream_write
1966 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: invalid\r\n\r\n")
1967 .await
1968 .unwrap();
1969 });
1970 let (_client_read, client_write) = tokio::io::duplex(1024);
1971 let mut upstream: BoxStream = Box::new(upstream_read);
1972 let mut client: BoxStream = Box::new(client_write);
1973
1974 assert!(matches!(
1975 forward_response(&mut upstream, &mut client).await,
1976 Err(HttpError::InvalidContentLength)
1977 ));
1978 }
1979
1980 #[tokio::test]
1981 async fn test_forward_response_accepts_equal_duplicate_content_length() {
1982 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1983
1984 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1985 tokio::spawn(async move {
1986 upstream_write
1987 .write_all(
1988 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello",
1989 )
1990 .await
1991 .unwrap();
1992 });
1993 let (mut client_read, client_write) = tokio::io::duplex(1024);
1994 let mut upstream: BoxStream = Box::new(upstream_read);
1995 let mut client: BoxStream = Box::new(client_write);
1996
1997 let result = forward_response(&mut upstream, &mut client).await.unwrap();
1998 assert_eq!(result.status, 200);
1999 client.shutdown().await.unwrap();
2000 let mut buf = Vec::new();
2001 client_read.read_to_end(&mut buf).await.unwrap();
2002 assert!(String::from_utf8_lossy(&buf).ends_with("hello"));
2003 }
2004
2005 #[tokio::test]
2006 async fn test_forward_response_rejects_conflicting_duplicate_content_length() {
2007 use tokio::io::AsyncWriteExt;
2008
2009 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
2010 tokio::spawn(async move {
2011 upstream_write
2012 .write_all(
2013 b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello",
2014 )
2015 .await
2016 .unwrap();
2017 });
2018 let (_client_read, client_write) = tokio::io::duplex(1024);
2019 let mut upstream: BoxStream = Box::new(upstream_read);
2020 let mut client: BoxStream = Box::new(client_write);
2021
2022 assert!(matches!(
2023 forward_response(&mut upstream, &mut client).await,
2024 Err(HttpError::ConflictingContentLength)
2025 ));
2026 }
2027
2028 #[tokio::test]
2029 async fn test_forward_response_rejects_invalid_chunk_size() {
2030 use tokio::io::AsyncWriteExt;
2031
2032 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
2033 tokio::spawn(async move {
2034 upstream_write
2035 .write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nnope\r\n")
2036 .await
2037 .unwrap();
2038 });
2039 let (_client_read, client_write) = tokio::io::duplex(1024);
2040 let mut upstream: BoxStream = Box::new(upstream_read);
2041 let mut client: BoxStream = Box::new(client_write);
2042
2043 assert!(matches!(
2044 forward_response(&mut upstream, &mut client).await,
2045 Err(HttpError::MalformedResponse(message)) if message.contains("invalid chunk size")
2046 ));
2047 }
2048
2049 #[tokio::test]
2050 async fn test_forward_response_bounds_informational_responses() {
2051 use tokio::io::AsyncWriteExt;
2052
2053 let response = b"HTTP/1.1 103 Early Hints\r\n\r\n";
2054 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
2055 tokio::spawn(async move {
2056 for _ in 0..=MAX_INFORMATIONAL_RESPONSES {
2057 upstream_write.write_all(response).await.unwrap();
2058 }
2059 });
2060 let (_client_read, client_write) = tokio::io::duplex(4096);
2061 let mut upstream: BoxStream = Box::new(upstream_read);
2062 let mut client: BoxStream = Box::new(client_write);
2063
2064 assert!(matches!(
2065 forward_response(&mut upstream, &mut client).await,
2066 Err(HttpError::TooManyInformationalResponses)
2067 ));
2068 }
2069}