Skip to main content

eggress_protocol_http/forward/
server.rs

1use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
2
3use crate::error::HttpError;
4use eggress_core::{BoxStream, TargetAddr, TargetHost};
5
6/// Limits for body copying.
7pub 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/// Report from body copying.
28#[derive(Debug, Default)]
29pub struct BodyCopyReport {
30    pub wire_bytes: u64,
31    pub decoded_bytes: u64,
32}
33
34/// Report from forwarding a response.
35#[derive(Debug, Default)]
36pub struct ForwardResponseReport {
37    pub bytes_forwarded: u64,
38}
39
40/// Copy a request body from reader to writer.
41///
42/// For Content-Length bodies, copies exactly `len` bytes.
43/// For chunked bodies, parses and forwards chunks with proper bounds.
44/// Returns byte counts for accounting.
45pub 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        // Read chunk size line
107        let size_line = read_bounded_line(reader, limits.max_chunk_size_line).await?;
108        wire_bytes += size_line.len() as u64;
109
110        // Parse chunk size (ignore extensions after ';')
111        let chunk_size = parse_chunk_size(&size_line)?;
112
113        // Forward the size line
114        writer.write_all(&size_line).await?;
115
116        if chunk_size == 0 {
117            // Read and forward trailers
118            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        // Validate chunk size against limit
138        if chunk_size > limits.max_chunk_size {
139            return Err(HttpError::MalformedRequest("chunk too large".into()));
140        }
141
142        // Validate decoded body limit
143        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        // Read exactly chunk_size data bytes
151        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        // Read and verify CRLF after chunk data
167        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
184/// Read a bounded line terminated by \r\n.
185async 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
235/// Parse a chunk size from a line (without trailing CRLF).
236/// Supports hex with optional extensions (after ';').
237fn 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/// Describes how the request body is framed.
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum RequestBodyKind {
258    None,
259    ContentLength(u64),
260    Chunked,
261}
262
263/// Determine the request body framing from parsed headers.
264///
265/// Validates:
266/// - Content-Length values (reject conflicting, accept equal duplicates)
267/// - Transfer-Encoding (reject TE + CL, require chunked to be final)
268/// - Only "chunked" transfer coding is supported in Phase 1
269pub 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            // Parse each Content-Length value
278            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            // Split comma-separated transfer codings
285            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    // Validate Content-Length
295    if !content_lengths.is_empty() {
296        // All values must be identical
297        let first = content_lengths[0];
298        if content_lengths.iter().any(|&cl| cl != first) {
299            return Err(HttpError::ConflictingContentLength);
300        }
301    }
302
303    // Validate Transfer-Encoding
304    if !transfer_encodings.is_empty() {
305        // TE + CL is rejected in Phase 1
306        if !content_lengths.is_empty() {
307            return Err(HttpError::TransferEncodingWithContentLength);
308        }
309
310        // Check if chunked is present but not the final coding
311        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        // Only "chunked" is supported in Phase 1
322        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
338/// Maximum size for the HTTP request head (request line + headers).
339const MAX_HEAD_SIZE: usize = 32 * 1024;
340
341/// Maximum size for the HTTP response head.
342const MAX_RESPONSE_HEAD_SIZE: usize = 32 * 1024;
343
344/// Bound informational responses so an upstream cannot keep the proxy in a
345/// response-head loop indefinitely.
346const MAX_INFORMATIONAL_RESPONSES: usize = 8;
347
348/// Maximum number of header lines.
349const MAX_HEADER_LINES: usize = 128;
350
351/// Headers that must not be forwarded across a proxy (RFC 2616 §13.5.1).
352///
353/// `Transfer-Encoding: chunked` is preserved because the chunked body is
354/// forwarded unchanged.
355fn 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
373/// Extract tokens from the `Connection` header value.
374///
375/// Per RFC 7230 §6.1, each token names a header that must be removed before
376/// forwarding.
377fn 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
387/// Filter hop-by-hop headers from a header list, returning only end-to-end headers.
388///
389/// Removes standard hop-by-hop headers plus any headers nominated by
390/// `Connection` tokens.  Preserves `Transfer-Encoding: chunked`.
391pub 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
403/// Return whether the request contains an expectation this forwarder cannot
404/// negotiate. Expectation values are collected case-insensitively and comma-
405/// separated values are treated independently.
406pub 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
415/// Build an origin-form HTTP request to send to the upstream server.
416///
417/// Converts the parsed absolute-form request into origin-form by:
418/// - Using only the path component as the request target
419/// - Filtering out hop-by-hop headers
420/// - Adding `Connection: close` to avoid keep-alive complications
421pub 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    // Ensure Connection: close for Phase 1 (no persistent forwarding)
434    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/// Parsed HTTP response from an upstream server.
446#[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    /// True if the upstream sent `Connection: close`.
455    pub connection_close: bool,
456}
457
458/// Read and parse an HTTP response head from the upstream.
459async 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    // Parse status line
489    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    // Parse response headers
508    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                // Check for "close" token (case-insensitive)
533                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    // Per RFC 7230 §3.3.3, when both Content-Length and Transfer-Encoding
542    // are present, Transfer-Encoding takes precedence. A proxy MUST ignore
543    // Content-Length if Transfer-Encoding is present.
544    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
579/// Result of forwarding a response, including upstream connection state.
580pub struct ForwardResult {
581    pub report: ForwardResponseReport,
582    /// HTTP status code of the forwarded response.
583    pub status: u16,
584    /// True if the upstream connection is still usable (no `Connection: close`).
585    pub upstream_alive: bool,
586    /// True if the response status indicates the client should not retry.
587    pub client_should_close: bool,
588}
589
590/// Forward the upstream response back to the client stream.
591///
592/// Writes the response status line and filtered headers to the client,
593/// then relays the body (if any) using content-length or chunked framing.
594pub 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    // Relay body based on framing
622    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    // Determine upstream alive: HTTP/1.1 default is keep-alive, HTTP/1.0 default is close
705    let upstream_alive = if response.connection_close {
706        false
707    } else if response.version.contains("1.1") {
708        true
709    } else {
710        // HTTP/1.0: alive only if explicitly requested via Keep-Alive
711        response
712            .headers
713            .iter()
714            .any(|(n, v)| n.eq_ignore_ascii_case("Keep-Alive") && !v.is_empty())
715    };
716
717    // Client should close if the upstream said close
718    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/// A parsed HTTP request ready for forwarding.
729#[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    /// True if the client sent `Connection: close`.
740    pub connection_close: bool,
741}
742
743impl ForwardRequest {
744    /// Compute the request body kind from parsed fields.
745    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
756/// Forward an HTTP request from a client to the target server.
757///
758/// Parses the absolute-form request, converts to origin-form, forwards
759/// the request, and returns the response.
760///
761/// # Arguments
762/// * `stream` - The client stream
763///
764/// # Returns
765/// The parsed forward request and the target address to connect to.
766pub 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
773/// Read and parse an HTTP forward request from an existing stream.
774///
775/// Unlike [`forward_request`], this borrows the stream rather than
776/// consuming it, enabling persistent-session loops.
777pub async fn forward_request_stream(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
778    read_forward_request(stream).await
779}
780
781/// Read and parse an HTTP forward request with absolute-form target.
782async 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        // Check for end of headers
802        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    // Parse request line
820    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    // Parse absolute-form target: http://host:port/path
837    let (target, path) = parse_absolute_uri(&raw_target)?;
838
839    // Parse headers
840    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            // Skip Proxy-Authorization header (don't forward it)
848            if name.eq_ignore_ascii_case("Proxy-Authorization") {
849                continue;
850            }
851
852            headers.push((name, value));
853        }
854    }
855
856    // Determine body framing from headers
857    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    // Determine Connection: close
865    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
883/// Parse an absolute-form URI into target and path.
884///
885/// Supports: http://host:port/path, http://host/path
886fn parse_absolute_uri(uri: &str) -> Result<(TargetAddr, String), HttpError> {
887    // Remove scheme and determine default port
888    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        // For HTTPS, we'd need TLS, but for now treat as HTTP
892        (stripped, 443)
893    } else {
894        return Err(HttpError::MalformedRequest(format!(
895            "absolute URI required, got: {}",
896            uri
897        )));
898    };
899
900    // Find path separator
901    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    // Parse authority with default port
910    let target = parse_authority_with_default(authority, default_port)?;
911
912    Ok((target, path.to_string()))
913}
914
915/// Parse an authority (host:port) into a TargetAddr with a default port.
916fn parse_authority_with_default(
917    authority: &str,
918    default_port: u16,
919) -> Result<TargetAddr, HttpError> {
920    // Handle IPv6 bracketed addresses
921    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        // Check for port after bracket
932        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    // Find the last ':' to split host and port
954    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    // Try to parse as IP first
968    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    // Otherwise treat as domain
976    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
986/// Parse a header line into (name, value).
987///
988/// Rejects header names or values containing control characters (NUL, CR, LF)
989/// per RFC 7230 §3.2.4.
990fn 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        // X-Custom and Keep-Alive should be removed (nominated by Connection),
1081        // plus connection itself is always removed.
1082        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    // ===== Body copy tests =====
1226
1227    #[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"; // only 3 bytes but Content-Length says 11
1381        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        // A single valid chunk of 100 bytes but max_decoded_body is 10
1424        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    // ===== Phase 2: HTTP framing and connection-state invariants =====
1445
1446    #[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        // When chunked is not the final coding and a non-chunked coding is present,
1520        // the unsupported encoding is rejected first (since only chunked is supported).
1521        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        // Upstream: server writes response into duplex, forward_response reads from the other end
1548        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        // Client: forward_response writes to client_write, we read from client_read
1554        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        // Verify the client received the forwarded response
1570        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    // ===== Phase 2 gap coverage: invariants 6–9 =====
1681
1682    #[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}