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;
27
28#[cfg(test)]
29mod tests;
30
31use std::io::{Read, Write};
32use std::net::{TcpStream, ToSocketAddrs};
33use std::sync::atomic::{AtomicUsize, Ordering};
34use std::sync::Arc;
35use std::time::Duration;
36
37pub use pool::ConnPool;
38
39use crate::application::Application;
40use crate::core::New;
41use crate::middleware::Middleware;
42use crate::mime_type::MimeType;
43use crate::range::Range;
44use crate::request::Request;
45use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
46use crate::server::ConnectionInfo;
47
48// Hop-by-hop headers that must not be forwarded (RFC 7230 §6.1)
49const HOP_BY_HOP: &[&str] = &[
50    "connection",
51    "keep-alive",
52    "proxy-authenticate",
53    "proxy-authorization",
54    "te",
55    "trailers",
56    "transfer-encoding",
57    "upgrade",
58];
59
60/// Load balancing strategy used by [`ReverseProxy`].
61pub enum LoadBalancing {
62    /// Distribute requests across backends in a cyclic order.
63    RoundRobin,
64}
65
66/// Reverse proxy middleware.
67///
68/// Forwards incoming requests to one of the configured backends over HTTP/1.1.
69/// On connection failure the next backend in the list is tried; when all
70/// backends have failed the middleware returns `502 Bad Gateway`.
71///
72/// Hop-by-hop headers are stripped before forwarding.  `X-Forwarded-For` and
73/// `Via` are added to every forwarded request.
74///
75/// Idle connections are pooled and reused across requests (up to
76/// [`ConnPool::new_default`] limits: 8 idle per backend, 60-second timeout).
77/// This eliminates per-request TCP handshake overhead and ephemeral-port
78/// exhaustion.  Use [`ReverseProxy::with_pool`] to share a pool across
79/// multiple proxy instances or to tune pool parameters.
80pub struct ReverseProxy {
81    backends: Vec<Backend>,
82    path_prefix: Option<String>,
83    connect_timeout: Duration,
84    read_timeout: Duration,
85    counter: AtomicUsize,
86    pool: Arc<ConnPool>,
87    breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
88}
89
90impl ReverseProxy {
91    /// Create a proxy that distributes requests across `backends` in
92    /// round-robin order.  Each entry must be `"http://host:port"` or
93    /// `"host:port"` (port defaults to 80).
94    pub fn new<I, S>(backends: I) -> Self
95    where
96        I: IntoIterator<Item = S>,
97        S: AsRef<str>,
98    {
99        Self {
100            backends: backends
101                .into_iter()
102                .filter_map(|u| Backend::parse(u.as_ref()))
103                .collect(),
104            path_prefix: None,
105            connect_timeout: Duration::from_secs(5),
106            read_timeout: Duration::from_secs(30),
107            counter: AtomicUsize::new(0),
108            pool: Arc::new(ConnPool::new_default()),
109            breaker: None,
110        }
111    }
112
113    /// Wire a [`crate::circuit_breaker::CircuitBreaker`] or
114    /// [`crate::circuit_breaker::RedisCircuitBreaker`] into this proxy:
115    /// before dialing a backend, its breaker state is checked (an `Open`
116    /// backend is skipped with no TCP attempt, exactly like a caller
117    /// manually checking `is_available` around each proxied call); after a
118    /// dial, `record_success`/`record_failure` is called automatically.
119    /// No breaker is wired by default — existing `ReverseProxy` callers see
120    /// no behavior change unless they opt in here.
121    ///
122    /// ```rust,no_run
123    /// use std::sync::Arc;
124    /// use rust_web_server::circuit_breaker::global as global_breaker;
125    /// use rust_web_server::proxy::ReverseProxy;
126    ///
127    /// let proxy = ReverseProxy::new(["http://backend-1:8080", "http://backend-2:8080"])
128    ///     .with_circuit_breaker(Arc::new(global_breaker()));
129    /// ```
130    pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
131        self.breaker = Some(breaker);
132        self
133    }
134
135    /// Only proxy requests whose URI starts with `prefix`.
136    ///
137    /// Other requests are passed through to the next layer in the middleware
138    /// chain (or the inner application).
139    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
140        self.path_prefix = Some(prefix.into());
141        self
142    }
143
144    /// Override the load balancing strategy (currently only `RoundRobin`).
145    pub fn strategy(self, _strategy: LoadBalancing) -> Self {
146        self
147    }
148
149    /// Override the TCP connect timeout (default: 5 000 ms).
150    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
151        self.connect_timeout = Duration::from_millis(ms);
152        self
153    }
154
155    /// Override the response read timeout (default: 30 000 ms).
156    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
157        self.read_timeout = Duration::from_millis(ms);
158        self
159    }
160
161    /// Attach a shared connection pool.
162    ///
163    /// Useful for sharing one pool across multiple `ReverseProxy` instances
164    /// or for tuning pool parameters (capacity, idle timeout).
165    pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
166        self.pool = pool;
167        self
168    }
169
170    /// Set the maximum number of idle connections per backend (default: 8).
171    pub fn max_idle_conns(mut self, n: usize) -> Self {
172        self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
173        self
174    }
175
176    fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
177        if self.backends.is_empty() {
178            return Err("no backends configured".to_string());
179        }
180        let n = self.backends.len();
181        let start = self.counter.fetch_add(1, Ordering::Relaxed);
182        let mut last_err = "all backends failed".to_string();
183        for attempt in 0..n {
184            let idx = (start + attempt) % n;
185            let backend = &self.backends[idx];
186            let key = format!("{}:{}", backend.host, backend.port);
187
188            if let Some(breaker) = &self.breaker {
189                if !breaker.is_available(&key) {
190                    last_err = format!("circuit open for {}", key);
191                    continue;
192                }
193            }
194
195            match self.try_backend(request, connection, backend) {
196                Ok(resp) => {
197                    if let Some(breaker) = &self.breaker {
198                        breaker.record_success(&key);
199                    }
200                    return Ok(resp);
201                }
202                Err(e) => {
203                    if let Some(breaker) = &self.breaker {
204                        breaker.record_failure(&key);
205                    }
206                    last_err = e;
207                }
208            }
209        }
210        Err(last_err)
211    }
212
213    fn try_backend(
214        &self,
215        request: &Request,
216        connection: &ConnectionInfo,
217        backend: &Backend,
218    ) -> Result<Response, String> {
219        let key = format!("{}:{}", backend.host, backend.port);
220
221        // Try a pooled connection first; fall back to a fresh one.
222        let stream = if let Some(pooled) = self.pool.acquire(&key) {
223            pooled
224        } else {
225            let addr_str = key.as_str();
226            let sock_addr = addr_str
227                .to_socket_addrs()
228                .map_err(|e| format!("DNS lookup for {} failed: {}", addr_str, e))?
229                .next()
230                .ok_or_else(|| format!("no address resolved for {}", addr_str))?;
231            TcpStream::connect_timeout(&sock_addr, self.connect_timeout)
232                .map_err(|e| format!("connect to {} failed: {}", addr_str, e))?
233        };
234
235        stream.set_read_timeout(Some(self.read_timeout)).map_err(|e| e.to_string())?;
236        stream.set_write_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;
237
238        // keep_alive = true: send Connection: keep-alive so the server holds
239        // the connection open after responding.
240        let req_bytes = build_request(request, &backend.host, &connection.client.ip, true);
241        let mut stream = stream;
242        stream.write_all(&req_bytes).map_err(|e| format!("write to backend failed: {}", e))?;
243
244        let mut tmp = [0u8; 4096];
245        let (header_bytes, body_prefix) = read_headers_only(&mut stream, &mut tmp)?;
246        let header_lower =
247            std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
248
249        if should_stream_response(&header_lower) {
250            // Streaming path — pipe bytes straight to the client.
251            // The connection cannot be reused while the body is in flight.
252            let mut resp = parse_status_and_headers(&header_bytes)?;
253            resp.stream_pipe =
254                Some(Box::new(ConcatReader::new(body_prefix, stream)));
255            Ok(resp)
256        } else {
257            // Buffered path — read the full body, then optionally return the
258            // connection to the pool.
259            let (resp_bytes, reusable) =
260                read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp)?;
261            if reusable {
262                self.pool.release(&key, stream);
263            }
264            Response::parse(&resp_bytes).map(normalize_upstream_response)
265        }
266    }
267}
268
269impl Middleware for ReverseProxy {
270    fn handle(
271        &self,
272        request: &Request,
273        connection: &ConnectionInfo,
274        next: &dyn Application,
275    ) -> Result<Response, String> {
276        if let Some(prefix) = &self.path_prefix {
277            if !request.request_uri.starts_with(prefix.as_str()) {
278                return next.execute(request, connection);
279            }
280        }
281        match self.proxy(request, connection) {
282            Ok(resp) => Ok(resp),
283            Err(_) => Ok(bad_gateway()),
284        }
285    }
286}
287
288// ── helpers ───────────────────────────────────────────────────────────────────
289
290pub(crate) fn build_request(
291    request: &Request,
292    backend_host: &str,
293    client_ip: &str,
294    keep_alive: bool,
295) -> Vec<u8> {
296    let mut out: Vec<u8> = Vec::new();
297    let _ = write!(
298        out,
299        "{} {} HTTP/1.1\r\nHost: {}\r\n",
300        request.method, request.request_uri, backend_host
301    );
302    for h in &request.headers {
303        let lower = h.name.to_lowercase();
304        if HOP_BY_HOP.contains(&lower.as_str()) || lower == "host" {
305            continue;
306        }
307        let _ = write!(out, "{}: {}\r\n", h.name, h.value);
308    }
309    let _ = write!(out, "X-Forwarded-For: {}\r\n", client_ip);
310    let _ = write!(out, "Via: 1.1 rws\r\n");
311    if keep_alive {
312        let _ = write!(out, "Connection: keep-alive\r\n");
313    } else {
314        let _ = write!(out, "Connection: close\r\n");
315    }
316    if !request.body.is_empty() {
317        let _ = write!(out, "Content-Length: {}\r\n", request.body.len());
318    }
319    let _ = write!(out, "\r\n");
320    out.extend_from_slice(&request.body);
321    out
322}
323
324/// Decode HTTP/1.1 chunked transfer-encoding from `stream`.
325///
326/// `buf[header_end..]` may already contain some body bytes that arrived in
327/// the same read as the headers.  Returns the fully decoded body.
328fn decode_chunked(
329    stream: &mut TcpStream,
330    buf: &[u8],
331    header_end: usize,
332    tmp: &mut [u8],
333) -> Result<Vec<u8>, String> {
334    // Seed `raw` with any body bytes already buffered alongside the headers.
335    let mut raw: Vec<u8> = buf[header_end..].to_vec();
336    let mut decoded: Vec<u8> = Vec::new();
337
338    loop {
339        // Wait until we have at least one complete chunk-size line (ends with \r\n).
340        let crlf = loop {
341            if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
342                break p;
343            }
344            let n = stream.read(tmp).map_err(|e| e.to_string())?;
345            if n == 0 {
346                return Err("chunked: premature EOF reading chunk size".to_string());
347            }
348            raw.extend_from_slice(&tmp[..n]);
349        };
350
351        // Chunk size is hex, optionally followed by chunk-extensions (";…").
352        let size_line = std::str::from_utf8(&raw[..crlf])
353            .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
354        let size_str = size_line.split(';').next().unwrap_or("").trim();
355        let chunk_size = usize::from_str_radix(size_str, 16)
356            .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
357        raw.drain(..crlf + 2); // consume "<size>\r\n"
358
359        if chunk_size == 0 {
360            // Last chunk — consume the trailing CRLF ("0\r\n\r\n" → trailing "\r\n" still pending).
361            while raw.len() < 2 {
362                let n = stream.read(tmp).map_err(|e| e.to_string())?;
363                if n == 0 {
364                    break;
365                }
366                raw.extend_from_slice(&tmp[..n]);
367            }
368            break;
369        }
370
371        // Read chunk data + trailing CRLF.
372        while raw.len() < chunk_size + 2 {
373            let n = stream.read(tmp).map_err(|e| e.to_string())?;
374            if n == 0 {
375                return Err("chunked: premature EOF reading chunk body".to_string());
376            }
377            raw.extend_from_slice(&tmp[..n]);
378        }
379        decoded.extend_from_slice(&raw[..chunk_size]);
380        raw.drain(..chunk_size + 2); // consume "<data>\r\n"
381    }
382
383    Ok(decoded)
384}
385
386/// Rewrite `buf` in-place: strip `Transfer-Encoding`, add `Content-Length`,
387/// replace the old (undecoded) body with `decoded`.
388fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
389    let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
390    buf.clear();
391    for line in header_str.lines() {
392        if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
393            continue;
394        }
395        buf.extend_from_slice(line.as_bytes());
396        buf.extend_from_slice(b"\r\n");
397    }
398    let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
399    buf.extend_from_slice(decoded);
400}
401
402/// Non-pooled version of the response reader, used by callers that send
403/// `Connection: close` (e.g. `proxy_http1`, `proxy_https1`).
404pub(crate) fn read_response(stream: &mut TcpStream) -> Result<Vec<u8>, String> {
405    read_response_from(stream)
406}
407
408pub(crate) fn read_response_from<R: Read>(stream: &mut R) -> Result<Vec<u8>, String> {
409    let mut buf: Vec<u8> = Vec::with_capacity(8192);
410    let mut tmp = [0u8; 4096];
411
412    let header_end = loop {
413        let n = stream.read(&mut tmp).map_err(|e| e.to_string())?;
414        if n == 0 {
415            return if buf.is_empty() {
416                Err("backend closed connection without sending a response".to_string())
417            } else {
418                Ok(buf)
419            };
420        }
421        buf.extend_from_slice(&tmp[..n]);
422        if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
423            break pos + 4;
424        }
425    };
426
427    let content_length = std::str::from_utf8(&buf[..header_end])
428        .unwrap_or("")
429        .lines()
430        .find_map(|line| {
431            line.to_lowercase()
432                .starts_with("content-length:")
433                .then(|| line.splitn(2, ':').nth(1)?.trim().parse::<usize>().ok())
434                .flatten()
435        });
436
437    match content_length {
438        Some(len) => {
439            while buf.len() < header_end + len {
440                let n = stream.read(&mut tmp).map_err(|e| e.to_string())?;
441                if n == 0 {
442                    break;
443                }
444                buf.extend_from_slice(&tmp[..n]);
445            }
446        }
447        None => loop {
448            let n = stream.read(&mut tmp).map_err(|e| e.to_string())?;
449            if n == 0 {
450                break;
451            }
452            buf.extend_from_slice(&tmp[..n]);
453        },
454    }
455
456    Ok(buf)
457}
458
459/// Forward a single HTTP/1.1 request to `host:port` and return the response.
460///
461/// This is the shared low-level building block used by [`crate::canary`] and
462/// [`crate::ingress`] so they don't have to duplicate the TCP + request/response
463/// marshalling code.
464pub(crate) fn proxy_http1(
465    request: &Request,
466    client_ip: &str,
467    host: &str,
468    port: u16,
469    connect_timeout: Duration,
470    read_timeout: Duration,
471) -> Result<Response, String> {
472    use std::net::ToSocketAddrs;
473    let addr_str = format!("{}:{}", host, port);
474    let sock_addr = addr_str
475        .to_socket_addrs()
476        .map_err(|e| format!("DNS lookup for {} failed: {}", addr_str, e))?
477        .next()
478        .ok_or_else(|| format!("no address resolved for {}", addr_str))?;
479    let stream = TcpStream::connect_timeout(&sock_addr, connect_timeout)
480        .map_err(|e| format!("connect to {} failed: {}", addr_str, e))?;
481    stream.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
482    stream.set_write_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;
483    let req_bytes = build_request(request, host, client_ip, false);
484    let mut stream = stream;
485    stream.write_all(&req_bytes).map_err(|e| format!("write to backend failed: {}", e))?;
486    let resp_bytes = read_response(&mut stream)?;
487    Response::parse(&resp_bytes).map(normalize_upstream_response)
488}
489
490/// Forward a single HTTPS/1.1 request to `host:port` over TLS and return the
491/// response. Requires the `http-client` or `http2` feature (both bring in
492/// `rustls` + `webpki-roots`).
493#[cfg(any(feature = "http-client", feature = "http2"))]
494pub(crate) fn proxy_https1(
495    request: &Request,
496    client_ip: &str,
497    host: &str,
498    port: u16,
499    connect_timeout: Duration,
500    read_timeout: Duration,
501) -> Result<Response, String> {
502    use rustls::pki_types::ServerName;
503    use rustls::ClientConfig;
504    use std::net::ToSocketAddrs;
505    use std::sync::Arc;
506
507    let addr_str = format!("{}:{}", host, port);
508    let sock_addr = addr_str
509        .to_socket_addrs()
510        .map_err(|e| format!("DNS lookup for {} failed: {}", addr_str, e))?
511        .next()
512        .ok_or_else(|| format!("no address resolved for {}", addr_str))?;
513
514    let stream = TcpStream::connect_timeout(&sock_addr, connect_timeout)
515        .map_err(|e| format!("connect to {} failed: {}", addr_str, e))?;
516    stream.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
517    stream.set_write_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;
518
519    let root_store =
520        rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
521    let config = Arc::new(
522        ClientConfig::builder()
523            .with_root_certificates(root_store)
524            .with_no_client_auth(),
525    );
526    let server_name = ServerName::try_from(host.to_string())
527        .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
528    let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
529    let mut tls = rustls::StreamOwned::new(conn, stream);
530
531    let req_bytes = build_request(request, host, client_ip, false);
532    tls.write_all(&req_bytes)
533        .map_err(|e| format!("write to upstream failed: {}", e))?;
534
535    let resp_bytes = read_response_from(&mut tls)?;
536    Response::parse(&resp_bytes).map(normalize_upstream_response)
537}
538
539/// Headers rws regenerates for a buffered body, plus hop-by-hop headers
540/// (RFC 9110 §7.6.1) that must not be forwarded.
541const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
542    "content-length", "transfer-encoding", "connection", "keep-alive",
543    "proxy-connection", "te", "trailer", "upgrade",
544];
545
546/// Make a buffered upstream response safe to re-serialize. The body is
547/// already held in `content_range_list`, from which `Response::generate`
548/// (and the HTTP/2 / HTTP/3 writers) emit `Content-Type` and
549/// `Content-Length` themselves, so the upstream's own copies would be
550/// duplicated — and a duplicate `content-length` is a protocol error that
551/// HTTP/2 clients reject outright. The upstream's `Content-Type` is moved
552/// onto the body instead; the status line is re-emitted as HTTP/1.1
553/// regardless of the backend's version.
554pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
555    response.http_version = crate::http::VERSION.http_1_1.to_string();
556    let content_type = response
557        .headers
558        .iter()
559        .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
560        .map(|h| h.value.clone());
561    let has_body = !response.content_range_list.is_empty();
562    if let (Some(content_type), [range]) = (&content_type, response.content_range_list.as_mut_slice()) {
563        range.content_type = content_type.clone();
564    }
565    response.headers.retain(|h| {
566        let name = h.name.to_ascii_lowercase();
567        if name == "content-type" {
568            // Keep it only when there's no body to carry it (e.g. HEAD, 304).
569            return !has_body;
570        }
571        !REGENERATED_OR_HOP_BY_HOP.contains(&name.as_str())
572    });
573    response
574}
575
576fn bad_gateway() -> Response {
577    let cr = Range::get_content_range(
578        b"502 Bad Gateway".to_vec(),
579        MimeType::TEXT_PLAIN.to_string(),
580    );
581    let mut r = Response::new();
582    r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
583    r.reason_phrase = STATUS_CODE_REASON_PHRASE
584        .n502_bad_gateway
585        .reason_phrase
586        .to_string();
587    r.content_range_list = vec![cr];
588    r
589}
590
591// ── Streaming proxy helpers ───────────────────────────────────────────────────
592
593/// Responses larger than this threshold are streamed instead of buffered.
594const STREAM_THRESHOLD: usize = 1024 * 1024; // 1 MB
595
596/// A `Read` implementation that drains a prefix buffer before reading from the
597/// inner stream. Used to replay body bytes that arrived with the HTTP headers.
598pub(crate) struct ConcatReader<R: Read + Send> {
599    prefix: Vec<u8>,
600    prefix_pos: usize,
601    inner: R,
602}
603
604impl<R: Read + Send> ConcatReader<R> {
605    fn new(prefix: Vec<u8>, inner: R) -> Self {
606        ConcatReader { prefix, prefix_pos: 0, inner }
607    }
608}
609
610impl<R: Read + Send> Read for ConcatReader<R> {
611    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
612        if self.prefix_pos < self.prefix.len() {
613            let avail = &self.prefix[self.prefix_pos..];
614            let n = buf.len().min(avail.len());
615            buf[..n].copy_from_slice(&avail[..n]);
616            self.prefix_pos += n;
617            return Ok(n);
618        }
619        self.inner.read(buf)
620    }
621}
622
623/// Read exactly the HTTP response headers (up to and including `\r\n\r\n`).
624///
625/// Returns `(header_bytes, body_prefix)` where `body_prefix` contains any
626/// body bytes that arrived in the same TCP segment as the headers.
627fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
628    let mut buf: Vec<u8> = Vec::with_capacity(4096);
629    loop {
630        let n = stream.read(tmp).map_err(|e| e.to_string())?;
631        if n == 0 {
632            return Err("backend closed connection before headers were complete".to_string());
633        }
634        buf.extend_from_slice(&tmp[..n]);
635        if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
636            let body_prefix = buf[pos + 4..].to_vec();
637            buf.truncate(pos + 4);
638            return Ok((buf, body_prefix));
639        }
640    }
641}
642
643/// Returns `true` when the response should be streamed rather than buffered.
644///
645/// Streams when any of the following hold:
646/// - `Content-Type: text/event-stream` — SSE
647/// - `Transfer-Encoding: chunked` — AI token streams, etc.
648/// - `Content-Length` exceeds 1 MB — large file downloads
649pub(crate) fn should_stream_response(header_lower: &str) -> bool {
650    let is_sse = header_lower.lines().any(|l| {
651        l.starts_with("content-type:") && l.contains("text/event-stream")
652    });
653    let is_chunked = header_lower.lines().any(|l| {
654        l.starts_with("transfer-encoding:") && l.contains("chunked")
655    });
656    let content_length: Option<usize> = header_lower.lines().find_map(|l| {
657        l.strip_prefix("content-length:")?.trim().parse().ok()
658    });
659    let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
660    is_sse || is_chunked || is_large
661}
662
663/// Parse the status line and headers from raw header bytes (ending at `\r\n\r\n`).
664fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
665    let s = std::str::from_utf8(header_bytes)
666        .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
667    let mut lines = s.lines();
668    let status_line = lines.next().ok_or("empty backend response")?;
669    let mut parts = status_line.splitn(3, ' ');
670    let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
671    let status_code: i16 = parts
672        .next()
673        .unwrap_or("502")
674        .parse()
675        .map_err(|_| format!("invalid status code in '{}'", status_line))?;
676    let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
677    let mut headers = Vec::new();
678    for line in lines {
679        let line = line.trim_end_matches('\r');
680        if line.is_empty() { break; }
681        if let Some(colon) = line.find(':') {
682            headers.push(crate::header::Header {
683                name: line[..colon].trim().to_string(),
684                value: line[colon + 1..].trim().to_string(),
685            });
686        }
687    }
688    Ok(Response {
689        http_version,
690        status_code,
691        reason_phrase,
692        headers,
693        content_range_list: vec![],
694        stream_file: None,
695        stream_pipe: None,
696    })
697}
698
699/// Read the remaining body after headers have already been read.
700///
701/// `header_bytes` ends with `\r\n\r\n`; `body_prefix` holds any body bytes
702/// that arrived in the same TCP read.  Handles all three body mechanisms
703/// (chunked, content-length, read-to-EOF).  Returns `(full_response_bytes, can_reuse)`.
704fn read_response_from_partial(
705    stream: &mut TcpStream,
706    header_bytes: Vec<u8>,
707    body_prefix: Vec<u8>,
708    tmp: &mut [u8],
709) -> Result<(Vec<u8>, bool), String> {
710    let header_end = header_bytes.len();
711    let mut buf = header_bytes;
712    buf.extend_from_slice(&body_prefix);
713
714    let header_lower =
715        std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
716    let connection_close =
717        header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
718    let is_chunked = header_lower
719        .lines()
720        .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
721    let content_length: Option<usize> = header_lower.lines().find_map(|l| {
722        l.strip_prefix("content-length:")?.trim().parse().ok()
723    });
724
725    if is_chunked {
726        let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
727        rewrite_as_content_length(&mut buf, header_end, &decoded);
728        Ok((buf, !connection_close))
729    } else if let Some(len) = content_length {
730        while buf.len() < header_end + len {
731            let n = stream.read(tmp).map_err(|e| e.to_string())?;
732            if n == 0 { break; }
733            buf.extend_from_slice(&tmp[..n]);
734        }
735        Ok((buf, !connection_close))
736    } else {
737        loop {
738            let n = stream.read(tmp).map_err(|e| e.to_string())?;
739            if n == 0 { break; }
740            buf.extend_from_slice(&tmp[..n]);
741        }
742        Ok((buf, false))
743    }
744}
745
746// ── Backend URL parsing ───────────────────────────────────────────────────────
747
748struct Backend {
749    host: String,
750    port: u16,
751    /// Whether the upstream connection should use TLS.
752    /// Set when the URL scheme is `https://`, `h2s://`, or `grpcs://`.
753    #[cfg_attr(not(feature = "http2"), allow(dead_code))]
754    tls: bool,
755}
756
757impl Backend {
758    fn parse(url: &str) -> Option<Self> {
759        let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
760            (r, true, 443u16)
761        } else if let Some(r) = url.strip_prefix("h2s://") {
762            (r, true, 443u16)
763        } else if let Some(r) = url.strip_prefix("grpcs://") {
764            (r, true, 443u16)
765        } else if let Some(r) = url.strip_prefix("http://") {
766            (r, false, 80u16)
767        } else if let Some(r) = url.strip_prefix("h2://") {
768            (r, false, 80u16)
769        } else if let Some(r) = url.strip_prefix("grpc://") {
770            (r, false, 80u16)
771        } else {
772            (url, false, 80u16)
773        };
774        // Drop any path component.
775        let host_port = rest.split('/').next().unwrap_or(rest);
776        let (host, port) = if let Some(colon) = host_port.rfind(':') {
777            let port_str = &host_port[colon + 1..];
778            if let Ok(p) = port_str.parse::<u16>() {
779                (host_port[..colon].to_string(), p)
780            } else {
781                (host_port.to_string(), default_port)
782            }
783        } else {
784            (host_port.to_string(), default_port)
785        };
786        if host.is_empty() {
787            return None;
788        }
789        Some(Backend { host, port, tls })
790    }
791}
792
793// ── HTTP/2 reverse proxy ──────────────────────────────────────────────────────
794
795/// Reverse proxy that forwards requests to HTTP/2 backends.
796///
797/// Wraps [`ReverseProxy`] and forces HTTP/2 (`h2`) for all upstream connections.
798/// Requires the `http2` Cargo feature.
799///
800/// This proxy also transparently handles gRPC traffic
801/// (`Content-Type: application/grpc*`) — gRPC DATA frames are forwarded
802/// as-is because gRPC is layered directly on HTTP/2.
803#[cfg(feature = "http2")]
804pub struct H2ReverseProxy {
805    inner: ReverseProxy,
806}
807
808#[cfg(feature = "http2")]
809impl H2ReverseProxy {
810    /// Create a proxy distributing requests across `backends` in round-robin order.
811    ///
812    /// Each backend entry can be:
813    /// - `"host:port"` — plain TCP (HTTP/2 cleartext)
814    /// - `"h2://host:port"` — plain TCP (explicit scheme)
815    /// - `"h2s://host:port"` — TLS (HTTP/2 over HTTPS; port defaults to 443)
816    /// - `"https://host:port"` — TLS (same as `h2s://`)
817    ///
818    /// TLS backends require the `http2` Cargo feature (includes `rustls` +
819    /// `webpki-roots`).  Certificate verification uses the WebPKI trust store.
820    pub fn new<I, S>(backends: I) -> Self
821    where
822        I: IntoIterator<Item = S>,
823        S: AsRef<str>,
824    {
825        H2ReverseProxy {
826            inner: ReverseProxy::new(backends),
827        }
828    }
829
830    /// Only proxy requests whose URI starts with `prefix`; pass others through.
831    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
832        self.inner = self.inner.path_prefix(prefix);
833        self
834    }
835
836    /// Override the TCP connect timeout (default: 5 s).
837    pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
838        self.inner = self.inner.connect_timeout_ms(ms);
839        self
840    }
841
842    /// Override the response read timeout (default: 30 s).
843    pub fn read_timeout_ms(mut self, ms: u64) -> Self {
844        self.inner = self.inner.read_timeout_ms(ms);
845        self
846    }
847}
848
849#[cfg(feature = "http2")]
850impl crate::middleware::Middleware for H2ReverseProxy {
851    fn handle(
852        &self,
853        request: &crate::request::Request,
854        connection: &crate::server::ConnectionInfo,
855        next: &dyn crate::application::Application,
856    ) -> Result<crate::response::Response, String> {
857        if let Some(prefix) = &self.inner.path_prefix {
858            if !request.request_uri.starts_with(prefix.as_str()) {
859                return next.execute(request, connection);
860            }
861        }
862        if self.inner.backends.is_empty() {
863            return Ok(bad_gateway());
864        }
865        let n = self.inner.backends.len();
866        let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
867        for attempt in 0..n {
868            let idx = (start + attempt) % n;
869            match try_backend_h2(request, &connection.client.ip, &self.inner.backends[idx],
870                                  self.inner.connect_timeout, self.inner.read_timeout) {
871                Ok(resp) => return Ok(resp),
872                Err(_) if attempt + 1 < n => continue,
873                Err(_) => break,
874            }
875        }
876        Ok(bad_gateway())
877    }
878}
879
880#[cfg(feature = "http2")]
881fn try_backend_h2(
882    request: &Request,
883    client_ip: &str,
884    backend: &Backend,
885    connect_timeout: Duration,
886    _read_timeout: Duration,
887) -> Result<Response, String> {
888    // `block_on_isolated` works on any tokio runtime flavor (including
889    // `current_thread`, unlike `tokio::task::block_in_place`, which requires
890    // `multi_thread` and panics otherwise) and doesn't pull a worker thread
891    // out of a shared pool; it also works with no runtime at all (HTTP/1.1
892    // thread pool), unlike the previous implementation, which just gave up.
893    crate::async_bridge::block_on_isolated(|| forward_h2_async(request, client_ip, backend, connect_timeout))
894}
895
896#[cfg(feature = "http2")]
897async fn forward_h2_async(
898    request: &Request,
899    client_ip: &str,
900    backend: &Backend,
901    connect_timeout: Duration,
902) -> Result<Response, String> {
903    let addr = format!("{}:{}", backend.host, backend.port);
904    let tcp = tokio::time::timeout(
905        connect_timeout,
906        tokio::net::TcpStream::connect(&addr),
907    )
908    .await
909    .map_err(|_| format!("h2 proxy: connect to {} timed out", addr))?
910    .map_err(|e| format!("h2 proxy: connect to {} failed: {}", addr, e))?;
911
912    if backend.tls {
913        use rustls::pki_types::ServerName;
914        use rustls::ClientConfig;
915        use std::sync::Arc;
916        use tokio_rustls::TlsConnector;
917
918        let root_store = rustls::RootCertStore::from_iter(
919            webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
920        );
921        let mut config = ClientConfig::builder()
922            .with_root_certificates(root_store)
923            .with_no_client_auth();
924        // Advertise h2 via ALPN so the server selects HTTP/2.
925        config.alpn_protocols = vec![b"h2".to_vec()];
926        let connector = TlsConnector::from(Arc::new(config));
927        let server_name = ServerName::try_from(backend.host.as_str())
928            .map_err(|e| format!("invalid upstream hostname '{}': {}", backend.host, e))?
929            .to_owned();
930        let tls_stream = connector
931            .connect(server_name, tcp)
932            .await
933            .map_err(|e| format!("h2 proxy: TLS handshake with {} failed: {}", addr, e))?;
934        send_h2_request(request, client_ip, backend, tls_stream).await
935    } else {
936        send_h2_request(request, client_ip, backend, tcp).await
937    }
938}
939
940/// Drive the h2 client handshake + request/response over any async I/O stream.
941///
942/// Accepts both plain `TcpStream` and `TlsStream<TcpStream>` — anything that
943/// satisfies `AsyncRead + AsyncWrite + Unpin + Send + 'static`.
944#[cfg(feature = "http2")]
945async fn send_h2_request<T>(
946    request: &Request,
947    client_ip: &str,
948    backend: &Backend,
949    stream: T,
950) -> Result<Response, String>
951where
952    T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
953{
954    use bytes::Bytes;
955    use http as hc;
956
957    let addr = format!("{}:{}", backend.host, backend.port);
958
959    let (send_req, conn) = h2::client::handshake(stream)
960        .await
961        .map_err(|e| format!("h2 proxy: handshake with {} failed: {}", addr, e))?;
962
963    tokio::spawn(async move {
964        let _ = conn.await;
965    });
966
967    let scheme = if backend.tls { "https" } else { "http" };
968    let uri_str = format!("{}://{}{}", scheme, addr, request.request_uri);
969    let uri: hc::Uri = uri_str.parse().map_err(|e: hc::uri::InvalidUri| e.to_string())?;
970    let method = hc::Method::from_bytes(request.method.as_bytes()).map_err(|e| e.to_string())?;
971
972    let mut builder = hc::Request::builder().method(method).uri(uri);
973    builder = builder.header("host", &backend.host);
974    for h in &request.headers {
975        let lower = h.name.to_lowercase();
976        if HOP_BY_HOP.contains(&lower.as_str()) || lower == "host" {
977            continue;
978        }
979        builder = builder.header(&h.name, &h.value);
980    }
981    builder = builder.header("x-forwarded-for", client_ip);
982    builder = builder.header("via", "2 rws");
983
984    let body_bytes = Bytes::from(request.body.clone());
985    let end_of_stream = body_bytes.is_empty();
986    let http_req = builder.body(()).map_err(|e| e.to_string())?;
987
988    let mut send_req = send_req.ready().await.map_err(|e| e.to_string())?;
989    let (resp_future, mut req_body) = send_req
990        .send_request(http_req, end_of_stream)
991        .map_err(|e| e.to_string())?;
992    if !end_of_stream {
993        req_body.send_data(body_bytes, true).map_err(|e| e.to_string())?;
994    }
995
996    let resp = resp_future.await.map_err(|e| e.to_string())?;
997    let (parts, mut body) = resp.into_parts();
998
999    let content_type = parts
1000        .headers
1001        .get("content-type")
1002        .and_then(|v| v.to_str().ok())
1003        .unwrap_or("application/octet-stream")
1004        .to_string();
1005
1006    let mut body_bytes: Vec<u8> = Vec::new();
1007    while let Some(chunk) = body.data().await {
1008        body_bytes.extend_from_slice(&chunk.map_err(|e| e.to_string())?);
1009    }
1010
1011    let mut response = Response::new();
1012    response.status_code = parts.status.as_u16() as i16;
1013    response.reason_phrase = parts.status.canonical_reason().unwrap_or("").to_string();
1014
1015    const H2_HOP: &[&str] = &[
1016        "connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-connection", "te",
1017    ];
1018    for (name, value) in &parts.headers {
1019        let lower = name.as_str().to_lowercase();
1020        if H2_HOP.contains(&lower.as_str()) {
1021            continue;
1022        }
1023        if let Ok(v) = value.to_str() {
1024            response.headers.push(crate::header::Header {
1025                name: name.as_str().to_string(),
1026                value: v.to_string(),
1027            });
1028        }
1029    }
1030
1031    if !body_bytes.is_empty() {
1032        response.content_range_list = vec![Range::get_content_range(body_bytes, content_type)];
1033    }
1034
1035    Ok(normalize_upstream_response(response))
1036}
1037
1038// ── gRPC proxy ────────────────────────────────────────────────────────────────
1039
1040/// gRPC reverse proxy middleware.
1041///
1042/// Recognises requests with `Content-Type: application/grpc*` and forwards them
1043/// to a backend over HTTP/2, leaving all other requests to the next layer.
1044///
1045/// Requires the `http2` Cargo feature.
1046///
1047/// # Example
1048///
1049/// ```rust,no_run
1050/// use rust_web_server::app::App;
1051/// use rust_web_server::core::New;
1052/// use rust_web_server::proxy::GrpcProxy;
1053///
1054/// let app = App::new()
1055///     .wrap(GrpcProxy::new(["grpc-service:50051"]));
1056/// ```
1057#[cfg(feature = "http2")]
1058pub struct GrpcProxy {
1059    inner: H2ReverseProxy,
1060}
1061
1062#[cfg(feature = "http2")]
1063impl GrpcProxy {
1064    /// Create a proxy distributing gRPC connections across `backends` in round-robin order.
1065    ///
1066    /// Each backend entry can be:
1067    /// - `"host:port"` — plain TCP (gRPC cleartext)
1068    /// - `"grpc://host:port"` — plain TCP (explicit scheme)
1069    /// - `"grpcs://host:port"` — TLS (gRPC over TLS; port defaults to 443)
1070    /// - `"https://host:port"` — TLS (same as `grpcs://`)
1071    ///
1072    /// TLS backends require the `http2` Cargo feature.
1073    pub fn new<I, S>(backends: I) -> Self
1074    where
1075        I: IntoIterator<Item = S>,
1076        S: AsRef<str>,
1077    {
1078        GrpcProxy { inner: H2ReverseProxy::new(backends) }
1079    }
1080
1081    /// Only proxy requests whose URI starts with `prefix`; pass others through.
1082    pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1083        self.inner = self.inner.path_prefix(prefix);
1084        self
1085    }
1086}
1087
1088#[cfg(feature = "http2")]
1089impl crate::middleware::Middleware for GrpcProxy {
1090    fn handle(
1091        &self,
1092        request: &crate::request::Request,
1093        connection: &crate::server::ConnectionInfo,
1094        next: &dyn crate::application::Application,
1095    ) -> Result<crate::response::Response, String> {
1096        let ct = request
1097            .get_header("content-type".to_string())
1098            .map(|h| h.value.as_str())
1099            .unwrap_or("");
1100        if ct.starts_with("application/grpc") {
1101            self.inner.handle(request, connection, next)
1102        } else {
1103            next.execute(request, connection)
1104        }
1105    }
1106}
1107
1108// ── Backend::parse unit tests ─────────────────────────────────────────────────
1109
1110#[cfg(test)]
1111mod backend_parse_tests {
1112    use super::Backend;
1113
1114    fn parse(url: &str) -> Option<(String, u16, bool)> {
1115        Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1116    }
1117
1118    #[test]
1119    fn bare_host_port() {
1120        assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1121    }
1122
1123    #[test]
1124    fn http_scheme() {
1125        assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1126    }
1127
1128    #[test]
1129    fn h2_scheme_plain() {
1130        assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1131    }
1132
1133    #[test]
1134    fn grpc_scheme_plain() {
1135        assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1136    }
1137
1138    #[test]
1139    fn https_scheme_sets_tls_and_default_port() {
1140        assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1141    }
1142
1143    #[test]
1144    fn https_scheme_explicit_port() {
1145        assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1146    }
1147
1148    #[test]
1149    fn h2s_scheme_sets_tls() {
1150        assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1151    }
1152
1153    #[test]
1154    fn h2s_scheme_explicit_port() {
1155        assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1156    }
1157
1158    #[test]
1159    fn grpcs_scheme_sets_tls() {
1160        assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1161    }
1162
1163    #[test]
1164    fn grpcs_scheme_explicit_port() {
1165        assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1166    }
1167
1168    #[test]
1169    fn empty_host_returns_none() {
1170        assert_eq!(None, parse("https://"));
1171    }
1172
1173    #[test]
1174    fn bare_host_no_port_defaults_to_80() {
1175        assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1176    }
1177}