1pub mod pool;
28pub mod status;
29#[cfg(feature = "http2")]
30#[doc(hidden)]
31pub mod async_upstream;
32#[cfg(not(target_arch = "wasm32"))]
33pub mod mirror;
34#[cfg(not(target_arch = "wasm32"))]
37#[doc(hidden)]
38pub mod upgrade;
39#[cfg(feature = "http2")]
40#[doc(hidden)]
41pub mod h2_upstream;
42#[cfg(feature = "http3")]
43#[doc(hidden)]
44pub mod h3_upstream;
45#[cfg(feature = "http2")]
46pub mod grpc_web;
47#[cfg(feature = "http2")]
48pub use grpc_web::GrpcWebLayer;
49
50#[cfg(test)]
51mod tests;
52
53use std::io::{Read, Write};
54use crate::ext::decimal::Decimal;
55use std::net::{TcpStream, ToSocketAddrs};
56use std::sync::atomic::{AtomicUsize, Ordering};
57use std::sync::Arc;
58use std::time::Duration;
59
60pub use pool::{ConnPool, Pool};
61
62use crate::application::Application;
63use crate::middleware::Middleware;
64use crate::request::Request;
65use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
66use crate::server::ConnectionInfo;
67
68const HOP_BY_HOP: &[&str] = &[
72 "connection",
73 "keep-alive",
74 "proxy-authenticate",
75 "proxy-authorization",
76 "proxy-connection",
77 "te",
78 "trailer",
79 "transfer-encoding",
80 "upgrade",
81];
82
83pub enum LoadBalancing {
85 RoundRobin,
87}
88
89pub struct ReverseProxy {
104 backends: Vec<Backend>,
105 path_prefix: Option<String>,
106 connect_timeout: Duration,
107 read_timeout: Duration,
108 counter: AtomicUsize,
109 pool: Arc<ConnPool>,
110 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
111 trust: Option<Arc<UpstreamTls>>,
112 tls_ca: Option<String>,
114 tls_client: Option<(String, String)>,
115}
116
117impl ReverseProxy {
118 pub fn new<I, S>(backends: I) -> Self
122 where
123 I: IntoIterator<Item = S>,
124 S: AsRef<str>,
125 {
126 Self {
127 backends: backends
128 .into_iter()
129 .filter_map(|u| Backend::parse(u.as_ref()))
130 .collect(),
131 path_prefix: None,
132 connect_timeout: Duration::from_secs(5),
133 read_timeout: Duration::from_secs(30),
134 counter: AtomicUsize::new(0),
135 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, pool::IDLE_TIMEOUT)),
136 breaker: None,
137 trust: None,
138 tls_ca: None,
139 tls_client: None,
140 }
141 }
142
143 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
162 self.breaker = Some(breaker);
163 self
164 }
165
166 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
171 self.path_prefix = Some(prefix.into());
172 self
173 }
174
175 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
177 self
178 }
179
180 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
182 self.connect_timeout = Duration::from_millis(ms);
183 self
184 }
185
186 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
188 self.read_timeout = Duration::from_millis(ms);
189 self
190 }
191
192 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
197 self.pool = pool;
198 self
199 }
200
201 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
205 self.tls_ca = Some(path.to_string());
206 self.rebuild_trust()?;
207 Ok(self)
208 }
209
210 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
214 self.tls_client = Some((cert.to_string(), key.to_string()));
215 self.rebuild_trust()?;
216 Ok(self)
217 }
218
219 fn rebuild_trust(&mut self) -> Result<(), String> {
220 let client = self.tls_client.as_ref().map(|(c, k)| (c.as_str(), k.as_str()));
221 self.trust = Some(Arc::new(UpstreamTls::new(self.tls_ca.as_deref(), client)?));
222 Ok(())
223 }
224
225 pub fn max_idle_conns(mut self, n: usize) -> Self {
227 self.pool = Arc::new(ConnPool::new(n, pool::IDLE_TIMEOUT));
228 self
229 }
230
231 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
232 if self.backends.is_empty() {
233 return Err("no backends configured".to_string());
234 }
235 let n = self.backends.len();
236 let start = self.counter.fetch_add(1, Ordering::Relaxed);
237 let mut last_err = "all backends failed".to_string();
238 for attempt in 0..n {
239 let idx = (start + attempt) % n;
240 let backend = &self.backends[idx];
241 let key = format!("{}:{}", backend.host, backend.port);
242
243 if let Some(breaker) = &self.breaker {
244 if !breaker.is_available(&key) {
245 last_err = format!("circuit open for {}", key);
246 continue;
247 }
248 }
249
250 match self.try_backend(request, connection, backend) {
251 Ok(resp) => {
252 if let Some(breaker) = &self.breaker {
253 breaker.record_success(&key);
254 }
255 return Ok(resp);
256 }
257 Err(e) => {
258 if let Some(breaker) = &self.breaker {
259 breaker.record_failure(&key);
260 }
261 last_err = e;
262 }
263 }
264 }
265 Err(last_err)
266 }
267
268 fn try_backend(
269 &self,
270 request: &Request,
271 connection: &ConnectionInfo,
272 backend: &Backend,
273 ) -> Result<Response, String> {
274 forward_upstream_trusting(
275 &self.pool,
276 request,
277 crate::proxy::Peer::of(connection),
278 &backend.host,
279 backend.port,
280 backend.tls,
281 self.trust.as_deref(),
282 self.connect_timeout,
283 self.read_timeout,
284 )
285 }
286}
287
288impl Middleware for ReverseProxy {
289 fn handle(
290 &self,
291 request: &Request,
292 connection: &ConnectionInfo,
293 next: &dyn Application,
294 ) -> Result<Response, String> {
295 if let Some(prefix) = &self.path_prefix {
296 if !request.request_uri.starts_with(prefix.as_str()) {
297 return next.execute(request, connection);
298 }
299 }
300 match self.proxy(request, connection) {
301 Ok(mut resp) => {
302 status::received(&mut resp, None);
303 Ok(resp)
304 }
305 Err(e) => {
306 let mut resp = bad_gateway();
307 status::failed_with(&mut resp, &e, None);
308 Ok(resp)
309 }
310 }
311 }
312}
313
314#[derive(Clone, Copy, Debug)]
319#[doc(hidden)]
320pub struct Peer<'a> {
321 pub ip: &'a str,
322 pub tls: bool,
323}
324
325impl<'a> Peer<'a> {
326 #[doc(hidden)]
327 pub fn of(connection: &'a ConnectionInfo) -> Self {
328 Peer { ip: &connection.client.ip, tls: connection.tls }
329 }
330
331 #[doc(hidden)]
333 pub fn plain(ip: &'a str) -> Self {
334 Peer { ip, tls: false }
335 }
336
337 #[doc(hidden)]
338 pub fn proto(&self) -> &'static str {
339 if self.tls { "https" } else { "http" }
340 }
341}
342
343const FORWARDING_HEADERS: [&str; 4] = ["x-forwarded-for", "x-forwarded-proto", "x-forwarded-host", "forwarded"];
345
346#[doc(hidden)]
351pub fn from_peer<'a>(request: &'a Request, client: Peer<'_>) -> std::borrow::Cow<'a, Request> {
352 let has = request.headers.iter().any(|h| FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
353 if !has || crate::server_config::ServerConfig::current().trusted_proxies.trusts(client.ip) {
354 return std::borrow::Cow::Borrowed(request);
355 }
356 let mut owned = request.clone();
357 owned.headers.retain(|h| !FORWARDING_HEADERS.iter().any(|n| h.name.eq_ignore_ascii_case(n)));
358 std::borrow::Cow::Owned(owned)
359}
360
361#[doc(hidden)]
366pub fn forwarded_value(request: &Request, client: Peer<'_>) -> Option<String> {
367 if !crate::server_config::ServerConfig::current().forwarded_header {
368 return None;
369 }
370 let mut element = format!("for={};proto={}", forwarded_node(client.ip), client.proto());
371 if let Some(host) = request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()) {
372 push_forwarded_host(&mut element, host);
373 }
374 let prior = request.headers.iter().filter(|h| h.name.eq_ignore_ascii_case("forwarded")).map(|h| h.value.trim()).filter(|v| !v.is_empty() && !v.bytes().any(|b| b < 0x20)).collect::<Vec<_>>();
375 if prior.is_empty() {
376 if let Some(earlier) = forwarded_from_x_forwarded(request, client) {
381 return Some(format!("{earlier}, {element}"));
382 }
383 }
384 Some(if prior.is_empty() { element } else { format!("{}, {element}", prior.join(", ")) })
385}
386
387fn forwarded_node(ip: &str) -> String {
390 let ip = ip.trim();
391 let parsed = ip.parse::<std::net::IpAddr>().ok().or_else(|| ip.parse::<std::net::SocketAddr>().ok().map(|a| a.ip()));
392 match parsed {
393 Some(std::net::IpAddr::V4(v4)) => v4.to_string(),
394 Some(std::net::IpAddr::V6(v6)) => match v6.to_ipv4_mapped() {
395 Some(v4) => v4.to_string(),
396 None => format!("\"[{v6}]\""),
397 },
398 None => "unknown".to_string(),
399 }
400}
401
402fn push_forwarded_host(element: &mut String, host: &str) {
403 if host.is_empty() || !host.bytes().all(|b| b.is_ascii_graphic() && b != b'"' && b != b'\\') {
404 return;
405 }
406 if host.bytes().all(|b| b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b)) {
407 element.push_str(&format!(";host={host}"));
408 } else {
409 element.push_str(&format!(";host=\"{host}\""));
410 }
411}
412
413fn forwarded_from_x_forwarded(request: &Request, client: Peer<'_>) -> Option<String> {
416 if !crate::server_config::ServerConfig::current().trusted_proxies.trusts(client.ip) {
417 return None;
418 }
419 let hops: Vec<&str> = request.headers.iter()
420 .filter(|h| h.name.eq_ignore_ascii_case("x-forwarded-for"))
421 .flat_map(|h| h.value.split(','))
422 .map(str::trim)
423 .filter(|h| !h.is_empty())
424 .collect();
425 if hops.is_empty() {
426 return None;
427 }
428 let mut elements = Vec::with_capacity(hops.len());
429 for (i, hop) in hops.iter().enumerate() {
430 let mut element = format!("for={}", forwarded_node(hop));
431 if i == 0 {
432 if let Some(proto) = request.header("X-Forwarded-Proto").map(|h| h.value.trim().to_ascii_lowercase()).filter(|p| p == "http" || p == "https") {
433 element.push_str(&format!(";proto={proto}"));
434 }
435 if let Some(host) = request.header("X-Forwarded-Host").map(|h| h.value.trim().to_string()) {
436 push_forwarded_host(&mut element, &host);
437 }
438 }
439 elements.push(element);
440 }
441 Some(elements.join(", "))
442}
443
444#[doc(hidden)]
445pub fn build_request(
446 request: &Request,
447 backend_host: &str,
448 client: Peer<'_>,
449 keep_alive: bool,
450) -> Vec<u8> {
451 let request = &*from_peer(request, client);
452 let connection = request.header(crate::header::Header::_CONNECTION).map(|h| h.value.as_ref());
458 #[cfg(not(target_arch = "wasm32"))]
461 let trace = crate::otel::propagation_headers();
462 #[cfg(target_arch = "wasm32")]
463 let trace: Option<(String, Option<String>)> = None;
464 let forwarded_header = forwarded_value(request, client);
465 let forwarded = |h: &&crate::header::Header| {
466 !h.name.is_empty()
468 && !is_hop_by_hop_or_host(&h.name)
469 && !h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_LENGTH)
470 && !h.name.eq_ignore_ascii_case("X-Forwarded-For")
471 && !(forwarded_header.is_some() && h.name.eq_ignore_ascii_case("Forwarded"))
472 && !connection.is_some_and(|c| connection_listed(c, &h.name))
473 && !(trace.is_some() && (h.name.eq_ignore_ascii_case("traceparent") || h.name.eq_ignore_ascii_case("baggage")))
474 };
475 let prior_xff = request.header("X-Forwarded-For").map(|h| h.value.as_ref()).filter(|v| !v.trim().is_empty());
476 let forwarded_proto = request.header("X-Forwarded-Proto").is_none().then(|| client.proto());
480 let forwarded_host = match request.header("X-Forwarded-Host") {
481 Some(_) => None,
482 None => request.header(crate::header::Header::_HOST).map(|h| h.value.as_ref()).filter(|v| !v.is_empty()),
483 };
484 let body_len = Decimal::new(request.body.len() as u128);
485 let capacity = request.method.len() + request.request_uri.len() + backend_host.len() + 25
486 + request.headers.iter().filter(forwarded).map(|h| h.name.len() + h.value.len() + 4).sum::<usize>()
487 + client.ip.len() + 19 + prior_xff.map_or(0, |v| v.len() + 2)
488 + forwarded_host.map_or(0, |v| v.len() + 20)
489 + forwarded_proto.map_or(0, |v| v.len() + 21)
490 + forwarded_header.as_ref().map_or(0, |v| v.len() + 13)
491 + trace.as_ref().map_or(0, |(t, b)| t.len() + 15 + b.as_ref().map_or(0, |b| b.len() + 11))
492 + 13
493 + 24
494 + if request.body.is_empty() { 0 } else { body_len.as_str().len() + 18 }
495 + 2
496 + request.body.len();
497 let mut out: Vec<u8> = Vec::with_capacity(capacity);
498 for part in [request.method.as_str(), " ", request.request_uri.as_str(), " HTTP/1.1\r\nHost: ", backend_host, "\r\n"] {
499 out.extend_from_slice(part.as_bytes());
500 }
501 for h in request.headers.iter().filter(forwarded) {
502 for part in [h.name.as_ref(), ": ", h.value.as_ref(), "\r\n"] {
503 out.extend_from_slice(part.as_bytes());
504 }
505 }
506 out.extend_from_slice(b"X-Forwarded-For: ");
507 if let Some(prior) = prior_xff {
508 out.extend_from_slice(prior.as_bytes());
509 out.extend_from_slice(b", ");
510 }
511 for part in [client.ip, "\r\n", "Via: 1.1 rws\r\n"] {
512 out.extend_from_slice(part.as_bytes());
513 }
514 if let Some(host) = forwarded_host {
515 for part in ["X-Forwarded-Host: ", host, "\r\n"] {
516 out.extend_from_slice(part.as_bytes());
517 }
518 }
519 if let Some(proto) = forwarded_proto {
520 for part in ["X-Forwarded-Proto: ", proto, "\r\n"] {
521 out.extend_from_slice(part.as_bytes());
522 }
523 }
524 if let Some(value) = &forwarded_header {
525 for part in ["Forwarded: ", value.as_str(), "\r\n"] {
526 out.extend_from_slice(part.as_bytes());
527 }
528 }
529 if let Some((traceparent, baggage)) = &trace {
530 for part in ["traceparent: ", traceparent, "\r\n"] {
531 out.extend_from_slice(part.as_bytes());
532 }
533 if let Some(baggage) = baggage {
534 for part in ["baggage: ", baggage, "\r\n"] {
535 out.extend_from_slice(part.as_bytes());
536 }
537 }
538 }
539 if keep_alive {
540 out.extend_from_slice(b"Connection: keep-alive\r\n");
541 } else {
542 out.extend_from_slice(b"Connection: close\r\n");
543 }
544 if !request.body.is_empty() {
545 out.extend_from_slice(b"Content-Length: ");
546 out.extend_from_slice(body_len.as_str().as_bytes());
547 out.extend_from_slice(b"\r\n");
548 }
549 out.extend_from_slice(b"\r\n");
550 out.extend_from_slice(&request.body);
551 out
552}
553
554fn is_hop_by_hop_or_host(name: &str) -> bool {
559 if name.is_ascii() {
560 name.eq_ignore_ascii_case("host") || HOP_BY_HOP.iter().any(|h| name.eq_ignore_ascii_case(h))
561 } else {
562 let lower = name.to_lowercase();
563 lower == "host" || HOP_BY_HOP.contains(&lower.as_str())
564 }
565}
566
567fn connection_listed(connection: &str, name: &str) -> bool {
570 connection.split(',').any(|token| token.trim().eq_ignore_ascii_case(name))
571}
572
573fn decode_chunked<S: Read>(
578 stream: &mut S,
579 buf: &[u8],
580 header_end: usize,
581 tmp: &mut [u8],
582) -> Result<Vec<u8>, String> {
583 let mut raw: Vec<u8> = buf[header_end..].to_vec();
585 let mut decoded: Vec<u8> = Vec::new();
586
587 loop {
588 let crlf = loop {
590 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
591 break p;
592 }
593 let n = stream.read(tmp).map_err(|e| e.to_string())?;
594 if n == 0 {
595 return Err("chunked: premature EOF reading chunk size".to_string());
596 }
597 raw.extend_from_slice(&tmp[..n]);
598 };
599
600 let size_line = std::str::from_utf8(&raw[..crlf])
602 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
603 let size_str = size_line.split(';').next().unwrap_or("").trim();
604 let chunk_size = usize::from_str_radix(size_str, 16)
605 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
606 raw.drain(..crlf + 2); if chunk_size == 0 {
609 loop {
613 let line_end = loop {
614 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
615 break p;
616 }
617 let n = stream.read(tmp).map_err(|e| e.to_string())?;
618 if n == 0 {
619 return Err("chunked: premature EOF in trailer".to_string());
620 }
621 raw.extend_from_slice(&tmp[..n]);
622 };
623 raw.drain(..line_end + 2);
624 if line_end == 0 {
625 break;
626 }
627 }
628 break;
629 }
630
631 while raw.len() < chunk_size + 2 {
633 let n = stream.read(tmp).map_err(|e| e.to_string())?;
634 if n == 0 {
635 return Err("chunked: premature EOF reading chunk body".to_string());
636 }
637 raw.extend_from_slice(&tmp[..n]);
638 }
639 decoded.extend_from_slice(&raw[..chunk_size]);
640 raw.drain(..chunk_size + 2); }
642
643 Ok(decoded)
644}
645
646fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
649 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
650 buf.clear();
651 for line in header_str.lines() {
652 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
653 continue;
654 }
655 buf.extend_from_slice(line.as_bytes());
656 buf.extend_from_slice(b"\r\n");
657 }
658 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
659 buf.extend_from_slice(decoded);
660}
661
662pub(crate) use crate::response::REGENERATED_OR_HOP_BY_HOP;
664
665#[cfg_attr(not(feature = "http2"), allow(dead_code))]
674#[doc(hidden)]
675pub fn normalize_upstream_response(mut response: Response) -> Response {
676 response.http_version.clear();
677 response.http_version.push_str(crate::http::VERSION.http_1_1);
678 let has_body = !response.content_range_list.is_empty();
679 if let [range] = response.content_range_list.as_mut_slice() {
680 if let Some(h) = response
681 .headers
682 .iter()
683 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
684 {
685 range.content_type.clear();
686 range.content_type.push_str(&h.value);
687 }
688 }
689 response.headers.retain(|h| {
692 if h.name.eq_ignore_ascii_case("content-type") {
693 return !has_body;
695 }
696 !REGENERATED_OR_HOP_BY_HOP.iter().any(|name| h.name.eq_ignore_ascii_case(name))
697 });
698 response
699}
700
701#[doc(hidden)]
702pub fn bad_gateway() -> Response {
703 crate::error::problem(STATUS_CODE_REASON_PHRASE.n502_bad_gateway, "")
704}
705
706const STREAM_THRESHOLD: usize = 1024 * 1024; #[doc(hidden)]
714pub struct ConcatReader<R: Read + Send> {
715 prefix: Vec<u8>,
716 prefix_pos: usize,
717 inner: R,
718}
719
720impl<R: Read + Send> ConcatReader<R> {
721 fn new(prefix: Vec<u8>, inner: R) -> Self {
722 ConcatReader { prefix, prefix_pos: 0, inner }
723 }
724}
725
726impl<R: Read + Send> Read for ConcatReader<R> {
727 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
728 if self.prefix_pos < self.prefix.len() {
729 let avail = &self.prefix[self.prefix_pos..];
730 let n = buf.len().min(avail.len());
731 buf[..n].copy_from_slice(&avail[..n]);
732 self.prefix_pos += n;
733 return Ok(n);
734 }
735 self.inner.read(buf)
736 }
737}
738
739#[doc(hidden)]
747pub struct LengthLimitedReader {
748 inner: Box<dyn Read + Send>,
749 remaining: u64,
750 declared: u64,
751}
752
753impl LengthLimitedReader {
754 #[doc(hidden)]
755 pub fn new(inner: Box<dyn Read + Send>, length: u64) -> Self {
756 LengthLimitedReader { inner, remaining: length, declared: length }
757 }
758}
759
760impl Read for LengthLimitedReader {
761 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
762 if self.remaining == 0 || buf.is_empty() {
763 return Ok(0);
764 }
765 let want = buf.len().min(self.remaining.min(usize::MAX as u64) as usize);
766 let n = self.inner.read(&mut buf[..want])?;
767 if n == 0 {
768 return Err(std::io::Error::new(
769 std::io::ErrorKind::UnexpectedEof,
770 format!("upstream body ended after {} of {} bytes", self.declared - self.remaining, self.declared),
771 ));
772 }
773 self.remaining -= n as u64;
774 Ok(n)
775 }
776}
777
778#[doc(hidden)]
781pub fn bounded_body(reader: Box<dyn Read + Send>, chunked: bool, content_length: Option<usize>) -> Box<dyn Read + Send> {
782 match content_length {
783 Some(len) if !chunked => Box::new(LengthLimitedReader::new(reader, len as u64)),
784 _ => reader,
785 }
786}
787
788
789#[doc(hidden)]
793pub const POOL_MAX_IDLE: usize = 64;
794
795#[doc(hidden)]
798pub fn shared_pool() -> &'static ConnPool {
799 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
800 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, pool::IDLE_TIMEOUT))
801}
802
803#[doc(hidden)]
806pub trait UpstreamIo: Read + Write + Send + 'static {
807 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
808
809 fn peer_closed(&self) -> bool {
813 false
814 }
815}
816
817impl UpstreamIo for TcpStream {
818 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
819 self.set_read_timeout(Some(read))?;
820 self.set_write_timeout(Some(write))
821 }
822
823 fn peer_closed(&self) -> bool {
824 socket_closed(self)
825 }
826}
827
828fn socket_closed(stream: &TcpStream) -> bool {
831 if stream.set_nonblocking(true).is_err() {
832 return true;
833 }
834 let peeked = stream.peek(&mut [0u8; 1]);
835 let restored = stream.set_nonblocking(false).is_ok();
836 !restored || !matches!(peeked, Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock)
837}
838
839pub(crate) fn acquire_open<S: UpstreamIo>(pool: &Pool<S>, key: &str) -> Option<S> {
842 std::iter::from_fn(|| pool.acquire(key)).find(|s| !s.peer_closed())
843}
844
845#[cfg(any(feature = "http-client", feature = "http2"))]
846#[doc(hidden)]
847pub type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
848
849#[cfg(any(feature = "http-client", feature = "http2"))]
850impl UpstreamIo for TlsStream {
851 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
852 self.sock.set_io_timeouts(read, write)
853 }
854
855 fn peer_closed(&self) -> bool {
856 socket_closed(&self.sock)
858 }
859}
860
861#[doc(hidden)]
867pub fn forward_upstream(
868 plain_pool: &ConnPool,
869 request: &Request,
870 client: Peer<'_>,
871 host: &str,
872 port: u16,
873 tls: bool,
874 connect_timeout: Duration,
875 read_timeout: Duration,
876) -> Result<Response, String> {
877 forward_upstream_trusting(plain_pool, request, client, host, port, tls, None, connect_timeout, read_timeout)
878}
879
880#[allow(clippy::too_many_arguments)]
883#[doc(hidden)]
884pub fn forward_upstream_trusting(
885 plain_pool: &ConnPool,
886 request: &Request,
887 client: Peer<'_>,
888 host: &str,
889 port: u16,
890 tls: bool,
891 trust: Option<&UpstreamTls>,
892 connect_timeout: Duration,
893 read_timeout: Duration,
894) -> Result<Response, String> {
895 forward_upstream_as(plain_pool, request, client, host, port, host, tls, trust, connect_timeout, read_timeout)
896}
897
898#[allow(clippy::too_many_arguments)]
902#[doc(hidden)]
903pub fn forward_upstream_as(
904 plain_pool: &ConnPool,
905 request: &Request,
906 client: Peer<'_>,
907 host: &str,
908 port: u16,
909 host_header: &str,
910 tls: bool,
911 trust: Option<&UpstreamTls>,
912 connect_timeout: Duration,
913 read_timeout: Duration,
914) -> Result<Response, String> {
915 #[cfg(not(target_arch = "wasm32"))]
918 let span = crate::otel::outbound_span(&request.method);
919 #[cfg(not(target_arch = "wasm32"))]
920 if let Some(span) = &span {
921 let scheme = if tls { "https" } else { "http" };
922 let path = request.request_uri.as_str();
923 for (key, value) in crate::otel::http_client_attributes(&request.method, &format!("{scheme}://{host}:{port}{path}")) {
924 span.set_attribute(key, value);
925 }
926 }
927 let result = forward_upstream_untraced(plain_pool, request, client, host, port, host_header, tls, trust, connect_timeout, read_timeout);
928 #[cfg(not(target_arch = "wasm32"))]
929 if let Some(span) = &span {
930 span.finish_http_client(result.as_ref().map(|r| r.status_code as u16).map_err(String::as_str));
931 }
932 result
933}
934
935#[allow(clippy::too_many_arguments)]
936fn forward_upstream_untraced(
937 plain_pool: &ConnPool,
938 request: &Request,
939 client: Peer<'_>,
940 host: &str,
941 port: u16,
942 host_header: &str,
943 tls: bool,
944 trust: Option<&UpstreamTls>,
945 connect_timeout: Duration,
946 read_timeout: Duration,
947) -> Result<Response, String> {
948 if tls {
949 #[cfg(any(feature = "http-client", feature = "http2"))]
950 {
951 let (pool, config) = match trust {
952 Some(t) => (t.pool, Arc::clone(&t.config)),
953 None => (shared_tls_pool(), tls_client_config()),
954 };
955 return forward_via(pool, request, client, host, port, host_header, read_timeout, || {
956 connect_tls_with(Arc::clone(&config), host, port, connect_timeout)
957 });
958 }
959 #[cfg(not(any(feature = "http-client", feature = "http2")))]
960 {
961 let _ = trust;
962 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
963 }
964 }
965 forward_via(plain_pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
966}
967
968pub struct UpstreamTls {
976 #[cfg(any(feature = "http-client", feature = "http2"))]
977 config: Arc<rustls::ClientConfig>,
978 #[cfg(any(feature = "http-client", feature = "http2"))]
979 pool: &'static Pool<TlsStream>,
980 #[cfg(feature = "http2")]
982 async_pool: &'static Pool<async_upstream::AsyncTlsStream>,
983 #[cfg(feature = "http2")]
985 h2_config: Arc<rustls::ClientConfig>,
986}
987
988impl UpstreamTls {
989 pub fn from_ca_file(path: &str) -> Result<UpstreamTls, String> {
991 UpstreamTls::new(Some(path), None)
992 }
993
994 pub fn new(ca_file: Option<&str>, client_cert: Option<(&str, &str)>) -> Result<UpstreamTls, String> {
998 #[cfg(any(feature = "http-client", feature = "http2"))]
999 {
1000 use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
1001 let roots = match ca_file {
1002 Some(path) => {
1003 let bytes = std::fs::read(path).map_err(|e| format!("cannot read CA file '{path}': {e}"))?;
1004 let certs = CertificateDer::pem_slice_iter(&bytes)
1005 .collect::<Result<Vec<_>, _>>()
1006 .map_err(|e| format!("cannot parse CA file '{path}': {e}"))?;
1007 if certs.is_empty() {
1008 return Err(format!("no certificates in CA file '{path}'"));
1009 }
1010 let mut roots = rustls::RootCertStore::empty();
1011 for cert in certs {
1012 roots.add(cert).map_err(|e| format!("invalid CA certificate in '{path}': {e}"))?;
1013 }
1014 roots
1015 }
1016 None => rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()),
1017 };
1018 let builder = crate::tls_client_builder().with_root_certificates(roots);
1019 let config = match client_cert {
1020 None => builder.with_no_client_auth(),
1021 Some((cert_path, key_path)) => {
1022 let bytes = std::fs::read(cert_path).map_err(|e| format!("cannot read client certificate '{cert_path}': {e}"))?;
1023 let chain = CertificateDer::pem_slice_iter(&bytes)
1024 .collect::<Result<Vec<_>, _>>()
1025 .map_err(|e| format!("cannot parse client certificate '{cert_path}': {e}"))?;
1026 if chain.is_empty() {
1027 return Err(format!("no certificate in '{cert_path}'"));
1028 }
1029 let bytes = std::fs::read(key_path).map_err(|e| format!("cannot read client key '{key_path}': {e}"))?;
1030 let key = PrivateKeyDer::from_pem_slice(&bytes).map_err(|e| format!("cannot parse client key '{key_path}': {e}"))?;
1031 builder.with_client_auth_cert(chain, key).map_err(|e| format!("client certificate '{cert_path}': {e}"))?
1032 }
1033 };
1034 let label = format!("{}|{}", ca_file.unwrap_or("webpki"), client_cert.map_or("", |(cert, _)| cert));
1035 Ok(UpstreamTls::with_config(config, &label))
1036 }
1037 #[cfg(not(any(feature = "http-client", feature = "http2")))]
1038 {
1039 let _ = client_cert;
1040 Err(format!("tls_ca_file '{}': HTTPS backends need the http2 or http-client feature", ca_file.unwrap_or("")))
1041 }
1042 }
1043
1044 #[cfg(any(feature = "http-client", feature = "http2"))]
1045 fn with_config(config: rustls::ClientConfig, label: &str) -> UpstreamTls {
1046 #[cfg(feature = "http2")]
1047 let h2_config = Arc::new(h2_upstream::with_h2_alpn(config.clone()));
1048 UpstreamTls {
1049 config: Arc::new(config),
1050 pool: tls_pool_for_ca(label),
1051 #[cfg(feature = "http2")]
1052 async_pool: async_tls_pool_for_ca(label),
1053 #[cfg(feature = "http2")]
1054 h2_config,
1055 }
1056 }
1057
1058 #[cfg(feature = "http2")]
1060 #[doc(hidden)]
1061 pub fn h2_config(&self) -> &Arc<rustls::ClientConfig> {
1062 &self.h2_config
1063 }
1064
1065 #[doc(hidden)]
1069 pub fn reject_all(label: &str) -> UpstreamTls {
1070 #[cfg(any(feature = "http-client", feature = "http2"))]
1071 {
1072 let config = crate::tls_client_builder().with_root_certificates(rustls::RootCertStore::empty()).with_no_client_auth();
1073 UpstreamTls::with_config(config, &format!("reject-all:{label}"))
1074 }
1075 #[cfg(not(any(feature = "http-client", feature = "http2")))]
1076 {
1077 let _ = label;
1078 UpstreamTls {}
1079 }
1080 }
1081
1082 #[cfg(feature = "http2")]
1084 #[doc(hidden)]
1085 pub fn async_tls(&self) -> async_upstream::AsyncTls {
1086 async_upstream::AsyncTls { config: Arc::clone(&self.config), pool: self.async_pool }
1087 }
1088
1089 #[cfg(any(feature = "http-client", feature = "http2"))]
1091 #[doc(hidden)]
1092 pub fn client_config(&self) -> Arc<rustls::ClientConfig> {
1093 Arc::clone(&self.config)
1094 }
1095}
1096
1097#[cfg(feature = "http2")]
1100fn async_tls_pool_for_ca(path: &str) -> &'static Pool<async_upstream::AsyncTlsStream> {
1101 static POOLS: std::sync::OnceLock<
1102 std::sync::Mutex<std::collections::HashMap<String, &'static Pool<async_upstream::AsyncTlsStream>>>,
1103 > = std::sync::OnceLock::new();
1104 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
1105 pools
1106 .entry(path.to_string())
1107 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, pool::IDLE_TIMEOUT))))
1108}
1109
1110#[cfg(any(feature = "http-client", feature = "http2"))]
1112fn tls_pool_for_ca(path: &str) -> &'static Pool<TlsStream> {
1113 static POOLS: std::sync::OnceLock<std::sync::Mutex<std::collections::HashMap<String, &'static Pool<TlsStream>>>> =
1114 std::sync::OnceLock::new();
1115 let mut pools = POOLS.get_or_init(Default::default).lock().unwrap_or_else(|e| e.into_inner());
1116 pools
1117 .entry(path.to_string())
1118 .or_insert_with(|| Box::leak(Box::new(Pool::new(POOL_MAX_IDLE, pool::IDLE_TIMEOUT))))
1119}
1120
1121#[doc(hidden)]
1125pub fn forward_pooled(
1126 pool: &ConnPool,
1127 request: &Request,
1128 client: Peer<'_>,
1129 host: &str,
1130 port: u16,
1131 connect_timeout: Duration,
1132 read_timeout: Duration,
1133) -> Result<Response, String> {
1134 forward_via(pool, request, client, host, port, host, read_timeout, || connect_tcp(host, port, connect_timeout))
1135}
1136
1137#[cfg(feature = "gateway-api")]
1140#[allow(clippy::too_many_arguments)]
1141#[doc(hidden)]
1142pub fn forward_pooled_with_host(
1143 pool: &ConnPool,
1144 request: &Request,
1145 client: Peer<'_>,
1146 host: &str,
1147 port: u16,
1148 host_header: &str,
1149 connect_timeout: Duration,
1150 read_timeout: Duration,
1151) -> Result<Response, String> {
1152 forward_via(pool, request, client, host, port, host_header, read_timeout, || connect_tcp(host, port, connect_timeout))
1153}
1154
1155#[allow(clippy::too_many_arguments)]
1162fn forward_via<S: UpstreamIo>(
1163 pool: &Pool<S>,
1164 request: &Request,
1165 client: Peer<'_>,
1166 host: &str,
1167 port: u16,
1168 host_header: &str,
1169 read_timeout: Duration,
1170 connect: impl Fn() -> Result<S, String>,
1171) -> Result<Response, String> {
1172 let key = format!("{}:{}", host, port);
1173 if let Some(stream) = acquire_open(pool, &key) {
1174 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1175 Exchange::Done(response) => return Ok(response),
1176 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
1178 }
1179 }
1180 let stream = connect()?;
1181 match exchange(pool, &key, stream, request, host_header, client, read_timeout) {
1182 Exchange::Done(response) => Ok(response),
1183 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
1184 }
1185}
1186
1187fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
1191 let key = format!("{}:{}", host, port);
1192 let addrs: Vec<_> = key
1193 .to_socket_addrs()
1194 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
1195 .collect();
1196 let mut last_error = format!("no address resolved for {}", key);
1197 for addr in addrs {
1198 match TcpStream::connect_timeout(&addr, connect_timeout) {
1199 Ok(stream) => {
1200 let _ = stream.set_nodelay(true);
1201 return Ok(stream);
1202 }
1203 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
1204 }
1205 }
1206 Err(last_error)
1207}
1208
1209#[cfg(any(feature = "http-client", feature = "http2"))]
1211fn shared_tls_pool() -> &'static Pool<TlsStream> {
1212 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
1213 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, pool::IDLE_TIMEOUT))
1214}
1215
1216#[cfg(any(feature = "http-client", feature = "http2"))]
1220#[doc(hidden)]
1221pub fn tls_client_config() -> Arc<rustls::ClientConfig> {
1222 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
1223 Arc::clone(CONFIG.get_or_init(|| {
1224 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
1225 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
1226 }))
1227}
1228
1229#[cfg(any(feature = "http-client", feature = "http2"))]
1230fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
1231 let tcp = connect_tcp(host, port, connect_timeout)?;
1232 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
1233 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
1234 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
1235 Ok(rustls::StreamOwned::new(conn, tcp))
1236}
1237
1238fn is_idempotent(method: &str) -> bool {
1239 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
1240}
1241
1242enum Exchange {
1243 Done(Response),
1244 NoResponse(String),
1246 Failed(String),
1247}
1248
1249fn exchange<S: UpstreamIo>(
1252 pool: &Pool<S>,
1253 key: &str,
1254 mut stream: S,
1255 request: &Request,
1256 host: &str,
1257 client: Peer<'_>,
1258 read_timeout: Duration,
1259) -> Exchange {
1260 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
1261 return Exchange::Failed(e.to_string());
1262 }
1263 let req_bytes = build_request(request, host, client, true);
1265 if let Err(e) = stream.write_all(&req_bytes) {
1266 return Exchange::NoResponse(format!("write to backend failed: {}", e));
1267 }
1268
1269 let mut tmp = [0u8; 4096];
1270 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
1271 Ok(head) => head,
1272 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
1273 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
1274 };
1275 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
1276
1277 if response_has_no_body(&request.method, &header_lower) {
1278 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
1279 pool.release(key, stream);
1280 }
1281 return match Response::parse_upstream(&header_bytes, REGENERATED_OR_HOP_BY_HOP) {
1282 Ok(response) => Exchange::Done(response),
1283 Err(e) => Exchange::Failed(e),
1284 };
1285 }
1286
1287 if should_stream_response(&header_lower) {
1288 return match parse_status_and_headers(&header_bytes) {
1291 Ok(mut response) => {
1292 let facts = HeadFacts::scan(&header_bytes);
1293 let body: Box<dyn Read + Send> = Box::new(ConcatReader::new(body_prefix, stream));
1294 response.stream_pipe = Some(bounded_body(body, facts.chunked, facts.content_length));
1295 Exchange::Done(response)
1296 }
1297 Err(e) => Exchange::Failed(e),
1298 };
1299 }
1300 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
1302 Ok((resp_bytes, reusable)) => {
1303 if reusable {
1304 pool.release(key, stream);
1305 }
1306 match Response::parse_upstream(&resp_bytes, REGENERATED_OR_HOP_BY_HOP) {
1307 Ok(response) => Exchange::Done(response),
1308 Err(e) => Exchange::Failed(e),
1309 }
1310 }
1311 Err(e) => Exchange::Failed(e),
1312 }
1313}
1314
1315#[cfg(test)]
1320fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
1321 read_response_head(stream, tmp).map_err(|e| match e {
1322 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
1323 })
1324}
1325
1326enum HeadError {
1330 NothingReceived(String),
1331 Partial(String),
1332}
1333
1334fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1335 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
1336}
1337
1338fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1344 let (mut head, mut rest) = read_response_head(stream, tmp)?;
1345 let mut interim = 0;
1346 while is_interim_response(&head) {
1347 crate::early_hints::forward_upstream(&head);
1348 interim += 1;
1349 if interim > MAX_INTERIM_RESPONSES {
1350 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
1351 }
1352 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
1353 }
1354 Ok((head, rest))
1355}
1356
1357fn read_response_head_from<S: Read>(
1361 stream: &mut S,
1362 tmp: &mut [u8],
1363 mut buf: Vec<u8>,
1364 received: bool,
1365) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
1366 loop {
1367 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
1368 let body_prefix = buf[pos + 4..].to_vec();
1369 buf.truncate(pos + 4);
1370 return Ok((buf, body_prefix));
1371 }
1372 let nothing = buf.is_empty() && !received;
1373 let n = match stream.read(tmp) {
1374 Ok(n) => n,
1375 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
1376 Err(e) => return Err(HeadError::Partial(e.to_string())),
1377 };
1378 if n == 0 {
1379 let msg = "backend closed connection before headers were complete".to_string();
1380 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
1381 }
1382 buf.extend_from_slice(&tmp[..n]);
1383 }
1384}
1385
1386#[doc(hidden)]
1388pub const MAX_INTERIM_RESPONSES: usize = 16;
1389
1390#[doc(hidden)]
1393pub fn is_interim_response(head: &[u8]) -> bool {
1394 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
1395 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
1396 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
1397}
1398
1399#[doc(hidden)]
1403pub fn response_has_no_body(method: &str, header_lower: &str) -> bool {
1404 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
1405 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
1406}
1407
1408#[doc(hidden)]
1415pub fn should_stream_response(header_lower: &str) -> bool {
1416 let is_sse = header_lower.lines().any(|l| {
1417 l.starts_with("content-type:") && l.contains("text/event-stream")
1418 });
1419 let is_chunked = header_lower.lines().any(|l| {
1420 l.starts_with("transfer-encoding:") && l.contains("chunked")
1421 });
1422 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1423 l.strip_prefix("content-length:")?.trim().parse().ok()
1424 });
1425 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
1426 is_sse || is_chunked || is_large
1427}
1428
1429#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1435#[doc(hidden)]
1436pub struct HeadFacts {
1437 #[doc(hidden)]
1439 pub status_no_body: bool,
1440 #[doc(hidden)]
1441 pub sse: bool,
1442 #[doc(hidden)]
1443 pub chunked: bool,
1444 #[doc(hidden)]
1446 pub content_length: Option<usize>,
1447 #[doc(hidden)]
1448 pub connection_close: bool,
1449}
1450
1451#[cfg_attr(not(feature = "http2"), allow(dead_code))]
1452impl HeadFacts {
1453 #[doc(hidden)]
1454 pub fn scan(head: &[u8]) -> HeadFacts {
1455 let text = std::str::from_utf8(head).unwrap_or("");
1456 let status = text.split_whitespace().nth(1).unwrap_or("");
1457 let mut facts = HeadFacts {
1458 status_no_body: status.starts_with('1') || status == "204" || status == "304",
1459 sse: false,
1460 chunked: false,
1461 content_length: None,
1462 connection_close: false,
1463 };
1464 for line in text.lines() {
1465 if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-type:") {
1466 facts.sse |= contains_ignore_ascii_case(value, "text/event-stream");
1467 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "transfer-encoding:") {
1468 facts.chunked |= contains_ignore_ascii_case(value, "chunked");
1469 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "content-length:") {
1470 if facts.content_length.is_none() {
1471 facts.content_length = value.trim().parse().ok();
1472 }
1473 } else if let Some(value) = strip_prefix_ignore_ascii_case(line, "connection:") {
1474 facts.connection_close |= contains_ignore_ascii_case(value, "close");
1475 }
1476 }
1477 facts
1478 }
1479
1480 #[doc(hidden)]
1482 pub fn no_body(&self, method: &str) -> bool {
1483 method.eq_ignore_ascii_case("HEAD") || self.status_no_body
1484 }
1485
1486 #[doc(hidden)]
1488 pub fn should_stream(&self) -> bool {
1489 self.sse || self.chunked || self.content_length.is_some_and(|n| n > STREAM_THRESHOLD)
1490 }
1491}
1492
1493fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, prefix: &str) -> Option<&'a str> {
1494 let bytes = line.as_bytes();
1495 (bytes.len() >= prefix.len() && bytes[..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()))
1497 .then(|| &line[prefix.len()..])
1498}
1499
1500fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
1501 haystack.as_bytes().windows(needle.len()).any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
1502}
1503
1504fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
1506 let s = std::str::from_utf8(header_bytes)
1507 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
1508 let mut lines = s.lines();
1509 let status_line = lines.next().ok_or("empty backend response")?;
1510 let mut parts = status_line.splitn(3, ' ');
1511 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
1512 let status_code: i16 = parts
1513 .next()
1514 .unwrap_or("502")
1515 .parse()
1516 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
1517 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
1518 let mut headers = Vec::new();
1519 for line in lines {
1520 let line = line.trim_end_matches('\r');
1521 if line.is_empty() { break; }
1522 if let Some(colon) = line.find(':') {
1523 headers.push(crate::header::Header {
1524 name: line[..colon].trim().to_string().into(),
1525 value: (line[colon + 1..].trim().to_string()).into(),
1526 });
1527 }
1528 }
1529 Ok(Response {
1530 http_version,
1531 status_code,
1532 reason_phrase,
1533 headers,
1534 content_range_list: vec![],
1535 stream_file: None,
1536 stream_pipe: None,
1537 trailers: None,
1538 })
1539}
1540
1541fn read_response_from_partial<S: Read>(
1547 stream: &mut S,
1548 header_bytes: Vec<u8>,
1549 body_prefix: Vec<u8>,
1550 tmp: &mut [u8],
1551) -> Result<(Vec<u8>, bool), String> {
1552 let header_end = header_bytes.len();
1553 let mut buf = header_bytes;
1554 buf.extend_from_slice(&body_prefix);
1555
1556 let header_lower =
1557 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
1558 let connection_close =
1559 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
1560 let is_chunked = header_lower
1561 .lines()
1562 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
1563 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
1564 l.strip_prefix("content-length:")?.trim().parse().ok()
1565 });
1566
1567 if is_chunked {
1568 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
1569 rewrite_as_content_length(&mut buf, header_end, &decoded);
1570 Ok((buf, !connection_close))
1571 } else if let Some(len) = content_length {
1572 while buf.len() < header_end + len {
1573 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1574 if n == 0 { break; }
1575 buf.extend_from_slice(&tmp[..n]);
1576 }
1577 Ok((buf, !connection_close))
1578 } else {
1579 loop {
1580 let n = stream.read(tmp).map_err(|e| e.to_string())?;
1581 if n == 0 { break; }
1582 buf.extend_from_slice(&tmp[..n]);
1583 }
1584 Ok((buf, false))
1585 }
1586}
1587
1588struct Backend {
1591 host: String,
1592 port: u16,
1593 tls: bool,
1596}
1597
1598impl Backend {
1599 fn parse(url: &str) -> Option<Self> {
1600 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
1601 (r, true, 443u16)
1602 } else if let Some(r) = url.strip_prefix("h2s://") {
1603 (r, true, 443u16)
1604 } else if let Some(r) = url.strip_prefix("grpcs://") {
1605 (r, true, 443u16)
1606 } else if let Some(r) = url.strip_prefix("http://") {
1607 (r, false, 80u16)
1608 } else if let Some(r) = url.strip_prefix("h2://") {
1609 (r, false, 80u16)
1610 } else if let Some(r) = url.strip_prefix("grpc://") {
1611 (r, false, 80u16)
1612 } else {
1613 (url, false, 80u16)
1614 };
1615 let host_port = rest.split('/').next().unwrap_or(rest);
1617 let (host, port) = if let Some(colon) = host_port.rfind(':') {
1618 let port_str = &host_port[colon + 1..];
1619 if let Ok(p) = port_str.parse::<u16>() {
1620 (host_port[..colon].to_string(), p)
1621 } else {
1622 (host_port.to_string(), default_port)
1623 }
1624 } else {
1625 (host_port.to_string(), default_port)
1626 };
1627 if host.is_empty() {
1628 return None;
1629 }
1630 Some(Backend { host, port, tls })
1631 }
1632}
1633
1634#[cfg(feature = "http2")]
1645pub struct H2ReverseProxy {
1646 inner: ReverseProxy,
1647}
1648
1649#[cfg(feature = "http2")]
1650impl H2ReverseProxy {
1651 pub fn new<I, S>(backends: I) -> Self
1662 where
1663 I: IntoIterator<Item = S>,
1664 S: AsRef<str>,
1665 {
1666 H2ReverseProxy {
1667 inner: ReverseProxy::new(backends),
1668 }
1669 }
1670
1671 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1673 self.inner = self.inner.path_prefix(prefix);
1674 self
1675 }
1676
1677 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1679 self.inner = self.inner.connect_timeout_ms(ms);
1680 self
1681 }
1682
1683 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1688 self.inner = self.inner.read_timeout_ms(ms);
1689 self
1690 }
1691
1692 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1695 self.inner = self.inner.tls_ca_file(path)?;
1696 Ok(self)
1697 }
1698
1699 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1702 self.inner = self.inner.tls_client_cert(cert, key)?;
1703 Ok(self)
1704 }
1705
1706 fn matches(&self, request: &Request) -> bool {
1707 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1708 }
1709}
1710
1711#[cfg(feature = "http2")]
1712impl crate::middleware::Middleware for H2ReverseProxy {
1713 fn handle(
1714 &self,
1715 request: &crate::request::Request,
1716 connection: &crate::server::ConnectionInfo,
1717 next: &dyn crate::application::Application,
1718 ) -> Result<crate::response::Response, String> {
1719 if !self.matches(request) {
1720 return next.execute(request, connection);
1721 }
1722 if self.inner.backends.is_empty() {
1723 let mut response = bad_gateway();
1724 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1725 return Ok(response);
1726 }
1727 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1729 let n = self.inner.backends.len();
1730 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1731 let mut last_error = String::new();
1732 for attempt in 0..attempts {
1733 let backend = &self.inner.backends[(start + attempt) % n];
1734 let tls = backend.tls.then(|| match &self.inner.trust {
1735 Some(trust) => trust.h2_config(),
1736 None => h2_upstream::default_tls(),
1737 });
1738 let target = h2_upstream::Target {
1739 backend,
1740 tls,
1741 connect_timeout: self.inner.connect_timeout,
1742 read_timeout: self.inner.read_timeout,
1743 authority: None,
1744 };
1745 match h2_upstream::forward(request, Peer::of(connection), &target) {
1746 Ok(mut response) => {
1747 status::received(&mut response, None);
1748 return Ok(response);
1749 }
1750 Err(e) => last_error = e,
1751 }
1752 }
1753 let mut response = bad_gateway();
1754 status::failed_with(&mut response, &last_error, None);
1755 Ok(response)
1756 }
1757
1758 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1761 true
1762 }
1763
1764 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1767 self.matches(request)
1768 }
1769}
1770
1771#[cfg(feature = "http3")]
1794pub struct H3ReverseProxy {
1795 inner: ReverseProxy,
1796 name: String,
1797 tcp_until: std::sync::Mutex<std::collections::HashMap<String, std::time::Instant>>,
1799}
1800
1801#[cfg(feature = "http3")]
1802impl H3ReverseProxy {
1803 pub fn new<I, S>(backends: I) -> Self
1809 where
1810 I: IntoIterator<Item = S>,
1811 S: AsRef<str>,
1812 {
1813 H3ReverseProxy { inner: ReverseProxy::new(backends), name: "h3-reverse-proxy".to_string(), tcp_until: std::sync::Mutex::default() }
1814 }
1815
1816 pub fn name(mut self, name: impl Into<String>) -> Self {
1818 self.name = name.into();
1819 self
1820 }
1821
1822 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1824 self.inner = self.inner.path_prefix(prefix);
1825 self
1826 }
1827
1828 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
1830 self.inner = self.inner.connect_timeout_ms(ms);
1831 self
1832 }
1833
1834 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
1837 self.inner = self.inner.read_timeout_ms(ms);
1838 self
1839 }
1840
1841 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
1844 self.inner = self.inner.tls_ca_file(path)?;
1845 Ok(self)
1846 }
1847
1848 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
1850 self.inner = self.inner.tls_client_cert(cert, key)?;
1851 Ok(self)
1852 }
1853
1854 fn matches(&self, request: &Request) -> bool {
1855 self.inner.path_prefix.as_ref().is_none_or(|prefix| request.request_uri.starts_with(prefix.as_str()))
1856 }
1857
1858 fn on_tcp(&self, key: &str) -> bool {
1859 let mut tcp_until = self.tcp_until.lock().unwrap_or_else(|e| e.into_inner());
1860 match tcp_until.get(key) {
1861 Some(until) if std::time::Instant::now() < *until => true,
1862 Some(_) => {
1863 tcp_until.remove(key);
1864 false
1865 }
1866 None => false,
1867 }
1868 }
1869
1870 fn attempt(&self, backend: &Backend, request: &Request, connection: &crate::server::ConnectionInfo) -> Result<Response, String> {
1873 let key = format!("{}:{}", backend.host, backend.port);
1874 if backend.tls && !self.on_tcp(&key) {
1875 let config = match &self.inner.trust {
1876 Some(trust) => trust.client_config(),
1877 None => tls_client_config(),
1878 };
1879 let target = h3_upstream::Target {
1880 host: &backend.host,
1881 port: backend.port,
1882 authority: None,
1883 tls: &config,
1884 connect_timeout: self.inner.connect_timeout,
1885 read_timeout: self.inner.read_timeout,
1886 };
1887 match h3_upstream::forward(request, Peer::of(connection), &target) {
1888 Err(h3_upstream::H3Error::Connect(e)) => {
1889 self.tcp_until.lock().unwrap_or_else(|e| e.into_inner()).insert(key.clone(), std::time::Instant::now() + crate::proxy_config::H3_FALLBACK);
1890 crate::proxy_config::upstream::observe_h3_fallback(&self.name);
1891 eprintln!("{}: HTTP/3 to {key} failed ({e}); using TCP for {} s", self.name, crate::proxy_config::H3_FALLBACK.as_secs());
1892 }
1893 result => return result.map_err(|e| e.to_string()),
1894 }
1895 }
1896 forward_upstream_trusting(
1897 &self.inner.pool,
1898 request,
1899 Peer::of(connection),
1900 &backend.host,
1901 backend.port,
1902 backend.tls,
1903 self.inner.trust.as_deref(),
1904 self.inner.connect_timeout,
1905 self.inner.read_timeout,
1906 )
1907 }
1908}
1909
1910#[cfg(feature = "http3")]
1911impl crate::middleware::Middleware for H3ReverseProxy {
1912 fn handle(
1913 &self,
1914 request: &crate::request::Request,
1915 connection: &crate::server::ConnectionInfo,
1916 next: &dyn crate::application::Application,
1917 ) -> Result<crate::response::Response, String> {
1918 if !self.matches(request) {
1919 return next.execute(request, connection);
1920 }
1921 if self.inner.backends.is_empty() {
1922 let mut response = bad_gateway();
1923 status::failed(&mut response, status::ProxyError::DestinationUnavailable, None, None);
1924 return Ok(response);
1925 }
1926 let attempts = if matches!(request.body, crate::request::Body::Stream(_)) { 1 } else { self.inner.backends.len() };
1928 let n = self.inner.backends.len();
1929 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1930 let mut last_error = String::new();
1931 for attempt in 0..attempts {
1932 match self.attempt(&self.inner.backends[(start + attempt) % n], request, connection) {
1933 Ok(mut response) => {
1934 status::received(&mut response, None);
1935 return Ok(response);
1936 }
1937 Err(e) => last_error = e,
1938 }
1939 }
1940 let mut response = bad_gateway();
1941 status::failed_with(&mut response, &last_error, None);
1942 Ok(response)
1943 }
1944
1945 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1946 true
1947 }
1948
1949 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
1951 self.matches(request)
1952 }
1953}
1954
1955#[cfg(feature = "http2")]
1959#[doc(hidden)]
1960pub fn forward_h2(
1961 request: &Request,
1962 client: Peer<'_>,
1963 url: &str,
1964 tls: bool,
1965 trust: Option<&UpstreamTls>,
1966 connect_timeout: Duration,
1967 read_timeout: Duration,
1968) -> Result<Response, String> {
1969 let mut backend = Backend::parse(url).ok_or_else(|| format!("invalid upstream address '{url}'"))?;
1970 backend.tls |= tls;
1971 let tls = backend.tls.then(|| match trust {
1972 Some(t) => t.h2_config(),
1973 None => h2_upstream::default_tls(),
1974 });
1975 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls, connect_timeout, read_timeout, authority: None })
1976}
1977
1978#[cfg(feature = "gateway-api")]
1983#[allow(clippy::too_many_arguments)]
1984#[doc(hidden)]
1985pub fn forward_tls_with_host(
1986 config: &Arc<rustls::ClientConfig>,
1987 label: &str,
1988 request: &Request,
1989 client: Peer<'_>,
1990 ip: &str,
1991 port: u16,
1992 server_name: &str,
1993 host_header: &str,
1994 connect_timeout: Duration,
1995 read_timeout: Duration,
1996) -> Result<Response, String> {
1997 let pool = tls_pool_for_ca(label);
1998 forward_via(pool, request, client, ip, port, host_header, read_timeout, || {
1999 let tcp = connect_tcp(ip, port, connect_timeout)?;
2000 let name = rustls::pki_types::ServerName::try_from(server_name.to_string()).map_err(|e| format!("invalid TLS server name '{server_name}': {e}"))?;
2001 let conn = rustls::ClientConnection::new(Arc::clone(config), name).map_err(|e| e.to_string())?;
2002 Ok(rustls::StreamOwned::new(conn, tcp))
2003 })
2004}
2005
2006#[cfg(feature = "gateway-api")]
2010#[doc(hidden)]
2011pub fn forward_h2c_with_host(
2012 request: &Request,
2013 client: Peer<'_>,
2014 host: &str,
2015 port: u16,
2016 authority: &str,
2017 connect_timeout: Duration,
2018 read_timeout: Duration,
2019) -> Result<Response, String> {
2020 let backend = Backend { host: host.to_string(), port, tls: false };
2021 h2_upstream::forward(request, client, &h2_upstream::Target { backend: &backend, tls: None, connect_timeout, read_timeout, authority: Some(authority) })
2022}
2023
2024#[cfg(feature = "http2")]
2045pub struct GrpcProxy {
2046 inner: H2ReverseProxy,
2047}
2048
2049#[cfg(feature = "http2")]
2050impl GrpcProxy {
2051 pub fn new<I, S>(backends: I) -> Self
2061 where
2062 I: IntoIterator<Item = S>,
2063 S: AsRef<str>,
2064 {
2065 GrpcProxy { inner: H2ReverseProxy::new(backends) }
2066 }
2067
2068 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
2070 self.inner = self.inner.path_prefix(prefix);
2071 self
2072 }
2073
2074 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
2076 self.inner = self.inner.connect_timeout_ms(ms);
2077 self
2078 }
2079
2080 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
2082 self.inner = self.inner.read_timeout_ms(ms);
2083 self
2084 }
2085
2086 pub fn tls_ca_file(mut self, path: &str) -> Result<Self, String> {
2088 self.inner = self.inner.tls_ca_file(path)?;
2089 Ok(self)
2090 }
2091
2092 pub fn tls_client_cert(mut self, cert: &str, key: &str) -> Result<Self, String> {
2095 self.inner = self.inner.tls_client_cert(cert, key)?;
2096 Ok(self)
2097 }
2098}
2099
2100#[cfg(feature = "http2")]
2101impl crate::middleware::Middleware for GrpcProxy {
2102 fn handle(
2103 &self,
2104 request: &crate::request::Request,
2105 connection: &crate::server::ConnectionInfo,
2106 next: &dyn crate::application::Application,
2107 ) -> Result<crate::response::Response, String> {
2108 if is_grpc(request) {
2109 self.inner.handle(request, connection, next)
2110 } else {
2111 next.execute(request, connection)
2112 }
2113 }
2114
2115 fn accepts_streamed_body(&self, _request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
2116 true
2117 }
2118
2119 fn answers_streamed_body(&self, request: &Request, _connection: &crate::server::ConnectionInfo) -> bool {
2120 is_grpc(request) && self.inner.matches(request)
2121 }
2122}
2123
2124#[cfg(feature = "http2")]
2125fn is_grpc(request: &Request) -> bool {
2126 request.header(crate::header::Header::_CONTENT_TYPE).is_some_and(|h| h.value.starts_with("application/grpc"))
2127}
2128
2129#[cfg(test)]
2132mod backend_parse_tests {
2133 use super::Backend;
2134
2135 fn parse(url: &str) -> Option<(String, u16, bool)> {
2136 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
2137 }
2138
2139 #[test]
2140 fn bare_host_port() {
2141 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
2142 }
2143
2144 #[test]
2145 fn http_scheme() {
2146 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
2147 }
2148
2149 #[test]
2150 fn h2_scheme_plain() {
2151 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
2152 }
2153
2154 #[test]
2155 fn grpc_scheme_plain() {
2156 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
2157 }
2158
2159 #[test]
2160 fn https_scheme_sets_tls_and_default_port() {
2161 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
2162 }
2163
2164 #[test]
2165 fn https_scheme_explicit_port() {
2166 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
2167 }
2168
2169 #[test]
2170 fn h2s_scheme_sets_tls() {
2171 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
2172 }
2173
2174 #[test]
2175 fn h2s_scheme_explicit_port() {
2176 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
2177 }
2178
2179 #[test]
2180 fn grpcs_scheme_sets_tls() {
2181 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
2182 }
2183
2184 #[test]
2185 fn grpcs_scheme_explicit_port() {
2186 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
2187 }
2188
2189 #[test]
2190 fn empty_host_returns_none() {
2191 assert_eq!(None, parse("https://"));
2192 }
2193
2194 #[test]
2195 fn bare_host_no_port_defaults_to_80() {
2196 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
2197 }
2198}