1pub mod pool;
27#[cfg(feature = "http2")]
28pub(crate) mod async_upstream;
29
30#[cfg(test)]
31mod tests;
32
33use std::io::{Read, Write};
34use std::net::{TcpStream, ToSocketAddrs};
35use std::sync::atomic::{AtomicUsize, Ordering};
36use std::sync::Arc;
37use std::time::Duration;
38
39pub use pool::{ConnPool, Pool};
40
41use crate::application::Application;
42use crate::core::New;
43use crate::middleware::Middleware;
44use crate::mime_type::MimeType;
45use crate::range::Range;
46use crate::request::Request;
47use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
48use crate::server::ConnectionInfo;
49
50const HOP_BY_HOP: &[&str] = &[
52 "connection",
53 "keep-alive",
54 "proxy-authenticate",
55 "proxy-authorization",
56 "te",
57 "trailers",
58 "transfer-encoding",
59 "upgrade",
60];
61
62pub enum LoadBalancing {
64 RoundRobin,
66}
67
68pub struct ReverseProxy {
83 backends: Vec<Backend>,
84 path_prefix: Option<String>,
85 connect_timeout: Duration,
86 read_timeout: Duration,
87 counter: AtomicUsize,
88 pool: Arc<ConnPool>,
89 breaker: Option<Arc<dyn crate::circuit_breaker::Breaker>>,
90}
91
92impl ReverseProxy {
93 pub fn new<I, S>(backends: I) -> Self
97 where
98 I: IntoIterator<Item = S>,
99 S: AsRef<str>,
100 {
101 Self {
102 backends: backends
103 .into_iter()
104 .filter_map(|u| Backend::parse(u.as_ref()))
105 .collect(),
106 path_prefix: None,
107 connect_timeout: Duration::from_secs(5),
108 read_timeout: Duration::from_secs(30),
109 counter: AtomicUsize::new(0),
110 pool: Arc::new(ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60))),
111 breaker: None,
112 }
113 }
114
115 pub fn with_circuit_breaker(mut self, breaker: Arc<dyn crate::circuit_breaker::Breaker>) -> Self {
133 self.breaker = Some(breaker);
134 self
135 }
136
137 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
142 self.path_prefix = Some(prefix.into());
143 self
144 }
145
146 pub fn strategy(self, _strategy: LoadBalancing) -> Self {
148 self
149 }
150
151 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
153 self.connect_timeout = Duration::from_millis(ms);
154 self
155 }
156
157 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
159 self.read_timeout = Duration::from_millis(ms);
160 self
161 }
162
163 pub fn with_pool(mut self, pool: Arc<ConnPool>) -> Self {
168 self.pool = pool;
169 self
170 }
171
172 pub fn max_idle_conns(mut self, n: usize) -> Self {
174 self.pool = Arc::new(ConnPool::new(n, Duration::from_secs(60)));
175 self
176 }
177
178 fn proxy(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
179 if self.backends.is_empty() {
180 return Err("no backends configured".to_string());
181 }
182 let n = self.backends.len();
183 let start = self.counter.fetch_add(1, Ordering::Relaxed);
184 let mut last_err = "all backends failed".to_string();
185 for attempt in 0..n {
186 let idx = (start + attempt) % n;
187 let backend = &self.backends[idx];
188 let key = format!("{}:{}", backend.host, backend.port);
189
190 if let Some(breaker) = &self.breaker {
191 if !breaker.is_available(&key) {
192 last_err = format!("circuit open for {}", key);
193 continue;
194 }
195 }
196
197 match self.try_backend(request, connection, backend) {
198 Ok(resp) => {
199 if let Some(breaker) = &self.breaker {
200 breaker.record_success(&key);
201 }
202 return Ok(resp);
203 }
204 Err(e) => {
205 if let Some(breaker) = &self.breaker {
206 breaker.record_failure(&key);
207 }
208 last_err = e;
209 }
210 }
211 }
212 Err(last_err)
213 }
214
215 fn try_backend(
216 &self,
217 request: &Request,
218 connection: &ConnectionInfo,
219 backend: &Backend,
220 ) -> Result<Response, String> {
221 forward_upstream(
222 &self.pool,
223 request,
224 &connection.client.ip,
225 &backend.host,
226 backend.port,
227 backend.tls,
228 self.connect_timeout,
229 self.read_timeout,
230 )
231 }
232}
233
234impl Middleware for ReverseProxy {
235 fn handle(
236 &self,
237 request: &Request,
238 connection: &ConnectionInfo,
239 next: &dyn Application,
240 ) -> Result<Response, String> {
241 if let Some(prefix) = &self.path_prefix {
242 if !request.request_uri.starts_with(prefix.as_str()) {
243 return next.execute(request, connection);
244 }
245 }
246 match self.proxy(request, connection) {
247 Ok(resp) => Ok(resp),
248 Err(_) => Ok(bad_gateway()),
249 }
250 }
251}
252
253pub(crate) fn build_request(
256 request: &Request,
257 backend_host: &str,
258 client_ip: &str,
259 keep_alive: bool,
260) -> Vec<u8> {
261 let mut out: Vec<u8> = Vec::new();
262 let _ = write!(
263 out,
264 "{} {} HTTP/1.1\r\nHost: {}\r\n",
265 request.method, request.request_uri, backend_host
266 );
267 for h in &request.headers {
268 let lower = h.name.to_lowercase();
269 if lower.is_empty() || HOP_BY_HOP.contains(&lower.as_str()) || lower == "host" {
271 continue;
272 }
273 let _ = write!(out, "{}: {}\r\n", h.name, h.value);
274 }
275 let _ = write!(out, "X-Forwarded-For: {}\r\n", client_ip);
276 let _ = write!(out, "Via: 1.1 rws\r\n");
277 if keep_alive {
278 let _ = write!(out, "Connection: keep-alive\r\n");
279 } else {
280 let _ = write!(out, "Connection: close\r\n");
281 }
282 if !request.body.is_empty() {
283 let _ = write!(out, "Content-Length: {}\r\n", request.body.len());
284 }
285 let _ = write!(out, "\r\n");
286 out.extend_from_slice(&request.body);
287 out
288}
289
290fn decode_chunked<S: Read>(
295 stream: &mut S,
296 buf: &[u8],
297 header_end: usize,
298 tmp: &mut [u8],
299) -> Result<Vec<u8>, String> {
300 let mut raw: Vec<u8> = buf[header_end..].to_vec();
302 let mut decoded: Vec<u8> = Vec::new();
303
304 loop {
305 let crlf = loop {
307 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
308 break p;
309 }
310 let n = stream.read(tmp).map_err(|e| e.to_string())?;
311 if n == 0 {
312 return Err("chunked: premature EOF reading chunk size".to_string());
313 }
314 raw.extend_from_slice(&tmp[..n]);
315 };
316
317 let size_line = std::str::from_utf8(&raw[..crlf])
319 .map_err(|_| "chunked: non-UTF-8 chunk size line".to_string())?;
320 let size_str = size_line.split(';').next().unwrap_or("").trim();
321 let chunk_size = usize::from_str_radix(size_str, 16)
322 .map_err(|_| format!("chunked: invalid chunk size '{}'", size_str))?;
323 raw.drain(..crlf + 2); if chunk_size == 0 {
326 loop {
330 let line_end = loop {
331 if let Some(p) = raw.windows(2).position(|w| w == b"\r\n") {
332 break p;
333 }
334 let n = stream.read(tmp).map_err(|e| e.to_string())?;
335 if n == 0 {
336 return Err("chunked: premature EOF in trailer".to_string());
337 }
338 raw.extend_from_slice(&tmp[..n]);
339 };
340 raw.drain(..line_end + 2);
341 if line_end == 0 {
342 break;
343 }
344 }
345 break;
346 }
347
348 while raw.len() < chunk_size + 2 {
350 let n = stream.read(tmp).map_err(|e| e.to_string())?;
351 if n == 0 {
352 return Err("chunked: premature EOF reading chunk body".to_string());
353 }
354 raw.extend_from_slice(&tmp[..n]);
355 }
356 decoded.extend_from_slice(&raw[..chunk_size]);
357 raw.drain(..chunk_size + 2); }
359
360 Ok(decoded)
361}
362
363fn rewrite_as_content_length(buf: &mut Vec<u8>, header_end: usize, decoded: &[u8]) {
366 let header_str = std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_string();
367 buf.clear();
368 for line in header_str.lines() {
369 if line.to_ascii_lowercase().starts_with("transfer-encoding:") || line.is_empty() {
370 continue;
371 }
372 buf.extend_from_slice(line.as_bytes());
373 buf.extend_from_slice(b"\r\n");
374 }
375 let _ = write!(buf, "Content-Length: {}\r\n\r\n", decoded.len());
376 buf.extend_from_slice(decoded);
377}
378
379const REGENERATED_OR_HOP_BY_HOP: &[&str] = &[
382 "content-length", "transfer-encoding", "connection", "keep-alive",
383 "proxy-connection", "te", "trailer", "upgrade",
384];
385
386pub(crate) fn normalize_upstream_response(mut response: Response) -> Response {
395 response.http_version = crate::http::VERSION.http_1_1.to_string();
396 let content_type = response
397 .headers
398 .iter()
399 .find(|h| h.name.eq_ignore_ascii_case(crate::header::Header::_CONTENT_TYPE))
400 .map(|h| h.value.clone());
401 let has_body = !response.content_range_list.is_empty();
402 if let (Some(content_type), [range]) = (&content_type, response.content_range_list.as_mut_slice()) {
403 range.content_type = content_type.clone();
404 }
405 response.headers.retain(|h| {
406 let name = h.name.to_ascii_lowercase();
407 if name == "content-type" {
408 return !has_body;
410 }
411 !REGENERATED_OR_HOP_BY_HOP.contains(&name.as_str())
412 });
413 response
414}
415
416fn bad_gateway() -> Response {
417 let cr = Range::get_content_range(
418 b"502 Bad Gateway".to_vec(),
419 MimeType::TEXT_PLAIN.to_string(),
420 );
421 let mut r = Response::new();
422 r.status_code = *STATUS_CODE_REASON_PHRASE.n502_bad_gateway.status_code;
423 r.reason_phrase = STATUS_CODE_REASON_PHRASE
424 .n502_bad_gateway
425 .reason_phrase
426 .to_string();
427 r.content_range_list = vec![cr];
428 r
429}
430
431const STREAM_THRESHOLD: usize = 1024 * 1024; pub(crate) struct ConcatReader<R: Read + Send> {
439 prefix: Vec<u8>,
440 prefix_pos: usize,
441 inner: R,
442}
443
444impl<R: Read + Send> ConcatReader<R> {
445 fn new(prefix: Vec<u8>, inner: R) -> Self {
446 ConcatReader { prefix, prefix_pos: 0, inner }
447 }
448}
449
450impl<R: Read + Send> Read for ConcatReader<R> {
451 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
452 if self.prefix_pos < self.prefix.len() {
453 let avail = &self.prefix[self.prefix_pos..];
454 let n = buf.len().min(avail.len());
455 buf[..n].copy_from_slice(&avail[..n]);
456 self.prefix_pos += n;
457 return Ok(n);
458 }
459 self.inner.read(buf)
460 }
461}
462
463
464pub(crate) const POOL_MAX_IDLE: usize = 64;
468
469pub(crate) fn shared_pool() -> &'static ConnPool {
472 static POOL: std::sync::OnceLock<ConnPool> = std::sync::OnceLock::new();
473 POOL.get_or_init(|| ConnPool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
474}
475
476pub(crate) trait UpstreamIo: Read + Write + Send + 'static {
479 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()>;
480}
481
482impl UpstreamIo for TcpStream {
483 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
484 self.set_read_timeout(Some(read))?;
485 self.set_write_timeout(Some(write))
486 }
487}
488
489#[cfg(any(feature = "http-client", feature = "http2"))]
490pub(crate) type TlsStream = rustls::StreamOwned<rustls::ClientConnection, TcpStream>;
491
492#[cfg(any(feature = "http-client", feature = "http2"))]
493impl UpstreamIo for TlsStream {
494 fn set_io_timeouts(&self, read: Duration, write: Duration) -> std::io::Result<()> {
495 self.sock.set_io_timeouts(read, write)
496 }
497}
498
499pub(crate) fn forward_upstream(
505 plain_pool: &ConnPool,
506 request: &Request,
507 client_ip: &str,
508 host: &str,
509 port: u16,
510 tls: bool,
511 connect_timeout: Duration,
512 read_timeout: Duration,
513) -> Result<Response, String> {
514 if tls {
515 #[cfg(any(feature = "http-client", feature = "http2"))]
516 return forward_via(shared_tls_pool(), request, client_ip, host, port, read_timeout, || {
517 connect_tls(host, port, connect_timeout)
518 });
519 #[cfg(not(any(feature = "http-client", feature = "http2")))]
520 return Err("HTTPS backends need the http2 or http-client feature, which this build doesn't include".to_string());
521 }
522 forward_pooled(plain_pool, request, client_ip, host, port, connect_timeout, read_timeout)
523}
524
525pub(crate) fn forward_pooled(
529 pool: &ConnPool,
530 request: &Request,
531 client_ip: &str,
532 host: &str,
533 port: u16,
534 connect_timeout: Duration,
535 read_timeout: Duration,
536) -> Result<Response, String> {
537 forward_via(pool, request, client_ip, host, port, read_timeout, || connect_tcp(host, port, connect_timeout))
538}
539
540fn forward_via<S: UpstreamIo>(
547 pool: &Pool<S>,
548 request: &Request,
549 client_ip: &str,
550 host: &str,
551 port: u16,
552 read_timeout: Duration,
553 connect: impl Fn() -> Result<S, String>,
554) -> Result<Response, String> {
555 let key = format!("{}:{}", host, port);
556 if let Some(stream) = pool.acquire(&key) {
557 match exchange(pool, &key, stream, request, host, client_ip, read_timeout) {
558 Exchange::Done(response) => return Ok(response),
559 Exchange::NoResponse(_) if is_idempotent(&request.method) => {} Exchange::NoResponse(e) | Exchange::Failed(e) => return Err(e),
561 }
562 }
563 let stream = connect()?;
564 match exchange(pool, &key, stream, request, host, client_ip, read_timeout) {
565 Exchange::Done(response) => Ok(response),
566 Exchange::NoResponse(e) | Exchange::Failed(e) => Err(e),
567 }
568}
569
570fn connect_tcp(host: &str, port: u16, connect_timeout: Duration) -> Result<TcpStream, String> {
574 let key = format!("{}:{}", host, port);
575 let addrs: Vec<_> = key
576 .to_socket_addrs()
577 .map_err(|e| format!("DNS lookup for {} failed: {}", key, e))?
578 .collect();
579 let mut last_error = format!("no address resolved for {}", key);
580 for addr in addrs {
581 match TcpStream::connect_timeout(&addr, connect_timeout) {
582 Ok(stream) => {
583 let _ = stream.set_nodelay(true);
584 return Ok(stream);
585 }
586 Err(e) => last_error = format!("connect to {} ({}) failed: {}", key, addr, e),
587 }
588 }
589 Err(last_error)
590}
591
592#[cfg(any(feature = "http-client", feature = "http2"))]
594fn shared_tls_pool() -> &'static Pool<TlsStream> {
595 static POOL: std::sync::OnceLock<Pool<TlsStream>> = std::sync::OnceLock::new();
596 POOL.get_or_init(|| Pool::new(POOL_MAX_IDLE, Duration::from_secs(60)))
597}
598
599#[cfg(any(feature = "http-client", feature = "http2"))]
603fn tls_client_config() -> Arc<rustls::ClientConfig> {
604 static CONFIG: std::sync::OnceLock<Arc<rustls::ClientConfig>> = std::sync::OnceLock::new();
605 Arc::clone(CONFIG.get_or_init(|| {
606 let roots = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
607 Arc::new(crate::tls_client_builder().with_root_certificates(roots).with_no_client_auth())
608 }))
609}
610
611#[cfg(any(feature = "http-client", feature = "http2"))]
614fn connect_tls(host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
615 connect_tls_with(tls_client_config(), host, port, connect_timeout)
616}
617
618#[cfg(any(feature = "http-client", feature = "http2"))]
619fn connect_tls_with(config: Arc<rustls::ClientConfig>, host: &str, port: u16, connect_timeout: Duration) -> Result<TlsStream, String> {
620 let tcp = connect_tcp(host, port, connect_timeout)?;
621 let server_name = rustls::pki_types::ServerName::try_from(host.to_string())
622 .map_err(|e| format!("invalid upstream hostname '{}': {}", host, e))?;
623 let conn = rustls::ClientConnection::new(config, server_name).map_err(|e| e.to_string())?;
624 Ok(rustls::StreamOwned::new(conn, tcp))
625}
626
627fn is_idempotent(method: &str) -> bool {
628 matches!(method, "GET" | "HEAD" | "OPTIONS" | "TRACE" | "PUT" | "DELETE")
629}
630
631enum Exchange {
632 Done(Response),
633 NoResponse(String),
635 Failed(String),
636}
637
638fn exchange<S: UpstreamIo>(
641 pool: &Pool<S>,
642 key: &str,
643 mut stream: S,
644 request: &Request,
645 host: &str,
646 client_ip: &str,
647 read_timeout: Duration,
648) -> Exchange {
649 if let Err(e) = stream.set_io_timeouts(read_timeout, Duration::from_secs(10)) {
650 return Exchange::Failed(e.to_string());
651 }
652 let req_bytes = build_request(request, host, client_ip, true);
654 if let Err(e) = stream.write_all(&req_bytes) {
655 return Exchange::NoResponse(format!("write to backend failed: {}", e));
656 }
657
658 let mut tmp = [0u8; 4096];
659 let (header_bytes, body_prefix) = match read_final_response_head(&mut stream, &mut tmp) {
660 Ok(head) => head,
661 Err(HeadError::NothingReceived(e)) => return Exchange::NoResponse(e),
662 Err(HeadError::Partial(e)) => return Exchange::Failed(e),
663 };
664 let header_lower = std::str::from_utf8(&header_bytes).unwrap_or("").to_ascii_lowercase();
665
666 if response_has_no_body(&request.method, &header_lower) {
667 if !header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close")) {
668 pool.release(key, stream);
669 }
670 return match Response::parse(&header_bytes) {
671 Ok(response) => Exchange::Done(normalize_upstream_response(response)),
672 Err(e) => Exchange::Failed(e),
673 };
674 }
675
676 if should_stream_response(&header_lower) {
677 return match parse_status_and_headers(&header_bytes) {
680 Ok(mut response) => {
681 response.stream_pipe = Some(Box::new(ConcatReader::new(body_prefix, stream)));
682 Exchange::Done(response)
683 }
684 Err(e) => Exchange::Failed(e),
685 };
686 }
687 match read_response_from_partial(&mut stream, header_bytes, body_prefix, &mut tmp) {
689 Ok((resp_bytes, reusable)) => {
690 if reusable {
691 pool.release(key, stream);
692 }
693 match Response::parse(&resp_bytes) {
694 Ok(response) => Exchange::Done(normalize_upstream_response(response)),
695 Err(e) => Exchange::Failed(e),
696 }
697 }
698 Err(e) => Exchange::Failed(e),
699 }
700}
701
702#[cfg(test)]
707fn read_headers_only(stream: &mut TcpStream, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
708 read_response_head(stream, tmp).map_err(|e| match e {
709 HeadError::NothingReceived(msg) | HeadError::Partial(msg) => msg,
710 })
711}
712
713enum HeadError {
717 NothingReceived(String),
718 Partial(String),
719}
720
721fn read_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
722 read_response_head_from(stream, tmp, Vec::with_capacity(4096), false)
723}
724
725fn read_final_response_head<S: Read>(stream: &mut S, tmp: &mut [u8]) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
730 let (mut head, mut rest) = read_response_head(stream, tmp)?;
731 let mut interim = 0;
732 while is_interim_response(&head) {
733 interim += 1;
734 if interim > MAX_INTERIM_RESPONSES {
735 return Err(HeadError::Partial("too many interim (1xx) responses from the backend".to_string()));
736 }
737 (head, rest) = read_response_head_from(stream, tmp, rest, true)?;
738 }
739 Ok((head, rest))
740}
741
742fn read_response_head_from<S: Read>(
746 stream: &mut S,
747 tmp: &mut [u8],
748 mut buf: Vec<u8>,
749 received: bool,
750) -> Result<(Vec<u8>, Vec<u8>), HeadError> {
751 loop {
752 if let Some(pos) = buf.windows(4).position(|w| w == b"\r\n\r\n") {
753 let body_prefix = buf[pos + 4..].to_vec();
754 buf.truncate(pos + 4);
755 return Ok((buf, body_prefix));
756 }
757 let nothing = buf.is_empty() && !received;
758 let n = match stream.read(tmp) {
759 Ok(n) => n,
760 Err(e) if nothing => return Err(HeadError::NothingReceived(e.to_string())),
761 Err(e) => return Err(HeadError::Partial(e.to_string())),
762 };
763 if n == 0 {
764 let msg = "backend closed connection before headers were complete".to_string();
765 return Err(if nothing { HeadError::NothingReceived(msg) } else { HeadError::Partial(msg) });
766 }
767 buf.extend_from_slice(&tmp[..n]);
768 }
769}
770
771pub(crate) const MAX_INTERIM_RESPONSES: usize = 16;
773
774pub(crate) fn is_interim_response(head: &[u8]) -> bool {
777 let line_end = head.iter().position(|b| *b == b'\r' || *b == b'\n').unwrap_or(head.len());
778 let status = head[..line_end].split(|b| *b == b' ').nth(1).unwrap_or(&[]);
779 status.len() == 3 && status.iter().all(u8::is_ascii_digit) && status[0] == b'1' && status != b"101"
780}
781
782pub(crate) fn response_has_no_body(method: &str, header_lower: &str) -> bool {
786 let status = header_lower.split_whitespace().nth(1).unwrap_or("");
787 method.eq_ignore_ascii_case("HEAD") || status.starts_with('1') || status == "204" || status == "304"
788}
789
790pub(crate) fn should_stream_response(header_lower: &str) -> bool {
797 let is_sse = header_lower.lines().any(|l| {
798 l.starts_with("content-type:") && l.contains("text/event-stream")
799 });
800 let is_chunked = header_lower.lines().any(|l| {
801 l.starts_with("transfer-encoding:") && l.contains("chunked")
802 });
803 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
804 l.strip_prefix("content-length:")?.trim().parse().ok()
805 });
806 let is_large = content_length.map_or(false, |n| n > STREAM_THRESHOLD);
807 is_sse || is_chunked || is_large
808}
809
810fn parse_status_and_headers(header_bytes: &[u8]) -> Result<Response, String> {
812 let s = std::str::from_utf8(header_bytes)
813 .map_err(|e| format!("non-UTF-8 response headers: {}", e))?;
814 let mut lines = s.lines();
815 let status_line = lines.next().ok_or("empty backend response")?;
816 let mut parts = status_line.splitn(3, ' ');
817 let http_version = parts.next().unwrap_or("HTTP/1.1").to_string();
818 let status_code: i16 = parts
819 .next()
820 .unwrap_or("502")
821 .parse()
822 .map_err(|_| format!("invalid status code in '{}'", status_line))?;
823 let reason_phrase = parts.next().unwrap_or("").trim_end_matches('\r').to_string();
824 let mut headers = Vec::new();
825 for line in lines {
826 let line = line.trim_end_matches('\r');
827 if line.is_empty() { break; }
828 if let Some(colon) = line.find(':') {
829 headers.push(crate::header::Header {
830 name: line[..colon].trim().to_string(),
831 value: line[colon + 1..].trim().to_string(),
832 });
833 }
834 }
835 Ok(Response {
836 http_version,
837 status_code,
838 reason_phrase,
839 headers,
840 content_range_list: vec![],
841 stream_file: None,
842 stream_pipe: None,
843 })
844}
845
846fn read_response_from_partial<S: Read>(
852 stream: &mut S,
853 header_bytes: Vec<u8>,
854 body_prefix: Vec<u8>,
855 tmp: &mut [u8],
856) -> Result<(Vec<u8>, bool), String> {
857 let header_end = header_bytes.len();
858 let mut buf = header_bytes;
859 buf.extend_from_slice(&body_prefix);
860
861 let header_lower =
862 std::str::from_utf8(&buf[..header_end]).unwrap_or("").to_ascii_lowercase();
863 let connection_close =
864 header_lower.lines().any(|l| l.starts_with("connection:") && l.contains("close"));
865 let is_chunked = header_lower
866 .lines()
867 .any(|l| l.starts_with("transfer-encoding:") && l.contains("chunked"));
868 let content_length: Option<usize> = header_lower.lines().find_map(|l| {
869 l.strip_prefix("content-length:")?.trim().parse().ok()
870 });
871
872 if is_chunked {
873 let decoded = decode_chunked(stream, &buf, header_end, tmp)?;
874 rewrite_as_content_length(&mut buf, header_end, &decoded);
875 Ok((buf, !connection_close))
876 } else if let Some(len) = content_length {
877 while buf.len() < header_end + len {
878 let n = stream.read(tmp).map_err(|e| e.to_string())?;
879 if n == 0 { break; }
880 buf.extend_from_slice(&tmp[..n]);
881 }
882 Ok((buf, !connection_close))
883 } else {
884 loop {
885 let n = stream.read(tmp).map_err(|e| e.to_string())?;
886 if n == 0 { break; }
887 buf.extend_from_slice(&tmp[..n]);
888 }
889 Ok((buf, false))
890 }
891}
892
893struct Backend {
896 host: String,
897 port: u16,
898 tls: bool,
901}
902
903impl Backend {
904 fn parse(url: &str) -> Option<Self> {
905 let (rest, tls, default_port) = if let Some(r) = url.strip_prefix("https://") {
906 (r, true, 443u16)
907 } else if let Some(r) = url.strip_prefix("h2s://") {
908 (r, true, 443u16)
909 } else if let Some(r) = url.strip_prefix("grpcs://") {
910 (r, true, 443u16)
911 } else if let Some(r) = url.strip_prefix("http://") {
912 (r, false, 80u16)
913 } else if let Some(r) = url.strip_prefix("h2://") {
914 (r, false, 80u16)
915 } else if let Some(r) = url.strip_prefix("grpc://") {
916 (r, false, 80u16)
917 } else {
918 (url, false, 80u16)
919 };
920 let host_port = rest.split('/').next().unwrap_or(rest);
922 let (host, port) = if let Some(colon) = host_port.rfind(':') {
923 let port_str = &host_port[colon + 1..];
924 if let Ok(p) = port_str.parse::<u16>() {
925 (host_port[..colon].to_string(), p)
926 } else {
927 (host_port.to_string(), default_port)
928 }
929 } else {
930 (host_port.to_string(), default_port)
931 };
932 if host.is_empty() {
933 return None;
934 }
935 Some(Backend { host, port, tls })
936 }
937}
938
939#[cfg(feature = "http2")]
950pub struct H2ReverseProxy {
951 inner: ReverseProxy,
952}
953
954#[cfg(feature = "http2")]
955impl H2ReverseProxy {
956 pub fn new<I, S>(backends: I) -> Self
967 where
968 I: IntoIterator<Item = S>,
969 S: AsRef<str>,
970 {
971 H2ReverseProxy {
972 inner: ReverseProxy::new(backends),
973 }
974 }
975
976 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
978 self.inner = self.inner.path_prefix(prefix);
979 self
980 }
981
982 pub fn connect_timeout_ms(mut self, ms: u64) -> Self {
984 self.inner = self.inner.connect_timeout_ms(ms);
985 self
986 }
987
988 pub fn read_timeout_ms(mut self, ms: u64) -> Self {
990 self.inner = self.inner.read_timeout_ms(ms);
991 self
992 }
993}
994
995#[cfg(feature = "http2")]
996impl crate::middleware::Middleware for H2ReverseProxy {
997 fn handle(
998 &self,
999 request: &crate::request::Request,
1000 connection: &crate::server::ConnectionInfo,
1001 next: &dyn crate::application::Application,
1002 ) -> Result<crate::response::Response, String> {
1003 if let Some(prefix) = &self.inner.path_prefix {
1004 if !request.request_uri.starts_with(prefix.as_str()) {
1005 return next.execute(request, connection);
1006 }
1007 }
1008 if self.inner.backends.is_empty() {
1009 return Ok(bad_gateway());
1010 }
1011 let n = self.inner.backends.len();
1012 let start = self.inner.counter.fetch_add(1, Ordering::Relaxed);
1013 for attempt in 0..n {
1014 let idx = (start + attempt) % n;
1015 match try_backend_h2(request, &connection.client.ip, &self.inner.backends[idx],
1016 self.inner.connect_timeout, self.inner.read_timeout) {
1017 Ok(resp) => return Ok(resp),
1018 Err(_) if attempt + 1 < n => continue,
1019 Err(_) => break,
1020 }
1021 }
1022 Ok(bad_gateway())
1023 }
1024}
1025
1026#[cfg(feature = "http2")]
1027fn try_backend_h2(
1028 request: &Request,
1029 client_ip: &str,
1030 backend: &Backend,
1031 connect_timeout: Duration,
1032 _read_timeout: Duration,
1033) -> Result<Response, String> {
1034 crate::async_bridge::block_on_isolated(|| forward_h2_async(request, client_ip, backend, connect_timeout))
1040}
1041
1042#[cfg(feature = "http2")]
1043async fn forward_h2_async(
1044 request: &Request,
1045 client_ip: &str,
1046 backend: &Backend,
1047 connect_timeout: Duration,
1048) -> Result<Response, String> {
1049 let addr = format!("{}:{}", backend.host, backend.port);
1050 let tcp = tokio::time::timeout(
1051 connect_timeout,
1052 tokio::net::TcpStream::connect(&addr),
1053 )
1054 .await
1055 .map_err(|_| format!("h2 proxy: connect to {} timed out", addr))?
1056 .map_err(|e| format!("h2 proxy: connect to {} failed: {}", addr, e))?;
1057
1058 if backend.tls {
1059 use rustls::pki_types::ServerName;
1060 use std::sync::Arc;
1061 use tokio_rustls::TlsConnector;
1062
1063 let root_store = rustls::RootCertStore::from_iter(
1064 webpki_roots::TLS_SERVER_ROOTS.iter().cloned(),
1065 );
1066 let mut config = crate::tls_client_builder()
1067 .with_root_certificates(root_store)
1068 .with_no_client_auth();
1069 config.alpn_protocols = vec![b"h2".to_vec()];
1071 let connector = TlsConnector::from(Arc::new(config));
1072 let server_name = ServerName::try_from(backend.host.as_str())
1073 .map_err(|e| format!("invalid upstream hostname '{}': {}", backend.host, e))?
1074 .to_owned();
1075 let tls_stream = connector
1076 .connect(server_name, tcp)
1077 .await
1078 .map_err(|e| format!("h2 proxy: TLS handshake with {} failed: {}", addr, e))?;
1079 send_h2_request(request, client_ip, backend, tls_stream).await
1080 } else {
1081 send_h2_request(request, client_ip, backend, tcp).await
1082 }
1083}
1084
1085#[cfg(feature = "http2")]
1090async fn send_h2_request<T>(
1091 request: &Request,
1092 client_ip: &str,
1093 backend: &Backend,
1094 stream: T,
1095) -> Result<Response, String>
1096where
1097 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
1098{
1099 use bytes::Bytes;
1100 use http as hc;
1101
1102 let addr = format!("{}:{}", backend.host, backend.port);
1103
1104 let (send_req, conn) = h2::client::handshake(stream)
1105 .await
1106 .map_err(|e| format!("h2 proxy: handshake with {} failed: {}", addr, e))?;
1107
1108 tokio::spawn(async move {
1109 let _ = conn.await;
1110 });
1111
1112 let scheme = if backend.tls { "https" } else { "http" };
1113 let uri_str = format!("{}://{}{}", scheme, addr, request.request_uri);
1114 let uri: hc::Uri = uri_str.parse().map_err(|e: hc::uri::InvalidUri| e.to_string())?;
1115 let method = hc::Method::from_bytes(request.method.as_bytes()).map_err(|e| e.to_string())?;
1116
1117 let mut builder = hc::Request::builder().method(method).uri(uri);
1118 builder = builder.header("host", &backend.host);
1119 for h in &request.headers {
1120 let lower = h.name.to_lowercase();
1121 if lower.is_empty() || HOP_BY_HOP.contains(&lower.as_str()) || lower == "host" {
1123 continue;
1124 }
1125 builder = builder.header(&h.name, &h.value);
1126 }
1127 builder = builder.header("x-forwarded-for", client_ip);
1128 builder = builder.header("via", "2 rws");
1129
1130 let body_bytes = Bytes::from(request.body.clone());
1131 let end_of_stream = body_bytes.is_empty();
1132 let http_req = builder.body(()).map_err(|e| e.to_string())?;
1133
1134 let mut send_req = send_req.ready().await.map_err(|e| e.to_string())?;
1135 let (resp_future, mut req_body) = send_req
1136 .send_request(http_req, end_of_stream)
1137 .map_err(|e| e.to_string())?;
1138 if !end_of_stream {
1139 req_body.send_data(body_bytes, true).map_err(|e| e.to_string())?;
1140 }
1141
1142 let resp = resp_future.await.map_err(|e| e.to_string())?;
1143 let (parts, mut body) = resp.into_parts();
1144
1145 let content_type = parts
1146 .headers
1147 .get("content-type")
1148 .and_then(|v| v.to_str().ok())
1149 .unwrap_or("application/octet-stream")
1150 .to_string();
1151
1152 let mut body_bytes: Vec<u8> = Vec::new();
1153 while let Some(chunk) = body.data().await {
1154 body_bytes.extend_from_slice(&chunk.map_err(|e| e.to_string())?);
1155 }
1156
1157 let mut response = Response::new();
1158 response.status_code = parts.status.as_u16() as i16;
1159 response.reason_phrase = parts.status.canonical_reason().unwrap_or("").to_string();
1160
1161 const H2_HOP: &[&str] = &[
1162 "connection", "keep-alive", "transfer-encoding", "upgrade", "proxy-connection", "te",
1163 ];
1164 for (name, value) in &parts.headers {
1165 let lower = name.as_str().to_lowercase();
1166 if H2_HOP.contains(&lower.as_str()) {
1167 continue;
1168 }
1169 if let Ok(v) = value.to_str() {
1170 response.headers.push(crate::header::Header {
1171 name: name.as_str().to_string(),
1172 value: v.to_string(),
1173 });
1174 }
1175 }
1176
1177 if !body_bytes.is_empty() {
1178 response.content_range_list = vec![Range::get_content_range(body_bytes, content_type)];
1179 }
1180
1181 Ok(normalize_upstream_response(response))
1182}
1183
1184#[cfg(feature = "http2")]
1204pub struct GrpcProxy {
1205 inner: H2ReverseProxy,
1206}
1207
1208#[cfg(feature = "http2")]
1209impl GrpcProxy {
1210 pub fn new<I, S>(backends: I) -> Self
1220 where
1221 I: IntoIterator<Item = S>,
1222 S: AsRef<str>,
1223 {
1224 GrpcProxy { inner: H2ReverseProxy::new(backends) }
1225 }
1226
1227 pub fn path_prefix(mut self, prefix: impl Into<String>) -> Self {
1229 self.inner = self.inner.path_prefix(prefix);
1230 self
1231 }
1232}
1233
1234#[cfg(feature = "http2")]
1235impl crate::middleware::Middleware for GrpcProxy {
1236 fn handle(
1237 &self,
1238 request: &crate::request::Request,
1239 connection: &crate::server::ConnectionInfo,
1240 next: &dyn crate::application::Application,
1241 ) -> Result<crate::response::Response, String> {
1242 let ct = request
1243 .get_header("content-type".to_string())
1244 .map(|h| h.value.as_str())
1245 .unwrap_or("");
1246 if ct.starts_with("application/grpc") {
1247 self.inner.handle(request, connection, next)
1248 } else {
1249 next.execute(request, connection)
1250 }
1251 }
1252}
1253
1254#[cfg(test)]
1257mod backend_parse_tests {
1258 use super::Backend;
1259
1260 fn parse(url: &str) -> Option<(String, u16, bool)> {
1261 Backend::parse(url).map(|b| (b.host, b.port, b.tls))
1262 }
1263
1264 #[test]
1265 fn bare_host_port() {
1266 assert_eq!(Some(("api.example.com".into(), 8080, false)), parse("api.example.com:8080"));
1267 }
1268
1269 #[test]
1270 fn http_scheme() {
1271 assert_eq!(Some(("backend".into(), 3000, false)), parse("http://backend:3000"));
1272 }
1273
1274 #[test]
1275 fn h2_scheme_plain() {
1276 assert_eq!(Some(("svc".into(), 50051, false)), parse("h2://svc:50051"));
1277 }
1278
1279 #[test]
1280 fn grpc_scheme_plain() {
1281 assert_eq!(Some(("svc".into(), 50051, false)), parse("grpc://svc:50051"));
1282 }
1283
1284 #[test]
1285 fn https_scheme_sets_tls_and_default_port() {
1286 assert_eq!(Some(("api.example.com".into(), 443, true)), parse("https://api.example.com"));
1287 }
1288
1289 #[test]
1290 fn https_scheme_explicit_port() {
1291 assert_eq!(Some(("api.example.com".into(), 8443, true)), parse("https://api.example.com:8443"));
1292 }
1293
1294 #[test]
1295 fn h2s_scheme_sets_tls() {
1296 assert_eq!(Some(("svc".into(), 443, true)), parse("h2s://svc"));
1297 }
1298
1299 #[test]
1300 fn h2s_scheme_explicit_port() {
1301 assert_eq!(Some(("svc".into(), 8443, true)), parse("h2s://svc:8443"));
1302 }
1303
1304 #[test]
1305 fn grpcs_scheme_sets_tls() {
1306 assert_eq!(Some(("grpc-svc".into(), 443, true)), parse("grpcs://grpc-svc"));
1307 }
1308
1309 #[test]
1310 fn grpcs_scheme_explicit_port() {
1311 assert_eq!(Some(("grpc-svc".into(), 50052, true)), parse("grpcs://grpc-svc:50052"));
1312 }
1313
1314 #[test]
1315 fn empty_host_returns_none() {
1316 assert_eq!(None, parse("https://"));
1317 }
1318
1319 #[test]
1320 fn bare_host_no_port_defaults_to_80() {
1321 assert_eq!(Some(("myhost".into(), 80, false)), parse("myhost"));
1322 }
1323}