1pub mod pool;
28pub mod status;
29#[cfg(feature = "http2")]
30#[doc(hidden)]
31pub mod async_upstream;
32#[cfg(not(target_arch = "wasm32"))]
33pub mod mirror;
34#[cfg(not(target_arch = "wasm32"))]
37#[doc(hidden)]
38pub mod upgrade;
39#[cfg(feature = "http2")]
40#[doc(hidden)]
41pub mod h2_upstream;
42#[cfg(feature = "http3")]
43#[doc(hidden)]
44pub mod h3_upstream;
45#[cfg(feature = "http2")]
46pub mod grpc_web;
47#[cfg(feature = "http2")]
48pub use grpc_web::GrpcWebLayer;
49
50#[cfg(test)]
51mod tests;
52
53use std::io::{Read, Write};
54use crate::ext::decimal::Decimal;
55use std::net::{TcpStream, ToSocketAddrs};
56use std::sync::atomic::{AtomicUsize, Ordering};
57use std::sync::Arc;
58use std::time::Duration;
59
60pub use pool::{ConnPool, Pool};
61
62use crate::application::Application;
63use crate::middleware::Middleware;
64use crate::request::Request;
65use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
66use crate::server::ConnectionInfo;
67
68const HOP_BY_HOP: &[&str] = &[
72 "connection",
73 "keep-alive",
74 "proxy-authenticate",
75 "proxy-authorization",
76 "proxy-connection",
77 "te",
78 "trailer",
79 "transfer-encoding",
80 "upgrade",
81];
82
83pub enum LoadBalancing {
85 RoundRobin,
87}
88
89pub struct ReverseProxy {
104 backends: Vec<Backend>,
105 path_prefix: Option<String>,
106 connect_timeout: Duration,
107 read_timeout: Duration,
108 counter: AtomicUsize,
109 pool: Arc<ConnPool>,
110 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
111 trust: Option<Arc<UpstreamTls>>,
112 tls_ca: Option<String>,
114 tls_client: Option<(String, String)>,
115}
116
117impl ReverseProxy {
118 pub fn new<I, S>(backends: I) -> Self
122 where
123 I: IntoIterator<Item = S>,
124 S: AsRef<str>,
125 {
126 Self {
127 backends: backends
128 .into_iter()
129 .filter_map(|u| Backend::parse(u.as_ref()))
130 .collect(),
131 path_prefix: None,
132 connect_timeout: Duration::from_secs(5),
133 read_timeout: Duration::from_secs(30),
134 counter: AtomicUsize::new(0),
135 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
136 breaker: None,
137 trust: None,
138 tls_ca: None,
139 tls_client: None,
140 }
141 }
142
143 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
162 self.breaker = Some(breaker);
163 self
164 }
165
166 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
171 self.path_prefix = Some(prefix.into());
172 self
173 }
174
175 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
177 self
178 }
179
180 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
182 self.connect_timeout = Duration::from_millis(ms);
183 self
184 }
185
186 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
188 self.read_timeout = Duration::from_millis(ms);
189 self
190 }
191
192 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
197 self.pool = pool;
198 self
199 }
200
201 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
205 self.tls_ca = Some(path.to_string());
206 self.rebuild_trust()?;
207 Ok(self)
208 }
209
210 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
214 self.tls_client = Some((cert.to_string(), key.to_string()));
215 self.rebuild_trust()?;
216 Ok(self)
217 }
218
219 fn rebuild_trust(&mut self) -> Result<(), String> {
220 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
221 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
222 Ok(())
223 }
224
225 pub fn max_idle_conns(mut self, n: usize) -> Self {
227 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
228 self
229 }
230
231 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
232 if self.backends.is_empty() {
233 return Err("no backends configured".to_string());
234 }
235 let n = self.backends.len();
236 let start = self.counter.fetch_add(1, Ordering::Relaxed);
237 let mut last_err = "all backends failed".to_string();
238 for attempt in 0..n {
239 let idx = (start + attempt) % n;
240 let backend = &self.backends[idx];
241 let key = format!("{}:{}", backend.host, backend.port);
242
243 if let Some(breaker) = &self.breaker {
244 if !breaker.is_available(&key) {
245 last_err = format!("circuit open for {}", key);
246 continue;
247 }
248 }
249
250 match self.try_backend(request, connection, backend) {
251 Ok(resp) => {
252 if let Some(breaker) = &self.breaker {
253 breaker.record_success(&key);
254 }
255 return Ok(resp);
256 }
257 Err(e) => {
258 if let Some(breaker) = &self.breaker {
259 breaker.record_failure(&key);
260 }
261 last_err = e;
262 }
263 }
264 }
265 Err(last_err)
266 }
267
268 fn try_backend(
269 &self,
270 request: &Request,
271 connection: &ConnectionInfo,
272 backend: &Backend,
273 ) -> Result<Response, String> {
274 forward_upstream_trusting(
275 &self.pool,
276 request,
277 crate::proxy::Peer::of(connection),
278 &backend.host,
279 backend.port,
280 backend.tls,
281 self.trust.as_deref(),
282 self.connect_timeout,
283 self.read_timeout,
284 )
285 }
286}
287
288impl Middleware for ReverseProxy {
289 fn handle(
290 &self,
291 request: &Request,
292 connection: &ConnectionInfo,
293 next: &dyn Application,
294 ) -> Result<Response, String> {
295 if let Some(prefix) = &self.path_prefix {
296 if !request.request_uri.starts_with(prefix.as_str()) {
297 return next.execute(request, connection);
298 }
299 }
300 match self.proxy(request, connection) {
301 Ok(mut resp) => {
302 status::received(&mut resp, None);
303 Ok(resp)
304 }
305 Err(e) => {
306 let mut resp = bad_gateway();
307 status::failed_with(&mut resp, &e, None);
308 Ok(resp)
309 }
310 }
311 }
312}
313
314#[derive(Clone, Copy, Debug)]
319#[doc(hidden)]
320pub struct Peer<'a> {
321 pub ip: &'a str,
322 pub tls: bool,
323}
324
325impl<'a> Peer<'a> {
326 #[doc(hidden)]
327 pub fn of(connection: &'a ConnectionInfo) -> Self {
328 Peer { ip: &connection.client.ip, tls: connection.tls }
329 }
330
331 #[doc(hidden)]
333 pub fn plain(ip: &'a str) -> Self {
334 Peer { ip, tls: false }
335 }
336
337 #[doc(hidden)]
338 pub fn proto(&self) -> &'static str {
339 if self.tls { "https" } else { "http" }
340 }
341}
342
343const FORWARDING_HEADERS: [&str; 4] = ["x-forwarded-for", "x-forwarded-proto", "x-forwarded-host", "forwarded"];
345
346#[doc(hidden)]
351pub fn from_peer<'a>(request: &'a Request, client: Peer<'_>) -> std::borrow::Cow<'a, Request> {
352 let has = request.headers.iter().any(|h| FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
353 if !has || crate::server_config::ServerConfig::current().trusted_proxies.trusts(client.ip) {
354 return std::borrow::Cow::Borrowed(request);
355 }
356 let mut owned = request.clone();
357 owned.headers.retain(|h| !FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
358 std::borrow::Cow::Owned(owned)
359}
360
361#[doc(hidden)]
366pub fn forwarded_value(request: &Request, client: Peer<'_>) -> Option<String> {
367 if !crate::server_config::ServerConfig::current().forwarded_header {
368 return None;
369 }
370 let node = match client.ip.parse::<std::net::IpAddr>() {
371 Ok(std::net::IpAddr::V4(v4)) => v4.to_string(),
372 Ok(std::net::IpAddr::V6(v6)) => match v6.to_ipv4_mapped() {
373 Some(v4) => v4.to_string(),
374 None => format!("\"[{v6}]\""),
375 },
376 Err(_) => "unknown".to_string(),
377 };
378 let mut element = format!("for={node};proto={}", client.proto());
379 if let Some(host) = request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|h: &&str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_graphic() && b != b'"' && b != b'\\')) {
380 let token = host.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b));
381 if token {
382 element.push_str(&format!(";host={host}"));
383 } else {
384 element.push_str(&format!(";host=\"{host}\""));
385 }
386 }
387 let prior = request.headers.iter().filter(|h| h.name.eq_ignore_ascii_case("forwarded")).map(|h| h.value.trim()).filter(|v| !v.is_empty() && !v.bytes().any(|b| b < 0x20)).collect::<Vec<_>>();
388 Some(if prior.is_empty() { element } else { format!("{}, {element}", prior.join(", ")) })
389}
390
391#[doc(hidden)]
392pub fn build_request(
393 request: &Request,
394 backend_host: &str,
395 client: Peer<'_>,
396 keep_alive: bool,
397) -> Vec<u8> {
398 let request = &*from_peer(request, client);
399 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
405 #[cfg(not(target_arch = "wasm32"))]
408 let trace = crate::otel::propagation_headers();
409 #[cfg(target_arch = "wasm32")]
410 let trace: Option<(String, Option<String>)> = None;
411 let forwarded_header = forwarded_value(request, client);
412 let forwarded = |h: &&crate::header::Header| {
413 !h.name.is_empty()
415 && !is_hop_by_hop_or_host(&h.name)
416 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
417 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
418 && !(forwarded_header.is_some() && h.name.eq_ignore_ascii_case("Forwarded"))
419 && !connection.is_some_and(|c| connection_listed(c, &h.name))
420 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
421 };
422 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
423 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
427 let forwarded_host = match request.header("X-Forwarded-Host") {
428 Some(_) => None,
429 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
430 };
431 let body_len = Decimal::new(request.body.len() as u128);
432 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
433 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
434 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
435 + forwarded_host.map_or(0, |v| v.len() + 20)
436 + forwarded_proto.map_or(0, |v| v.len() + 21)
437 + forwarded_header.as_ref().map_or(0, |v| v.len() + 13)
438 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
439 + 13
440 + 24
441 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
442 + 2
443 + request.body.len();
444 let mut out: Vec<u8> = Vec::with_capacity(capacity);
445 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
446 out.extend_from_slice(part.as_bytes());
447 }
448 for h in request.headers.iter().filter(forwarded) {
449 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
450 out.extend_from_slice(part.as_bytes());
451 }
452 }
453 out.extend_from_slice(b"X-Forwarded-For: ");
454 if let Some(prior) = prior_xff {
455 out.extend_from_slice(prior.as_bytes());
456 out.extend_from_slice(b", ");
457 }
458 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
459 out.extend_from_slice(part.as_bytes());
460 }
461 if let Some(host) = forwarded_host {
462 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
463 out.extend_from_slice(part.as_bytes());
464 }
465 }
466 if let Some(proto) = forwarded_proto {
467 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
468 out.extend_from_slice(part.as_bytes());
469 }
470 }
471 if let Some(value) = &forwarded_header {
472 for part in ["Forwarded: ", value.as_str(), "\r\n"] {
473 out.extend_from_slice(part.as_bytes());
474 }
475 }
476 if let Some((traceparent, baggage)) = &trace {
477 for part in ["traceparent: ", traceparent, "\r\n"] {
478 out.extend_from_slice(part.as_bytes());
479 }
480 if let Some(baggage) = baggage {
481 for part in ["baggage: ", baggage, "\r\n"] {
482 out.extend_from_slice(part.as_bytes());
483 }
484 }
485 }
486 if keep_alive {
487 out.extend_from_slice(b"Connection: keep-alive\r\n");
488 } else {
489 out.extend_from_slice(b"Connection: close\r\n");
490 }
491 if !request.body.is_empty() {
492 out.extend_from_slice(b"Content-Length: ");
493 out.extend_from_slice(body_len.as_str().as_bytes());
494 out.extend_from_slice(b"\r\n");
495 }
496 out.extend_from_slice(b"\r\n");
497 out.extend_from_slice(&request.body);
498 out
499}
500
501fn is_hop_by_hop_or_host(name: &str) -> bool {
506 if name.is_ascii() {
507 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
508 } else {
509 let lower = name.to_lowercase();
510 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
511 }
512}
513
514fn connection_listed(connection: &str, name: &str) -> bool {
517 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
518}
519
520fn decode_chunked<S: Read>(
525 stream: &mut S,
526 buf: &[u8],
527 header_end: usize,
528 tmp: &mut [u8],
529) -> Result<Vec<u8>, String> {
530 let mut raw: Vec<u8> = buf[header_end..].to_vec();
532 let mut decoded: Vec<u8> = Vec::new();
533
534 loop {
535 let crlf = loop {
537 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
538 break p;
539 }
540 let n = stream.read(tmp).map_err(|e| e.to_string())?;
541 if n == 0 {
542 return Err("chunked: premature EOF reading chunk size".to_string());
543 }
544 raw.extend_from_slice(&tmp[..n]);
545 };
546
547 let size_line = std::str::from_utf8(&raw[..crlf])
549 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
550 let size_str = size_line.split(';').next().unwrap_or("").trim();
551 let chunk_size = usize::from_str_radix(size_str, 16)
552 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
553 raw.drain(..crlf + 2); if chunk_size == 0 {
556 loop {
560 let line_end = loop {
561 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
562 break p;
563 }
564 let n = stream.read(tmp).map_err(|e| e.to_string())?;
565 if n == 0 {
566 return Err("chunked: premature EOF in trailer".to_string());
567 }
568 raw.extend_from_slice(&tmp[..n]);
569 };
570 raw.drain(..line_end + 2);
571 if line_end == 0 {
572 break;
573 }
574 }
575 break;
576 }
577
578 while raw.len() < chunk_size + 2 {
580 let n = stream.read(tmp).map_err(|e| e.to_string())?;
581 if n == 0 {
582 return Err("chunked: premature EOF reading chunk body".to_string());
583 }
584 raw.extend_from_slice(&tmp[..n]);
585 }
586 decoded.extend_from_slice(&raw[..chunk_size]);
587 raw.drain(..chunk_size + 2); }
589
590 Ok(decoded)
591}
592
593fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
596 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
597 buf.clear();
598 for line in header_str.lines() {
599 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
600 continue;
601 }
602 buf.extend_from_slice(line.as_bytes());
603 buf.extend_from_slice(b"\r\n");
604 }
605 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
606 buf.extend_from_slice(decoded);
607}
608
609pub(crate) use crate::response::REGENERATED_OR_HOP_BY_HOP;
611
612#[cfg_attr(not(feature = "http2"), allow(dead_code))]
621#[doc(hidden)]
622pub fn normalize_upstream_response(mut response: Response) -> Response {
623 response.http_version.clear();
624 response.http_version.push_str(crate::http::VERSION.http_1_1);
625 let has_body = !response.content_range_list.is_empty();
626 if let [range] = response.content_range_list.as_mut_slice() {
627 if let Some(h) = response
628 .headers
629 .iter()
630 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
631 {
632 range.content_type.clear();
633 range.content_type.push_str(&h.value);
634 }
635 }
636 response.headers.retain(|h| {
639 if h.name.eq_ignore_ascii_case("content-type") {
640 return !has_body;
642 }
643 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
644 });
645 response
646}
647
648#[doc(hidden)]
649pub fn bad_gateway() -> Response {
650 crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
651}
652
653const STREAM_THRESHOLD: usize = 1024 * 1024; #[doc(hidden)]
661pub struct ConcatReader<R: Read + Send> {
662 prefix: Vec<u8>,
663 prefix_pos: usize,
664 inner: R,
665}
666
667impl<R: Read + Send> ConcatReader<R> {
668 fn new(prefix: Vec<u8>, inner: R) -> Self {
669 ConcatReader { prefix, prefix_pos: 0, inner }
670 }
671}
672
673impl<R: Read + Send> Read for ConcatReader<R> {
674 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
675 if self.prefix_pos < self.prefix.len() {
676 let avail = &self.prefix[self.prefix_pos..];
677 let n = buf.len().min(avail.len());
678 buf[..n].copy_from_slice(&avail[..n]);
679 self.prefix_pos += n;
680 return Ok(n);
681 }
682 self.inner.read(buf)
683 }
684}
685
686
687#[doc(hidden)]
691pub const POOL_MAX_IDLE: usize = 64;
692
693#[doc(hidden)]
696pub fn shared_pool() -> &'static ConnPool {
697 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
698 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
699}
700
701#[doc(hidden)]
704pub trait UpstreamIo: Read + Write + Send + 'static {
705 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
706}
707
708impl UpstreamIo for TcpStream {
709 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
710 self.set_read_timeout(Some(read))?;
711 self.set_write_timeout(Some(write))
712 }
713}
714
715#[cfg(any(feature = "http-client", feature = "http2"))]
716#[doc(hidden)]
717pub type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
718
719#[cfg(any(feature = "http-client", feature = "http2"))]
720impl UpstreamIo for TlsStream {
721 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
722 self.sock.set_io_timeouts(read, write)
723 }
724}
725
726#[doc(hidden)]
732pub fn forward_upstream(
733 plain_pool: &ConnPool,
734 request: &Request,
735 client: Peer<'_>,
736 host: &str,
737 port: u16,
738 tls: bool,
739 connect_timeout: Duration,
740 read_timeout: Duration,
741) -> Result<Response, String> {
742 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
743}
744
745#[allow(clippy::too_many_arguments)]
748#[doc(hidden)]
749pub fn forward_upstream_trusting(
750 plain_pool: &ConnPool,
751 request: &Request,
752 client: Peer<'_>,
753 host: &str,
754 port: u16,
755 tls: bool,
756 trust: Option<&UpstreamTls>,
757 connect_timeout: Duration,
758 read_timeout: Duration,
759) -> Result<Response, String> {
760 forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
761}
762
763#[allow(clippy::too_many_arguments)]
767#[doc(hidden)]
768pub fn forward_upstream_as(
769 plain_pool: &ConnPool,
770 request: &Request,
771 client: Peer<'_>,
772 host: &str,
773 port: u16,
774 host_header: &str,
775 tls: bool,
776 trust: Option<&UpstreamTls>,
777 connect_timeout: Duration,
778 read_timeout: Duration,
779) -> Result<Response, String> {
780 #[cfg(not(target_arch = "wasm32"))]
783 let span = crate::otel::outbound_span(&request.method);
784 #[cfg(not(target_arch = "wasm32"))]
785 if let Some(span) = &span {
786 let scheme = if tls { "https" } else { "http" };
787 let path = request.request_uri.as_str();
788 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
789 span.set_attribute(key, value);
790 }
791 }
792 let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
793 #[cfg(not(target_arch = "wasm32"))]
794 if let Some(span) = &span {
795 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
796 }
797 result
798}
799
800#[allow(clippy::too_many_arguments)]
801fn forward_upstream_untraced(
802 plain_pool: &ConnPool,
803 request: &Request,
804 client: Peer<'_>,
805 host: &str,
806 port: u16,
807 host_header: &str,
808 tls: bool,
809 trust: Option<&UpstreamTls>,
810 connect_timeout: Duration,
811 read_timeout: Duration,
812) -> Result<Response, String> {
813 if tls {
814 #[cfg(any(feature = "http-client", feature = "http2"))]
815 {
816 let (pool, config) = match trust {
817 Some(t) => (t.pool, Arc::clone(&t.config)),
818 None => (shared_tls_pool(), tls_client_config()),
819 };
820 return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
821 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
822 });
823 }
824 #[cfg(not(any(feature = "http-client", feature = "http2")))]
825 {
826 let _ = trust;
827 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
828 }
829 }
830 forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
831}
832
833pub struct UpstreamTls {
841 #[cfg(any(feature = "http-client", feature = "http2"))]
842 config: Arc<rustls::ClientConfig>,
843 #[cfg(any(feature = "http-client", feature = "http2"))]
844 pool: &'static Pool<TlsStream>,
845 #[cfg(feature = "http2")]
847 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
848 #[cfg(feature = "http2")]
850 h2_config: Arc<rustls::ClientConfig>,
851}
852
853impl UpstreamTls {
854 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
856 UpstreamTls::new(Some(path), None)
857 }
858
859 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
863 #[cfg(any(feature = "http-client", feature = "http2"))]
864 {
865 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
866 let roots = match ca_file {
867 Some(path) => {
868 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
869 let certs = CertificateDer::pem_slice_iter(&bytes)
870 .collect::<Result<Vec<_>, _>>()
871 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
872 if certs.is_empty() {
873 return Err(format!("no certificates in CA file '{path}'"));
874 }
875 let mut roots = rustls::RootCertStore::empty();
876 for cert in certs {
877 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
878 }
879 roots
880 }
881 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
882 };
883 let builder = crate::tls_client_builder().with_root_certificates(roots);
884 let config = match client_cert {
885 None => builder.with_no_client_auth(),
886 Some((cert_path, key_path)) => {
887 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
888 let chain = CertificateDer::pem_slice_iter(&bytes)
889 .collect::<Result<Vec<_>, _>>()
890 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
891 if chain.is_empty() {
892 return Err(format!("no certificate in '{cert_path}'"));
893 }
894 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
895 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
896 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
897 }
898 };
899 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
900 Ok(UpstreamTls::with_config(config, &label))
901 }
902 #[cfg(not(any(feature = "http-client", feature = "http2")))]
903 {
904 let _ = client_cert;
905 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
906 }
907 }
908
909 #[cfg(any(feature = "http-client", feature = "http2"))]
910 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
911 #[cfg(feature = "http2")]
912 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
913 UpstreamTls {
914 config: Arc::new(config),
915 pool: tls_pool_for_ca(label),
916 #[cfg(feature = "http2")]
917 async_pool: async_tls_pool_for_ca(label),
918 #[cfg(feature = "http2")]
919 h2_config,
920 }
921 }
922
923 #[cfg(feature = "http2")]
925 #[doc(hidden)]
926 pub fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
927 &self.h2_config
928 }
929
930 #[doc(hidden)]
934 pub fn reject_all(label: &str) -> UpstreamTls {
935 #[cfg(any(feature = "http-client", feature = "http2"))]
936 {
937 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
938 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
939 }
940 #[cfg(not(any(feature = "http-client", feature = "http2")))]
941 {
942 let _ = label;
943 UpstreamTls {}
944 }
945 }
946
947 #[cfg(feature = "http2")]
949 #[doc(hidden)]
950 pub fn async_tls(&self) -> async_upstream::AsyncTls {
951 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
952 }
953
954 #[cfg(any(feature = "http-client", feature = "http2"))]
956 #[doc(hidden)]
957 pub fn client_config(&self) -> Arc<rustls::ClientConfig> {
958 Arc::clone(&self.config)
959 }
960}
961
962#[cfg(feature = "http2")]
965fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
966 static POOLS: std::sync::OnceLock<
967 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
968 > = std::sync::OnceLock::new();
969 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
970 pools
971 .entry(path.to_string())
972 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
973}
974
975#[cfg(any(feature = "http-client", feature = "http2"))]
977fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
978 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
979 std::sync::OnceLock::new();
980 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
981 pools
982 .entry(path.to_string())
983 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
984}
985
986#[doc(hidden)]
990pub fn forward_pooled(
991 pool: &ConnPool,
992 request: &Request,
993 client: Peer<'_>,
994 host: &str,
995 port: u16,
996 connect_timeout: Duration,
997 read_timeout: Duration,
998) -> Result<Response, String> {
999 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
1000}
1001
1002#[cfg(feature = "gateway-api")]
1005#[allow(clippy::too_many_arguments)]
1006#[doc(hidden)]
1007pub fn forward_pooled_with_host(
1008 pool: &ConnPool,
1009 request: &Request,
1010 client: Peer<'_>,
1011 host: &str,
1012 port: u16,
1013 host_header: &str,
1014 connect_timeout: Duration,
1015 read_timeout: Duration,
1016) -> Result<Response, String> {
1017 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
1018}
1019
1020#[allow(clippy::too_many_arguments)]
1027fn forward_via<S: UpstreamIo>(
1028 pool: &Pool<S>,
1029 request: &Request,
1030 client: Peer<'_>,
1031 host: &str,
1032 port: u16,
1033 host_header: &str,
1034 read_timeout: Duration,
1035 connect: impl Fn() -> Result<S, String>,
1036) -> Result<Response, String> {
1037 let key = format!("{}:{}", host, port);
1038 if let Some(stream) = pool.acquire(&key) {
1039 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1040 Exchange::Done(response) => return Ok(response),
1041 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
1043 }
1044 }
1045 let stream = connect()?;
1046 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1047 Exchange::Done(response) => Ok(response),
1048 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
1049 }
1050}
1051
1052fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
1056 let key = format!("{}:{}", host, port);
1057 let addrs: Vec<_> = key
1058 .to_socket_addrs()
1059 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
1060 .collect();
1061 let mut last_error = format!("no address resolved for {}", key);
1062 for addr in addrs {
1063 match TcpStream::connect_timeout(&addr, connect_timeout) {
1064 Ok(stream) => {
1065 let _ = stream.set_nodelay(true);
1066 return Ok(stream);
1067 }
1068 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
1069 }
1070 }
1071 Err(last_error)
1072}
1073
1074#[cfg(any(feature = "http-client", feature = "http2"))]
1076fn shared_tls_pool() -> &'static Pool<TlsStream> {
1077 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
1078 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
1079}
1080
1081#[cfg(any(feature = "http-client", feature = "http2"))]
1085#[doc(hidden)]
1086pub fn tls_client_config() -> Arc<rustls::ClientConfig> {
1087 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1088 Arc::clone(CONFIG.get_or_init(|| {
1089 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1090 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1091 }))
1092}
1093
1094#[cfg(any(feature = "http-client", feature = "http2"))]
1095fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1096 let tcp = connect_tcp(host, port, connect_timeout)?;
1097 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1098 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1099 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1100 Ok(rustls::StreamOwned::new(conn, tcp))
1101}
1102
1103fn is_idempotent(method: &str) -> bool {
1104 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1105}
1106
1107enum Exchange {
1108 Done(Response),
1109 NoResponse(String),
1111 Failed(String),
1112}
1113
1114fn exchange<S: UpstreamIo>(
1117 pool: &Pool<S>,
1118 key: &str,
1119 mut stream: S,
1120 request: &Request,
1121 host: &str,
1122 client: Peer<'_>,
1123 read_timeout: Duration,
1124) -> Exchange {
1125 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1126 return Exchange::Failed(e.to_string());
1127 }
1128 let req_bytes = build_request(request, host, client, true);
1130 if let Err(e) = stream.write_all(&req_bytes) {
1131 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1132 }
1133
1134 let mut tmp = [0u8; 4096];
1135 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1136 Ok(head) => head,
1137 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1138 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1139 };
1140 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1141
1142 if response_has_no_body(&request.method, &header_lower) {
1143 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1144 pool.release(key, stream);
1145 }
1146 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1147 Ok(response) => Exchange::Done(response),
1148 Err(e) => Exchange::Failed(e),
1149 };
1150 }
1151
1152 if should_stream_response(&header_lower) {
1153 return match parse_status_and_headers(&header_bytes) {
1156 Ok(mut response) => {
1157 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1158 Exchange::Done(response)
1159 }
1160 Err(e) => Exchange::Failed(e),
1161 };
1162 }
1163 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1165 Ok((resp_bytes, reusable)) => {
1166 if reusable {
1167 pool.release(key, stream);
1168 }
1169 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1170 Ok(response) => Exchange::Done(response),
1171 Err(e) => Exchange::Failed(e),
1172 }
1173 }
1174 Err(e) => Exchange::Failed(e),
1175 }
1176}
1177
1178#[cfg(test)]
1183fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1184 read_response_head(stream, tmp).map_err(|e| match e {
1185 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1186 })
1187}
1188
1189enum HeadError {
1193 NothingReceived(String),
1194 Partial(String),
1195}
1196
1197fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1198 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1199}
1200
1201fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1207 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1208 let mut interim = 0;
1209 while is_interim_response(&head) {
1210 crate::early_hints::forward_upstream(&head);
1211 interim += 1;
1212 if interim > MAX_INTERIM_RESPONSES {
1213 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1214 }
1215 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1216 }
1217 Ok((head, rest))
1218}
1219
1220fn read_response_head_from<S: Read>(
1224 stream: &mut S,
1225 tmp: &mut [u8],
1226 mut buf: Vec<u8>,
1227 received: bool,
1228) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1229 loop {
1230 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1231 let body_prefix = buf[pos + 4..].to_vec();
1232 buf.truncate(pos + 4);
1233 return Ok((buf, body_prefix));
1234 }
1235 let nothing = buf.is_empty() && !received;
1236 let n = match stream.read(tmp) {
1237 Ok(n) => n,
1238 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1239 Err(e) => return Err(HeadError::Partial(e.to_string())),
1240 };
1241 if n == 0 {
1242 let msg = "backend closed connection before headers were complete".to_string();
1243 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1244 }
1245 buf.extend_from_slice(&tmp[..n]);
1246 }
1247}
1248
1249#[doc(hidden)]
1251pub const MAX_INTERIM_RESPONSES: usize = 16;
1252
1253#[doc(hidden)]
1256pub fn is_interim_response(head: &[u8]) -> bool {
1257 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1258 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1259 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1260}
1261
1262#[doc(hidden)]
1266pub fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1267 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1268 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1269}
1270
1271#[doc(hidden)]
1278pub fn should_stream_response(header_lower: &str) -> bool {
1279 let is_sse = header_lower.lines().any(|l| {
1280 l.starts_with("content-type:") && l.contains("text/event-stream")
1281 });
1282 let is_chunked = header_lower.lines().any(|l| {
1283 l.starts_with("transfer-encoding:") && l.contains("chunked")
1284 });
1285 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1286 l.strip_prefix("content-length:")?.trim().parse().ok()
1287 });
1288 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1289 is_sse || is_chunked || is_large
1290}
1291
1292#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1298#[doc(hidden)]
1299pub struct HeadFacts {
1300 #[doc(hidden)]
1302 pub status_no_body: bool,
1303 #[doc(hidden)]
1304 pub sse: bool,
1305 #[doc(hidden)]
1306 pub chunked: bool,
1307 #[doc(hidden)]
1309 pub content_length: Option<usize>,
1310 #[doc(hidden)]
1311 pub connection_close: bool,
1312}
1313
1314#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1315impl HeadFacts {
1316 #[doc(hidden)]
1317 pub fn scan(head: &[u8]) -> HeadFacts {
1318 let text = std::str::from_utf8(head).unwrap_or("");
1319 let status = text.split_whitespace().nth(1).unwrap_or("");
1320 let mut facts = HeadFacts {
1321 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1322 sse: false,
1323 chunked: false,
1324 content_length: None,
1325 connection_close: false,
1326 };
1327 for line in text.lines() {
1328 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1329 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1330 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1331 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1332 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1333 if facts.content_length.is_none() {
1334 facts.content_length = value.trim().parse().ok();
1335 }
1336 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1337 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1338 }
1339 }
1340 facts
1341 }
1342
1343 #[doc(hidden)]
1345 pub fn no_body(&self, method: &str) -> bool {
1346 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1347 }
1348
1349 #[doc(hidden)]
1351 pub fn should_stream(&self) -> bool {
1352 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1353 }
1354}
1355
1356fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1357 let bytes = line.as_bytes();
1358 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1360 .then(|| &line[prefix.len()..])
1361}
1362
1363fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1364 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1365}
1366
1367fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1369 let s = std::str::from_utf8(header_bytes)
1370 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1371 let mut lines = s.lines();
1372 let status_line = lines.next().ok_or("empty backend response")?;
1373 let mut parts = status_line.splitn(3, ' ');
1374 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1375 let status_code: i16 = parts
1376 .next()
1377 .unwrap_or("502")
1378 .parse()
1379 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1380 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1381 let mut headers = Vec::new();
1382 for line in lines {
1383 let line = line.trim_end_matches('\r');
1384 if line.is_empty() { break; }
1385 if let Some(colon) = line.find(':') {
1386 headers.push(crate::header::Header {
1387 name: line[..colon].trim().to_string().into(),
1388 value: (line[colon + 1..].trim().to_string()).into(),
1389 });
1390 }
1391 }
1392 Ok(Response {
1393 http_version,
1394 status_code,
1395 reason_phrase,
1396 headers,
1397 content_range_list: vec![],
1398 stream_file: None,
1399 stream_pipe: None,
1400 trailers: None,
1401 })
1402}
1403
1404fn read_response_from_partial<S: Read>(
1410 stream: &mut S,
1411 header_bytes: Vec<u8>,
1412 body_prefix: Vec<u8>,
1413 tmp: &mut [u8],
1414) -> Result<(Vec<u8>, bool), String> {
1415 let header_end = header_bytes.len();
1416 let mut buf = header_bytes;
1417 buf.extend_from_slice(&body_prefix);
1418
1419 let header_lower =
1420 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1421 let connection_close =
1422 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1423 let is_chunked = header_lower
1424 .lines()
1425 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1426 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1427 l.strip_prefix("content-length:")?.trim().parse().ok()
1428 });
1429
1430 if is_chunked {
1431 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1432 rewrite_as_content_length(&mut buf, header_end, &decoded);
1433 Ok((buf, !connection_close))
1434 } else if let Some(len) = content_length {
1435 while buf.len() < header_end + len {
1436 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1437 if n == 0 { break; }
1438 buf.extend_from_slice(&tmp[..n]);
1439 }
1440 Ok((buf, !connection_close))
1441 } else {
1442 loop {
1443 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1444 if n == 0 { break; }
1445 buf.extend_from_slice(&tmp[..n]);
1446 }
1447 Ok((buf, false))
1448 }
1449}
1450
1451struct Backend {
1454 host: String,
1455 port: u16,
1456 tls: bool,
1459}
1460
1461impl Backend {
1462 fn parse(url: &str) -> Option<Self> {
1463 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1464 (r, true, 443u16)
1465 } else if let Some(r) = url.strip_prefix("h2s://") {
1466 (r, true, 443u16)
1467 } else if let Some(r) = url.strip_prefix("grpcs://") {
1468 (r, true, 443u16)
1469 } else if let Some(r) = url.strip_prefix("http://") {
1470 (r, false, 80u16)
1471 } else if let Some(r) = url.strip_prefix("h2://") {
1472 (r, false, 80u16)
1473 } else if let Some(r) = url.strip_prefix("grpc://") {
1474 (r, false, 80u16)
1475 } else {
1476 (url, false, 80u16)
1477 };
1478 let host_port = rest.split('/').next().unwrap_or(rest);
1480 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1481 let port_str = &host_port[colon + 1..];
1482 if let Ok(p) = port_str.parse::<u16>() {
1483 (host_port[..colon].to_string(), p)
1484 } else {
1485 (host_port.to_string(), default_port)
1486 }
1487 } else {
1488 (host_port.to_string(), default_port)
1489 };
1490 if host.is_empty() {
1491 return None;
1492 }
1493 Some(Backend { host, port, tls })
1494 }
1495}
1496
1497#[cfg(feature = "http2")]
1508pub struct H2ReverseProxy {
1509 inner: ReverseProxy,
1510}
1511
1512#[cfg(feature = "http2")]
1513impl H2ReverseProxy {
1514 pub fn new<I, S>(backends: I) -> Self
1525 where
1526 I: IntoIterator<Item = S>,
1527 S: AsRef<str>,
1528 {
1529 H2ReverseProxy {
1530 inner: ReverseProxy::new(backends),
1531 }
1532 }
1533
1534 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1536 self.inner = self.inner.path_prefix(prefix);
1537 self
1538 }
1539
1540 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1542 self.inner = self.inner.connect_timeout_ms(ms);
1543 self
1544 }
1545
1546 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1551 self.inner = self.inner.read_timeout_ms(ms);
1552 self
1553 }
1554
1555 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1558 self.inner = self.inner.tls_ca_file(path)?;
1559 Ok(self)
1560 }
1561
1562 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1565 self.inner = self.inner.tls_client_cert(cert, key)?;
1566 Ok(self)
1567 }
1568
1569 fn matches(&self, request: &Request) -> bool {
1570 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1571 }
1572}
1573
1574#[cfg(feature = "http2")]
1575impl crate::middleware::Middleware for H2ReverseProxy {
1576 fn handle(
1577 &self,
1578 request: &crate::request::Request,
1579 connection: &crate::server::ConnectionInfo,
1580 next: &dyn crate::application::Application,
1581 ) -> Result<crate::response::Response, String> {
1582 if !self.matches(request) {
1583 return next.execute(request, connection);
1584 }
1585 if self.inner.backends.is_empty() {
1586 let mut response = bad_gateway();
1587 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1588 return Ok(response);
1589 }
1590 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1592 let n = self.inner.backends.len();
1593 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1594 let mut last_error = String::new();
1595 for attempt in 0..attempts {
1596 let backend = &self.inner.backends[(start + attempt) % n];
1597 let tls = backend.tls.then(|| match &self.inner.trust {
1598 Some(trust) => trust.h2_config(),
1599 None => h2_upstream::default_tls(),
1600 });
1601 let target = h2_upstream::Target {
1602 backend,
1603 tls,
1604 connect_timeout: self.inner.connect_timeout,
1605 read_timeout: self.inner.read_timeout,
1606 authority: None,
1607 };
1608 match h2_upstream::forward(request, Peer::of(connection), &target) {
1609 Ok(mut response) => {
1610 status::received(&mut response, None);
1611 return Ok(response);
1612 }
1613 Err(e) => last_error = e,
1614 }
1615 }
1616 let mut response = bad_gateway();
1617 status::failed_with(&mut response, &last_error, None);
1618 Ok(response)
1619 }
1620
1621 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1624 true
1625 }
1626
1627 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1630 self.matches(request)
1631 }
1632}
1633
1634#[cfg(feature = "http3")]
1657pub struct H3ReverseProxy {
1658 inner: ReverseProxy,
1659 name: String,
1660 tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1662}
1663
1664#[cfg(feature = "http3")]
1665impl H3ReverseProxy {
1666 pub fn new<I, S>(backends: I) -> Self
1672 where
1673 I: IntoIterator<Item = S>,
1674 S: AsRef<str>,
1675 {
1676 H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1677 }
1678
1679 pub fn name(mut self, name: impl Into<String>) -> Self {
1681 self.name = name.into();
1682 self
1683 }
1684
1685 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1687 self.inner = self.inner.path_prefix(prefix);
1688 self
1689 }
1690
1691 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1693 self.inner = self.inner.connect_timeout_ms(ms);
1694 self
1695 }
1696
1697 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1700 self.inner = self.inner.read_timeout_ms(ms);
1701 self
1702 }
1703
1704 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1707 self.inner = self.inner.tls_ca_file(path)?;
1708 Ok(self)
1709 }
1710
1711 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1713 self.inner = self.inner.tls_client_cert(cert, key)?;
1714 Ok(self)
1715 }
1716
1717 fn matches(&self, request: &Request) -> bool {
1718 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1719 }
1720
1721 fn on_tcp(&self, key: &str) -> bool {
1722 let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1723 match tcp_until.get(key) {
1724 Some(until) if std::time::Instant::now() < *until => true,
1725 Some(_) => {
1726 tcp_until.remove(key);
1727 false
1728 }
1729 None => false,
1730 }
1731 }
1732
1733 fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1736 let key = format!("{}:{}", backend.host, backend.port);
1737 if backend.tls && !self.on_tcp(&key) {
1738 let config = match &self.inner.trust {
1739 Some(trust) => trust.client_config(),
1740 None => tls_client_config(),
1741 };
1742 let target = h3_upstream::Target {
1743 host: &backend.host,
1744 port: backend.port,
1745 authority: None,
1746 tls: &config,
1747 connect_timeout: self.inner.connect_timeout,
1748 read_timeout: self.inner.read_timeout,
1749 };
1750 match h3_upstream::forward(request, Peer::of(connection), &target) {
1751 Err(h3_upstream::H3Error::Connect(e)) => {
1752 self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1753 crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1754 eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1755 }
1756 result => return result.map_err(|e| e.to_string()),
1757 }
1758 }
1759 forward_upstream_trusting(
1760 &self.inner.pool,
1761 request,
1762 Peer::of(connection),
1763 &backend.host,
1764 backend.port,
1765 backend.tls,
1766 self.inner.trust.as_deref(),
1767 self.inner.connect_timeout,
1768 self.inner.read_timeout,
1769 )
1770 }
1771}
1772
1773#[cfg(feature = "http3")]
1774impl crate::middleware::Middleware for H3ReverseProxy {
1775 fn handle(
1776 &self,
1777 request: &crate::request::Request,
1778 connection: &crate::server::ConnectionInfo,
1779 next: &dyn crate::application::Application,
1780 ) -> Result<crate::response::Response, String> {
1781 if !self.matches(request) {
1782 return next.execute(request, connection);
1783 }
1784 if self.inner.backends.is_empty() {
1785 let mut response = bad_gateway();
1786 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1787 return Ok(response);
1788 }
1789 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1791 let n = self.inner.backends.len();
1792 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1793 let mut last_error = String::new();
1794 for attempt in 0..attempts {
1795 match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1796 Ok(mut response) => {
1797 status::received(&mut response, None);
1798 return Ok(response);
1799 }
1800 Err(e) => last_error = e,
1801 }
1802 }
1803 let mut response = bad_gateway();
1804 status::failed_with(&mut response, &last_error, None);
1805 Ok(response)
1806 }
1807
1808 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1809 true
1810 }
1811
1812 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1814 self.matches(request)
1815 }
1816}
1817
1818#[cfg(feature = "http2")]
1822#[doc(hidden)]
1823pub fn forward_h2(
1824 request: &Request,
1825 client: Peer<'_>,
1826 url: &str,
1827 tls: bool,
1828 trust: Option<&UpstreamTls>,
1829 connect_timeout: Duration,
1830 read_timeout: Duration,
1831) -> Result<Response, String> {
1832 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1833 backend.tls |= tls;
1834 let tls = backend.tls.then(|| match trust {
1835 Some(t) => t.h2_config(),
1836 None => h2_upstream::default_tls(),
1837 });
1838 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1839}
1840
1841#[cfg(feature = "gateway-api")]
1846#[allow(clippy::too_many_arguments)]
1847#[doc(hidden)]
1848pub fn forward_tls_with_host(
1849 config: &Arc<rustls::ClientConfig>,
1850 label: &str,
1851 request: &Request,
1852 client: Peer<'_>,
1853 ip: &str,
1854 port: u16,
1855 server_name: &str,
1856 host_header: &str,
1857 connect_timeout: Duration,
1858 read_timeout: Duration,
1859) -> Result<Response, String> {
1860 let pool = tls_pool_for_ca(label);
1861 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1862 let tcp = connect_tcp(ip, port, connect_timeout)?;
1863 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1864 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1865 Ok(rustls::StreamOwned::new(conn, tcp))
1866 })
1867}
1868
1869#[cfg(feature = "gateway-api")]
1873#[doc(hidden)]
1874pub fn forward_h2c_with_host(
1875 request: &Request,
1876 client: Peer<'_>,
1877 host: &str,
1878 port: u16,
1879 authority: &str,
1880 connect_timeout: Duration,
1881 read_timeout: Duration,
1882) -> Result<Response, String> {
1883 let backend = Backend { host: host.to_string(), port, tls: false };
1884 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1885}
1886
1887#[cfg(feature = "http2")]
1908pub struct GrpcProxy {
1909 inner: H2ReverseProxy,
1910}
1911
1912#[cfg(feature = "http2")]
1913impl GrpcProxy {
1914 pub fn new<I, S>(backends: I) -> Self
1924 where
1925 I: IntoIterator<Item = S>,
1926 S: AsRef<str>,
1927 {
1928 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1929 }
1930
1931 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1933 self.inner = self.inner.path_prefix(prefix);
1934 self
1935 }
1936
1937 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1939 self.inner = self.inner.connect_timeout_ms(ms);
1940 self
1941 }
1942
1943 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1945 self.inner = self.inner.read_timeout_ms(ms);
1946 self
1947 }
1948
1949 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1951 self.inner = self.inner.tls_ca_file(path)?;
1952 Ok(self)
1953 }
1954
1955 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1958 self.inner = self.inner.tls_client_cert(cert, key)?;
1959 Ok(self)
1960 }
1961}
1962
1963#[cfg(feature = "http2")]
1964impl crate::middleware::Middleware for GrpcProxy {
1965 fn handle(
1966 &self,
1967 request: &crate::request::Request,
1968 connection: &crate::server::ConnectionInfo,
1969 next: &dyn crate::application::Application,
1970 ) -> Result<crate::response::Response, String> {
1971 if is_grpc(request) {
1972 self.inner.handle(request, connection, next)
1973 } else {
1974 next.execute(request, connection)
1975 }
1976 }
1977
1978 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1979 true
1980 }
1981
1982 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1983 is_grpc(request) && self.inner.matches(request)
1984 }
1985}
1986
1987#[cfg(feature = "http2")]
1988fn is_grpc(request: &Request) -> bool {
1989 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1990}
1991
1992#[cfg(test)]
1995mod backend_parse_tests {
1996 use super::Backend;
1997
1998 fn parse(url: &str) -> Option<(String, u16, bool)> {
1999 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
2000 }
2001
2002 #[test]
2003 fn bare_host_port() {
2004 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
2005 }
2006
2007 #[test]
2008 fn http_scheme() {
2009 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
2010 }
2011
2012 #[test]
2013 fn h2_scheme_plain() {
2014 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
2015 }
2016
2017 #[test]
2018 fn grpc_scheme_plain() {
2019 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
2020 }
2021
2022 #[test]
2023 fn https_scheme_sets_tls_and_default_port() {
2024 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
2025 }
2026
2027 #[test]
2028 fn https_scheme_explicit_port() {
2029 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
2030 }
2031
2032 #[test]
2033 fn h2s_scheme_sets_tls() {
2034 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
2035 }
2036
2037 #[test]
2038 fn h2s_scheme_explicit_port() {
2039 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
2040 }
2041
2042 #[test]
2043 fn grpcs_scheme_sets_tls() {
2044 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
2045 }
2046
2047 #[test]
2048 fn grpcs_scheme_explicit_port() {
2049 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
2050 }
2051
2052 #[test]
2053 fn empty_host_returns_none() {
2054 assert_eq!(None, parse("https://"));
2055 }
2056
2057 #[test]
2058 fn bare_host_no_port_defaults_to_80() {
2059 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
2060 }
2061}