1pub mod pool;
27#[cfg(feature = "http2")]
28pub(crate) mod async_upstream;
29#[cfg(not(target_arch = "wasm32"))]
30pub mod mirror;
31#[cfg(feature = "http2")]
32pub(crate) mod h2_upstream;
33#[cfg(feature = "http3")]
34pub(crate) mod h3_upstream;
35#[cfg(feature = "http2")]
36pub mod grpc_web;
37#[cfg(feature = "http2")]
38pub use grpc_web::GrpcWebLayer;
39
40#[cfg(test)]
41mod tests;
42
43use std::io::{Read, Write};
44use crate::ext::decimal::Decimal;
45use std::net::{TcpStream, ToSocketAddrs};
46use std::sync::atomic::{AtomicUsize, Ordering};
47use std::sync::Arc;
48use std::time::Duration;
49
50pub use pool::{ConnPool, Pool};
51
52use crate::application::Application;
53use crate::core::New;
54use crate::middleware::Middleware;
55use crate::mime_type::MimeType;
56use crate::range::Range;
57use crate::request::Request;
58use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
59use crate::server::ConnectionInfo;
60
61const HOP_BY_HOP: &[&str] = &[
65 "connection",
66 "keep-alive",
67 "proxy-authenticate",
68 "proxy-authorization",
69 "proxy-connection",
70 "te",
71 "trailer",
72 "transfer-encoding",
73 "upgrade",
74];
75
76pub enum LoadBalancing {
78 RoundRobin,
80}
81
82pub struct ReverseProxy {
97 backends: Vec<Backend>,
98 path_prefix: Option<String>,
99 connect_timeout: Duration,
100 read_timeout: Duration,
101 counter: AtomicUsize,
102 pool: Arc<ConnPool>,
103 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
104 trust: Option<Arc<UpstreamTls>>,
105 tls_ca: Option<String>,
107 tls_client: Option<(String, String)>,
108}
109
110impl ReverseProxy {
111 pub fn new<I, S>(backends: I) -> Self
115 where
116 I: IntoIterator<Item = S>,
117 S: AsRef<str>,
118 {
119 Self {
120 backends: backends
121 .into_iter()
122 .filter_map(|u| Backend::parse(u.as_ref()))
123 .collect(),
124 path_prefix: None,
125 connect_timeout: Duration::from_secs(5),
126 read_timeout: Duration::from_secs(30),
127 counter: AtomicUsize::new(0),
128 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
129 breaker: None,
130 trust: None,
131 tls_ca: None,
132 tls_client: None,
133 }
134 }
135
136 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
154 self.breaker = Some(breaker);
155 self
156 }
157
158 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
163 self.path_prefix = Some(prefix.into());
164 self
165 }
166
167 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
169 self
170 }
171
172 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
174 self.connect_timeout = Duration::from_millis(ms);
175 self
176 }
177
178 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
180 self.read_timeout = Duration::from_millis(ms);
181 self
182 }
183
184 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
189 self.pool = pool;
190 self
191 }
192
193 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
197 self.tls_ca = Some(path.to_string());
198 self.rebuild_trust()?;
199 Ok(self)
200 }
201
202 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
206 self.tls_client = Some((cert.to_string(), key.to_string()));
207 self.rebuild_trust()?;
208 Ok(self)
209 }
210
211 fn rebuild_trust(&mut self) -> Result<(), String> {
212 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
213 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
214 Ok(())
215 }
216
217 pub fn max_idle_conns(mut self, n: usize) -> Self {
219 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
220 self
221 }
222
223 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
224 if self.backends.is_empty() {
225 return Err("no backends configured".to_string());
226 }
227 let n = self.backends.len();
228 let start = self.counter.fetch_add(1, Ordering::Relaxed);
229 let mut last_err = "all backends failed".to_string();
230 for attempt in 0..n {
231 let idx = (start + attempt) % n;
232 let backend = &self.backends[idx];
233 let key = format!("{}:{}", backend.host, backend.port);
234
235 if let Some(breaker) = &self.breaker {
236 if !breaker.is_available(&key) {
237 last_err = format!("circuit open for {}", key);
238 continue;
239 }
240 }
241
242 match self.try_backend(request, connection, backend) {
243 Ok(resp) => {
244 if let Some(breaker) = &self.breaker {
245 breaker.record_success(&key);
246 }
247 return Ok(resp);
248 }
249 Err(e) => {
250 if let Some(breaker) = &self.breaker {
251 breaker.record_failure(&key);
252 }
253 last_err = e;
254 }
255 }
256 }
257 Err(last_err)
258 }
259
260 fn try_backend(
261 &self,
262 request: &Request,
263 connection: &ConnectionInfo,
264 backend: &Backend,
265 ) -> Result<Response, String> {
266 forward_upstream_trusting(
267 &self.pool,
268 request,
269 crate::proxy::Peer::of(connection),
270 &backend.host,
271 backend.port,
272 backend.tls,
273 self.trust.as_deref(),
274 self.connect_timeout,
275 self.read_timeout,
276 )
277 }
278}
279
280impl Middleware for ReverseProxy {
281 fn handle(
282 &self,
283 request: &Request,
284 connection: &ConnectionInfo,
285 next: &dyn Application,
286 ) -> Result<Response, String> {
287 if let Some(prefix) = &self.path_prefix {
288 if !request.request_uri.starts_with(prefix.as_str()) {
289 return next.execute(request, connection);
290 }
291 }
292 match self.proxy(request, connection) {
293 Ok(resp) => Ok(resp),
294 Err(_) => Ok(bad_gateway()),
295 }
296 }
297}
298
299#[derive(Clone, Copy, Debug)]
304pub(crate) struct Peer<'a> {
305 pub ip: &'a str,
306 pub tls: bool,
307}
308
309impl<'a> Peer<'a> {
310 pub(crate) fn of(connection: &'a ConnectionInfo) -> Self {
311 Peer { ip: &connection.client.ip, tls: connection.tls }
312 }
313
314 pub(crate) fn plain(ip: &'a str) -> Self {
316 Peer { ip, tls: false }
317 }
318
319 pub(crate) fn proto(&self) -> &'static str {
320 if self.tls { "https" } else { "http" }
321 }
322}
323
324pub(crate) fn build_request(
325 request: &Request,
326 backend_host: &str,
327 client: Peer<'_>,
328 keep_alive: bool,
329) -> Vec<u8> {
330 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
336 #[cfg(not(target_arch = "wasm32"))]
339 let trace = crate::otel::propagation_headers();
340 #[cfg(target_arch = "wasm32")]
341 let trace: Option<(String, Option<String>)> = None;
342 let forwarded = |h: &&crate::header::Header| {
343 !h.name.is_empty()
345 && !is_hop_by_hop_or_host(&h.name)
346 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
347 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
348 && !connection.is_some_and(|c| connection_listed(c, &h.name))
349 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
350 };
351 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
352 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
356 let forwarded_host = match request.header("X-Forwarded-Host") {
357 Some(_) => None,
358 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
359 };
360 let body_len = Decimal::new(request.body.len() as u128);
361 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
362 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
363 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
364 + forwarded_host.map_or(0, |v| v.len() + 20)
365 + forwarded_proto.map_or(0, |v| v.len() + 21)
366 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
367 + 13
368 + 24
369 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
370 + 2
371 + request.body.len();
372 let mut out: Vec<u8> = Vec::with_capacity(capacity);
373 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
374 out.extend_from_slice(part.as_bytes());
375 }
376 for h in request.headers.iter().filter(forwarded) {
377 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
378 out.extend_from_slice(part.as_bytes());
379 }
380 }
381 out.extend_from_slice(b"X-Forwarded-For: ");
382 if let Some(prior) = prior_xff {
383 out.extend_from_slice(prior.as_bytes());
384 out.extend_from_slice(b", ");
385 }
386 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
387 out.extend_from_slice(part.as_bytes());
388 }
389 if let Some(host) = forwarded_host {
390 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
391 out.extend_from_slice(part.as_bytes());
392 }
393 }
394 if let Some(proto) = forwarded_proto {
395 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
396 out.extend_from_slice(part.as_bytes());
397 }
398 }
399 if let Some((traceparent, baggage)) = &trace {
400 for part in ["traceparent: ", traceparent, "\r\n"] {
401 out.extend_from_slice(part.as_bytes());
402 }
403 if let Some(baggage) = baggage {
404 for part in ["baggage: ", baggage, "\r\n"] {
405 out.extend_from_slice(part.as_bytes());
406 }
407 }
408 }
409 if keep_alive {
410 out.extend_from_slice(b"Connection: keep-alive\r\n");
411 } else {
412 out.extend_from_slice(b"Connection: close\r\n");
413 }
414 if !request.body.is_empty() {
415 out.extend_from_slice(b"Content-Length: ");
416 out.extend_from_slice(body_len.as_str().as_bytes());
417 out.extend_from_slice(b"\r\n");
418 }
419 out.extend_from_slice(b"\r\n");
420 out.extend_from_slice(&request.body);
421 out
422}
423
424fn is_hop_by_hop_or_host(name: &str) -> bool {
429 if name.is_ascii() {
430 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
431 } else {
432 let lower = name.to_lowercase();
433 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
434 }
435}
436
437fn connection_listed(connection: &str, name: &str) -> bool {
440 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
441}
442
443fn decode_chunked<S: Read>(
448 stream: &mut S,
449 buf: &[u8],
450 header_end: usize,
451 tmp: &mut [u8],
452) -> Result<Vec<u8>, String> {
453 let mut raw: Vec<u8> = buf[header_end..].to_vec();
455 let mut decoded: Vec<u8> = Vec::new();
456
457 loop {
458 let crlf = loop {
460 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
461 break p;
462 }
463 let n = stream.read(tmp).map_err(|e| e.to_string())?;
464 if n == 0 {
465 return Err("chunked: premature EOF reading chunk size".to_string());
466 }
467 raw.extend_from_slice(&tmp[..n]);
468 };
469
470 let size_line = std::str::from_utf8(&raw[..crlf])
472 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
473 let size_str = size_line.split(';').next().unwrap_or("").trim();
474 let chunk_size = usize::from_str_radix(size_str, 16)
475 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
476 raw.drain(..crlf + 2); if chunk_size == 0 {
479 loop {
483 let line_end = loop {
484 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
485 break p;
486 }
487 let n = stream.read(tmp).map_err(|e| e.to_string())?;
488 if n == 0 {
489 return Err("chunked: premature EOF in trailer".to_string());
490 }
491 raw.extend_from_slice(&tmp[..n]);
492 };
493 raw.drain(..line_end + 2);
494 if line_end == 0 {
495 break;
496 }
497 }
498 break;
499 }
500
501 while raw.len() < chunk_size + 2 {
503 let n = stream.read(tmp).map_err(|e| e.to_string())?;
504 if n == 0 {
505 return Err("chunked: premature EOF reading chunk body".to_string());
506 }
507 raw.extend_from_slice(&tmp[..n]);
508 }
509 decoded.extend_from_slice(&raw[..chunk_size]);
510 raw.drain(..chunk_size + 2); }
512
513 Ok(decoded)
514}
515
516fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
519 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
520 buf.clear();
521 for line in header_str.lines() {
522 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
523 continue;
524 }
525 buf.extend_from_slice(line.as_bytes());
526 buf.extend_from_slice(b"\r\n");
527 }
528 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
529 buf.extend_from_slice(decoded);
530}
531
532pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
535 "content-length", "transfer-encoding", "connection", "keep-alive",
536 "proxy-connection", "te", "trailer", "upgrade",
537];
538
539#[cfg_attr(not(feature = "http2"), allow(dead_code))]
548pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
549 response.http_version.clear();
550 response.http_version.push_str(crate::http::VERSION.http_1_1);
551 let has_body = !response.content_range_list.is_empty();
552 if let [range] = response.content_range_list.as_mut_slice() {
553 if let Some(h) = response
554 .headers
555 .iter()
556 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
557 {
558 range.content_type.clear();
559 range.content_type.push_str(&h.value);
560 }
561 }
562 response.headers.retain(|h| {
565 if h.name.eq_ignore_ascii_case("content-type") {
566 return !has_body;
568 }
569 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
570 });
571 response
572}
573
574pub(crate) fn bad_gateway() -> Response {
575 let cr = Range::get_content_range(
576 b"502 Bad Gateway".to_vec(),
577 MimeType::TEXT_PLAIN.to_string(),
578 );
579 let mut r = Response::new();
580 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
581 r.reason_phrase = STATUS_CODE_REASON_PHRASE
582 .n502_bad_gateway
583 .reason_phrase
584 .to_string();
585 r.content_range_list = vec![cr];
586 r
587}
588
589const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
597 prefix: Vec<u8>,
598 prefix_pos: usize,
599 inner: R,
600}
601
602impl<R: Read + Send> ConcatReader<R> {
603 fn new(prefix: Vec<u8>, inner: R) -> Self {
604 ConcatReader { prefix, prefix_pos: 0, inner }
605 }
606}
607
608impl<R: Read + Send> Read for ConcatReader<R> {
609 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
610 if self.prefix_pos < self.prefix.len() {
611 let avail = &self.prefix[self.prefix_pos..];
612 let n = buf.len().min(avail.len());
613 buf[..n].copy_from_slice(&avail[..n]);
614 self.prefix_pos += n;
615 return Ok(n);
616 }
617 self.inner.read(buf)
618 }
619}
620
621
622pub(crate) const POOL_MAX_IDLE: usize = 64;
626
627pub(crate) fn shared_pool() -> &'static ConnPool {
630 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
631 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
632}
633
634pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
637 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
638}
639
640impl UpstreamIo for TcpStream {
641 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
642 self.set_read_timeout(Some(read))?;
643 self.set_write_timeout(Some(write))
644 }
645}
646
647#[cfg(any(feature = "http-client", feature = "http2"))]
648pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
649
650#[cfg(any(feature = "http-client", feature = "http2"))]
651impl UpstreamIo for TlsStream {
652 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
653 self.sock.set_io_timeouts(read, write)
654 }
655}
656
657pub(crate) fn forward_upstream(
663 plain_pool: &ConnPool,
664 request: &Request,
665 client: Peer<'_>,
666 host: &str,
667 port: u16,
668 tls: bool,
669 connect_timeout: Duration,
670 read_timeout: Duration,
671) -> Result<Response, String> {
672 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
673}
674
675#[allow(clippy::too_many_arguments)]
678pub(crate) fn forward_upstream_trusting(
679 plain_pool: &ConnPool,
680 request: &Request,
681 client: Peer<'_>,
682 host: &str,
683 port: u16,
684 tls: bool,
685 trust: Option<&UpstreamTls>,
686 connect_timeout: Duration,
687 read_timeout: Duration,
688) -> Result<Response, String> {
689 #[cfg(not(target_arch = "wasm32"))]
692 let span = crate::otel::outbound_span(&request.method);
693 #[cfg(not(target_arch = "wasm32"))]
694 if let Some(span) = &span {
695 let scheme = if tls { "https" } else { "http" };
696 let path = request.request_uri.as_str();
697 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
698 span.set_attribute(key, value);
699 }
700 }
701 let result = forward_upstream_untraced(plain_pool, request, client, host, port, tls, trust, connect_timeout, read_timeout);
702 #[cfg(not(target_arch = "wasm32"))]
703 if let Some(span) = &span {
704 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
705 }
706 result
707}
708
709#[allow(clippy::too_many_arguments)]
710fn forward_upstream_untraced(
711 plain_pool: &ConnPool,
712 request: &Request,
713 client: Peer<'_>,
714 host: &str,
715 port: u16,
716 tls: bool,
717 trust: Option<&UpstreamTls>,
718 connect_timeout: Duration,
719 read_timeout: Duration,
720) -> Result<Response, String> {
721 if tls {
722 #[cfg(any(feature = "http-client", feature = "http2"))]
723 {
724 let (pool, config) = match trust {
725 Some(t) => (t.pool, Arc::clone(&t.config)),
726 None => (shared_tls_pool(), tls_client_config()),
727 };
728 return forward_via(pool, request, client, host, port, host, read_timeout, || {
729 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
730 });
731 }
732 #[cfg(not(any(feature = "http-client", feature = "http2")))]
733 {
734 let _ = trust;
735 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
736 }
737 }
738 forward_pooled(plain_pool, request, client, host, port, connect_timeout, read_timeout)
739}
740
741pub struct UpstreamTls {
749 #[cfg(any(feature = "http-client", feature = "http2"))]
750 config: Arc<rustls::ClientConfig>,
751 #[cfg(any(feature = "http-client", feature = "http2"))]
752 pool: &'static Pool<TlsStream>,
753 #[cfg(feature = "http2")]
755 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
756 #[cfg(feature = "http2")]
758 h2_config: Arc<rustls::ClientConfig>,
759}
760
761impl UpstreamTls {
762 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
764 UpstreamTls::new(Some(path), None)
765 }
766
767 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
771 #[cfg(any(feature = "http-client", feature = "http2"))]
772 {
773 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
774 let roots = match ca_file {
775 Some(path) => {
776 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
777 let certs = CertificateDer::pem_slice_iter(&bytes)
778 .collect::<Result<Vec<_>, _>>()
779 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
780 if certs.is_empty() {
781 return Err(format!("no certificates in CA file '{path}'"));
782 }
783 let mut roots = rustls::RootCertStore::empty();
784 for cert in certs {
785 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
786 }
787 roots
788 }
789 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
790 };
791 let builder = crate::tls_client_builder().with_root_certificates(roots);
792 let config = match client_cert {
793 None => builder.with_no_client_auth(),
794 Some((cert_path, key_path)) => {
795 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
796 let chain = CertificateDer::pem_slice_iter(&bytes)
797 .collect::<Result<Vec<_>, _>>()
798 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
799 if chain.is_empty() {
800 return Err(format!("no certificate in '{cert_path}'"));
801 }
802 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
803 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
804 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
805 }
806 };
807 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
808 Ok(UpstreamTls::with_config(config, &label))
809 }
810 #[cfg(not(any(feature = "http-client", feature = "http2")))]
811 {
812 let _ = client_cert;
813 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
814 }
815 }
816
817 #[cfg(any(feature = "http-client", feature = "http2"))]
818 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
819 #[cfg(feature = "http2")]
820 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
821 UpstreamTls {
822 config: Arc::new(config),
823 pool: tls_pool_for_ca(label),
824 #[cfg(feature = "http2")]
825 async_pool: async_tls_pool_for_ca(label),
826 #[cfg(feature = "http2")]
827 h2_config,
828 }
829 }
830
831 #[cfg(feature = "http2")]
833 pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
834 &self.h2_config
835 }
836
837 pub(crate) fn reject_all(label: &str) -> UpstreamTls {
841 #[cfg(any(feature = "http-client", feature = "http2"))]
842 {
843 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
844 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
845 }
846 #[cfg(not(any(feature = "http-client", feature = "http2")))]
847 {
848 let _ = label;
849 UpstreamTls {}
850 }
851 }
852
853 #[cfg(feature = "http2")]
855 pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
856 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
857 }
858
859 #[cfg(any(feature = "http-client", feature = "http2"))]
861 pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
862 Arc::clone(&self.config)
863 }
864}
865
866#[cfg(feature = "http2")]
869fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
870 static POOLS: std::sync::OnceLock<
871 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
872 > = std::sync::OnceLock::new();
873 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
874 pools
875 .entry(path.to_string())
876 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
877}
878
879#[cfg(any(feature = "http-client", feature = "http2"))]
881fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
882 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
883 std::sync::OnceLock::new();
884 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
885 pools
886 .entry(path.to_string())
887 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
888}
889
890pub(crate) fn forward_pooled(
894 pool: &ConnPool,
895 request: &Request,
896 client: Peer<'_>,
897 host: &str,
898 port: u16,
899 connect_timeout: Duration,
900 read_timeout: Duration,
901) -> Result<Response, String> {
902 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
903}
904
905#[cfg(feature = "gateway-api")]
908#[allow(clippy::too_many_arguments)]
909pub(crate) fn forward_pooled_with_host(
910 pool: &ConnPool,
911 request: &Request,
912 client: Peer<'_>,
913 host: &str,
914 port: u16,
915 host_header: &str,
916 connect_timeout: Duration,
917 read_timeout: Duration,
918) -> Result<Response, String> {
919 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
920}
921
922#[allow(clippy::too_many_arguments)]
929fn forward_via<S: UpstreamIo>(
930 pool: &Pool<S>,
931 request: &Request,
932 client: Peer<'_>,
933 host: &str,
934 port: u16,
935 host_header: &str,
936 read_timeout: Duration,
937 connect: impl Fn() -> Result<S, String>,
938) -> Result<Response, String> {
939 let key = format!("{}:{}", host, port);
940 if let Some(stream) = pool.acquire(&key) {
941 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
942 Exchange::Done(response) => return Ok(response),
943 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
945 }
946 }
947 let stream = connect()?;
948 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
949 Exchange::Done(response) => Ok(response),
950 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
951 }
952}
953
954fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
958 let key = format!("{}:{}", host, port);
959 let addrs: Vec<_> = key
960 .to_socket_addrs()
961 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
962 .collect();
963 let mut last_error = format!("no address resolved for {}", key);
964 for addr in addrs {
965 match TcpStream::connect_timeout(&addr, connect_timeout) {
966 Ok(stream) => {
967 let _ = stream.set_nodelay(true);
968 return Ok(stream);
969 }
970 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
971 }
972 }
973 Err(last_error)
974}
975
976#[cfg(any(feature = "http-client", feature = "http2"))]
978fn shared_tls_pool() -> &'static Pool<TlsStream> {
979 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
980 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
981}
982
983#[cfg(any(feature = "http-client", feature = "http2"))]
987pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
988 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
989 Arc::clone(CONFIG.get_or_init(|| {
990 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
991 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
992 }))
993}
994
995#[cfg(any(feature = "http-client", feature = "http2"))]
996fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
997 let tcp = connect_tcp(host, port, connect_timeout)?;
998 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
999 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1000 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1001 Ok(rustls::StreamOwned::new(conn, tcp))
1002}
1003
1004fn is_idempotent(method: &str) -> bool {
1005 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1006}
1007
1008enum Exchange {
1009 Done(Response),
1010 NoResponse(String),
1012 Failed(String),
1013}
1014
1015fn exchange<S: UpstreamIo>(
1018 pool: &Pool<S>,
1019 key: &str,
1020 mut stream: S,
1021 request: &Request,
1022 host: &str,
1023 client: Peer<'_>,
1024 read_timeout: Duration,
1025) -> Exchange {
1026 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1027 return Exchange::Failed(e.to_string());
1028 }
1029 let req_bytes = build_request(request, host, client, true);
1031 if let Err(e) = stream.write_all(&req_bytes) {
1032 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1033 }
1034
1035 let mut tmp = [0u8; 4096];
1036 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1037 Ok(head) => head,
1038 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1039 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1040 };
1041 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1042
1043 if response_has_no_body(&request.method, &header_lower) {
1044 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1045 pool.release(key, stream);
1046 }
1047 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1048 Ok(response) => Exchange::Done(response),
1049 Err(e) => Exchange::Failed(e),
1050 };
1051 }
1052
1053 if should_stream_response(&header_lower) {
1054 return match parse_status_and_headers(&header_bytes) {
1057 Ok(mut response) => {
1058 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1059 Exchange::Done(response)
1060 }
1061 Err(e) => Exchange::Failed(e),
1062 };
1063 }
1064 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1066 Ok((resp_bytes, reusable)) => {
1067 if reusable {
1068 pool.release(key, stream);
1069 }
1070 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1071 Ok(response) => Exchange::Done(response),
1072 Err(e) => Exchange::Failed(e),
1073 }
1074 }
1075 Err(e) => Exchange::Failed(e),
1076 }
1077}
1078
1079#[cfg(test)]
1084fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1085 read_response_head(stream, tmp).map_err(|e| match e {
1086 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1087 })
1088}
1089
1090enum HeadError {
1094 NothingReceived(String),
1095 Partial(String),
1096}
1097
1098fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1099 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1100}
1101
1102fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1108 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1109 let mut interim = 0;
1110 while is_interim_response(&head) {
1111 crate::early_hints::forward_upstream(&head);
1112 interim += 1;
1113 if interim > MAX_INTERIM_RESPONSES {
1114 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1115 }
1116 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1117 }
1118 Ok((head, rest))
1119}
1120
1121fn read_response_head_from<S: Read>(
1125 stream: &mut S,
1126 tmp: &mut [u8],
1127 mut buf: Vec<u8>,
1128 received: bool,
1129) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1130 loop {
1131 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1132 let body_prefix = buf[pos + 4..].to_vec();
1133 buf.truncate(pos + 4);
1134 return Ok((buf, body_prefix));
1135 }
1136 let nothing = buf.is_empty() && !received;
1137 let n = match stream.read(tmp) {
1138 Ok(n) => n,
1139 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1140 Err(e) => return Err(HeadError::Partial(e.to_string())),
1141 };
1142 if n == 0 {
1143 let msg = "backend closed connection before headers were complete".to_string();
1144 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1145 }
1146 buf.extend_from_slice(&tmp[..n]);
1147 }
1148}
1149
1150pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1152
1153pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1156 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1157 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1158 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1159}
1160
1161pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1165 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1166 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1167}
1168
1169pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1176 let is_sse = header_lower.lines().any(|l| {
1177 l.starts_with("content-type:") && l.contains("text/event-stream")
1178 });
1179 let is_chunked = header_lower.lines().any(|l| {
1180 l.starts_with("transfer-encoding:") && l.contains("chunked")
1181 });
1182 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1183 l.strip_prefix("content-length:")?.trim().parse().ok()
1184 });
1185 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1186 is_sse || is_chunked || is_large
1187}
1188
1189#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1195pub(crate) struct HeadFacts {
1196 pub(crate) status_no_body: bool,
1198 pub(crate) sse: bool,
1199 pub(crate) chunked: bool,
1200 pub(crate) content_length: Option<usize>,
1202 pub(crate) connection_close: bool,
1203}
1204
1205#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1206impl HeadFacts {
1207 pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1208 let text = std::str::from_utf8(head).unwrap_or("");
1209 let status = text.split_whitespace().nth(1).unwrap_or("");
1210 let mut facts = HeadFacts {
1211 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1212 sse: false,
1213 chunked: false,
1214 content_length: None,
1215 connection_close: false,
1216 };
1217 for line in text.lines() {
1218 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1219 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1220 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1221 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1222 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1223 if facts.content_length.is_none() {
1224 facts.content_length = value.trim().parse().ok();
1225 }
1226 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1227 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1228 }
1229 }
1230 facts
1231 }
1232
1233 pub(crate) fn no_body(&self, method: &str) -> bool {
1235 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1236 }
1237
1238 pub(crate) fn should_stream(&self) -> bool {
1240 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1241 }
1242}
1243
1244fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1245 let bytes = line.as_bytes();
1246 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1248 .then(|| &line[prefix.len()..])
1249}
1250
1251fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1252 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1253}
1254
1255fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1257 let s = std::str::from_utf8(header_bytes)
1258 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1259 let mut lines = s.lines();
1260 let status_line = lines.next().ok_or("empty backend response")?;
1261 let mut parts = status_line.splitn(3, ' ');
1262 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1263 let status_code: i16 = parts
1264 .next()
1265 .unwrap_or("502")
1266 .parse()
1267 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1268 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1269 let mut headers = Vec::new();
1270 for line in lines {
1271 let line = line.trim_end_matches('\r');
1272 if line.is_empty() { break; }
1273 if let Some(colon) = line.find(':') {
1274 headers.push(crate::header::Header {
1275 name: line[..colon].trim().to_string().into(),
1276 value: (line[colon + 1..].trim().to_string()).into(),
1277 });
1278 }
1279 }
1280 Ok(Response {
1281 http_version,
1282 status_code,
1283 reason_phrase,
1284 headers,
1285 content_range_list: vec![],
1286 stream_file: None,
1287 stream_pipe: None,
1288 trailers: None,
1289 })
1290}
1291
1292fn read_response_from_partial<S: Read>(
1298 stream: &mut S,
1299 header_bytes: Vec<u8>,
1300 body_prefix: Vec<u8>,
1301 tmp: &mut [u8],
1302) -> Result<(Vec<u8>, bool), String> {
1303 let header_end = header_bytes.len();
1304 let mut buf = header_bytes;
1305 buf.extend_from_slice(&body_prefix);
1306
1307 let header_lower =
1308 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1309 let connection_close =
1310 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1311 let is_chunked = header_lower
1312 .lines()
1313 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1314 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1315 l.strip_prefix("content-length:")?.trim().parse().ok()
1316 });
1317
1318 if is_chunked {
1319 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1320 rewrite_as_content_length(&mut buf, header_end, &decoded);
1321 Ok((buf, !connection_close))
1322 } else if let Some(len) = content_length {
1323 while buf.len() < header_end + len {
1324 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1325 if n == 0 { break; }
1326 buf.extend_from_slice(&tmp[..n]);
1327 }
1328 Ok((buf, !connection_close))
1329 } else {
1330 loop {
1331 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1332 if n == 0 { break; }
1333 buf.extend_from_slice(&tmp[..n]);
1334 }
1335 Ok((buf, false))
1336 }
1337}
1338
1339struct Backend {
1342 host: String,
1343 port: u16,
1344 tls: bool,
1347}
1348
1349impl Backend {
1350 fn parse(url: &str) -> Option<Self> {
1351 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1352 (r, true, 443u16)
1353 } else if let Some(r) = url.strip_prefix("h2s://") {
1354 (r, true, 443u16)
1355 } else if let Some(r) = url.strip_prefix("grpcs://") {
1356 (r, true, 443u16)
1357 } else if let Some(r) = url.strip_prefix("http://") {
1358 (r, false, 80u16)
1359 } else if let Some(r) = url.strip_prefix("h2://") {
1360 (r, false, 80u16)
1361 } else if let Some(r) = url.strip_prefix("grpc://") {
1362 (r, false, 80u16)
1363 } else {
1364 (url, false, 80u16)
1365 };
1366 let host_port = rest.split('/').next().unwrap_or(rest);
1368 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1369 let port_str = &host_port[colon + 1..];
1370 if let Ok(p) = port_str.parse::<u16>() {
1371 (host_port[..colon].to_string(), p)
1372 } else {
1373 (host_port.to_string(), default_port)
1374 }
1375 } else {
1376 (host_port.to_string(), default_port)
1377 };
1378 if host.is_empty() {
1379 return None;
1380 }
1381 Some(Backend { host, port, tls })
1382 }
1383}
1384
1385#[cfg(feature = "http2")]
1396pub struct H2ReverseProxy {
1397 inner: ReverseProxy,
1398}
1399
1400#[cfg(feature = "http2")]
1401impl H2ReverseProxy {
1402 pub fn new<I, S>(backends: I) -> Self
1413 where
1414 I: IntoIterator<Item = S>,
1415 S: AsRef<str>,
1416 {
1417 H2ReverseProxy {
1418 inner: ReverseProxy::new(backends),
1419 }
1420 }
1421
1422 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1424 self.inner = self.inner.path_prefix(prefix);
1425 self
1426 }
1427
1428 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1430 self.inner = self.inner.connect_timeout_ms(ms);
1431 self
1432 }
1433
1434 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1439 self.inner = self.inner.read_timeout_ms(ms);
1440 self
1441 }
1442
1443 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1446 self.inner = self.inner.tls_ca_file(path)?;
1447 Ok(self)
1448 }
1449
1450 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1453 self.inner = self.inner.tls_client_cert(cert, key)?;
1454 Ok(self)
1455 }
1456
1457 fn matches(&self, request: &Request) -> bool {
1458 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1459 }
1460}
1461
1462#[cfg(feature = "http2")]
1463impl crate::middleware::Middleware for H2ReverseProxy {
1464 fn handle(
1465 &self,
1466 request: &crate::request::Request,
1467 connection: &crate::server::ConnectionInfo,
1468 next: &dyn crate::application::Application,
1469 ) -> Result<crate::response::Response, String> {
1470 if !self.matches(request) {
1471 return next.execute(request, connection);
1472 }
1473 if self.inner.backends.is_empty() {
1474 return Ok(bad_gateway());
1475 }
1476 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1478 let n = self.inner.backends.len();
1479 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1480 for attempt in 0..attempts {
1481 let backend = &self.inner.backends[(start + attempt) % n];
1482 let tls = backend.tls.then(|| match &self.inner.trust {
1483 Some(trust) => trust.h2_config(),
1484 None => h2_upstream::default_tls(),
1485 });
1486 let target = h2_upstream::Target {
1487 backend,
1488 tls,
1489 connect_timeout: self.inner.connect_timeout,
1490 read_timeout: self.inner.read_timeout,
1491 authority: None,
1492 };
1493 if let Ok(response) = h2_upstream::forward(request, Peer::of(connection), &target) {
1494 return Ok(response);
1495 }
1496 }
1497 Ok(bad_gateway())
1498 }
1499
1500 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1503 true
1504 }
1505
1506 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1509 self.matches(request)
1510 }
1511}
1512
1513#[cfg(feature = "http3")]
1535pub struct H3ReverseProxy {
1536 inner: ReverseProxy,
1537 name: String,
1538 tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1540}
1541
1542#[cfg(feature = "http3")]
1543impl H3ReverseProxy {
1544 pub fn new<I, S>(backends: I) -> Self
1550 where
1551 I: IntoIterator<Item = S>,
1552 S: AsRef<str>,
1553 {
1554 H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1555 }
1556
1557 pub fn name(mut self, name: impl Into<String>) -> Self {
1559 self.name = name.into();
1560 self
1561 }
1562
1563 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1565 self.inner = self.inner.path_prefix(prefix);
1566 self
1567 }
1568
1569 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1571 self.inner = self.inner.connect_timeout_ms(ms);
1572 self
1573 }
1574
1575 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1578 self.inner = self.inner.read_timeout_ms(ms);
1579 self
1580 }
1581
1582 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1585 self.inner = self.inner.tls_ca_file(path)?;
1586 Ok(self)
1587 }
1588
1589 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1591 self.inner = self.inner.tls_client_cert(cert, key)?;
1592 Ok(self)
1593 }
1594
1595 fn matches(&self, request: &Request) -> bool {
1596 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1597 }
1598
1599 fn on_tcp(&self, key: &str) -> bool {
1600 let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1601 match tcp_until.get(key) {
1602 Some(until) if std::time::Instant::now() < *until => true,
1603 Some(_) => {
1604 tcp_until.remove(key);
1605 false
1606 }
1607 None => false,
1608 }
1609 }
1610
1611 fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1614 let key = format!("{}:{}", backend.host, backend.port);
1615 if backend.tls && !self.on_tcp(&key) {
1616 let config = match &self.inner.trust {
1617 Some(trust) => trust.client_config(),
1618 None => tls_client_config(),
1619 };
1620 let target = h3_upstream::Target {
1621 host: &backend.host,
1622 port: backend.port,
1623 tls: &config,
1624 connect_timeout: self.inner.connect_timeout,
1625 read_timeout: self.inner.read_timeout,
1626 };
1627 match h3_upstream::forward(request, Peer::of(connection), &target) {
1628 Err(h3_upstream::H3Error::Connect(e)) => {
1629 self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1630 crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1631 eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1632 }
1633 result => return result.map_err(|e| e.to_string()),
1634 }
1635 }
1636 forward_upstream_trusting(
1637 &self.inner.pool,
1638 request,
1639 Peer::of(connection),
1640 &backend.host,
1641 backend.port,
1642 backend.tls,
1643 self.inner.trust.as_deref(),
1644 self.inner.connect_timeout,
1645 self.inner.read_timeout,
1646 )
1647 }
1648}
1649
1650#[cfg(feature = "http3")]
1651impl crate::middleware::Middleware for H3ReverseProxy {
1652 fn handle(
1653 &self,
1654 request: &crate::request::Request,
1655 connection: &crate::server::ConnectionInfo,
1656 next: &dyn crate::application::Application,
1657 ) -> Result<crate::response::Response, String> {
1658 if !self.matches(request) {
1659 return next.execute(request, connection);
1660 }
1661 if self.inner.backends.is_empty() {
1662 return Ok(bad_gateway());
1663 }
1664 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1666 let n = self.inner.backends.len();
1667 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1668 for attempt in 0..attempts {
1669 if let Ok(response) = self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1670 return Ok(response);
1671 }
1672 }
1673 Ok(bad_gateway())
1674 }
1675
1676 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1677 true
1678 }
1679
1680 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1682 self.matches(request)
1683 }
1684}
1685
1686#[cfg(feature = "http2")]
1690pub(crate) fn forward_h2(
1691 request: &Request,
1692 client: Peer<'_>,
1693 url: &str,
1694 tls: bool,
1695 trust: Option<&UpstreamTls>,
1696 connect_timeout: Duration,
1697 read_timeout: Duration,
1698) -> Result<Response, String> {
1699 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1700 backend.tls |= tls;
1701 let tls = backend.tls.then(|| match trust {
1702 Some(t) => t.h2_config(),
1703 None => h2_upstream::default_tls(),
1704 });
1705 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1706}
1707
1708#[cfg(feature = "gateway-api")]
1713#[allow(clippy::too_many_arguments)]
1714pub(crate) fn forward_tls_with_host(
1715 config: &Arc<rustls::ClientConfig>,
1716 label: &str,
1717 request: &Request,
1718 client: Peer<'_>,
1719 ip: &str,
1720 port: u16,
1721 server_name: &str,
1722 host_header: &str,
1723 connect_timeout: Duration,
1724 read_timeout: Duration,
1725) -> Result<Response, String> {
1726 let pool = tls_pool_for_ca(label);
1727 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1728 let tcp = connect_tcp(ip, port, connect_timeout)?;
1729 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1730 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1731 Ok(rustls::StreamOwned::new(conn, tcp))
1732 })
1733}
1734
1735#[cfg(feature = "gateway-api")]
1739pub(crate) fn forward_h2c_with_host(
1740 request: &Request,
1741 client: Peer<'_>,
1742 host: &str,
1743 port: u16,
1744 authority: &str,
1745 connect_timeout: Duration,
1746 read_timeout: Duration,
1747) -> Result<Response, String> {
1748 let backend = Backend { host: host.to_string(), port, tls: false };
1749 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1750}
1751
1752#[cfg(feature = "http2")]
1772pub struct GrpcProxy {
1773 inner: H2ReverseProxy,
1774}
1775
1776#[cfg(feature = "http2")]
1777impl GrpcProxy {
1778 pub fn new<I, S>(backends: I) -> Self
1788 where
1789 I: IntoIterator<Item = S>,
1790 S: AsRef<str>,
1791 {
1792 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1793 }
1794
1795 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1797 self.inner = self.inner.path_prefix(prefix);
1798 self
1799 }
1800
1801 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1803 self.inner = self.inner.connect_timeout_ms(ms);
1804 self
1805 }
1806
1807 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1809 self.inner = self.inner.read_timeout_ms(ms);
1810 self
1811 }
1812
1813 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1815 self.inner = self.inner.tls_ca_file(path)?;
1816 Ok(self)
1817 }
1818
1819 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1822 self.inner = self.inner.tls_client_cert(cert, key)?;
1823 Ok(self)
1824 }
1825}
1826
1827#[cfg(feature = "http2")]
1828impl crate::middleware::Middleware for GrpcProxy {
1829 fn handle(
1830 &self,
1831 request: &crate::request::Request,
1832 connection: &crate::server::ConnectionInfo,
1833 next: &dyn crate::application::Application,
1834 ) -> Result<crate::response::Response, String> {
1835 if is_grpc(request) {
1836 self.inner.handle(request, connection, next)
1837 } else {
1838 next.execute(request, connection)
1839 }
1840 }
1841
1842 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1843 true
1844 }
1845
1846 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1847 is_grpc(request) && self.inner.matches(request)
1848 }
1849}
1850
1851#[cfg(feature = "http2")]
1852fn is_grpc(request: &Request) -> bool {
1853 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1854}
1855
1856#[cfg(test)]
1859mod backend_parse_tests {
1860 use super::Backend;
1861
1862 fn parse(url: &str) -> Option<(String, u16, bool)> {
1863 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1864 }
1865
1866 #[test]
1867 fn bare_host_port() {
1868 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1869 }
1870
1871 #[test]
1872 fn http_scheme() {
1873 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1874 }
1875
1876 #[test]
1877 fn h2_scheme_plain() {
1878 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1879 }
1880
1881 #[test]
1882 fn grpc_scheme_plain() {
1883 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1884 }
1885
1886 #[test]
1887 fn https_scheme_sets_tls_and_default_port() {
1888 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1889 }
1890
1891 #[test]
1892 fn https_scheme_explicit_port() {
1893 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1894 }
1895
1896 #[test]
1897 fn h2s_scheme_sets_tls() {
1898 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1899 }
1900
1901 #[test]
1902 fn h2s_scheme_explicit_port() {
1903 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1904 }
1905
1906 #[test]
1907 fn grpcs_scheme_sets_tls() {
1908 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1909 }
1910
1911 #[test]
1912 fn grpcs_scheme_explicit_port() {
1913 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1914 }
1915
1916 #[test]
1917 fn empty_host_returns_none() {
1918 assert_eq!(None, parse("https://"));
1919 }
1920
1921 #[test]
1922 fn bare_host_no_port_defaults_to_80() {
1923 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1924 }
1925}