1pub mod pool;
27pub mod status;
28#[cfg(feature = "http2")]
29pub(crate) mod async_upstream;
30#[cfg(not(target_arch = "wasm32"))]
31pub mod mirror;
32#[cfg(feature = "http2")]
33pub(crate) mod h2_upstream;
34#[cfg(feature = "http3")]
35pub(crate) mod h3_upstream;
36#[cfg(feature = "http2")]
37pub mod grpc_web;
38#[cfg(feature = "http2")]
39pub use grpc_web::GrpcWebLayer;
40
41#[cfg(test)]
42mod tests;
43
44use std::io::{Read, Write};
45use crate::ext::decimal::Decimal;
46use std::net::{TcpStream, ToSocketAddrs};
47use std::sync::atomic::{AtomicUsize, Ordering};
48use std::sync::Arc;
49use std::time::Duration;
50
51pub use pool::{ConnPool, Pool};
52
53use crate::application::Application;
54use crate::middleware::Middleware;
55use crate::request::Request;
56use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
57use crate::server::ConnectionInfo;
58
59const HOP_BY_HOP: &[&str] = &[
63 "connection",
64 "keep-alive",
65 "proxy-authenticate",
66 "proxy-authorization",
67 "proxy-connection",
68 "te",
69 "trailer",
70 "transfer-encoding",
71 "upgrade",
72];
73
74pub enum LoadBalancing {
76 RoundRobin,
78}
79
80pub struct ReverseProxy {
95 backends: Vec<Backend>,
96 path_prefix: Option<String>,
97 connect_timeout: Duration,
98 read_timeout: Duration,
99 counter: AtomicUsize,
100 pool: Arc<ConnPool>,
101 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
102 trust: Option<Arc<UpstreamTls>>,
103 tls_ca: Option<String>,
105 tls_client: Option<(String, String)>,
106}
107
108impl ReverseProxy {
109 pub fn new<I, S>(backends: I) -> Self
113 where
114 I: IntoIterator<Item = S>,
115 S: AsRef<str>,
116 {
117 Self {
118 backends: backends
119 .into_iter()
120 .filter_map(|u| Backend::parse(u.as_ref()))
121 .collect(),
122 path_prefix: None,
123 connect_timeout: Duration::from_secs(5),
124 read_timeout: Duration::from_secs(30),
125 counter: AtomicUsize::new(0),
126 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
127 breaker: None,
128 trust: None,
129 tls_ca: None,
130 tls_client: None,
131 }
132 }
133
134 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
152 self.breaker = Some(breaker);
153 self
154 }
155
156 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
161 self.path_prefix = Some(prefix.into());
162 self
163 }
164
165 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
167 self
168 }
169
170 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
172 self.connect_timeout = Duration::from_millis(ms);
173 self
174 }
175
176 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
178 self.read_timeout = Duration::from_millis(ms);
179 self
180 }
181
182 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
187 self.pool = pool;
188 self
189 }
190
191 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
195 self.tls_ca = Some(path.to_string());
196 self.rebuild_trust()?;
197 Ok(self)
198 }
199
200 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
204 self.tls_client = Some((cert.to_string(), key.to_string()));
205 self.rebuild_trust()?;
206 Ok(self)
207 }
208
209 fn rebuild_trust(&mut self) -> Result<(), String> {
210 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
211 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
212 Ok(())
213 }
214
215 pub fn max_idle_conns(mut self, n: usize) -> Self {
217 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
218 self
219 }
220
221 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
222 if self.backends.is_empty() {
223 return Err("no backends configured".to_string());
224 }
225 let n = self.backends.len();
226 let start = self.counter.fetch_add(1, Ordering::Relaxed);
227 let mut last_err = "all backends failed".to_string();
228 for attempt in 0..n {
229 let idx = (start + attempt) % n;
230 let backend = &self.backends[idx];
231 let key = format!("{}:{}", backend.host, backend.port);
232
233 if let Some(breaker) = &self.breaker {
234 if !breaker.is_available(&key) {
235 last_err = format!("circuit open for {}", key);
236 continue;
237 }
238 }
239
240 match self.try_backend(request, connection, backend) {
241 Ok(resp) => {
242 if let Some(breaker) = &self.breaker {
243 breaker.record_success(&key);
244 }
245 return Ok(resp);
246 }
247 Err(e) => {
248 if let Some(breaker) = &self.breaker {
249 breaker.record_failure(&key);
250 }
251 last_err = e;
252 }
253 }
254 }
255 Err(last_err)
256 }
257
258 fn try_backend(
259 &self,
260 request: &Request,
261 connection: &ConnectionInfo,
262 backend: &Backend,
263 ) -> Result<Response, String> {
264 forward_upstream_trusting(
265 &self.pool,
266 request,
267 crate::proxy::Peer::of(connection),
268 &backend.host,
269 backend.port,
270 backend.tls,
271 self.trust.as_deref(),
272 self.connect_timeout,
273 self.read_timeout,
274 )
275 }
276}
277
278impl Middleware for ReverseProxy {
279 fn handle(
280 &self,
281 request: &Request,
282 connection: &ConnectionInfo,
283 next: &dyn Application,
284 ) -> Result<Response, String> {
285 if let Some(prefix) = &self.path_prefix {
286 if !request.request_uri.starts_with(prefix.as_str()) {
287 return next.execute(request, connection);
288 }
289 }
290 match self.proxy(request, connection) {
291 Ok(mut resp) => {
292 status::received(&mut resp, None);
293 Ok(resp)
294 }
295 Err(e) => {
296 let mut resp = bad_gateway();
297 status::failed_with(&mut resp, &e, None);
298 Ok(resp)
299 }
300 }
301 }
302}
303
304#[derive(Clone, Copy, Debug)]
309pub(crate) struct Peer<'a> {
310 pub ip: &'a str,
311 pub tls: bool,
312}
313
314impl<'a> Peer<'a> {
315 pub(crate) fn of(connection: &'a ConnectionInfo) -> Self {
316 Peer { ip: &connection.client.ip, tls: connection.tls }
317 }
318
319 pub(crate) fn plain(ip: &'a str) -> Self {
321 Peer { ip, tls: false }
322 }
323
324 pub(crate) fn proto(&self) -> &'static str {
325 if self.tls { "https" } else { "http" }
326 }
327}
328
329pub(crate) fn forwarded_value(request: &Request, client: Peer<'_>) -> Option<String> {
334 if !crate::server_config::ServerConfig::current().forwarded_header {
335 return None;
336 }
337 let node = match client.ip.parse::<std::net::IpAddr>() {
338 Ok(std::net::IpAddr::V4(v4)) => v4.to_string(),
339 Ok(std::net::IpAddr::V6(v6)) => match v6.to_ipv4_mapped() {
340 Some(v4) => v4.to_string(),
341 None => format!("\"[{v6}]\""),
342 },
343 Err(_) => "unknown".to_string(),
344 };
345 let mut element = format!("for={node};proto={}", client.proto());
346 if let Some(host) = request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|h: &&str| !h.is_empty() && h.bytes().all(|b| b.is_ascii_graphic() && b != b'"' && b != b'\\')) {
347 let token = host.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b));
348 if token {
349 element.push_str(&format!(";host={host}"));
350 } else {
351 element.push_str(&format!(";host=\"{host}\""));
352 }
353 }
354 let prior = request.headers.iter().filter(|h| h.name.eq_ignore_ascii_case("forwarded")).map(|h| h.value.trim()).filter(|v| !v.is_empty() && !v.bytes().any(|b| b < 0x20)).collect::<Vec<_>>();
355 Some(if prior.is_empty() { element } else { format!("{}, {element}", prior.join(", ")) })
356}
357
358pub(crate) fn build_request(
359 request: &Request,
360 backend_host: &str,
361 client: Peer<'_>,
362 keep_alive: bool,
363) -> Vec<u8> {
364 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
370 #[cfg(not(target_arch = "wasm32"))]
373 let trace = crate::otel::propagation_headers();
374 #[cfg(target_arch = "wasm32")]
375 let trace: Option<(String, Option<String>)> = None;
376 let forwarded_header = forwarded_value(request, client);
377 let forwarded = |h: &&crate::header::Header| {
378 !h.name.is_empty()
380 && !is_hop_by_hop_or_host(&h.name)
381 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
382 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
383 && !(forwarded_header.is_some() && h.name.eq_ignore_ascii_case("Forwarded"))
384 && !connection.is_some_and(|c| connection_listed(c, &h.name))
385 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
386 };
387 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
388 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
392 let forwarded_host = match request.header("X-Forwarded-Host") {
393 Some(_) => None,
394 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
395 };
396 let body_len = Decimal::new(request.body.len() as u128);
397 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
398 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
399 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
400 + forwarded_host.map_or(0, |v| v.len() + 20)
401 + forwarded_proto.map_or(0, |v| v.len() + 21)
402 + forwarded_header.as_ref().map_or(0, |v| v.len() + 13)
403 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
404 + 13
405 + 24
406 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
407 + 2
408 + request.body.len();
409 let mut out: Vec<u8> = Vec::with_capacity(capacity);
410 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
411 out.extend_from_slice(part.as_bytes());
412 }
413 for h in request.headers.iter().filter(forwarded) {
414 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
415 out.extend_from_slice(part.as_bytes());
416 }
417 }
418 out.extend_from_slice(b"X-Forwarded-For: ");
419 if let Some(prior) = prior_xff {
420 out.extend_from_slice(prior.as_bytes());
421 out.extend_from_slice(b", ");
422 }
423 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
424 out.extend_from_slice(part.as_bytes());
425 }
426 if let Some(host) = forwarded_host {
427 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
428 out.extend_from_slice(part.as_bytes());
429 }
430 }
431 if let Some(proto) = forwarded_proto {
432 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
433 out.extend_from_slice(part.as_bytes());
434 }
435 }
436 if let Some(value) = &forwarded_header {
437 for part in ["Forwarded: ", value.as_str(), "\r\n"] {
438 out.extend_from_slice(part.as_bytes());
439 }
440 }
441 if let Some((traceparent, baggage)) = &trace {
442 for part in ["traceparent: ", traceparent, "\r\n"] {
443 out.extend_from_slice(part.as_bytes());
444 }
445 if let Some(baggage) = baggage {
446 for part in ["baggage: ", baggage, "\r\n"] {
447 out.extend_from_slice(part.as_bytes());
448 }
449 }
450 }
451 if keep_alive {
452 out.extend_from_slice(b"Connection: keep-alive\r\n");
453 } else {
454 out.extend_from_slice(b"Connection: close\r\n");
455 }
456 if !request.body.is_empty() {
457 out.extend_from_slice(b"Content-Length: ");
458 out.extend_from_slice(body_len.as_str().as_bytes());
459 out.extend_from_slice(b"\r\n");
460 }
461 out.extend_from_slice(b"\r\n");
462 out.extend_from_slice(&request.body);
463 out
464}
465
466fn is_hop_by_hop_or_host(name: &str) -> bool {
471 if name.is_ascii() {
472 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
473 } else {
474 let lower = name.to_lowercase();
475 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
476 }
477}
478
479fn connection_listed(connection: &str, name: &str) -> bool {
482 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
483}
484
485fn decode_chunked<S: Read>(
490 stream: &mut S,
491 buf: &[u8],
492 header_end: usize,
493 tmp: &mut [u8],
494) -> Result<Vec<u8>, String> {
495 let mut raw: Vec<u8> = buf[header_end..].to_vec();
497 let mut decoded: Vec<u8> = Vec::new();
498
499 loop {
500 let crlf = loop {
502 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
503 break p;
504 }
505 let n = stream.read(tmp).map_err(|e| e.to_string())?;
506 if n == 0 {
507 return Err("chunked: premature EOF reading chunk size".to_string());
508 }
509 raw.extend_from_slice(&tmp[..n]);
510 };
511
512 let size_line = std::str::from_utf8(&raw[..crlf])
514 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
515 let size_str = size_line.split(';').next().unwrap_or("").trim();
516 let chunk_size = usize::from_str_radix(size_str, 16)
517 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
518 raw.drain(..crlf + 2); if chunk_size == 0 {
521 loop {
525 let line_end = loop {
526 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
527 break p;
528 }
529 let n = stream.read(tmp).map_err(|e| e.to_string())?;
530 if n == 0 {
531 return Err("chunked: premature EOF in trailer".to_string());
532 }
533 raw.extend_from_slice(&tmp[..n]);
534 };
535 raw.drain(..line_end + 2);
536 if line_end == 0 {
537 break;
538 }
539 }
540 break;
541 }
542
543 while raw.len() < chunk_size + 2 {
545 let n = stream.read(tmp).map_err(|e| e.to_string())?;
546 if n == 0 {
547 return Err("chunked: premature EOF reading chunk body".to_string());
548 }
549 raw.extend_from_slice(&tmp[..n]);
550 }
551 decoded.extend_from_slice(&raw[..chunk_size]);
552 raw.drain(..chunk_size + 2); }
554
555 Ok(decoded)
556}
557
558fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
561 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
562 buf.clear();
563 for line in header_str.lines() {
564 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
565 continue;
566 }
567 buf.extend_from_slice(line.as_bytes());
568 buf.extend_from_slice(b"\r\n");
569 }
570 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
571 buf.extend_from_slice(decoded);
572}
573
574pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
577 "content-length", "transfer-encoding", "connection", "keep-alive",
578 "proxy-connection", "te", "trailer", "upgrade",
579];
580
581#[cfg_attr(not(feature = "http2"), allow(dead_code))]
590pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
591 response.http_version.clear();
592 response.http_version.push_str(crate::http::VERSION.http_1_1);
593 let has_body = !response.content_range_list.is_empty();
594 if let [range] = response.content_range_list.as_mut_slice() {
595 if let Some(h) = response
596 .headers
597 .iter()
598 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
599 {
600 range.content_type.clear();
601 range.content_type.push_str(&h.value);
602 }
603 }
604 response.headers.retain(|h| {
607 if h.name.eq_ignore_ascii_case("content-type") {
608 return !has_body;
610 }
611 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
612 });
613 response
614}
615
616pub(crate) fn bad_gateway() -> Response {
617 crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
618}
619
620const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
628 prefix: Vec<u8>,
629 prefix_pos: usize,
630 inner: R,
631}
632
633impl<R: Read + Send> ConcatReader<R> {
634 fn new(prefix: Vec<u8>, inner: R) -> Self {
635 ConcatReader { prefix, prefix_pos: 0, inner }
636 }
637}
638
639impl<R: Read + Send> Read for ConcatReader<R> {
640 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
641 if self.prefix_pos < self.prefix.len() {
642 let avail = &self.prefix[self.prefix_pos..];
643 let n = buf.len().min(avail.len());
644 buf[..n].copy_from_slice(&avail[..n]);
645 self.prefix_pos += n;
646 return Ok(n);
647 }
648 self.inner.read(buf)
649 }
650}
651
652
653pub(crate) const POOL_MAX_IDLE: usize = 64;
657
658pub(crate) fn shared_pool() -> &'static ConnPool {
661 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
662 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
663}
664
665pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
668 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
669}
670
671impl UpstreamIo for TcpStream {
672 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
673 self.set_read_timeout(Some(read))?;
674 self.set_write_timeout(Some(write))
675 }
676}
677
678#[cfg(any(feature = "http-client", feature = "http2"))]
679pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
680
681#[cfg(any(feature = "http-client", feature = "http2"))]
682impl UpstreamIo for TlsStream {
683 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
684 self.sock.set_io_timeouts(read, write)
685 }
686}
687
688pub(crate) fn forward_upstream(
694 plain_pool: &ConnPool,
695 request: &Request,
696 client: Peer<'_>,
697 host: &str,
698 port: u16,
699 tls: bool,
700 connect_timeout: Duration,
701 read_timeout: Duration,
702) -> Result<Response, String> {
703 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
704}
705
706#[allow(clippy::too_many_arguments)]
709pub(crate) fn forward_upstream_trusting(
710 plain_pool: &ConnPool,
711 request: &Request,
712 client: Peer<'_>,
713 host: &str,
714 port: u16,
715 tls: bool,
716 trust: Option<&UpstreamTls>,
717 connect_timeout: Duration,
718 read_timeout: Duration,
719) -> Result<Response, String> {
720 forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
721}
722
723#[allow(clippy::too_many_arguments)]
727pub(crate) fn forward_upstream_as(
728 plain_pool: &ConnPool,
729 request: &Request,
730 client: Peer<'_>,
731 host: &str,
732 port: u16,
733 host_header: &str,
734 tls: bool,
735 trust: Option<&UpstreamTls>,
736 connect_timeout: Duration,
737 read_timeout: Duration,
738) -> Result<Response, String> {
739 #[cfg(not(target_arch = "wasm32"))]
742 let span = crate::otel::outbound_span(&request.method);
743 #[cfg(not(target_arch = "wasm32"))]
744 if let Some(span) = &span {
745 let scheme = if tls { "https" } else { "http" };
746 let path = request.request_uri.as_str();
747 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
748 span.set_attribute(key, value);
749 }
750 }
751 let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
752 #[cfg(not(target_arch = "wasm32"))]
753 if let Some(span) = &span {
754 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
755 }
756 result
757}
758
759#[allow(clippy::too_many_arguments)]
760fn forward_upstream_untraced(
761 plain_pool: &ConnPool,
762 request: &Request,
763 client: Peer<'_>,
764 host: &str,
765 port: u16,
766 host_header: &str,
767 tls: bool,
768 trust: Option<&UpstreamTls>,
769 connect_timeout: Duration,
770 read_timeout: Duration,
771) -> Result<Response, String> {
772 if tls {
773 #[cfg(any(feature = "http-client", feature = "http2"))]
774 {
775 let (pool, config) = match trust {
776 Some(t) => (t.pool, Arc::clone(&t.config)),
777 None => (shared_tls_pool(), tls_client_config()),
778 };
779 return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
780 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
781 });
782 }
783 #[cfg(not(any(feature = "http-client", feature = "http2")))]
784 {
785 let _ = trust;
786 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
787 }
788 }
789 forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
790}
791
792pub struct UpstreamTls {
800 #[cfg(any(feature = "http-client", feature = "http2"))]
801 config: Arc<rustls::ClientConfig>,
802 #[cfg(any(feature = "http-client", feature = "http2"))]
803 pool: &'static Pool<TlsStream>,
804 #[cfg(feature = "http2")]
806 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
807 #[cfg(feature = "http2")]
809 h2_config: Arc<rustls::ClientConfig>,
810}
811
812impl UpstreamTls {
813 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
815 UpstreamTls::new(Some(path), None)
816 }
817
818 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
822 #[cfg(any(feature = "http-client", feature = "http2"))]
823 {
824 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
825 let roots = match ca_file {
826 Some(path) => {
827 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
828 let certs = CertificateDer::pem_slice_iter(&bytes)
829 .collect::<Result<Vec<_>, _>>()
830 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
831 if certs.is_empty() {
832 return Err(format!("no certificates in CA file '{path}'"));
833 }
834 let mut roots = rustls::RootCertStore::empty();
835 for cert in certs {
836 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
837 }
838 roots
839 }
840 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
841 };
842 let builder = crate::tls_client_builder().with_root_certificates(roots);
843 let config = match client_cert {
844 None => builder.with_no_client_auth(),
845 Some((cert_path, key_path)) => {
846 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
847 let chain = CertificateDer::pem_slice_iter(&bytes)
848 .collect::<Result<Vec<_>, _>>()
849 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
850 if chain.is_empty() {
851 return Err(format!("no certificate in '{cert_path}'"));
852 }
853 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
854 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
855 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
856 }
857 };
858 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
859 Ok(UpstreamTls::with_config(config, &label))
860 }
861 #[cfg(not(any(feature = "http-client", feature = "http2")))]
862 {
863 let _ = client_cert;
864 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
865 }
866 }
867
868 #[cfg(any(feature = "http-client", feature = "http2"))]
869 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
870 #[cfg(feature = "http2")]
871 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
872 UpstreamTls {
873 config: Arc::new(config),
874 pool: tls_pool_for_ca(label),
875 #[cfg(feature = "http2")]
876 async_pool: async_tls_pool_for_ca(label),
877 #[cfg(feature = "http2")]
878 h2_config,
879 }
880 }
881
882 #[cfg(feature = "http2")]
884 pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
885 &self.h2_config
886 }
887
888 pub(crate) fn reject_all(label: &str) -> UpstreamTls {
892 #[cfg(any(feature = "http-client", feature = "http2"))]
893 {
894 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
895 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
896 }
897 #[cfg(not(any(feature = "http-client", feature = "http2")))]
898 {
899 let _ = label;
900 UpstreamTls {}
901 }
902 }
903
904 #[cfg(feature = "http2")]
906 pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
907 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
908 }
909
910 #[cfg(any(feature = "http-client", feature = "http2"))]
912 pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
913 Arc::clone(&self.config)
914 }
915}
916
917#[cfg(feature = "http2")]
920fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
921 static POOLS: std::sync::OnceLock<
922 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
923 > = std::sync::OnceLock::new();
924 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
925 pools
926 .entry(path.to_string())
927 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
928}
929
930#[cfg(any(feature = "http-client", feature = "http2"))]
932fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
933 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
934 std::sync::OnceLock::new();
935 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
936 pools
937 .entry(path.to_string())
938 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
939}
940
941pub(crate) fn forward_pooled(
945 pool: &ConnPool,
946 request: &Request,
947 client: Peer<'_>,
948 host: &str,
949 port: u16,
950 connect_timeout: Duration,
951 read_timeout: Duration,
952) -> Result<Response, String> {
953 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
954}
955
956#[cfg(feature = "gateway-api")]
959#[allow(clippy::too_many_arguments)]
960pub(crate) fn forward_pooled_with_host(
961 pool: &ConnPool,
962 request: &Request,
963 client: Peer<'_>,
964 host: &str,
965 port: u16,
966 host_header: &str,
967 connect_timeout: Duration,
968 read_timeout: Duration,
969) -> Result<Response, String> {
970 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
971}
972
973#[allow(clippy::too_many_arguments)]
980fn forward_via<S: UpstreamIo>(
981 pool: &Pool<S>,
982 request: &Request,
983 client: Peer<'_>,
984 host: &str,
985 port: u16,
986 host_header: &str,
987 read_timeout: Duration,
988 connect: impl Fn() -> Result<S, String>,
989) -> Result<Response, String> {
990 let key = format!("{}:{}", host, port);
991 if let Some(stream) = pool.acquire(&key) {
992 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
993 Exchange::Done(response) => return Ok(response),
994 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
996 }
997 }
998 let stream = connect()?;
999 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1000 Exchange::Done(response) => Ok(response),
1001 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
1002 }
1003}
1004
1005fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
1009 let key = format!("{}:{}", host, port);
1010 let addrs: Vec<_> = key
1011 .to_socket_addrs()
1012 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
1013 .collect();
1014 let mut last_error = format!("no address resolved for {}", key);
1015 for addr in addrs {
1016 match TcpStream::connect_timeout(&addr, connect_timeout) {
1017 Ok(stream) => {
1018 let _ = stream.set_nodelay(true);
1019 return Ok(stream);
1020 }
1021 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
1022 }
1023 }
1024 Err(last_error)
1025}
1026
1027#[cfg(any(feature = "http-client", feature = "http2"))]
1029fn shared_tls_pool() -> &'static Pool<TlsStream> {
1030 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
1031 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
1032}
1033
1034#[cfg(any(feature = "http-client", feature = "http2"))]
1038pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
1039 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1040 Arc::clone(CONFIG.get_or_init(|| {
1041 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1042 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1043 }))
1044}
1045
1046#[cfg(any(feature = "http-client", feature = "http2"))]
1047fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1048 let tcp = connect_tcp(host, port, connect_timeout)?;
1049 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1050 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1051 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1052 Ok(rustls::StreamOwned::new(conn, tcp))
1053}
1054
1055fn is_idempotent(method: &str) -> bool {
1056 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1057}
1058
1059enum Exchange {
1060 Done(Response),
1061 NoResponse(String),
1063 Failed(String),
1064}
1065
1066fn exchange<S: UpstreamIo>(
1069 pool: &Pool<S>,
1070 key: &str,
1071 mut stream: S,
1072 request: &Request,
1073 host: &str,
1074 client: Peer<'_>,
1075 read_timeout: Duration,
1076) -> Exchange {
1077 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1078 return Exchange::Failed(e.to_string());
1079 }
1080 let req_bytes = build_request(request, host, client, true);
1082 if let Err(e) = stream.write_all(&req_bytes) {
1083 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1084 }
1085
1086 let mut tmp = [0u8; 4096];
1087 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1088 Ok(head) => head,
1089 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1090 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1091 };
1092 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1093
1094 if response_has_no_body(&request.method, &header_lower) {
1095 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1096 pool.release(key, stream);
1097 }
1098 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1099 Ok(response) => Exchange::Done(response),
1100 Err(e) => Exchange::Failed(e),
1101 };
1102 }
1103
1104 if should_stream_response(&header_lower) {
1105 return match parse_status_and_headers(&header_bytes) {
1108 Ok(mut response) => {
1109 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1110 Exchange::Done(response)
1111 }
1112 Err(e) => Exchange::Failed(e),
1113 };
1114 }
1115 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1117 Ok((resp_bytes, reusable)) => {
1118 if reusable {
1119 pool.release(key, stream);
1120 }
1121 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1122 Ok(response) => Exchange::Done(response),
1123 Err(e) => Exchange::Failed(e),
1124 }
1125 }
1126 Err(e) => Exchange::Failed(e),
1127 }
1128}
1129
1130#[cfg(test)]
1135fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1136 read_response_head(stream, tmp).map_err(|e| match e {
1137 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1138 })
1139}
1140
1141enum HeadError {
1145 NothingReceived(String),
1146 Partial(String),
1147}
1148
1149fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1150 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1151}
1152
1153fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1159 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1160 let mut interim = 0;
1161 while is_interim_response(&head) {
1162 crate::early_hints::forward_upstream(&head);
1163 interim += 1;
1164 if interim > MAX_INTERIM_RESPONSES {
1165 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1166 }
1167 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1168 }
1169 Ok((head, rest))
1170}
1171
1172fn read_response_head_from<S: Read>(
1176 stream: &mut S,
1177 tmp: &mut [u8],
1178 mut buf: Vec<u8>,
1179 received: bool,
1180) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1181 loop {
1182 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1183 let body_prefix = buf[pos + 4..].to_vec();
1184 buf.truncate(pos + 4);
1185 return Ok((buf, body_prefix));
1186 }
1187 let nothing = buf.is_empty() && !received;
1188 let n = match stream.read(tmp) {
1189 Ok(n) => n,
1190 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1191 Err(e) => return Err(HeadError::Partial(e.to_string())),
1192 };
1193 if n == 0 {
1194 let msg = "backend closed connection before headers were complete".to_string();
1195 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1196 }
1197 buf.extend_from_slice(&tmp[..n]);
1198 }
1199}
1200
1201pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1203
1204pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1207 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1208 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1209 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1210}
1211
1212pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1216 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1217 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1218}
1219
1220pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1227 let is_sse = header_lower.lines().any(|l| {
1228 l.starts_with("content-type:") && l.contains("text/event-stream")
1229 });
1230 let is_chunked = header_lower.lines().any(|l| {
1231 l.starts_with("transfer-encoding:") && l.contains("chunked")
1232 });
1233 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1234 l.strip_prefix("content-length:")?.trim().parse().ok()
1235 });
1236 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1237 is_sse || is_chunked || is_large
1238}
1239
1240#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1246pub(crate) struct HeadFacts {
1247 pub(crate) status_no_body: bool,
1249 pub(crate) sse: bool,
1250 pub(crate) chunked: bool,
1251 pub(crate) content_length: Option<usize>,
1253 pub(crate) connection_close: bool,
1254}
1255
1256#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1257impl HeadFacts {
1258 pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1259 let text = std::str::from_utf8(head).unwrap_or("");
1260 let status = text.split_whitespace().nth(1).unwrap_or("");
1261 let mut facts = HeadFacts {
1262 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1263 sse: false,
1264 chunked: false,
1265 content_length: None,
1266 connection_close: false,
1267 };
1268 for line in text.lines() {
1269 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1270 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1271 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1272 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1273 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1274 if facts.content_length.is_none() {
1275 facts.content_length = value.trim().parse().ok();
1276 }
1277 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1278 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1279 }
1280 }
1281 facts
1282 }
1283
1284 pub(crate) fn no_body(&self, method: &str) -> bool {
1286 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1287 }
1288
1289 pub(crate) fn should_stream(&self) -> bool {
1291 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1292 }
1293}
1294
1295fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1296 let bytes = line.as_bytes();
1297 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1299 .then(|| &line[prefix.len()..])
1300}
1301
1302fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1303 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1304}
1305
1306fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1308 let s = std::str::from_utf8(header_bytes)
1309 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1310 let mut lines = s.lines();
1311 let status_line = lines.next().ok_or("empty backend response")?;
1312 let mut parts = status_line.splitn(3, ' ');
1313 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1314 let status_code: i16 = parts
1315 .next()
1316 .unwrap_or("502")
1317 .parse()
1318 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1319 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1320 let mut headers = Vec::new();
1321 for line in lines {
1322 let line = line.trim_end_matches('\r');
1323 if line.is_empty() { break; }
1324 if let Some(colon) = line.find(':') {
1325 headers.push(crate::header::Header {
1326 name: line[..colon].trim().to_string().into(),
1327 value: (line[colon + 1..].trim().to_string()).into(),
1328 });
1329 }
1330 }
1331 Ok(Response {
1332 http_version,
1333 status_code,
1334 reason_phrase,
1335 headers,
1336 content_range_list: vec![],
1337 stream_file: None,
1338 stream_pipe: None,
1339 trailers: None,
1340 })
1341}
1342
1343fn read_response_from_partial<S: Read>(
1349 stream: &mut S,
1350 header_bytes: Vec<u8>,
1351 body_prefix: Vec<u8>,
1352 tmp: &mut [u8],
1353) -> Result<(Vec<u8>, bool), String> {
1354 let header_end = header_bytes.len();
1355 let mut buf = header_bytes;
1356 buf.extend_from_slice(&body_prefix);
1357
1358 let header_lower =
1359 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1360 let connection_close =
1361 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1362 let is_chunked = header_lower
1363 .lines()
1364 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1365 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1366 l.strip_prefix("content-length:")?.trim().parse().ok()
1367 });
1368
1369 if is_chunked {
1370 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1371 rewrite_as_content_length(&mut buf, header_end, &decoded);
1372 Ok((buf, !connection_close))
1373 } else if let Some(len) = content_length {
1374 while buf.len() < header_end + len {
1375 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1376 if n == 0 { break; }
1377 buf.extend_from_slice(&tmp[..n]);
1378 }
1379 Ok((buf, !connection_close))
1380 } else {
1381 loop {
1382 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1383 if n == 0 { break; }
1384 buf.extend_from_slice(&tmp[..n]);
1385 }
1386 Ok((buf, false))
1387 }
1388}
1389
1390struct Backend {
1393 host: String,
1394 port: u16,
1395 tls: bool,
1398}
1399
1400impl Backend {
1401 fn parse(url: &str) -> Option<Self> {
1402 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1403 (r, true, 443u16)
1404 } else if let Some(r) = url.strip_prefix("h2s://") {
1405 (r, true, 443u16)
1406 } else if let Some(r) = url.strip_prefix("grpcs://") {
1407 (r, true, 443u16)
1408 } else if let Some(r) = url.strip_prefix("http://") {
1409 (r, false, 80u16)
1410 } else if let Some(r) = url.strip_prefix("h2://") {
1411 (r, false, 80u16)
1412 } else if let Some(r) = url.strip_prefix("grpc://") {
1413 (r, false, 80u16)
1414 } else {
1415 (url, false, 80u16)
1416 };
1417 let host_port = rest.split('/').next().unwrap_or(rest);
1419 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1420 let port_str = &host_port[colon + 1..];
1421 if let Ok(p) = port_str.parse::<u16>() {
1422 (host_port[..colon].to_string(), p)
1423 } else {
1424 (host_port.to_string(), default_port)
1425 }
1426 } else {
1427 (host_port.to_string(), default_port)
1428 };
1429 if host.is_empty() {
1430 return None;
1431 }
1432 Some(Backend { host, port, tls })
1433 }
1434}
1435
1436#[cfg(feature = "http2")]
1447pub struct H2ReverseProxy {
1448 inner: ReverseProxy,
1449}
1450
1451#[cfg(feature = "http2")]
1452impl H2ReverseProxy {
1453 pub fn new<I, S>(backends: I) -> Self
1464 where
1465 I: IntoIterator<Item = S>,
1466 S: AsRef<str>,
1467 {
1468 H2ReverseProxy {
1469 inner: ReverseProxy::new(backends),
1470 }
1471 }
1472
1473 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1475 self.inner = self.inner.path_prefix(prefix);
1476 self
1477 }
1478
1479 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1481 self.inner = self.inner.connect_timeout_ms(ms);
1482 self
1483 }
1484
1485 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1490 self.inner = self.inner.read_timeout_ms(ms);
1491 self
1492 }
1493
1494 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1497 self.inner = self.inner.tls_ca_file(path)?;
1498 Ok(self)
1499 }
1500
1501 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1504 self.inner = self.inner.tls_client_cert(cert, key)?;
1505 Ok(self)
1506 }
1507
1508 fn matches(&self, request: &Request) -> bool {
1509 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1510 }
1511}
1512
1513#[cfg(feature = "http2")]
1514impl crate::middleware::Middleware for H2ReverseProxy {
1515 fn handle(
1516 &self,
1517 request: &crate::request::Request,
1518 connection: &crate::server::ConnectionInfo,
1519 next: &dyn crate::application::Application,
1520 ) -> Result<crate::response::Response, String> {
1521 if !self.matches(request) {
1522 return next.execute(request, connection);
1523 }
1524 if self.inner.backends.is_empty() {
1525 let mut response = bad_gateway();
1526 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1527 return Ok(response);
1528 }
1529 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1531 let n = self.inner.backends.len();
1532 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1533 let mut last_error = String::new();
1534 for attempt in 0..attempts {
1535 let backend = &self.inner.backends[(start + attempt) % n];
1536 let tls = backend.tls.then(|| match &self.inner.trust {
1537 Some(trust) => trust.h2_config(),
1538 None => h2_upstream::default_tls(),
1539 });
1540 let target = h2_upstream::Target {
1541 backend,
1542 tls,
1543 connect_timeout: self.inner.connect_timeout,
1544 read_timeout: self.inner.read_timeout,
1545 authority: None,
1546 };
1547 match h2_upstream::forward(request, Peer::of(connection), &target) {
1548 Ok(mut response) => {
1549 status::received(&mut response, None);
1550 return Ok(response);
1551 }
1552 Err(e) => last_error = e,
1553 }
1554 }
1555 let mut response = bad_gateway();
1556 status::failed_with(&mut response, &last_error, None);
1557 Ok(response)
1558 }
1559
1560 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1563 true
1564 }
1565
1566 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1569 self.matches(request)
1570 }
1571}
1572
1573#[cfg(feature = "http3")]
1595pub struct H3ReverseProxy {
1596 inner: ReverseProxy,
1597 name: String,
1598 tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1600}
1601
1602#[cfg(feature = "http3")]
1603impl H3ReverseProxy {
1604 pub fn new<I, S>(backends: I) -> Self
1610 where
1611 I: IntoIterator<Item = S>,
1612 S: AsRef<str>,
1613 {
1614 H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1615 }
1616
1617 pub fn name(mut self, name: impl Into<String>) -> Self {
1619 self.name = name.into();
1620 self
1621 }
1622
1623 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1625 self.inner = self.inner.path_prefix(prefix);
1626 self
1627 }
1628
1629 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1631 self.inner = self.inner.connect_timeout_ms(ms);
1632 self
1633 }
1634
1635 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1638 self.inner = self.inner.read_timeout_ms(ms);
1639 self
1640 }
1641
1642 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1645 self.inner = self.inner.tls_ca_file(path)?;
1646 Ok(self)
1647 }
1648
1649 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1651 self.inner = self.inner.tls_client_cert(cert, key)?;
1652 Ok(self)
1653 }
1654
1655 fn matches(&self, request: &Request) -> bool {
1656 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1657 }
1658
1659 fn on_tcp(&self, key: &str) -> bool {
1660 let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1661 match tcp_until.get(key) {
1662 Some(until) if std::time::Instant::now() < *until => true,
1663 Some(_) => {
1664 tcp_until.remove(key);
1665 false
1666 }
1667 None => false,
1668 }
1669 }
1670
1671 fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1674 let key = format!("{}:{}", backend.host, backend.port);
1675 if backend.tls && !self.on_tcp(&key) {
1676 let config = match &self.inner.trust {
1677 Some(trust) => trust.client_config(),
1678 None => tls_client_config(),
1679 };
1680 let target = h3_upstream::Target {
1681 host: &backend.host,
1682 port: backend.port,
1683 authority: None,
1684 tls: &config,
1685 connect_timeout: self.inner.connect_timeout,
1686 read_timeout: self.inner.read_timeout,
1687 };
1688 match h3_upstream::forward(request, Peer::of(connection), &target) {
1689 Err(h3_upstream::H3Error::Connect(e)) => {
1690 self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1691 crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1692 eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1693 }
1694 result => return result.map_err(|e| e.to_string()),
1695 }
1696 }
1697 forward_upstream_trusting(
1698 &self.inner.pool,
1699 request,
1700 Peer::of(connection),
1701 &backend.host,
1702 backend.port,
1703 backend.tls,
1704 self.inner.trust.as_deref(),
1705 self.inner.connect_timeout,
1706 self.inner.read_timeout,
1707 )
1708 }
1709}
1710
1711#[cfg(feature = "http3")]
1712impl crate::middleware::Middleware for H3ReverseProxy {
1713 fn handle(
1714 &self,
1715 request: &crate::request::Request,
1716 connection: &crate::server::ConnectionInfo,
1717 next: &dyn crate::application::Application,
1718 ) -> Result<crate::response::Response, String> {
1719 if !self.matches(request) {
1720 return next.execute(request, connection);
1721 }
1722 if self.inner.backends.is_empty() {
1723 let mut response = bad_gateway();
1724 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1725 return Ok(response);
1726 }
1727 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1729 let n = self.inner.backends.len();
1730 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1731 let mut last_error = String::new();
1732 for attempt in 0..attempts {
1733 match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1734 Ok(mut response) => {
1735 status::received(&mut response, None);
1736 return Ok(response);
1737 }
1738 Err(e) => last_error = e,
1739 }
1740 }
1741 let mut response = bad_gateway();
1742 status::failed_with(&mut response, &last_error, None);
1743 Ok(response)
1744 }
1745
1746 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1747 true
1748 }
1749
1750 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1752 self.matches(request)
1753 }
1754}
1755
1756#[cfg(feature = "http2")]
1760pub(crate) fn forward_h2(
1761 request: &Request,
1762 client: Peer<'_>,
1763 url: &str,
1764 tls: bool,
1765 trust: Option<&UpstreamTls>,
1766 connect_timeout: Duration,
1767 read_timeout: Duration,
1768) -> Result<Response, String> {
1769 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1770 backend.tls |= tls;
1771 let tls = backend.tls.then(|| match trust {
1772 Some(t) => t.h2_config(),
1773 None => h2_upstream::default_tls(),
1774 });
1775 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1776}
1777
1778#[cfg(feature = "gateway-api")]
1783#[allow(clippy::too_many_arguments)]
1784pub(crate) fn forward_tls_with_host(
1785 config: &Arc<rustls::ClientConfig>,
1786 label: &str,
1787 request: &Request,
1788 client: Peer<'_>,
1789 ip: &str,
1790 port: u16,
1791 server_name: &str,
1792 host_header: &str,
1793 connect_timeout: Duration,
1794 read_timeout: Duration,
1795) -> Result<Response, String> {
1796 let pool = tls_pool_for_ca(label);
1797 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1798 let tcp = connect_tcp(ip, port, connect_timeout)?;
1799 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1800 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1801 Ok(rustls::StreamOwned::new(conn, tcp))
1802 })
1803}
1804
1805#[cfg(feature = "gateway-api")]
1809pub(crate) fn forward_h2c_with_host(
1810 request: &Request,
1811 client: Peer<'_>,
1812 host: &str,
1813 port: u16,
1814 authority: &str,
1815 connect_timeout: Duration,
1816 read_timeout: Duration,
1817) -> Result<Response, String> {
1818 let backend = Backend { host: host.to_string(), port, tls: false };
1819 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1820}
1821
1822#[cfg(feature = "http2")]
1842pub struct GrpcProxy {
1843 inner: H2ReverseProxy,
1844}
1845
1846#[cfg(feature = "http2")]
1847impl GrpcProxy {
1848 pub fn new<I, S>(backends: I) -> Self
1858 where
1859 I: IntoIterator<Item = S>,
1860 S: AsRef<str>,
1861 {
1862 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1863 }
1864
1865 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1867 self.inner = self.inner.path_prefix(prefix);
1868 self
1869 }
1870
1871 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1873 self.inner = self.inner.connect_timeout_ms(ms);
1874 self
1875 }
1876
1877 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1879 self.inner = self.inner.read_timeout_ms(ms);
1880 self
1881 }
1882
1883 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1885 self.inner = self.inner.tls_ca_file(path)?;
1886 Ok(self)
1887 }
1888
1889 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1892 self.inner = self.inner.tls_client_cert(cert, key)?;
1893 Ok(self)
1894 }
1895}
1896
1897#[cfg(feature = "http2")]
1898impl crate::middleware::Middleware for GrpcProxy {
1899 fn handle(
1900 &self,
1901 request: &crate::request::Request,
1902 connection: &crate::server::ConnectionInfo,
1903 next: &dyn crate::application::Application,
1904 ) -> Result<crate::response::Response, String> {
1905 if is_grpc(request) {
1906 self.inner.handle(request, connection, next)
1907 } else {
1908 next.execute(request, connection)
1909 }
1910 }
1911
1912 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1913 true
1914 }
1915
1916 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1917 is_grpc(request) && self.inner.matches(request)
1918 }
1919}
1920
1921#[cfg(feature = "http2")]
1922fn is_grpc(request: &Request) -> bool {
1923 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1924}
1925
1926#[cfg(test)]
1929mod backend_parse_tests {
1930 use super::Backend;
1931
1932 fn parse(url: &str) -> Option<(String, u16, bool)> {
1933 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1934 }
1935
1936 #[test]
1937 fn bare_host_port() {
1938 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1939 }
1940
1941 #[test]
1942 fn http_scheme() {
1943 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1944 }
1945
1946 #[test]
1947 fn h2_scheme_plain() {
1948 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1949 }
1950
1951 #[test]
1952 fn grpc_scheme_plain() {
1953 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1954 }
1955
1956 #[test]
1957 fn https_scheme_sets_tls_and_default_port() {
1958 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1959 }
1960
1961 #[test]
1962 fn https_scheme_explicit_port() {
1963 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1964 }
1965
1966 #[test]
1967 fn h2s_scheme_sets_tls() {
1968 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1969 }
1970
1971 #[test]
1972 fn h2s_scheme_explicit_port() {
1973 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1974 }
1975
1976 #[test]
1977 fn grpcs_scheme_sets_tls() {
1978 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1979 }
1980
1981 #[test]
1982 fn grpcs_scheme_explicit_port() {
1983 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1984 }
1985
1986 #[test]
1987 fn empty_host_returns_none() {
1988 assert_eq!(None, parse("https://"));
1989 }
1990
1991 #[test]
1992 fn bare_host_no_port_defaults_to_80() {
1993 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1994 }
1995}