1pub mod pool;
27#[cfg(feature = "http2")]
28pub(crate) mod async_upstream;
29#[cfg(feature = "http2")]
30pub(crate) mod h2_upstream;
31#[cfg(feature = "http2")]
32pub mod grpc_web;
33#[cfg(feature = "http2")]
34pub use grpc_web::GrpcWebLayer;
35
36#[cfg(test)]
37mod tests;
38
39use std::io::{Read, Write};
40use crate::ext::decimal::Decimal;
41use std::net::{TcpStream, ToSocketAddrs};
42use std::sync::atomic::{AtomicUsize, Ordering};
43use std::sync::Arc;
44use std::time::Duration;
45
46pub use pool::{ConnPool, Pool};
47
48use crate::application::Application;
49use crate::core::New;
50use crate::middleware::Middleware;
51use crate::mime_type::MimeType;
52use crate::range::Range;
53use crate::request::Request;
54use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
55use crate::server::ConnectionInfo;
56
57const HOP_BY_HOP: &[&str] = &[
61 "connection",
62 "keep-alive",
63 "proxy-authenticate",
64 "proxy-authorization",
65 "proxy-connection",
66 "te",
67 "trailer",
68 "transfer-encoding",
69 "upgrade",
70];
71
72pub enum LoadBalancing {
74 RoundRobin,
76}
77
78pub struct ReverseProxy {
93 backends: Vec<Backend>,
94 path_prefix: Option<String>,
95 connect_timeout: Duration,
96 read_timeout: Duration,
97 counter: AtomicUsize,
98 pool: Arc<ConnPool>,
99 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
100 trust: Option<Arc<UpstreamTls>>,
101 tls_ca: Option<String>,
103 tls_client: Option<(String, String)>,
104}
105
106impl ReverseProxy {
107 pub fn new<I, S>(backends: I) -> Self
111 where
112 I: IntoIterator<Item = S>,
113 S: AsRef<str>,
114 {
115 Self {
116 backends: backends
117 .into_iter()
118 .filter_map(|u| Backend::parse(u.as_ref()))
119 .collect(),
120 path_prefix: None,
121 connect_timeout: Duration::from_secs(5),
122 read_timeout: Duration::from_secs(30),
123 counter: AtomicUsize::new(0),
124 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
125 breaker: None,
126 trust: None,
127 tls_ca: None,
128 tls_client: None,
129 }
130 }
131
132 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
150 self.breaker = Some(breaker);
151 self
152 }
153
154 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
159 self.path_prefix = Some(prefix.into());
160 self
161 }
162
163 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
165 self
166 }
167
168 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
170 self.connect_timeout = Duration::from_millis(ms);
171 self
172 }
173
174 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
176 self.read_timeout = Duration::from_millis(ms);
177 self
178 }
179
180 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
185 self.pool = pool;
186 self
187 }
188
189 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
193 self.tls_ca = Some(path.to_string());
194 self.rebuild_trust()?;
195 Ok(self)
196 }
197
198 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
202 self.tls_client = Some((cert.to_string(), key.to_string()));
203 self.rebuild_trust()?;
204 Ok(self)
205 }
206
207 fn rebuild_trust(&mut self) -> Result<(), String> {
208 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
209 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
210 Ok(())
211 }
212
213 pub fn max_idle_conns(mut self, n: usize) -> Self {
215 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
216 self
217 }
218
219 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
220 if self.backends.is_empty() {
221 return Err("no backends configured".to_string());
222 }
223 let n = self.backends.len();
224 let start = self.counter.fetch_add(1, Ordering::Relaxed);
225 let mut last_err = "all backends failed".to_string();
226 for attempt in 0..n {
227 let idx = (start + attempt) % n;
228 let backend = &self.backends[idx];
229 let key = format!("{}:{}", backend.host, backend.port);
230
231 if let Some(breaker) = &self.breaker {
232 if !breaker.is_available(&key) {
233 last_err = format!("circuit open for {}", key);
234 continue;
235 }
236 }
237
238 match self.try_backend(request, connection, backend) {
239 Ok(resp) => {
240 if let Some(breaker) = &self.breaker {
241 breaker.record_success(&key);
242 }
243 return Ok(resp);
244 }
245 Err(e) => {
246 if let Some(breaker) = &self.breaker {
247 breaker.record_failure(&key);
248 }
249 last_err = e;
250 }
251 }
252 }
253 Err(last_err)
254 }
255
256 fn try_backend(
257 &self,
258 request: &Request,
259 connection: &ConnectionInfo,
260 backend: &Backend,
261 ) -> Result<Response, String> {
262 forward_upstream_trusting(
263 &self.pool,
264 request,
265 &connection.client.ip,
266 &backend.host,
267 backend.port,
268 backend.tls,
269 self.trust.as_deref(),
270 self.connect_timeout,
271 self.read_timeout,
272 )
273 }
274}
275
276impl Middleware for ReverseProxy {
277 fn handle(
278 &self,
279 request: &Request,
280 connection: &ConnectionInfo,
281 next: &dyn Application,
282 ) -> Result<Response, String> {
283 if let Some(prefix) = &self.path_prefix {
284 if !request.request_uri.starts_with(prefix.as_str()) {
285 return next.execute(request, connection);
286 }
287 }
288 match self.proxy(request, connection) {
289 Ok(resp) => Ok(resp),
290 Err(_) => Ok(bad_gateway()),
291 }
292 }
293}
294
295pub(crate) fn build_request(
298 request: &Request,
299 backend_host: &str,
300 client_ip: &str,
301 keep_alive: bool,
302) -> Vec<u8> {
303 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
309 #[cfg(not(target_arch = "wasm32"))]
312 let trace = crate::otel::propagation_headers();
313 #[cfg(target_arch = "wasm32")]
314 let trace: Option<(String, Option<String>)> = None;
315 let forwarded = |h: &&crate::header::Header| {
316 !h.name.is_empty()
318 && !is_hop_by_hop_or_host(&h.name)
319 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
320 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
321 && !connection.is_some_and(|c| connection_listed(c, &h.name))
322 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
323 };
324 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
325 let forwarded_host = match request.header("X-Forwarded-Host") {
328 Some(_) => None,
329 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
330 };
331 let body_len = Decimal::new(request.body.len() as u128);
332 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
333 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
334 + client_ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
335 + forwarded_host.map_or(0, |v| v.len() + 20)
336 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
337 + 13
338 + 24
339 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
340 + 2
341 + request.body.len();
342 let mut out: Vec<u8> = Vec::with_capacity(capacity);
343 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
344 out.extend_from_slice(part.as_bytes());
345 }
346 for h in request.headers.iter().filter(forwarded) {
347 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
348 out.extend_from_slice(part.as_bytes());
349 }
350 }
351 out.extend_from_slice(b"X-Forwarded-For: ");
352 if let Some(prior) = prior_xff {
353 out.extend_from_slice(prior.as_bytes());
354 out.extend_from_slice(b", ");
355 }
356 for part in [client_ip, "\r\n", "Via: 1.1 rws\r\n"] {
357 out.extend_from_slice(part.as_bytes());
358 }
359 if let Some(host) = forwarded_host {
360 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
361 out.extend_from_slice(part.as_bytes());
362 }
363 }
364 if let Some((traceparent, baggage)) = &trace {
365 for part in ["traceparent: ", traceparent, "\r\n"] {
366 out.extend_from_slice(part.as_bytes());
367 }
368 if let Some(baggage) = baggage {
369 for part in ["baggage: ", baggage, "\r\n"] {
370 out.extend_from_slice(part.as_bytes());
371 }
372 }
373 }
374 if keep_alive {
375 out.extend_from_slice(b"Connection: keep-alive\r\n");
376 } else {
377 out.extend_from_slice(b"Connection: close\r\n");
378 }
379 if !request.body.is_empty() {
380 out.extend_from_slice(b"Content-Length: ");
381 out.extend_from_slice(body_len.as_str().as_bytes());
382 out.extend_from_slice(b"\r\n");
383 }
384 out.extend_from_slice(b"\r\n");
385 out.extend_from_slice(&request.body);
386 out
387}
388
389fn is_hop_by_hop_or_host(name: &str) -> bool {
394 if name.is_ascii() {
395 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
396 } else {
397 let lower = name.to_lowercase();
398 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
399 }
400}
401
402fn connection_listed(connection: &str, name: &str) -> bool {
405 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
406}
407
408fn decode_chunked<S: Read>(
413 stream: &mut S,
414 buf: &[u8],
415 header_end: usize,
416 tmp: &mut [u8],
417) -> Result<Vec<u8>, String> {
418 let mut raw: Vec<u8> = buf[header_end..].to_vec();
420 let mut decoded: Vec<u8> = Vec::new();
421
422 loop {
423 let crlf = loop {
425 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
426 break p;
427 }
428 let n = stream.read(tmp).map_err(|e| e.to_string())?;
429 if n == 0 {
430 return Err("chunked: premature EOF reading chunk size".to_string());
431 }
432 raw.extend_from_slice(&tmp[..n]);
433 };
434
435 let size_line = std::str::from_utf8(&raw[..crlf])
437 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
438 let size_str = size_line.split(';').next().unwrap_or("").trim();
439 let chunk_size = usize::from_str_radix(size_str, 16)
440 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
441 raw.drain(..crlf + 2); if chunk_size == 0 {
444 loop {
448 let line_end = loop {
449 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
450 break p;
451 }
452 let n = stream.read(tmp).map_err(|e| e.to_string())?;
453 if n == 0 {
454 return Err("chunked: premature EOF in trailer".to_string());
455 }
456 raw.extend_from_slice(&tmp[..n]);
457 };
458 raw.drain(..line_end + 2);
459 if line_end == 0 {
460 break;
461 }
462 }
463 break;
464 }
465
466 while raw.len() < chunk_size + 2 {
468 let n = stream.read(tmp).map_err(|e| e.to_string())?;
469 if n == 0 {
470 return Err("chunked: premature EOF reading chunk body".to_string());
471 }
472 raw.extend_from_slice(&tmp[..n]);
473 }
474 decoded.extend_from_slice(&raw[..chunk_size]);
475 raw.drain(..chunk_size + 2); }
477
478 Ok(decoded)
479}
480
481fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
484 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
485 buf.clear();
486 for line in header_str.lines() {
487 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
488 continue;
489 }
490 buf.extend_from_slice(line.as_bytes());
491 buf.extend_from_slice(b"\r\n");
492 }
493 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
494 buf.extend_from_slice(decoded);
495}
496
497pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
500 "content-length", "transfer-encoding", "connection", "keep-alive",
501 "proxy-connection", "te", "trailer", "upgrade",
502];
503
504#[cfg_attr(not(feature = "http2"), allow(dead_code))]
513pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
514 response.http_version.clear();
515 response.http_version.push_str(crate::http::VERSION.http_1_1);
516 let has_body = !response.content_range_list.is_empty();
517 if let [range] = response.content_range_list.as_mut_slice() {
518 if let Some(h) = response
519 .headers
520 .iter()
521 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
522 {
523 range.content_type.clear();
524 range.content_type.push_str(&h.value);
525 }
526 }
527 response.headers.retain(|h| {
530 if h.name.eq_ignore_ascii_case("content-type") {
531 return !has_body;
533 }
534 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
535 });
536 response
537}
538
539pub(crate) fn bad_gateway() -> Response {
540 let cr = Range::get_content_range(
541 b"502 Bad Gateway".to_vec(),
542 MimeType::TEXT_PLAIN.to_string(),
543 );
544 let mut r = Response::new();
545 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
546 r.reason_phrase = STATUS_CODE_REASON_PHRASE
547 .n502_bad_gateway
548 .reason_phrase
549 .to_string();
550 r.content_range_list = vec![cr];
551 r
552}
553
554const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
562 prefix: Vec<u8>,
563 prefix_pos: usize,
564 inner: R,
565}
566
567impl<R: Read + Send> ConcatReader<R> {
568 fn new(prefix: Vec<u8>, inner: R) -> Self {
569 ConcatReader { prefix, prefix_pos: 0, inner }
570 }
571}
572
573impl<R: Read + Send> Read for ConcatReader<R> {
574 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
575 if self.prefix_pos < self.prefix.len() {
576 let avail = &self.prefix[self.prefix_pos..];
577 let n = buf.len().min(avail.len());
578 buf[..n].copy_from_slice(&avail[..n]);
579 self.prefix_pos += n;
580 return Ok(n);
581 }
582 self.inner.read(buf)
583 }
584}
585
586
587pub(crate) const POOL_MAX_IDLE: usize = 64;
591
592pub(crate) fn shared_pool() -> &'static ConnPool {
595 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
596 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
597}
598
599pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
602 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
603}
604
605impl UpstreamIo for TcpStream {
606 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
607 self.set_read_timeout(Some(read))?;
608 self.set_write_timeout(Some(write))
609 }
610}
611
612#[cfg(any(feature = "http-client", feature = "http2"))]
613pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
614
615#[cfg(any(feature = "http-client", feature = "http2"))]
616impl UpstreamIo for TlsStream {
617 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
618 self.sock.set_io_timeouts(read, write)
619 }
620}
621
622pub(crate) fn forward_upstream(
628 plain_pool: &ConnPool,
629 request: &Request,
630 client_ip: &str,
631 host: &str,
632 port: u16,
633 tls: bool,
634 connect_timeout: Duration,
635 read_timeout: Duration,
636) -> Result<Response, String> {
637 forward_upstream_trusting(plain_pool, request, client_ip, host, port, tls, None, connect_timeout, read_timeout)
638}
639
640#[allow(clippy::too_many_arguments)]
643pub(crate) fn forward_upstream_trusting(
644 plain_pool: &ConnPool,
645 request: &Request,
646 client_ip: &str,
647 host: &str,
648 port: u16,
649 tls: bool,
650 trust: Option<&UpstreamTls>,
651 connect_timeout: Duration,
652 read_timeout: Duration,
653) -> Result<Response, String> {
654 #[cfg(not(target_arch = "wasm32"))]
657 let span = crate::otel::outbound_span(&request.method);
658 #[cfg(not(target_arch = "wasm32"))]
659 if let Some(span) = &span {
660 let scheme = if tls { "https" } else { "http" };
661 let path = request.request_uri.as_str();
662 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
663 span.set_attribute(key, value);
664 }
665 }
666 let result = forward_upstream_untraced(plain_pool, request, client_ip, host, port, tls, trust, connect_timeout, read_timeout);
667 #[cfg(not(target_arch = "wasm32"))]
668 if let Some(span) = &span {
669 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
670 }
671 result
672}
673
674#[allow(clippy::too_many_arguments)]
675fn forward_upstream_untraced(
676 plain_pool: &ConnPool,
677 request: &Request,
678 client_ip: &str,
679 host: &str,
680 port: u16,
681 tls: bool,
682 trust: Option<&UpstreamTls>,
683 connect_timeout: Duration,
684 read_timeout: Duration,
685) -> Result<Response, String> {
686 if tls {
687 #[cfg(any(feature = "http-client", feature = "http2"))]
688 {
689 let (pool, config) = match trust {
690 Some(t) => (t.pool, Arc::clone(&t.config)),
691 None => (shared_tls_pool(), tls_client_config()),
692 };
693 return forward_via(pool, request, client_ip, host, port, host, read_timeout, || {
694 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
695 });
696 }
697 #[cfg(not(any(feature = "http-client", feature = "http2")))]
698 {
699 let _ = trust;
700 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
701 }
702 }
703 forward_pooled(plain_pool, request, client_ip, host, port, connect_timeout, read_timeout)
704}
705
706pub struct UpstreamTls {
714 #[cfg(any(feature = "http-client", feature = "http2"))]
715 config: Arc<rustls::ClientConfig>,
716 #[cfg(any(feature = "http-client", feature = "http2"))]
717 pool: &'static Pool<TlsStream>,
718 #[cfg(feature = "http2")]
720 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
721 #[cfg(feature = "http2")]
723 h2_config: Arc<rustls::ClientConfig>,
724}
725
726impl UpstreamTls {
727 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
729 UpstreamTls::new(Some(path), None)
730 }
731
732 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
736 #[cfg(any(feature = "http-client", feature = "http2"))]
737 {
738 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
739 let roots = match ca_file {
740 Some(path) => {
741 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
742 let certs = CertificateDer::pem_slice_iter(&bytes)
743 .collect::<Result<Vec<_>, _>>()
744 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
745 if certs.is_empty() {
746 return Err(format!("no certificates in CA file '{path}'"));
747 }
748 let mut roots = rustls::RootCertStore::empty();
749 for cert in certs {
750 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
751 }
752 roots
753 }
754 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
755 };
756 let builder = crate::tls_client_builder().with_root_certificates(roots);
757 let config = match client_cert {
758 None => builder.with_no_client_auth(),
759 Some((cert_path, key_path)) => {
760 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
761 let chain = CertificateDer::pem_slice_iter(&bytes)
762 .collect::<Result<Vec<_>, _>>()
763 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
764 if chain.is_empty() {
765 return Err(format!("no certificate in '{cert_path}'"));
766 }
767 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
768 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
769 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
770 }
771 };
772 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
773 Ok(UpstreamTls::with_config(config, &label))
774 }
775 #[cfg(not(any(feature = "http-client", feature = "http2")))]
776 {
777 let _ = client_cert;
778 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
779 }
780 }
781
782 #[cfg(any(feature = "http-client", feature = "http2"))]
783 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
784 #[cfg(feature = "http2")]
785 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
786 UpstreamTls {
787 config: Arc::new(config),
788 pool: tls_pool_for_ca(label),
789 #[cfg(feature = "http2")]
790 async_pool: async_tls_pool_for_ca(label),
791 #[cfg(feature = "http2")]
792 h2_config,
793 }
794 }
795
796 #[cfg(feature = "http2")]
798 pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
799 &self.h2_config
800 }
801
802 pub(crate) fn reject_all(label: &str) -> UpstreamTls {
806 #[cfg(any(feature = "http-client", feature = "http2"))]
807 {
808 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
809 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
810 }
811 #[cfg(not(any(feature = "http-client", feature = "http2")))]
812 {
813 let _ = label;
814 UpstreamTls {}
815 }
816 }
817
818 #[cfg(feature = "http2")]
820 pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
821 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
822 }
823
824 #[cfg(any(feature = "http-client", feature = "http2"))]
826 pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
827 Arc::clone(&self.config)
828 }
829}
830
831#[cfg(feature = "http2")]
834fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
835 static POOLS: std::sync::OnceLock<
836 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
837 > = std::sync::OnceLock::new();
838 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
839 pools
840 .entry(path.to_string())
841 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
842}
843
844#[cfg(any(feature = "http-client", feature = "http2"))]
846fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
847 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
848 std::sync::OnceLock::new();
849 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
850 pools
851 .entry(path.to_string())
852 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
853}
854
855pub(crate) fn forward_pooled(
859 pool: &ConnPool,
860 request: &Request,
861 client_ip: &str,
862 host: &str,
863 port: u16,
864 connect_timeout: Duration,
865 read_timeout: Duration,
866) -> Result<Response, String> {
867 forward_via(pool, request, client_ip, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
868}
869
870#[cfg(feature = "gateway-api")]
873#[allow(clippy::too_many_arguments)]
874pub(crate) fn forward_pooled_with_host(
875 pool: &ConnPool,
876 request: &Request,
877 client_ip: &str,
878 host: &str,
879 port: u16,
880 host_header: &str,
881 connect_timeout: Duration,
882 read_timeout: Duration,
883) -> Result<Response, String> {
884 forward_via(pool, request, client_ip, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
885}
886
887#[allow(clippy::too_many_arguments)]
894fn forward_via<S: UpstreamIo>(
895 pool: &Pool<S>,
896 request: &Request,
897 client_ip: &str,
898 host: &str,
899 port: u16,
900 host_header: &str,
901 read_timeout: Duration,
902 connect: impl Fn() -> Result<S, String>,
903) -> Result<Response, String> {
904 let key = format!("{}:{}", host, port);
905 if let Some(stream) = pool.acquire(&key) {
906 match exchange(pool, &key, stream, request, host_header, client_ip, read_timeout) {
907 Exchange::Done(response) => return Ok(response),
908 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
910 }
911 }
912 let stream = connect()?;
913 match exchange(pool, &key, stream, request, host_header, client_ip, read_timeout) {
914 Exchange::Done(response) => Ok(response),
915 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
916 }
917}
918
919fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
923 let key = format!("{}:{}", host, port);
924 let addrs: Vec<_> = key
925 .to_socket_addrs()
926 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
927 .collect();
928 let mut last_error = format!("no address resolved for {}", key);
929 for addr in addrs {
930 match TcpStream::connect_timeout(&addr, connect_timeout) {
931 Ok(stream) => {
932 let _ = stream.set_nodelay(true);
933 return Ok(stream);
934 }
935 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
936 }
937 }
938 Err(last_error)
939}
940
941#[cfg(any(feature = "http-client", feature = "http2"))]
943fn shared_tls_pool() -> &'static Pool<TlsStream> {
944 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
945 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
946}
947
948#[cfg(any(feature = "http-client", feature = "http2"))]
952pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
953 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
954 Arc::clone(CONFIG.get_or_init(|| {
955 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
956 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
957 }))
958}
959
960#[cfg(any(feature = "http-client", feature = "http2"))]
961fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
962 let tcp = connect_tcp(host, port, connect_timeout)?;
963 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
964 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
965 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
966 Ok(rustls::StreamOwned::new(conn, tcp))
967}
968
969fn is_idempotent(method: &str) -> bool {
970 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
971}
972
973enum Exchange {
974 Done(Response),
975 NoResponse(String),
977 Failed(String),
978}
979
980fn exchange<S: UpstreamIo>(
983 pool: &Pool<S>,
984 key: &str,
985 mut stream: S,
986 request: &Request,
987 host: &str,
988 client_ip: &str,
989 read_timeout: Duration,
990) -> Exchange {
991 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
992 return Exchange::Failed(e.to_string());
993 }
994 let req_bytes = build_request(request, host, client_ip, true);
996 if let Err(e) = stream.write_all(&req_bytes) {
997 return Exchange::NoResponse(format!("write to backend failed: {}", e));
998 }
999
1000 let mut tmp = [0u8; 4096];
1001 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1002 Ok(head) => head,
1003 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1004 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1005 };
1006 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1007
1008 if response_has_no_body(&request.method, &header_lower) {
1009 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1010 pool.release(key, stream);
1011 }
1012 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1013 Ok(response) => Exchange::Done(response),
1014 Err(e) => Exchange::Failed(e),
1015 };
1016 }
1017
1018 if should_stream_response(&header_lower) {
1019 return match parse_status_and_headers(&header_bytes) {
1022 Ok(mut response) => {
1023 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1024 Exchange::Done(response)
1025 }
1026 Err(e) => Exchange::Failed(e),
1027 };
1028 }
1029 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1031 Ok((resp_bytes, reusable)) => {
1032 if reusable {
1033 pool.release(key, stream);
1034 }
1035 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1036 Ok(response) => Exchange::Done(response),
1037 Err(e) => Exchange::Failed(e),
1038 }
1039 }
1040 Err(e) => Exchange::Failed(e),
1041 }
1042}
1043
1044#[cfg(test)]
1049fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1050 read_response_head(stream, tmp).map_err(|e| match e {
1051 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1052 })
1053}
1054
1055enum HeadError {
1059 NothingReceived(String),
1060 Partial(String),
1061}
1062
1063fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1064 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1065}
1066
1067fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1072 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1073 let mut interim = 0;
1074 while is_interim_response(&head) {
1075 interim += 1;
1076 if interim > MAX_INTERIM_RESPONSES {
1077 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1078 }
1079 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1080 }
1081 Ok((head, rest))
1082}
1083
1084fn read_response_head_from<S: Read>(
1088 stream: &mut S,
1089 tmp: &mut [u8],
1090 mut buf: Vec<u8>,
1091 received: bool,
1092) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1093 loop {
1094 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1095 let body_prefix = buf[pos + 4..].to_vec();
1096 buf.truncate(pos + 4);
1097 return Ok((buf, body_prefix));
1098 }
1099 let nothing = buf.is_empty() && !received;
1100 let n = match stream.read(tmp) {
1101 Ok(n) => n,
1102 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1103 Err(e) => return Err(HeadError::Partial(e.to_string())),
1104 };
1105 if n == 0 {
1106 let msg = "backend closed connection before headers were complete".to_string();
1107 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1108 }
1109 buf.extend_from_slice(&tmp[..n]);
1110 }
1111}
1112
1113pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1115
1116pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1119 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1120 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1121 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1122}
1123
1124pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1128 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1129 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1130}
1131
1132pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1139 let is_sse = header_lower.lines().any(|l| {
1140 l.starts_with("content-type:") && l.contains("text/event-stream")
1141 });
1142 let is_chunked = header_lower.lines().any(|l| {
1143 l.starts_with("transfer-encoding:") && l.contains("chunked")
1144 });
1145 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1146 l.strip_prefix("content-length:")?.trim().parse().ok()
1147 });
1148 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1149 is_sse || is_chunked || is_large
1150}
1151
1152#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1158pub(crate) struct HeadFacts {
1159 pub(crate) status_no_body: bool,
1161 pub(crate) sse: bool,
1162 pub(crate) chunked: bool,
1163 pub(crate) content_length: Option<usize>,
1165 pub(crate) connection_close: bool,
1166}
1167
1168#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1169impl HeadFacts {
1170 pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1171 let text = std::str::from_utf8(head).unwrap_or("");
1172 let status = text.split_whitespace().nth(1).unwrap_or("");
1173 let mut facts = HeadFacts {
1174 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1175 sse: false,
1176 chunked: false,
1177 content_length: None,
1178 connection_close: false,
1179 };
1180 for line in text.lines() {
1181 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1182 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1183 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1184 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1185 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1186 if facts.content_length.is_none() {
1187 facts.content_length = value.trim().parse().ok();
1188 }
1189 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1190 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1191 }
1192 }
1193 facts
1194 }
1195
1196 pub(crate) fn no_body(&self, method: &str) -> bool {
1198 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1199 }
1200
1201 pub(crate) fn should_stream(&self) -> bool {
1203 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1204 }
1205}
1206
1207fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1208 let bytes = line.as_bytes();
1209 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1211 .then(|| &line[prefix.len()..])
1212}
1213
1214fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1215 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1216}
1217
1218fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1220 let s = std::str::from_utf8(header_bytes)
1221 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1222 let mut lines = s.lines();
1223 let status_line = lines.next().ok_or("empty backend response")?;
1224 let mut parts = status_line.splitn(3, ' ');
1225 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1226 let status_code: i16 = parts
1227 .next()
1228 .unwrap_or("502")
1229 .parse()
1230 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1231 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1232 let mut headers = Vec::new();
1233 for line in lines {
1234 let line = line.trim_end_matches('\r');
1235 if line.is_empty() { break; }
1236 if let Some(colon) = line.find(':') {
1237 headers.push(crate::header::Header {
1238 name: line[..colon].trim().to_string().into(),
1239 value: (line[colon + 1..].trim().to_string()).into(),
1240 });
1241 }
1242 }
1243 Ok(Response {
1244 http_version,
1245 status_code,
1246 reason_phrase,
1247 headers,
1248 content_range_list: vec![],
1249 stream_file: None,
1250 stream_pipe: None,
1251 trailers: None,
1252 })
1253}
1254
1255fn read_response_from_partial<S: Read>(
1261 stream: &mut S,
1262 header_bytes: Vec<u8>,
1263 body_prefix: Vec<u8>,
1264 tmp: &mut [u8],
1265) -> Result<(Vec<u8>, bool), String> {
1266 let header_end = header_bytes.len();
1267 let mut buf = header_bytes;
1268 buf.extend_from_slice(&body_prefix);
1269
1270 let header_lower =
1271 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1272 let connection_close =
1273 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1274 let is_chunked = header_lower
1275 .lines()
1276 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1277 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1278 l.strip_prefix("content-length:")?.trim().parse().ok()
1279 });
1280
1281 if is_chunked {
1282 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1283 rewrite_as_content_length(&mut buf, header_end, &decoded);
1284 Ok((buf, !connection_close))
1285 } else if let Some(len) = content_length {
1286 while buf.len() < header_end + len {
1287 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1288 if n == 0 { break; }
1289 buf.extend_from_slice(&tmp[..n]);
1290 }
1291 Ok((buf, !connection_close))
1292 } else {
1293 loop {
1294 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1295 if n == 0 { break; }
1296 buf.extend_from_slice(&tmp[..n]);
1297 }
1298 Ok((buf, false))
1299 }
1300}
1301
1302struct Backend {
1305 host: String,
1306 port: u16,
1307 tls: bool,
1310}
1311
1312impl Backend {
1313 fn parse(url: &str) -> Option<Self> {
1314 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1315 (r, true, 443u16)
1316 } else if let Some(r) = url.strip_prefix("h2s://") {
1317 (r, true, 443u16)
1318 } else if let Some(r) = url.strip_prefix("grpcs://") {
1319 (r, true, 443u16)
1320 } else if let Some(r) = url.strip_prefix("http://") {
1321 (r, false, 80u16)
1322 } else if let Some(r) = url.strip_prefix("h2://") {
1323 (r, false, 80u16)
1324 } else if let Some(r) = url.strip_prefix("grpc://") {
1325 (r, false, 80u16)
1326 } else {
1327 (url, false, 80u16)
1328 };
1329 let host_port = rest.split('/').next().unwrap_or(rest);
1331 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1332 let port_str = &host_port[colon + 1..];
1333 if let Ok(p) = port_str.parse::<u16>() {
1334 (host_port[..colon].to_string(), p)
1335 } else {
1336 (host_port.to_string(), default_port)
1337 }
1338 } else {
1339 (host_port.to_string(), default_port)
1340 };
1341 if host.is_empty() {
1342 return None;
1343 }
1344 Some(Backend { host, port, tls })
1345 }
1346}
1347
1348#[cfg(feature = "http2")]
1359pub struct H2ReverseProxy {
1360 inner: ReverseProxy,
1361}
1362
1363#[cfg(feature = "http2")]
1364impl H2ReverseProxy {
1365 pub fn new<I, S>(backends: I) -> Self
1376 where
1377 I: IntoIterator<Item = S>,
1378 S: AsRef<str>,
1379 {
1380 H2ReverseProxy {
1381 inner: ReverseProxy::new(backends),
1382 }
1383 }
1384
1385 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1387 self.inner = self.inner.path_prefix(prefix);
1388 self
1389 }
1390
1391 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1393 self.inner = self.inner.connect_timeout_ms(ms);
1394 self
1395 }
1396
1397 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1402 self.inner = self.inner.read_timeout_ms(ms);
1403 self
1404 }
1405
1406 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1409 self.inner = self.inner.tls_ca_file(path)?;
1410 Ok(self)
1411 }
1412
1413 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1416 self.inner = self.inner.tls_client_cert(cert, key)?;
1417 Ok(self)
1418 }
1419
1420 fn matches(&self, request: &Request) -> bool {
1421 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1422 }
1423}
1424
1425#[cfg(feature = "http2")]
1426impl crate::middleware::Middleware for H2ReverseProxy {
1427 fn handle(
1428 &self,
1429 request: &crate::request::Request,
1430 connection: &crate::server::ConnectionInfo,
1431 next: &dyn crate::application::Application,
1432 ) -> Result<crate::response::Response, String> {
1433 if !self.matches(request) {
1434 return next.execute(request, connection);
1435 }
1436 if self.inner.backends.is_empty() {
1437 return Ok(bad_gateway());
1438 }
1439 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1441 let n = self.inner.backends.len();
1442 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1443 for attempt in 0..attempts {
1444 let backend = &self.inner.backends[(start + attempt) % n];
1445 let tls = backend.tls.then(|| match &self.inner.trust {
1446 Some(trust) => trust.h2_config(),
1447 None => h2_upstream::default_tls(),
1448 });
1449 let target = h2_upstream::Target {
1450 backend,
1451 tls,
1452 connect_timeout: self.inner.connect_timeout,
1453 read_timeout: self.inner.read_timeout,
1454 authority: None,
1455 };
1456 if let Ok(response) = h2_upstream::forward(request, &connection.client.ip, &target) {
1457 return Ok(response);
1458 }
1459 }
1460 Ok(bad_gateway())
1461 }
1462
1463 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1466 true
1467 }
1468
1469 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1472 self.matches(request)
1473 }
1474}
1475
1476#[cfg(feature = "http2")]
1480pub(crate) fn forward_h2(
1481 request: &Request,
1482 client_ip: &str,
1483 url: &str,
1484 tls: bool,
1485 trust: Option<&UpstreamTls>,
1486 connect_timeout: Duration,
1487 read_timeout: Duration,
1488) -> Result<Response, String> {
1489 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1490 backend.tls |= tls;
1491 let tls = backend.tls.then(|| match trust {
1492 Some(t) => t.h2_config(),
1493 None => h2_upstream::default_tls(),
1494 });
1495 h2_upstream::forward(request, client_ip, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1496}
1497
1498#[cfg(feature = "gateway-api")]
1503#[allow(clippy::too_many_arguments)]
1504pub(crate) fn forward_tls_with_host(
1505 config: &Arc<rustls::ClientConfig>,
1506 label: &str,
1507 request: &Request,
1508 client_ip: &str,
1509 ip: &str,
1510 port: u16,
1511 server_name: &str,
1512 host_header: &str,
1513 connect_timeout: Duration,
1514 read_timeout: Duration,
1515) -> Result<Response, String> {
1516 let pool = tls_pool_for_ca(label);
1517 forward_via(pool, request, client_ip, ip, port, host_header, read_timeout, || {
1518 let tcp = connect_tcp(ip, port, connect_timeout)?;
1519 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1520 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1521 Ok(rustls::StreamOwned::new(conn, tcp))
1522 })
1523}
1524
1525#[cfg(feature = "gateway-api")]
1529pub(crate) fn forward_h2c_with_host(
1530 request: &Request,
1531 client_ip: &str,
1532 host: &str,
1533 port: u16,
1534 authority: &str,
1535 connect_timeout: Duration,
1536 read_timeout: Duration,
1537) -> Result<Response, String> {
1538 let backend = Backend { host: host.to_string(), port, tls: false };
1539 h2_upstream::forward(request, client_ip, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1540}
1541
1542#[cfg(feature = "http2")]
1562pub struct GrpcProxy {
1563 inner: H2ReverseProxy,
1564}
1565
1566#[cfg(feature = "http2")]
1567impl GrpcProxy {
1568 pub fn new<I, S>(backends: I) -> Self
1578 where
1579 I: IntoIterator<Item = S>,
1580 S: AsRef<str>,
1581 {
1582 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1583 }
1584
1585 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1587 self.inner = self.inner.path_prefix(prefix);
1588 self
1589 }
1590
1591 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1593 self.inner = self.inner.connect_timeout_ms(ms);
1594 self
1595 }
1596
1597 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1599 self.inner = self.inner.read_timeout_ms(ms);
1600 self
1601 }
1602
1603 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1605 self.inner = self.inner.tls_ca_file(path)?;
1606 Ok(self)
1607 }
1608
1609 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1612 self.inner = self.inner.tls_client_cert(cert, key)?;
1613 Ok(self)
1614 }
1615}
1616
1617#[cfg(feature = "http2")]
1618impl crate::middleware::Middleware for GrpcProxy {
1619 fn handle(
1620 &self,
1621 request: &crate::request::Request,
1622 connection: &crate::server::ConnectionInfo,
1623 next: &dyn crate::application::Application,
1624 ) -> Result<crate::response::Response, String> {
1625 if is_grpc(request) {
1626 self.inner.handle(request, connection, next)
1627 } else {
1628 next.execute(request, connection)
1629 }
1630 }
1631
1632 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1633 true
1634 }
1635
1636 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1637 is_grpc(request) && self.inner.matches(request)
1638 }
1639}
1640
1641#[cfg(feature = "http2")]
1642fn is_grpc(request: &Request) -> bool {
1643 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1644}
1645
1646#[cfg(test)]
1649mod backend_parse_tests {
1650 use super::Backend;
1651
1652 fn parse(url: &str) -> Option<(String, u16, bool)> {
1653 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1654 }
1655
1656 #[test]
1657 fn bare_host_port() {
1658 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1659 }
1660
1661 #[test]
1662 fn http_scheme() {
1663 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1664 }
1665
1666 #[test]
1667 fn h2_scheme_plain() {
1668 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1669 }
1670
1671 #[test]
1672 fn grpc_scheme_plain() {
1673 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1674 }
1675
1676 #[test]
1677 fn https_scheme_sets_tls_and_default_port() {
1678 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1679 }
1680
1681 #[test]
1682 fn https_scheme_explicit_port() {
1683 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1684 }
1685
1686 #[test]
1687 fn h2s_scheme_sets_tls() {
1688 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1689 }
1690
1691 #[test]
1692 fn h2s_scheme_explicit_port() {
1693 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1694 }
1695
1696 #[test]
1697 fn grpcs_scheme_sets_tls() {
1698 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1699 }
1700
1701 #[test]
1702 fn grpcs_scheme_explicit_port() {
1703 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1704 }
1705
1706 #[test]
1707 fn empty_host_returns_none() {
1708 assert_eq!(None, parse("https://"));
1709 }
1710
1711 #[test]
1712 fn bare_host_no_port_defaults_to_80() {
1713 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1714 }
1715}