1#![cfg_attr(test, allow(clippy::unwrap_used))]
2#![doc(html_favicon_url = "https://salvo.rs/favicon-32x32.png")]
39#![doc(html_logo_url = "https://salvo.rs/images/logo.svg")]
40#![cfg_attr(docsrs, feature(doc_cfg))]
41
42use std::convert::Infallible;
43use std::error::Error as StdError;
44use std::fmt::{self, Debug, Formatter};
45
46use hyper::upgrade::OnUpgrade;
47use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode};
48use salvo_core::http::header::{
49 AUTHORIZATION, CONNECTION, HOST, HeaderMap, HeaderName, HeaderValue, UPGRADE,
50};
51use salvo_core::http::uri::Uri;
52use salvo_core::http::{ReqBody, ResBody, StatusCode};
53use salvo_core::routing::normalize_url_path;
54use salvo_core::{
55 BoxedError, Depot, Error, FlowCtrl, Handler, Request, Response, async_trait, cfg_feature,
56};
57
58cfg_feature! {
59 #![feature = "hyper-client"]
60 mod hyper_client;
61 pub use hyper_client::*;
62}
63cfg_feature! {
64 #![feature = "reqwest-client"]
65 mod reqwest_client;
66 pub use reqwest_client::*;
67}
68
69cfg_feature! {
70 #![feature = "unix-sock-client"]
71 #[cfg(unix)]
72 mod unix_sock_client;
73 #[cfg(unix)]
74 pub use unix_sock_client::*;
75}
76
77type HyperRequest = hyper::Request<ReqBody>;
78type HyperResponse = hyper::Response<ResBody>;
79
80const X_FORWARDED_FOR_HEADER_NAME: &str = "x-forwarded-for";
81const HOP_BY_HOP_HEADERS: &[&str] = &[
82 "connection",
83 "keep-alive",
84 "proxy-authenticate",
85 "proxy-authorization",
86 "te",
87 "trailer",
88 "transfer-encoding",
89 "upgrade",
90];
91
92const QUERY_ENCODE_SET: &AsciiSet = &CONTROLS
93 .add(b' ')
94 .add(b'"')
95 .add(b'#')
96 .add(b'<')
97 .add(b'>')
98 .add(b'`');
99const PATH_ENCODE_SET: &AsciiSet = &QUERY_ENCODE_SET
100 .add(b'?')
101 .add(b'^')
102 .add(b'`')
103 .add(b'{')
104 .add(b'}');
105
106#[inline]
112pub(crate) fn encode_url_path(path: &str) -> String {
113 use std::fmt::Write as _;
114
115 let mut out = String::with_capacity(path.len());
116 let mut first = true;
117 for segment in path.split('/') {
118 if first {
119 first = false;
120 } else {
121 out.push('/');
122 }
123 let _ = write!(
126 &mut out,
127 "{}",
128 utf8_percent_encode(segment, PATH_ENCODE_SET)
129 );
130 }
131 out
132}
133
134pub trait Client: Send + Sync + 'static {
139 type Error: StdError + Send + Sync + 'static;
141
142 fn execute(
144 &self,
145 req: HyperRequest,
146 upgraded: Option<OnUpgrade>,
147 ) -> impl Future<Output = Result<HyperResponse, Self::Error>> + Send;
148}
149
150pub trait Upstreams: Send + Sync + 'static {
156 type Error: StdError + Send + Sync + 'static;
158
159 fn elect(
161 &self,
162 req: &Request,
163 depot: &Depot,
164 ) -> impl Future<Output = Result<&str, Self::Error>> + Send;
165}
166impl Upstreams for &'static str {
167 type Error = Infallible;
168
169 async fn elect(&self, _: &Request, _: &Depot) -> Result<&str, Self::Error> {
170 Ok(*self)
171 }
172}
173impl Upstreams for String {
174 type Error = Infallible;
175 async fn elect(&self, _: &Request, _: &Depot) -> Result<&str, Self::Error> {
176 Ok(self.as_str())
177 }
178}
179
180impl<const N: usize> Upstreams for [&'static str; N] {
181 type Error = Error;
182 async fn elect(&self, _: &Request, _: &Depot) -> Result<&str, Self::Error> {
183 if self.is_empty() {
184 return Err(Error::other("upstreams is empty"));
185 }
186 let index = fastrand::usize(..self.len());
187 Ok(self[index])
188 }
189}
190
191impl<T> Upstreams for Vec<T>
192where
193 T: AsRef<str> + Send + Sync + 'static,
194{
195 type Error = Error;
196 async fn elect(&self, _: &Request, _: &Depot) -> Result<&str, Self::Error> {
197 if self.is_empty() {
198 return Err(Error::other("upstreams is empty"));
199 }
200 let index = fastrand::usize(..self.len());
201 Ok(self[index].as_ref())
202 }
203}
204
205pub type UrlPartGetter = Box<dyn Fn(&Request, &Depot) -> Option<String> + Send + Sync + 'static>;
207
208pub type HostHeaderGetter =
210 Box<dyn Fn(&Uri, &Request, &Depot) -> Option<String> + Send + Sync + 'static>;
211
212pub fn default_url_path_getter(req: &Request, _depot: &Depot) -> Option<String> {
217 req.params().tail().map(str::to_owned)
218}
219
220fn contains_ambiguous_path_escape(path: &str) -> bool {
221 let bytes = path.as_bytes();
222 let mut index = 0;
223 while index + 2 < bytes.len() {
224 if bytes[index] == b'%'
225 && let (Some(high), Some(low)) =
226 (hex_value(bytes[index + 1]), hex_value(bytes[index + 2]))
227 {
228 let decoded = high << 4 | low;
229 if matches!(decoded, b'.' | b'/' | b'\\' | b'%') {
230 return true;
231 }
232 index += 3;
233 continue;
234 }
235 index += 1;
236 }
237 false
238}
239
240fn contains_parent_dir_component(path: &str) -> bool {
241 path.split(['/', '\\']).any(|part| part == "..")
242}
243
244fn hex_value(byte: u8) -> Option<u8> {
245 match byte {
246 b'0'..=b'9' => Some(byte - b'0'),
247 b'a'..=b'f' => Some(byte - b'a' + 10),
248 b'A'..=b'F' => Some(byte - b'A' + 10),
249 _ => None,
250 }
251}
252pub fn default_url_query_getter(req: &Request, _depot: &Depot) -> Option<String> {
254 req.uri().query().map(Into::into)
255}
256
257pub fn default_host_header_getter(
263 forward_uri: &Uri,
264 _req: &Request,
265 _depot: &Depot,
266) -> Option<String> {
267 if let Some(host) = forward_uri.host() {
268 return Some(String::from(host));
269 }
270
271 None
272}
273
274pub fn standard_host_header_getter(
280 forward_uri: &Uri,
281 req: &Request,
282 _depot: &Depot,
283) -> Option<String> {
284 let mut parts: Vec<String> = Vec::with_capacity(2);
285
286 if let Some(host) = forward_uri.host() {
287 parts.push(host.to_owned());
288
289 if let Some(scheme) = forward_uri.scheme_str()
290 && let Some(port) = forward_uri.port_u16()
291 && (scheme == "http" && port != 80 || scheme == "https" && port != 443)
292 {
293 parts.push(port.to_string());
294 }
295 }
296
297 if parts.is_empty() {
298 default_host_header_getter(forward_uri, req, _depot)
299 } else {
300 Some(parts.join(":"))
301 }
302}
303
304pub fn preserve_original_host_header_getter(
307 forward_uri: &Uri,
308 req: &Request,
309 _depot: &Depot,
310) -> Option<String> {
311 if let Some(host_header) = req.headers().get(HOST)
312 && let Ok(host) = host_header.to_str()
313 {
314 return Some(host.to_owned());
315 }
316
317 default_host_header_getter(forward_uri, req, _depot)
318}
319
320#[non_exhaustive]
322pub struct Proxy<U, C>
323where
324 U: Upstreams,
325 C: Client,
326{
327 pub upstreams: U,
329 pub client: C,
331 pub url_path_getter: UrlPartGetter,
333 pub url_query_getter: UrlPartGetter,
335 pub host_header_getter: HostHeaderGetter,
337 pub client_ip_forwarding_enabled: bool,
339 pub strict_path_normalization_enabled: bool,
341 pub strip_authorization_header_enabled: bool,
343}
344
345impl<U, C> Debug for Proxy<U, C>
346where
347 U: Upstreams,
348 C: Client,
349{
350 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
351 f.debug_struct("Proxy")
352 .field(
353 "client_ip_forwarding_enabled",
354 &self.client_ip_forwarding_enabled,
355 )
356 .field(
357 "strict_path_normalization_enabled",
358 &self.strict_path_normalization_enabled,
359 )
360 .field(
361 "strip_authorization_header_enabled",
362 &self.strip_authorization_header_enabled,
363 )
364 .finish_non_exhaustive()
365 }
366}
367
368impl<U, C> Proxy<U, C>
369where
370 U: Upstreams,
371 U::Error: Into<BoxedError>,
372 C: Client,
373{
374 #[must_use]
381 pub fn new(upstreams: U, client: C) -> Self {
382 Self {
383 upstreams,
384 client,
385 url_path_getter: Box::new(default_url_path_getter),
386 url_query_getter: Box::new(default_url_query_getter),
387 host_header_getter: Box::new(standard_host_header_getter),
388 client_ip_forwarding_enabled: false,
389 strict_path_normalization_enabled: true,
390 strip_authorization_header_enabled: false,
391 }
392 }
393
394 pub fn with_client_ip_forwarding(upstreams: U, client: C) -> Self {
399 Self {
400 upstreams,
401 client,
402 url_path_getter: Box::new(default_url_path_getter),
403 url_query_getter: Box::new(default_url_query_getter),
404 host_header_getter: Box::new(standard_host_header_getter),
405 client_ip_forwarding_enabled: true,
406 strict_path_normalization_enabled: true,
407 strip_authorization_header_enabled: false,
408 }
409 }
410
411 #[inline]
413 #[must_use]
414 pub fn url_path_getter<G>(mut self, url_path_getter: G) -> Self
415 where
416 G: Fn(&Request, &Depot) -> Option<String> + Send + Sync + 'static,
417 {
418 self.url_path_getter = Box::new(url_path_getter);
419 self
420 }
421
422 #[inline]
424 #[must_use]
425 pub fn url_query_getter<G>(mut self, url_query_getter: G) -> Self
426 where
427 G: Fn(&Request, &Depot) -> Option<String> + Send + Sync + 'static,
428 {
429 self.url_query_getter = Box::new(url_query_getter);
430 self
431 }
432
433 #[inline]
435 #[must_use]
436 pub fn host_header_getter<G>(mut self, host_header_getter: G) -> Self
437 where
438 G: Fn(&Uri, &Request, &Depot) -> Option<String> + Send + Sync + 'static,
439 {
440 self.host_header_getter = Box::new(host_header_getter);
441 self
442 }
443
444 #[inline]
451 #[must_use]
452 pub fn strict_path_normalization(mut self, enable: bool) -> Self {
453 self.strict_path_normalization_enabled = enable;
454 self
455 }
456
457 #[inline]
459 pub fn upstreams(&self) -> &U {
460 &self.upstreams
461 }
462 #[inline]
464 pub fn upstreams_mut(&mut self) -> &mut U {
465 &mut self.upstreams
466 }
467
468 #[inline]
470 pub fn client(&self) -> &C {
471 &self.client
472 }
473 #[inline]
475 pub fn client_mut(&mut self) -> &mut C {
476 &mut self.client
477 }
478
479 #[inline]
484 #[must_use]
485 pub fn client_ip_forwarding(mut self, enable: bool) -> Self {
486 self.client_ip_forwarding_enabled = enable;
487 self
488 }
489
490 #[inline]
500 #[must_use]
501 pub fn strip_authorization_header(mut self, enable: bool) -> Self {
502 self.strip_authorization_header_enabled = enable;
503 self
504 }
505
506 async fn build_proxied_request(
507 &self,
508 req: &mut Request,
509 depot: &Depot,
510 ) -> Result<HyperRequest, Error> {
511 let upstream = self
512 .upstreams
513 .elect(req, depot)
514 .await
515 .map_err(Error::other)?;
516
517 if upstream.is_empty() {
518 tracing::error!("upstreams is empty");
519 return Err(Error::other("upstreams is empty"));
520 }
521
522 let path = (self.url_path_getter)(req, depot).unwrap_or_else(|| {
523 tracing::debug!("url_path_getter returned None; forwarding to upstream root path");
528 String::new()
529 });
530 if self.strict_path_normalization_enabled {
531 if contains_ambiguous_path_escape(&path) {
532 return Err(Error::other("ambiguous percent-encoded path"));
533 }
534 if contains_parent_dir_component(&path) {
535 return Err(Error::other("parent directory path segment"));
536 }
537 }
538 let path = encode_url_path(&normalize_url_path(&path));
539 let query = (self.url_query_getter)(req, depot);
540 let path_and_query = if let Some(query) = query {
541 if let Some(stripped) = query.strip_prefix('?') {
542 format!("{path}?{}", utf8_percent_encode(stripped, QUERY_ENCODE_SET))
543 } else {
544 format!("{path}?{}", utf8_percent_encode(&query, QUERY_ENCODE_SET))
545 }
546 } else {
547 path
548 };
549 let forward_url = if upstream.ends_with('/') && path_and_query.starts_with('/') {
550 format!("{}{}", upstream.trim_end_matches('/'), path_and_query)
551 } else if upstream.ends_with('/') || path_and_query.starts_with('/') {
552 format!("{upstream}{path_and_query}")
553 } else if path_and_query.is_empty() {
554 upstream.to_owned()
555 } else {
556 format!("{upstream}/{path_and_query}")
557 };
558 let forward_url: Uri = TryFrom::try_from(forward_url).map_err(Error::other)?;
559 let mut request_builder = hyper::Request::builder()
560 .method(req.method())
561 .uri(&forward_url);
562 let connection_headers = connection_header_names(req.headers());
563 let upgrade_type = get_upgrade_type(req.headers()).map(str::to_owned);
564 for (key, value) in req.headers() {
565 if key == HOST || is_hop_by_hop_header(key, &connection_headers) {
566 continue;
567 }
568 if self.strip_authorization_header_enabled && key == AUTHORIZATION {
569 continue;
570 }
571 request_builder = request_builder.header(key, value);
572 }
573 if let Some(upgrade_type) = upgrade_type {
574 request_builder =
575 request_builder.header(CONNECTION, HeaderValue::from_static("upgrade"));
576 match HeaderValue::from_str(&upgrade_type) {
577 Ok(upgrade_type) => {
578 request_builder = request_builder.header(UPGRADE, upgrade_type);
579 }
580 Err(e) => {
581 tracing::error!(error = ?e, "invalid upgrade header value");
582 }
583 }
584 }
585 if let Some(host_value) = (self.host_header_getter)(&forward_url, req, depot) {
586 match HeaderValue::from_str(&host_value) {
587 Ok(host_value) => {
588 request_builder = request_builder.header(HOST, host_value);
589 }
590 Err(e) => {
591 tracing::error!(error = ?e, "invalid host header value");
592 }
593 }
594 }
595
596 if self.client_ip_forwarding_enabled {
597 let xff_header_name = HeaderName::from_static(X_FORWARDED_FOR_HEADER_NAME);
598 if let Some(client_ip) = req.remote_addr().ip() {
599 match HeaderValue::from_str(&client_ip.to_string()) {
600 Ok(xff) => {
601 if let Some(headers) = request_builder.headers_mut() {
602 headers.insert(&xff_header_name, xff);
603 }
604 }
605 Err(e) => {
606 tracing::error!(error = ?e, "invalid x-forwarded-for header value");
607 }
608 }
609 }
610 }
611
612 request_builder.body(req.take_body()).map_err(Error::other)
613 }
614}
615
616#[async_trait]
617impl<U, C> Handler for Proxy<U, C>
618where
619 U: Upstreams,
620 U::Error: Into<BoxedError>,
621 C: Client,
622{
623 async fn handle(
624 &self,
625 req: &mut Request,
626 depot: &mut Depot,
627 res: &mut Response,
628 _ctrl: &mut FlowCtrl,
629 ) {
630 match self.build_proxied_request(req, depot).await {
631 Ok(proxied_request) => {
632 match self
633 .client
634 .execute(proxied_request, req.extensions_mut().remove())
635 .await
636 {
637 Ok(response) => {
638 let (
639 salvo_core::http::response::Parts {
640 status,
641 headers,
643 ..
645 },
646 body,
647 ) = response.into_parts();
648 res.status_code(status);
649 append_end_to_end_headers(res.headers_mut(), &headers, status);
650 res.body(body);
651 }
652 Err(e) => {
653 tracing::error!( error = ?e, uri = ?req.uri(), "get response data failed: {}", e);
654 res.status_code(StatusCode::INTERNAL_SERVER_ERROR);
655 }
656 }
657 }
658 Err(e) => {
659 tracing::error!(error = ?e, "build proxied request failed");
660 res.status_code(StatusCode::BAD_REQUEST);
661 }
662 }
663 }
664}
665
666fn connection_header_names(headers: &HeaderMap) -> Vec<HeaderName> {
667 headers
668 .get_all(CONNECTION)
669 .iter()
670 .filter_map(|value| value.to_str().ok())
671 .flat_map(|value| value.split(','))
672 .filter_map(|name| HeaderName::from_bytes(name.trim().as_bytes()).ok())
673 .collect()
674}
675
676fn is_hop_by_hop_header(name: &HeaderName, connection_headers: &[HeaderName]) -> bool {
677 HOP_BY_HOP_HEADERS
678 .iter()
679 .any(|hop_header| name.as_str().eq_ignore_ascii_case(hop_header))
680 || connection_headers.iter().any(|header| header == name)
681}
682
683fn append_end_to_end_headers(destination: &mut HeaderMap, source: &HeaderMap, status: StatusCode) {
684 let connection_headers = connection_header_names(source);
685 let upgrade_type = if status == StatusCode::SWITCHING_PROTOCOLS {
686 get_upgrade_type(source).map(str::to_owned)
687 } else {
688 None
689 };
690 for name in source.keys() {
691 if is_hop_by_hop_header(name, &connection_headers) {
692 continue;
693 }
694 for value in source.get_all(name) {
695 destination.append(name, value.to_owned());
696 }
697 }
698 if let Some(upgrade_type) = upgrade_type {
699 destination.append(CONNECTION, HeaderValue::from_static("upgrade"));
700 match HeaderValue::from_str(&upgrade_type) {
701 Ok(upgrade_type) => {
702 destination.append(UPGRADE, upgrade_type);
703 }
704 Err(e) => {
705 tracing::error!(error = ?e, "invalid upgrade header value");
706 }
707 }
708 }
709}
710
711#[inline]
712fn get_upgrade_type(headers: &HeaderMap) -> Option<&str> {
713 if connection_header_names(headers).contains(&UPGRADE)
714 && let Some(upgrade_value) = headers.get(&UPGRADE)
715 {
716 tracing::debug!(
717 "found upgrade header with value: {:?}",
718 upgrade_value.to_str()
719 );
720 return upgrade_value.to_str().ok();
721 }
722
723 None
724}
725
726#[inline]
734pub(crate) fn upgrade_types_match(request: Option<&str>, response: Option<&str>) -> bool {
735 match (request, response) {
736 (Some(req), Some(resp)) => req.eq_ignore_ascii_case(resp),
737 (None, None) => true,
738 _ => false,
739 }
740}
741
742#[cfg(test)]
744mod tests {
745 use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6};
746 use std::str::FromStr;
747
748 use futures_util::{SinkExt, StreamExt};
749 use salvo_core::conn::{Acceptor, Listener, SocketAddr};
750 use salvo_core::prelude::{Router, Server, StatusError, TcpListener, handler};
751 use salvo_extra::websocket::WebSocketUpgrade;
752 use tokio::io::{AsyncReadExt, AsyncWriteExt};
753 use tokio_tungstenite::tungstenite::Message;
754 use tokio_tungstenite::tungstenite::protocol::Role;
755
756 use super::*;
757
758 #[handler]
759 async fn websocket_echo(req: &mut Request, res: &mut Response) -> Result<(), StatusError> {
760 WebSocketUpgrade::new()
761 .upgrade(req, res, |mut ws| async move {
762 while let Some(message) = ws.recv().await {
763 let Ok(message) = message else {
764 return;
765 };
766 if ws.send(message).await.is_err() {
767 return;
768 }
769 }
770 })
771 .await
772 }
773
774 async fn spawn_server(router: Router) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) {
775 let acceptor = TcpListener::new("127.0.0.1:0").bind().await;
776 let addr = acceptor.holdings()[0]
777 .local_addr
778 .clone()
779 .into_std()
780 .unwrap();
781 let handle = tokio::spawn(async move {
782 Server::new(acceptor).serve(router).await;
783 });
784 (addr, handle)
785 }
786
787 #[test]
788 fn test_encode_url_path() {
789 let path = "/test/path";
790 let encoded_path = encode_url_path(path);
791 assert_eq!(encoded_path, "/test/path");
792 }
793
794 #[test]
795 fn test_upgrade_types_match_is_case_insensitive() {
796 assert!(upgrade_types_match(Some("WebSocket"), Some("websocket")));
798 assert!(upgrade_types_match(Some("WEBSOCKET"), Some("WebSocket")));
799 assert!(upgrade_types_match(Some("h2c"), Some("h2c")));
800 assert!(upgrade_types_match(None, None));
801
802 assert!(!upgrade_types_match(Some("websocket"), Some("h2c")));
803 assert!(!upgrade_types_match(Some("websocket"), None));
804 assert!(!upgrade_types_match(None, Some("websocket")));
805 }
806
807 #[test]
808 fn test_encode_url_path_preserves_segments_and_escapes_unsafe_chars() {
809 assert_eq!(encode_url_path(""), "");
811
812 assert_eq!(encode_url_path("/"), "/");
814 assert_eq!(encode_url_path("//a//b//"), "//a//b//");
815
816 assert_eq!(encode_url_path("a b/c d"), "a%20b/c%20d");
819 assert_eq!(encode_url_path("a/{b}/c"), "a/%7Bb%7D/c");
820 }
821
822 #[test]
823 fn test_default_url_path_getter_uses_raw_tail() {
824 let mut request = Request::new();
825 request
826 .params_mut()
827 .insert("**rest", "guide/../index.html".to_owned());
828 let depot = Depot::new();
829
830 assert_eq!(
831 default_url_path_getter(&request, &depot).as_deref(),
832 Some("guide/../index.html")
833 );
834 }
835
836 #[test]
837 fn test_contains_ambiguous_path_escape() {
838 assert!(contains_ambiguous_path_escape("%2e%2e/admin"));
839 assert!(contains_ambiguous_path_escape("api%2Fadmin"));
840 assert!(contains_ambiguous_path_escape("api%5cadmin"));
841 assert!(contains_ambiguous_path_escape("%252e%252e/admin"));
842 assert!(!contains_ambiguous_path_escape("guide.v1/index.html"));
843 assert!(!contains_ambiguous_path_escape("files/%20space"));
844 }
845
846 #[test]
847 fn test_contains_parent_dir_component() {
848 assert!(contains_parent_dir_component("../admin"));
849 assert!(contains_parent_dir_component("api/../admin"));
850 assert!(contains_parent_dir_component(r"api\..\admin"));
851 assert!(!contains_parent_dir_component("guide.v1/index.html"));
852 assert!(!contains_parent_dir_component("files/%2e%2e/admin"));
853 assert!(!contains_parent_dir_component("..hidden/admin"));
854 }
855
856 #[test]
857 fn test_get_upgrade_type() {
858 let mut headers = HeaderMap::new();
859 headers.insert(CONNECTION, HeaderValue::from_static("upgrade"));
860 headers.insert(UPGRADE, HeaderValue::from_static("websocket"));
861 let upgrade_type = get_upgrade_type(&headers);
862 assert_eq!(upgrade_type, Some("websocket"));
863 }
864
865 #[test]
866 fn test_get_upgrade_type_checks_all_connection_headers() {
867 let mut headers = HeaderMap::new();
868 headers.append(CONNECTION, HeaderValue::from_static("keep-alive"));
869 headers.append(CONNECTION, HeaderValue::from_static("Upgrade"));
870 headers.insert(UPGRADE, HeaderValue::from_static("websocket"));
871
872 let upgrade_type = get_upgrade_type(&headers);
873
874 assert_eq!(upgrade_type, Some("websocket"));
875 }
876
877 #[test]
878 fn test_connection_header_names() {
879 let mut headers = HeaderMap::new();
880 headers.append(CONNECTION, HeaderValue::from_static("keep-alive, x-remove"));
881 headers.append(CONNECTION, HeaderValue::from_static("x-second"));
882
883 let names = connection_header_names(&headers);
884 assert!(names.contains(&HeaderName::from_static("keep-alive")));
885 assert!(names.contains(&HeaderName::from_static("x-remove")));
886 assert!(names.contains(&HeaderName::from_static("x-second")));
887 }
888
889 #[test]
890 fn test_host_header_handling() {
891 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
892 let uri = Uri::from_str("http://host.tld/test").unwrap();
893 let mut req = Request::new();
894 let depot = Depot::new();
895
896 assert_eq!(
897 default_host_header_getter(&uri, &req, &depot),
898 Some("host.tld".to_owned())
899 );
900
901 let uri_with_port = Uri::from_str("http://host.tld:8080/test").unwrap();
902 assert_eq!(
903 default_host_header_getter(&uri_with_port, &req, &depot),
904 Some("host.tld".to_owned())
905 );
906 assert_eq!(
907 standard_host_header_getter(&uri_with_port, &req, &depot),
908 Some("host.tld:8080".to_owned())
909 );
910
911 let uri_with_http_port = Uri::from_str("http://host.tld:80/test").unwrap();
912 assert_eq!(
913 standard_host_header_getter(&uri_with_http_port, &req, &depot),
914 Some("host.tld".to_owned())
915 );
916
917 let uri_with_https_port = Uri::from_str("https://host.tld:443/test").unwrap();
918 assert_eq!(
919 standard_host_header_getter(&uri_with_https_port, &req, &depot),
920 Some("host.tld".to_owned())
921 );
922
923 let uri_with_non_https_scheme_and_https_port =
924 Uri::from_str("http://host.tld:443/test").unwrap();
925 assert_eq!(
926 standard_host_header_getter(&uri_with_non_https_scheme_and_https_port, &req, &depot),
927 Some("host.tld:443".to_owned())
928 );
929
930 req.headers_mut()
931 .insert(HOST, HeaderValue::from_static("test.host.tld"));
932 assert_eq!(
933 preserve_original_host_header_getter(&uri, &req, &depot),
934 Some("test.host.tld".to_owned())
935 );
936 }
937
938 #[test]
939 fn test_proxy_default_host_header_getter_includes_port() {
940 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
941 let proxy = Proxy::new(vec!["http://host.tld:8080"], HyperClient::default());
944 let uri = Uri::from_str("http://host.tld:8080/test").unwrap();
945 let req = Request::new();
946 let depot = Depot::new();
947 assert_eq!(
948 (proxy.host_header_getter)(&uri, &req, &depot),
949 Some("host.tld:8080".to_owned())
950 );
951 }
952
953 #[tokio::test]
954 async fn test_build_proxied_request_strips_hop_by_hop_headers() {
955 let proxy = Proxy::new(vec!["http://example.com"], HyperClient::default());
956 let mut request = Request::new();
957 let depot = Depot::new();
958
959 request
960 .headers_mut()
961 .insert(HOST, HeaderValue::from_static("client.example"));
962 request
963 .headers_mut()
964 .insert(CONNECTION, HeaderValue::from_static("keep-alive, x-remove"));
965 request.headers_mut().insert(
966 HeaderName::from_static("keep-alive"),
967 HeaderValue::from_static("timeout=5"),
968 );
969 request.headers_mut().insert(
970 HeaderName::from_static("x-remove"),
971 HeaderValue::from_static("secret"),
972 );
973 request.headers_mut().insert(
974 HeaderName::from_static("te"),
975 HeaderValue::from_static("trailers"),
976 );
977 request.headers_mut().insert(
978 HeaderName::from_static("transfer-encoding"),
979 HeaderValue::from_static("chunked"),
980 );
981 request.headers_mut().insert(
982 HeaderName::from_static("x-keep"),
983 HeaderValue::from_static("ok"),
984 );
985
986 let proxied = proxy
987 .build_proxied_request(&mut request, &depot)
988 .await
989 .unwrap();
990
991 assert!(proxied.headers().get(CONNECTION).is_none());
992 assert!(
993 proxied
994 .headers()
995 .get(HeaderName::from_static("keep-alive"))
996 .is_none()
997 );
998 assert!(
999 proxied
1000 .headers()
1001 .get(HeaderName::from_static("x-remove"))
1002 .is_none()
1003 );
1004 assert!(
1005 proxied
1006 .headers()
1007 .get(HeaderName::from_static("te"))
1008 .is_none()
1009 );
1010 assert!(
1011 proxied
1012 .headers()
1013 .get(HeaderName::from_static("transfer-encoding"))
1014 .is_none()
1015 );
1016 assert_eq!(
1017 proxied.headers().get(HeaderName::from_static("x-keep")),
1018 Some(&HeaderValue::from_static("ok"))
1019 );
1020 }
1021
1022 #[tokio::test]
1023 async fn test_build_proxied_request_regenerates_upgrade_headers() {
1024 let proxy = Proxy::new(vec!["http://example.com"], HyperClient::default());
1025 let mut request = Request::new();
1026 let depot = Depot::new();
1027
1028 request
1029 .headers_mut()
1030 .insert(CONNECTION, HeaderValue::from_static("x-remove, Upgrade"));
1031 request
1032 .headers_mut()
1033 .insert(UPGRADE, HeaderValue::from_static("websocket"));
1034 request.headers_mut().insert(
1035 HeaderName::from_static("x-remove"),
1036 HeaderValue::from_static("secret"),
1037 );
1038
1039 let proxied = proxy
1040 .build_proxied_request(&mut request, &depot)
1041 .await
1042 .unwrap();
1043
1044 assert_eq!(
1045 proxied.headers().get(CONNECTION),
1046 Some(&HeaderValue::from_static("upgrade"))
1047 );
1048 assert_eq!(
1049 proxied.headers().get(UPGRADE),
1050 Some(&HeaderValue::from_static("websocket"))
1051 );
1052 assert!(
1053 proxied
1054 .headers()
1055 .get(HeaderName::from_static("x-remove"))
1056 .is_none()
1057 );
1058 }
1059
1060 #[tokio::test]
1061 async fn test_build_proxied_request_forwards_authorization_by_default() {
1062 let proxy = Proxy::new(vec!["http://example.com"], HyperClient::default());
1063 let mut request = Request::new();
1064 let depot = Depot::new();
1065
1066 request
1067 .headers_mut()
1068 .insert(AUTHORIZATION, HeaderValue::from_static("Bearer secret"));
1069
1070 let proxied = proxy
1071 .build_proxied_request(&mut request, &depot)
1072 .await
1073 .unwrap();
1074
1075 assert_eq!(
1076 proxied.headers().get(AUTHORIZATION),
1077 Some(&HeaderValue::from_static("Bearer secret"))
1078 );
1079 }
1080
1081 #[tokio::test]
1082 async fn test_build_proxied_request_strips_authorization_when_enabled() {
1083 let proxy = Proxy::new(vec!["http://example.com"], HyperClient::default())
1084 .strip_authorization_header(true);
1085 let mut request = Request::new();
1086 let depot = Depot::new();
1087
1088 request
1089 .headers_mut()
1090 .insert(AUTHORIZATION, HeaderValue::from_static("Bearer secret"));
1091 request.headers_mut().insert(
1092 HeaderName::from_static("x-keep"),
1093 HeaderValue::from_static("ok"),
1094 );
1095
1096 let proxied = proxy
1097 .build_proxied_request(&mut request, &depot)
1098 .await
1099 .unwrap();
1100
1101 assert!(proxied.headers().get(AUTHORIZATION).is_none());
1102 assert_eq!(
1103 proxied.headers().get(HeaderName::from_static("x-keep")),
1104 Some(&HeaderValue::from_static("ok"))
1105 );
1106 }
1107
1108 #[tokio::test]
1109 async fn test_proxy_websocket_connection_with_split_connection_headers() {
1110 let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
1111
1112 let upstream_router = Router::with_path("ws").goal(websocket_echo);
1113 let (upstream_addr, upstream_server) = spawn_server(upstream_router).await;
1114
1115 let proxy_router = Router::with_path("{**rest}").goal(Proxy::new(
1116 vec![format!("http://{upstream_addr}")],
1117 HyperClient::default(),
1118 ));
1119 let (proxy_addr, proxy_server) = spawn_server(proxy_router).await;
1120
1121 let mut stream = tokio::net::TcpStream::connect(proxy_addr).await.unwrap();
1122 let request = format!(
1123 "\
1124GET /ws HTTP/1.1\r\n\
1125Host: {proxy_addr}\r\n\
1126Connection: keep-alive\r\n\
1127Connection: Upgrade\r\n\
1128Upgrade: websocket\r\n\
1129Sec-WebSocket-Version: 13\r\n\
1130Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n\
1131\r\n"
1132 );
1133 stream.write_all(request.as_bytes()).await.unwrap();
1134
1135 let mut response = Vec::new();
1136 let mut buffer = [0; 1024];
1137 let header_end = loop {
1138 let read = stream.read(&mut buffer).await.unwrap();
1139 assert_ne!(
1140 read, 0,
1141 "server closed before websocket handshake completed"
1142 );
1143 response.extend_from_slice(&buffer[..read]);
1144 if let Some(position) = response.windows(4).position(|window| window == b"\r\n\r\n") {
1145 break position + 4;
1146 }
1147 };
1148 let extra = response.split_off(header_end);
1149 let response_head = String::from_utf8_lossy(&response);
1150 assert!(
1151 response_head.starts_with("HTTP/1.1 101"),
1152 "unexpected websocket handshake response: {response_head}"
1153 );
1154 let response_head_lower = response_head.to_ascii_lowercase();
1155 assert!(
1156 response_head_lower.contains("\r\nconnection: upgrade\r\n"),
1157 "missing connection upgrade header: {response_head}"
1158 );
1159 assert!(
1160 response_head_lower.contains("\r\nupgrade: websocket\r\n"),
1161 "missing upgrade header: {response_head}"
1162 );
1163
1164 let mut websocket = tokio_tungstenite::WebSocketStream::from_partially_read(
1165 stream,
1166 extra,
1167 Role::Client,
1168 None,
1169 )
1170 .await;
1171
1172 websocket
1173 .send(Message::text("proxied websocket"))
1174 .await
1175 .unwrap();
1176 let echoed = websocket.next().await.unwrap().unwrap();
1177 assert_eq!(echoed.into_text().unwrap(), "proxied websocket");
1178
1179 websocket.close(None).await.unwrap();
1180 proxy_server.abort();
1181 upstream_server.abort();
1182 }
1183
1184 #[tokio::test]
1185 async fn test_client_ip_forwarding() {
1186 let xff_header_name = HeaderName::from_static(X_FORWARDED_FOR_HEADER_NAME);
1187
1188 let mut request = Request::new();
1189 let depot = Depot::new();
1190
1191 let proxy_without_forwarding =
1193 Proxy::new(vec!["http://example.com"], HyperClient::default());
1194
1195 assert!(!proxy_without_forwarding.client_ip_forwarding_enabled);
1196
1197 let proxy_with_forwarding = proxy_without_forwarding.client_ip_forwarding(true);
1198
1199 assert!(proxy_with_forwarding.client_ip_forwarding_enabled);
1200
1201 let proxy =
1202 Proxy::with_client_ip_forwarding(vec!["http://example.com"], HyperClient::default());
1203 assert!(proxy.client_ip_forwarding_enabled);
1204
1205 match proxy.build_proxied_request(&mut request, &depot).await {
1206 Ok(req) => assert!(req.headers().get(&xff_header_name).is_none()),
1207 _ => panic!("expected Ok"),
1208 }
1209
1210 *request.remote_addr_mut() =
1211 SocketAddr::from(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 12345));
1212
1213 match proxy.build_proxied_request(&mut request, &depot).await {
1214 Ok(req) => assert_eq!(
1215 req.headers().get(&xff_header_name),
1216 Some(&HeaderValue::from_static("127.0.0.1"))
1217 ),
1218 _ => panic!("expected Ok"),
1219 }
1220
1221 *request.remote_addr_mut() =
1223 SocketAddr::from(SocketAddrV6::new(Ipv6Addr::LOCALHOST, 12345, 0, 0));
1224
1225 match proxy.build_proxied_request(&mut request, &depot).await {
1226 Ok(req) => assert_eq!(
1227 req.headers().get(&xff_header_name),
1228 Some(&HeaderValue::from_static("::1"))
1229 ),
1230 _ => panic!("expected Ok"),
1231 }
1232
1233 *request.remote_addr_mut() = SocketAddr::Unknown;
1234
1235 match proxy.build_proxied_request(&mut request, &depot).await {
1236 Ok(req) => assert!(req.headers().get(&xff_header_name).is_none()),
1237 _ => panic!("expected Ok"),
1238 }
1239
1240 request.headers_mut().insert(
1242 &xff_header_name,
1243 HeaderValue::from_static("10.72.0.1, 127.0.0.1"),
1244 );
1245 *request.remote_addr_mut() =
1246 SocketAddr::from(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 12345));
1247
1248 match proxy.build_proxied_request(&mut request, &depot).await {
1249 Ok(req) => assert_eq!(
1250 req.headers().get(&xff_header_name),
1251 Some(&HeaderValue::from_static("127.0.0.1"))
1252 ),
1253 _ => panic!("expected Ok"),
1254 }
1255 }
1256
1257 #[tokio::test]
1258 async fn test_build_proxied_request_rejects_parent_dir_tail_by_default() {
1259 let mut request = Request::new();
1260 request.params_mut().insert("**rest", "../admin".to_owned());
1261 let depot = Depot::new();
1262 let proxy = Proxy::new(vec!["http://example.com/api"], HyperClient::default());
1263
1264 assert!(
1265 proxy
1266 .build_proxied_request(&mut request, &depot)
1267 .await
1268 .is_err()
1269 );
1270 }
1271
1272 #[tokio::test]
1273 async fn test_build_proxied_request_can_opt_out_of_parent_dir_rejection() {
1274 let mut request = Request::new();
1275 request.params_mut().insert("**rest", "../admin".to_owned());
1276 let depot = Depot::new();
1277 let proxy = Proxy::new(vec!["http://example.com/api"], HyperClient::default())
1278 .strict_path_normalization(false);
1279
1280 let req = proxy
1281 .build_proxied_request(&mut request, &depot)
1282 .await
1283 .unwrap();
1284 assert_eq!(req.uri().to_string(), "http://example.com/api/admin");
1285 }
1286
1287 #[tokio::test]
1288 async fn test_build_proxied_request_normalizes_safe_tail() {
1289 let mut request = Request::new();
1290 request
1291 .params_mut()
1292 .insert("**rest", "guide\\index.html".to_owned());
1293 let depot = Depot::new();
1294 let proxy = Proxy::new(vec!["http://example.com/api"], HyperClient::default());
1295
1296 let proxied_request = proxy
1297 .build_proxied_request(&mut request, &depot)
1298 .await
1299 .unwrap();
1300 assert_eq!(
1301 proxied_request.uri().to_string(),
1302 "http://example.com/api/guide/index.html"
1303 );
1304 }
1305
1306 #[tokio::test]
1307 async fn test_build_proxied_request_rejects_ambiguous_encoded_tail_by_default() {
1308 let mut request = Request::new();
1309 request
1310 .params_mut()
1311 .insert("**rest", "%2e%2e/secrets/.env".to_owned());
1312 let depot = Depot::new();
1313 let proxy = Proxy::new(vec!["http://example.com/api"], HyperClient::default());
1314
1315 assert!(
1316 proxy
1317 .build_proxied_request(&mut request, &depot)
1318 .await
1319 .is_err()
1320 );
1321 }
1322
1323 #[tokio::test]
1324 async fn test_build_proxied_request_can_opt_out_of_strict_path_normalization() {
1325 let mut request = Request::new();
1326 request
1327 .params_mut()
1328 .insert("**rest", "%2e%2e/secrets/.env".to_owned());
1329 let depot = Depot::new();
1330 let proxy = Proxy::new(vec!["http://example.com/api"], HyperClient::default())
1331 .strict_path_normalization(false);
1332
1333 let proxied_request = proxy
1334 .build_proxied_request(&mut request, &depot)
1335 .await
1336 .unwrap();
1337 assert_eq!(
1338 proxied_request.uri().to_string(),
1339 "http://example.com/api/%2e%2e/secrets/.env"
1340 );
1341 }
1342
1343 #[tokio::test]
1344 async fn test_build_proxied_request_strict_path_normalization_rejects_ambiguous_escapes() {
1345 for path in [
1346 "%2e%2e/secrets/.env",
1347 "api%2fadmin",
1348 "api%5cadmin",
1349 "%252e%252e/secrets/.env",
1350 ] {
1351 let mut request = Request::new();
1352 request.params_mut().insert("**rest", path.to_owned());
1353 let depot = Depot::new();
1354 let proxy = Proxy::new(vec!["http://example.com/api"], HyperClient::default());
1355
1356 let err = proxy.build_proxied_request(&mut request, &depot).await;
1357 assert!(err.is_err(), "path should be rejected: {path}");
1358 }
1359 }
1360
1361 #[test]
1362 fn test_append_end_to_end_headers_strips_response_hop_by_hop_headers() {
1363 let mut source = HeaderMap::new();
1364 source.insert(CONNECTION, HeaderValue::from_static("x-remove"));
1365 source.insert(UPGRADE, HeaderValue::from_static("websocket"));
1366 source.insert(
1367 HeaderName::from_static("transfer-encoding"),
1368 HeaderValue::from_static("chunked"),
1369 );
1370 source.insert(
1371 HeaderName::from_static("x-remove"),
1372 HeaderValue::from_static("secret"),
1373 );
1374 source.insert(
1375 HeaderName::from_static("x-keep"),
1376 HeaderValue::from_static("ok"),
1377 );
1378
1379 let mut destination = HeaderMap::new();
1380 append_end_to_end_headers(&mut destination, &source, StatusCode::OK);
1381
1382 assert!(destination.get(CONNECTION).is_none());
1383 assert!(destination.get(UPGRADE).is_none());
1384 assert!(
1385 destination
1386 .get(HeaderName::from_static("transfer-encoding"))
1387 .is_none()
1388 );
1389 assert!(
1390 destination
1391 .get(HeaderName::from_static("x-remove"))
1392 .is_none()
1393 );
1394 assert_eq!(
1395 destination.get(HeaderName::from_static("x-keep")),
1396 Some(&HeaderValue::from_static("ok"))
1397 );
1398 }
1399
1400 #[test]
1401 fn test_append_end_to_end_headers_preserves_upgrade_handshake_on_101() {
1402 let mut source = HeaderMap::new();
1403 source.insert(CONNECTION, HeaderValue::from_static("Upgrade, x-remove"));
1404 source.insert(UPGRADE, HeaderValue::from_static("websocket"));
1405 source.insert(
1406 HeaderName::from_static("transfer-encoding"),
1407 HeaderValue::from_static("chunked"),
1408 );
1409 source.insert(
1410 HeaderName::from_static("x-remove"),
1411 HeaderValue::from_static("secret"),
1412 );
1413 source.insert(
1414 HeaderName::from_static("x-keep"),
1415 HeaderValue::from_static("ok"),
1416 );
1417
1418 let mut destination = HeaderMap::new();
1419 append_end_to_end_headers(&mut destination, &source, StatusCode::SWITCHING_PROTOCOLS);
1420
1421 assert_eq!(
1422 destination.get(CONNECTION),
1423 Some(&HeaderValue::from_static("upgrade"))
1424 );
1425 assert_eq!(
1426 destination.get(UPGRADE),
1427 Some(&HeaderValue::from_static("websocket"))
1428 );
1429 assert!(
1430 destination
1431 .get(HeaderName::from_static("transfer-encoding"))
1432 .is_none()
1433 );
1434 assert!(
1435 destination
1436 .get(HeaderName::from_static("x-remove"))
1437 .is_none()
1438 );
1439 assert_eq!(
1440 destination.get(HeaderName::from_static("x-keep")),
1441 Some(&HeaderValue::from_static("ok"))
1442 );
1443 }
1444}