Skip to main content

rust_web_server/proxy/
mod.rs

1//! Reverse proxy middleware with round-robin load balancing.
2//!
3//! `ReverseProxy` implements [`Middleware`] — wrap any application with it and
4//! all matching requests are forwarded to one of the configured backends over
5//! plain HTTP/1.1.  Failed backends are skipped and the next one is tried
6//! before returning `502 Bad Gateway`.
7//!
8//! # Example
9//!
10//! ```rust,no_run
11//! use rust_web_server::app::App;
12//! use rust_web_server::core::New;
13//! use rust_web_server::proxy::{LoadBalancing, ReverseProxy};
14//!
15//! // Proxy every request across two backends in round-robin order.
16//! let app = App::new()
17//!     .wrap(ReverseProxy::new(["http://backend-1:8080", "http://backend-2:8080"])
18//!         .strategy(LoadBalancing::RoundRobin));
19//!
20//! // Only proxy /api/* requests; everything else is handled locally.
21//! let app2 = App::new()
22//!     .wrap(ReverseProxy::new(["http://api-service:3000"])
23//!         .path_prefix("/api"));
24//! ```
25
26pub mod pool;
27pub mod status;
28#[cfg(feature = "http2")]
29pub(crate) mod async_upstream;
30#[cfg(not(target_arch = "wasm32"))]
31pub mod mirror;
32#[cfg(feature = "http2")]
33pub(crate) mod h2_upstream;
34#[cfg(feature = "http3")]
35pub(crate) mod h3_upstream;
36#[cfg(feature = "http2")]
37pub mod grpc_web;
38#[cfg(feature = "http2")]
39pub use grpc_web::GrpcWebLayer;
40
41#[cfg(test)]
42mod tests;
43
44use std::io::{Read, Write};
45use crate::ext::decimal::Decimal;
46use std::net::{TcpStream, ToSocketAddrs};
47use std::sync::atomic::{AtomicUsize, Ordering};
48use std::sync::Arc;
49use std::time::Duration;
50
51pub use pool::{ConnPool, Pool};
52
53use crate::application::Application;
54use crate::middleware::Middleware;
55use crate::request::Request;
56use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
57use crate::server::ConnectionInfo;
58
59// Hop-by-hop headers that must not be forwarded (RFC 9110 §7.6.1), plus
60// `proxy-connection` (legacy clients). Headers named in `Connection` are
61// dropped too (`connection_listed`).
62const HOP_BY_HOP: &[&str] = &[
63    "connection",
64    "keep-alive",
65    "proxy-authenticate",
66    "proxy-authorization",
67    "proxy-connection",
68    "te",
69    "trailer",
70    "transfer-encoding",
71    "upgrade",
72];
73
74/// Load balancing strategy used by [`ReverseProxy`].
75pub enum LoadBalancing {
76    /// Distribute requests across backends in a cyclic order.
77    RoundRobin,
78}
79
80/// Reverse proxy middleware.
81///
82/// Forwards incoming requests to one of the configured backends over HTTP/1.1.
83/// On connection failure the next backend in the list is tried; when all
84/// backends have failed the middleware returns `502 Bad Gateway`.
85///
86/// Hop-by-hop headers are stripped before forwarding.  `X-Forwarded-For` and
87/// `Via` are added to every forwarded request.
88///
89/// Idle connections are pooled and reused across requests (up to
90/// 64 idle per backend, 60-second timeout).
91/// This eliminates per-request TCP handshake overhead and ephemeral-port
92/// exhaustion.  Use [`ReverseProxy::with_pool`] to share a pool across
93/// multiple proxy instances or to tune pool parameters.
94pub struct ReverseProxy {
95    backends: Vec<Backend>,
96    path_prefix: Option<String>,
97    connect_timeout: Duration,
98    read_timeout: Duration,
99    counter: AtomicUsize,
100    pool: Arc<ConnPool>,
101    breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
102    trust: Option<Arc<UpstreamTls>>,
103    /// What `trust` was built from, so each TLS option adds to the others.
104    tls_ca: Option<String>,
105    tls_client: Option<(String, String)>,
106}
107
108impl ReverseProxy {
109    /// Create a proxy that distributes requests across `backends` in
110    /// round-robin order.  Each entry must be `"http://host:port"` or
111    /// `"host:port"` (port defaults to 80).
112    pub fn new<I, S>(backends: I) -> Self
113    where
114        I: IntoIterator<Item = S>,
115        S: AsRef<str>,
116    {
117        Self {
118            backends: backends
119                .into_iter()
120                .filter_map(|u| Backend::parse(u.as_ref()))
121                .collect(),
122            path_prefix: None,
123            connect_timeout: Duration::from_secs(5),
124            read_timeout: Duration::from_secs(30),
125            counter: AtomicUsize::new(0),
126            pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
127            breaker: None,
128            trust: None,
129            tls_ca: None,
130            tls_client: None,
131        }
132    }
133
134    /// Wire a [`crate::circuit_breaker::CircuitBreaker`] or
135    /// [`crate::circuit_breaker::RedisCircuitBreaker`] into this proxy:
136    /// before dialing a backend, its breaker state is checked (an `Open`
137    /// backend is skipped with no TCP attempt, exactly like a caller
138    /// manually checking `is_available` around each proxied call); after a
139    /// dial, `record_success`/`record_failure` is called automatically.
140    /// No breaker is wired by default — existing `ReverseProxy` callers see
141    /// no behavior change unless they opt in here.
142    ///
143    /// ```rust,no_run
144    /// use std::sync::Arc;
145    /// use rust_web_server::circuit_breaker::global as global_breaker;
146    /// use rust_web_server::proxy::ReverseProxy;
147    ///
148    /// let proxy = ReverseProxy::new(["http://backend-1:8080", "http://backend-2:8080"])
149    ///     .with_circuit_breaker(Arc::new(global_breaker()));
150    /// ```
151    pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
152        self.breaker = Some(breaker);
153        self
154    }
155
156    /// Only proxy requests whose URI starts with `prefix`.
157    ///
158    /// Other requests are passed through to the next layer in the middleware
159    /// chain (or the inner application).
160    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
161        self.path_prefix = Some(prefix.into());
162        self
163    }
164
165    /// Override the load balancing strategy (currently only `RoundRobin`).
166    pub fn strategy(self, _strategy: LoadBalancing) -> Self {
167        self
168    }
169
170    /// Override the TCP connect timeout (default: 5 000 ms).
171    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
172        self.connect_timeout = Duration::from_millis(ms);
173        self
174    }
175
176    /// Override the response read timeout (default: 30 000 ms).
177    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
178        self.read_timeout = Duration::from_millis(ms);
179        self
180    }
181
182    /// Attach a shared connection pool.
183    ///
184    /// Useful for sharing one pool across multiple `ReverseProxy` instances
185    /// or for tuning pool parameters (capacity, idle timeout).
186    pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
187        self.pool = pool;
188        self
189    }
190
191    /// Trust the CA certificates in the PEM file `path` for `https://`
192    /// backends, **instead of** the public WebPKI roots — for internal
193    /// services behind a private CA or a self-signed certificate.
194    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
195        self.tls_ca = Some(path.to_string());
196        self.rebuild_trust()?;
197        Ok(self)
198    }
199
200    /// Present the client certificate in the PEM file `cert` (with its
201    /// private key in `key`) to `https://` backends that require one —
202    /// mutual TLS to the upstream. Since 18.3.0.
203    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
204        self.tls_client = Some((cert.to_string(), key.to_string()));
205        self.rebuild_trust()?;
206        Ok(self)
207    }
208
209    fn rebuild_trust(&mut self) -> Result<(), String> {
210        let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
211        self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
212        Ok(())
213    }
214
215    /// Set the maximum number of idle connections per backend (default: 64).
216    pub fn max_idle_conns(mut self, n: usize) -> Self {
217        self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
218        self
219    }
220
221    fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
222        if self.backends.is_empty() {
223            return Err("no backends configured".to_string());
224        }
225        let n = self.backends.len();
226        let start = self.counter.fetch_add(1, Ordering::Relaxed);
227        let mut last_err = "all backends failed".to_string();
228        for attempt in 0..n {
229            let idx = (start + attempt) % n;
230            let backend = &self.backends[idx];
231            let key = format!("{}:{}", backend.host, backend.port);
232
233            if let Some(breaker) = &self.breaker {
234                if !breaker.is_available(&key) {
235                    last_err = format!("circuit open for {}", key);
236                    continue;
237                }
238            }
239
240            match self.try_backend(request, connection, backend) {
241                Ok(resp) => {
242                    if let Some(breaker) = &self.breaker {
243                        breaker.record_success(&key);
244                    }
245                    return Ok(resp);
246                }
247                Err(e) => {
248                    if let Some(breaker) = &self.breaker {
249                        breaker.record_failure(&key);
250                    }
251                    last_err = e;
252                }
253            }
254        }
255        Err(last_err)
256    }
257
258    fn try_backend(
259        &self,
260        request: &Request,
261        connection: &ConnectionInfo,
262        backend: &Backend,
263    ) -> Result<Response, String> {
264        forward_upstream_trusting(
265            &self.pool,
266            request,
267            crate::proxy::Peer::of(connection),
268            &backend.host,
269            backend.port,
270            backend.tls,
271            self.trust.as_deref(),
272            self.connect_timeout,
273            self.read_timeout,
274        )
275    }
276}
277
278impl Middleware for ReverseProxy {
279    fn handle(
280        &self,
281        request: &Request,
282        connection: &ConnectionInfo,
283        next: &dyn Application,
284    ) -> Result<Response, String> {
285        if let Some(prefix) = &self.path_prefix {
286            if !request.request_uri.starts_with(prefix.as_str()) {
287                return next.execute(request, connection);
288            }
289        }
290        match self.proxy(request, connection) {
291            Ok(mut resp) => {
292                status::received(&mut resp, None);
293                Ok(resp)
294            }
295            Err(e) => {
296                let mut resp = bad_gateway();
297                status::failed_with(&mut resp, &e, None);
298                Ok(resp)
299            }
300        }
301    }
302}
303
304// ── helpers ───────────────────────────────────────────────────────────────────
305
306/// The client as the forwarders need it: its address for `X-Forwarded-For`
307/// and whether it connected over TLS, for `X-Forwarded-Proto`.
308#[derive(Clone, Copy, Debug)]
309pub(crate) struct Peer<'a> {
310    pub ip: &'a str,
311    pub tls: bool,
312}
313
314impl<'a> Peer<'a> {
315    pub(crate) fn of(connection: &'a ConnectionInfo) -> Self {
316        Peer { ip: &connection.client.ip, tls: connection.tls }
317    }
318
319    /// A plain-HTTP client at `ip` (tests and callers without a connection).
320    pub(crate) fn plain(ip: &'a str) -> Self {
321        Peer { ip, tls: false }
322    }
323
324    pub(crate) fn proto(&self) -> &'static str {
325        if self.tls { "https" } else { "http" }
326    }
327}
328
329/// The `Forwarded` value for this hop (RFC 7239, 18.20.0): the request's
330/// own `Forwarded` (from proxies before this one) extended with
331/// `for=<client>;proto=<http|https>;host=<Host>`; `None` when
332/// `forwarded_header = "off"`.
333pub(crate) fn forwarded_value(request: &Request, client: Peer<'_>) -> Option<String> {
334    if !crate::server_config::ServerConfig::current().forwarded_header {
335        return None;
336    }
337    let node = match client.ip.parse::<std::net::IpAddr>() {
338        Ok(std::net::IpAddr::V4(v4)) => v4.to_string(),
339        Ok(std::net::IpAddr::V6(v6)) => match v6.to_ipv4_mapped() {
340            Some(v4) => v4.to_string(),
341            None => format!("\"[{v6}]\""),
342        },
343        Err(_) => "unknown".to_string(),
344    };
345    let mut element = format!("for={node};proto={}", client.proto());
346    if let Some(host) = request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|h: &&str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_graphic() && b != b'"' && b != b'\\')) {
347        let token = host.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b));
348        if token {
349            element.push_str(&format!(";host={host}"));
350        } else {
351            element.push_str(&format!(";host=\"{host}\""));
352        }
353    }
354    let prior = request.headers.iter().filter(|h| h.name.eq_ignore_ascii_case("forwarded")).map(|h| h.value.trim()).filter(|v| !v.is_empty() && !v.bytes().any(|b| b < 0x20)).collect::<Vec<_>>();
355    Some(if prior.is_empty() { element } else { format!("{}, {element}", prior.join(", ")) })
356}
357
358pub(crate) fn build_request(
359    request: &Request,
360    backend_host: &str,
361    client: Peer<'_>,
362    keep_alive: bool,
363) -> Vec<u8> {
364    // One buffer, sized up front and filled with extend_from_slice — no
365    // write!/format machinery, no lower-cased copy of each header name.
366    // `Content-Length` is regenerated from the (possibly de-chunked) body and
367    // `X-Forwarded-For` is extended below, so the client's copies are dropped:
368    // two `Content-Length` lines are a protocol error and a desync risk.
369    let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
370    // Inside a traced request the current span's `traceparent` (and the
371    // request's `baggage`) replace the client's.
372    #[cfg(not(target_arch = "wasm32"))]
373    let trace = crate::otel::propagation_headers();
374    #[cfg(target_arch = "wasm32")]
375    let trace: Option<(String, Option<String>)> = None;
376    let forwarded_header = forwarded_value(request, client);
377    let forwarded = |h: &&crate::header::Header| {
378        // An empty name would be sent as a malformed ": value" line.
379        !h.name.is_empty()
380            && !is_hop_by_hop_or_host(&h.name)
381            && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
382            && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
383            && !(forwarded_header.is_some() && h.name.eq_ignore_ascii_case("Forwarded"))
384            && !connection.is_some_and(|c| connection_listed(c, &h.name))
385            && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
386    };
387    let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
388    // The Host the client asked for, unless an outer proxy already said
389    // (its X-Forwarded-Host passes through like any header).
390    // http or https, as the client connected, unless an outer proxy said.
391    let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
392    let forwarded_host = match request.header("X-Forwarded-Host") {
393        Some(_) => None,
394        None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
395    };
396    let body_len = Decimal::new(request.body.len() as u128);
397    let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
398        + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
399        + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
400        + forwarded_host.map_or(0, |v| v.len() + 20)
401        + forwarded_proto.map_or(0, |v| v.len() + 21)
402        + forwarded_header.as_ref().map_or(0, |v| v.len() + 13)
403        + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
404        + 13
405        + 24
406        + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
407        + 2
408        + request.body.len();
409    let mut out: Vec<u8> = Vec::with_capacity(capacity);
410    for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
411        out.extend_from_slice(part.as_bytes());
412    }
413    for h in request.headers.iter().filter(forwarded) {
414        for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
415            out.extend_from_slice(part.as_bytes());
416        }
417    }
418    out.extend_from_slice(b"X-Forwarded-For: ");
419    if let Some(prior) = prior_xff {
420        out.extend_from_slice(prior.as_bytes());
421        out.extend_from_slice(b", ");
422    }
423    for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
424        out.extend_from_slice(part.as_bytes());
425    }
426    if let Some(host) = forwarded_host {
427        for part in ["X-Forwarded-Host: ", host, "\r\n"] {
428            out.extend_from_slice(part.as_bytes());
429        }
430    }
431    if let Some(proto) = forwarded_proto {
432        for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
433            out.extend_from_slice(part.as_bytes());
434        }
435    }
436    if let Some(value) = &forwarded_header {
437        for part in ["Forwarded: ", value.as_str(), "\r\n"] {
438            out.extend_from_slice(part.as_bytes());
439        }
440    }
441    if let Some((traceparent, baggage)) = &trace {
442        for part in ["traceparent: ", traceparent, "\r\n"] {
443            out.extend_from_slice(part.as_bytes());
444        }
445        if let Some(baggage) = baggage {
446            for part in ["baggage: ", baggage, "\r\n"] {
447                out.extend_from_slice(part.as_bytes());
448            }
449        }
450    }
451    if keep_alive {
452        out.extend_from_slice(b"Connection: keep-alive\r\n");
453    } else {
454        out.extend_from_slice(b"Connection: close\r\n");
455    }
456    if !request.body.is_empty() {
457        out.extend_from_slice(b"Content-Length: ");
458        out.extend_from_slice(body_len.as_str().as_bytes());
459        out.extend_from_slice(b"\r\n");
460    }
461    out.extend_from_slice(b"\r\n");
462    out.extend_from_slice(&request.body);
463    out
464}
465
466/// Whether a request header is dropped on the way upstream: hop-by-hop
467/// headers and `Host` (rws sends its own). Matches `to_lowercase()` against
468/// the lower-case list — Unicode lower-casing, so a non-ASCII name (where
469/// e.g. the Kelvin sign lower-cases to `k`) still takes that path.
470fn is_hop_by_hop_or_host(name: &str) -> bool {
471    if name.is_ascii() {
472        name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
473    } else {
474        let lower = name.to_lowercase();
475        lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
476    }
477}
478
479/// Whether `name` is one of the comma-separated tokens of a `Connection`
480/// header value — those headers are hop-by-hop for this connection only.
481fn connection_listed(connection: &str, name: &str) -> bool {
482    connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
483}
484
485/// Decode HTTP/1.1 chunked transfer-encoding from `stream`.
486///
487/// `buf[header_end..]` may already contain some body bytes that arrived in
488/// the same read as the headers.  Returns the fully decoded body.
489fn decode_chunked<S: Read>(
490    stream: &mut S,
491    buf: &[u8],
492    header_end: usize,
493    tmp: &mut [u8],
494) -> Result<Vec<u8>, String> {
495    // Seed `raw` with any body bytes already buffered alongside the headers.
496    let mut raw: Vec<u8> = buf[header_end..].to_vec();
497    let mut decoded: Vec<u8> = Vec::new();
498
499    loop {
500        // Wait until we have at least one complete chunk-size line (ends with \r\n).
501        let crlf = loop {
502            if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
503                break p;
504            }
505            let n = stream.read(tmp).map_err(|e| e.to_string())?;
506            if n == 0 {
507                return Err("chunked: premature EOF reading chunk size".to_string());
508            }
509            raw.extend_from_slice(&tmp[..n]);
510        };
511
512        // Chunk size is hex, optionally followed by chunk-extensions (";…").
513        let size_line = std::str::from_utf8(&raw[..crlf])
514            .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
515        let size_str = size_line.split(';').next().unwrap_or("").trim();
516        let chunk_size = usize::from_str_radix(size_str, 16)
517            .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
518        raw.drain(..crlf + 2); // consume "<size>\r\n"
519
520        if chunk_size == 0 {
521            // Last chunk: skip trailer fields up to the empty line that ends
522            // the message. Leaving any of it unread would corrupt the next
523            // response on a pooled connection.
524            loop {
525                let line_end = loop {
526                    if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
527                        break p;
528                    }
529                    let n = stream.read(tmp).map_err(|e| e.to_string())?;
530                    if n == 0 {
531                        return Err("chunked: premature EOF in trailer".to_string());
532                    }
533                    raw.extend_from_slice(&tmp[..n]);
534                };
535                raw.drain(..line_end + 2);
536                if line_end == 0 {
537                    break;
538                }
539            }
540            break;
541        }
542
543        // Read chunk data + trailing CRLF.
544        while raw.len() < chunk_size + 2 {
545            let n = stream.read(tmp).map_err(|e| e.to_string())?;
546            if n == 0 {
547                return Err("chunked: premature EOF reading chunk body".to_string());
548            }
549            raw.extend_from_slice(&tmp[..n]);
550        }
551        decoded.extend_from_slice(&raw[..chunk_size]);
552        raw.drain(..chunk_size + 2); // consume "<data>\r\n"
553    }
554
555    Ok(decoded)
556}
557
558/// Rewrite `buf` in-place: strip `Transfer-Encoding`, add `Content-Length`,
559/// replace the old (undecoded) body with `decoded`.
560fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
561    let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
562    buf.clear();
563    for line in header_str.lines() {
564        if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
565            continue;
566        }
567        buf.extend_from_slice(line.as_bytes());
568        buf.extend_from_slice(b"\r\n");
569    }
570    let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
571    buf.extend_from_slice(decoded);
572}
573
574/// Headers rws regenerates for a buffered body, plus hop-by-hop headers
575/// (RFC 9110 §7.6.1) that must not be forwarded.
576pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
577    "content-length", "transfer-encoding", "connection", "keep-alive",
578    "proxy-connection", "te", "trailer", "upgrade",
579];
580
581/// Make a buffered upstream response safe to re-serialize. The body is
582/// already held in `content_range_list`, from which `Response::generate`
583/// (and the HTTP/2 / HTTP/3 writers) emit `Content-Type` and
584/// `Content-Length` themselves, so the upstream's own copies would be
585/// duplicated — and a duplicate `content-length` is a protocol error that
586/// HTTP/2 clients reject outright. The upstream's `Content-Type` is moved
587/// onto the body instead; the status line is re-emitted as HTTP/1.1
588/// regardless of the backend's version.
589#[cfg_attr(not(feature = "http2"), allow(dead_code))]
590pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
591    response.http_version.clear();
592    response.http_version.push_str(crate::http::VERSION.http_1_1);
593    let has_body = !response.content_range_list.is_empty();
594    if let [range] = response.content_range_list.as_mut_slice() {
595        if let Some(h) = response
596            .headers
597            .iter()
598            .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
599        {
600            range.content_type.clear();
601            range.content_type.push_str(&h.value);
602        }
603    }
604    // Compared in place (the list is lower-case ASCII), not lower-cased
605    // into a new String per header.
606    response.headers.retain(|h| {
607        if h.name.eq_ignore_ascii_case("content-type") {
608            // Keep it only when there's no body to carry it (e.g. HEAD, 304).
609            return !has_body;
610        }
611        !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
612    });
613    response
614}
615
616pub(crate) fn bad_gateway() -> Response {
617    crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
618}
619
620// ── Streaming proxy helpers ───────────────────────────────────────────────────
621
622/// Responses larger than this threshold are streamed instead of buffered.
623const STREAM_THRESHOLD: usize = 1024 * 1024; // 1 MB
624
625/// A `Read` implementation that drains a prefix buffer before reading from the
626/// inner stream. Used to replay body bytes that arrived with the HTTP headers.
627pub(crate) struct ConcatReader<R: Read + Send> {
628    prefix: Vec<u8>,
629    prefix_pos: usize,
630    inner: R,
631}
632
633impl<R: Read + Send> ConcatReader<R> {
634    fn new(prefix: Vec<u8>, inner: R) -> Self {
635        ConcatReader { prefix, prefix_pos: 0, inner }
636    }
637}
638
639impl<R: Read + Send> Read for ConcatReader<R> {
640    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
641        if self.prefix_pos < self.prefix.len() {
642            let avail = &self.prefix[self.prefix_pos..];
643            let n = buf.len().min(avail.len());
644            buf[..n].copy_from_slice(&avail[..n]);
645            self.prefix_pos += n;
646            return Ok(n);
647        }
648        self.inner.read(buf)
649    }
650}
651
652
653/// Idle backend connections kept per `host:port`. 64 matches common nginx
654/// `keepalive` settings and the benchmark's concurrency; fewer meant most
655/// requests at 64-way concurrency still paid a TCP connect.
656pub(crate) const POOL_MAX_IDLE: usize = 64;
657
658/// The process-wide backend connection pool used by config-driven proxy
659/// routes (`proxy_config::DynamicProxy`), keyed by `host:port`.
660pub(crate) fn shared_pool() -> &'static ConnPool {
661    static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
662    POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
663}
664
665/// A backend connection the pooled forwarder drives: plain TCP, or TLS
666/// over TCP for `https://` backends.
667pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
668    fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
669}
670
671impl UpstreamIo for TcpStream {
672    fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
673        self.set_read_timeout(Some(read))?;
674        self.set_write_timeout(Some(write))
675    }
676}
677
678#[cfg(any(feature = "http-client", feature = "http2"))]
679pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
680
681#[cfg(any(feature = "http-client", feature = "http2"))]
682impl UpstreamIo for TlsStream {
683    fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
684        self.sock.set_io_timeouts(read, write)
685    }
686}
687
688/// Forward `request` to an HTTP or HTTPS backend at `host:port`, over a
689/// pooled keep-alive connection either way: plain backends use `plain_pool`,
690/// TLS backends the process-wide TLS pool (one handshake per connection, not
691/// per request). The single entry point for `ReverseProxy`, config-driven
692/// routes, `canary` and `ingress`.
693pub(crate) fn forward_upstream(
694    plain_pool: &ConnPool,
695    request: &Request,
696    client: Peer<'_>,
697    host: &str,
698    port: u16,
699    tls: bool,
700    connect_timeout: Duration,
701    read_timeout: Duration,
702) -> Result<Response, String> {
703    forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
704}
705
706/// [`forward_upstream`] with the trust settings of one upstream: `trust`
707/// replaces the WebPKI roots for TLS backends (a private CA).
708#[allow(clippy::too_many_arguments)]
709pub(crate) fn forward_upstream_trusting(
710    plain_pool: &ConnPool,
711    request: &Request,
712    client: Peer<'_>,
713    host: &str,
714    port: u16,
715    tls: bool,
716    trust: Option<&UpstreamTls>,
717    connect_timeout: Duration,
718    read_timeout: Duration,
719) -> Result<Response, String> {
720    forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
721}
722
723/// [`forward_upstream_trusting`], sending `host_header` as the `Host` instead
724/// of the backend's address (a config route's `preserve_host` /
725/// `upstream_host`).
726#[allow(clippy::too_many_arguments)]
727pub(crate) fn forward_upstream_as(
728    plain_pool: &ConnPool,
729    request: &Request,
730    client: Peer<'_>,
731    host: &str,
732    port: u16,
733    host_header: &str,
734    tls: bool,
735    trust: Option<&UpstreamTls>,
736    connect_timeout: Duration,
737    read_timeout: Duration,
738) -> Result<Response, String> {
739    // Inside a traced request: a client span per forward, current while the
740    // request is built, so the upstream's `traceparent` names it.
741    #[cfg(not(target_arch = "wasm32"))]
742    let span = crate::otel::outbound_span(&request.method);
743    #[cfg(not(target_arch = "wasm32"))]
744    if let Some(span) = &span {
745        let scheme = if tls { "https" } else { "http" };
746        let path = request.request_uri.as_str();
747        for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
748            span.set_attribute(key, value);
749        }
750    }
751    let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
752    #[cfg(not(target_arch = "wasm32"))]
753    if let Some(span) = &span {
754        span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
755    }
756    result
757}
758
759#[allow(clippy::too_many_arguments)]
760fn forward_upstream_untraced(
761    plain_pool: &ConnPool,
762    request: &Request,
763    client: Peer<'_>,
764    host: &str,
765    port: u16,
766    host_header: &str,
767    tls: bool,
768    trust: Option<&UpstreamTls>,
769    connect_timeout: Duration,
770    read_timeout: Duration,
771) -> Result<Response, String> {
772    if tls {
773        #[cfg(any(feature = "http-client", feature = "http2"))]
774        {
775            let (pool, config) = match trust {
776                Some(t) => (t.pool, Arc::clone(&t.config)),
777                None => (shared_tls_pool(), tls_client_config()),
778            };
779            return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
780                connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
781            });
782        }
783        #[cfg(not(any(feature = "http-client", feature = "http2")))]
784        {
785            let _ = trust;
786            return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
787        }
788    }
789    forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
790}
791
792/// Trust settings for one upstream's TLS backends: the certificates in a CA
793/// file **instead of** the WebPKI roots — for internal services behind a
794/// private CA or a self-signed certificate (nginx's
795/// `proxy_ssl_trusted_certificate`) — and/or a client certificate rws
796/// presents to backends that require one (`proxy_ssl_certificate`, since
797/// 18.3.0). Connections are pooled per setting, so two upstreams at the same
798/// address with different trust never share one.
799pub struct UpstreamTls {
800    #[cfg(any(feature = "http-client", feature = "http2"))]
801    config: Arc<rustls::ClientConfig>,
802    #[cfg(any(feature = "http-client", feature = "http2"))]
803    pool: &'static Pool<TlsStream>,
804    /// Connections for the non-blocking forwarder, made with `config`.
805    #[cfg(feature = "http2")]
806    async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
807    /// `config` offering HTTP/2 by ALPN, for HTTP/2 and gRPC upstreams.
808    #[cfg(feature = "http2")]
809    h2_config: Arc<rustls::ClientConfig>,
810}
811
812impl UpstreamTls {
813    /// Load the PEM certificates in `path` as the only trusted roots.
814    pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
815        UpstreamTls::new(Some(path), None)
816    }
817
818    /// Trust the PEM certificates in `ca_file` (the WebPKI roots when `None`)
819    /// and, with `client_cert` = `(certificate chain PEM, private key PEM)`,
820    /// present that certificate to backends that ask for one.
821    pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
822        #[cfg(any(feature = "http-client", feature = "http2"))]
823        {
824            use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
825            let roots = match ca_file {
826                Some(path) => {
827                    let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
828                    let certs = CertificateDer::pem_slice_iter(&bytes)
829                        .collect::<Result<Vec<_>, _>>()
830                        .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
831                    if certs.is_empty() {
832                        return Err(format!("no certificates in CA file '{path}'"));
833                    }
834                    let mut roots = rustls::RootCertStore::empty();
835                    for cert in certs {
836                        roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
837                    }
838                    roots
839                }
840                None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
841            };
842            let builder = crate::tls_client_builder().with_root_certificates(roots);
843            let config = match client_cert {
844                None => builder.with_no_client_auth(),
845                Some((cert_path, key_path)) => {
846                    let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
847                    let chain = CertificateDer::pem_slice_iter(&bytes)
848                        .collect::<Result<Vec<_>, _>>()
849                        .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
850                    if chain.is_empty() {
851                        return Err(format!("no certificate in '{cert_path}'"));
852                    }
853                    let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
854                    let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
855                    builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
856                }
857            };
858            let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
859            Ok(UpstreamTls::with_config(config, &label))
860        }
861        #[cfg(not(any(feature = "http-client", feature = "http2")))]
862        {
863            let _ = client_cert;
864            Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
865        }
866    }
867
868    #[cfg(any(feature = "http-client", feature = "http2"))]
869    fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
870        #[cfg(feature = "http2")]
871        let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
872        UpstreamTls {
873            config: Arc::new(config),
874            pool: tls_pool_for_ca(label),
875            #[cfg(feature = "http2")]
876            async_pool: async_tls_pool_for_ca(label),
877            #[cfg(feature = "http2")]
878            h2_config,
879        }
880    }
881
882    /// The client configuration for HTTP/2 upstreams (ALPN `h2`).
883    #[cfg(feature = "http2")]
884    pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
885        &self.h2_config
886    }
887
888    /// Trust nothing: every TLS handshake fails, so the upstream answers 502.
889    /// Used when a configured CA file can't be loaded — failing closed rather
890    /// than falling back to the public roots.
891    pub(crate) fn reject_all(label: &str) -> UpstreamTls {
892        #[cfg(any(feature = "http-client", feature = "http2"))]
893        {
894            let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
895            UpstreamTls::with_config(config, &format!("reject-all:{label}"))
896        }
897        #[cfg(not(any(feature = "http-client", feature = "http2")))]
898        {
899            let _ = label;
900            UpstreamTls {}
901        }
902    }
903
904    /// How the non-blocking forwarder reaches these backends.
905    #[cfg(feature = "http2")]
906    pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
907        async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
908    }
909
910    /// The client configuration (for health checks).
911    #[cfg(any(feature = "http-client", feature = "http2"))]
912    pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
913        Arc::clone(&self.config)
914    }
915}
916
917/// One non-blocking connection pool per CA file, for the life of the process
918/// (a reload that keeps the same file keeps its warm connections).
919#[cfg(feature = "http2")]
920fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
921    static POOLS: std::sync::OnceLock<
922        std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
923    > = std::sync::OnceLock::new();
924    let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
925    pools
926        .entry(path.to_string())
927        .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
928}
929
930/// One pool per CA file, for the life of the process.
931#[cfg(any(feature = "http-client", feature = "http2"))]
932fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
933    static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
934        std::sync::OnceLock::new();
935    let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
936    pools
937        .entry(path.to_string())
938        .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
939}
940
941/// Forward `request` to `host:port` over plain HTTP/1.1, reusing an idle
942/// connection from `pool` when there is one and returning the connection to
943/// `pool` after a complete, reusable response.
944pub(crate) fn forward_pooled(
945    pool: &ConnPool,
946    request: &Request,
947    client: Peer<'_>,
948    host: &str,
949    port: u16,
950    connect_timeout: Duration,
951    read_timeout: Duration,
952) -> Result<Response, String> {
953    forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
954}
955
956/// [`forward_pooled`], sending `host_header` as the `Host` (the client's, for
957/// a gateway that must preserve it) instead of the backend's address.
958#[cfg(feature = "gateway-api")]
959#[allow(clippy::too_many_arguments)]
960pub(crate) fn forward_pooled_with_host(
961    pool: &ConnPool,
962    request: &Request,
963    client: Peer<'_>,
964    host: &str,
965    port: u16,
966    host_header: &str,
967    connect_timeout: Duration,
968    read_timeout: Duration,
969) -> Result<Response, String> {
970    forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
971}
972
973/// The pooled request/response cycle, for any connection type.
974///
975/// A backend may close an idle keep-alive connection at any moment, so a
976/// pooled connection that fails before *any* response byte arrives is
977/// retried once on a fresh connection — but only for idempotent methods
978/// (RFC 9110 §9.2.2): the request may already have reached the backend.
979#[allow(clippy::too_many_arguments)]
980fn forward_via<S: UpstreamIo>(
981    pool: &Pool<S>,
982    request: &Request,
983    client: Peer<'_>,
984    host: &str,
985    port: u16,
986    host_header: &str,
987    read_timeout: Duration,
988    connect: impl Fn() -> Result<S, String>,
989) -> Result<Response, String> {
990    let key = format!("{}:{}", host, port);
991    if let Some(stream) = pool.acquire(&key) {
992        match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
993            Exchange::Done(response) => return Ok(response),
994            Exchange::NoResponse(_) if is_idempotent(&request.method) => {} // stale: retry fresh
995            Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
996        }
997    }
998    let stream = connect()?;
999    match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1000        Exchange::Done(response) => Ok(response),
1001        Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
1002    }
1003}
1004
1005/// Connect to the first address of `host:port` that accepts. `localhost`
1006/// resolves to `::1` before `127.0.0.1` on many systems; a backend listening
1007/// on IPv4 only must still be reachable by name.
1008fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
1009    let key = format!("{}:{}", host, port);
1010    let addrs: Vec<_> = key
1011        .to_socket_addrs()
1012        .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
1013        .collect();
1014    let mut last_error = format!("no address resolved for {}", key);
1015    for addr in addrs {
1016        match TcpStream::connect_timeout(&addr, connect_timeout) {
1017            Ok(stream) => {
1018                let _ = stream.set_nodelay(true);
1019                return Ok(stream);
1020            }
1021            Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
1022        }
1023    }
1024    Err(last_error)
1025}
1026
1027/// The process-wide pool of TLS connections to `https://` backends.
1028#[cfg(any(feature = "http-client", feature = "http2"))]
1029fn shared_tls_pool() -> &'static Pool<TlsStream> {
1030    static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
1031    POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
1032}
1033
1034/// TLS client settings for backends: the Mozilla root store, no client
1035/// certificate. Built once — parsing the root store per request was a large
1036/// part of the per-request cost of HTTPS upstreams.
1037#[cfg(any(feature = "http-client", feature = "http2"))]
1038pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
1039    static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1040    Arc::clone(CONFIG.get_or_init(|| {
1041        let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1042        Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1043    }))
1044}
1045
1046#[cfg(any(feature = "http-client", feature = "http2"))]
1047fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1048    let tcp = connect_tcp(host, port, connect_timeout)?;
1049    let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1050        .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1051    let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1052    Ok(rustls::StreamOwned::new(conn, tcp))
1053}
1054
1055fn is_idempotent(method: &str) -> bool {
1056    matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1057}
1058
1059enum Exchange {
1060    Done(Response),
1061    /// Failed before any response byte (write error, or closed/errored on the first read).
1062    NoResponse(String),
1063    Failed(String),
1064}
1065
1066/// One request/response on `stream`. A fully read, keep-alive response puts
1067/// the connection back in `pool`; a streamed response keeps it until done.
1068fn exchange<S: UpstreamIo>(
1069    pool: &Pool<S>,
1070    key: &str,
1071    mut stream: S,
1072    request: &Request,
1073    host: &str,
1074    client: Peer<'_>,
1075    read_timeout: Duration,
1076) -> Exchange {
1077    if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1078        return Exchange::Failed(e.to_string());
1079    }
1080    // keep_alive = true: ask the backend to hold the connection open.
1081    let req_bytes = build_request(request, host, client, true);
1082    if let Err(e) = stream.write_all(&req_bytes) {
1083        return Exchange::NoResponse(format!("write to backend failed: {}", e));
1084    }
1085
1086    let mut tmp = [0u8; 4096];
1087    let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1088        Ok(head) => head,
1089        Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1090        Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1091    };
1092    let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1093
1094    if response_has_no_body(&request.method, &header_lower) {
1095        if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1096            pool.release(key, stream);
1097        }
1098        return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1099            Ok(response) => Exchange::Done(response),
1100            Err(e) => Exchange::Failed(e),
1101        };
1102    }
1103
1104    if should_stream_response(&header_lower) {
1105        // Streaming path — pipe bytes straight to the client; the connection
1106        // can't be reused while the body is in flight.
1107        return match parse_status_and_headers(&header_bytes) {
1108            Ok(mut response) => {
1109                response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1110                Exchange::Done(response)
1111            }
1112            Err(e) => Exchange::Failed(e),
1113        };
1114    }
1115    // Buffered path — read the full body, then return the connection to the pool.
1116    match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1117        Ok((resp_bytes, reusable)) => {
1118            if reusable {
1119                pool.release(key, stream);
1120            }
1121            match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1122                Ok(response) => Exchange::Done(response),
1123                Err(e) => Exchange::Failed(e),
1124            }
1125        }
1126        Err(e) => Exchange::Failed(e),
1127    }
1128}
1129
1130/// Read exactly the HTTP response headers (up to and including `\r\n\r\n`).
1131///
1132/// Returns `(header_bytes, body_prefix)` where `body_prefix` contains any
1133/// body bytes that arrived in the same TCP segment as the headers.
1134#[cfg(test)]
1135fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1136    read_response_head(stream, tmp).map_err(|e| match e {
1137        HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1138    })
1139}
1140
1141/// Why reading a response head failed: before any byte arrived (the request
1142/// may never have been processed — e.g. a pooled connection the backend had
1143/// already closed) or part-way through.
1144enum HeadError {
1145    NothingReceived(String),
1146    Partial(String),
1147}
1148
1149fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1150    read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1151}
1152
1153/// The final response head: interim `1xx` heads (`100 Continue`,
1154/// `103 Early Hints`, …) are read past (RFC 9110 §15.2); a `103`'s `Link`
1155/// values are passed on to the client (`early_hints::forward_upstream`). Returning one as
1156/// the response left the real response in the connection, and once pooled
1157/// the next request — possibly another client's — received it.
1158fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1159    let (mut head, mut rest) = read_response_head(stream, tmp)?;
1160    let mut interim = 0;
1161    while is_interim_response(&head) {
1162        crate::early_hints::forward_upstream(&head);
1163        interim += 1;
1164        if interim > MAX_INTERIM_RESPONSES {
1165            return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1166        }
1167        (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1168    }
1169    Ok((head, rest))
1170}
1171
1172/// Read until a complete head is in `buf` (which may already hold bytes
1173/// that followed a previous head). `received` — bytes already came from this
1174/// response, so a failure is never "nothing received".
1175fn read_response_head_from<S: Read>(
1176    stream: &mut S,
1177    tmp: &mut [u8],
1178    mut buf: Vec<u8>,
1179    received: bool,
1180) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1181    loop {
1182        if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1183            let body_prefix = buf[pos + 4..].to_vec();
1184            buf.truncate(pos + 4);
1185            return Ok((buf, body_prefix));
1186        }
1187        let nothing = buf.is_empty() && !received;
1188        let n = match stream.read(tmp) {
1189            Ok(n) => n,
1190            Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1191            Err(e) => return Err(HeadError::Partial(e.to_string())),
1192        };
1193        if n == 0 {
1194            let msg = "backend closed connection before headers were complete".to_string();
1195            return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1196        }
1197        buf.extend_from_slice(&tmp[..n]);
1198    }
1199}
1200
1201/// Interim responses tolerated before the final one.
1202pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1203
1204/// An interim (1xx) response head other than `101 Switching Protocols`
1205/// (which ends the HTTP exchange): the final response follows it.
1206pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1207    let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1208    let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1209    status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1210}
1211
1212/// RFC 9112 §6.3: a response to HEAD, and any 1xx, 204 or 304 response, ends
1213/// with its head — whatever `Content-Length` says (for HEAD it describes the
1214/// GET body). Waiting for that body hung until the read timeout (a 502).
1215pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1216    let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1217    method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1218}
1219
1220/// Returns `true` when the response should be streamed rather than buffered.
1221///
1222/// Streams when any of the following hold:
1223/// - `Content-Type: text/event-stream` — SSE
1224/// - `Transfer-Encoding: chunked` — AI token streams, etc.
1225/// - `Content-Length` exceeds 1 MB — large file downloads
1226pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1227    let is_sse = header_lower.lines().any(|l| {
1228        l.starts_with("content-type:") && l.contains("text/event-stream")
1229    });
1230    let is_chunked = header_lower.lines().any(|l| {
1231        l.starts_with("transfer-encoding:") && l.contains("chunked")
1232    });
1233    let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1234        l.strip_prefix("content-length:")?.trim().parse().ok()
1235    });
1236    let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1237    is_sse || is_chunked || is_large
1238}
1239
1240/// What the forwarders need from an upstream response head, found in one
1241/// case-insensitive pass over the raw bytes — the same answers as
1242/// [`response_has_no_body`], [`should_stream_response`] and the
1243/// `connection:` / `content-length:` scans over a lower-cased copy, without
1244/// making that copy.
1245#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1246pub(crate) struct HeadFacts {
1247    /// The second whitespace-separated token of the head (the status code).
1248    pub(crate) status_no_body: bool,
1249    pub(crate) sse: bool,
1250    pub(crate) chunked: bool,
1251    /// The first `content-length:` value that parses.
1252    pub(crate) content_length: Option<usize>,
1253    pub(crate) connection_close: bool,
1254}
1255
1256#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1257impl HeadFacts {
1258    pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1259        let text = std::str::from_utf8(head).unwrap_or("");
1260        let status = text.split_whitespace().nth(1).unwrap_or("");
1261        let mut facts = HeadFacts {
1262            status_no_body: status.starts_with('1') || status == "204" || status == "304",
1263            sse: false,
1264            chunked: false,
1265            content_length: None,
1266            connection_close: false,
1267        };
1268        for line in text.lines() {
1269            if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1270                facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1271            } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1272                facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1273            } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1274                if facts.content_length.is_none() {
1275                    facts.content_length = value.trim().parse().ok();
1276                }
1277            } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1278                facts.connection_close |= contains_ignore_ascii_case(value, "close");
1279            }
1280        }
1281        facts
1282    }
1283
1284    /// [`response_has_no_body`].
1285    pub(crate) fn no_body(&self, method: &str) -> bool {
1286        method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1287    }
1288
1289    /// [`should_stream_response`].
1290    pub(crate) fn should_stream(&self) -> bool {
1291        self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1292    }
1293}
1294
1295fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1296    let bytes = line.as_bytes();
1297    // `prefix` is ASCII, so a byte match ends on a char boundary.
1298    (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1299        .then(|| &line[prefix.len()..])
1300}
1301
1302fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1303    haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1304}
1305
1306/// Parse the status line and headers from raw header bytes (ending at `\r\n\r\n`).
1307fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1308    let s = std::str::from_utf8(header_bytes)
1309        .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1310    let mut lines = s.lines();
1311    let status_line = lines.next().ok_or("empty backend response")?;
1312    let mut parts = status_line.splitn(3, ' ');
1313    let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1314    let status_code: i16 = parts
1315        .next()
1316        .unwrap_or("502")
1317        .parse()
1318        .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1319    let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1320    let mut headers = Vec::new();
1321    for line in lines {
1322        let line = line.trim_end_matches('\r');
1323        if line.is_empty() { break; }
1324        if let Some(colon) = line.find(':') {
1325            headers.push(crate::header::Header {
1326                name: line[..colon].trim().to_string().into(),
1327                value: (line[colon + 1..].trim().to_string()).into(),
1328            });
1329        }
1330    }
1331    Ok(Response {
1332        http_version,
1333        status_code,
1334        reason_phrase,
1335        headers,
1336        content_range_list: vec![],
1337        stream_file: None,
1338        stream_pipe: None,
1339        trailers: None,
1340    })
1341}
1342
1343/// Read the remaining body after headers have already been read.
1344///
1345/// `header_bytes` ends with `\r\n\r\n`; `body_prefix` holds any body bytes
1346/// that arrived in the same TCP read.  Handles all three body mechanisms
1347/// (chunked, content-length, read-to-EOF).  Returns `(full_response_bytes, can_reuse)`.
1348fn read_response_from_partial<S: Read>(
1349    stream: &mut S,
1350    header_bytes: Vec<u8>,
1351    body_prefix: Vec<u8>,
1352    tmp: &mut [u8],
1353) -> Result<(Vec<u8>, bool), String> {
1354    let header_end = header_bytes.len();
1355    let mut buf = header_bytes;
1356    buf.extend_from_slice(&body_prefix);
1357
1358    let header_lower =
1359        std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1360    let connection_close =
1361        header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1362    let is_chunked = header_lower
1363        .lines()
1364        .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1365    let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1366        l.strip_prefix("content-length:")?.trim().parse().ok()
1367    });
1368
1369    if is_chunked {
1370        let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1371        rewrite_as_content_length(&mut buf, header_end, &decoded);
1372        Ok((buf, !connection_close))
1373    } else if let Some(len) = content_length {
1374        while buf.len() < header_end + len {
1375            let n = stream.read(tmp).map_err(|e| e.to_string())?;
1376            if n == 0 { break; }
1377            buf.extend_from_slice(&tmp[..n]);
1378        }
1379        Ok((buf, !connection_close))
1380    } else {
1381        loop {
1382            let n = stream.read(tmp).map_err(|e| e.to_string())?;
1383            if n == 0 { break; }
1384            buf.extend_from_slice(&tmp[..n]);
1385        }
1386        Ok((buf, false))
1387    }
1388}
1389
1390// ── Backend URL parsing ───────────────────────────────────────────────────────
1391
1392struct Backend {
1393    host: String,
1394    port: u16,
1395    /// Whether the upstream connection should use TLS.
1396    /// Set when the URL scheme is `https://`, `h2s://`, or `grpcs://`.
1397    tls: bool,
1398}
1399
1400impl Backend {
1401    fn parse(url: &str) -> Option<Self> {
1402        let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1403            (r, true, 443u16)
1404        } else if let Some(r) = url.strip_prefix("h2s://") {
1405            (r, true, 443u16)
1406        } else if let Some(r) = url.strip_prefix("grpcs://") {
1407            (r, true, 443u16)
1408        } else if let Some(r) = url.strip_prefix("http://") {
1409            (r, false, 80u16)
1410        } else if let Some(r) = url.strip_prefix("h2://") {
1411            (r, false, 80u16)
1412        } else if let Some(r) = url.strip_prefix("grpc://") {
1413            (r, false, 80u16)
1414        } else {
1415            (url, false, 80u16)
1416        };
1417        // Drop any path component.
1418        let host_port = rest.split('/').next().unwrap_or(rest);
1419        let (host, port) = if let Some(colon) = host_port.rfind(':') {
1420            let port_str = &host_port[colon + 1..];
1421            if let Ok(p) = port_str.parse::<u16>() {
1422                (host_port[..colon].to_string(), p)
1423            } else {
1424                (host_port.to_string(), default_port)
1425            }
1426        } else {
1427            (host_port.to_string(), default_port)
1428        };
1429        if host.is_empty() {
1430            return None;
1431        }
1432        Some(Backend { host, port, tls })
1433    }
1434}
1435
1436// ── HTTP/2 reverse proxy ──────────────────────────────────────────────────────
1437
1438/// Reverse proxy that forwards requests to HTTP/2 backends.
1439///
1440/// Wraps [`ReverseProxy`] and forces HTTP/2 (`h2`) for all upstream connections.
1441/// Requires the `http2` Cargo feature.
1442///
1443/// This proxy also transparently handles gRPC traffic
1444/// (`Content-Type: application/grpc*`) — gRPC DATA frames are forwarded
1445/// as-is because gRPC is layered directly on HTTP/2.
1446#[cfg(feature = "http2")]
1447pub struct H2ReverseProxy {
1448    inner: ReverseProxy,
1449}
1450
1451#[cfg(feature = "http2")]
1452impl H2ReverseProxy {
1453    /// Create a proxy distributing requests across `backends` in round-robin order.
1454    ///
1455    /// Each backend entry can be:
1456    /// - `"host:port"` — plain TCP (HTTP/2 cleartext)
1457    /// - `"h2://host:port"` — plain TCP (explicit scheme)
1458    /// - `"h2s://host:port"` — TLS (HTTP/2 over HTTPS; port defaults to 443)
1459    /// - `"https://host:port"` — TLS (same as `h2s://`)
1460    ///
1461    /// TLS backends require the `http2` Cargo feature (includes `rustls` +
1462    /// `webpki-roots`).  Certificate verification uses the WebPKI trust store.
1463    pub fn new<I, S>(backends: I) -> Self
1464    where
1465        I: IntoIterator<Item = S>,
1466        S: AsRef<str>,
1467    {
1468        H2ReverseProxy {
1469            inner: ReverseProxy::new(backends),
1470        }
1471    }
1472
1473    /// Only proxy requests whose URI starts with `prefix`; pass others through.
1474    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1475        self.inner = self.inner.path_prefix(prefix);
1476        self
1477    }
1478
1479    /// Override the TCP connect timeout (default: 5 s).
1480    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1481        self.inner = self.inner.connect_timeout_ms(ms);
1482        self
1483    }
1484
1485    /// Override the read timeout (default: 30 s). It bounds each wait for
1486    /// the upstream — the handshake, the response head, every body frame — so
1487    /// a streaming response (a gRPC server stream) ends only when it goes
1488    /// quiet for that long.
1489    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1490        self.inner = self.inner.read_timeout_ms(ms);
1491        self
1492    }
1493
1494    /// Trust the CA certificates in the PEM file `path` for `h2s://` /
1495    /// `https://` backends instead of the WebPKI roots. Since 18.3.0.
1496    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1497        self.inner = self.inner.tls_ca_file(path)?;
1498        Ok(self)
1499    }
1500
1501    /// Present a client certificate to TLS backends that require one (mutual
1502    /// TLS). Since 18.3.0.
1503    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1504        self.inner = self.inner.tls_client_cert(cert, key)?;
1505        Ok(self)
1506    }
1507
1508    fn matches(&self, request: &Request) -> bool {
1509        self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1510    }
1511}
1512
1513#[cfg(feature = "http2")]
1514impl crate::middleware::Middleware for H2ReverseProxy {
1515    fn handle(
1516        &self,
1517        request: &crate::request::Request,
1518        connection: &crate::server::ConnectionInfo,
1519        next: &dyn crate::application::Application,
1520    ) -> Result<crate::response::Response, String> {
1521        if !self.matches(request) {
1522            return next.execute(request, connection);
1523        }
1524        if self.inner.backends.is_empty() {
1525            let mut response = bad_gateway();
1526            status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1527            return Ok(response);
1528        }
1529        // A streamed request body can be sent once: no second backend then.
1530        let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1531        let n = self.inner.backends.len();
1532        let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1533        let mut last_error = String::new();
1534        for attempt in 0..attempts {
1535            let backend = &self.inner.backends[(start + attempt) % n];
1536            let tls = backend.tls.then(|| match &self.inner.trust {
1537                Some(trust) => trust.h2_config(),
1538                None => h2_upstream::default_tls(),
1539            });
1540            let target = h2_upstream::Target {
1541                backend,
1542                tls,
1543                connect_timeout: self.inner.connect_timeout,
1544                read_timeout: self.inner.read_timeout,
1545                authority: None,
1546            };
1547            match h2_upstream::forward(request, Peer::of(connection), &target) {
1548                Ok(mut response) => {
1549                    status::received(&mut response, None);
1550                    return Ok(response);
1551                }
1552                Err(e) => last_error = e,
1553            }
1554        }
1555        let mut response = bad_gateway();
1556        status::failed_with(&mut response, &last_error, None);
1557        Ok(response)
1558    }
1559
1560    /// Headers only: a request this proxy doesn't forward passes through
1561    /// untouched.
1562    fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1563        true
1564    }
1565
1566    /// Requests this proxy forwards are sent as their body arrives (gRPC
1567    /// client streaming and bidirectional calls).
1568    fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1569        self.matches(request)
1570    }
1571}
1572
1573// ── HTTP/3 reverse proxy ──────────────────────────────────────────────────────
1574
1575/// Reverse proxy that forwards requests to `https://` backends over HTTP/3
1576/// (QUIC). Since 18.13.0; requires the `http3` feature (default).
1577///
1578/// A backend QUIC can't reach (UDP blocked, no handshake within the connect
1579/// timeout, a TLS failure) is sent the request over HTTPS on TCP instead,
1580/// and stays on TCP for five minutes; `rws_upstream_h3_fallbacks_total`
1581/// counts it under the proxy's [`name`](Self::name).
1582///
1583/// ```rust,no_run
1584/// use rust_web_server::app::App;
1585/// use rust_web_server::core::New;
1586/// use rust_web_server::proxy::H3ReverseProxy;
1587///
1588/// let proxy = H3ReverseProxy::new(["https://edge1.internal:443", "https://edge2.internal:443"])
1589///     .path_prefix("/api")
1590///     .tls_ca_file("/etc/rws/internal-ca.pem")
1591///     .unwrap();
1592/// let app = App::new().wrap(proxy);
1593/// ```
1594#[cfg(feature = "http3")]
1595pub struct H3ReverseProxy {
1596    inner: ReverseProxy,
1597    name: String,
1598    /// Backends on TCP until the time kept here.
1599    tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1600}
1601
1602#[cfg(feature = "http3")]
1603impl H3ReverseProxy {
1604    /// A proxy distributing requests across `backends` (`https://host:port`,
1605    /// port 443 by default) in round-robin order. Certificates are checked
1606    /// against the WebPKI roots unless [`tls_ca_file`](Self::tls_ca_file)
1607    /// says otherwise. A backend that isn't `https://` is reached over
1608    /// HTTP/1.1 on TCP.
1609    pub fn new<I, S>(backends: I) -> Self
1610    where
1611        I: IntoIterator<Item = S>,
1612        S: AsRef<str>,
1613    {
1614        H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1615    }
1616
1617    /// The `upstream` label of this proxy's fallback metric.
1618    pub fn name(mut self, name: impl Into<String>) -> Self {
1619        self.name = name.into();
1620        self
1621    }
1622
1623    /// Only proxy requests whose URI starts with `prefix`; pass others through.
1624    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1625        self.inner = self.inner.path_prefix(prefix);
1626        self
1627    }
1628
1629    /// QUIC handshake (and TCP connect) timeout (default: 5 s).
1630    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1631        self.inner = self.inner.connect_timeout_ms(ms);
1632        self
1633    }
1634
1635    /// Longest wait for the backend — the response head and each body
1636    /// chunk (default: 30 s).
1637    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1638        self.inner = self.inner.read_timeout_ms(ms);
1639        self
1640    }
1641
1642    /// Trust the CA certificates in the PEM file `path` instead of the
1643    /// WebPKI roots.
1644    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1645        self.inner = self.inner.tls_ca_file(path)?;
1646        Ok(self)
1647    }
1648
1649    /// Present a client certificate to backends that require one.
1650    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1651        self.inner = self.inner.tls_client_cert(cert, key)?;
1652        Ok(self)
1653    }
1654
1655    fn matches(&self, request: &Request) -> bool {
1656        self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1657    }
1658
1659    fn on_tcp(&self, key: &str) -> bool {
1660        let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1661        match tcp_until.get(key) {
1662            Some(until) if std::time::Instant::now() < *until => true,
1663            Some(_) => {
1664                tcp_until.remove(key);
1665                false
1666            }
1667            None => false,
1668        }
1669    }
1670
1671    /// One attempt at `backend`: HTTP/3 unless it's on TCP, TCP when QUIC
1672    /// can't connect.
1673    fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1674        let key = format!("{}:{}", backend.host, backend.port);
1675        if backend.tls && !self.on_tcp(&key) {
1676            let config = match &self.inner.trust {
1677                Some(trust) => trust.client_config(),
1678                None => tls_client_config(),
1679            };
1680            let target = h3_upstream::Target {
1681                host: &backend.host,
1682                port: backend.port,
1683                authority: None,
1684                tls: &config,
1685                connect_timeout: self.inner.connect_timeout,
1686                read_timeout: self.inner.read_timeout,
1687            };
1688            match h3_upstream::forward(request, Peer::of(connection), &target) {
1689                Err(h3_upstream::H3Error::Connect(e)) => {
1690                    self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1691                    crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1692                    eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1693                }
1694                result => return result.map_err(|e| e.to_string()),
1695            }
1696        }
1697        forward_upstream_trusting(
1698            &self.inner.pool,
1699            request,
1700            Peer::of(connection),
1701            &backend.host,
1702            backend.port,
1703            backend.tls,
1704            self.inner.trust.as_deref(),
1705            self.inner.connect_timeout,
1706            self.inner.read_timeout,
1707        )
1708    }
1709}
1710
1711#[cfg(feature = "http3")]
1712impl crate::middleware::Middleware for H3ReverseProxy {
1713    fn handle(
1714        &self,
1715        request: &crate::request::Request,
1716        connection: &crate::server::ConnectionInfo,
1717        next: &dyn crate::application::Application,
1718    ) -> Result<crate::response::Response, String> {
1719        if !self.matches(request) {
1720            return next.execute(request, connection);
1721        }
1722        if self.inner.backends.is_empty() {
1723            let mut response = bad_gateway();
1724            status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1725            return Ok(response);
1726        }
1727        // A streamed request body can be sent once: no second backend then.
1728        let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1729        let n = self.inner.backends.len();
1730        let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1731        let mut last_error = String::new();
1732        for attempt in 0..attempts {
1733            match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1734                Ok(mut response) => {
1735                    status::received(&mut response, None);
1736                    return Ok(response);
1737                }
1738                Err(e) => last_error = e,
1739            }
1740        }
1741        let mut response = bad_gateway();
1742        status::failed_with(&mut response, &last_error, None);
1743        Ok(response)
1744    }
1745
1746    fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1747        true
1748    }
1749
1750    /// Requests this proxy forwards are sent as their body arrives.
1751    fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1752        self.matches(request)
1753    }
1754}
1755
1756/// Forward `request` over HTTP/2 to `url` (`h2://`, `grpc://`, `h2s://`,
1757/// `grpcs://`, `https://` or `host:port`) — the config file's `grpc` action.
1758/// `tls` forces TLS for a bare `host:port` (`[[upstream]] tls = true`).
1759#[cfg(feature = "http2")]
1760pub(crate) fn forward_h2(
1761    request: &Request,
1762    client: Peer<'_>,
1763    url: &str,
1764    tls: bool,
1765    trust: Option<&UpstreamTls>,
1766    connect_timeout: Duration,
1767    read_timeout: Duration,
1768) -> Result<Response, String> {
1769    let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1770    backend.tls |= tls;
1771    let tls = backend.tls.then(|| match trust {
1772        Some(t) => t.h2_config(),
1773        None => h2_upstream::default_tls(),
1774    });
1775    h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1776}
1777
1778/// Forward over TLS to `ip:port` with `config`, `server_name` as SNI (and
1779/// the name `config`'s verifier checks), `host_header` as the `Host` — the
1780/// Gateway API router's BackendTLSPolicy backends. Connections are pooled
1781/// per `label` (one per distinct policy content).
1782#[cfg(feature = "gateway-api")]
1783#[allow(clippy::too_many_arguments)]
1784pub(crate) fn forward_tls_with_host(
1785    config: &Arc<rustls::ClientConfig>,
1786    label: &str,
1787    request: &Request,
1788    client: Peer<'_>,
1789    ip: &str,
1790    port: u16,
1791    server_name: &str,
1792    host_header: &str,
1793    connect_timeout: Duration,
1794    read_timeout: Duration,
1795) -> Result<Response, String> {
1796    let pool = tls_pool_for_ca(label);
1797    forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1798        let tcp = connect_tcp(ip, port, connect_timeout)?;
1799        let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1800        let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1801        Ok(rustls::StreamOwned::new(conn, tcp))
1802    })
1803}
1804
1805/// Forward over cleartext HTTP/2 with prior knowledge (h2c) to `host:port`,
1806/// sending `authority` as `:authority` — the Gateway API router's
1807/// `kubernetes.io/h2c` backends.
1808#[cfg(feature = "gateway-api")]
1809pub(crate) fn forward_h2c_with_host(
1810    request: &Request,
1811    client: Peer<'_>,
1812    host: &str,
1813    port: u16,
1814    authority: &str,
1815    connect_timeout: Duration,
1816    read_timeout: Duration,
1817) -> Result<Response, String> {
1818    let backend = Backend { host: host.to_string(), port, tls: false };
1819    h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1820}
1821
1822// ── gRPC proxy ────────────────────────────────────────────────────────────────
1823
1824/// gRPC reverse proxy middleware.
1825///
1826/// Recognises requests with `Content-Type: application/grpc*` and forwards them
1827/// to a backend over HTTP/2, leaving all other requests to the next layer.
1828///
1829/// Requires the `http2` Cargo feature.
1830///
1831/// # Example
1832///
1833/// ```rust,no_run
1834/// use rust_web_server::app::App;
1835/// use rust_web_server::core::New;
1836/// use rust_web_server::proxy::GrpcProxy;
1837///
1838/// let app = App::new()
1839///     .wrap(GrpcProxy::new(["grpc-service:50051"]));
1840/// ```
1841#[cfg(feature = "http2")]
1842pub struct GrpcProxy {
1843    inner: H2ReverseProxy,
1844}
1845
1846#[cfg(feature = "http2")]
1847impl GrpcProxy {
1848    /// Create a proxy distributing gRPC connections across `backends` in round-robin order.
1849    ///
1850    /// Each backend entry can be:
1851    /// - `"host:port"` — plain TCP (gRPC cleartext)
1852    /// - `"grpc://host:port"` — plain TCP (explicit scheme)
1853    /// - `"grpcs://host:port"` — TLS (gRPC over TLS; port defaults to 443)
1854    /// - `"https://host:port"` — TLS (same as `grpcs://`)
1855    ///
1856    /// TLS backends require the `http2` Cargo feature.
1857    pub fn new<I, S>(backends: I) -> Self
1858    where
1859        I: IntoIterator<Item = S>,
1860        S: AsRef<str>,
1861    {
1862        GrpcProxy { inner: H2ReverseProxy::new(backends) }
1863    }
1864
1865    /// Only proxy requests whose URI starts with `prefix`; pass others through.
1866    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1867        self.inner = self.inner.path_prefix(prefix);
1868        self
1869    }
1870
1871    /// See [`H2ReverseProxy::connect_timeout_ms`].
1872    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1873        self.inner = self.inner.connect_timeout_ms(ms);
1874        self
1875    }
1876
1877    /// See [`H2ReverseProxy::read_timeout_ms`]: a stream quiet for longer ends.
1878    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1879        self.inner = self.inner.read_timeout_ms(ms);
1880        self
1881    }
1882
1883    /// Trust this CA for `grpcs://` backends. Since 18.3.0.
1884    pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1885        self.inner = self.inner.tls_ca_file(path)?;
1886        Ok(self)
1887    }
1888
1889    /// Present a client certificate to `grpcs://` backends (mutual TLS).
1890    /// Since 18.3.0.
1891    pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1892        self.inner = self.inner.tls_client_cert(cert, key)?;
1893        Ok(self)
1894    }
1895}
1896
1897#[cfg(feature = "http2")]
1898impl crate::middleware::Middleware for GrpcProxy {
1899    fn handle(
1900        &self,
1901        request: &crate::request::Request,
1902        connection: &crate::server::ConnectionInfo,
1903        next: &dyn crate::application::Application,
1904    ) -> Result<crate::response::Response, String> {
1905        if is_grpc(request) {
1906            self.inner.handle(request, connection, next)
1907        } else {
1908            next.execute(request, connection)
1909        }
1910    }
1911
1912    fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1913        true
1914    }
1915
1916    fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1917        is_grpc(request) && self.inner.matches(request)
1918    }
1919}
1920
1921#[cfg(feature = "http2")]
1922fn is_grpc(request: &Request) -> bool {
1923    request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1924}
1925
1926// ── Backend::parse unit tests ─────────────────────────────────────────────────
1927
1928#[cfg(test)]
1929mod backend_parse_tests {
1930    use super::Backend;
1931
1932    fn parse(url: &str) -> Option<(String, u16, bool)> {
1933        Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1934    }
1935
1936    #[test]
1937    fn bare_host_port() {
1938        assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1939    }
1940
1941    #[test]
1942    fn http_scheme() {
1943        assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1944    }
1945
1946    #[test]
1947    fn h2_scheme_plain() {
1948        assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1949    }
1950
1951    #[test]
1952    fn grpc_scheme_plain() {
1953        assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1954    }
1955
1956    #[test]
1957    fn https_scheme_sets_tls_and_default_port() {
1958        assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1959    }
1960
1961    #[test]
1962    fn https_scheme_explicit_port() {
1963        assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1964    }
1965
1966    #[test]
1967    fn h2s_scheme_sets_tls() {
1968        assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1969    }
1970
1971    #[test]
1972    fn h2s_scheme_explicit_port() {
1973        assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1974    }
1975
1976    #[test]
1977    fn grpcs_scheme_sets_tls() {
1978        assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1979    }
1980
1981    #[test]
1982    fn grpcs_scheme_explicit_port() {
1983        assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1984    }
1985
1986    #[test]
1987    fn empty_host_returns_none() {
1988        assert_eq!(None, parse("https://"));
1989    }
1990
1991    #[test]
1992    fn bare_host_no_port_defaults_to_80() {
1993        assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1994    }
1995}