1pub 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
59const 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
74pub enum LoadBalancing {
76 RoundRobin,
78}
79
80pub 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 tls_ca: Option<String>,
105 tls_client: Option<(String, String)>,
106}
107
108impl ReverseProxy {
109 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 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 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
161 self.path_prefix = Some(prefix.into());
162 self
163 }
164
165 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
167 self
168 }
169
170 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
172 self.connect_timeout = Duration::from_millis(ms);
173 self
174 }
175
176 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
178 self.read_timeout = Duration::from_millis(ms);
179 self
180 }
181
182 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
187 self.pool = pool;
188 self
189 }
190
191 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 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 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#[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 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
329pub(crate) fn build_request(
330 request: &Request,
331 backend_host: &str,
332 client: Peer<'_>,
333 keep_alive: bool,
334) -> Vec<u8> {
335 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
341 #[cfg(not(target_arch = "wasm32"))]
344 let trace = crate::otel::propagation_headers();
345 #[cfg(target_arch = "wasm32")]
346 let trace: Option<(String, Option<String>)> = None;
347 let forwarded = |h: &&crate::header::Header| {
348 !h.name.is_empty()
350 && !is_hop_by_hop_or_host(&h.name)
351 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
352 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
353 && !connection.is_some_and(|c| connection_listed(c, &h.name))
354 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
355 };
356 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
357 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
361 let forwarded_host = match request.header("X-Forwarded-Host") {
362 Some(_) => None,
363 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
364 };
365 let body_len = Decimal::new(request.body.len() as u128);
366 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
367 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
368 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
369 + forwarded_host.map_or(0, |v| v.len() + 20)
370 + forwarded_proto.map_or(0, |v| v.len() + 21)
371 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
372 + 13
373 + 24
374 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
375 + 2
376 + request.body.len();
377 let mut out: Vec<u8> = Vec::with_capacity(capacity);
378 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
379 out.extend_from_slice(part.as_bytes());
380 }
381 for h in request.headers.iter().filter(forwarded) {
382 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
383 out.extend_from_slice(part.as_bytes());
384 }
385 }
386 out.extend_from_slice(b"X-Forwarded-For: ");
387 if let Some(prior) = prior_xff {
388 out.extend_from_slice(prior.as_bytes());
389 out.extend_from_slice(b", ");
390 }
391 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
392 out.extend_from_slice(part.as_bytes());
393 }
394 if let Some(host) = forwarded_host {
395 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
396 out.extend_from_slice(part.as_bytes());
397 }
398 }
399 if let Some(proto) = forwarded_proto {
400 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
401 out.extend_from_slice(part.as_bytes());
402 }
403 }
404 if let Some((traceparent, baggage)) = &trace {
405 for part in ["traceparent: ", traceparent, "\r\n"] {
406 out.extend_from_slice(part.as_bytes());
407 }
408 if let Some(baggage) = baggage {
409 for part in ["baggage: ", baggage, "\r\n"] {
410 out.extend_from_slice(part.as_bytes());
411 }
412 }
413 }
414 if keep_alive {
415 out.extend_from_slice(b"Connection: keep-alive\r\n");
416 } else {
417 out.extend_from_slice(b"Connection: close\r\n");
418 }
419 if !request.body.is_empty() {
420 out.extend_from_slice(b"Content-Length: ");
421 out.extend_from_slice(body_len.as_str().as_bytes());
422 out.extend_from_slice(b"\r\n");
423 }
424 out.extend_from_slice(b"\r\n");
425 out.extend_from_slice(&request.body);
426 out
427}
428
429fn is_hop_by_hop_or_host(name: &str) -> bool {
434 if name.is_ascii() {
435 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
436 } else {
437 let lower = name.to_lowercase();
438 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
439 }
440}
441
442fn connection_listed(connection: &str, name: &str) -> bool {
445 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
446}
447
448fn decode_chunked<S: Read>(
453 stream: &mut S,
454 buf: &[u8],
455 header_end: usize,
456 tmp: &mut [u8],
457) -> Result<Vec<u8>, String> {
458 let mut raw: Vec<u8> = buf[header_end..].to_vec();
460 let mut decoded: Vec<u8> = Vec::new();
461
462 loop {
463 let crlf = loop {
465 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
466 break p;
467 }
468 let n = stream.read(tmp).map_err(|e| e.to_string())?;
469 if n == 0 {
470 return Err("chunked: premature EOF reading chunk size".to_string());
471 }
472 raw.extend_from_slice(&tmp[..n]);
473 };
474
475 let size_line = std::str::from_utf8(&raw[..crlf])
477 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
478 let size_str = size_line.split(';').next().unwrap_or("").trim();
479 let chunk_size = usize::from_str_radix(size_str, 16)
480 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
481 raw.drain(..crlf + 2); if chunk_size == 0 {
484 loop {
488 let line_end = loop {
489 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
490 break p;
491 }
492 let n = stream.read(tmp).map_err(|e| e.to_string())?;
493 if n == 0 {
494 return Err("chunked: premature EOF in trailer".to_string());
495 }
496 raw.extend_from_slice(&tmp[..n]);
497 };
498 raw.drain(..line_end + 2);
499 if line_end == 0 {
500 break;
501 }
502 }
503 break;
504 }
505
506 while raw.len() < chunk_size + 2 {
508 let n = stream.read(tmp).map_err(|e| e.to_string())?;
509 if n == 0 {
510 return Err("chunked: premature EOF reading chunk body".to_string());
511 }
512 raw.extend_from_slice(&tmp[..n]);
513 }
514 decoded.extend_from_slice(&raw[..chunk_size]);
515 raw.drain(..chunk_size + 2); }
517
518 Ok(decoded)
519}
520
521fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
524 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
525 buf.clear();
526 for line in header_str.lines() {
527 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
528 continue;
529 }
530 buf.extend_from_slice(line.as_bytes());
531 buf.extend_from_slice(b"\r\n");
532 }
533 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
534 buf.extend_from_slice(decoded);
535}
536
537pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
540 "content-length", "transfer-encoding", "connection", "keep-alive",
541 "proxy-connection", "te", "trailer", "upgrade",
542];
543
544#[cfg_attr(not(feature = "http2"), allow(dead_code))]
553pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
554 response.http_version.clear();
555 response.http_version.push_str(crate::http::VERSION.http_1_1);
556 let has_body = !response.content_range_list.is_empty();
557 if let [range] = response.content_range_list.as_mut_slice() {
558 if let Some(h) = response
559 .headers
560 .iter()
561 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
562 {
563 range.content_type.clear();
564 range.content_type.push_str(&h.value);
565 }
566 }
567 response.headers.retain(|h| {
570 if h.name.eq_ignore_ascii_case("content-type") {
571 return !has_body;
573 }
574 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
575 });
576 response
577}
578
579pub(crate) fn bad_gateway() -> Response {
580 crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
581}
582
583const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
591 prefix: Vec<u8>,
592 prefix_pos: usize,
593 inner: R,
594}
595
596impl<R: Read + Send> ConcatReader<R> {
597 fn new(prefix: Vec<u8>, inner: R) -> Self {
598 ConcatReader { prefix, prefix_pos: 0, inner }
599 }
600}
601
602impl<R: Read + Send> Read for ConcatReader<R> {
603 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
604 if self.prefix_pos < self.prefix.len() {
605 let avail = &self.prefix[self.prefix_pos..];
606 let n = buf.len().min(avail.len());
607 buf[..n].copy_from_slice(&avail[..n]);
608 self.prefix_pos += n;
609 return Ok(n);
610 }
611 self.inner.read(buf)
612 }
613}
614
615
616pub(crate) const POOL_MAX_IDLE: usize = 64;
620
621pub(crate) fn shared_pool() -> &'static ConnPool {
624 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
625 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
626}
627
628pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
631 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
632}
633
634impl UpstreamIo for TcpStream {
635 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
636 self.set_read_timeout(Some(read))?;
637 self.set_write_timeout(Some(write))
638 }
639}
640
641#[cfg(any(feature = "http-client", feature = "http2"))]
642pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
643
644#[cfg(any(feature = "http-client", feature = "http2"))]
645impl UpstreamIo for TlsStream {
646 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
647 self.sock.set_io_timeouts(read, write)
648 }
649}
650
651pub(crate) fn forward_upstream(
657 plain_pool: &ConnPool,
658 request: &Request,
659 client: Peer<'_>,
660 host: &str,
661 port: u16,
662 tls: bool,
663 connect_timeout: Duration,
664 read_timeout: Duration,
665) -> Result<Response, String> {
666 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
667}
668
669#[allow(clippy::too_many_arguments)]
672pub(crate) fn forward_upstream_trusting(
673 plain_pool: &ConnPool,
674 request: &Request,
675 client: Peer<'_>,
676 host: &str,
677 port: u16,
678 tls: bool,
679 trust: Option<&UpstreamTls>,
680 connect_timeout: Duration,
681 read_timeout: Duration,
682) -> Result<Response, String> {
683 forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
684}
685
686#[allow(clippy::too_many_arguments)]
690pub(crate) fn forward_upstream_as(
691 plain_pool: &ConnPool,
692 request: &Request,
693 client: Peer<'_>,
694 host: &str,
695 port: u16,
696 host_header: &str,
697 tls: bool,
698 trust: Option<&UpstreamTls>,
699 connect_timeout: Duration,
700 read_timeout: Duration,
701) -> Result<Response, String> {
702 #[cfg(not(target_arch = "wasm32"))]
705 let span = crate::otel::outbound_span(&request.method);
706 #[cfg(not(target_arch = "wasm32"))]
707 if let Some(span) = &span {
708 let scheme = if tls { "https" } else { "http" };
709 let path = request.request_uri.as_str();
710 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
711 span.set_attribute(key, value);
712 }
713 }
714 let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
715 #[cfg(not(target_arch = "wasm32"))]
716 if let Some(span) = &span {
717 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
718 }
719 result
720}
721
722#[allow(clippy::too_many_arguments)]
723fn forward_upstream_untraced(
724 plain_pool: &ConnPool,
725 request: &Request,
726 client: Peer<'_>,
727 host: &str,
728 port: u16,
729 host_header: &str,
730 tls: bool,
731 trust: Option<&UpstreamTls>,
732 connect_timeout: Duration,
733 read_timeout: Duration,
734) -> Result<Response, String> {
735 if tls {
736 #[cfg(any(feature = "http-client", feature = "http2"))]
737 {
738 let (pool, config) = match trust {
739 Some(t) => (t.pool, Arc::clone(&t.config)),
740 None => (shared_tls_pool(), tls_client_config()),
741 };
742 return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
743 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
744 });
745 }
746 #[cfg(not(any(feature = "http-client", feature = "http2")))]
747 {
748 let _ = trust;
749 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
750 }
751 }
752 forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
753}
754
755pub struct UpstreamTls {
763 #[cfg(any(feature = "http-client", feature = "http2"))]
764 config: Arc<rustls::ClientConfig>,
765 #[cfg(any(feature = "http-client", feature = "http2"))]
766 pool: &'static Pool<TlsStream>,
767 #[cfg(feature = "http2")]
769 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
770 #[cfg(feature = "http2")]
772 h2_config: Arc<rustls::ClientConfig>,
773}
774
775impl UpstreamTls {
776 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
778 UpstreamTls::new(Some(path), None)
779 }
780
781 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
785 #[cfg(any(feature = "http-client", feature = "http2"))]
786 {
787 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
788 let roots = match ca_file {
789 Some(path) => {
790 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
791 let certs = CertificateDer::pem_slice_iter(&bytes)
792 .collect::<Result<Vec<_>, _>>()
793 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
794 if certs.is_empty() {
795 return Err(format!("no certificates in CA file '{path}'"));
796 }
797 let mut roots = rustls::RootCertStore::empty();
798 for cert in certs {
799 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
800 }
801 roots
802 }
803 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
804 };
805 let builder = crate::tls_client_builder().with_root_certificates(roots);
806 let config = match client_cert {
807 None => builder.with_no_client_auth(),
808 Some((cert_path, key_path)) => {
809 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
810 let chain = CertificateDer::pem_slice_iter(&bytes)
811 .collect::<Result<Vec<_>, _>>()
812 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
813 if chain.is_empty() {
814 return Err(format!("no certificate in '{cert_path}'"));
815 }
816 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
817 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
818 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
819 }
820 };
821 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
822 Ok(UpstreamTls::with_config(config, &label))
823 }
824 #[cfg(not(any(feature = "http-client", feature = "http2")))]
825 {
826 let _ = client_cert;
827 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
828 }
829 }
830
831 #[cfg(any(feature = "http-client", feature = "http2"))]
832 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
833 #[cfg(feature = "http2")]
834 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
835 UpstreamTls {
836 config: Arc::new(config),
837 pool: tls_pool_for_ca(label),
838 #[cfg(feature = "http2")]
839 async_pool: async_tls_pool_for_ca(label),
840 #[cfg(feature = "http2")]
841 h2_config,
842 }
843 }
844
845 #[cfg(feature = "http2")]
847 pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
848 &self.h2_config
849 }
850
851 pub(crate) fn reject_all(label: &str) -> UpstreamTls {
855 #[cfg(any(feature = "http-client", feature = "http2"))]
856 {
857 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
858 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
859 }
860 #[cfg(not(any(feature = "http-client", feature = "http2")))]
861 {
862 let _ = label;
863 UpstreamTls {}
864 }
865 }
866
867 #[cfg(feature = "http2")]
869 pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
870 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
871 }
872
873 #[cfg(any(feature = "http-client", feature = "http2"))]
875 pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
876 Arc::clone(&self.config)
877 }
878}
879
880#[cfg(feature = "http2")]
883fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
884 static POOLS: std::sync::OnceLock<
885 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
886 > = std::sync::OnceLock::new();
887 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
888 pools
889 .entry(path.to_string())
890 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
891}
892
893#[cfg(any(feature = "http-client", feature = "http2"))]
895fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
896 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
897 std::sync::OnceLock::new();
898 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
899 pools
900 .entry(path.to_string())
901 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
902}
903
904pub(crate) fn forward_pooled(
908 pool: &ConnPool,
909 request: &Request,
910 client: Peer<'_>,
911 host: &str,
912 port: u16,
913 connect_timeout: Duration,
914 read_timeout: Duration,
915) -> Result<Response, String> {
916 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
917}
918
919#[cfg(feature = "gateway-api")]
922#[allow(clippy::too_many_arguments)]
923pub(crate) fn forward_pooled_with_host(
924 pool: &ConnPool,
925 request: &Request,
926 client: Peer<'_>,
927 host: &str,
928 port: u16,
929 host_header: &str,
930 connect_timeout: Duration,
931 read_timeout: Duration,
932) -> Result<Response, String> {
933 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
934}
935
936#[allow(clippy::too_many_arguments)]
943fn forward_via<S: UpstreamIo>(
944 pool: &Pool<S>,
945 request: &Request,
946 client: Peer<'_>,
947 host: &str,
948 port: u16,
949 host_header: &str,
950 read_timeout: Duration,
951 connect: impl Fn() -> Result<S, String>,
952) -> Result<Response, String> {
953 let key = format!("{}:{}", host, port);
954 if let Some(stream) = pool.acquire(&key) {
955 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
956 Exchange::Done(response) => return Ok(response),
957 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
959 }
960 }
961 let stream = connect()?;
962 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
963 Exchange::Done(response) => Ok(response),
964 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
965 }
966}
967
968fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
972 let key = format!("{}:{}", host, port);
973 let addrs: Vec<_> = key
974 .to_socket_addrs()
975 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
976 .collect();
977 let mut last_error = format!("no address resolved for {}", key);
978 for addr in addrs {
979 match TcpStream::connect_timeout(&addr, connect_timeout) {
980 Ok(stream) => {
981 let _ = stream.set_nodelay(true);
982 return Ok(stream);
983 }
984 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
985 }
986 }
987 Err(last_error)
988}
989
990#[cfg(any(feature = "http-client", feature = "http2"))]
992fn shared_tls_pool() -> &'static Pool<TlsStream> {
993 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
994 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
995}
996
997#[cfg(any(feature = "http-client", feature = "http2"))]
1001pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
1002 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1003 Arc::clone(CONFIG.get_or_init(|| {
1004 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1005 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1006 }))
1007}
1008
1009#[cfg(any(feature = "http-client", feature = "http2"))]
1010fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1011 let tcp = connect_tcp(host, port, connect_timeout)?;
1012 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1013 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1014 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1015 Ok(rustls::StreamOwned::new(conn, tcp))
1016}
1017
1018fn is_idempotent(method: &str) -> bool {
1019 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1020}
1021
1022enum Exchange {
1023 Done(Response),
1024 NoResponse(String),
1026 Failed(String),
1027}
1028
1029fn exchange<S: UpstreamIo>(
1032 pool: &Pool<S>,
1033 key: &str,
1034 mut stream: S,
1035 request: &Request,
1036 host: &str,
1037 client: Peer<'_>,
1038 read_timeout: Duration,
1039) -> Exchange {
1040 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1041 return Exchange::Failed(e.to_string());
1042 }
1043 let req_bytes = build_request(request, host, client, true);
1045 if let Err(e) = stream.write_all(&req_bytes) {
1046 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1047 }
1048
1049 let mut tmp = [0u8; 4096];
1050 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1051 Ok(head) => head,
1052 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1053 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1054 };
1055 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1056
1057 if response_has_no_body(&request.method, &header_lower) {
1058 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1059 pool.release(key, stream);
1060 }
1061 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1062 Ok(response) => Exchange::Done(response),
1063 Err(e) => Exchange::Failed(e),
1064 };
1065 }
1066
1067 if should_stream_response(&header_lower) {
1068 return match parse_status_and_headers(&header_bytes) {
1071 Ok(mut response) => {
1072 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1073 Exchange::Done(response)
1074 }
1075 Err(e) => Exchange::Failed(e),
1076 };
1077 }
1078 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1080 Ok((resp_bytes, reusable)) => {
1081 if reusable {
1082 pool.release(key, stream);
1083 }
1084 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1085 Ok(response) => Exchange::Done(response),
1086 Err(e) => Exchange::Failed(e),
1087 }
1088 }
1089 Err(e) => Exchange::Failed(e),
1090 }
1091}
1092
1093#[cfg(test)]
1098fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1099 read_response_head(stream, tmp).map_err(|e| match e {
1100 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1101 })
1102}
1103
1104enum HeadError {
1108 NothingReceived(String),
1109 Partial(String),
1110}
1111
1112fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1113 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1114}
1115
1116fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1122 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1123 let mut interim = 0;
1124 while is_interim_response(&head) {
1125 crate::early_hints::forward_upstream(&head);
1126 interim += 1;
1127 if interim > MAX_INTERIM_RESPONSES {
1128 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1129 }
1130 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1131 }
1132 Ok((head, rest))
1133}
1134
1135fn read_response_head_from<S: Read>(
1139 stream: &mut S,
1140 tmp: &mut [u8],
1141 mut buf: Vec<u8>,
1142 received: bool,
1143) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1144 loop {
1145 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1146 let body_prefix = buf[pos + 4..].to_vec();
1147 buf.truncate(pos + 4);
1148 return Ok((buf, body_prefix));
1149 }
1150 let nothing = buf.is_empty() && !received;
1151 let n = match stream.read(tmp) {
1152 Ok(n) => n,
1153 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1154 Err(e) => return Err(HeadError::Partial(e.to_string())),
1155 };
1156 if n == 0 {
1157 let msg = "backend closed connection before headers were complete".to_string();
1158 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1159 }
1160 buf.extend_from_slice(&tmp[..n]);
1161 }
1162}
1163
1164pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1166
1167pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1170 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1171 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1172 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1173}
1174
1175pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1179 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1180 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1181}
1182
1183pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1190 let is_sse = header_lower.lines().any(|l| {
1191 l.starts_with("content-type:") && l.contains("text/event-stream")
1192 });
1193 let is_chunked = header_lower.lines().any(|l| {
1194 l.starts_with("transfer-encoding:") && l.contains("chunked")
1195 });
1196 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1197 l.strip_prefix("content-length:")?.trim().parse().ok()
1198 });
1199 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1200 is_sse || is_chunked || is_large
1201}
1202
1203#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1209pub(crate) struct HeadFacts {
1210 pub(crate) status_no_body: bool,
1212 pub(crate) sse: bool,
1213 pub(crate) chunked: bool,
1214 pub(crate) content_length: Option<usize>,
1216 pub(crate) connection_close: bool,
1217}
1218
1219#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1220impl HeadFacts {
1221 pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1222 let text = std::str::from_utf8(head).unwrap_or("");
1223 let status = text.split_whitespace().nth(1).unwrap_or("");
1224 let mut facts = HeadFacts {
1225 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1226 sse: false,
1227 chunked: false,
1228 content_length: None,
1229 connection_close: false,
1230 };
1231 for line in text.lines() {
1232 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1233 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1234 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1235 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1236 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1237 if facts.content_length.is_none() {
1238 facts.content_length = value.trim().parse().ok();
1239 }
1240 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1241 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1242 }
1243 }
1244 facts
1245 }
1246
1247 pub(crate) fn no_body(&self, method: &str) -> bool {
1249 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1250 }
1251
1252 pub(crate) fn should_stream(&self) -> bool {
1254 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1255 }
1256}
1257
1258fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1259 let bytes = line.as_bytes();
1260 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1262 .then(|| &line[prefix.len()..])
1263}
1264
1265fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1266 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1267}
1268
1269fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1271 let s = std::str::from_utf8(header_bytes)
1272 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1273 let mut lines = s.lines();
1274 let status_line = lines.next().ok_or("empty backend response")?;
1275 let mut parts = status_line.splitn(3, ' ');
1276 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1277 let status_code: i16 = parts
1278 .next()
1279 .unwrap_or("502")
1280 .parse()
1281 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1282 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1283 let mut headers = Vec::new();
1284 for line in lines {
1285 let line = line.trim_end_matches('\r');
1286 if line.is_empty() { break; }
1287 if let Some(colon) = line.find(':') {
1288 headers.push(crate::header::Header {
1289 name: line[..colon].trim().to_string().into(),
1290 value: (line[colon + 1..].trim().to_string()).into(),
1291 });
1292 }
1293 }
1294 Ok(Response {
1295 http_version,
1296 status_code,
1297 reason_phrase,
1298 headers,
1299 content_range_list: vec![],
1300 stream_file: None,
1301 stream_pipe: None,
1302 trailers: None,
1303 })
1304}
1305
1306fn read_response_from_partial<S: Read>(
1312 stream: &mut S,
1313 header_bytes: Vec<u8>,
1314 body_prefix: Vec<u8>,
1315 tmp: &mut [u8],
1316) -> Result<(Vec<u8>, bool), String> {
1317 let header_end = header_bytes.len();
1318 let mut buf = header_bytes;
1319 buf.extend_from_slice(&body_prefix);
1320
1321 let header_lower =
1322 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1323 let connection_close =
1324 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1325 let is_chunked = header_lower
1326 .lines()
1327 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1328 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1329 l.strip_prefix("content-length:")?.trim().parse().ok()
1330 });
1331
1332 if is_chunked {
1333 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1334 rewrite_as_content_length(&mut buf, header_end, &decoded);
1335 Ok((buf, !connection_close))
1336 } else if let Some(len) = content_length {
1337 while buf.len() < header_end + len {
1338 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1339 if n == 0 { break; }
1340 buf.extend_from_slice(&tmp[..n]);
1341 }
1342 Ok((buf, !connection_close))
1343 } else {
1344 loop {
1345 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1346 if n == 0 { break; }
1347 buf.extend_from_slice(&tmp[..n]);
1348 }
1349 Ok((buf, false))
1350 }
1351}
1352
1353struct Backend {
1356 host: String,
1357 port: u16,
1358 tls: bool,
1361}
1362
1363impl Backend {
1364 fn parse(url: &str) -> Option<Self> {
1365 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1366 (r, true, 443u16)
1367 } else if let Some(r) = url.strip_prefix("h2s://") {
1368 (r, true, 443u16)
1369 } else if let Some(r) = url.strip_prefix("grpcs://") {
1370 (r, true, 443u16)
1371 } else if let Some(r) = url.strip_prefix("http://") {
1372 (r, false, 80u16)
1373 } else if let Some(r) = url.strip_prefix("h2://") {
1374 (r, false, 80u16)
1375 } else if let Some(r) = url.strip_prefix("grpc://") {
1376 (r, false, 80u16)
1377 } else {
1378 (url, false, 80u16)
1379 };
1380 let host_port = rest.split('/').next().unwrap_or(rest);
1382 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1383 let port_str = &host_port[colon + 1..];
1384 if let Ok(p) = port_str.parse::<u16>() {
1385 (host_port[..colon].to_string(), p)
1386 } else {
1387 (host_port.to_string(), default_port)
1388 }
1389 } else {
1390 (host_port.to_string(), default_port)
1391 };
1392 if host.is_empty() {
1393 return None;
1394 }
1395 Some(Backend { host, port, tls })
1396 }
1397}
1398
1399#[cfg(feature = "http2")]
1410pub struct H2ReverseProxy {
1411 inner: ReverseProxy,
1412}
1413
1414#[cfg(feature = "http2")]
1415impl H2ReverseProxy {
1416 pub fn new<I, S>(backends: I) -> Self
1427 where
1428 I: IntoIterator<Item = S>,
1429 S: AsRef<str>,
1430 {
1431 H2ReverseProxy {
1432 inner: ReverseProxy::new(backends),
1433 }
1434 }
1435
1436 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1438 self.inner = self.inner.path_prefix(prefix);
1439 self
1440 }
1441
1442 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1444 self.inner = self.inner.connect_timeout_ms(ms);
1445 self
1446 }
1447
1448 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1453 self.inner = self.inner.read_timeout_ms(ms);
1454 self
1455 }
1456
1457 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1460 self.inner = self.inner.tls_ca_file(path)?;
1461 Ok(self)
1462 }
1463
1464 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1467 self.inner = self.inner.tls_client_cert(cert, key)?;
1468 Ok(self)
1469 }
1470
1471 fn matches(&self, request: &Request) -> bool {
1472 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1473 }
1474}
1475
1476#[cfg(feature = "http2")]
1477impl crate::middleware::Middleware for H2ReverseProxy {
1478 fn handle(
1479 &self,
1480 request: &crate::request::Request,
1481 connection: &crate::server::ConnectionInfo,
1482 next: &dyn crate::application::Application,
1483 ) -> Result<crate::response::Response, String> {
1484 if !self.matches(request) {
1485 return next.execute(request, connection);
1486 }
1487 if self.inner.backends.is_empty() {
1488 let mut response = bad_gateway();
1489 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1490 return Ok(response);
1491 }
1492 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1494 let n = self.inner.backends.len();
1495 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1496 let mut last_error = String::new();
1497 for attempt in 0..attempts {
1498 let backend = &self.inner.backends[(start + attempt) % n];
1499 let tls = backend.tls.then(|| match &self.inner.trust {
1500 Some(trust) => trust.h2_config(),
1501 None => h2_upstream::default_tls(),
1502 });
1503 let target = h2_upstream::Target {
1504 backend,
1505 tls,
1506 connect_timeout: self.inner.connect_timeout,
1507 read_timeout: self.inner.read_timeout,
1508 authority: None,
1509 };
1510 match h2_upstream::forward(request, Peer::of(connection), &target) {
1511 Ok(mut response) => {
1512 status::received(&mut response, None);
1513 return Ok(response);
1514 }
1515 Err(e) => last_error = e,
1516 }
1517 }
1518 let mut response = bad_gateway();
1519 status::failed_with(&mut response, &last_error, None);
1520 Ok(response)
1521 }
1522
1523 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1526 true
1527 }
1528
1529 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1532 self.matches(request)
1533 }
1534}
1535
1536#[cfg(feature = "http3")]
1558pub struct H3ReverseProxy {
1559 inner: ReverseProxy,
1560 name: String,
1561 tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1563}
1564
1565#[cfg(feature = "http3")]
1566impl H3ReverseProxy {
1567 pub fn new<I, S>(backends: I) -> Self
1573 where
1574 I: IntoIterator<Item = S>,
1575 S: AsRef<str>,
1576 {
1577 H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1578 }
1579
1580 pub fn name(mut self, name: impl Into<String>) -> Self {
1582 self.name = name.into();
1583 self
1584 }
1585
1586 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1588 self.inner = self.inner.path_prefix(prefix);
1589 self
1590 }
1591
1592 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1594 self.inner = self.inner.connect_timeout_ms(ms);
1595 self
1596 }
1597
1598 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1601 self.inner = self.inner.read_timeout_ms(ms);
1602 self
1603 }
1604
1605 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1608 self.inner = self.inner.tls_ca_file(path)?;
1609 Ok(self)
1610 }
1611
1612 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1614 self.inner = self.inner.tls_client_cert(cert, key)?;
1615 Ok(self)
1616 }
1617
1618 fn matches(&self, request: &Request) -> bool {
1619 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1620 }
1621
1622 fn on_tcp(&self, key: &str) -> bool {
1623 let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1624 match tcp_until.get(key) {
1625 Some(until) if std::time::Instant::now() < *until => true,
1626 Some(_) => {
1627 tcp_until.remove(key);
1628 false
1629 }
1630 None => false,
1631 }
1632 }
1633
1634 fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1637 let key = format!("{}:{}", backend.host, backend.port);
1638 if backend.tls && !self.on_tcp(&key) {
1639 let config = match &self.inner.trust {
1640 Some(trust) => trust.client_config(),
1641 None => tls_client_config(),
1642 };
1643 let target = h3_upstream::Target {
1644 host: &backend.host,
1645 port: backend.port,
1646 authority: None,
1647 tls: &config,
1648 connect_timeout: self.inner.connect_timeout,
1649 read_timeout: self.inner.read_timeout,
1650 };
1651 match h3_upstream::forward(request, Peer::of(connection), &target) {
1652 Err(h3_upstream::H3Error::Connect(e)) => {
1653 self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1654 crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1655 eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1656 }
1657 result => return result.map_err(|e| e.to_string()),
1658 }
1659 }
1660 forward_upstream_trusting(
1661 &self.inner.pool,
1662 request,
1663 Peer::of(connection),
1664 &backend.host,
1665 backend.port,
1666 backend.tls,
1667 self.inner.trust.as_deref(),
1668 self.inner.connect_timeout,
1669 self.inner.read_timeout,
1670 )
1671 }
1672}
1673
1674#[cfg(feature = "http3")]
1675impl crate::middleware::Middleware for H3ReverseProxy {
1676 fn handle(
1677 &self,
1678 request: &crate::request::Request,
1679 connection: &crate::server::ConnectionInfo,
1680 next: &dyn crate::application::Application,
1681 ) -> Result<crate::response::Response, String> {
1682 if !self.matches(request) {
1683 return next.execute(request, connection);
1684 }
1685 if self.inner.backends.is_empty() {
1686 let mut response = bad_gateway();
1687 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1688 return Ok(response);
1689 }
1690 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1692 let n = self.inner.backends.len();
1693 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1694 let mut last_error = String::new();
1695 for attempt in 0..attempts {
1696 match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1697 Ok(mut response) => {
1698 status::received(&mut response, None);
1699 return Ok(response);
1700 }
1701 Err(e) => last_error = e,
1702 }
1703 }
1704 let mut response = bad_gateway();
1705 status::failed_with(&mut response, &last_error, None);
1706 Ok(response)
1707 }
1708
1709 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1710 true
1711 }
1712
1713 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1715 self.matches(request)
1716 }
1717}
1718
1719#[cfg(feature = "http2")]
1723pub(crate) fn forward_h2(
1724 request: &Request,
1725 client: Peer<'_>,
1726 url: &str,
1727 tls: bool,
1728 trust: Option<&UpstreamTls>,
1729 connect_timeout: Duration,
1730 read_timeout: Duration,
1731) -> Result<Response, String> {
1732 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1733 backend.tls |= tls;
1734 let tls = backend.tls.then(|| match trust {
1735 Some(t) => t.h2_config(),
1736 None => h2_upstream::default_tls(),
1737 });
1738 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1739}
1740
1741#[cfg(feature = "gateway-api")]
1746#[allow(clippy::too_many_arguments)]
1747pub(crate) fn forward_tls_with_host(
1748 config: &Arc<rustls::ClientConfig>,
1749 label: &str,
1750 request: &Request,
1751 client: Peer<'_>,
1752 ip: &str,
1753 port: u16,
1754 server_name: &str,
1755 host_header: &str,
1756 connect_timeout: Duration,
1757 read_timeout: Duration,
1758) -> Result<Response, String> {
1759 let pool = tls_pool_for_ca(label);
1760 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1761 let tcp = connect_tcp(ip, port, connect_timeout)?;
1762 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1763 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1764 Ok(rustls::StreamOwned::new(conn, tcp))
1765 })
1766}
1767
1768#[cfg(feature = "gateway-api")]
1772pub(crate) fn forward_h2c_with_host(
1773 request: &Request,
1774 client: Peer<'_>,
1775 host: &str,
1776 port: u16,
1777 authority: &str,
1778 connect_timeout: Duration,
1779 read_timeout: Duration,
1780) -> Result<Response, String> {
1781 let backend = Backend { host: host.to_string(), port, tls: false };
1782 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1783}
1784
1785#[cfg(feature = "http2")]
1805pub struct GrpcProxy {
1806 inner: H2ReverseProxy,
1807}
1808
1809#[cfg(feature = "http2")]
1810impl GrpcProxy {
1811 pub fn new<I, S>(backends: I) -> Self
1821 where
1822 I: IntoIterator<Item = S>,
1823 S: AsRef<str>,
1824 {
1825 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1826 }
1827
1828 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1830 self.inner = self.inner.path_prefix(prefix);
1831 self
1832 }
1833
1834 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1836 self.inner = self.inner.connect_timeout_ms(ms);
1837 self
1838 }
1839
1840 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1842 self.inner = self.inner.read_timeout_ms(ms);
1843 self
1844 }
1845
1846 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1848 self.inner = self.inner.tls_ca_file(path)?;
1849 Ok(self)
1850 }
1851
1852 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1855 self.inner = self.inner.tls_client_cert(cert, key)?;
1856 Ok(self)
1857 }
1858}
1859
1860#[cfg(feature = "http2")]
1861impl crate::middleware::Middleware for GrpcProxy {
1862 fn handle(
1863 &self,
1864 request: &crate::request::Request,
1865 connection: &crate::server::ConnectionInfo,
1866 next: &dyn crate::application::Application,
1867 ) -> Result<crate::response::Response, String> {
1868 if is_grpc(request) {
1869 self.inner.handle(request, connection, next)
1870 } else {
1871 next.execute(request, connection)
1872 }
1873 }
1874
1875 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1876 true
1877 }
1878
1879 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1880 is_grpc(request) && self.inner.matches(request)
1881 }
1882}
1883
1884#[cfg(feature = "http2")]
1885fn is_grpc(request: &Request) -> bool {
1886 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1887}
1888
1889#[cfg(test)]
1892mod backend_parse_tests {
1893 use super::Backend;
1894
1895 fn parse(url: &str) -> Option<(String, u16, bool)> {
1896 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1897 }
1898
1899 #[test]
1900 fn bare_host_port() {
1901 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1902 }
1903
1904 #[test]
1905 fn http_scheme() {
1906 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1907 }
1908
1909 #[test]
1910 fn h2_scheme_plain() {
1911 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1912 }
1913
1914 #[test]
1915 fn grpc_scheme_plain() {
1916 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1917 }
1918
1919 #[test]
1920 fn https_scheme_sets_tls_and_default_port() {
1921 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1922 }
1923
1924 #[test]
1925 fn https_scheme_explicit_port() {
1926 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1927 }
1928
1929 #[test]
1930 fn h2s_scheme_sets_tls() {
1931 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1932 }
1933
1934 #[test]
1935 fn h2s_scheme_explicit_port() {
1936 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1937 }
1938
1939 #[test]
1940 fn grpcs_scheme_sets_tls() {
1941 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1942 }
1943
1944 #[test]
1945 fn grpcs_scheme_explicit_port() {
1946 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1947 }
1948
1949 #[test]
1950 fn empty_host_returns_none() {
1951 assert_eq!(None, parse("https://"));
1952 }
1953
1954 #[test]
1955 fn bare_host_no_port_defaults_to_80() {
1956 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1957 }
1958}