1use http_body_util::{BodyExt, Full};
25use hyper::{Request, Response, StatusCode, body::Incoming};
26use std::convert::Infallible;
27use std::net::SocketAddr;
28use std::sync::Arc;
29
30use crate::error::WiseGateError;
31use crate::types::{ConfigProvider, RateLimiter};
32use crate::{auth, headers, ip_filter, rate_limiter};
33
34pub async fn handle_request<C: ConfigProvider>(
85 req: Request<Incoming>,
86 peer_addr: SocketAddr,
87 forward_host: Arc<str>,
88 forward_port: u16,
89 limiter: RateLimiter,
90 config: Arc<C>,
91 http_client: reqwest::Client,
92) -> Result<Response<Full<bytes::Bytes>>, Infallible> {
93 let real_client_ip: Option<String> = match ip_filter::extract_and_validate_real_ip(
95 req.headers(),
96 peer_addr.ip(),
97 config.as_ref(),
98 ) {
99 Some(ip) => Some(ip),
100 None => {
101 if config.allowed_proxy_ips().is_none() {
102 None
104 } else {
105 let err = WiseGateError::InvalidIp("missing or invalid proxy headers".into());
107 return Ok(create_error_response(err.status_code(), err.user_message()));
108 }
109 }
110 };
111
112 if let Some(ref ip) = real_client_ip
114 && ip_filter::is_ip_blocked(ip, config.as_ref())
115 {
116 let err = WiseGateError::IpBlocked(ip.clone());
117 return Ok(create_error_response(err.status_code(), err.user_message()));
118 }
119
120 let request_path = req.uri().path();
122 if is_url_pattern_blocked(request_path, config.as_ref()) {
123 let err = WiseGateError::PatternBlocked(request_path.to_string());
124 return Ok(create_error_response(err.status_code(), err.user_message()));
125 }
126
127 let request_method = req.method().as_str();
129 if is_method_blocked(request_method, config.as_ref()) {
130 let err = WiseGateError::MethodBlocked(request_method.to_string());
131 return Ok(create_error_response(err.status_code(), err.user_message()));
132 }
133
134 if let Some(ref ip) = real_client_ip
137 && !rate_limiter::check_rate_limit(&limiter, ip, config.as_ref()).await
138 {
139 let err = WiseGateError::RateLimitExceeded(ip.clone());
140 return Ok(create_error_response(err.status_code(), err.user_message()));
141 }
142
143 if config.is_auth_enabled() {
146 let auth_header = req
147 .headers()
148 .get(headers::AUTHORIZATION)
149 .and_then(|v| v.to_str().ok());
150
151 let basic_auth_enabled = config.is_basic_auth_enabled();
152 let bearer_auth_enabled = config.is_bearer_auth_enabled();
153
154 let basic_auth_passed = if basic_auth_enabled {
155 match auth::check_basic_auth_bounded(auth_header, config.auth_credentials()).await {
156 Ok(passed) => passed,
157 Err(_) => {
158 return Ok(create_error_response(
159 StatusCode::SERVICE_UNAVAILABLE,
160 "Authentication temporarily unavailable",
161 ));
162 }
163 }
164 } else {
165 false
166 };
167 let bearer_auth_passed =
168 bearer_auth_enabled && auth::check_bearer_token(auth_header, config.bearer_token());
169
170 if !basic_auth_passed && !bearer_auth_passed {
172 return Ok(create_unauthorized_response(config.auth_realm()));
173 }
174 }
175
176 forward_request(
180 req,
181 real_client_ip.as_deref(),
182 &forward_host,
183 forward_port,
184 config.as_ref(),
185 &http_client,
186 )
187 .await
188}
189
190async fn forward_request(
192 req: Request<Incoming>,
193 real_client_ip: Option<&str>,
194 host: &str,
195 port: u16,
196 config: &impl ConfigProvider,
197 http_client: &reqwest::Client,
198) -> Result<Response<Full<bytes::Bytes>>, Infallible> {
199 let proxy_config = config.proxy_config();
200 let (parts, body) = req.into_parts();
201
202 if proxy_config.max_body_size > 0
204 && let Some(content_length) = parts
205 .headers
206 .get(headers::CONTENT_LENGTH)
207 .and_then(|v| v.to_str().ok())
208 .and_then(|v| v.parse::<usize>().ok())
209 && content_length > proxy_config.max_body_size
210 {
211 let err = WiseGateError::BodyTooLarge {
212 size: content_length,
213 max: proxy_config.max_body_size,
214 };
215 return Ok(create_body_error_response(&err));
216 }
217
218 let body_bytes = match read_request_body(
219 body,
220 proxy_config.max_body_size,
221 config.client_body_timeout(),
222 )
223 .await
224 {
225 Ok(bytes) => bytes,
226 Err(err) => {
227 return Ok(create_body_error_response(&err));
228 }
229 };
230
231 let strip_auth = config.is_auth_enabled() && !config.forward_authorization_header();
232 forward_with_reqwest(
233 parts,
234 body_bytes,
235 real_client_ip,
236 host,
237 port,
238 http_client,
239 strip_auth,
240 )
241 .await
242}
243
244async fn read_request_body<B>(
245 body: B,
246 max_size: usize,
247 timeout: std::time::Duration,
248) -> Result<bytes::Bytes, WiseGateError>
249where
250 B: hyper::body::Body<Data = bytes::Bytes> + Unpin,
251 B::Error: std::fmt::Display,
252{
253 tokio::time::timeout(timeout, collect_request_body(body, max_size))
254 .await
255 .map_err(|_| WiseGateError::ClientBodyTimeout)?
256}
257
258fn create_body_error_response(err: &WiseGateError) -> Response<Full<bytes::Bytes>> {
259 let mut response = create_error_response(err.status_code(), err.user_message());
260 response.headers_mut().insert(
262 headers::CONNECTION,
263 hyper::header::HeaderValue::from_static("close"),
264 );
265 response
266}
267
268async fn collect_request_body<B>(
271 mut body: B,
272 max_size: usize,
273) -> Result<bytes::Bytes, WiseGateError>
274where
275 B: hyper::body::Body<Data = bytes::Bytes> + Unpin,
276 B::Error: std::fmt::Display,
277{
278 let mut buffer = bytes::BytesMut::new();
279 while let Some(frame) = body.frame().await {
280 let frame = frame.map_err(|err| WiseGateError::BodyReadError(err.to_string()))?;
281 if let Ok(data) = frame.into_data() {
282 let size = buffer.len().saturating_add(data.len());
283 if max_size > 0 && size > max_size {
284 return Err(WiseGateError::BodyTooLarge {
285 size,
286 max: max_size,
287 });
288 }
289 buffer.extend_from_slice(&data);
290 }
291 }
292 Ok(buffer.freeze())
293}
294
295async fn forward_with_reqwest(
297 parts: hyper::http::request::Parts,
298 body_bytes: bytes::Bytes,
299 real_client_ip: Option<&str>,
300 host: &str,
301 port: u16,
302 client: &reqwest::Client,
303 strip_auth: bool,
304) -> Result<Response<Full<bytes::Bytes>>, Infallible> {
305 let destination_uri = format!(
307 "http://{}:{}{}",
308 host,
309 port,
310 parts.uri.path_and_query().map_or("", |pq| pq.as_str())
311 );
312
313 let method = match reqwest::Method::from_bytes(parts.method.as_str().as_bytes()) {
315 Ok(m) => m,
316 Err(_) => {
317 let err =
318 WiseGateError::MethodBlocked(format!("{} (unsupported)", parts.method.as_str()));
319 return Ok(create_error_response(err.status_code(), err.user_message()));
320 }
321 };
322 let mut req_builder = client.request(method, &destination_uri);
323
324 let nominated = headers::connection_nominated(&parts.headers);
329 for (name, value) in parts.headers.iter() {
330 if name != headers::HOST
331 && name != headers::CONTENT_LENGTH
332 && name != headers::X_REAL_IP
333 && !(strip_auth && name == headers::AUTHORIZATION)
334 && !headers::is_hop_by_hop_for(name.as_str(), &nominated)
335 && let Ok(header_value) = value.to_str()
336 {
337 req_builder = req_builder.header(name.as_str(), header_value);
338 }
339 }
340 if let Some(ip) = real_client_ip {
341 req_builder = req_builder.header(headers::X_REAL_IP, ip);
342 }
343
344 if !body_bytes.is_empty() {
346 req_builder = req_builder.body(body_bytes);
347 }
348
349 match req_builder.send().await {
351 Ok(response) => {
352 let status = response.status();
353 let resp_headers = response.headers().clone();
354
355 match response.bytes().await {
356 Ok(body_bytes) => {
357 let mut hyper_response = match Response::builder()
358 .status(status.as_u16())
359 .body(Full::new(body_bytes))
360 {
361 Ok(resp) => resp,
362 Err(e) => {
363 let err = WiseGateError::ProxyError(format!(
364 "Failed to build response: {}",
365 e
366 ));
367 return Ok(create_error_response(
368 err.status_code(),
369 err.user_message(),
370 ));
371 }
372 };
373
374 let nominated = headers::connection_nominated(&resp_headers);
377 for (name, value) in resp_headers.iter() {
378 if !headers::is_hop_by_hop_for(name.as_str(), &nominated)
379 && let (Ok(hyper_name), Ok(hyper_value)) = (
380 hyper::header::HeaderName::from_bytes(name.as_str().as_bytes()),
381 hyper::header::HeaderValue::from_bytes(value.as_bytes()),
382 )
383 {
384 hyper_response.headers_mut().append(hyper_name, hyper_value);
385 }
386 }
387
388 Ok(hyper_response)
389 }
390 Err(e) => {
391 let err = WiseGateError::BodyReadError(format!("response: {}", e));
392 Ok(create_error_response(err.status_code(), err.user_message()))
393 }
394 }
395 }
396 Err(err) => {
397 let wise_err = if err.is_timeout() {
399 WiseGateError::UpstreamTimeout(err.to_string())
400 } else if err.is_connect() {
401 WiseGateError::UpstreamConnectionFailed(err.to_string())
402 } else {
403 WiseGateError::ProxyError(err.to_string())
404 };
405 Ok(create_error_response(
406 wise_err.status_code(),
407 wise_err.user_message(),
408 ))
409 }
410 }
411}
412
413pub fn create_error_response(status: StatusCode, message: &str) -> Response<Full<bytes::Bytes>> {
438 Response::builder()
439 .status(status)
440 .header(headers::CONTENT_TYPE, "text/plain")
441 .body(Full::new(bytes::Bytes::from(message.to_string())))
442 .unwrap_or_else(|_| {
443 Response::new(Full::new(bytes::Bytes::from("Internal Server Error")))
445 })
446}
447
448pub fn create_unauthorized_response(realm: &str) -> Response<Full<bytes::Bytes>> {
461 let sanitized_realm = realm.replace('\\', "\\\\").replace('"', "\\\"");
463 Response::builder()
464 .status(StatusCode::UNAUTHORIZED)
465 .header(
466 headers::WWW_AUTHENTICATE,
467 format!("Basic realm=\"{}\"", sanitized_realm),
468 )
469 .header(headers::CONTENT_TYPE, "text/plain")
470 .body(Full::new(bytes::Bytes::from("401 Unauthorized")))
471 .unwrap_or_else(|_| Response::new(Full::new(bytes::Bytes::from("401 Unauthorized"))))
472}
473
474fn is_url_pattern_blocked(path: &str, config: &impl ConfigProvider) -> bool {
477 let blocked_patterns = config.blocked_patterns();
478 if blocked_patterns.is_empty() {
479 return false;
480 }
481
482 let decoded_path = url_decode(path);
484 let has_encoding = decoded_path != path;
485
486 let path_lower = path.to_lowercase();
488 let decoded_lower = if has_encoding {
490 Some(decoded_path.to_lowercase())
491 } else {
492 None
493 };
494
495 blocked_patterns.iter().any(|pattern| {
497 path_lower.contains(pattern.as_str())
498 || decoded_lower
499 .as_ref()
500 .is_some_and(|dl| dl.contains(pattern.as_str()))
501 })
502}
503
504fn url_decode(input: &str) -> String {
508 let mut bytes = Vec::with_capacity(input.len());
509 let input_bytes = input.as_bytes();
510 let mut i = 0;
511
512 while i < input_bytes.len() {
513 if input_bytes[i] == b'%' && i + 2 < input_bytes.len() {
514 let hi = hex_digit(input_bytes[i + 1]);
516 let lo = hex_digit(input_bytes[i + 2]);
517 if let (Some(h), Some(l)) = (hi, lo) {
518 bytes.push(h << 4 | l);
519 i += 3;
520 continue;
521 }
522 }
523 bytes.push(input_bytes[i]);
524 i += 1;
525 }
526
527 String::from_utf8_lossy(&bytes).into_owned()
529}
530
531fn hex_digit(b: u8) -> Option<u8> {
533 match b {
534 b'0'..=b'9' => Some(b - b'0'),
535 b'a'..=b'f' => Some(b - b'a' + 10),
536 b'A'..=b'F' => Some(b - b'A' + 10),
537 _ => None,
538 }
539}
540
541fn is_method_blocked(method: &str, config: &impl ConfigProvider) -> bool {
543 let blocked_methods = config.blocked_methods();
544 blocked_methods
545 .iter()
546 .any(|blocked_method| blocked_method.eq_ignore_ascii_case(method))
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use crate::test_utils::TestConfig;
553 use http_body_util::BodyExt;
554
555 #[tokio::test]
556 async fn body_limit_rejects_chunked_stream_without_waiting_for_eof() {
557 use futures::{FutureExt, StreamExt, stream};
558 use hyper::body::Frame;
559
560 let frames = stream::iter([
561 Ok::<_, Infallible>(Frame::data(bytes::Bytes::from_static(b"abc"))),
562 Ok(Frame::data(bytes::Bytes::from_static(b"def"))),
563 ])
564 .chain(stream::pending());
565 let body = http_body_util::StreamBody::new(frames);
566 let result = collect_request_body(body, 5)
567 .now_or_never()
568 .expect("must reject before the stream ends");
569 assert!(matches!(
570 result,
571 Err(WiseGateError::BodyTooLarge { size: 6, max: 5 })
572 ));
573 }
574
575 #[tokio::test]
576 async fn body_limit_accepts_exact_limit_and_zero_remains_unlimited() {
577 for limit in [0, 6] {
578 let result =
579 collect_request_body(Full::new(bytes::Bytes::from_static(b"abcdef")), limit)
580 .await
581 .unwrap();
582 assert_eq!(result.as_ref(), b"abcdef");
583 }
584 }
585
586 #[tokio::test]
587 async fn unfinished_body_times_out_and_closes_connection() {
588 use futures::stream;
589 use hyper::body::Frame;
590
591 let stream = stream::pending::<Result<Frame<bytes::Bytes>, Infallible>>();
592 let err = read_request_body(
593 http_body_util::StreamBody::new(stream),
594 0,
595 std::time::Duration::from_millis(1),
596 )
597 .await
598 .unwrap_err();
599 let response = create_body_error_response(&err);
600 assert_eq!(response.status(), StatusCode::REQUEST_TIMEOUT);
601 assert_eq!(response.headers()[headers::CONNECTION], "close");
602 assert!(!err.is_server_error());
603 }
604
605 #[test]
610 fn test_url_decode_no_encoding() {
611 assert_eq!(url_decode("/path/to/file"), "/path/to/file");
612 assert_eq!(url_decode("hello"), "hello");
613 assert_eq!(url_decode(""), "");
614 }
615
616 #[test]
617 fn test_url_decode_simple_encoding() {
618 assert_eq!(url_decode("%20"), " ");
619 assert_eq!(url_decode("hello%20world"), "hello world");
620 assert_eq!(url_decode("%2F"), "/");
621 }
622
623 #[test]
624 fn test_url_decode_dot_encoding() {
625 assert_eq!(url_decode("%2e"), ".");
627 assert_eq!(url_decode("%2E"), ".");
628 assert_eq!(url_decode(".%2ephp"), "..php");
629 }
630
631 #[test]
632 fn test_url_decode_php_bypass() {
633 assert_eq!(url_decode(".ph%70"), ".php");
635 assert_eq!(url_decode("%2ephp"), ".php");
636 assert_eq!(url_decode(".%70%68%70"), ".php");
637 }
638
639 #[test]
640 fn test_url_decode_env_bypass() {
641 assert_eq!(url_decode(".%65nv"), ".env");
643 assert_eq!(url_decode("%2eenv"), ".env");
644 assert_eq!(url_decode("%2e%65%6e%76"), ".env");
645 }
646
647 #[test]
648 fn test_url_decode_multiple_encodings() {
649 assert_eq!(url_decode("%2F%2e%2e%2Fetc%2Fpasswd"), "/../etc/passwd");
650 }
651
652 #[test]
653 fn test_url_decode_invalid_hex() {
654 assert_eq!(url_decode("%GG"), "%GG");
656 assert_eq!(url_decode("%"), "%");
657 assert_eq!(url_decode("%2"), "%2");
658 assert_eq!(url_decode("%ZZ"), "%ZZ");
659 }
660
661 #[test]
662 fn test_url_decode_mixed_content() {
663 assert_eq!(url_decode("path%2Fto%2Ffile.txt"), "path/to/file.txt");
664 assert_eq!(url_decode("hello%20%26%20world"), "hello & world");
665 }
666
667 #[test]
668 fn test_url_decode_unicode() {
669 assert_eq!(url_decode("%C3%A9"), "é"); assert_eq!(url_decode("caf%C3%A9"), "café");
672 }
673
674 #[test]
679 fn test_url_pattern_blocked_simple() {
680 let config = TestConfig::new().with_blocked_patterns(vec![".php", ".env"]);
681
682 assert!(is_url_pattern_blocked("/file.php", &config));
683 assert!(is_url_pattern_blocked("/.env", &config));
684 assert!(is_url_pattern_blocked("/path/to/file.php", &config));
685 }
686
687 #[test]
688 fn test_url_pattern_not_blocked() {
689 let config = TestConfig::new().with_blocked_patterns(vec![".php", ".env"]);
690
691 assert!(!is_url_pattern_blocked("/file.html", &config));
692 assert!(!is_url_pattern_blocked("/path/to/file.js", &config));
693 assert!(!is_url_pattern_blocked("/", &config));
694 }
695
696 #[test]
697 fn test_url_pattern_blocked_empty_patterns() {
698 let config = TestConfig::new();
699
700 assert!(!is_url_pattern_blocked("/file.php", &config));
701 assert!(!is_url_pattern_blocked("/.env", &config));
702 }
703
704 #[test]
705 fn test_url_pattern_blocked_bypass_attempt() {
706 let config = TestConfig::new().with_blocked_patterns(vec![".php", ".env", "admin"]);
707
708 assert!(is_url_pattern_blocked("/.ph%70", &config)); assert!(is_url_pattern_blocked("/%2eenv", &config)); assert!(is_url_pattern_blocked("/adm%69n", &config)); }
713
714 #[test]
715 fn test_url_pattern_blocked_double_encoding_attempt() {
716 let config = TestConfig::new().with_blocked_patterns(vec![".php"]);
717
718 assert!(is_url_pattern_blocked("/.ph%70", &config));
720 }
721
722 #[test]
723 fn test_url_pattern_blocked_case_insensitive() {
724 let config = TestConfig::new().with_blocked_patterns(vec![".php"]);
725
726 assert!(is_url_pattern_blocked("/file.PHP", &config));
728 assert!(is_url_pattern_blocked("/file.php", &config));
729 assert!(is_url_pattern_blocked("/file.Php", &config));
730 }
731
732 #[test]
733 fn test_url_pattern_blocked_partial_match() {
734 let config = TestConfig::new().with_blocked_patterns(vec!["admin"]);
735
736 assert!(is_url_pattern_blocked("/admin/panel", &config));
737 assert!(is_url_pattern_blocked("/path/admin", &config));
738 assert!(is_url_pattern_blocked("/administrator", &config)); }
740
741 #[test]
746 fn test_method_blocked() {
747 let config = TestConfig::new().with_blocked_methods(vec!["TRACE", "CONNECT"]);
748
749 assert!(is_method_blocked("TRACE", &config));
750 assert!(is_method_blocked("CONNECT", &config));
751 }
752
753 #[test]
754 fn test_method_not_blocked() {
755 let config = TestConfig::new().with_blocked_methods(vec!["TRACE", "CONNECT"]);
756
757 assert!(!is_method_blocked("GET", &config));
758 assert!(!is_method_blocked("POST", &config));
759 assert!(!is_method_blocked("PUT", &config));
760 assert!(!is_method_blocked("DELETE", &config));
761 }
762
763 #[test]
764 fn test_method_blocked_empty_list() {
765 let config = TestConfig::new();
766
767 assert!(!is_method_blocked("TRACE", &config));
768 assert!(!is_method_blocked("GET", &config));
769 }
770
771 #[test]
772 fn test_method_blocked_case_insensitive() {
773 let config = TestConfig::new().with_blocked_methods(vec!["TRACE"]);
774
775 assert!(is_method_blocked("TRACE", &config));
776 assert!(is_method_blocked("trace", &config));
777 assert!(is_method_blocked("Trace", &config));
778 }
779
780 #[test]
785 fn test_create_error_response_status() {
786 let response = create_error_response(StatusCode::NOT_FOUND, "Not Found");
787 assert_eq!(response.status(), StatusCode::NOT_FOUND);
788
789 let response = create_error_response(StatusCode::FORBIDDEN, "Forbidden");
790 assert_eq!(response.status(), StatusCode::FORBIDDEN);
791
792 let response = create_error_response(StatusCode::TOO_MANY_REQUESTS, "Rate limited");
793 assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
794 }
795
796 #[test]
797 fn test_create_error_response_content_type() {
798 let response = create_error_response(StatusCode::NOT_FOUND, "Not Found");
799 assert_eq!(
800 response.headers().get("content-type").unwrap(),
801 "text/plain"
802 );
803 }
804
805 #[tokio::test]
806 async fn test_create_error_response_body() {
807 let response = create_error_response(StatusCode::NOT_FOUND, "Resource not found");
808 let body = response.into_body().collect().await.unwrap().to_bytes();
809 assert_eq!(body, "Resource not found");
810 }
811
812 #[tokio::test]
813 async fn test_create_error_response_empty_message() {
814 let response = create_error_response(StatusCode::NO_CONTENT, "");
815 let body = response.into_body().collect().await.unwrap().to_bytes();
816 assert_eq!(body, "");
817 }
818
819 #[test]
824 fn test_unauthorized_response_status() {
825 let response = create_unauthorized_response("WiseGate");
826 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
827 }
828
829 #[test]
830 fn test_unauthorized_response_www_authenticate_header() {
831 let response = create_unauthorized_response("WiseGate");
832 let header = response
833 .headers()
834 .get("www-authenticate")
835 .unwrap()
836 .to_str()
837 .unwrap();
838 assert_eq!(header, "Basic realm=\"WiseGate\"");
839 }
840
841 #[test]
842 fn test_unauthorized_response_realm_with_quotes() {
843 let response = create_unauthorized_response("My \"Realm\"");
844 let header = response
845 .headers()
846 .get("www-authenticate")
847 .unwrap()
848 .to_str()
849 .unwrap();
850 assert_eq!(header, "Basic realm=\"My \\\"Realm\\\"\"");
851 }
852
853 #[test]
854 fn test_unauthorized_response_realm_with_backslash() {
855 let response = create_unauthorized_response("My\\Realm");
856 let header = response
857 .headers()
858 .get("www-authenticate")
859 .unwrap()
860 .to_str()
861 .unwrap();
862 assert_eq!(header, "Basic realm=\"My\\\\Realm\"");
863 }
864
865 #[test]
866 fn test_unauthorized_response_content_type() {
867 let response = create_unauthorized_response("WiseGate");
868 assert_eq!(
869 response.headers().get("content-type").unwrap(),
870 "text/plain"
871 );
872 }
873
874 #[test]
879 fn test_url_decode_double_encoding_not_decoded_twice() {
880 assert_eq!(url_decode("%252e"), "%2e");
882 assert_eq!(url_decode("%2565nv"), "%65nv");
883 }
884}