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 = "http2")]
34pub mod grpc_web;
35#[cfg(feature = "http2")]
36pub use grpc_web::GrpcWebLayer;
37
38#[cfg(test)]
39mod tests;
40
41use std::io::{Read, Write};
42use crate::ext::decimal::Decimal;
43use std::net::{TcpStream, ToSocketAddrs};
44use std::sync::atomic::{AtomicUsize, Ordering};
45use std::sync::Arc;
46use std::time::Duration;
47
48pub use pool::{ConnPool, Pool};
49
50use crate::application::Application;
51use crate::core::New;
52use crate::middleware::Middleware;
53use crate::mime_type::MimeType;
54use crate::range::Range;
55use crate::request::Request;
56use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
57use crate::server::ConnectionInfo;
58
59const HOP_BY_HOP: &[&str] = &[
63 "connection",
64 "keep-alive",
65 "proxy-authenticate",
66 "proxy-authorization",
67 "proxy-connection",
68 "te",
69 "trailer",
70 "transfer-encoding",
71 "upgrade",
72];
73
74pub enum LoadBalancing {
76 RoundRobin,
78}
79
80pub struct ReverseProxy {
95 backends: Vec<Backend>,
96 path_prefix: Option<String>,
97 connect_timeout: Duration,
98 read_timeout: Duration,
99 counter: AtomicUsize,
100 pool: Arc<ConnPool>,
101 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
102 trust: Option<Arc<UpstreamTls>>,
103 tls_ca: Option<String>,
105 tls_client: Option<(String, String)>,
106}
107
108impl ReverseProxy {
109 pub fn new<I, S>(backends: I) -> Self
113 where
114 I: IntoIterator<Item = S>,
115 S: AsRef<str>,
116 {
117 Self {
118 backends: backends
119 .into_iter()
120 .filter_map(|u| Backend::parse(u.as_ref()))
121 .collect(),
122 path_prefix: None,
123 connect_timeout: Duration::from_secs(5),
124 read_timeout: Duration::from_secs(30),
125 counter: AtomicUsize::new(0),
126 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
127 breaker: None,
128 trust: None,
129 tls_ca: None,
130 tls_client: None,
131 }
132 }
133
134 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
152 self.breaker = Some(breaker);
153 self
154 }
155
156 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
161 self.path_prefix = Some(prefix.into());
162 self
163 }
164
165 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
167 self
168 }
169
170 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
172 self.connect_timeout = Duration::from_millis(ms);
173 self
174 }
175
176 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
178 self.read_timeout = Duration::from_millis(ms);
179 self
180 }
181
182 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
187 self.pool = pool;
188 self
189 }
190
191 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
195 self.tls_ca = Some(path.to_string());
196 self.rebuild_trust()?;
197 Ok(self)
198 }
199
200 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
204 self.tls_client = Some((cert.to_string(), key.to_string()));
205 self.rebuild_trust()?;
206 Ok(self)
207 }
208
209 fn rebuild_trust(&mut self) -> Result<(), String> {
210 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
211 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
212 Ok(())
213 }
214
215 pub fn max_idle_conns(mut self, n: usize) -> Self {
217 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
218 self
219 }
220
221 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
222 if self.backends.is_empty() {
223 return Err("no backends configured".to_string());
224 }
225 let n = self.backends.len();
226 let start = self.counter.fetch_add(1, Ordering::Relaxed);
227 let mut last_err = "all backends failed".to_string();
228 for attempt in 0..n {
229 let idx = (start + attempt) % n;
230 let backend = &self.backends[idx];
231 let key = format!("{}:{}", backend.host, backend.port);
232
233 if let Some(breaker) = &self.breaker {
234 if !breaker.is_available(&key) {
235 last_err = format!("circuit open for {}", key);
236 continue;
237 }
238 }
239
240 match self.try_backend(request, connection, backend) {
241 Ok(resp) => {
242 if let Some(breaker) = &self.breaker {
243 breaker.record_success(&key);
244 }
245 return Ok(resp);
246 }
247 Err(e) => {
248 if let Some(breaker) = &self.breaker {
249 breaker.record_failure(&key);
250 }
251 last_err = e;
252 }
253 }
254 }
255 Err(last_err)
256 }
257
258 fn try_backend(
259 &self,
260 request: &Request,
261 connection: &ConnectionInfo,
262 backend: &Backend,
263 ) -> Result<Response, String> {
264 forward_upstream_trusting(
265 &self.pool,
266 request,
267 crate::proxy::Peer::of(connection),
268 &backend.host,
269 backend.port,
270 backend.tls,
271 self.trust.as_deref(),
272 self.connect_timeout,
273 self.read_timeout,
274 )
275 }
276}
277
278impl Middleware for ReverseProxy {
279 fn handle(
280 &self,
281 request: &Request,
282 connection: &ConnectionInfo,
283 next: &dyn Application,
284 ) -> Result<Response, String> {
285 if let Some(prefix) = &self.path_prefix {
286 if !request.request_uri.starts_with(prefix.as_str()) {
287 return next.execute(request, connection);
288 }
289 }
290 match self.proxy(request, connection) {
291 Ok(resp) => Ok(resp),
292 Err(_) => Ok(bad_gateway()),
293 }
294 }
295}
296
297#[derive(Clone, Copy, Debug)]
302pub(crate) struct Peer<'a> {
303 pub ip: &'a str,
304 pub tls: bool,
305}
306
307impl<'a> Peer<'a> {
308 pub(crate) fn of(connection: &'a ConnectionInfo) -> Self {
309 Peer { ip: &connection.client.ip, tls: connection.tls }
310 }
311
312 pub(crate) fn plain(ip: &'a str) -> Self {
314 Peer { ip, tls: false }
315 }
316
317 pub(crate) fn proto(&self) -> &'static str {
318 if self.tls { "https" } else { "http" }
319 }
320}
321
322pub(crate) fn build_request(
323 request: &Request,
324 backend_host: &str,
325 client: Peer<'_>,
326 keep_alive: bool,
327) -> Vec<u8> {
328 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
334 #[cfg(not(target_arch = "wasm32"))]
337 let trace = crate::otel::propagation_headers();
338 #[cfg(target_arch = "wasm32")]
339 let trace: Option<(String, Option<String>)> = None;
340 let forwarded = |h: &&crate::header::Header| {
341 !h.name.is_empty()
343 && !is_hop_by_hop_or_host(&h.name)
344 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
345 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
346 && !connection.is_some_and(|c| connection_listed(c, &h.name))
347 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
348 };
349 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
350 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
354 let forwarded_host = match request.header("X-Forwarded-Host") {
355 Some(_) => None,
356 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
357 };
358 let body_len = Decimal::new(request.body.len() as u128);
359 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
360 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
361 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
362 + forwarded_host.map_or(0, |v| v.len() + 20)
363 + forwarded_proto.map_or(0, |v| v.len() + 21)
364 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
365 + 13
366 + 24
367 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
368 + 2
369 + request.body.len();
370 let mut out: Vec<u8> = Vec::with_capacity(capacity);
371 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
372 out.extend_from_slice(part.as_bytes());
373 }
374 for h in request.headers.iter().filter(forwarded) {
375 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
376 out.extend_from_slice(part.as_bytes());
377 }
378 }
379 out.extend_from_slice(b"X-Forwarded-For: ");
380 if let Some(prior) = prior_xff {
381 out.extend_from_slice(prior.as_bytes());
382 out.extend_from_slice(b", ");
383 }
384 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
385 out.extend_from_slice(part.as_bytes());
386 }
387 if let Some(host) = forwarded_host {
388 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
389 out.extend_from_slice(part.as_bytes());
390 }
391 }
392 if let Some(proto) = forwarded_proto {
393 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
394 out.extend_from_slice(part.as_bytes());
395 }
396 }
397 if let Some((traceparent, baggage)) = &trace {
398 for part in ["traceparent: ", traceparent, "\r\n"] {
399 out.extend_from_slice(part.as_bytes());
400 }
401 if let Some(baggage) = baggage {
402 for part in ["baggage: ", baggage, "\r\n"] {
403 out.extend_from_slice(part.as_bytes());
404 }
405 }
406 }
407 if keep_alive {
408 out.extend_from_slice(b"Connection: keep-alive\r\n");
409 } else {
410 out.extend_from_slice(b"Connection: close\r\n");
411 }
412 if !request.body.is_empty() {
413 out.extend_from_slice(b"Content-Length: ");
414 out.extend_from_slice(body_len.as_str().as_bytes());
415 out.extend_from_slice(b"\r\n");
416 }
417 out.extend_from_slice(b"\r\n");
418 out.extend_from_slice(&request.body);
419 out
420}
421
422fn is_hop_by_hop_or_host(name: &str) -> bool {
427 if name.is_ascii() {
428 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
429 } else {
430 let lower = name.to_lowercase();
431 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
432 }
433}
434
435fn connection_listed(connection: &str, name: &str) -> bool {
438 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
439}
440
441fn decode_chunked<S: Read>(
446 stream: &mut S,
447 buf: &[u8],
448 header_end: usize,
449 tmp: &mut [u8],
450) -> Result<Vec<u8>, String> {
451 let mut raw: Vec<u8> = buf[header_end..].to_vec();
453 let mut decoded: Vec<u8> = Vec::new();
454
455 loop {
456 let crlf = loop {
458 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
459 break p;
460 }
461 let n = stream.read(tmp).map_err(|e| e.to_string())?;
462 if n == 0 {
463 return Err("chunked: premature EOF reading chunk size".to_string());
464 }
465 raw.extend_from_slice(&tmp[..n]);
466 };
467
468 let size_line = std::str::from_utf8(&raw[..crlf])
470 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
471 let size_str = size_line.split(';').next().unwrap_or("").trim();
472 let chunk_size = usize::from_str_radix(size_str, 16)
473 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
474 raw.drain(..crlf + 2); if chunk_size == 0 {
477 loop {
481 let line_end = loop {
482 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
483 break p;
484 }
485 let n = stream.read(tmp).map_err(|e| e.to_string())?;
486 if n == 0 {
487 return Err("chunked: premature EOF in trailer".to_string());
488 }
489 raw.extend_from_slice(&tmp[..n]);
490 };
491 raw.drain(..line_end + 2);
492 if line_end == 0 {
493 break;
494 }
495 }
496 break;
497 }
498
499 while raw.len() < chunk_size + 2 {
501 let n = stream.read(tmp).map_err(|e| e.to_string())?;
502 if n == 0 {
503 return Err("chunked: premature EOF reading chunk body".to_string());
504 }
505 raw.extend_from_slice(&tmp[..n]);
506 }
507 decoded.extend_from_slice(&raw[..chunk_size]);
508 raw.drain(..chunk_size + 2); }
510
511 Ok(decoded)
512}
513
514fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
517 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
518 buf.clear();
519 for line in header_str.lines() {
520 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
521 continue;
522 }
523 buf.extend_from_slice(line.as_bytes());
524 buf.extend_from_slice(b"\r\n");
525 }
526 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
527 buf.extend_from_slice(decoded);
528}
529
530pub(crate) const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
533 "content-length", "transfer-encoding", "connection", "keep-alive",
534 "proxy-connection", "te", "trailer", "upgrade",
535];
536
537#[cfg_attr(not(feature = "http2"), allow(dead_code))]
546pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
547 response.http_version.clear();
548 response.http_version.push_str(crate::http::VERSION.http_1_1);
549 let has_body = !response.content_range_list.is_empty();
550 if let [range] = response.content_range_list.as_mut_slice() {
551 if let Some(h) = response
552 .headers
553 .iter()
554 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
555 {
556 range.content_type.clear();
557 range.content_type.push_str(&h.value);
558 }
559 }
560 response.headers.retain(|h| {
563 if h.name.eq_ignore_ascii_case("content-type") {
564 return !has_body;
566 }
567 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
568 });
569 response
570}
571
572pub(crate) fn bad_gateway() -> Response {
573 let cr = Range::get_content_range(
574 b"502 Bad Gateway".to_vec(),
575 MimeType::TEXT_PLAIN.to_string(),
576 );
577 let mut r = Response::new();
578 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
579 r.reason_phrase = STATUS_CODE_REASON_PHRASE
580 .n502_bad_gateway
581 .reason_phrase
582 .to_string();
583 r.content_range_list = vec![cr];
584 r
585}
586
587const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
595 prefix: Vec<u8>,
596 prefix_pos: usize,
597 inner: R,
598}
599
600impl<R: Read + Send> ConcatReader<R> {
601 fn new(prefix: Vec<u8>, inner: R) -> Self {
602 ConcatReader { prefix, prefix_pos: 0, inner }
603 }
604}
605
606impl<R: Read + Send> Read for ConcatReader<R> {
607 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
608 if self.prefix_pos < self.prefix.len() {
609 let avail = &self.prefix[self.prefix_pos..];
610 let n = buf.len().min(avail.len());
611 buf[..n].copy_from_slice(&avail[..n]);
612 self.prefix_pos += n;
613 return Ok(n);
614 }
615 self.inner.read(buf)
616 }
617}
618
619
620pub(crate) const POOL_MAX_IDLE: usize = 64;
624
625pub(crate) fn shared_pool() -> &'static ConnPool {
628 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
629 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
630}
631
632pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
635 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
636}
637
638impl UpstreamIo for TcpStream {
639 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
640 self.set_read_timeout(Some(read))?;
641 self.set_write_timeout(Some(write))
642 }
643}
644
645#[cfg(any(feature = "http-client", feature = "http2"))]
646pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
647
648#[cfg(any(feature = "http-client", feature = "http2"))]
649impl UpstreamIo for TlsStream {
650 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
651 self.sock.set_io_timeouts(read, write)
652 }
653}
654
655pub(crate) fn forward_upstream(
661 plain_pool: &ConnPool,
662 request: &Request,
663 client: Peer<'_>,
664 host: &str,
665 port: u16,
666 tls: bool,
667 connect_timeout: Duration,
668 read_timeout: Duration,
669) -> Result<Response, String> {
670 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
671}
672
673#[allow(clippy::too_many_arguments)]
676pub(crate) fn forward_upstream_trusting(
677 plain_pool: &ConnPool,
678 request: &Request,
679 client: Peer<'_>,
680 host: &str,
681 port: u16,
682 tls: bool,
683 trust: Option<&UpstreamTls>,
684 connect_timeout: Duration,
685 read_timeout: Duration,
686) -> Result<Response, String> {
687 #[cfg(not(target_arch = "wasm32"))]
690 let span = crate::otel::outbound_span(&request.method);
691 #[cfg(not(target_arch = "wasm32"))]
692 if let Some(span) = &span {
693 let scheme = if tls { "https" } else { "http" };
694 let path = request.request_uri.as_str();
695 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
696 span.set_attribute(key, value);
697 }
698 }
699 let result = forward_upstream_untraced(plain_pool, request, client, host, port, tls, trust, connect_timeout, read_timeout);
700 #[cfg(not(target_arch = "wasm32"))]
701 if let Some(span) = &span {
702 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
703 }
704 result
705}
706
707#[allow(clippy::too_many_arguments)]
708fn forward_upstream_untraced(
709 plain_pool: &ConnPool,
710 request: &Request,
711 client: Peer<'_>,
712 host: &str,
713 port: u16,
714 tls: bool,
715 trust: Option<&UpstreamTls>,
716 connect_timeout: Duration,
717 read_timeout: Duration,
718) -> Result<Response, String> {
719 if tls {
720 #[cfg(any(feature = "http-client", feature = "http2"))]
721 {
722 let (pool, config) = match trust {
723 Some(t) => (t.pool, Arc::clone(&t.config)),
724 None => (shared_tls_pool(), tls_client_config()),
725 };
726 return forward_via(pool, request, client, host, port, host, read_timeout, || {
727 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
728 });
729 }
730 #[cfg(not(any(feature = "http-client", feature = "http2")))]
731 {
732 let _ = trust;
733 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
734 }
735 }
736 forward_pooled(plain_pool, request, client, host, port, connect_timeout, read_timeout)
737}
738
739pub struct UpstreamTls {
747 #[cfg(any(feature = "http-client", feature = "http2"))]
748 config: Arc<rustls::ClientConfig>,
749 #[cfg(any(feature = "http-client", feature = "http2"))]
750 pool: &'static Pool<TlsStream>,
751 #[cfg(feature = "http2")]
753 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
754 #[cfg(feature = "http2")]
756 h2_config: Arc<rustls::ClientConfig>,
757}
758
759impl UpstreamTls {
760 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
762 UpstreamTls::new(Some(path), None)
763 }
764
765 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
769 #[cfg(any(feature = "http-client", feature = "http2"))]
770 {
771 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
772 let roots = match ca_file {
773 Some(path) => {
774 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
775 let certs = CertificateDer::pem_slice_iter(&bytes)
776 .collect::<Result<Vec<_>, _>>()
777 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
778 if certs.is_empty() {
779 return Err(format!("no certificates in CA file '{path}'"));
780 }
781 let mut roots = rustls::RootCertStore::empty();
782 for cert in certs {
783 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
784 }
785 roots
786 }
787 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
788 };
789 let builder = crate::tls_client_builder().with_root_certificates(roots);
790 let config = match client_cert {
791 None => builder.with_no_client_auth(),
792 Some((cert_path, key_path)) => {
793 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
794 let chain = CertificateDer::pem_slice_iter(&bytes)
795 .collect::<Result<Vec<_>, _>>()
796 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
797 if chain.is_empty() {
798 return Err(format!("no certificate in '{cert_path}'"));
799 }
800 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
801 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
802 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
803 }
804 };
805 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
806 Ok(UpstreamTls::with_config(config, &label))
807 }
808 #[cfg(not(any(feature = "http-client", feature = "http2")))]
809 {
810 let _ = client_cert;
811 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
812 }
813 }
814
815 #[cfg(any(feature = "http-client", feature = "http2"))]
816 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
817 #[cfg(feature = "http2")]
818 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
819 UpstreamTls {
820 config: Arc::new(config),
821 pool: tls_pool_for_ca(label),
822 #[cfg(feature = "http2")]
823 async_pool: async_tls_pool_for_ca(label),
824 #[cfg(feature = "http2")]
825 h2_config,
826 }
827 }
828
829 #[cfg(feature = "http2")]
831 pub(crate) fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
832 &self.h2_config
833 }
834
835 pub(crate) fn reject_all(label: &str) -> UpstreamTls {
839 #[cfg(any(feature = "http-client", feature = "http2"))]
840 {
841 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
842 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
843 }
844 #[cfg(not(any(feature = "http-client", feature = "http2")))]
845 {
846 let _ = label;
847 UpstreamTls {}
848 }
849 }
850
851 #[cfg(feature = "http2")]
853 pub(crate) fn async_tls(&self) -> async_upstream::AsyncTls {
854 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
855 }
856
857 #[cfg(any(feature = "http-client", feature = "http2"))]
859 pub(crate) fn client_config(&self) -> Arc<rustls::ClientConfig> {
860 Arc::clone(&self.config)
861 }
862}
863
864#[cfg(feature = "http2")]
867fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
868 static POOLS: std::sync::OnceLock<
869 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
870 > = std::sync::OnceLock::new();
871 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
872 pools
873 .entry(path.to_string())
874 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
875}
876
877#[cfg(any(feature = "http-client", feature = "http2"))]
879fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
880 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
881 std::sync::OnceLock::new();
882 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
883 pools
884 .entry(path.to_string())
885 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))))
886}
887
888pub(crate) fn forward_pooled(
892 pool: &ConnPool,
893 request: &Request,
894 client: Peer<'_>,
895 host: &str,
896 port: u16,
897 connect_timeout: Duration,
898 read_timeout: Duration,
899) -> Result<Response, String> {
900 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
901}
902
903#[cfg(feature = "gateway-api")]
906#[allow(clippy::too_many_arguments)]
907pub(crate) fn forward_pooled_with_host(
908 pool: &ConnPool,
909 request: &Request,
910 client: Peer<'_>,
911 host: &str,
912 port: u16,
913 host_header: &str,
914 connect_timeout: Duration,
915 read_timeout: Duration,
916) -> Result<Response, String> {
917 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
918}
919
920#[allow(clippy::too_many_arguments)]
927fn forward_via<S: UpstreamIo>(
928 pool: &Pool<S>,
929 request: &Request,
930 client: Peer<'_>,
931 host: &str,
932 port: u16,
933 host_header: &str,
934 read_timeout: Duration,
935 connect: impl Fn() -> Result<S, String>,
936) -> Result<Response, String> {
937 let key = format!("{}:{}", host, port);
938 if let Some(stream) = pool.acquire(&key) {
939 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
940 Exchange::Done(response) => return Ok(response),
941 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
943 }
944 }
945 let stream = connect()?;
946 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
947 Exchange::Done(response) => Ok(response),
948 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
949 }
950}
951
952fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
956 let key = format!("{}:{}", host, port);
957 let addrs: Vec<_> = key
958 .to_socket_addrs()
959 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
960 .collect();
961 let mut last_error = format!("no address resolved for {}", key);
962 for addr in addrs {
963 match TcpStream::connect_timeout(&addr, connect_timeout) {
964 Ok(stream) => {
965 let _ = stream.set_nodelay(true);
966 return Ok(stream);
967 }
968 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
969 }
970 }
971 Err(last_error)
972}
973
974#[cfg(any(feature = "http-client", feature = "http2"))]
976fn shared_tls_pool() -> &'static Pool<TlsStream> {
977 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
978 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
979}
980
981#[cfg(any(feature = "http-client", feature = "http2"))]
985pub(crate) fn tls_client_config() -> Arc<rustls::ClientConfig> {
986 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
987 Arc::clone(CONFIG.get_or_init(|| {
988 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
989 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
990 }))
991}
992
993#[cfg(any(feature = "http-client", feature = "http2"))]
994fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
995 let tcp = connect_tcp(host, port, connect_timeout)?;
996 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
997 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
998 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
999 Ok(rustls::StreamOwned::new(conn, tcp))
1000}
1001
1002fn is_idempotent(method: &str) -> bool {
1003 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1004}
1005
1006enum Exchange {
1007 Done(Response),
1008 NoResponse(String),
1010 Failed(String),
1011}
1012
1013fn exchange<S: UpstreamIo>(
1016 pool: &Pool<S>,
1017 key: &str,
1018 mut stream: S,
1019 request: &Request,
1020 host: &str,
1021 client: Peer<'_>,
1022 read_timeout: Duration,
1023) -> Exchange {
1024 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1025 return Exchange::Failed(e.to_string());
1026 }
1027 let req_bytes = build_request(request, host, client, true);
1029 if let Err(e) = stream.write_all(&req_bytes) {
1030 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1031 }
1032
1033 let mut tmp = [0u8; 4096];
1034 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1035 Ok(head) => head,
1036 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1037 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1038 };
1039 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1040
1041 if response_has_no_body(&request.method, &header_lower) {
1042 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1043 pool.release(key, stream);
1044 }
1045 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1046 Ok(response) => Exchange::Done(response),
1047 Err(e) => Exchange::Failed(e),
1048 };
1049 }
1050
1051 if should_stream_response(&header_lower) {
1052 return match parse_status_and_headers(&header_bytes) {
1055 Ok(mut response) => {
1056 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
1057 Exchange::Done(response)
1058 }
1059 Err(e) => Exchange::Failed(e),
1060 };
1061 }
1062 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1064 Ok((resp_bytes, reusable)) => {
1065 if reusable {
1066 pool.release(key, stream);
1067 }
1068 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1069 Ok(response) => Exchange::Done(response),
1070 Err(e) => Exchange::Failed(e),
1071 }
1072 }
1073 Err(e) => Exchange::Failed(e),
1074 }
1075}
1076
1077#[cfg(test)]
1082fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1083 read_response_head(stream, tmp).map_err(|e| match e {
1084 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1085 })
1086}
1087
1088enum HeadError {
1092 NothingReceived(String),
1093 Partial(String),
1094}
1095
1096fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1097 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1098}
1099
1100fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1106 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1107 let mut interim = 0;
1108 while is_interim_response(&head) {
1109 crate::early_hints::forward_upstream(&head);
1110 interim += 1;
1111 if interim > MAX_INTERIM_RESPONSES {
1112 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1113 }
1114 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1115 }
1116 Ok((head, rest))
1117}
1118
1119fn read_response_head_from<S: Read>(
1123 stream: &mut S,
1124 tmp: &mut [u8],
1125 mut buf: Vec<u8>,
1126 received: bool,
1127) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1128 loop {
1129 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1130 let body_prefix = buf[pos + 4..].to_vec();
1131 buf.truncate(pos + 4);
1132 return Ok((buf, body_prefix));
1133 }
1134 let nothing = buf.is_empty() && !received;
1135 let n = match stream.read(tmp) {
1136 Ok(n) => n,
1137 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1138 Err(e) => return Err(HeadError::Partial(e.to_string())),
1139 };
1140 if n == 0 {
1141 let msg = "backend closed connection before headers were complete".to_string();
1142 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1143 }
1144 buf.extend_from_slice(&tmp[..n]);
1145 }
1146}
1147
1148pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
1150
1151pub(crate) fn is_interim_response(head: &[u8]) -> bool {
1154 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1155 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1156 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1157}
1158
1159pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1163 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1164 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1165}
1166
1167pub(crate) fn should_stream_response(header_lower: &str) -> bool {
1174 let is_sse = header_lower.lines().any(|l| {
1175 l.starts_with("content-type:") && l.contains("text/event-stream")
1176 });
1177 let is_chunked = header_lower.lines().any(|l| {
1178 l.starts_with("transfer-encoding:") && l.contains("chunked")
1179 });
1180 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1181 l.strip_prefix("content-length:")?.trim().parse().ok()
1182 });
1183 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1184 is_sse || is_chunked || is_large
1185}
1186
1187#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1193pub(crate) struct HeadFacts {
1194 pub(crate) status_no_body: bool,
1196 pub(crate) sse: bool,
1197 pub(crate) chunked: bool,
1198 pub(crate) content_length: Option<usize>,
1200 pub(crate) connection_close: bool,
1201}
1202
1203#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1204impl HeadFacts {
1205 pub(crate) fn scan(head: &[u8]) -> HeadFacts {
1206 let text = std::str::from_utf8(head).unwrap_or("");
1207 let status = text.split_whitespace().nth(1).unwrap_or("");
1208 let mut facts = HeadFacts {
1209 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1210 sse: false,
1211 chunked: false,
1212 content_length: None,
1213 connection_close: false,
1214 };
1215 for line in text.lines() {
1216 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1217 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1218 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1219 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1220 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1221 if facts.content_length.is_none() {
1222 facts.content_length = value.trim().parse().ok();
1223 }
1224 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1225 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1226 }
1227 }
1228 facts
1229 }
1230
1231 pub(crate) fn no_body(&self, method: &str) -> bool {
1233 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1234 }
1235
1236 pub(crate) fn should_stream(&self) -> bool {
1238 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1239 }
1240}
1241
1242fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1243 let bytes = line.as_bytes();
1244 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1246 .then(|| &line[prefix.len()..])
1247}
1248
1249fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1250 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1251}
1252
1253fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1255 let s = std::str::from_utf8(header_bytes)
1256 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1257 let mut lines = s.lines();
1258 let status_line = lines.next().ok_or("empty backend response")?;
1259 let mut parts = status_line.splitn(3, ' ');
1260 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1261 let status_code: i16 = parts
1262 .next()
1263 .unwrap_or("502")
1264 .parse()
1265 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1266 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1267 let mut headers = Vec::new();
1268 for line in lines {
1269 let line = line.trim_end_matches('\r');
1270 if line.is_empty() { break; }
1271 if let Some(colon) = line.find(':') {
1272 headers.push(crate::header::Header {
1273 name: line[..colon].trim().to_string().into(),
1274 value: (line[colon + 1..].trim().to_string()).into(),
1275 });
1276 }
1277 }
1278 Ok(Response {
1279 http_version,
1280 status_code,
1281 reason_phrase,
1282 headers,
1283 content_range_list: vec![],
1284 stream_file: None,
1285 stream_pipe: None,
1286 trailers: None,
1287 })
1288}
1289
1290fn read_response_from_partial<S: Read>(
1296 stream: &mut S,
1297 header_bytes: Vec<u8>,
1298 body_prefix: Vec<u8>,
1299 tmp: &mut [u8],
1300) -> Result<(Vec<u8>, bool), String> {
1301 let header_end = header_bytes.len();
1302 let mut buf = header_bytes;
1303 buf.extend_from_slice(&body_prefix);
1304
1305 let header_lower =
1306 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1307 let connection_close =
1308 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1309 let is_chunked = header_lower
1310 .lines()
1311 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1312 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1313 l.strip_prefix("content-length:")?.trim().parse().ok()
1314 });
1315
1316 if is_chunked {
1317 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1318 rewrite_as_content_length(&mut buf, header_end, &decoded);
1319 Ok((buf, !connection_close))
1320 } else if let Some(len) = content_length {
1321 while buf.len() < header_end + len {
1322 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1323 if n == 0 { break; }
1324 buf.extend_from_slice(&tmp[..n]);
1325 }
1326 Ok((buf, !connection_close))
1327 } else {
1328 loop {
1329 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1330 if n == 0 { break; }
1331 buf.extend_from_slice(&tmp[..n]);
1332 }
1333 Ok((buf, false))
1334 }
1335}
1336
1337struct Backend {
1340 host: String,
1341 port: u16,
1342 tls: bool,
1345}
1346
1347impl Backend {
1348 fn parse(url: &str) -> Option<Self> {
1349 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1350 (r, true, 443u16)
1351 } else if let Some(r) = url.strip_prefix("h2s://") {
1352 (r, true, 443u16)
1353 } else if let Some(r) = url.strip_prefix("grpcs://") {
1354 (r, true, 443u16)
1355 } else if let Some(r) = url.strip_prefix("http://") {
1356 (r, false, 80u16)
1357 } else if let Some(r) = url.strip_prefix("h2://") {
1358 (r, false, 80u16)
1359 } else if let Some(r) = url.strip_prefix("grpc://") {
1360 (r, false, 80u16)
1361 } else {
1362 (url, false, 80u16)
1363 };
1364 let host_port = rest.split('/').next().unwrap_or(rest);
1366 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1367 let port_str = &host_port[colon + 1..];
1368 if let Ok(p) = port_str.parse::<u16>() {
1369 (host_port[..colon].to_string(), p)
1370 } else {
1371 (host_port.to_string(), default_port)
1372 }
1373 } else {
1374 (host_port.to_string(), default_port)
1375 };
1376 if host.is_empty() {
1377 return None;
1378 }
1379 Some(Backend { host, port, tls })
1380 }
1381}
1382
1383#[cfg(feature = "http2")]
1394pub struct H2ReverseProxy {
1395 inner: ReverseProxy,
1396}
1397
1398#[cfg(feature = "http2")]
1399impl H2ReverseProxy {
1400 pub fn new<I, S>(backends: I) -> Self
1411 where
1412 I: IntoIterator<Item = S>,
1413 S: AsRef<str>,
1414 {
1415 H2ReverseProxy {
1416 inner: ReverseProxy::new(backends),
1417 }
1418 }
1419
1420 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1422 self.inner = self.inner.path_prefix(prefix);
1423 self
1424 }
1425
1426 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1428 self.inner = self.inner.connect_timeout_ms(ms);
1429 self
1430 }
1431
1432 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1437 self.inner = self.inner.read_timeout_ms(ms);
1438 self
1439 }
1440
1441 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1444 self.inner = self.inner.tls_ca_file(path)?;
1445 Ok(self)
1446 }
1447
1448 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1451 self.inner = self.inner.tls_client_cert(cert, key)?;
1452 Ok(self)
1453 }
1454
1455 fn matches(&self, request: &Request) -> bool {
1456 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1457 }
1458}
1459
1460#[cfg(feature = "http2")]
1461impl crate::middleware::Middleware for H2ReverseProxy {
1462 fn handle(
1463 &self,
1464 request: &crate::request::Request,
1465 connection: &crate::server::ConnectionInfo,
1466 next: &dyn crate::application::Application,
1467 ) -> Result<crate::response::Response, String> {
1468 if !self.matches(request) {
1469 return next.execute(request, connection);
1470 }
1471 if self.inner.backends.is_empty() {
1472 return Ok(bad_gateway());
1473 }
1474 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1476 let n = self.inner.backends.len();
1477 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1478 for attempt in 0..attempts {
1479 let backend = &self.inner.backends[(start + attempt) % n];
1480 let tls = backend.tls.then(|| match &self.inner.trust {
1481 Some(trust) => trust.h2_config(),
1482 None => h2_upstream::default_tls(),
1483 });
1484 let target = h2_upstream::Target {
1485 backend,
1486 tls,
1487 connect_timeout: self.inner.connect_timeout,
1488 read_timeout: self.inner.read_timeout,
1489 authority: None,
1490 };
1491 if let Ok(response) = h2_upstream::forward(request, Peer::of(connection), &target) {
1492 return Ok(response);
1493 }
1494 }
1495 Ok(bad_gateway())
1496 }
1497
1498 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1501 true
1502 }
1503
1504 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1507 self.matches(request)
1508 }
1509}
1510
1511#[cfg(feature = "http2")]
1515pub(crate) fn forward_h2(
1516 request: &Request,
1517 client: Peer<'_>,
1518 url: &str,
1519 tls: bool,
1520 trust: Option<&UpstreamTls>,
1521 connect_timeout: Duration,
1522 read_timeout: Duration,
1523) -> Result<Response, String> {
1524 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1525 backend.tls |= tls;
1526 let tls = backend.tls.then(|| match trust {
1527 Some(t) => t.h2_config(),
1528 None => h2_upstream::default_tls(),
1529 });
1530 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1531}
1532
1533#[cfg(feature = "gateway-api")]
1538#[allow(clippy::too_many_arguments)]
1539pub(crate) fn forward_tls_with_host(
1540 config: &Arc<rustls::ClientConfig>,
1541 label: &str,
1542 request: &Request,
1543 client: Peer<'_>,
1544 ip: &str,
1545 port: u16,
1546 server_name: &str,
1547 host_header: &str,
1548 connect_timeout: Duration,
1549 read_timeout: Duration,
1550) -> Result<Response, String> {
1551 let pool = tls_pool_for_ca(label);
1552 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1553 let tcp = connect_tcp(ip, port, connect_timeout)?;
1554 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
1555 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
1556 Ok(rustls::StreamOwned::new(conn, tcp))
1557 })
1558}
1559
1560#[cfg(feature = "gateway-api")]
1564pub(crate) fn forward_h2c_with_host(
1565 request: &Request,
1566 client: Peer<'_>,
1567 host: &str,
1568 port: u16,
1569 authority: &str,
1570 connect_timeout: Duration,
1571 read_timeout: Duration,
1572) -> Result<Response, String> {
1573 let backend = Backend { host: host.to_string(), port, tls: false };
1574 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
1575}
1576
1577#[cfg(feature = "http2")]
1597pub struct GrpcProxy {
1598 inner: H2ReverseProxy,
1599}
1600
1601#[cfg(feature = "http2")]
1602impl GrpcProxy {
1603 pub fn new<I, S>(backends: I) -> Self
1613 where
1614 I: IntoIterator<Item = S>,
1615 S: AsRef<str>,
1616 {
1617 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1618 }
1619
1620 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1622 self.inner = self.inner.path_prefix(prefix);
1623 self
1624 }
1625
1626 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1628 self.inner = self.inner.connect_timeout_ms(ms);
1629 self
1630 }
1631
1632 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1634 self.inner = self.inner.read_timeout_ms(ms);
1635 self
1636 }
1637
1638 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1640 self.inner = self.inner.tls_ca_file(path)?;
1641 Ok(self)
1642 }
1643
1644 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1647 self.inner = self.inner.tls_client_cert(cert, key)?;
1648 Ok(self)
1649 }
1650}
1651
1652#[cfg(feature = "http2")]
1653impl crate::middleware::Middleware for GrpcProxy {
1654 fn handle(
1655 &self,
1656 request: &crate::request::Request,
1657 connection: &crate::server::ConnectionInfo,
1658 next: &dyn crate::application::Application,
1659 ) -> Result<crate::response::Response, String> {
1660 if is_grpc(request) {
1661 self.inner.handle(request, connection, next)
1662 } else {
1663 next.execute(request, connection)
1664 }
1665 }
1666
1667 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1668 true
1669 }
1670
1671 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1672 is_grpc(request) && self.inner.matches(request)
1673 }
1674}
1675
1676#[cfg(feature = "http2")]
1677fn is_grpc(request: &Request) -> bool {
1678 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
1679}
1680
1681#[cfg(test)]
1684mod backend_parse_tests {
1685 use super::Backend;
1686
1687 fn parse(url: &str) -> Option<(String, u16, bool)> {
1688 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1689 }
1690
1691 #[test]
1692 fn bare_host_port() {
1693 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1694 }
1695
1696 #[test]
1697 fn http_scheme() {
1698 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1699 }
1700
1701 #[test]
1702 fn h2_scheme_plain() {
1703 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1704 }
1705
1706 #[test]
1707 fn grpc_scheme_plain() {
1708 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1709 }
1710
1711 #[test]
1712 fn https_scheme_sets_tls_and_default_port() {
1713 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1714 }
1715
1716 #[test]
1717 fn https_scheme_explicit_port() {
1718 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1719 }
1720
1721 #[test]
1722 fn h2s_scheme_sets_tls() {
1723 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1724 }
1725
1726 #[test]
1727 fn h2s_scheme_explicit_port() {
1728 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1729 }
1730
1731 #[test]
1732 fn grpcs_scheme_sets_tls() {
1733 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1734 }
1735
1736 #[test]
1737 fn grpcs_scheme_explicit_port() {
1738 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1739 }
1740
1741 #[test]
1742 fn empty_host_returns_none() {
1743 assert_eq!(None, parse("https://"));
1744 }
1745
1746 #[test]
1747 fn bare_host_no_port_defaults_to_80() {
1748 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1749 }
1750}