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(not(target_arch = "wasm32"))]
35pub(crate) mod upgrade;
36#[cfg(feature = "http2")]
37pub(crate) mod h2_upstream;
38#[cfg(feature = "http3")]
39pub(crate) mod h3_upstream;
40#[cfg(feature = "http2")]
41pub mod grpc_web;
42#[cfg(feature = "http2")]
43pub use grpc_web::GrpcWebLayer;
44
45#[cfg(test)]
46mod tests;
47
48use std::io::{Read, Write};
49use crate::ext::decimal::Decimal;
50use std::net::{TcpStream, ToSocketAddrs};
51use std::sync::atomic::{AtomicUsize, Ordering};
52use std::sync::Arc;
53use std::time::Duration;
54
55pub use pool::{ConnPool, Pool};
56
57use crate::application::Application;
58use crate::middleware::Middleware;
59use crate::request::Request;
60use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
61use crate::server::ConnectionInfo;
62
63const HOP_BY_HOP: &[&str] = &[
67 "connection",
68 "keep-alive",
69 "proxy-authenticate",
70 "proxy-authorization",
71 "proxy-connection",
72 "te",
73 "trailer",
74 "transfer-encoding",
75 "upgrade",
76];
77
78pub enum LoadBalancing {
80 RoundRobin,
82}
83
84pub struct ReverseProxy {
99 backends: Vec<Backend>,
100 path_prefix: Option<String>,
101 connect_timeout: Duration,
102 read_timeout: Duration,
103 counter: AtomicUsize,
104 pool: Arc<ConnPool>,
105 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
106 trust: Option<Arc<UpstreamTls>>,
107 tls_ca: Option<String>,
109 tls_client: Option<(String, String)>,
110}
111
112impl ReverseProxy {
113 pub fn new<I, S>(backends: I) -> Self
117 where
118 I: IntoIterator<Item = S>,
119 S: AsRef<str>,
120 {
121 Self {
122 backends: backends
123 .into_iter()
124 .filter_map(|u| Backend::parse(u.as_ref()))
125 .collect(),
126 path_prefix: None,
127 connect_timeout: Duration::from_secs(5),
128 read_timeout: Duration::from_secs(30),
129 counter: AtomicUsize::new(0),
130 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
131 breaker: None,
132 trust: None,
133 tls_ca: None,
134 tls_client: None,
135 }
136 }
137
138 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
156 self.breaker = Some(breaker);
157 self
158 }
159
160 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
165 self.path_prefix = Some(prefix.into());
166 self
167 }
168
169 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
171 self
172 }
173
174 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
176 self.connect_timeout = Duration::from_millis(ms);
177 self
178 }
179
180 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
182 self.read_timeout = Duration::from_millis(ms);
183 self
184 }
185
186 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
191 self.pool = pool;
192 self
193 }
194
195 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
199 self.tls_ca = Some(path.to_string());
200 self.rebuild_trust()?;
201 Ok(self)
202 }
203
204 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
208 self.tls_client = Some((cert.to_string(), key.to_string()));
209 self.rebuild_trust()?;
210 Ok(self)
211 }
212
213 fn rebuild_trust(&mut self) -> Result<(), String> {
214 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
215 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
216 Ok(())
217 }
218
219 pub fn max_idle_conns(mut self, n: usize) -> Self {
221 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
222 self
223 }
224
225 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
226 if self.backends.is_empty() {
227 return Err("no backends configured".to_string());
228 }
229 let n = self.backends.len();
230 let start = self.counter.fetch_add(1, Ordering::Relaxed);
231 let mut last_err = "all backends failed".to_string();
232 for attempt in 0..n {
233 let idx = (start + attempt) % n;
234 let backend = &self.backends[idx];
235 let key = format!("{}:{}", backend.host, backend.port);
236
237 if let Some(breaker) = &self.breaker {
238 if !breaker.is_available(&key) {
239 last_err = format!("circuit open for {}", key);
240 continue;
241 }
242 }
243
244 match self.try_backend(request, connection, backend) {
245 Ok(resp) => {
246 if let Some(breaker) = &self.breaker {
247 breaker.record_success(&key);
248 }
249 return Ok(resp);
250 }
251 Err(e) => {
252 if let Some(breaker) = &self.breaker {
253 breaker.record_failure(&key);
254 }
255 last_err = e;
256 }
257 }
258 }
259 Err(last_err)
260 }
261
262 fn try_backend(
263 &self,
264 request: &Request,
265 connection: &ConnectionInfo,
266 backend: &Backend,
267 ) -> Result<Response, String> {
268 forward_upstream_trusting(
269 &self.pool,
270 request,
271 crate::proxy::Peer::of(connection),
272 &backend.host,
273 backend.port,
274 backend.tls,
275 self.trust.as_deref(),
276 self.connect_timeout,
277 self.read_timeout,
278 )
279 }
280}
281
282impl Middleware for ReverseProxy {
283 fn handle(
284 &self,
285 request: &Request,
286 connection: &ConnectionInfo,
287 next: &dyn Application,
288 ) -> Result<Response, String> {
289 if let Some(prefix) = &self.path_prefix {
290 if !request.request_uri.starts_with(prefix.as_str()) {
291 return next.execute(request, connection);
292 }
293 }
294 match self.proxy(request, connection) {
295 Ok(mut resp) => {
296 status::received(&mut resp, None);
297 Ok(resp)
298 }
299 Err(e) => {
300 let mut resp = bad_gateway();
301 status::failed_with(&mut resp, &e, None);
302 Ok(resp)
303 }
304 }
305 }
306}
307
308#[derive(Clone, Copy, Debug)]
313pub(crate) struct Peer<'a> {
314 pub ip: &'a str,
315 pub tls: bool,
316}
317
318impl<'a> Peer<'a> {
319 pub(crate) fn of(connection: &'a ConnectionInfo) -> Self {
320 Peer { ip: &connection.client.ip, tls: connection.tls }
321 }
322
323 pub(crate) fn plain(ip: &'a str) -> Self {
325 Peer { ip, tls: false }
326 }
327
328 pub(crate) fn proto(&self) -> &'static str {
329 if self.tls { "https" } else { "http" }
330 }
331}
332
333const FORWARDING_HEADERS: [&str; 4] = ["x-forwarded-for", "x-forwarded-proto", "x-forwarded-host", "forwarded"];
335
336pub(crate) fn from_peer<'a>(request: &'a Request, client: Peer<'_>) -> std::borrow::Cow<'a, Request> {
341 let has = request.headers.iter().any(|h| FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
342 if !has || crate::server_config::ServerConfig::current().trusted_proxies.trusts(client.ip) {
343 return std::borrow::Cow::Borrowed(request);
344 }
345 let mut owned = request.clone();
346 owned.headers.retain(|h| !FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
347 std::borrow::Cow::Owned(owned)
348}
349
350pub(crate) fn forwarded_value(request: &Request, client: Peer<'_>) -> Option<String> {
355 if !crate::server_config::ServerConfig::current().forwarded_header {
356 return None;
357 }
358 let node = match client.ip.parse::<std::net::IpAddr>() {
359 Ok(std::net::IpAddr::V4(v4)) => v4.to_string(),
360 Ok(std::net::IpAddr::V6(v6)) => match v6.to_ipv4_mapped() {
361 Some(v4) => v4.to_string(),
362 None => format!("\"[{v6}]\""),
363 },
364 Err(_) => "unknown".to_string(),
365 };
366 let mut element = format!("for={node};proto={}", client.proto());
367 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'\\')) {
368 let token = host.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b));
369 if token {
370 element.push_str(&format!(";host={host}"));
371 } else {
372 element.push_str(&format!(";host=\"{host}\""));
373 }
374 }
375 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<_>>();
376 Some(if prior.is_empty() { element } else { format!("{}, {element}", prior.join(", ")) })
377}
378
379pub(crate) fn build_request(
380 request: &Request,
381 backend_host: &str,
382 client: Peer<'_>,
383 keep_alive: bool,
384) -> Vec<u8> {
385 let request = &*from_peer(request, client);
386 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
392 #[cfg(not(target_arch = "wasm32"))]
395 let trace = crate::otel::propagation_headers();
396 #[cfg(target_arch = "wasm32")]
397 let trace: Option<(String, Option<String>)> = None;
398 let forwarded_header = forwarded_value(request, client);
399 let forwarded = |h: &&crate::header::Header| {
400 !h.name.is_empty()
402 && !is_hop_by_hop_or_host(&h.name)
403 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
404 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
405 && !(forwarded_header.is_some() && h.name.eq_ignore_ascii_case("Forwarded"))
406 && !connection.is_some_and(|c| connection_listed(c, &h.name))
407 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
408 };
409 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
410 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
414 let forwarded_host = match request.header("X-Forwarded-Host") {
415 Some(_) => None,
416 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
417 };
418 let body_len = Decimal::new(request.body.len() as u128);
419 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
420 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
421 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
422 + forwarded_host.map_or(0, |v| v.len() + 20)
423 + forwarded_proto.map_or(0, |v| v.len() + 21)
424 + forwarded_header.as_ref().map_or(0, |v| v.len() + 13)
425 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
426 + 13
427 + 24
428 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
429 + 2
430 + request.body.len();
431 let mut out: Vec<u8> = Vec::with_capacity(capacity);
432 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
433 out.extend_from_slice(part.as_bytes());
434 }
435 for h in request.headers.iter().filter(forwarded) {
436 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
437 out.extend_from_slice(part.as_bytes());
438 }
439 }
440 out.extend_from_slice(b"X-Forwarded-For: ");
441 if let Some(prior) = prior_xff {
442 out.extend_from_slice(prior.as_bytes());
443 out.extend_from_slice(b", ");
444 }
445 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
446 out.extend_from_slice(part.as_bytes());
447 }
448 if let Some(host) = forwarded_host {
449 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
450 out.extend_from_slice(part.as_bytes());
451 }
452 }
453 if let Some(proto) = forwarded_proto {
454 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
455 out.extend_from_slice(part.as_bytes());
456 }
457 }
458 if let Some(value) = &forwarded_header {
459 for part in ["Forwarded: ", value.as_str(), "\r\n"] {
460 out.extend_from_slice(part.as_bytes());
461 }
462 }
463 if let Some((traceparent, baggage)) = &trace {
464 for part in ["traceparent: ", traceparent, "\r\n"] {
465 out.extend_from_slice(part.as_bytes());
466 }
467 if let Some(baggage) = baggage {
468 for part in ["baggage: ", baggage, "\r\n"] {
469 out.extend_from_slice(part.as_bytes());
470 }
471 }
472 }
473 if keep_alive {
474 out.extend_from_slice(b"Connection: keep-alive\r\n");
475 } else {
476 out.extend_from_slice(b"Connection: close\r\n");
477 }
478 if !request.body.is_empty() {
479 out.extend_from_slice(b"Content-Length: ");
480 out.extend_from_slice(body_len.as_str().as_bytes());
481 out.extend_from_slice(b"\r\n");
482 }
483 out.extend_from_slice(b"\r\n");
484 out.extend_from_slice(&request.body);
485 out
486}
487
488fn is_hop_by_hop_or_host(name: &str) -> bool {
493 if name.is_ascii() {
494 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
495 } else {
496 let lower = name.to_lowercase();
497 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
498 }
499}
500
501fn connection_listed(connection: &str, name: &str) -> bool {
504 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
505}
506
507fn decode_chunked<S: Read>(
512 stream: &mut S,
513 buf: &[u8],
514 header_end: usize,
515 tmp: &mut [u8],
516) -> Result<Vec<u8>, String> {
517 let mut raw: Vec<u8> = buf[header_end..].to_vec();
519 let mut decoded: Vec<u8> = Vec::new();
520
521 loop {
522 let crlf = loop {
524 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
525 break p;
526 }
527 let n = stream.read(tmp).map_err(|e| e.to_string())?;
528 if n == 0 {
529 return Err("chunked: premature EOF reading chunk size".to_string());
530 }
531 raw.extend_from_slice(&tmp[..n]);
532 };
533
534 let size_line = std::str::from_utf8(&raw[..crlf])
536 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
537 let size_str = size_line.split(';').next().unwrap_or("").trim();
538 let chunk_size = usize::from_str_radix(size_str, 16)
539 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
540 raw.drain(..crlf + 2); if chunk_size == 0 {
543 loop {
547 let line_end = loop {
548 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
549 break p;
550 }
551 let n = stream.read(tmp).map_err(|e| e.to_string())?;
552 if n == 0 {
553 return Err("chunked: premature EOF in trailer".to_string());
554 }
555 raw.extend_from_slice(&tmp[..n]);
556 };
557 raw.drain(..line_end + 2);
558 if line_end == 0 {
559 break;
560 }
561 }
562 break;
563 }
564
565 while raw.len() < chunk_size + 2 {
567 let n = stream.read(tmp).map_err(|e| e.to_string())?;
568 if n == 0 {
569 return Err("chunked: premature EOF reading chunk body".to_string());
570 }
571 raw.extend_from_slice(&tmp[..n]);
572 }
573 decoded.extend_from_slice(&raw[..chunk_size]);
574 raw.drain(..chunk_size + 2); }
576
577 Ok(decoded)
578}
579
580fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
583 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
584 buf.clear();
585 for line in header_str.lines() {
586 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
587 continue;
588 }
589 buf.extend_from_slice(line.as_bytes());
590 buf.extend_from_slice(b"\r\n");
591 }
592 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
593 buf.extend_from_slice(decoded);
594}
595
596pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
599 "content-length", "transfer-encoding", "connection", "keep-alive",
600 "proxy-connection", "te", "trailer", "upgrade",
601];
602
603#[cfg_attr(not(feature = "http2"), allow(dead_code))]
612pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
613 response.http_version.clear();
614 response.http_version.push_str(crate::http::VERSION.http_1_1);
615 let has_body = !response.content_range_list.is_empty();
616 if let [range] = response.content_range_list.as_mut_slice() {
617 if let Some(h) = response
618 .headers
619 .iter()
620 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
621 {
622 range.content_type.clear();
623 range.content_type.push_str(&h.value);
624 }
625 }
626 response.headers.retain(|h| {
629 if h.name.eq_ignore_ascii_case("content-type") {
630 return !has_body;
632 }
633 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
634 });
635 response
636}
637
638pub(crate) fn bad_gateway() -> Response {
639 crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
640}
641
642const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
650 prefix: Vec<u8>,
651 prefix_pos: usize,
652 inner: R,
653}
654
655impl<R: Read + Send> ConcatReader<R> {
656 fn new(prefix: Vec<u8>, inner: R) -> Self {
657 ConcatReader { prefix, prefix_pos: 0, inner }
658 }
659}
660
661impl<R: Read + Send> Read for ConcatReader<R> {
662 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
663 if self.prefix_pos < self.prefix.len() {
664 let avail = &self.prefix[self.prefix_pos..];
665 let n = buf.len().min(avail.len());
666 buf[..n].copy_from_slice(&avail[..n]);
667 self.prefix_pos += n;
668 return Ok(n);
669 }
670 self.inner.read(buf)
671 }
672}
673
674
675pub(crate) const POOL_MAX_IDLE: usize = 64;
679
680pub(crate) fn shared_pool() -> &'static ConnPool {
683 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
684 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
685}
686
687pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
690 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
691}
692
693impl UpstreamIo for TcpStream {
694 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
695 self.set_read_timeout(Some(read))?;
696 self.set_write_timeout(Some(write))
697 }
698}
699
700#[cfg(any(feature = "http-client", feature = "http2"))]
701pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
702
703#[cfg(any(feature = "http-client", feature = "http2"))]
704impl UpstreamIo for TlsStream {
705 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
706 self.sock.set_io_timeouts(read, write)
707 }
708}
709
710pub(crate) fn forward_upstream(
716 plain_pool: &ConnPool,
717 request: &Request,
718 client: Peer<'_>,
719 host: &str,
720 port: u16,
721 tls: bool,
722 connect_timeout: Duration,
723 read_timeout: Duration,
724) -> Result<Response, String> {
725 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
726}
727
728#[allow(clippy::too_many_arguments)]
731pub(crate) fn forward_upstream_trusting(
732 plain_pool: &ConnPool,
733 request: &Request,
734 client: Peer<'_>,
735 host: &str,
736 port: u16,
737 tls: bool,
738 trust: Option<&UpstreamTls>,
739 connect_timeout: Duration,
740 read_timeout: Duration,
741) -> Result<Response, String> {
742 forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
743}
744
745#[allow(clippy::too_many_arguments)]
749pub(crate) fn forward_upstream_as(
750 plain_pool: &ConnPool,
751 request: &Request,
752 client: Peer<'_>,
753 host: &str,
754 port: u16,
755 host_header: &str,
756 tls: bool,
757 trust: Option<&UpstreamTls>,
758 connect_timeout: Duration,
759 read_timeout: Duration,
760) -> Result<Response, String> {
761 #[cfg(not(target_arch = "wasm32"))]
764 let span = crate::otel::outbound_span(&request.method);
765 #[cfg(not(target_arch = "wasm32"))]
766 if let Some(span) = &span {
767 let scheme = if tls { "https" } else { "http" };
768 let path = request.request_uri.as_str();
769 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
770 span.set_attribute(key, value);
771 }
772 }
773 let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
774 #[cfg(not(target_arch = "wasm32"))]
775 if let Some(span) = &span {
776 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
777 }
778 result
779}
780
781#[allow(clippy::too_many_arguments)]
782fn forward_upstream_untraced(
783 plain_pool: &ConnPool,
784 request: &Request,
785 client: Peer<'_>,
786 host: &str,
787 port: u16,
788 host_header: &str,
789 tls: bool,
790 trust: Option<&UpstreamTls>,
791 connect_timeout: Duration,
792 read_timeout: Duration,
793) -> Result<Response, String> {
794 if tls {
795 #[cfg(any(feature = "http-client", feature = "http2"))]
796 {
797 let (pool, config) = match trust {
798 Some(t) => (t.pool, Arc::clone(&t.config)),
799 None => (shared_tls_pool(), tls_client_config()),
800 };
801 return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
802 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
803 });
804 }
805 #[cfg(not(any(feature = "http-client", feature = "http2")))]
806 {
807 let _ = trust;
808 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
809 }
810 }
811 forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
812}
813
814pub struct UpstreamTls {
822 #[cfg(any(feature = "http-client", feature = "http2"))]
823 config: Arc<rustls::ClientConfig>,
824 #[cfg(any(feature = "http-client", feature = "http2"))]
825 pool: &'static Pool<TlsStream>,
826 #[cfg(feature = "http2")]
828 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
829 #[cfg(feature = "http2")]
831 h2_config: Arc<rustls::ClientConfig>,
832}
833
834impl UpstreamTls {
835 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
837 UpstreamTls::new(Some(path), None)
838 }
839
840 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
844 #[cfg(any(feature = "http-client", feature = "http2"))]
845 {
846 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
847 let roots = match ca_file {
848 Some(path) => {
849 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
850 let certs = CertificateDer::pem_slice_iter(&bytes)
851 .collect::<Result<Vec<_>, _>>()
852 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
853 if certs.is_empty() {
854 return Err(format!("no certificates in CA file '{path}'"));
855 }
856 let mut roots = rustls::RootCertStore::empty();
857 for cert in certs {
858 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
859 }
860 roots
861 }
862 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
863 };
864 let builder = crate::tls_client_builder().with_root_certificates(roots);
865 let config = match client_cert {
866 None => builder.with_no_client_auth(),
867 Some((cert_path, key_path)) => {
868 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
869 let chain = CertificateDer::pem_slice_iter(&bytes)
870 .collect::<Result<Vec<_>, _>>()
871 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
872 if chain.is_empty() {
873 return Err(format!("no certificate in '{cert_path}'"));
874 }
875 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
876 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
877 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
878 }
879 };
880 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
881 Ok(UpstreamTls::with_config(config, &label))
882 }
883 #[cfg(not(any(feature = "http-client", feature = "http2")))]
884 {
885 let _ = client_cert;
886 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
887 }
888 }
889
890 #[cfg(any(feature = "http-client", feature = "http2"))]
891 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
892 #[cfg(feature = "http2")]
893 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
894 UpstreamTls {
895 config: Arc::new(config),
896 pool: tls_pool_for_ca(label),
897 #[cfg(feature = "http2")]
898 async_pool: async_tls_pool_for_ca(label),
899 #[cfg(feature = "http2")]
900 h2_config,
901 }
902 }
903
904 #[cfg(feature = "http2")]
906 pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
907 &self.h2_config
908 }
909
910 pub(crate) fn reject_all(label: &str) -> UpstreamTls {
914 #[cfg(any(feature = "http-client", feature = "http2"))]
915 {
916 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
917 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
918 }
919 #[cfg(not(any(feature = "http-client", feature = "http2")))]
920 {
921 let _ = label;
922 UpstreamTls {}
923 }
924 }
925
926 #[cfg(feature = "http2")]
928 pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
929 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
930 }
931
932 #[cfg(any(feature = "http-client", feature = "http2"))]
934 pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
935 Arc::clone(&self.config)
936 }
937}
938
939#[cfg(feature = "http2")]
942fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
943 static POOLS: std::sync::OnceLock<
944 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
945 > = std::sync::OnceLock::new();
946 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
947 pools
948 .entry(path.to_string())
949 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
950}
951
952#[cfg(any(feature = "http-client", feature = "http2"))]
954fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
955 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
956 std::sync::OnceLock::new();
957 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
958 pools
959 .entry(path.to_string())
960 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
961}
962
963pub(crate) fn forward_pooled(
967 pool: &ConnPool,
968 request: &Request,
969 client: Peer<'_>,
970 host: &str,
971 port: u16,
972 connect_timeout: Duration,
973 read_timeout: Duration,
974) -> Result<Response, String> {
975 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
976}
977
978#[cfg(feature = "gateway-api")]
981#[allow(clippy::too_many_arguments)]
982pub(crate) fn forward_pooled_with_host(
983 pool: &ConnPool,
984 request: &Request,
985 client: Peer<'_>,
986 host: &str,
987 port: u16,
988 host_header: &str,
989 connect_timeout: Duration,
990 read_timeout: Duration,
991) -> Result<Response, String> {
992 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
993}
994
995#[allow(clippy::too_many_arguments)]
1002fn forward_via<S: UpstreamIo>(
1003 pool: &Pool<S>,
1004 request: &Request,
1005 client: Peer<'_>,
1006 host: &str,
1007 port: u16,
1008 host_header: &str,
1009 read_timeout: Duration,
1010 connect: impl Fn() -> Result<S, String>,
1011) -> Result<Response, String> {
1012 let key = format!("{}:{}", host, port);
1013 if let Some(stream) = pool.acquire(&key) {
1014 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1015 Exchange::Done(response) => return Ok(response),
1016 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
1018 }
1019 }
1020 let stream = connect()?;
1021 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1022 Exchange::Done(response) => Ok(response),
1023 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
1024 }
1025}
1026
1027fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
1031 let key = format!("{}:{}", host, port);
1032 let addrs: Vec<_> = key
1033 .to_socket_addrs()
1034 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
1035 .collect();
1036 let mut last_error = format!("no address resolved for {}", key);
1037 for addr in addrs {
1038 match TcpStream::connect_timeout(&addr, connect_timeout) {
1039 Ok(stream) => {
1040 let _ = stream.set_nodelay(true);
1041 return Ok(stream);
1042 }
1043 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
1044 }
1045 }
1046 Err(last_error)
1047}
1048
1049#[cfg(any(feature = "http-client", feature = "http2"))]
1051fn shared_tls_pool() -> &'static Pool<TlsStream> {
1052 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
1053 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
1054}
1055
1056#[cfg(any(feature = "http-client", feature = "http2"))]
1060pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
1061 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1062 Arc::clone(CONFIG.get_or_init(|| {
1063 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1064 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1065 }))
1066}
1067
1068#[cfg(any(feature = "http-client", feature = "http2"))]
1069fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1070 let tcp = connect_tcp(host, port, connect_timeout)?;
1071 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1072 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1073 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1074 Ok(rustls::StreamOwned::new(conn, tcp))
1075}
1076
1077fn is_idempotent(method: &str) -> bool {
1078 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1079}
1080
1081enum Exchange {
1082 Done(Response),
1083 NoResponse(String),
1085 Failed(String),
1086}
1087
1088fn exchange<S: UpstreamIo>(
1091 pool: &Pool<S>,
1092 key: &str,
1093 mut stream: S,
1094 request: &Request,
1095 host: &str,
1096 client: Peer<'_>,
1097 read_timeout: Duration,
1098) -> Exchange {
1099 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1100 return Exchange::Failed(e.to_string());
1101 }
1102 let req_bytes = build_request(request, host, client, true);
1104 if let Err(e) = stream.write_all(&req_bytes) {
1105 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1106 }
1107
1108 let mut tmp = [0u8; 4096];
1109 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1110 Ok(head) => head,
1111 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1112 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1113 };
1114 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1115
1116 if response_has_no_body(&request.method, &header_lower) {
1117 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1118 pool.release(key, stream);
1119 }
1120 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1121 Ok(response) => Exchange::Done(response),
1122 Err(e) => Exchange::Failed(e),
1123 };
1124 }
1125
1126 if should_stream_response(&header_lower) {
1127 return match parse_status_and_headers(&header_bytes) {
1130 Ok(mut response) => {
1131 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1132 Exchange::Done(response)
1133 }
1134 Err(e) => Exchange::Failed(e),
1135 };
1136 }
1137 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1139 Ok((resp_bytes, reusable)) => {
1140 if reusable {
1141 pool.release(key, stream);
1142 }
1143 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1144 Ok(response) => Exchange::Done(response),
1145 Err(e) => Exchange::Failed(e),
1146 }
1147 }
1148 Err(e) => Exchange::Failed(e),
1149 }
1150}
1151
1152#[cfg(test)]
1157fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1158 read_response_head(stream, tmp).map_err(|e| match e {
1159 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1160 })
1161}
1162
1163enum HeadError {
1167 NothingReceived(String),
1168 Partial(String),
1169}
1170
1171fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1172 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1173}
1174
1175fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1181 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1182 let mut interim = 0;
1183 while is_interim_response(&head) {
1184 crate::early_hints::forward_upstream(&head);
1185 interim += 1;
1186 if interim > MAX_INTERIM_RESPONSES {
1187 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1188 }
1189 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1190 }
1191 Ok((head, rest))
1192}
1193
1194fn read_response_head_from<S: Read>(
1198 stream: &mut S,
1199 tmp: &mut [u8],
1200 mut buf: Vec<u8>,
1201 received: bool,
1202) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1203 loop {
1204 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1205 let body_prefix = buf[pos + 4..].to_vec();
1206 buf.truncate(pos + 4);
1207 return Ok((buf, body_prefix));
1208 }
1209 let nothing = buf.is_empty() && !received;
1210 let n = match stream.read(tmp) {
1211 Ok(n) => n,
1212 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1213 Err(e) => return Err(HeadError::Partial(e.to_string())),
1214 };
1215 if n == 0 {
1216 let msg = "backend closed connection before headers were complete".to_string();
1217 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1218 }
1219 buf.extend_from_slice(&tmp[..n]);
1220 }
1221}
1222
1223pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1225
1226pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1229 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1230 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1231 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1232}
1233
1234pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1238 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1239 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1240}
1241
1242pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1249 let is_sse = header_lower.lines().any(|l| {
1250 l.starts_with("content-type:") && l.contains("text/event-stream")
1251 });
1252 let is_chunked = header_lower.lines().any(|l| {
1253 l.starts_with("transfer-encoding:") && l.contains("chunked")
1254 });
1255 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1256 l.strip_prefix("content-length:")?.trim().parse().ok()
1257 });
1258 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1259 is_sse || is_chunked || is_large
1260}
1261
1262#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1268pub(crate) struct HeadFacts {
1269 pub(crate) status_no_body: bool,
1271 pub(crate) sse: bool,
1272 pub(crate) chunked: bool,
1273 pub(crate) content_length: Option<usize>,
1275 pub(crate) connection_close: bool,
1276}
1277
1278#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1279impl HeadFacts {
1280 pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1281 let text = std::str::from_utf8(head).unwrap_or("");
1282 let status = text.split_whitespace().nth(1).unwrap_or("");
1283 let mut facts = HeadFacts {
1284 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1285 sse: false,
1286 chunked: false,
1287 content_length: None,
1288 connection_close: false,
1289 };
1290 for line in text.lines() {
1291 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1292 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1293 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1294 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1295 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1296 if facts.content_length.is_none() {
1297 facts.content_length = value.trim().parse().ok();
1298 }
1299 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1300 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1301 }
1302 }
1303 facts
1304 }
1305
1306 pub(crate) fn no_body(&self, method: &str) -> bool {
1308 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1309 }
1310
1311 pub(crate) fn should_stream(&self) -> bool {
1313 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1314 }
1315}
1316
1317fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1318 let bytes = line.as_bytes();
1319 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1321 .then(|| &line[prefix.len()..])
1322}
1323
1324fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1325 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1326}
1327
1328fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1330 let s = std::str::from_utf8(header_bytes)
1331 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1332 let mut lines = s.lines();
1333 let status_line = lines.next().ok_or("empty backend response")?;
1334 let mut parts = status_line.splitn(3, ' ');
1335 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1336 let status_code: i16 = parts
1337 .next()
1338 .unwrap_or("502")
1339 .parse()
1340 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1341 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1342 let mut headers = Vec::new();
1343 for line in lines {
1344 let line = line.trim_end_matches('\r');
1345 if line.is_empty() { break; }
1346 if let Some(colon) = line.find(':') {
1347 headers.push(crate::header::Header {
1348 name: line[..colon].trim().to_string().into(),
1349 value: (line[colon + 1..].trim().to_string()).into(),
1350 });
1351 }
1352 }
1353 Ok(Response {
1354 http_version,
1355 status_code,
1356 reason_phrase,
1357 headers,
1358 content_range_list: vec![],
1359 stream_file: None,
1360 stream_pipe: None,
1361 trailers: None,
1362 })
1363}
1364
1365fn read_response_from_partial<S: Read>(
1371 stream: &mut S,
1372 header_bytes: Vec<u8>,
1373 body_prefix: Vec<u8>,
1374 tmp: &mut [u8],
1375) -> Result<(Vec<u8>, bool), String> {
1376 let header_end = header_bytes.len();
1377 let mut buf = header_bytes;
1378 buf.extend_from_slice(&body_prefix);
1379
1380 let header_lower =
1381 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1382 let connection_close =
1383 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1384 let is_chunked = header_lower
1385 .lines()
1386 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1387 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1388 l.strip_prefix("content-length:")?.trim().parse().ok()
1389 });
1390
1391 if is_chunked {
1392 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1393 rewrite_as_content_length(&mut buf, header_end, &decoded);
1394 Ok((buf, !connection_close))
1395 } else if let Some(len) = content_length {
1396 while buf.len() < header_end + len {
1397 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1398 if n == 0 { break; }
1399 buf.extend_from_slice(&tmp[..n]);
1400 }
1401 Ok((buf, !connection_close))
1402 } else {
1403 loop {
1404 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1405 if n == 0 { break; }
1406 buf.extend_from_slice(&tmp[..n]);
1407 }
1408 Ok((buf, false))
1409 }
1410}
1411
1412struct Backend {
1415 host: String,
1416 port: u16,
1417 tls: bool,
1420}
1421
1422impl Backend {
1423 fn parse(url: &str) -> Option<Self> {
1424 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1425 (r, true, 443u16)
1426 } else if let Some(r) = url.strip_prefix("h2s://") {
1427 (r, true, 443u16)
1428 } else if let Some(r) = url.strip_prefix("grpcs://") {
1429 (r, true, 443u16)
1430 } else if let Some(r) = url.strip_prefix("http://") {
1431 (r, false, 80u16)
1432 } else if let Some(r) = url.strip_prefix("h2://") {
1433 (r, false, 80u16)
1434 } else if let Some(r) = url.strip_prefix("grpc://") {
1435 (r, false, 80u16)
1436 } else {
1437 (url, false, 80u16)
1438 };
1439 let host_port = rest.split('/').next().unwrap_or(rest);
1441 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1442 let port_str = &host_port[colon + 1..];
1443 if let Ok(p) = port_str.parse::<u16>() {
1444 (host_port[..colon].to_string(), p)
1445 } else {
1446 (host_port.to_string(), default_port)
1447 }
1448 } else {
1449 (host_port.to_string(), default_port)
1450 };
1451 if host.is_empty() {
1452 return None;
1453 }
1454 Some(Backend { host, port, tls })
1455 }
1456}
1457
1458#[cfg(feature = "http2")]
1469pub struct H2ReverseProxy {
1470 inner: ReverseProxy,
1471}
1472
1473#[cfg(feature = "http2")]
1474impl H2ReverseProxy {
1475 pub fn new<I, S>(backends: I) -> Self
1486 where
1487 I: IntoIterator<Item = S>,
1488 S: AsRef<str>,
1489 {
1490 H2ReverseProxy {
1491 inner: ReverseProxy::new(backends),
1492 }
1493 }
1494
1495 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1497 self.inner = self.inner.path_prefix(prefix);
1498 self
1499 }
1500
1501 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1503 self.inner = self.inner.connect_timeout_ms(ms);
1504 self
1505 }
1506
1507 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1512 self.inner = self.inner.read_timeout_ms(ms);
1513 self
1514 }
1515
1516 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1519 self.inner = self.inner.tls_ca_file(path)?;
1520 Ok(self)
1521 }
1522
1523 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1526 self.inner = self.inner.tls_client_cert(cert, key)?;
1527 Ok(self)
1528 }
1529
1530 fn matches(&self, request: &Request) -> bool {
1531 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1532 }
1533}
1534
1535#[cfg(feature = "http2")]
1536impl crate::middleware::Middleware for H2ReverseProxy {
1537 fn handle(
1538 &self,
1539 request: &crate::request::Request,
1540 connection: &crate::server::ConnectionInfo,
1541 next: &dyn crate::application::Application,
1542 ) -> Result<crate::response::Response, String> {
1543 if !self.matches(request) {
1544 return next.execute(request, connection);
1545 }
1546 if self.inner.backends.is_empty() {
1547 let mut response = bad_gateway();
1548 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1549 return Ok(response);
1550 }
1551 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1553 let n = self.inner.backends.len();
1554 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1555 let mut last_error = String::new();
1556 for attempt in 0..attempts {
1557 let backend = &self.inner.backends[(start + attempt) % n];
1558 let tls = backend.tls.then(|| match &self.inner.trust {
1559 Some(trust) => trust.h2_config(),
1560 None => h2_upstream::default_tls(),
1561 });
1562 let target = h2_upstream::Target {
1563 backend,
1564 tls,
1565 connect_timeout: self.inner.connect_timeout,
1566 read_timeout: self.inner.read_timeout,
1567 authority: None,
1568 };
1569 match h2_upstream::forward(request, Peer::of(connection), &target) {
1570 Ok(mut response) => {
1571 status::received(&mut response, None);
1572 return Ok(response);
1573 }
1574 Err(e) => last_error = e,
1575 }
1576 }
1577 let mut response = bad_gateway();
1578 status::failed_with(&mut response, &last_error, None);
1579 Ok(response)
1580 }
1581
1582 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1585 true
1586 }
1587
1588 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1591 self.matches(request)
1592 }
1593}
1594
1595#[cfg(feature = "http3")]
1617pub struct H3ReverseProxy {
1618 inner: ReverseProxy,
1619 name: String,
1620 tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1622}
1623
1624#[cfg(feature = "http3")]
1625impl H3ReverseProxy {
1626 pub fn new<I, S>(backends: I) -> Self
1632 where
1633 I: IntoIterator<Item = S>,
1634 S: AsRef<str>,
1635 {
1636 H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1637 }
1638
1639 pub fn name(mut self, name: impl Into<String>) -> Self {
1641 self.name = name.into();
1642 self
1643 }
1644
1645 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1647 self.inner = self.inner.path_prefix(prefix);
1648 self
1649 }
1650
1651 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1653 self.inner = self.inner.connect_timeout_ms(ms);
1654 self
1655 }
1656
1657 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1660 self.inner = self.inner.read_timeout_ms(ms);
1661 self
1662 }
1663
1664 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1667 self.inner = self.inner.tls_ca_file(path)?;
1668 Ok(self)
1669 }
1670
1671 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1673 self.inner = self.inner.tls_client_cert(cert, key)?;
1674 Ok(self)
1675 }
1676
1677 fn matches(&self, request: &Request) -> bool {
1678 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1679 }
1680
1681 fn on_tcp(&self, key: &str) -> bool {
1682 let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1683 match tcp_until.get(key) {
1684 Some(until) if std::time::Instant::now() < *until => true,
1685 Some(_) => {
1686 tcp_until.remove(key);
1687 false
1688 }
1689 None => false,
1690 }
1691 }
1692
1693 fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1696 let key = format!("{}:{}", backend.host, backend.port);
1697 if backend.tls && !self.on_tcp(&key) {
1698 let config = match &self.inner.trust {
1699 Some(trust) => trust.client_config(),
1700 None => tls_client_config(),
1701 };
1702 let target = h3_upstream::Target {
1703 host: &backend.host,
1704 port: backend.port,
1705 authority: None,
1706 tls: &config,
1707 connect_timeout: self.inner.connect_timeout,
1708 read_timeout: self.inner.read_timeout,
1709 };
1710 match h3_upstream::forward(request, Peer::of(connection), &target) {
1711 Err(h3_upstream::H3Error::Connect(e)) => {
1712 self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1713 crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1714 eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1715 }
1716 result => return result.map_err(|e| e.to_string()),
1717 }
1718 }
1719 forward_upstream_trusting(
1720 &self.inner.pool,
1721 request,
1722 Peer::of(connection),
1723 &backend.host,
1724 backend.port,
1725 backend.tls,
1726 self.inner.trust.as_deref(),
1727 self.inner.connect_timeout,
1728 self.inner.read_timeout,
1729 )
1730 }
1731}
1732
1733#[cfg(feature = "http3")]
1734impl crate::middleware::Middleware for H3ReverseProxy {
1735 fn handle(
1736 &self,
1737 request: &crate::request::Request,
1738 connection: &crate::server::ConnectionInfo,
1739 next: &dyn crate::application::Application,
1740 ) -> Result<crate::response::Response, String> {
1741 if !self.matches(request) {
1742 return next.execute(request, connection);
1743 }
1744 if self.inner.backends.is_empty() {
1745 let mut response = bad_gateway();
1746 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1747 return Ok(response);
1748 }
1749 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1751 let n = self.inner.backends.len();
1752 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1753 let mut last_error = String::new();
1754 for attempt in 0..attempts {
1755 match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1756 Ok(mut response) => {
1757 status::received(&mut response, None);
1758 return Ok(response);
1759 }
1760 Err(e) => last_error = e,
1761 }
1762 }
1763 let mut response = bad_gateway();
1764 status::failed_with(&mut response, &last_error, None);
1765 Ok(response)
1766 }
1767
1768 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1769 true
1770 }
1771
1772 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1774 self.matches(request)
1775 }
1776}
1777
1778#[cfg(feature = "http2")]
1782pub(crate) fn forward_h2(
1783 request: &Request,
1784 client: Peer<'_>,
1785 url: &str,
1786 tls: bool,
1787 trust: Option<&UpstreamTls>,
1788 connect_timeout: Duration,
1789 read_timeout: Duration,
1790) -> Result<Response, String> {
1791 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1792 backend.tls |= tls;
1793 let tls = backend.tls.then(|| match trust {
1794 Some(t) => t.h2_config(),
1795 None => h2_upstream::default_tls(),
1796 });
1797 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1798}
1799
1800#[cfg(feature = "gateway-api")]
1805#[allow(clippy::too_many_arguments)]
1806pub(crate) fn forward_tls_with_host(
1807 config: &Arc<rustls::ClientConfig>,
1808 label: &str,
1809 request: &Request,
1810 client: Peer<'_>,
1811 ip: &str,
1812 port: u16,
1813 server_name: &str,
1814 host_header: &str,
1815 connect_timeout: Duration,
1816 read_timeout: Duration,
1817) -> Result<Response, String> {
1818 let pool = tls_pool_for_ca(label);
1819 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1820 let tcp = connect_tcp(ip, port, connect_timeout)?;
1821 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1822 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1823 Ok(rustls::StreamOwned::new(conn, tcp))
1824 })
1825}
1826
1827#[cfg(feature = "gateway-api")]
1831pub(crate) fn forward_h2c_with_host(
1832 request: &Request,
1833 client: Peer<'_>,
1834 host: &str,
1835 port: u16,
1836 authority: &str,
1837 connect_timeout: Duration,
1838 read_timeout: Duration,
1839) -> Result<Response, String> {
1840 let backend = Backend { host: host.to_string(), port, tls: false };
1841 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1842}
1843
1844#[cfg(feature = "http2")]
1864pub struct GrpcProxy {
1865 inner: H2ReverseProxy,
1866}
1867
1868#[cfg(feature = "http2")]
1869impl GrpcProxy {
1870 pub fn new<I, S>(backends: I) -> Self
1880 where
1881 I: IntoIterator<Item = S>,
1882 S: AsRef<str>,
1883 {
1884 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1885 }
1886
1887 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1889 self.inner = self.inner.path_prefix(prefix);
1890 self
1891 }
1892
1893 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1895 self.inner = self.inner.connect_timeout_ms(ms);
1896 self
1897 }
1898
1899 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1901 self.inner = self.inner.read_timeout_ms(ms);
1902 self
1903 }
1904
1905 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1907 self.inner = self.inner.tls_ca_file(path)?;
1908 Ok(self)
1909 }
1910
1911 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1914 self.inner = self.inner.tls_client_cert(cert, key)?;
1915 Ok(self)
1916 }
1917}
1918
1919#[cfg(feature = "http2")]
1920impl crate::middleware::Middleware for GrpcProxy {
1921 fn handle(
1922 &self,
1923 request: &crate::request::Request,
1924 connection: &crate::server::ConnectionInfo,
1925 next: &dyn crate::application::Application,
1926 ) -> Result<crate::response::Response, String> {
1927 if is_grpc(request) {
1928 self.inner.handle(request, connection, next)
1929 } else {
1930 next.execute(request, connection)
1931 }
1932 }
1933
1934 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1935 true
1936 }
1937
1938 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1939 is_grpc(request) && self.inner.matches(request)
1940 }
1941}
1942
1943#[cfg(feature = "http2")]
1944fn is_grpc(request: &Request) -> bool {
1945 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1946}
1947
1948#[cfg(test)]
1951mod backend_parse_tests {
1952 use super::Backend;
1953
1954 fn parse(url: &str) -> Option<(String, u16, bool)> {
1955 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1956 }
1957
1958 #[test]
1959 fn bare_host_port() {
1960 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1961 }
1962
1963 #[test]
1964 fn http_scheme() {
1965 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1966 }
1967
1968 #[test]
1969 fn h2_scheme_plain() {
1970 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1971 }
1972
1973 #[test]
1974 fn grpc_scheme_plain() {
1975 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1976 }
1977
1978 #[test]
1979 fn https_scheme_sets_tls_and_default_port() {
1980 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1981 }
1982
1983 #[test]
1984 fn https_scheme_explicit_port() {
1985 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1986 }
1987
1988 #[test]
1989 fn h2s_scheme_sets_tls() {
1990 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1991 }
1992
1993 #[test]
1994 fn h2s_scheme_explicit_port() {
1995 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1996 }
1997
1998 #[test]
1999 fn grpcs_scheme_sets_tls() {
2000 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
2001 }
2002
2003 #[test]
2004 fn grpcs_scheme_explicit_port() {
2005 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
2006 }
2007
2008 #[test]
2009 fn empty_host_returns_none() {
2010 assert_eq!(None, parse("https://"));
2011 }
2012
2013 #[test]
2014 fn bare_host_no_port_defaults_to_80() {
2015 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
2016 }
2017}