1pub mod pool;
27
28#[cfg(test)]
29mod tests;
30
31use std::io::{Read, Write};
32use std::net::{TcpStream, ToSocketAddrs};
33use std::sync::atomic::{AtomicUsize, Ordering};
34use std::sync::Arc;
35use std::time::Duration;
36
37pub use pool::ConnPool;
38
39use crate::application::Application;
40use crate::core::New;
41use crate::middleware::Middleware;
42use crate::mime_type::MimeType;
43use crate::range::Range;
44use crate::request::Request;
45use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
46use crate::server::ConnectionInfo;
47
48const HOP_BY_HOP: &[&str] = &[
50 "connection",
51 "keep-alive",
52 "proxy-authenticate",
53 "proxy-authorization",
54 "te",
55 "trailers",
56 "transfer-encoding",
57 "upgrade",
58];
59
60pub enum LoadBalancing {
62 RoundRobin,
64}
65
66pub struct ReverseProxy {
81 backends: Vec<Backend>,
82 path_prefix: Option<String>,
83 connect_timeout: Duration,
84 read_timeout: Duration,
85 counter: AtomicUsize,
86 pool: Arc<ConnPool>,
87 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
88}
89
90impl ReverseProxy {
91 pub fn new<I, S>(backends: I) -> Self
95 where
96 I: IntoIterator<Item = S>,
97 S: AsRef<str>,
98 {
99 Self {
100 backends: backends
101 .into_iter()
102 .filter_map(|u| Backend::parse(u.as_ref()))
103 .collect(),
104 path_prefix: None,
105 connect_timeout: Duration::from_secs(5),
106 read_timeout: Duration::from_secs(30),
107 counter: AtomicUsize::new(0),
108 pool: Arc::new(ConnPool::new_default()),
109 breaker: None,
110 }
111 }
112
113 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
131 self.breaker = Some(breaker);
132 self
133 }
134
135 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
140 self.path_prefix = Some(prefix.into());
141 self
142 }
143
144 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
146 self
147 }
148
149 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
151 self.connect_timeout = Duration::from_millis(ms);
152 self
153 }
154
155 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
157 self.read_timeout = Duration::from_millis(ms);
158 self
159 }
160
161 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
166 self.pool = pool;
167 self
168 }
169
170 pub fn max_idle_conns(mut self, n: usize) -> Self {
172 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
173 self
174 }
175
176 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
177 if self.backends.is_empty() {
178 return Err("no backends configured".to_string());
179 }
180 let n = self.backends.len();
181 let start = self.counter.fetch_add(1, Ordering::Relaxed);
182 let mut last_err = "all backends failed".to_string();
183 for attempt in 0..n {
184 let idx = (start + attempt) % n;
185 let backend = &self.backends[idx];
186 let key = format!("{}:{}", backend.host, backend.port);
187
188 if let Some(breaker) = &self.breaker {
189 if !breaker.is_available(&key) {
190 last_err = format!("circuit open for {}", key);
191 continue;
192 }
193 }
194
195 match self.try_backend(request, connection, backend) {
196 Ok(resp) => {
197 if let Some(breaker) = &self.breaker {
198 breaker.record_success(&key);
199 }
200 return Ok(resp);
201 }
202 Err(e) => {
203 if let Some(breaker) = &self.breaker {
204 breaker.record_failure(&key);
205 }
206 last_err = e;
207 }
208 }
209 }
210 Err(last_err)
211 }
212
213 fn try_backend(
214 &self,
215 request: &Request,
216 connection: &ConnectionInfo,
217 backend: &Backend,
218 ) -> Result<Response, String> {
219 let key = format!("{}:{}", backend.host, backend.port);
220
221 let stream = if let Some(pooled) = self.pool.acquire(&key) {
223 pooled
224 } else {
225 let addr_str = key.as_str();
226 let sock_addr = addr_str
227 .to_socket_addrs()
228 .map_err(|e| format!("DNS lookup for {} failed: {}", addr_str, e))?
229 .next()
230 .ok_or_else(|| format!("no address resolved for {}", addr_str))?;
231 TcpStream::connect_timeout(&sock_addr, self.connect_timeout)
232 .map_err(|e| format!("connect to {} failed: {}", addr_str, e))?
233 };
234
235 stream.set_read_timeout(Some(self.read_timeout)).map_err(|e| e.to_string())?;
236 stream.set_write_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;
237
238 let req_bytes = build_request(request, &backend.host, &connection.client.ip, true);
241 let mut stream = stream;
242 stream.write_all(&req_bytes).map_err(|e| format!("write to backend failed: {}", e))?;
243
244 let mut tmp = [0u8; 4096];
245 let (header_bytes, body_prefix) = read_headers_only(&mut stream, &mut tmp)?;
246 let header_lower =
247 std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
248
249 if should_stream_response(&header_lower) {
250 let mut resp = parse_status_and_headers(&header_bytes)?;
253 resp.stream_pipe =
254 Some(Box::new(ConcatReader::new(body_prefix, stream)));
255 Ok(resp)
256 } else {
257 let (resp_bytes, reusable) =
260 read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp)?;
261 if reusable {
262 self.pool.release(&key, stream);
263 }
264 Response::parse(&resp_bytes).map(normalize_upstream_response)
265 }
266 }
267}
268
269impl Middleware for ReverseProxy {
270 fn handle(
271 &self,
272 request: &Request,
273 connection: &ConnectionInfo,
274 next: &dyn Application,
275 ) -> Result<Response, String> {
276 if let Some(prefix) = &self.path_prefix {
277 if !request.request_uri.starts_with(prefix.as_str()) {
278 return next.execute(request, connection);
279 }
280 }
281 match self.proxy(request, connection) {
282 Ok(resp) => Ok(resp),
283 Err(_) => Ok(bad_gateway()),
284 }
285 }
286}
287
288pub(crate) fn build_request(
291 request: &Request,
292 backend_host: &str,
293 client_ip: &str,
294 keep_alive: bool,
295) -> Vec<u8> {
296 let mut out: Vec<u8> = Vec::new();
297 let _ = write!(
298 out,
299 "{} {} HTTP/1.1\r\nHost: {}\r\n",
300 request.method, request.request_uri, backend_host
301 );
302 for h in &request.headers {
303 let lower = h.name.to_lowercase();
304 if HOP_BY_HOP.contains(&lower.as_str()) || lower == "host" {
305 continue;
306 }
307 let _ = write!(out, "{}: {}\r\n", h.name, h.value);
308 }
309 let _ = write!(out, "X-Forwarded-For: {}\r\n", client_ip);
310 let _ = write!(out, "Via: 1.1 rws\r\n");
311 if keep_alive {
312 let _ = write!(out, "Connection: keep-alive\r\n");
313 } else {
314 let _ = write!(out, "Connection: close\r\n");
315 }
316 if !request.body.is_empty() {
317 let _ = write!(out, "Content-Length: {}\r\n", request.body.len());
318 }
319 let _ = write!(out, "\r\n");
320 out.extend_from_slice(&request.body);
321 out
322}
323
324fn decode_chunked(
329 stream: &mut TcpStream,
330 buf: &[u8],
331 header_end: usize,
332 tmp: &mut [u8],
333) -> Result<Vec<u8>, String> {
334 let mut raw: Vec<u8> = buf[header_end..].to_vec();
336 let mut decoded: Vec<u8> = Vec::new();
337
338 loop {
339 let crlf = loop {
341 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
342 break p;
343 }
344 let n = stream.read(tmp).map_err(|e| e.to_string())?;
345 if n == 0 {
346 return Err("chunked: premature EOF reading chunk size".to_string());
347 }
348 raw.extend_from_slice(&tmp[..n]);
349 };
350
351 let size_line = std::str::from_utf8(&raw[..crlf])
353 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
354 let size_str = size_line.split(';').next().unwrap_or("").trim();
355 let chunk_size = usize::from_str_radix(size_str, 16)
356 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
357 raw.drain(..crlf + 2); if chunk_size == 0 {
360 while raw.len() < 2 {
362 let n = stream.read(tmp).map_err(|e| e.to_string())?;
363 if n == 0 {
364 break;
365 }
366 raw.extend_from_slice(&tmp[..n]);
367 }
368 break;
369 }
370
371 while raw.len() < chunk_size + 2 {
373 let n = stream.read(tmp).map_err(|e| e.to_string())?;
374 if n == 0 {
375 return Err("chunked: premature EOF reading chunk body".to_string());
376 }
377 raw.extend_from_slice(&tmp[..n]);
378 }
379 decoded.extend_from_slice(&raw[..chunk_size]);
380 raw.drain(..chunk_size + 2); }
382
383 Ok(decoded)
384}
385
386fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
389 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
390 buf.clear();
391 for line in header_str.lines() {
392 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
393 continue;
394 }
395 buf.extend_from_slice(line.as_bytes());
396 buf.extend_from_slice(b"\r\n");
397 }
398 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
399 buf.extend_from_slice(decoded);
400}
401
402pub(crate) fn read_response(stream: &mut TcpStream) -> Result<Vec<u8>, String> {
405 read_response_from(stream)
406}
407
408pub(crate) fn read_response_from<R: Read>(stream: &mut R) -> Result<Vec<u8>, String> {
409 let mut buf: Vec<u8> = Vec::with_capacity(8192);
410 let mut tmp = [0u8; 4096];
411
412 let header_end = loop {
413 let n = stream.read(&mut tmp).map_err(|e| e.to_string())?;
414 if n == 0 {
415 return if buf.is_empty() {
416 Err("backend closed connection without sending a response".to_string())
417 } else {
418 Ok(buf)
419 };
420 }
421 buf.extend_from_slice(&tmp[..n]);
422 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
423 break pos + 4;
424 }
425 };
426
427 let content_length = std::str::from_utf8(&buf[..header_end])
428 .unwrap_or("")
429 .lines()
430 .find_map(|line| {
431 line.to_lowercase()
432 .starts_with("content-length:")
433 .then(|| line.splitn(2, ':').nth(1)?.trim().parse::<usize>().ok())
434 .flatten()
435 });
436
437 match content_length {
438 Some(len) => {
439 while buf.len() < header_end + len {
440 let n = stream.read(&mut tmp).map_err(|e| e.to_string())?;
441 if n == 0 {
442 break;
443 }
444 buf.extend_from_slice(&tmp[..n]);
445 }
446 }
447 None => loop {
448 let n = stream.read(&mut tmp).map_err(|e| e.to_string())?;
449 if n == 0 {
450 break;
451 }
452 buf.extend_from_slice(&tmp[..n]);
453 },
454 }
455
456 Ok(buf)
457}
458
459pub(crate) fn proxy_http1(
465 request: &Request,
466 client_ip: &str,
467 host: &str,
468 port: u16,
469 connect_timeout: Duration,
470 read_timeout: Duration,
471) -> Result<Response, String> {
472 use std::net::ToSocketAddrs;
473 let addr_str = format!("{}:{}", host, port);
474 let sock_addr = addr_str
475 .to_socket_addrs()
476 .map_err(|e| format!("DNS lookup for {} failed: {}", addr_str, e))?
477 .next()
478 .ok_or_else(|| format!("no address resolved for {}", addr_str))?;
479 let stream = TcpStream::connect_timeout(&sock_addr, connect_timeout)
480 .map_err(|e| format!("connect to {} failed: {}", addr_str, e))?;
481 stream.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
482 stream.set_write_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;
483 let req_bytes = build_request(request, host, client_ip, false);
484 let mut stream = stream;
485 stream.write_all(&req_bytes).map_err(|e| format!("write to backend failed: {}", e))?;
486 let resp_bytes = read_response(&mut stream)?;
487 Response::parse(&resp_bytes).map(normalize_upstream_response)
488}
489
490#[cfg(any(feature = "http-client", feature = "http2"))]
494pub(crate) fn proxy_https1(
495 request: &Request,
496 client_ip: &str,
497 host: &str,
498 port: u16,
499 connect_timeout: Duration,
500 read_timeout: Duration,
501) -> Result<Response, String> {
502 use rustls::pki_types::ServerName;
503 use rustls::ClientConfig;
504 use std::net::ToSocketAddrs;
505 use std::sync::Arc;
506
507 let addr_str = format!("{}:{}", host, port);
508 let sock_addr = addr_str
509 .to_socket_addrs()
510 .map_err(|e| format!("DNS lookup for {} failed: {}", addr_str, e))?
511 .next()
512 .ok_or_else(|| format!("no address resolved for {}", addr_str))?;
513
514 let stream = TcpStream::connect_timeout(&sock_addr, connect_timeout)
515 .map_err(|e| format!("connect to {} failed: {}", addr_str, e))?;
516 stream.set_read_timeout(Some(read_timeout)).map_err(|e| e.to_string())?;
517 stream.set_write_timeout(Some(Duration::from_secs(10))).map_err(|e| e.to_string())?;
518
519 let root_store =
520 rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
521 let config = Arc::new(
522 ClientConfig::builder()
523 .with_root_certificates(root_store)
524 .with_no_client_auth(),
525 );
526 let server_name = ServerName::try_from(host.to_string())
527 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
528 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
529 let mut tls = rustls::StreamOwned::new(conn, stream);
530
531 let req_bytes = build_request(request, host, client_ip, false);
532 tls.write_all(&req_bytes)
533 .map_err(|e| format!("write to upstream failed: {}", e))?;
534
535 let resp_bytes = read_response_from(&mut tls)?;
536 Response::parse(&resp_bytes).map(normalize_upstream_response)
537}
538
539const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
542 "content-length", "transfer-encoding", "connection", "keep-alive",
543 "proxy-connection", "te", "trailer", "upgrade",
544];
545
546pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
555 response.http_version = crate::http::VERSION.http_1_1.to_string();
556 let content_type = response
557 .headers
558 .iter()
559 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
560 .map(|h| h.value.clone());
561 let has_body = !response.content_range_list.is_empty();
562 if let (Some(content_type), [range]) = (&content_type, response.content_range_list.as_mut_slice()) {
563 range.content_type = content_type.clone();
564 }
565 response.headers.retain(|h| {
566 let name = h.name.to_ascii_lowercase();
567 if name == "content-type" {
568 return !has_body;
570 }
571 !REGENERATED_OR_HOP_BY_HOP.contains(&name.as_str())
572 });
573 response
574}
575
576fn bad_gateway() -> Response {
577 let cr = Range::get_content_range(
578 b"502 Bad Gateway".to_vec(),
579 MimeType::TEXT_PLAIN.to_string(),
580 );
581 let mut r = Response::new();
582 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
583 r.reason_phrase = STATUS_CODE_REASON_PHRASE
584 .n502_bad_gateway
585 .reason_phrase
586 .to_string();
587 r.content_range_list = vec![cr];
588 r
589}
590
591const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
599 prefix: Vec<u8>,
600 prefix_pos: usize,
601 inner: R,
602}
603
604impl<R: Read + Send> ConcatReader<R> {
605 fn new(prefix: Vec<u8>, inner: R) -> Self {
606 ConcatReader { prefix, prefix_pos: 0, inner }
607 }
608}
609
610impl<R: Read + Send> Read for ConcatReader<R> {
611 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
612 if self.prefix_pos < self.prefix.len() {
613 let avail = &self.prefix[self.prefix_pos..];
614 let n = buf.len().min(avail.len());
615 buf[..n].copy_from_slice(&avail[..n]);
616 self.prefix_pos += n;
617 return Ok(n);
618 }
619 self.inner.read(buf)
620 }
621}
622
623fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
628 let mut buf: Vec<u8> = Vec::with_capacity(4096);
629 loop {
630 let n = stream.read(tmp).map_err(|e| e.to_string())?;
631 if n == 0 {
632 return Err("backend closed connection before headers were complete".to_string());
633 }
634 buf.extend_from_slice(&tmp[..n]);
635 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
636 let body_prefix = buf[pos + 4..].to_vec();
637 buf.truncate(pos + 4);
638 return Ok((buf, body_prefix));
639 }
640 }
641}
642
643pub(crate) fn should_stream_response(header_lower: &str) -> bool {
650 let is_sse = header_lower.lines().any(|l| {
651 l.starts_with("content-type:") && l.contains("text/event-stream")
652 });
653 let is_chunked = header_lower.lines().any(|l| {
654 l.starts_with("transfer-encoding:") && l.contains("chunked")
655 });
656 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
657 l.strip_prefix("content-length:")?.trim().parse().ok()
658 });
659 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
660 is_sse || is_chunked || is_large
661}
662
663fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
665 let s = std::str::from_utf8(header_bytes)
666 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
667 let mut lines = s.lines();
668 let status_line = lines.next().ok_or("empty backend response")?;
669 let mut parts = status_line.splitn(3, ' ');
670 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
671 let status_code: i16 = parts
672 .next()
673 .unwrap_or("502")
674 .parse()
675 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
676 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
677 let mut headers = Vec::new();
678 for line in lines {
679 let line = line.trim_end_matches('\r');
680 if line.is_empty() { break; }
681 if let Some(colon) = line.find(':') {
682 headers.push(crate::header::Header {
683 name: line[..colon].trim().to_string(),
684 value: line[colon + 1..].trim().to_string(),
685 });
686 }
687 }
688 Ok(Response {
689 http_version,
690 status_code,
691 reason_phrase,
692 headers,
693 content_range_list: vec![],
694 stream_file: None,
695 stream_pipe: None,
696 })
697}
698
699fn read_response_from_partial(
705 stream: &mut TcpStream,
706 header_bytes: Vec<u8>,
707 body_prefix: Vec<u8>,
708 tmp: &mut [u8],
709) -> Result<(Vec<u8>, bool), String> {
710 let header_end = header_bytes.len();
711 let mut buf = header_bytes;
712 buf.extend_from_slice(&body_prefix);
713
714 let header_lower =
715 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
716 let connection_close =
717 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
718 let is_chunked = header_lower
719 .lines()
720 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
721 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
722 l.strip_prefix("content-length:")?.trim().parse().ok()
723 });
724
725 if is_chunked {
726 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
727 rewrite_as_content_length(&mut buf, header_end, &decoded);
728 Ok((buf, !connection_close))
729 } else if let Some(len) = content_length {
730 while buf.len() < header_end + len {
731 let n = stream.read(tmp).map_err(|e| e.to_string())?;
732 if n == 0 { break; }
733 buf.extend_from_slice(&tmp[..n]);
734 }
735 Ok((buf, !connection_close))
736 } else {
737 loop {
738 let n = stream.read(tmp).map_err(|e| e.to_string())?;
739 if n == 0 { break; }
740 buf.extend_from_slice(&tmp[..n]);
741 }
742 Ok((buf, false))
743 }
744}
745
746struct Backend {
749 host: String,
750 port: u16,
751 #[cfg_attr(not(feature = "http2"), allow(dead_code))]
754 tls: bool,
755}
756
757impl Backend {
758 fn parse(url: &str) -> Option<Self> {
759 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
760 (r, true, 443u16)
761 } else if let Some(r) = url.strip_prefix("h2s://") {
762 (r, true, 443u16)
763 } else if let Some(r) = url.strip_prefix("grpcs://") {
764 (r, true, 443u16)
765 } else if let Some(r) = url.strip_prefix("http://") {
766 (r, false, 80u16)
767 } else if let Some(r) = url.strip_prefix("h2://") {
768 (r, false, 80u16)
769 } else if let Some(r) = url.strip_prefix("grpc://") {
770 (r, false, 80u16)
771 } else {
772 (url, false, 80u16)
773 };
774 let host_port = rest.split('/').next().unwrap_or(rest);
776 let (host, port) = if let Some(colon) = host_port.rfind(':') {
777 let port_str = &host_port[colon + 1..];
778 if let Ok(p) = port_str.parse::<u16>() {
779 (host_port[..colon].to_string(), p)
780 } else {
781 (host_port.to_string(), default_port)
782 }
783 } else {
784 (host_port.to_string(), default_port)
785 };
786 if host.is_empty() {
787 return None;
788 }
789 Some(Backend { host, port, tls })
790 }
791}
792
793#[cfg(feature = "http2")]
804pub struct H2ReverseProxy {
805 inner: ReverseProxy,
806}
807
808#[cfg(feature = "http2")]
809impl H2ReverseProxy {
810 pub fn new<I, S>(backends: I) -> Self
821 where
822 I: IntoIterator<Item = S>,
823 S: AsRef<str>,
824 {
825 H2ReverseProxy {
826 inner: ReverseProxy::new(backends),
827 }
828 }
829
830 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
832 self.inner = self.inner.path_prefix(prefix);
833 self
834 }
835
836 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
838 self.inner = self.inner.connect_timeout_ms(ms);
839 self
840 }
841
842 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
844 self.inner = self.inner.read_timeout_ms(ms);
845 self
846 }
847}
848
849#[cfg(feature = "http2")]
850impl crate::middleware::Middleware for H2ReverseProxy {
851 fn handle(
852 &self,
853 request: &crate::request::Request,
854 connection: &crate::server::ConnectionInfo,
855 next: &dyn crate::application::Application,
856 ) -> Result<crate::response::Response, String> {
857 if let Some(prefix) = &self.inner.path_prefix {
858 if !request.request_uri.starts_with(prefix.as_str()) {
859 return next.execute(request, connection);
860 }
861 }
862 if self.inner.backends.is_empty() {
863 return Ok(bad_gateway());
864 }
865 let n = self.inner.backends.len();
866 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
867 for attempt in 0..n {
868 let idx = (start + attempt) % n;
869 match try_backend_h2(request, &connection.client.ip, &self.inner.backends[idx],
870 self.inner.connect_timeout, self.inner.read_timeout) {
871 Ok(resp) => return Ok(resp),
872 Err(_) if attempt + 1 < n => continue,
873 Err(_) => break,
874 }
875 }
876 Ok(bad_gateway())
877 }
878}
879
880#[cfg(feature = "http2")]
881fn try_backend_h2(
882 request: &Request,
883 client_ip: &str,
884 backend: &Backend,
885 connect_timeout: Duration,
886 _read_timeout: Duration,
887) -> Result<Response, String> {
888 crate::async_bridge::block_on_isolated(|| forward_h2_async(request, client_ip, backend, connect_timeout))
894}
895
896#[cfg(feature = "http2")]
897async fn forward_h2_async(
898 request: &Request,
899 client_ip: &str,
900 backend: &Backend,
901 connect_timeout: Duration,
902) -> Result<Response, String> {
903 let addr = format!("{}:{}", backend.host, backend.port);
904 let tcp = tokio::time::timeout(
905 connect_timeout,
906 tokio::net::TcpStream::connect(&addr),
907 )
908 .await
909 .map_err(|_| format!("h2 proxy: connect to {} timed out", addr))?
910 .map_err(|e| format!("h2 proxy: connect to {} failed: {}", addr, e))?;
911
912 if backend.tls {
913 use rustls::pki_types::ServerName;
914 use rustls::ClientConfig;
915 use std::sync::Arc;
916 use tokio_rustls::TlsConnector;
917
918 let root_store = rustls::RootCertStore::from_iter(
919 webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
920 );
921 let mut config = ClientConfig::builder()
922 .with_root_certificates(root_store)
923 .with_no_client_auth();
924 config.alpn_protocols = vec![b"h2".to_vec()];
926 let connector = TlsConnector::from(Arc::new(config));
927 let server_name = ServerName::try_from(backend.host.as_str())
928 .map_err(|e| format!("invalid upstream hostname '{}': {}", backend.host, e))?
929 .to_owned();
930 let tls_stream = connector
931 .connect(server_name, tcp)
932 .await
933 .map_err(|e| format!("h2 proxy: TLS handshake with {} failed: {}", addr, e))?;
934 send_h2_request(request, client_ip, backend, tls_stream).await
935 } else {
936 send_h2_request(request, client_ip, backend, tcp).await
937 }
938}
939
940#[cfg(feature = "http2")]
945async fn send_h2_request<T>(
946 request: &Request,
947 client_ip: &str,
948 backend: &Backend,
949 stream: T,
950) -> Result<Response, String>
951where
952 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
953{
954 use bytes::Bytes;
955 use http as hc;
956
957 let addr = format!("{}:{}", backend.host, backend.port);
958
959 let (send_req, conn) = h2::client::handshake(stream)
960 .await
961 .map_err(|e| format!("h2 proxy: handshake with {} failed: {}", addr, e))?;
962
963 tokio::spawn(async move {
964 let _ = conn.await;
965 });
966
967 let scheme = if backend.tls { "https" } else { "http" };
968 let uri_str = format!("{}://{}{}", scheme, addr, request.request_uri);
969 let uri: hc::Uri = uri_str.parse().map_err(|e: hc::uri::InvalidUri| e.to_string())?;
970 let method = hc::Method::from_bytes(request.method.as_bytes()).map_err(|e| e.to_string())?;
971
972 let mut builder = hc::Request::builder().method(method).uri(uri);
973 builder = builder.header("host", &backend.host);
974 for h in &request.headers {
975 let lower = h.name.to_lowercase();
976 if HOP_BY_HOP.contains(&lower.as_str()) || lower == "host" {
977 continue;
978 }
979 builder = builder.header(&h.name, &h.value);
980 }
981 builder = builder.header("x-forwarded-for", client_ip);
982 builder = builder.header("via", "2 rws");
983
984 let body_bytes = Bytes::from(request.body.clone());
985 let end_of_stream = body_bytes.is_empty();
986 let http_req = builder.body(()).map_err(|e| e.to_string())?;
987
988 let mut send_req = send_req.ready().await.map_err(|e| e.to_string())?;
989 let (resp_future, mut req_body) = send_req
990 .send_request(http_req, end_of_stream)
991 .map_err(|e| e.to_string())?;
992 if !end_of_stream {
993 req_body.send_data(body_bytes, true).map_err(|e| e.to_string())?;
994 }
995
996 let resp = resp_future.await.map_err(|e| e.to_string())?;
997 let (parts, mut body) = resp.into_parts();
998
999 let content_type = parts
1000 .headers
1001 .get("content-type")
1002 .and_then(|v| v.to_str().ok())
1003 .unwrap_or("application/octet-stream")
1004 .to_string();
1005
1006 let mut body_bytes: Vec<u8> = Vec::new();
1007 while let Some(chunk) = body.data().await {
1008 body_bytes.extend_from_slice(&chunk.map_err(|e| e.to_string())?);
1009 }
1010
1011 let mut response = Response::new();
1012 response.status_code = parts.status.as_u16() as i16;
1013 response.reason_phrase = parts.status.canonical_reason().unwrap_or("").to_string();
1014
1015 const H2_HOP: &[&str] = &[
1016 "connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-connection", "te",
1017 ];
1018 for (name, value) in &parts.headers {
1019 let lower = name.as_str().to_lowercase();
1020 if H2_HOP.contains(&lower.as_str()) {
1021 continue;
1022 }
1023 if let Ok(v) = value.to_str() {
1024 response.headers.push(crate::header::Header {
1025 name: name.as_str().to_string(),
1026 value: v.to_string(),
1027 });
1028 }
1029 }
1030
1031 if !body_bytes.is_empty() {
1032 response.content_range_list = vec![Range::get_content_range(body_bytes, content_type)];
1033 }
1034
1035 Ok(normalize_upstream_response(response))
1036}
1037
1038#[cfg(feature = "http2")]
1058pub struct GrpcProxy {
1059 inner: H2ReverseProxy,
1060}
1061
1062#[cfg(feature = "http2")]
1063impl GrpcProxy {
1064 pub fn new<I, S>(backends: I) -> Self
1074 where
1075 I: IntoIterator<Item = S>,
1076 S: AsRef<str>,
1077 {
1078 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1079 }
1080
1081 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1083 self.inner = self.inner.path_prefix(prefix);
1084 self
1085 }
1086}
1087
1088#[cfg(feature = "http2")]
1089impl crate::middleware::Middleware for GrpcProxy {
1090 fn handle(
1091 &self,
1092 request: &crate::request::Request,
1093 connection: &crate::server::ConnectionInfo,
1094 next: &dyn crate::application::Application,
1095 ) -> Result<crate::response::Response, String> {
1096 let ct = request
1097 .get_header("content-type".to_string())
1098 .map(|h| h.value.as_str())
1099 .unwrap_or("");
1100 if ct.starts_with("application/grpc") {
1101 self.inner.handle(request, connection, next)
1102 } else {
1103 next.execute(request, connection)
1104 }
1105 }
1106}
1107
1108#[cfg(test)]
1111mod backend_parse_tests {
1112 use super::Backend;
1113
1114 fn parse(url: &str) -> Option<(String, u16, bool)> {
1115 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1116 }
1117
1118 #[test]
1119 fn bare_host_port() {
1120 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1121 }
1122
1123 #[test]
1124 fn http_scheme() {
1125 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1126 }
1127
1128 #[test]
1129 fn h2_scheme_plain() {
1130 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1131 }
1132
1133 #[test]
1134 fn grpc_scheme_plain() {
1135 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1136 }
1137
1138 #[test]
1139 fn https_scheme_sets_tls_and_default_port() {
1140 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1141 }
1142
1143 #[test]
1144 fn https_scheme_explicit_port() {
1145 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1146 }
1147
1148 #[test]
1149 fn h2s_scheme_sets_tls() {
1150 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1151 }
1152
1153 #[test]
1154 fn h2s_scheme_explicit_port() {
1155 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1156 }
1157
1158 #[test]
1159 fn grpcs_scheme_sets_tls() {
1160 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1161 }
1162
1163 #[test]
1164 fn grpcs_scheme_explicit_port() {
1165 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1166 }
1167
1168 #[test]
1169 fn empty_host_returns_none() {
1170 assert_eq!(None, parse("https://"));
1171 }
1172
1173 #[test]
1174 fn bare_host_no_port_defaults_to_80() {
1175 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1176 }
1177}