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: 64 * 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/// Bound a response chunk before converting it to a platform `usize`.
352const MAX_RESPONSE_CHUNK_SIZE: u64 = 64 * 1024 * 1024;
353
354/// Total byte budget for chunked-response trailers after the terminating
355/// zero chunk, so an upstream cannot stream them indefinitely.
356const MAX_TRAILER_BYTES: usize = 64 * 1024;
357
358/// Headers that must not be forwarded across a proxy (RFC 2616 §13.5.1).
359///
360/// `Transfer-Encoding: chunked` is preserved because the chunked body is
361/// forwarded unchanged.
362fn is_hop_by_hop_header(name: &str, value: &str) -> bool {
363    let lower = name.to_ascii_lowercase();
364    match lower.as_str() {
365        "transfer-encoding" => !value.eq_ignore_ascii_case("chunked"),
366        _ => matches!(
367            lower.as_str(),
368            "connection"
369                | "keep-alive"
370                | "proxy-authenticate"
371                | "proxy-authorization"
372                | "te"
373                | "trailers"
374                | "upgrade"
375                | "proxy-connection"
376        ),
377    }
378}
379
380/// Extract tokens from the `Connection` header value.
381///
382/// Per RFC 7230 §6.1, each token names a header that must be removed before
383/// forwarding.
384fn connection_tokens(headers: &[(String, String)]) -> std::collections::HashSet<String> {
385    headers
386        .iter()
387        .filter(|(name, _)| name.eq_ignore_ascii_case("connection"))
388        .flat_map(|(_, value)| value.split(','))
389        .map(|token| token.trim().to_ascii_lowercase())
390        .filter(|token| !token.is_empty())
391        .collect()
392}
393
394/// Filter hop-by-hop headers from a header list, returning only end-to-end headers.
395///
396/// Removes standard hop-by-hop headers plus any headers nominated by
397/// `Connection` tokens.  Preserves `Transfer-Encoding: chunked`.
398pub fn filter_hop_by_hop(headers: &[(String, String)]) -> Vec<(String, String)> {
399    let nominated = connection_tokens(headers);
400    headers
401        .iter()
402        .filter(|(name, value)| {
403            let lower = name.to_ascii_lowercase();
404            !is_hop_by_hop_header(&lower, value) && !nominated.contains(&lower)
405        })
406        .cloned()
407        .collect()
408}
409
410/// Return whether the request contains an expectation this forwarder cannot
411/// negotiate. Expectation values are collected case-insensitively and comma-
412/// separated values are treated independently.
413pub fn has_unsupported_expectation(headers: &[(String, String)]) -> bool {
414    headers.iter().any(|(name, value)| {
415        name.eq_ignore_ascii_case("Expect")
416            && value
417                .split(',')
418                .any(|expectation| !expectation.trim().is_empty())
419    })
420}
421
422/// Build an origin-form HTTP request to send to the upstream server.
423///
424/// Converts the parsed absolute-form request into origin-form by:
425/// - Using only the path component as the request target
426/// - Filtering out hop-by-hop headers
427/// - Adding `Connection: close` to avoid keep-alive complications
428pub fn build_origin_request(request: &ForwardRequest) -> String {
429    let filtered = filter_hop_by_hop(&request.headers);
430
431    let mut req = format!(
432        "{} {} {}\r\n",
433        request.method, request.path, request.version
434    );
435
436    for (name, value) in &filtered {
437        req.push_str(&format!("{}: {}\r\n", name, value));
438    }
439
440    // Ensure Connection: close for Phase 1 (no persistent forwarding)
441    if !filtered
442        .iter()
443        .any(|(n, _)| n.eq_ignore_ascii_case("Connection"))
444    {
445        req.push_str("Connection: close\r\n");
446    }
447
448    req.push_str("\r\n");
449    req
450}
451
452/// Parsed HTTP response from an upstream server.
453#[derive(Debug)]
454pub struct ForwardResponse {
455    pub version: String,
456    pub status: u16,
457    pub reason: String,
458    pub headers: Vec<(String, String)>,
459    pub content_length: Option<u64>,
460    pub is_chunked: bool,
461    /// True if the upstream sent `Connection: close`.
462    pub connection_close: bool,
463}
464
465/// Read and parse an HTTP response head from the upstream.
466///
467/// `stream` is typically a `BufReader<&mut BoxStream>` so per-byte header
468/// scans are served from the 8 KiB buffer rather than issuing a syscall per
469/// header byte (B-01).
470async fn read_response_head<R: AsyncRead + Unpin>(
471    stream: &mut R,
472) -> Result<ForwardResponse, HttpError> {
473    let mut head_buf = Vec::with_capacity(1024);
474    let mut temp = [0u8; 1];
475    // Track `\r\n` occurrences during the read so that a flood of empty
476    // header lines cannot slip under MAX_RESPONSE_HEAD_SIZE while still
477    // exceeding the per-line limit.
478    let mut crlf_count: usize = 0;
479
480    loop {
481        if head_buf.len() >= MAX_RESPONSE_HEAD_SIZE {
482            return Err(HttpError::HeaderTooLarge);
483        }
484
485        let n = stream.read(&mut temp).await?;
486        if n == 0 {
487            return Err(HttpError::MalformedResponse(
488                "unexpected EOF reading response".into(),
489            ));
490        }
491
492        head_buf.push(temp[0]);
493
494        if head_buf.len() >= 2 {
495            let len = head_buf.len();
496            if &head_buf[len - 2..] == b"\r\n" {
497                crlf_count += 1;
498                // The status line and the terminating empty line also have
499                // CRLFs, but are not header lines.
500                if crlf_count > MAX_HEADER_LINES + 2 {
501                    return Err(HttpError::TooManyHeaders);
502                }
503            }
504        }
505
506        if head_buf.len() >= 4 {
507            let len = head_buf.len();
508            if &head_buf[len - 4..] == b"\r\n\r\n" {
509                break;
510            }
511        }
512    }
513
514    let head_str = String::from_utf8_lossy(&head_buf);
515    let mut lines = head_str.split("\r\n");
516
517    // Parse status line
518    let status_line = lines
519        .next()
520        .ok_or_else(|| HttpError::MalformedResponse("empty response".into()))?;
521
522    let parts: Vec<&str> = status_line.split_whitespace().collect();
523    if parts.len() < 2 {
524        return Err(HttpError::MalformedResponse(format!(
525            "invalid status line: {}",
526            status_line
527        )));
528    }
529
530    let version = parts[0].to_string();
531    let status: u16 = parts[1]
532        .parse()
533        .map_err(|e| HttpError::MalformedResponse(format!("invalid status code: {}", e)))?;
534    let reason = parts.get(2).unwrap_or(&"").to_string();
535
536    // Parse response headers
537    let mut headers = Vec::new();
538    let mut content_length = None;
539    let mut is_chunked = false;
540    let mut connection_close = false;
541
542    let mut header_count = 0;
543    for line in lines {
544        if line.is_empty() {
545            break;
546        }
547        header_count += 1;
548        if header_count > MAX_HEADER_LINES {
549            return Err(HttpError::TooManyHeaders);
550        }
551        if let Some((name, value)) = parse_header_line(line) {
552            if name.eq_ignore_ascii_case("Content-Length") {
553                let parsed = value
554                    .parse::<u64>()
555                    .map_err(|_| HttpError::InvalidContentLength)?;
556                if content_length.is_some_and(|previous| previous != parsed) {
557                    return Err(HttpError::ConflictingContentLength);
558                }
559                content_length = Some(parsed);
560            } else if name.eq_ignore_ascii_case("Transfer-Encoding") {
561                for coding in value.split(',') {
562                    let coding_name = coding.trim().split(';').next().unwrap_or("").trim();
563                    if coding_name.eq_ignore_ascii_case("chunked") {
564                        is_chunked = true;
565                    }
566                }
567            } else if name.eq_ignore_ascii_case("Connection") {
568                // Check for "close" token (case-insensitive)
569                connection_close = value
570                    .split(',')
571                    .any(|t| t.trim().eq_ignore_ascii_case("close"));
572            }
573            headers.push((name, value));
574        }
575    }
576
577    // Per RFC 7230 §3.3.3, when both Content-Length and Transfer-Encoding
578    // are present, Transfer-Encoding takes precedence. A proxy MUST ignore
579    // Content-Length if Transfer-Encoding is present.
580    if is_chunked {
581        content_length = None;
582    }
583
584    Ok(ForwardResponse {
585        version,
586        status,
587        reason,
588        headers,
589        content_length,
590        is_chunked,
591        connection_close,
592    })
593}
594
595fn format_response_head(
596    response: &ForwardResponse,
597    force_close: bool,
598) -> Result<String, HttpError> {
599    let filtered = filter_hop_by_hop(&response.headers);
600    if !response
601        .reason
602        .bytes()
603        .all(|byte| (0x20..=0x7e).contains(&byte))
604    {
605        return Err(HttpError::MalformedResponse(
606            "response reason contains non-printable bytes".into(),
607        ));
608    }
609    let mut head = format!("HTTP/1.1 {} {}\r\n", response.status, response.reason);
610
611    for (name, value) in &filtered {
612        if name.contains(['\r', '\n']) || value.contains(['\r', '\n']) {
613            return Err(HttpError::MalformedResponse(
614                "response header contains a line break".into(),
615            ));
616        }
617        head.push_str(&format!("{}: {}\r\n", name, value));
618    }
619
620    if force_close
621        && !filtered
622            .iter()
623            .any(|(n, _)| n.eq_ignore_ascii_case("Connection"))
624    {
625        head.push_str("Connection: close\r\n");
626    }
627
628    head.push_str("\r\n");
629    Ok(head)
630}
631
632/// Result of forwarding a response, including upstream connection state.
633pub struct ForwardResult {
634    pub report: ForwardResponseReport,
635    /// HTTP status code of the forwarded response.
636    pub status: u16,
637    /// True if the upstream connection is still usable (no `Connection: close`).
638    pub upstream_alive: bool,
639    /// True if the response status indicates the client should not retry.
640    pub client_should_close: bool,
641}
642
643/// Forward the upstream response back to the client stream.
644///
645/// Writes the response status line and filtered headers to the client,
646/// then relays the body (if any) using content-length or chunked framing.
647pub async fn forward_response(
648    upstream: &mut BoxStream,
649    client: &mut BoxStream,
650) -> Result<ForwardResult, HttpError> {
651    // Buffer upstream reads so single-byte header scans do not issue a
652    // syscall per header byte (B-01/O-01). The BufReader preserves any
653    // bytes read ahead of the body for the subsequent relay loops.
654    let mut upstream_buf = tokio::io::BufReader::new(&mut *upstream);
655    let mut informational_responses = 0;
656    let mut bytes_forwarded: u64 = 0;
657    let response = loop {
658        let response = read_response_head(&mut upstream_buf).await?;
659        if response.status == 101 {
660            return Err(HttpError::UpgradeUnsupported);
661        }
662        if (100..200).contains(&response.status) {
663            informational_responses += 1;
664            if informational_responses > MAX_INFORMATIONAL_RESPONSES {
665                return Err(HttpError::TooManyInformationalResponses);
666            }
667            let head = format_response_head(&response, false)?;
668            client.write_all(head.as_bytes()).await?;
669            bytes_forwarded += head.len() as u64;
670            continue;
671        }
672        break response;
673    };
674    let head = format_response_head(&response, true)?;
675    client.write_all(head.as_bytes()).await?;
676    bytes_forwarded += head.len() as u64;
677
678    // Relay body based on framing
679    let mut eof_framing = false;
680    match (response.content_length, response.is_chunked) {
681        (Some(len), _) => {
682            let mut remaining = len;
683            let mut buf = [0u8; 8192];
684            while remaining > 0 {
685                let to_read = (remaining as usize).min(buf.len());
686                let n = upstream_buf.read(&mut buf[..to_read]).await?;
687                if n == 0 {
688                    return Err(HttpError::MalformedResponse(
689                        "unexpected EOF in response body".into(),
690                    ));
691                }
692                client.write_all(&buf[..n]).await?;
693                bytes_forwarded += n as u64;
694                remaining -= n as u64;
695            }
696        }
697        (None, true) => {
698            let mut size_line_buf = Vec::new();
699            loop {
700                size_line_buf.clear();
701                read_bounded_line_into(&mut upstream_buf, &mut size_line_buf, 1024).await?;
702
703                let size_str = String::from_utf8_lossy(&size_line_buf);
704                let size_str = size_str.trim_end_matches("\r\n");
705                let size_str = size_str.split(';').next().unwrap_or("").trim();
706                let chunk_size = u64::from_str_radix(size_str, 16).map_err(|e| {
707                    HttpError::MalformedResponse(format!("invalid chunk size: {}", e))
708                })?;
709                if chunk_size > MAX_RESPONSE_CHUNK_SIZE {
710                    return Err(HttpError::MalformedResponse(
711                        "response chunk too large".into(),
712                    ));
713                }
714
715                client.write_all(&size_line_buf).await?;
716                bytes_forwarded += size_line_buf.len() as u64;
717
718                if chunk_size == 0 {
719                    let mut trailer_total = 0usize;
720                    loop {
721                        let mut trailer = Vec::new();
722                        read_bounded_line_into(&mut upstream_buf, &mut trailer, 8192).await?;
723                        trailer_total += trailer.len();
724                        if trailer_total > MAX_TRAILER_BYTES {
725                            return Err(HttpError::MalformedResponse(
726                                "response trailers exceed maximum total size".into(),
727                            ));
728                        }
729                        client.write_all(&trailer).await?;
730                        bytes_forwarded += trailer.len() as u64;
731                        if trailer == b"\r\n" {
732                            break;
733                        }
734                        if !trailer.ends_with(b"\r\n") {
735                            break;
736                        }
737                    }
738                    break;
739                }
740
741                let mut remaining =
742                    usize::try_from(chunk_size.checked_add(2).ok_or_else(|| {
743                        HttpError::MalformedResponse("response chunk size overflow".into())
744                    })?)
745                    .map_err(|_| HttpError::MalformedResponse("response chunk too large".into()))?;
746                let mut buf = [0u8; 8192];
747                while remaining > 0 {
748                    let to_read = remaining.min(buf.len());
749                    let n = upstream_buf.read(&mut buf[..to_read]).await?;
750                    if n == 0 {
751                        return Ok(ForwardResult {
752                            report: ForwardResponseReport { bytes_forwarded },
753                            status: response.status,
754                            upstream_alive: false,
755                            client_should_close: true,
756                        });
757                    }
758                    client.write_all(&buf[..n]).await?;
759                    bytes_forwarded += n as u64;
760                    remaining -= n;
761                }
762            }
763        }
764        (None, false) => {
765            // No Content-Length and no Transfer-Encoding: the response body
766            // ends at connection close, so the upstream is fully drained.
767            eof_framing = true;
768            let mut buf = [0u8; 8192];
769            loop {
770                let n = upstream_buf.read(&mut buf).await?;
771                if n == 0 {
772                    break;
773                }
774                client.write_all(&buf[..n]).await?;
775                bytes_forwarded += n as u64;
776            }
777        }
778    }
779
780    // Determine upstream alive: HTTP/1.1 default is keep-alive, HTTP/1.0 default is close
781    let mut upstream_alive = if response.connection_close {
782        false
783    } else if response.version.contains("1.1") {
784        true
785    } else {
786        // HTTP/1.0: alive only if explicitly requested via Keep-Alive
787        response
788            .headers
789            .iter()
790            .any(|(n, v)| n.eq_ignore_ascii_case("Keep-Alive") && !v.is_empty())
791    };
792    if eof_framing {
793        // A connection closed by EOF framing can never be reused.
794        upstream_alive = false;
795    }
796
797    // Client should close if the upstream said close
798    let client_should_close = response.connection_close;
799
800    Ok(ForwardResult {
801        report: ForwardResponseReport { bytes_forwarded },
802        status: response.status,
803        upstream_alive,
804        client_should_close,
805    })
806}
807
808/// A parsed HTTP request ready for forwarding.
809#[derive(Debug, Clone)]
810pub struct ForwardRequest {
811    pub method: String,
812    pub path: String,
813    pub version: String,
814    pub headers: Vec<(String, String)>,
815    pub target: TargetAddr,
816    pub has_body: bool,
817    pub content_length: Option<u64>,
818    pub is_chunked: bool,
819    /// True if the client sent `Connection: close`.
820    pub connection_close: bool,
821}
822
823impl ForwardRequest {
824    /// Compute the request body kind from parsed fields.
825    pub fn body_kind(&self) -> RequestBodyKind {
826        if self.is_chunked {
827            RequestBodyKind::Chunked
828        } else if let Some(len) = self.content_length {
829            RequestBodyKind::ContentLength(len)
830        } else {
831            RequestBodyKind::None
832        }
833    }
834}
835
836/// Forward an HTTP request from a client to the target server.
837///
838/// Parses the absolute-form request, converts to origin-form, forwards
839/// the request, and returns the response.
840///
841/// # Arguments
842/// * `stream` - The client stream
843///
844/// # Returns
845/// The parsed forward request and the target address to connect to.
846pub async fn forward_request(stream: BoxStream) -> Result<(ForwardRequest, BoxStream), HttpError> {
847    // Buffer reads so the incremental head parse does not issue one
848    // syscall per byte; unconsumed prefetch stays available to later
849    // reads on the returned stream (including keep-alive re-parses).
850    let mut stream: BoxStream = Box::new(tokio::io::BufReader::new(stream));
851    let request = read_forward_request(&mut stream).await?;
852    Ok((request, stream))
853}
854
855/// Read and parse an HTTP forward request from an existing stream.
856///
857/// Unlike [`forward_request`], this borrows the stream rather than
858/// consuming it, enabling persistent-session loops.
859pub async fn forward_request_stream(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
860    read_forward_request(stream).await
861}
862
863/// Read and parse an HTTP forward request with absolute-form target.
864async fn read_forward_request(stream: &mut BoxStream) -> Result<ForwardRequest, HttpError> {
865    let mut head_buf = Vec::with_capacity(1024);
866    let mut temp = [0u8; 1];
867    let mut header_count = 0;
868    let mut saw_request_line = false;
869
870    loop {
871        if head_buf.len() >= MAX_HEAD_SIZE {
872            return Err(HttpError::HeaderTooLarge);
873        }
874
875        let n = stream.read(&mut temp).await?;
876        if n == 0 {
877            return Err(HttpError::MalformedRequest(
878                "unexpected EOF reading request".into(),
879            ));
880        }
881
882        head_buf.push(temp[0]);
883
884        // Check for end of headers
885        if head_buf.len() >= 4 {
886            let len = head_buf.len();
887            if &head_buf[len - 4..] == b"\r\n\r\n" {
888                break;
889            }
890            if head_buf.len() >= 2 && &head_buf[len - 2..] == b"\r\n" {
891                // The first CRLF terminates the request line and is not a
892                // header. The final empty line is handled by the terminator
893                // check above, so only actual header lines are counted.
894                if saw_request_line {
895                    header_count += 1;
896                } else {
897                    saw_request_line = true;
898                }
899                if header_count > MAX_HEADER_LINES {
900                    return Err(HttpError::TooManyHeaders);
901                }
902            }
903        }
904    }
905
906    let head_str = String::from_utf8_lossy(&head_buf);
907    let mut lines = head_str.split("\r\n");
908
909    // Parse request line
910    let request_line = lines
911        .next()
912        .ok_or_else(|| HttpError::MalformedRequest("empty request".into()))?;
913
914    let parts: Vec<&str> = request_line.split_whitespace().collect();
915    if parts.len() != 3 {
916        return Err(HttpError::MalformedRequest(format!(
917            "expected 3 parts in request line, got {}",
918            parts.len()
919        )));
920    }
921
922    let method = parts[0].to_string();
923    let raw_target = parts[1].to_string();
924    let version = parts[2].to_string();
925    if version != "HTTP/1.0" && version != "HTTP/1.1" {
926        return Err(HttpError::MalformedRequest(format!(
927            "unsupported HTTP version: {version}"
928        )));
929    }
930
931    // Parse absolute-form target: http://host:port/path
932    let (target, path) = parse_absolute_uri(&raw_target)?;
933
934    // Parse headers
935    let mut headers = Vec::new();
936
937    for line in lines {
938        if line.is_empty() {
939            break;
940        }
941        if let Some((name, value)) = parse_header_line(line) {
942            // Skip Proxy-Authorization header (don't forward it)
943            if name.eq_ignore_ascii_case("Proxy-Authorization") {
944                continue;
945            }
946
947            headers.push((name, value));
948        }
949    }
950
951    // Determine body framing from headers
952    let body_kind = determine_request_body_kind(&headers)?;
953    let (has_body, content_length, is_chunked) = match body_kind {
954        RequestBodyKind::None => (false, None, false),
955        RequestBodyKind::ContentLength(len) => (len > 0, Some(len), false),
956        RequestBodyKind::Chunked => (true, None, true),
957    };
958
959    // Determine Connection: close
960    let connection_close = headers.iter().any(|(n, v)| {
961        n.eq_ignore_ascii_case("Connection")
962            && v.split(',').any(|t| t.trim().eq_ignore_ascii_case("close"))
963    });
964
965    Ok(ForwardRequest {
966        method,
967        path,
968        version,
969        headers,
970        target,
971        has_body,
972        content_length,
973        is_chunked,
974        connection_close,
975    })
976}
977
978/// Parse an absolute-form URI into target and path.
979///
980/// Supports: http://host:port/path, http://host/path
981fn parse_absolute_uri(uri: &str) -> Result<(TargetAddr, String), HttpError> {
982    // Remove scheme and determine default port
983    let (rest, default_port) = if let Some(stripped) = uri.strip_prefix("http://") {
984        (stripped, 80)
985    } else if let Some(stripped) = uri.strip_prefix("https://") {
986        // For HTTPS, we'd need TLS, but for now treat as HTTP
987        (stripped, 443)
988    } else {
989        return Err(HttpError::MalformedRequest(format!(
990            "absolute URI required, got: {}",
991            uri
992        )));
993    };
994
995    // Find path separator
996    let path_start = rest.find('/').unwrap_or(rest.len());
997    let authority = &rest[..path_start];
998    let path = if path_start < rest.len() {
999        &rest[path_start..]
1000    } else {
1001        "/"
1002    };
1003
1004    // Parse authority with default port
1005    let target = parse_authority_with_default(authority, default_port)?;
1006
1007    Ok((target, path.to_string()))
1008}
1009
1010/// Parse an authority (host:port) into a TargetAddr with a default port.
1011fn parse_authority_with_default(
1012    authority: &str,
1013    default_port: u16,
1014) -> Result<TargetAddr, HttpError> {
1015    // Handle IPv6 bracketed addresses
1016    if authority.starts_with('[') {
1017        let bracket_end = authority.find(']').ok_or_else(|| {
1018            HttpError::TargetParseError("unclosed bracket in IPv6 address".into())
1019        })?;
1020
1021        let ip_str = &authority[1..bracket_end];
1022        let ip: std::net::IpAddr = ip_str
1023            .parse()
1024            .map_err(|e| HttpError::TargetParseError(format!("invalid IPv6 address: {}", e)))?;
1025
1026        // Check for port after bracket
1027        let port = if authority
1028            .as_bytes()
1029            .get(bracket_end + 1)
1030            .is_some_and(|&b| b == b':')
1031        {
1032            let port_str = authority.get(bracket_end + 2..).ok_or_else(|| {
1033                HttpError::TargetParseError("missing port after IPv6 address".into())
1034            })?;
1035            port_str
1036                .parse()
1037                .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?
1038        } else {
1039            default_port
1040        };
1041
1042        return Ok(TargetAddr {
1043            host: TargetHost::Ip(ip),
1044            port,
1045        });
1046    }
1047
1048    // Find the last ':' to split host and port
1049    let colon_pos = authority.rfind(':');
1050
1051    let (host_str, port) = if let Some(colon_pos) = colon_pos {
1052        let host_str = &authority[..colon_pos];
1053        let port_str = &authority[colon_pos + 1..];
1054        let port: u16 = port_str
1055            .parse()
1056            .map_err(|e| HttpError::TargetParseError(format!("invalid port: {}", e)))?;
1057        (host_str, port)
1058    } else {
1059        (authority, default_port)
1060    };
1061
1062    // Try to parse as IP first
1063    if let Ok(ip) = host_str.parse::<std::net::IpAddr>() {
1064        return Ok(TargetAddr {
1065            host: TargetHost::Ip(ip),
1066            port,
1067        });
1068    }
1069
1070    // Otherwise treat as domain
1071    if host_str.is_empty() {
1072        return Err(HttpError::TargetParseError("empty host".into()));
1073    }
1074
1075    Ok(TargetAddr {
1076        host: TargetHost::Domain(host_str.to_string()),
1077        port,
1078    })
1079}
1080
1081/// Parse a header line into (name, value).
1082///
1083/// Rejects header names or values containing control characters (NUL, CR, LF)
1084/// per RFC 7230 §3.2.4.
1085fn parse_header_line(line: &str) -> Option<(String, String)> {
1086    let colon_pos = line.find(':')?;
1087    let name = line[..colon_pos].trim().to_string();
1088    let value = line[colon_pos + 1..].trim().to_string();
1089
1090    if name.is_empty() {
1091        return None;
1092    }
1093    if name.bytes().any(|b| b == b'\0' || b == b'\r' || b == b'\n') {
1094        return None;
1095    }
1096    if value
1097        .bytes()
1098        .any(|b| b == b'\0' || b == b'\r' || b == b'\n')
1099    {
1100        return None;
1101    }
1102
1103    Some((name, value))
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108    use super::*;
1109
1110    #[test]
1111    fn test_parse_absolute_uri() {
1112        let (target, path) = parse_absolute_uri("http://example.com:8080/path").unwrap();
1113        assert_eq!(
1114            target,
1115            TargetAddr {
1116                host: TargetHost::Domain("example.com".to_string()),
1117                port: 8080,
1118            }
1119        );
1120        assert_eq!(path, "/path");
1121    }
1122
1123    #[test]
1124    fn test_parse_absolute_uri_no_path() {
1125        let (target, path) = parse_absolute_uri("http://example.com:80").unwrap();
1126        assert_eq!(
1127            target,
1128            TargetAddr {
1129                host: TargetHost::Domain("example.com".to_string()),
1130                port: 80,
1131            }
1132        );
1133        assert_eq!(path, "/");
1134    }
1135
1136    #[test]
1137    fn test_parse_absolute_uri_ipv4() {
1138        let (target, path) = parse_absolute_uri("http://192.168.1.1:3000/api").unwrap();
1139        assert_eq!(
1140            target,
1141            TargetAddr {
1142                host: TargetHost::Ip("192.168.1.1".parse().unwrap()),
1143                port: 3000,
1144            }
1145        );
1146        assert_eq!(path, "/api");
1147    }
1148
1149    #[test]
1150    fn test_parse_absolute_uri_no_scheme() {
1151        assert!(parse_absolute_uri("example.com/path").is_err());
1152    }
1153
1154    #[test]
1155    fn test_parse_header_line() {
1156        let (name, value) = parse_header_line("Content-Type: text/html").unwrap();
1157        assert_eq!(name, "Content-Type");
1158        assert_eq!(value, "text/html");
1159    }
1160
1161    #[test]
1162    fn test_parse_header_line_no_colon() {
1163        assert!(parse_header_line("NoColon").is_none());
1164    }
1165
1166    #[test]
1167    fn test_filter_hop_by_hop_connection_nominated() {
1168        let headers = vec![
1169            ("Connection".into(), "X-Custom, Keep-Alive".into()),
1170            ("X-Custom".into(), "value".into()),
1171            ("Keep-Alive".into(), "timeout=5".into()),
1172            ("Content-Type".into(), "text/html".into()),
1173        ];
1174        let filtered = filter_hop_by_hop(&headers);
1175        // X-Custom and Keep-Alive should be removed (nominated by Connection),
1176        // plus connection itself is always removed.
1177        assert_eq!(filtered.len(), 1);
1178        assert_eq!(filtered[0].0, "Content-Type");
1179    }
1180
1181    #[test]
1182    fn test_filter_hop_by_hop_preserves_transfer_encoding_chunked() {
1183        let headers = vec![
1184            ("Transfer-Encoding".into(), "chunked".into()),
1185            ("Content-Type".into(), "application/json".into()),
1186        ];
1187        let filtered = filter_hop_by_hop(&headers);
1188        assert_eq!(filtered.len(), 2);
1189        assert!(filtered.iter().any(|(n, _)| n == "Transfer-Encoding"));
1190    }
1191
1192    #[test]
1193    fn test_filter_hop_by_hop_removes_transfer_encoding_non_chunked() {
1194        let headers = vec![
1195            ("Transfer-Encoding".into(), "gzip".into()),
1196            ("Content-Type".into(), "text/html".into()),
1197        ];
1198        let filtered = filter_hop_by_hop(&headers);
1199        assert_eq!(filtered.len(), 1);
1200        assert_eq!(filtered[0].0, "Content-Type");
1201    }
1202
1203    #[test]
1204    fn test_filter_connection_tokens_empty() {
1205        let headers = vec![("Content-Type".into(), "text/html".into())];
1206        let tokens = connection_tokens(&headers);
1207        assert!(tokens.is_empty());
1208    }
1209
1210    #[test]
1211    fn test_filter_connection_tokens_multiple() {
1212        let headers = vec![("Connection".into(), "close, Upgrade".into())];
1213        let tokens = connection_tokens(&headers);
1214        assert!(tokens.contains("close"));
1215        assert!(tokens.contains("upgrade"));
1216    }
1217
1218    #[test]
1219    fn test_determine_body_none() {
1220        let headers = vec![("Host".into(), "example.com".into())];
1221        assert_eq!(
1222            determine_request_body_kind(&headers).unwrap(),
1223            RequestBodyKind::None
1224        );
1225    }
1226
1227    #[test]
1228    fn test_determine_body_content_length() {
1229        let headers = vec![("Content-Length".into(), "42".into())];
1230        assert_eq!(
1231            determine_request_body_kind(&headers).unwrap(),
1232            RequestBodyKind::ContentLength(42)
1233        );
1234    }
1235
1236    #[test]
1237    fn test_determine_body_duplicate_equal_cl() {
1238        let headers = vec![
1239            ("Content-Length".into(), "42".into()),
1240            ("Content-Length".into(), "42".into()),
1241        ];
1242        assert_eq!(
1243            determine_request_body_kind(&headers).unwrap(),
1244            RequestBodyKind::ContentLength(42)
1245        );
1246    }
1247
1248    #[test]
1249    fn test_determine_body_conflicting_cl() {
1250        let headers = vec![
1251            ("Content-Length".into(), "42".into()),
1252            ("Content-Length".into(), "100".into()),
1253        ];
1254        assert!(matches!(
1255            determine_request_body_kind(&headers),
1256            Err(HttpError::ConflictingContentLength)
1257        ));
1258    }
1259
1260    #[test]
1261    fn test_determine_body_invalid_cl() {
1262        let headers = vec![("Content-Length".into(), "abc".into())];
1263        assert!(matches!(
1264            determine_request_body_kind(&headers),
1265            Err(HttpError::InvalidContentLength)
1266        ));
1267    }
1268
1269    #[test]
1270    fn test_determine_body_chunked() {
1271        let headers = vec![("Transfer-Encoding".into(), "chunked".into())];
1272        assert_eq!(
1273            determine_request_body_kind(&headers).unwrap(),
1274            RequestBodyKind::Chunked
1275        );
1276    }
1277
1278    #[test]
1279    fn test_determine_body_te_plus_cl() {
1280        let headers = vec![
1281            ("Transfer-Encoding".into(), "chunked".into()),
1282            ("Content-Length".into(), "42".into()),
1283        ];
1284        assert!(matches!(
1285            determine_request_body_kind(&headers),
1286            Err(HttpError::TransferEncodingWithContentLength)
1287        ));
1288    }
1289
1290    #[test]
1291    fn test_determine_body_unsupported_te() {
1292        let headers = vec![("Transfer-Encoding".into(), "gzip".into())];
1293        assert!(matches!(
1294            determine_request_body_kind(&headers),
1295            Err(HttpError::UnsupportedTransferEncoding(_))
1296        ));
1297    }
1298
1299    #[test]
1300    fn test_determine_body_chunked_not_final() {
1301        let headers = vec![("Transfer-Encoding".into(), "chunked, gzip".into())];
1302        assert!(matches!(
1303            determine_request_body_kind(&headers),
1304            Err(HttpError::ChunkedNotFinal)
1305        ));
1306    }
1307
1308    #[test]
1309    fn test_determine_body_mixed_header_casing() {
1310        let headers = vec![
1311            ("content-length".into(), "42".into()),
1312            ("CONTENT-LENGTH".into(), "42".into()),
1313        ];
1314        assert_eq!(
1315            determine_request_body_kind(&headers).unwrap(),
1316            RequestBodyKind::ContentLength(42)
1317        );
1318    }
1319
1320    // ===== Body copy tests =====
1321
1322    #[tokio::test]
1323    async fn test_copy_chunked_body_simple() {
1324        let input = b"5\r\nhello\r\n0\r\n\r\n";
1325        let mut reader = &input[..];
1326        let mut writer = Vec::new();
1327        let limits = BodyCopyLimits::default();
1328
1329        let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1330            .await
1331            .unwrap();
1332        assert_eq!(report.decoded_bytes, 5);
1333        assert_eq!(writer, input);
1334    }
1335
1336    #[tokio::test]
1337    async fn test_copy_chunked_body_multiple_chunks() {
1338        let input = b"5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
1339        let mut reader = &input[..];
1340        let mut writer = Vec::new();
1341        let limits = BodyCopyLimits::default();
1342
1343        let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1344            .await
1345            .unwrap();
1346        assert_eq!(report.decoded_bytes, 11);
1347        assert_eq!(writer, input);
1348    }
1349
1350    #[tokio::test]
1351    async fn test_copy_chunked_body_uppercase_hex() {
1352        let input = b"5\r\nhello\r\n0\r\n\r\n";
1353        let mut reader = &input[..];
1354        let mut writer = Vec::new();
1355        let limits = BodyCopyLimits::default();
1356
1357        let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1358            .await
1359            .unwrap();
1360        assert_eq!(report.decoded_bytes, 5);
1361    }
1362
1363    #[tokio::test]
1364    async fn test_copy_chunked_body_with_extension() {
1365        let input = b"5;ext=value\r\nhello\r\n0\r\n\r\n";
1366        let mut reader = &input[..];
1367        let mut writer = Vec::new();
1368        let limits = BodyCopyLimits::default();
1369
1370        let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1371            .await
1372            .unwrap();
1373        assert_eq!(report.decoded_bytes, 5);
1374    }
1375
1376    #[tokio::test]
1377    async fn test_copy_chunked_body_with_trailer() {
1378        let input = b"5\r\nhello\r\n0\r\nTrailer: value\r\n\r\n";
1379        let mut reader = &input[..];
1380        let mut writer = Vec::new();
1381        let limits = BodyCopyLimits::default();
1382
1383        let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits)
1384            .await
1385            .unwrap();
1386        assert_eq!(report.decoded_bytes, 5);
1387    }
1388
1389    #[tokio::test]
1390    async fn test_copy_chunked_body_malformed_hex() {
1391        let input = b"ZZ\r\nhello\r\n0\r\n\r\n";
1392        let mut reader = &input[..];
1393        let mut writer = Vec::new();
1394        let limits = BodyCopyLimits::default();
1395
1396        let result =
1397            copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1398        assert!(result.is_err());
1399    }
1400
1401    #[tokio::test]
1402    async fn test_copy_chunked_body_empty_size() {
1403        let input = b"\r\nhello\r\n0\r\n\r\n";
1404        let mut reader = &input[..];
1405        let mut writer = Vec::new();
1406        let limits = BodyCopyLimits::default();
1407
1408        let result =
1409            copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1410        assert!(result.is_err());
1411    }
1412
1413    #[tokio::test]
1414    async fn test_copy_chunked_body_missing_crlf() {
1415        let input = b"5\r\nhelloX\r\n0\r\n\r\n";
1416        let mut reader = &input[..];
1417        let mut writer = Vec::new();
1418        let limits = BodyCopyLimits::default();
1419
1420        let result =
1421            copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1422        assert!(result.is_err());
1423    }
1424
1425    #[tokio::test]
1426    async fn test_copy_chunked_body_oversized_chunk() {
1427        let input = b"FFFFFFFFFFFFFFFF\r\nhello\r\n0\r\n\r\n";
1428        let mut reader = &input[..];
1429        let mut writer = Vec::new();
1430        let limits = BodyCopyLimits {
1431            max_chunk_size: 1024,
1432            ..Default::default()
1433        };
1434
1435        let result =
1436            copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1437        assert!(result.is_err());
1438    }
1439
1440    #[tokio::test]
1441    async fn test_copy_content_length_body() {
1442        let input = b"hello world";
1443        let mut reader = &input[..];
1444        let mut writer = Vec::new();
1445        let limits = BodyCopyLimits::default();
1446
1447        let report = copy_request_body(
1448            &mut reader,
1449            &mut writer,
1450            RequestBodyKind::ContentLength(11),
1451            &limits,
1452        )
1453        .await
1454        .unwrap();
1455        assert_eq!(report.wire_bytes, 11);
1456        assert_eq!(report.decoded_bytes, 11);
1457        assert_eq!(writer, input);
1458    }
1459
1460    #[tokio::test]
1461    async fn test_copy_none_body() {
1462        let mut reader = &b""[..];
1463        let mut writer = Vec::new();
1464        let limits = BodyCopyLimits::default();
1465
1466        let report = copy_request_body(&mut reader, &mut writer, RequestBodyKind::None, &limits)
1467            .await
1468            .unwrap();
1469        assert_eq!(report.wire_bytes, 0);
1470        assert_eq!(report.decoded_bytes, 0);
1471    }
1472
1473    #[tokio::test]
1474    async fn test_copy_content_length_body_premature_eof() {
1475        let input = b"hel"; // only 3 bytes but Content-Length says 11
1476        let mut reader = &input[..];
1477        let mut writer = Vec::new();
1478        let limits = BodyCopyLimits::default();
1479
1480        let result = copy_request_body(
1481            &mut reader,
1482            &mut writer,
1483            RequestBodyKind::ContentLength(11),
1484            &limits,
1485        )
1486        .await;
1487        assert!(result.is_err());
1488        let err = result.unwrap_err();
1489        let msg = format!("{}", err);
1490        assert!(
1491            msg.contains("unexpected EOF"),
1492            "error should mention EOF: {}",
1493            msg
1494        );
1495    }
1496
1497    #[tokio::test]
1498    async fn test_copy_content_length_body_zero_length() {
1499        let input = b"";
1500        let mut reader = &input[..];
1501        let mut writer = Vec::new();
1502        let limits = BodyCopyLimits::default();
1503
1504        let report = copy_request_body(
1505            &mut reader,
1506            &mut writer,
1507            RequestBodyKind::ContentLength(0),
1508            &limits,
1509        )
1510        .await
1511        .unwrap();
1512        assert_eq!(report.wire_bytes, 0);
1513        assert_eq!(report.decoded_bytes, 0);
1514    }
1515
1516    #[tokio::test]
1517    async fn test_copy_chunked_body_decoded_limit_exceeded() {
1518        // A single valid chunk of 100 bytes but max_decoded_body is 10
1519        let chunk_data = "x".repeat(100);
1520        let input = format!("64\r\n{}\r\n0\r\n\r\n", chunk_data);
1521        let mut reader = input.as_bytes();
1522        let mut writer = Vec::new();
1523        let limits = BodyCopyLimits {
1524            max_decoded_body: 10,
1525            ..Default::default()
1526        };
1527
1528        let result =
1529            copy_request_body(&mut reader, &mut writer, RequestBodyKind::Chunked, &limits).await;
1530        assert!(result.is_err());
1531        let msg = format!("{}", result.unwrap_err());
1532        assert!(
1533            msg.contains("decoded body too large"),
1534            "error should mention decoded body limit: {}",
1535            msg
1536        );
1537    }
1538
1539    // ===== Phase 2: HTTP framing and connection-state invariants =====
1540
1541    #[test]
1542    fn test_te_plus_cl_rejected_not_forwarded() {
1543        let headers = vec![
1544            ("Transfer-Encoding".into(), "chunked".into()),
1545            ("Content-Length".into(), "0".into()),
1546        ];
1547        let result = determine_request_body_kind(&headers);
1548        assert!(
1549            matches!(result, Err(HttpError::TransferEncodingWithContentLength)),
1550            "TE+CL must be rejected to prevent ambiguous framing: {:?}",
1551            result
1552        );
1553    }
1554
1555    #[test]
1556    fn test_conflicting_cl_values_rejected() {
1557        let headers = vec![
1558            ("Content-Length".into(), "10".into()),
1559            ("Content-Length".into(), "20".into()),
1560        ];
1561        let result = determine_request_body_kind(&headers);
1562        assert!(
1563            matches!(result, Err(HttpError::ConflictingContentLength)),
1564            "conflicting CL values must be rejected: {:?}",
1565            result
1566        );
1567    }
1568
1569    #[test]
1570    fn test_equal_duplicate_cl_deterministic() {
1571        let headers = vec![
1572            ("Content-Length".into(), "42".into()),
1573            ("Content-Length".into(), "42".into()),
1574        ];
1575        let kind = determine_request_body_kind(&headers).unwrap();
1576        assert_eq!(kind, RequestBodyKind::ContentLength(42));
1577    }
1578
1579    #[test]
1580    fn test_connection_nominated_headers_removed() {
1581        let headers = vec![
1582            ("Connection".into(), "X-Foo, X-Bar".into()),
1583            ("X-Foo".into(), "a".into()),
1584            ("X-Bar".into(), "b".into()),
1585            ("Content-Type".into(), "text/html".into()),
1586        ];
1587        let filtered = filter_hop_by_hop(&headers);
1588        let names: Vec<_> = filtered.iter().map(|(n, _)| n.as_str()).collect();
1589        assert_eq!(names, vec!["Content-Type"]);
1590    }
1591
1592    #[test]
1593    fn test_ipv6_literal_authority_roundtrip() {
1594        let (target, path) = parse_absolute_uri("http://[::1]:8080/api").unwrap();
1595        assert_eq!(
1596            target,
1597            TargetAddr {
1598                host: TargetHost::Ip("::1".parse().unwrap()),
1599                port: 8080,
1600            }
1601        );
1602        assert_eq!(path, "/api");
1603    }
1604
1605    #[test]
1606    fn test_ipv6_literal_no_port() {
1607        let (target, _path) = parse_absolute_uri("http://[::1]/path").unwrap();
1608        assert_eq!(target.port, 80);
1609        assert_eq!(target.host, TargetHost::Ip("::1".parse().unwrap()));
1610    }
1611
1612    #[test]
1613    fn test_chunked_not_final_rejected() {
1614        // When chunked is not the final coding and a non-chunked coding is present,
1615        // the unsupported encoding is rejected first (since only chunked is supported).
1616        let headers = vec![("Transfer-Encoding".into(), "gzip, chunked".into())];
1617        let result = determine_request_body_kind(&headers);
1618        assert!(
1619            matches!(
1620                result,
1621                Err(HttpError::UnsupportedTransferEncoding(_)) | Err(HttpError::ChunkedNotFinal)
1622            ),
1623            "chunked not final with unsupported coding must be rejected: {:?}",
1624            result
1625        );
1626    }
1627
1628    #[test]
1629    fn test_unsupported_transfer_encoding_rejected() {
1630        let headers = vec![("Transfer-Encoding".into(), "deflate".into())];
1631        let result = determine_request_body_kind(&headers);
1632        assert!(
1633            matches!(result, Err(HttpError::UnsupportedTransferEncoding(_))),
1634            "unsupported TE must be rejected: {:?}",
1635            result
1636        );
1637    }
1638
1639    #[tokio::test]
1640    async fn test_upstream_connection_close_detected() {
1641        let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\n\r\nhello";
1642        // Upstream: server writes response into duplex, forward_response reads from the other end
1643        let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1644        tokio::spawn(async move {
1645            upstream_write.write_all(response).await.unwrap();
1646            upstream_write.shutdown().await.ok();
1647        });
1648        // Client: forward_response writes to client_write, we read from client_read
1649        let (mut client_read, client_write) = tokio::io::duplex(4096);
1650
1651        let mut upstream: BoxStream = Box::new(upstream_read);
1652        let mut client: BoxStream = Box::new(client_write);
1653        let result = forward_response(&mut upstream, &mut client).await;
1654        assert!(result.is_ok());
1655        let fwd = result.unwrap();
1656        assert!(
1657            !fwd.upstream_alive,
1658            "Connection: close should make upstream not alive"
1659        );
1660        assert!(
1661            fwd.client_should_close,
1662            "client should close when upstream says close"
1663        );
1664        // Verify the client received the forwarded response
1665        let mut buf = Vec::new();
1666        let _ = tokio::time::timeout(
1667            std::time::Duration::from_secs(1),
1668            client_read.read_to_end(&mut buf),
1669        )
1670        .await;
1671        let resp = String::from_utf8_lossy(&buf);
1672        assert!(
1673            resp.contains("200 OK"),
1674            "client should receive response: {resp}"
1675        );
1676        assert!(resp.contains("hello"), "client should receive body: {resp}");
1677    }
1678
1679    #[tokio::test]
1680    async fn test_upstream_http11_keepalive_default() {
1681        let response = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
1682        let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1683        tokio::spawn(async move {
1684            upstream_write.write_all(response).await.unwrap();
1685            tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1686        });
1687        let (mut client_read, client_write) = tokio::io::duplex(4096);
1688
1689        let mut upstream: BoxStream = Box::new(upstream_read);
1690        let mut client: BoxStream = Box::new(client_write);
1691        let result = forward_response(&mut upstream, &mut client).await;
1692        assert!(result.is_ok());
1693        let fwd = result.unwrap();
1694        assert!(
1695            fwd.upstream_alive,
1696            "HTTP/1.1 without Connection: close should be alive"
1697        );
1698        assert!(!fwd.client_should_close);
1699        let mut buf = Vec::new();
1700        let _ = tokio::time::timeout(
1701            std::time::Duration::from_secs(1),
1702            client_read.read_to_end(&mut buf),
1703        )
1704        .await;
1705        let resp = String::from_utf8_lossy(&buf);
1706        assert!(
1707            resp.contains("200 OK"),
1708            "client should receive response: {resp}"
1709        );
1710    }
1711
1712    #[test]
1713    fn test_filter_hop_by_hop_removes_upgrade() {
1714        let headers = vec![
1715            ("Upgrade".into(), "websocket".into()),
1716            ("Content-Type".into(), "text/html".into()),
1717        ];
1718        let filtered = filter_hop_by_hop(&headers);
1719        assert_eq!(filtered.len(), 1);
1720        assert_eq!(filtered[0].0, "Content-Type");
1721    }
1722
1723    #[test]
1724    fn test_filter_hop_by_hop_removes_proxy_connection() {
1725        let headers = vec![
1726            ("Proxy-Connection".into(), "keep-alive".into()),
1727            ("Content-Type".into(), "text/html".into()),
1728        ];
1729        let filtered = filter_hop_by_hop(&headers);
1730        assert_eq!(filtered.len(), 1);
1731        assert_eq!(filtered[0].0, "Content-Type");
1732    }
1733
1734    #[tokio::test]
1735    async fn test_request_body_kind_none_has_no_body() {
1736        let headers = vec![("Host".into(), "example.com".into())];
1737        let kind = determine_request_body_kind(&headers).unwrap();
1738        assert_eq!(kind, RequestBodyKind::None);
1739        assert!(!matches!(kind, RequestBodyKind::ContentLength(0)));
1740    }
1741
1742    #[test]
1743    fn test_forward_request_body_kind_dispatches_correctly() {
1744        let req_none = ForwardRequest {
1745            method: "GET".into(),
1746            path: "/".into(),
1747            version: "HTTP/1.1".into(),
1748            headers: vec![],
1749            target: TargetAddr {
1750                host: TargetHost::Domain("example.com".into()),
1751                port: 80,
1752            },
1753            has_body: false,
1754            content_length: None,
1755            is_chunked: false,
1756            connection_close: false,
1757        };
1758        assert_eq!(req_none.body_kind(), RequestBodyKind::None);
1759
1760        let req_cl = ForwardRequest {
1761            content_length: Some(100),
1762            has_body: true,
1763            ..req_none.clone()
1764        };
1765        assert_eq!(req_cl.body_kind(), RequestBodyKind::ContentLength(100));
1766
1767        let req_chunked = ForwardRequest {
1768            is_chunked: true,
1769            has_body: true,
1770            ..req_none.clone()
1771        };
1772        assert_eq!(req_chunked.body_kind(), RequestBodyKind::Chunked);
1773    }
1774
1775    // ===== Phase 2 gap coverage: invariants 6–9 =====
1776
1777    #[tokio::test]
1778    async fn test_copy_request_body_premature_eof() {
1779        let input = b"short";
1780        let mut reader = &input[..];
1781        let mut writer = Vec::new();
1782        let limits = BodyCopyLimits::default();
1783
1784        let result = copy_request_body(
1785            &mut reader,
1786            &mut writer,
1787            RequestBodyKind::ContentLength(100),
1788            &limits,
1789        )
1790        .await;
1791        assert!(
1792            result.is_err(),
1793            "Content-Length body with premature EOF must fail"
1794        );
1795        let msg = format!("{}", result.unwrap_err());
1796        assert!(
1797            msg.contains("unexpected EOF"),
1798            "error should mention unexpected EOF: {msg}"
1799        );
1800    }
1801
1802    #[tokio::test]
1803    async fn test_forward_request_stream_after_failure() {
1804        use tokio::io::AsyncWriteExt;
1805
1806        let (client_read, mut client_write) = tokio::io::duplex(4096);
1807        let mut stream: BoxStream = Box::new(client_read);
1808
1809        let bad_request = b"INVALID\r\n\r\n";
1810        client_write.write_all(bad_request).await.unwrap();
1811
1812        let result = forward_request_stream(&mut stream).await;
1813        assert!(result.is_err(), "malformed request must fail");
1814
1815        let good_request = b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n";
1816        client_write.write_all(good_request).await.unwrap();
1817
1818        let result2 = forward_request_stream(&mut stream).await;
1819        assert!(
1820            result2.is_ok(),
1821            "valid request after failure must succeed: {:?}",
1822            result2.err()
1823        );
1824        let req = result2.unwrap();
1825        assert_eq!(req.method, "GET");
1826        assert_eq!(req.path, "/");
1827    }
1828
1829    #[tokio::test]
1830    async fn test_forward_request_rejects_unsupported_http_version() {
1831        let (client_read, mut client_write) = tokio::io::duplex(4096);
1832        let mut stream: BoxStream = Box::new(client_read);
1833        client_write
1834            .write_all(b"GET http://example.com/ HTTP/9.9\r\n\r\n")
1835            .await
1836            .unwrap();
1837
1838        let error = forward_request_stream(&mut stream).await.unwrap_err();
1839        assert!(
1840            matches!(error, HttpError::MalformedRequest(message) if message.contains("HTTP/9.9"))
1841        );
1842    }
1843
1844    #[tokio::test]
1845    async fn test_response_header_limit_allows_maximum_header_count() {
1846        let (client_read, mut client_write) = tokio::io::duplex(32 * 1024);
1847        let mut stream: BoxStream = Box::new(client_read);
1848        let mut response = String::from("HTTP/1.1 200 OK\r\n");
1849        for index in 0..MAX_HEADER_LINES {
1850            response.push_str(&format!("X-Test-{index}: value\r\n"));
1851        }
1852        response.push_str("\r\n");
1853        client_write.write_all(response.as_bytes()).await.unwrap();
1854
1855        let parsed = read_response_head(&mut stream).await.unwrap();
1856        assert_eq!(parsed.headers.len(), MAX_HEADER_LINES);
1857    }
1858
1859    #[test]
1860    fn test_build_origin_request_strips_upgrade() {
1861        let req = ForwardRequest {
1862            method: "GET".into(),
1863            path: "/".into(),
1864            version: "HTTP/1.1".into(),
1865            headers: vec![
1866                ("Host".into(), "example.com".into()),
1867                ("Upgrade".into(), "websocket".into()),
1868                ("Connection".into(), "Upgrade".into()),
1869            ],
1870            target: TargetAddr {
1871                host: TargetHost::Domain("example.com".into()),
1872                port: 80,
1873            },
1874            has_body: false,
1875            content_length: None,
1876            is_chunked: false,
1877            connection_close: false,
1878        };
1879        let origin = build_origin_request(&req);
1880        assert!(
1881            !origin.to_lowercase().contains("upgrade"),
1882            "Upgrade header must be stripped from forwarded request: {origin}"
1883        );
1884        assert!(
1885            !origin.to_lowercase().contains("connection: upgrade"),
1886            "Connection: Upgrade must be stripped: {origin}"
1887        );
1888        assert!(
1889            origin.contains("Connection: close"),
1890            "proxy must add Connection: close: {origin}"
1891        );
1892    }
1893
1894    #[test]
1895    fn test_expectation_detection_is_case_insensitive_and_comma_aware() {
1896        assert!(has_unsupported_expectation(&[(
1897            "eXpEcT".into(),
1898            "foo, 100-continue".into()
1899        ),]));
1900        assert!(!has_unsupported_expectation(&[(
1901            "Expect".into(),
1902            "  ,  ".into()
1903        )]));
1904    }
1905
1906    #[tokio::test]
1907    async fn test_forward_response_forwards_informational_responses_before_final() {
1908        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1909
1910        let response = b"HTTP/1.1 103 Early Hints\r\nLink: </style.css>\r\n\r\nHTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello";
1911        let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
1912        tokio::spawn(async move {
1913            upstream_write.write_all(response).await.unwrap();
1914        });
1915        let (mut client_read, client_write) = tokio::io::duplex(4096);
1916
1917        let mut upstream: BoxStream = Box::new(upstream_read);
1918        let mut client: BoxStream = Box::new(client_write);
1919
1920        let result = forward_response(&mut upstream, &mut client).await.unwrap();
1921        assert_eq!(result.status, 200);
1922        client.shutdown().await.unwrap();
1923
1924        let mut buf = Vec::new();
1925        client_read.read_to_end(&mut buf).await.unwrap();
1926        let resp = String::from_utf8_lossy(&buf);
1927        assert!(
1928            resp.starts_with("HTTP/1.1 103 Early"),
1929            "unexpected forwarded response: {resp:?}"
1930        );
1931        assert!(resp.contains("HTTP/1.1 100 Continue"));
1932        assert!(resp.contains("HTTP/1.1 200 OK"));
1933        assert!(resp.ends_with("hello"));
1934        assert!(resp.find("103").unwrap() < resp.find("100").unwrap());
1935        assert!(resp.find("100").unwrap() < resp.find("200").unwrap());
1936    }
1937
1938    #[tokio::test]
1939    async fn test_forward_response_rejects_switching_protocols() {
1940        use tokio::io::AsyncWriteExt;
1941
1942        let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1943        tokio::spawn(async move {
1944            upstream_write
1945                .write_all(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\n\r\n")
1946                .await
1947                .unwrap();
1948        });
1949        let (_client_read, client_write) = tokio::io::duplex(1024);
1950        let mut upstream: BoxStream = Box::new(upstream_read);
1951        let mut client: BoxStream = Box::new(client_write);
1952
1953        assert!(matches!(
1954            forward_response(&mut upstream, &mut client).await,
1955            Err(HttpError::UpgradeUnsupported)
1956        ));
1957    }
1958
1959    #[tokio::test]
1960    async fn test_forward_response_rejects_invalid_content_length() {
1961        use tokio::io::AsyncWriteExt;
1962
1963        let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1964        tokio::spawn(async move {
1965            upstream_write
1966                .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: invalid\r\n\r\n")
1967                .await
1968                .unwrap();
1969        });
1970        let (_client_read, client_write) = tokio::io::duplex(1024);
1971        let mut upstream: BoxStream = Box::new(upstream_read);
1972        let mut client: BoxStream = Box::new(client_write);
1973
1974        assert!(matches!(
1975            forward_response(&mut upstream, &mut client).await,
1976            Err(HttpError::InvalidContentLength)
1977        ));
1978    }
1979
1980    #[tokio::test]
1981    async fn test_forward_response_accepts_equal_duplicate_content_length() {
1982        use tokio::io::{AsyncReadExt, AsyncWriteExt};
1983
1984        let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
1985        tokio::spawn(async move {
1986            upstream_write
1987                .write_all(
1988                    b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 5\r\n\r\nhello",
1989                )
1990                .await
1991                .unwrap();
1992        });
1993        let (mut client_read, client_write) = tokio::io::duplex(1024);
1994        let mut upstream: BoxStream = Box::new(upstream_read);
1995        let mut client: BoxStream = Box::new(client_write);
1996
1997        let result = forward_response(&mut upstream, &mut client).await.unwrap();
1998        assert_eq!(result.status, 200);
1999        client.shutdown().await.unwrap();
2000        let mut buf = Vec::new();
2001        client_read.read_to_end(&mut buf).await.unwrap();
2002        assert!(String::from_utf8_lossy(&buf).ends_with("hello"));
2003    }
2004
2005    #[tokio::test]
2006    async fn test_forward_response_rejects_conflicting_duplicate_content_length() {
2007        use tokio::io::AsyncWriteExt;
2008
2009        let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
2010        tokio::spawn(async move {
2011            upstream_write
2012                .write_all(
2013                    b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\nContent-Length: 6\r\n\r\nhello",
2014                )
2015                .await
2016                .unwrap();
2017        });
2018        let (_client_read, client_write) = tokio::io::duplex(1024);
2019        let mut upstream: BoxStream = Box::new(upstream_read);
2020        let mut client: BoxStream = Box::new(client_write);
2021
2022        assert!(matches!(
2023            forward_response(&mut upstream, &mut client).await,
2024            Err(HttpError::ConflictingContentLength)
2025        ));
2026    }
2027
2028    #[tokio::test]
2029    async fn test_forward_response_rejects_invalid_chunk_size() {
2030        use tokio::io::AsyncWriteExt;
2031
2032        let (upstream_read, mut upstream_write) = tokio::io::duplex(1024);
2033        tokio::spawn(async move {
2034            upstream_write
2035                .write_all(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\nnope\r\n")
2036                .await
2037                .unwrap();
2038        });
2039        let (_client_read, client_write) = tokio::io::duplex(1024);
2040        let mut upstream: BoxStream = Box::new(upstream_read);
2041        let mut client: BoxStream = Box::new(client_write);
2042
2043        assert!(matches!(
2044            forward_response(&mut upstream, &mut client).await,
2045            Err(HttpError::MalformedResponse(message)) if message.contains("invalid chunk size")
2046        ));
2047    }
2048
2049    #[tokio::test]
2050    async fn test_forward_response_bounds_informational_responses() {
2051        use tokio::io::AsyncWriteExt;
2052
2053        let response = b"HTTP/1.1 103 Early Hints\r\n\r\n";
2054        let (upstream_read, mut upstream_write) = tokio::io::duplex(4096);
2055        tokio::spawn(async move {
2056            for _ in 0..=MAX_INFORMATIONAL_RESPONSES {
2057                upstream_write.write_all(response).await.unwrap();
2058            }
2059        });
2060        let (_client_read, client_write) = tokio::io::duplex(4096);
2061        let mut upstream: BoxStream = Box::new(upstream_read);
2062        let mut client: BoxStream = Box::new(client_write);
2063
2064        assert!(matches!(
2065            forward_response(&mut upstream, &mut client).await,
2066            Err(HttpError::TooManyInformationalResponses)
2067        ));
2068    }
2069}