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: 1024 * 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
351fn is_hop_by_hop_header(name: &str, value: &str) -> bool {
356 let lower = name.to_ascii_lowercase();
357 match lower.as_str() {
358 "transfer-encoding" => !value.eq_ignore_ascii_case("chunked"),
359 _ => matches!(
360 lower.as_str(),
361 "connection"
362 | "keep-alive"
363 | "proxy-authenticate"
364 | "proxy-authorization"
365 | "te"
366 | "trailers"
367 | "upgrade"
368 | "proxy-connection"
369 ),
370 }
371}
372
373fn connection_tokens(headers: &[(String, String)]) -> std::collections::HashSet<String> {
378 headers
379 .iter()
380 .filter(|(name, _)| name.eq_ignore_ascii_case("connection"))
381 .flat_map(|(_, value)| value.split(','))
382 .map(|token| token.trim().to_ascii_lowercase())
383 .filter(|token| !token.is_empty())
384 .collect()
385}
386
387pub fn filter_hop_by_hop(headers: &[(String, String)]) -> Vec<(String, String)> {
392 let nominated = connection_tokens(headers);
393 headers
394 .iter()
395 .filter(|(name, value)| {
396 let lower = name.to_ascii_lowercase();
397 !is_hop_by_hop_header(&lower, value) && !nominated.contains(&lower)
398 })
399 .cloned()
400 .collect()
401}
402
403pub fn has_unsupported_expectation(headers: &[(String, String)]) -> bool {
407 headers.iter().any(|(name, value)| {
408 name.eq_ignore_ascii_case("Expect")
409 && value
410 .split(',')
411 .any(|expectation| !expectation.trim().is_empty())
412 })
413}
414
415pub fn build_origin_request(request: &ForwardRequest) -> String {
422 let filtered = filter_hop_by_hop(&request.headers);
423
424 let mut req = format!(
425 "{} {} {}\r\n",
426 request.method, request.path, request.version
427 );
428
429 for (name, value) in &filtered {
430 req.push_str(&format!("{}: {}\r\n", name, value));
431 }
432
433 if !filtered
435 .iter()
436 .any(|(n, _)| n.eq_ignore_ascii_case("Connection"))
437 {
438 req.push_str("Connection: close\r\n");
439 }
440
441 req.push_str("\r\n");
442 req
443}
444
445#[derive(Debug)]
447pub struct ForwardResponse {
448 pub version: String,
449 pub status: u16,
450 pub reason: String,
451 pub headers: Vec<(String, String)>,
452 pub content_length: Option<u64>,
453 pub is_chunked: bool,
454 pub connection_close: bool,
456}
457
458async fn read_response_head(stream: &mut BoxStream) -> Result<ForwardResponse, HttpError> {
460 let mut head_buf = Vec::with_capacity(1024);
461 let mut temp = [0u8; 1];
462
463 loop {
464 if head_buf.len() >= MAX_RESPONSE_HEAD_SIZE {
465 return Err(HttpError::HeaderTooLarge);
466 }
467
468 let n = stream.read(&mut temp).await?;
469 if n == 0 {
470 return Err(HttpError::MalformedResponse(
471 "unexpected EOF reading response".into(),
472 ));
473 }
474
475 head_buf.push(temp[0]);
476
477 if head_buf.len() >= 4 {
478 let len = head_buf.len();
479 if &head_buf[len - 4..] == b"\r\n\r\n" {
480 break;
481 }
482 }
483 }
484
485 let head_str = String::from_utf8_lossy(&head_buf);
486 let mut lines = head_str.split("\r\n");
487
488 let status_line = lines
490 .next()
491 .ok_or_else(|| HttpError::MalformedResponse("empty response".into()))?;
492
493 let parts: Vec<&str> = status_line.split_whitespace().collect();
494 if parts.len() < 2 {
495 return Err(HttpError::MalformedResponse(format!(
496 "invalid status line: {}",
497 status_line
498 )));
499 }
500
501 let version = parts[0].to_string();
502 let status: u16 = parts[1]
503 .parse()
504 .map_err(|e| HttpError::MalformedResponse(format!("invalid status code: {}", e)))?;
505 let reason = parts.get(2).unwrap_or(&"").to_string();
506
507 let mut headers = Vec::new();
509 let mut content_length = None;
510 let mut is_chunked = false;
511 let mut connection_close = false;
512
513 for line in lines {
514 if line.is_empty() {
515 break;
516 }
517 if let Some((name, value)) = parse_header_line(line) {
518 if name.eq_ignore_ascii_case("Content-Length") {
519 content_length = Some(
520 value
521 .parse::<u64>()
522 .map_err(|_| HttpError::InvalidContentLength)?,
523 );
524 } else if name.eq_ignore_ascii_case("Transfer-Encoding") {
525 for coding in value.split(',') {
526 let coding_name = coding.trim().split(';').next().unwrap_or("").trim();
527 if coding_name.eq_ignore_ascii_case("chunked") {
528 is_chunked = true;
529 }
530 }
531 } else if name.eq_ignore_ascii_case("Connection") {
532 connection_close = value
534 .split(',')
535 .any(|t| t.trim().eq_ignore_ascii_case("close"));
536 }
537 headers.push((name, value));
538 }
539 }
540
541 if is_chunked {
545 content_length = None;
546 }
547
548 Ok(ForwardResponse {
549 version,
550 status,
551 reason,
552 headers,
553 content_length,
554 is_chunked,
555 connection_close,
556 })
557}
558
559fn format_response_head(response: &ForwardResponse, force_close: bool) -> String {
560 let filtered = filter_hop_by_hop(&response.headers);
561 let mut head = format!("HTTP/1.1 {} {}\r\n", response.status, response.reason);
562
563 for (name, value) in &filtered {
564 head.push_str(&format!("{}: {}\r\n", name, value));
565 }
566
567 if force_close
568 && !filtered
569 .iter()
570 .any(|(n, _)| n.eq_ignore_ascii_case("Connection"))
571 {
572 head.push_str("Connection: close\r\n");
573 }
574
575 head.push_str("\r\n");
576 head
577}
578
579pub struct ForwardResult {
581 pub report: ForwardResponseReport,
582 pub status: u16,
584 pub upstream_alive: bool,
586 pub client_should_close: bool,
588}
589
590pub async fn forward_response(
595 upstream: &mut BoxStream,
596 client: &mut BoxStream,
597) -> Result<ForwardResult, HttpError> {
598 let mut informational_responses = 0;
599 let mut bytes_forwarded: u64 = 0;
600 let response = loop {
601 let response = read_response_head(upstream).await?;
602 if response.status == 101 {
603 return Err(HttpError::UpgradeUnsupported);
604 }
605 if (100..200).contains(&response.status) {
606 informational_responses += 1;
607 if informational_responses > MAX_INFORMATIONAL_RESPONSES {
608 return Err(HttpError::TooManyInformationalResponses);
609 }
610 let head = format_response_head(&response, false);
611 client.write_all(head.as_bytes()).await?;
612 bytes_forwarded += head.len() as u64;
613 continue;
614 }
615 break response;
616 };
617 let head = format_response_head(&response, true);
618 client.write_all(head.as_bytes()).await?;
619 bytes_forwarded += head.len() as u64;
620
621 match (response.content_length, response.is_chunked) {
623 (Some(len), _) => {
624 let mut remaining = len;
625 let mut buf = [0u8; 8192];
626 while remaining > 0 {
627 let to_read = (remaining as usize).min(buf.len());
628 let n = upstream.read(&mut buf[..to_read]).await?;
629 if n == 0 {
630 return Err(HttpError::MalformedResponse(
631 "unexpected EOF in response body".into(),
632 ));
633 }
634 client.write_all(&buf[..n]).await?;
635 bytes_forwarded += n as u64;
636 remaining -= n as u64;
637 }
638 }
639 (None, true) => {
640 let mut size_line_buf = Vec::new();
641 loop {
642 size_line_buf.clear();
643 read_bounded_line_into(upstream, &mut size_line_buf, 1024).await?;
644
645 let size_str = String::from_utf8_lossy(&size_line_buf);
646 let size_str = size_str.trim_end_matches("\r\n");
647 let size_str = size_str.split(';').next().unwrap_or("").trim();
648 let chunk_size = usize::from_str_radix(size_str, 16).map_err(|e| {
649 HttpError::MalformedResponse(format!("invalid chunk size: {}", e))
650 })?;
651
652 client.write_all(&size_line_buf).await?;
653 bytes_forwarded += size_line_buf.len() as u64;
654
655 if chunk_size == 0 {
656 loop {
657 let mut trailer = Vec::new();
658 read_bounded_line_into(upstream, &mut trailer, 8192).await?;
659 client.write_all(&trailer).await?;
660 bytes_forwarded += trailer.len() as u64;
661 if trailer.len() >= 2 && &trailer[trailer.len() - 2..] == b"\r\n" {
662 if &trailer[..2] == b"\r\n" {
663 break;
664 }
665 } else {
666 break;
667 }
668 }
669 break;
670 }
671
672 let mut remaining = chunk_size + 2;
673 let mut buf = [0u8; 8192];
674 while remaining > 0 {
675 let to_read = remaining.min(buf.len());
676 let n = upstream.read(&mut buf[..to_read]).await?;
677 if n == 0 {
678 return Ok(ForwardResult {
679 report: ForwardResponseReport { bytes_forwarded },
680 status: response.status,
681 upstream_alive: false,
682 client_should_close: true,
683 });
684 }
685 client.write_all(&buf[..n]).await?;
686 bytes_forwarded += n as u64;
687 remaining -= n;
688 }
689 }
690 }
691 (None, false) => {
692 let mut buf = [0u8; 8192];
693 loop {
694 let n = upstream.read(&mut buf).await?;
695 if n == 0 {
696 break;
697 }
698 client.write_all(&buf[..n]).await?;
699 bytes_forwarded += n as u64;
700 }
701 }
702 }
703
704 let upstream_alive = if response.connection_close {
706 false
707 } else if response.version.contains("1.1") {
708 true
709 } else {
710 response
712 .headers
713 .iter()
714 .any(|(n, v)| n.eq_ignore_ascii_case("Keep-Alive") && !v.is_empty())
715 };
716
717 let client_should_close = response.connection_close;
719
720 Ok(ForwardResult {
721 report: ForwardResponseReport { bytes_forwarded },
722 status: response.status,
723 upstream_alive,
724 client_should_close,
725 })
726}
727
728#[derive(Debug, Clone)]
730pub struct ForwardRequest {
731 pub method: String,
732 pub path: String,
733 pub version: String,
734 pub headers: Vec<(String, String)>,
735 pub target: TargetAddr,
736 pub has_body: bool,
737 pub content_length: Option<u64>,
738 pub is_chunked: bool,
739 pub connection_close: bool,
741}
742
743impl ForwardRequest {
744 pub fn body_kind(&self) -> RequestBodyKind {
746 if self.is_chunked {
747 RequestBodyKind::Chunked
748 } else if let Some(len) = self.content_length {
749 RequestBodyKind::ContentLength(len)
750 } else {
751 RequestBodyKind::None
752 }
753 }
754}
755
756pub async fn forward_request(
767 mut stream: BoxStream,
768) -> Result<(ForwardRequest, BoxStream), HttpError> {
769 let request = read_forward_request(&mut stream).await?;
770 Ok((request, stream))
771}
772
773pub async fn forward_request_stream(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
778 read_forward_request(stream).await
779}
780
781async fn read_forward_request(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
783 let mut head_buf = Vec::with_capacity(1024);
784 let mut temp = [0u8; 1];
785 let mut header_count = 0;
786
787 loop {
788 if head_buf.len() >= MAX_HEAD_SIZE {
789 return Err(HttpError::HeaderTooLarge);
790 }
791
792 let n = stream.read(&mut temp).await?;
793 if n == 0 {
794 return Err(HttpError::MalformedRequest(
795 "unexpected EOF reading request".into(),
796 ));
797 }
798
799 head_buf.push(temp[0]);
800
801 if head_buf.len() >= 4 {
803 let len = head_buf.len();
804 if &head_buf[len - 4..] == b"\r\n\r\n" {
805 break;
806 }
807 if head_buf.len() >= 2 && &head_buf[len - 2..] == b"\r\n" {
808 header_count += 1;
809 if header_count > MAX_HEADER_LINES {
810 return Err(HttpError::TooManyHeaders);
811 }
812 }
813 }
814 }
815
816 let head_str = String::from_utf8_lossy(&head_buf);
817 let mut lines = head_str.split("\r\n");
818
819 let request_line = lines
821 .next()
822 .ok_or_else(|| HttpError::MalformedRequest("empty request".into()))?;
823
824 let parts: Vec<&str> = request_line.split_whitespace().collect();
825 if parts.len() != 3 {
826 return Err(HttpError::MalformedRequest(format!(
827 "expected 3 parts in request line, got {}",
828 parts.len()
829 )));
830 }
831
832 let method = parts[0].to_string();
833 let raw_target = parts[1].to_string();
834 let version = parts[2].to_string();
835
836 let (target, path) = parse_absolute_uri(&raw_target)?;
838
839 let mut headers = Vec::new();
841
842 for line in lines {
843 if line.is_empty() {
844 break;
845 }
846 if let Some((name, value)) = parse_header_line(line) {
847 if name.eq_ignore_ascii_case("Proxy-Authorization") {
849 continue;
850 }
851
852 headers.push((name, value));
853 }
854 }
855
856 let body_kind = determine_request_body_kind(&headers)?;
858 let (has_body, content_length, is_chunked) = match body_kind {
859 RequestBodyKind::None => (false, None, false),
860 RequestBodyKind::ContentLength(len) => (len > 0, Some(len), false),
861 RequestBodyKind::Chunked => (true, None, true),
862 };
863
864 let connection_close = headers.iter().any(|(n, v)| {
866 n.eq_ignore_ascii_case("Connection")
867 && v.split(',').any(|t| t.trim().eq_ignore_ascii_case("close"))
868 });
869
870 Ok(ForwardRequest {
871 method,
872 path,
873 version,
874 headers,
875 target,
876 has_body,
877 content_length,
878 is_chunked,
879 connection_close,
880 })
881}
882
883fn parse_absolute_uri(uri: &str) -> Result<(TargetAddr, String), HttpError> {
887 let (rest, default_port) = if let Some(stripped) = uri.strip_prefix("http://") {
889 (stripped, 80)
890 } else if let Some(stripped) = uri.strip_prefix("https://") {
891 (stripped, 443)
893 } else {
894 return Err(HttpError::MalformedRequest(format!(
895 "absolute URI required, got: {}",
896 uri
897 )));
898 };
899
900 let path_start = rest.find('/').unwrap_or(rest.len());
902 let authority = &rest[..path_start];
903 let path = if path_start < rest.len() {
904 &rest[path_start..]
905 } else {
906 "/"
907 };
908
909 let target = parse_authority_with_default(authority, default_port)?;
911
912 Ok((target, path.to_string()))
913}
914
915fn parse_authority_with_default(
917 authority: &str,
918 default_port: u16,
919) -> Result<TargetAddr, HttpError> {
920 if authority.starts_with('[') {
922 let bracket_end = authority.find(']').ok_or_else(|| {
923 HttpError::TargetParseError("unclosed bracket in IPv6 address".into())
924 })?;
925
926 let ip_str = &authority[1..bracket_end];
927 let ip: std::net::IpAddr = ip_str
928 .parse()
929 .map_err(|e| HttpError::TargetParseError(format!("invalid IPv6 address: {}", e)))?;
930
931 let port = if authority
933 .as_bytes()
934 .get(bracket_end + 1)
935 .is_some_and(|&b| b == b':')
936 {
937 let port_str = authority.get(bracket_end + 2..).ok_or_else(|| {
938 HttpError::TargetParseError("missing port after IPv6 address".into())
939 })?;
940 port_str
941 .parse()
942 .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?
943 } else {
944 default_port
945 };
946
947 return Ok(TargetAddr {
948 host: TargetHost::Ip(ip),
949 port,
950 });
951 }
952
953 let colon_pos = authority.rfind(':');
955
956 let (host_str, port) = if let Some(colon_pos) = colon_pos {
957 let host_str = &authority[..colon_pos];
958 let port_str = &authority[colon_pos + 1..];
959 let port: u16 = port_str
960 .parse()
961 .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?;
962 (host_str, port)
963 } else {
964 (authority, default_port)
965 };
966
967 if let Ok(ip) = host_str.parse::<std::net::IpAddr>() {
969 return Ok(TargetAddr {
970 host: TargetHost::Ip(ip),
971 port,
972 });
973 }
974
975 if host_str.is_empty() {
977 return Err(HttpError::TargetParseError("empty host".into()));
978 }
979
980 Ok(TargetAddr {
981 host: TargetHost::Domain(host_str.to_string()),
982 port,
983 })
984}
985
986fn parse_header_line(line: &str) -> Option<(String, String)> {
991 let colon_pos = line.find(':')?;
992 let name = line[..colon_pos].trim().to_string();
993 let value = line[colon_pos + 1..].trim().to_string();
994
995 if name.is_empty() {
996 return None;
997 }
998 if name.bytes().any(|b| b == b'\0' || b == b'\r' || b == b'\n') {
999 return None;
1000 }
1001 if value
1002 .bytes()
1003 .any(|b| b == b'\0' || b == b'\r' || b == b'\n')
1004 {
1005 return None;
1006 }
1007
1008 Some((name, value))
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014
1015 #[test]
1016 fn test_parse_absolute_uri() {
1017 let (target, path) = parse_absolute_uri("http://example.com:8080/path").unwrap();
1018 assert_eq!(
1019 target,
1020 TargetAddr {
1021 host: TargetHost::Domain("example.com".to_string()),
1022 port: 8080,
1023 }
1024 );
1025 assert_eq!(path, "/path");
1026 }
1027
1028 #[test]
1029 fn test_parse_absolute_uri_no_path() {
1030 let (target, path) = parse_absolute_uri("http://example.com:80").unwrap();
1031 assert_eq!(
1032 target,
1033 TargetAddr {
1034 host: TargetHost::Domain("example.com".to_string()),
1035 port: 80,
1036 }
1037 );
1038 assert_eq!(path, "/");
1039 }
1040
1041 #[test]
1042 fn test_parse_absolute_uri_ipv4() {
1043 let (target, path) = parse_absolute_uri("http://192.168.1.1:3000/api").unwrap();
1044 assert_eq!(
1045 target,
1046 TargetAddr {
1047 host: TargetHost::Ip("192.168.1.1".parse().unwrap()),
1048 port: 3000,
1049 }
1050 );
1051 assert_eq!(path, "/api");
1052 }
1053
1054 #[test]
1055 fn test_parse_absolute_uri_no_scheme() {
1056 assert!(parse_absolute_uri("example.com/path").is_err());
1057 }
1058
1059 #[test]
1060 fn test_parse_header_line() {
1061 let (name, value) = parse_header_line("Content-Type: text/html").unwrap();
1062 assert_eq!(name, "Content-Type");
1063 assert_eq!(value, "text/html");
1064 }
1065
1066 #[test]
1067 fn test_parse_header_line_no_colon() {
1068 assert!(parse_header_line("NoColon").is_none());
1069 }
1070
1071 #[test]
1072 fn test_filter_hop_by_hop_connection_nominated() {
1073 let headers = vec![
1074 ("Connection".into(), "X-Custom, Keep-Alive".into()),
1075 ("X-Custom".into(), "value".into()),
1076 ("Keep-Alive".into(), "timeout=5".into()),
1077 ("Content-Type".into(), "text/html".into()),
1078 ];
1079 let filtered = filter_hop_by_hop(&headers);
1080 assert_eq!(filtered.len(), 1);
1083 assert_eq!(filtered[0].0, "Content-Type");
1084 }
1085
1086 #[test]
1087 fn test_filter_hop_by_hop_preserves_transfer_encoding_chunked() {
1088 let headers = vec![
1089 ("Transfer-Encoding".into(), "chunked".into()),
1090 ("Content-Type".into(), "application/json".into()),
1091 ];
1092 let filtered = filter_hop_by_hop(&headers);
1093 assert_eq!(filtered.len(), 2);
1094 assert!(filtered.iter().any(|(n, _)| n == "Transfer-Encoding"));
1095 }
1096
1097 #[test]
1098 fn test_filter_hop_by_hop_removes_transfer_encoding_non_chunked() {
1099 let headers = vec![
1100 ("Transfer-Encoding".into(), "gzip".into()),
1101 ("Content-Type".into(), "text/html".into()),
1102 ];
1103 let filtered = filter_hop_by_hop(&headers);
1104 assert_eq!(filtered.len(), 1);
1105 assert_eq!(filtered[0].0, "Content-Type");
1106 }
1107
1108 #[test]
1109 fn test_filter_connection_tokens_empty() {
1110 let headers = vec![("Content-Type".into(), "text/html".into())];
1111 let tokens = connection_tokens(&headers);
1112 assert!(tokens.is_empty());
1113 }
1114
1115 #[test]
1116 fn test_filter_connection_tokens_multiple() {
1117 let headers = vec![("Connection".into(), "close, Upgrade".into())];
1118 let tokens = connection_tokens(&headers);
1119 assert!(tokens.contains("close"));
1120 assert!(tokens.contains("upgrade"));
1121 }
1122
1123 #[test]
1124 fn test_determine_body_none() {
1125 let headers = vec![("Host".into(), "example.com".into())];
1126 assert_eq!(
1127 determine_request_body_kind(&headers).unwrap(),
1128 RequestBodyKind::None
1129 );
1130 }
1131
1132 #[test]
1133 fn test_determine_body_content_length() {
1134 let headers = vec![("Content-Length".into(), "42".into())];
1135 assert_eq!(
1136 determine_request_body_kind(&headers).unwrap(),
1137 RequestBodyKind::ContentLength(42)
1138 );
1139 }
1140
1141 #[test]
1142 fn test_determine_body_duplicate_equal_cl() {
1143 let headers = vec![
1144 ("Content-Length".into(), "42".into()),
1145 ("Content-Length".into(), "42".into()),
1146 ];
1147 assert_eq!(
1148 determine_request_body_kind(&headers).unwrap(),
1149 RequestBodyKind::ContentLength(42)
1150 );
1151 }
1152
1153 #[test]
1154 fn test_determine_body_conflicting_cl() {
1155 let headers = vec![
1156 ("Content-Length".into(), "42".into()),
1157 ("Content-Length".into(), "100".into()),
1158 ];
1159 assert!(matches!(
1160 determine_request_body_kind(&headers),
1161 Err(HttpError::ConflictingContentLength)
1162 ));
1163 }
1164
1165 #[test]
1166 fn test_determine_body_invalid_cl() {
1167 let headers = vec![("Content-Length".into(), "abc".into())];
1168 assert!(matches!(
1169 determine_request_body_kind(&headers),
1170 Err(HttpError::InvalidContentLength)
1171 ));
1172 }
1173
1174 #[test]
1175 fn test_determine_body_chunked() {
1176 let headers = vec![("Transfer-Encoding".into(), "chunked".into())];
1177 assert_eq!(
1178 determine_request_body_kind(&headers).unwrap(),
1179 RequestBodyKind::Chunked
1180 );
1181 }
1182
1183 #[test]
1184 fn test_determine_body_te_plus_cl() {
1185 let headers = vec![
1186 ("Transfer-Encoding".into(), "chunked".into()),
1187 ("Content-Length".into(), "42".into()),
1188 ];
1189 assert!(matches!(
1190 determine_request_body_kind(&headers),
1191 Err(HttpError::TransferEncodingWithContentLength)
1192 ));
1193 }
1194
1195 #[test]
1196 fn test_determine_body_unsupported_te() {
1197 let headers = vec![("Transfer-Encoding".into(), "gzip".into())];
1198 assert!(matches!(
1199 determine_request_body_kind(&headers),
1200 Err(HttpError::UnsupportedTransferEncoding(_))
1201 ));
1202 }
1203
1204 #[test]
1205 fn test_determine_body_chunked_not_final() {
1206 let headers = vec![("Transfer-Encoding".into(), "chunked, gzip".into())];
1207 assert!(matches!(
1208 determine_request_body_kind(&headers),
1209 Err(HttpError::ChunkedNotFinal)
1210 ));
1211 }
1212
1213 #[test]
1214 fn test_determine_body_mixed_header_casing() {
1215 let headers = vec![
1216 ("content-length".into(), "42".into()),
1217 ("CONTENT-LENGTH".into(), "42".into()),
1218 ];
1219 assert_eq!(
1220 determine_request_body_kind(&headers).unwrap(),
1221 RequestBodyKind::ContentLength(42)
1222 );
1223 }
1224
1225 #[tokio::test]
1228 async fn test_copy_chunked_body_simple() {
1229 let input = b"5\r\nhello\r\n0\r\n\r\n";
1230 let mut reader = &input[..];
1231 let mut writer = Vec::new();
1232 let limits = BodyCopyLimits::default();
1233
1234 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1235 .await
1236 .unwrap();
1237 assert_eq!(report.decoded_bytes, 5);
1238 assert_eq!(writer, input);
1239 }
1240
1241 #[tokio::test]
1242 async fn test_copy_chunked_body_multiple_chunks() {
1243 let input = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
1244 let mut reader = &input[..];
1245 let mut writer = Vec::new();
1246 let limits = BodyCopyLimits::default();
1247
1248 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1249 .await
1250 .unwrap();
1251 assert_eq!(report.decoded_bytes, 11);
1252 assert_eq!(writer, input);
1253 }
1254
1255 #[tokio::test]
1256 async fn test_copy_chunked_body_uppercase_hex() {
1257 let input = b"5\r\nhello\r\n0\r\n\r\n";
1258 let mut reader = &input[..];
1259 let mut writer = Vec::new();
1260 let limits = BodyCopyLimits::default();
1261
1262 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1263 .await
1264 .unwrap();
1265 assert_eq!(report.decoded_bytes, 5);
1266 }
1267
1268 #[tokio::test]
1269 async fn test_copy_chunked_body_with_extension() {
1270 let input = b"5;ext=value\r\nhello\r\n0\r\n\r\n";
1271 let mut reader = &input[..];
1272 let mut writer = Vec::new();
1273 let limits = BodyCopyLimits::default();
1274
1275 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1276 .await
1277 .unwrap();
1278 assert_eq!(report.decoded_bytes, 5);
1279 }
1280
1281 #[tokio::test]
1282 async fn test_copy_chunked_body_with_trailer() {
1283 let input = b"5\r\nhello\r\n0\r\nTrailer: value\r\n\r\n";
1284 let mut reader = &input[..];
1285 let mut writer = Vec::new();
1286 let limits = BodyCopyLimits::default();
1287
1288 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1289 .await
1290 .unwrap();
1291 assert_eq!(report.decoded_bytes, 5);
1292 }
1293
1294 #[tokio::test]
1295 async fn test_copy_chunked_body_malformed_hex() {
1296 let input = b"ZZ\r\nhello\r\n0\r\n\r\n";
1297 let mut reader = &input[..];
1298 let mut writer = Vec::new();
1299 let limits = BodyCopyLimits::default();
1300
1301 let result =
1302 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1303 assert!(result.is_err());
1304 }
1305
1306 #[tokio::test]
1307 async fn test_copy_chunked_body_empty_size() {
1308 let input = b"\r\nhello\r\n0\r\n\r\n";
1309 let mut reader = &input[..];
1310 let mut writer = Vec::new();
1311 let limits = BodyCopyLimits::default();
1312
1313 let result =
1314 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1315 assert!(result.is_err());
1316 }
1317
1318 #[tokio::test]
1319 async fn test_copy_chunked_body_missing_crlf() {
1320 let input = b"5\r\nhelloX\r\n0\r\n\r\n";
1321 let mut reader = &input[..];
1322 let mut writer = Vec::new();
1323 let limits = BodyCopyLimits::default();
1324
1325 let result =
1326 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1327 assert!(result.is_err());
1328 }
1329
1330 #[tokio::test]
1331 async fn test_copy_chunked_body_oversized_chunk() {
1332 let input = b"FFFFFFFFFFFFFFFF\r\nhello\r\n0\r\n\r\n";
1333 let mut reader = &input[..];
1334 let mut writer = Vec::new();
1335 let limits = BodyCopyLimits {
1336 max_chunk_size: 1024,
1337 ..Default::default()
1338 };
1339
1340 let result =
1341 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1342 assert!(result.is_err());
1343 }
1344
1345 #[tokio::test]
1346 async fn test_copy_content_length_body() {
1347 let input = b"hello world";
1348 let mut reader = &input[..];
1349 let mut writer = Vec::new();
1350 let limits = BodyCopyLimits::default();
1351
1352 let report = copy_request_body(
1353 &mut reader,
1354 &mut writer,
1355 RequestBodyKind::ContentLength(11),
1356 &limits,
1357 )
1358 .await
1359 .unwrap();
1360 assert_eq!(report.wire_bytes, 11);
1361 assert_eq!(report.decoded_bytes, 11);
1362 assert_eq!(writer, input);
1363 }
1364
1365 #[tokio::test]
1366 async fn test_copy_none_body() {
1367 let mut reader = &b""[..];
1368 let mut writer = Vec::new();
1369 let limits = BodyCopyLimits::default();
1370
1371 let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::None, &limits)
1372 .await
1373 .unwrap();
1374 assert_eq!(report.wire_bytes, 0);
1375 assert_eq!(report.decoded_bytes, 0);
1376 }
1377
1378 #[tokio::test]
1379 async fn test_copy_content_length_body_premature_eof() {
1380 let input = b"hel"; let mut reader = &input[..];
1382 let mut writer = Vec::new();
1383 let limits = BodyCopyLimits::default();
1384
1385 let result = copy_request_body(
1386 &mut reader,
1387 &mut writer,
1388 RequestBodyKind::ContentLength(11),
1389 &limits,
1390 )
1391 .await;
1392 assert!(result.is_err());
1393 let err = result.unwrap_err();
1394 let msg = format!("{}", err);
1395 assert!(
1396 msg.contains("unexpected EOF"),
1397 "error should mention EOF: {}",
1398 msg
1399 );
1400 }
1401
1402 #[tokio::test]
1403 async fn test_copy_content_length_body_zero_length() {
1404 let input = b"";
1405 let mut reader = &input[..];
1406 let mut writer = Vec::new();
1407 let limits = BodyCopyLimits::default();
1408
1409 let report = copy_request_body(
1410 &mut reader,
1411 &mut writer,
1412 RequestBodyKind::ContentLength(0),
1413 &limits,
1414 )
1415 .await
1416 .unwrap();
1417 assert_eq!(report.wire_bytes, 0);
1418 assert_eq!(report.decoded_bytes, 0);
1419 }
1420
1421 #[tokio::test]
1422 async fn test_copy_chunked_body_decoded_limit_exceeded() {
1423 let chunk_data = "x".repeat(100);
1425 let input = format!("64\r\n{}\r\n0\r\n\r\n", chunk_data);
1426 let mut reader = input.as_bytes();
1427 let mut writer = Vec::new();
1428 let limits = BodyCopyLimits {
1429 max_decoded_body: 10,
1430 ..Default::default()
1431 };
1432
1433 let result =
1434 copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1435 assert!(result.is_err());
1436 let msg = format!("{}", result.unwrap_err());
1437 assert!(
1438 msg.contains("decoded body too large"),
1439 "error should mention decoded body limit: {}",
1440 msg
1441 );
1442 }
1443
1444 #[test]
1447 fn test_te_plus_cl_rejected_not_forwarded() {
1448 let headers = vec![
1449 ("Transfer-Encoding".into(), "chunked".into()),
1450 ("Content-Length".into(), "0".into()),
1451 ];
1452 let result = determine_request_body_kind(&headers);
1453 assert!(
1454 matches!(result, Err(HttpError::TransferEncodingWithContentLength)),
1455 "TE+CL must be rejected to prevent ambiguous framing: {:?}",
1456 result
1457 );
1458 }
1459
1460 #[test]
1461 fn test_conflicting_cl_values_rejected() {
1462 let headers = vec![
1463 ("Content-Length".into(), "10".into()),
1464 ("Content-Length".into(), "20".into()),
1465 ];
1466 let result = determine_request_body_kind(&headers);
1467 assert!(
1468 matches!(result, Err(HttpError::ConflictingContentLength)),
1469 "conflicting CL values must be rejected: {:?}",
1470 result
1471 );
1472 }
1473
1474 #[test]
1475 fn test_equal_duplicate_cl_deterministic() {
1476 let headers = vec![
1477 ("Content-Length".into(), "42".into()),
1478 ("Content-Length".into(), "42".into()),
1479 ];
1480 let kind = determine_request_body_kind(&headers).unwrap();
1481 assert_eq!(kind, RequestBodyKind::ContentLength(42));
1482 }
1483
1484 #[test]
1485 fn test_connection_nominated_headers_removed() {
1486 let headers = vec![
1487 ("Connection".into(), "X-Foo, X-Bar".into()),
1488 ("X-Foo".into(), "a".into()),
1489 ("X-Bar".into(), "b".into()),
1490 ("Content-Type".into(), "text/html".into()),
1491 ];
1492 let filtered = filter_hop_by_hop(&headers);
1493 let names: Vec<_> = filtered.iter().map(|(n, _)| n.as_str()).collect();
1494 assert_eq!(names, vec!["Content-Type"]);
1495 }
1496
1497 #[test]
1498 fn test_ipv6_literal_authority_roundtrip() {
1499 let (target, path) = parse_absolute_uri("http://[::1]:8080/api").unwrap();
1500 assert_eq!(
1501 target,
1502 TargetAddr {
1503 host: TargetHost::Ip("::1".parse().unwrap()),
1504 port: 8080,
1505 }
1506 );
1507 assert_eq!(path, "/api");
1508 }
1509
1510 #[test]
1511 fn test_ipv6_literal_no_port() {
1512 let (target, _path) = parse_absolute_uri("http://[::1]/path").unwrap();
1513 assert_eq!(target.port, 80);
1514 assert_eq!(target.host, TargetHost::Ip("::1".parse().unwrap()));
1515 }
1516
1517 #[test]
1518 fn test_chunked_not_final_rejected() {
1519 let headers = vec![("Transfer-Encoding".into(), "gzip, chunked".into())];
1522 let result = determine_request_body_kind(&headers);
1523 assert!(
1524 matches!(
1525 result,
1526 Err(HttpError::UnsupportedTransferEncoding(_)) | Err(HttpError::ChunkedNotFinal)
1527 ),
1528 "chunked not final with unsupported coding must be rejected: {:?}",
1529 result
1530 );
1531 }
1532
1533 #[test]
1534 fn test_unsupported_transfer_encoding_rejected() {
1535 let headers = vec![("Transfer-Encoding".into(), "deflate".into())];
1536 let result = determine_request_body_kind(&headers);
1537 assert!(
1538 matches!(result, Err(HttpError::UnsupportedTransferEncoding(_))),
1539 "unsupported TE must be rejected: {:?}",
1540 result
1541 );
1542 }
1543
1544 #[tokio::test]
1545 async fn test_upstream_connection_close_detected() {
1546 let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello";
1547 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1549 tokio::spawn(async move {
1550 upstream_write.write_all(response).await.unwrap();
1551 upstream_write.shutdown().await.ok();
1552 });
1553 let (mut client_read, client_write) = tokio::io::duplex(4096);
1555
1556 let mut upstream: BoxStream = Box::new(upstream_read);
1557 let mut client: BoxStream = Box::new(client_write);
1558 let result = forward_response(&mut upstream, &mut client).await;
1559 assert!(result.is_ok());
1560 let fwd = result.unwrap();
1561 assert!(
1562 !fwd.upstream_alive,
1563 "Connection: close should make upstream not alive"
1564 );
1565 assert!(
1566 fwd.client_should_close,
1567 "client should close when upstream says close"
1568 );
1569 let mut buf = Vec::new();
1571 let _ = tokio::time::timeout(
1572 std::time::Duration::from_secs(1),
1573 client_read.read_to_end(&mut buf),
1574 )
1575 .await;
1576 let resp = String::from_utf8_lossy(&buf);
1577 assert!(
1578 resp.contains("200 OK"),
1579 "client should receive response: {resp}"
1580 );
1581 assert!(resp.contains("hello"), "client should receive body: {resp}");
1582 }
1583
1584 #[tokio::test]
1585 async fn test_upstream_http11_keepalive_default() {
1586 let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
1587 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1588 tokio::spawn(async move {
1589 upstream_write.write_all(response).await.unwrap();
1590 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1591 });
1592 let (mut client_read, client_write) = tokio::io::duplex(4096);
1593
1594 let mut upstream: BoxStream = Box::new(upstream_read);
1595 let mut client: BoxStream = Box::new(client_write);
1596 let result = forward_response(&mut upstream, &mut client).await;
1597 assert!(result.is_ok());
1598 let fwd = result.unwrap();
1599 assert!(
1600 fwd.upstream_alive,
1601 "HTTP/1.1 without Connection: close should be alive"
1602 );
1603 assert!(!fwd.client_should_close);
1604 let mut buf = Vec::new();
1605 let _ = tokio::time::timeout(
1606 std::time::Duration::from_secs(1),
1607 client_read.read_to_end(&mut buf),
1608 )
1609 .await;
1610 let resp = String::from_utf8_lossy(&buf);
1611 assert!(
1612 resp.contains("200 OK"),
1613 "client should receive response: {resp}"
1614 );
1615 }
1616
1617 #[test]
1618 fn test_filter_hop_by_hop_removes_upgrade() {
1619 let headers = vec![
1620 ("Upgrade".into(), "websocket".into()),
1621 ("Content-Type".into(), "text/html".into()),
1622 ];
1623 let filtered = filter_hop_by_hop(&headers);
1624 assert_eq!(filtered.len(), 1);
1625 assert_eq!(filtered[0].0, "Content-Type");
1626 }
1627
1628 #[test]
1629 fn test_filter_hop_by_hop_removes_proxy_connection() {
1630 let headers = vec![
1631 ("Proxy-Connection".into(), "keep-alive".into()),
1632 ("Content-Type".into(), "text/html".into()),
1633 ];
1634 let filtered = filter_hop_by_hop(&headers);
1635 assert_eq!(filtered.len(), 1);
1636 assert_eq!(filtered[0].0, "Content-Type");
1637 }
1638
1639 #[tokio::test]
1640 async fn test_request_body_kind_none_has_no_body() {
1641 let headers = vec![("Host".into(), "example.com".into())];
1642 let kind = determine_request_body_kind(&headers).unwrap();
1643 assert_eq!(kind, RequestBodyKind::None);
1644 assert!(!matches!(kind, RequestBodyKind::ContentLength(0)));
1645 }
1646
1647 #[test]
1648 fn test_forward_request_body_kind_dispatches_correctly() {
1649 let req_none = ForwardRequest {
1650 method: "GET".into(),
1651 path: "/".into(),
1652 version: "HTTP/1.1".into(),
1653 headers: vec![],
1654 target: TargetAddr {
1655 host: TargetHost::Domain("example.com".into()),
1656 port: 80,
1657 },
1658 has_body: false,
1659 content_length: None,
1660 is_chunked: false,
1661 connection_close: false,
1662 };
1663 assert_eq!(req_none.body_kind(), RequestBodyKind::None);
1664
1665 let req_cl = ForwardRequest {
1666 content_length: Some(100),
1667 has_body: true,
1668 ..req_none.clone()
1669 };
1670 assert_eq!(req_cl.body_kind(), RequestBodyKind::ContentLength(100));
1671
1672 let req_chunked = ForwardRequest {
1673 is_chunked: true,
1674 has_body: true,
1675 ..req_none.clone()
1676 };
1677 assert_eq!(req_chunked.body_kind(), RequestBodyKind::Chunked);
1678 }
1679
1680 #[tokio::test]
1683 async fn test_copy_request_body_premature_eof() {
1684 let input = b"short";
1685 let mut reader = &input[..];
1686 let mut writer = Vec::new();
1687 let limits = BodyCopyLimits::default();
1688
1689 let result = copy_request_body(
1690 &mut reader,
1691 &mut writer,
1692 RequestBodyKind::ContentLength(100),
1693 &limits,
1694 )
1695 .await;
1696 assert!(
1697 result.is_err(),
1698 "Content-Length body with premature EOF must fail"
1699 );
1700 let msg = format!("{}", result.unwrap_err());
1701 assert!(
1702 msg.contains("unexpected EOF"),
1703 "error should mention unexpected EOF: {msg}"
1704 );
1705 }
1706
1707 #[tokio::test]
1708 async fn test_forward_request_stream_after_failure() {
1709 use tokio::io::AsyncWriteExt;
1710
1711 let (client_read, mut client_write) = tokio::io::duplex(4096);
1712 let mut stream: BoxStream = Box::new(client_read);
1713
1714 let bad_request = b"INVALID\r\n\r\n";
1715 client_write.write_all(bad_request).await.unwrap();
1716
1717 let result = forward_request_stream(&mut stream).await;
1718 assert!(result.is_err(), "malformed request must fail");
1719
1720 let good_request = b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1721 client_write.write_all(good_request).await.unwrap();
1722
1723 let result2 = forward_request_stream(&mut stream).await;
1724 assert!(
1725 result2.is_ok(),
1726 "valid request after failure must succeed: {:?}",
1727 result2.err()
1728 );
1729 let req = result2.unwrap();
1730 assert_eq!(req.method, "GET");
1731 assert_eq!(req.path, "/");
1732 }
1733
1734 #[test]
1735 fn test_build_origin_request_strips_upgrade() {
1736 let req = ForwardRequest {
1737 method: "GET".into(),
1738 path: "/".into(),
1739 version: "HTTP/1.1".into(),
1740 headers: vec![
1741 ("Host".into(), "example.com".into()),
1742 ("Upgrade".into(), "websocket".into()),
1743 ("Connection".into(), "Upgrade".into()),
1744 ],
1745 target: TargetAddr {
1746 host: TargetHost::Domain("example.com".into()),
1747 port: 80,
1748 },
1749 has_body: false,
1750 content_length: None,
1751 is_chunked: false,
1752 connection_close: false,
1753 };
1754 let origin = build_origin_request(&req);
1755 assert!(
1756 !origin.to_lowercase().contains("upgrade"),
1757 "Upgrade header must be stripped from forwarded request: {origin}"
1758 );
1759 assert!(
1760 !origin.to_lowercase().contains("connection: upgrade"),
1761 "Connection: Upgrade must be stripped: {origin}"
1762 );
1763 assert!(
1764 origin.contains("Connection: close"),
1765 "proxy must add Connection: close: {origin}"
1766 );
1767 }
1768
1769 #[test]
1770 fn test_expectation_detection_is_case_insensitive_and_comma_aware() {
1771 assert!(has_unsupported_expectation(&[(
1772 "eXpEcT".into(),
1773 "foo, 100-continue".into()
1774 ),]));
1775 assert!(!has_unsupported_expectation(&[(
1776 "Expect".into(),
1777 " , ".into()
1778 )]));
1779 }
1780
1781 #[tokio::test]
1782 async fn test_forward_response_forwards_informational_responses_before_final() {
1783 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1784
1785 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";
1786 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1787 tokio::spawn(async move {
1788 upstream_write.write_all(response).await.unwrap();
1789 });
1790 let (mut client_read, client_write) = tokio::io::duplex(4096);
1791
1792 let mut upstream: BoxStream = Box::new(upstream_read);
1793 let mut client: BoxStream = Box::new(client_write);
1794
1795 let result = forward_response(&mut upstream, &mut client).await.unwrap();
1796 assert_eq!(result.status, 200);
1797 client.shutdown().await.unwrap();
1798
1799 let mut buf = Vec::new();
1800 client_read.read_to_end(&mut buf).await.unwrap();
1801 let resp = String::from_utf8_lossy(&buf);
1802 assert!(
1803 resp.starts_with("HTTP/1.1 103 Early"),
1804 "unexpected forwarded response: {resp:?}"
1805 );
1806 assert!(resp.contains("HTTP/1.1 100 Continue"));
1807 assert!(resp.contains("HTTP/1.1 200 OK"));
1808 assert!(resp.ends_with("hello"));
1809 assert!(resp.find("103").unwrap() < resp.find("100").unwrap());
1810 assert!(resp.find("100").unwrap() < resp.find("200").unwrap());
1811 }
1812
1813 #[tokio::test]
1814 async fn test_forward_response_rejects_switching_protocols() {
1815 use tokio::io::AsyncWriteExt;
1816
1817 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1818 tokio::spawn(async move {
1819 upstream_write
1820 .write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n")
1821 .await
1822 .unwrap();
1823 });
1824 let (_client_read, client_write) = tokio::io::duplex(1024);
1825 let mut upstream: BoxStream = Box::new(upstream_read);
1826 let mut client: BoxStream = Box::new(client_write);
1827
1828 assert!(matches!(
1829 forward_response(&mut upstream, &mut client).await,
1830 Err(HttpError::UpgradeUnsupported)
1831 ));
1832 }
1833
1834 #[tokio::test]
1835 async fn test_forward_response_rejects_invalid_content_length() {
1836 use tokio::io::AsyncWriteExt;
1837
1838 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1839 tokio::spawn(async move {
1840 upstream_write
1841 .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: invalid\r\n\r\n")
1842 .await
1843 .unwrap();
1844 });
1845 let (_client_read, client_write) = tokio::io::duplex(1024);
1846 let mut upstream: BoxStream = Box::new(upstream_read);
1847 let mut client: BoxStream = Box::new(client_write);
1848
1849 assert!(matches!(
1850 forward_response(&mut upstream, &mut client).await,
1851 Err(HttpError::InvalidContentLength)
1852 ));
1853 }
1854
1855 #[tokio::test]
1856 async fn test_forward_response_rejects_invalid_chunk_size() {
1857 use tokio::io::AsyncWriteExt;
1858
1859 let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1860 tokio::spawn(async move {
1861 upstream_write
1862 .write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nnope\r\n")
1863 .await
1864 .unwrap();
1865 });
1866 let (_client_read, client_write) = tokio::io::duplex(1024);
1867 let mut upstream: BoxStream = Box::new(upstream_read);
1868 let mut client: BoxStream = Box::new(client_write);
1869
1870 assert!(matches!(
1871 forward_response(&mut upstream, &mut client).await,
1872 Err(HttpError::MalformedResponse(message)) if message.contains("invalid chunk size")
1873 ));
1874 }
1875
1876 #[tokio::test]
1877 async fn test_forward_response_bounds_informational_responses() {
1878 use tokio::io::AsyncWriteExt;
1879
1880 let response = b"HTTP/1.1 103 Early Hints\r\n\r\n";
1881 let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1882 tokio::spawn(async move {
1883 for _ in 0..=MAX_INFORMATIONAL_RESPONSES {
1884 upstream_write.write_all(response).await.unwrap();
1885 }
1886 });
1887 let (_client_read, client_write) = tokio::io::duplex(4096);
1888 let mut upstream: BoxStream = Box::new(upstream_read);
1889 let mut client: BoxStream = Box::new(client_write);
1890
1891 assert!(matches!(
1892 forward_response(&mut upstream, &mut client).await,
1893 Err(HttpError::TooManyInformationalResponses)
1894 ));
1895 }
1896}