1use axum::{
4 extract::{ConnectInfo, Request},
5 http::{HeaderMap, HeaderValue, StatusCode, header},
6 middleware::Next,
7 response::Response,
8};
9use std::net::SocketAddr;
10use std::time::{Duration, Instant};
11use std::{
12 future::Future,
13 pin::Pin,
14 task::{Context, Poll},
15};
16use tower::{Layer, Service};
17
18#[derive(Clone)]
20pub struct PjsMiddleware {
21 enable_compression: bool,
22 enable_metrics: bool,
23 max_request_size: usize,
24}
25
26impl PjsMiddleware {
27 pub fn new() -> Self {
29 Self {
30 enable_compression: true,
31 enable_metrics: true,
32 max_request_size: 10 * 1024 * 1024, }
34 }
35
36 pub fn with_compression(mut self, enabled: bool) -> Self {
38 self.enable_compression = enabled;
39 self
40 }
41
42 pub fn with_metrics(mut self, enabled: bool) -> Self {
44 self.enable_metrics = enabled;
45 self
46 }
47
48 pub fn with_max_request_size(mut self, size: usize) -> Self {
50 self.max_request_size = size;
51 self
52 }
53}
54
55impl Default for PjsMiddleware {
56 fn default() -> Self {
57 Self::new()
58 }
59}
60
61impl<S> Layer<S> for PjsMiddleware {
62 type Service = PjsMiddlewareService<S>;
63
64 fn layer(&self, inner: S) -> Self::Service {
65 PjsMiddlewareService {
66 inner,
67 config: self.clone(),
68 }
69 }
70}
71
72#[derive(Clone)]
74pub struct PjsMiddlewareService<S> {
75 inner: S,
76 config: PjsMiddleware,
77}
78
79impl<S> Service<Request> for PjsMiddlewareService<S>
80where
81 S: Service<Request, Response = Response> + Clone + Send + 'static,
82 S::Future: Send + 'static,
83{
84 type Response = Response;
85 type Error = S::Error;
86 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
87
88 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
89 self.inner.poll_ready(cx)
90 }
91
92 fn call(&mut self, request: Request) -> Self::Future {
93 let mut inner = self.inner.clone();
94 let config = self.config.clone();
95
96 Box::pin(async move {
97 let start_time = Instant::now();
98
99 if let Some(content_length) = request.headers().get(header::CONTENT_LENGTH)
101 && let Ok(length_str) = content_length.to_str()
102 && let Ok(length) = length_str.parse::<usize>()
103 && length > config.max_request_size
104 {
105 return Ok(Response::builder()
106 .status(StatusCode::PAYLOAD_TOO_LARGE)
107 .body("Request too large".into())
108 .map_err(|_| Response::new("Failed to build error response".into()))
109 .unwrap_or_else(|err_response| err_response));
110 }
111
112 let mut response = inner.call(request).await?;
114
115 if config.enable_metrics {
117 let duration = start_time.elapsed();
118 if let Ok(duration_value) = HeaderValue::from_str(&duration.as_millis().to_string())
119 {
120 response
121 .headers_mut()
122 .insert("X-PJS-Duration-Ms", duration_value);
123 }
124
125 let version_value = HeaderValue::from_static(env!("CARGO_PKG_VERSION"));
126 response
127 .headers_mut()
128 .insert("X-PJS-Version", version_value);
129 }
130
131 if config.enable_compression {
133 response
134 .headers_mut()
135 .insert("X-PJS-Compression", HeaderValue::from_static("available"));
136 }
137
138 Ok(response)
139 })
140 }
141}
142
143#[derive(Debug, Clone, Default)]
167pub struct TrustedProxyConfig {
168 pub trusted_proxies: Vec<std::net::IpAddr>,
171}
172
173impl TrustedProxyConfig {
174 pub fn new(trusted_proxies: Vec<std::net::IpAddr>) -> Self {
176 Self { trusted_proxies }
177 }
178
179 fn contains(&self, ip: std::net::IpAddr) -> bool {
184 self.trusted_proxies.iter().any(|p| p.to_canonical() == ip)
185 }
186}
187
188#[derive(Debug, Clone)]
190pub struct RateLimitConfig {
191 pub max_requests_per_window: u32,
193 pub window_duration: std::time::Duration,
195 pub trusted_proxies: Option<TrustedProxyConfig>,
198}
199
200impl Default for RateLimitConfig {
201 fn default() -> Self {
202 Self {
203 max_requests_per_window: 100,
204 window_duration: std::time::Duration::from_secs(60),
205 trusted_proxies: None,
206 }
207 }
208}
209
210impl RateLimitConfig {
211 pub fn new(requests_per_minute: u32) -> Self {
213 Self {
214 max_requests_per_window: requests_per_minute,
215 window_duration: std::time::Duration::from_secs(60),
216 trusted_proxies: None,
217 }
218 }
219
220 pub fn with_window(mut self, duration: std::time::Duration) -> Self {
222 self.window_duration = duration;
223 self
224 }
225
226 pub fn with_trusted_proxies(mut self, config: TrustedProxyConfig) -> Self {
228 self.trusted_proxies = Some(config);
229 self
230 }
231}
232
233#[derive(Clone)]
239pub struct RateLimitMiddleware {
240 limiter: std::sync::Arc<crate::security::rate_limit::WebSocketRateLimiter>,
241 trusted_proxies: Option<TrustedProxyConfig>,
242}
243
244impl RateLimitMiddleware {
245 pub fn new(config: RateLimitConfig) -> Self {
252 let trusted_proxies = config.trusted_proxies.clone();
253 let rate_limit_config = crate::security::rate_limit::RateLimitConfig {
254 max_requests_per_window: config.max_requests_per_window,
255 window_duration: config.window_duration,
256 ..Default::default()
257 };
258
259 let limiter = std::sync::Arc::new(crate::security::rate_limit::WebSocketRateLimiter::new(
260 rate_limit_config,
261 ));
262 limiter.spawn_cleanup_task(crate::security::rate_limit::DEFAULT_CLEANUP_INTERVAL);
263
264 Self {
265 limiter,
266 trusted_proxies,
267 }
268 }
269
270 pub fn from_limiter(
277 limiter: std::sync::Arc<crate::security::rate_limit::WebSocketRateLimiter>,
278 ) -> Self {
279 limiter.spawn_cleanup_task(crate::security::rate_limit::DEFAULT_CLEANUP_INTERVAL);
280
281 Self {
282 limiter,
283 trusted_proxies: None,
284 }
285 }
286
287 pub fn with_trusted_proxies(mut self, config: TrustedProxyConfig) -> Self {
289 self.trusted_proxies = Some(config);
290 self
291 }
292}
293
294impl<S> Layer<S> for RateLimitMiddleware {
295 type Service = RateLimitService<S>;
296
297 fn layer(&self, inner: S) -> Self::Service {
298 RateLimitService {
299 inner,
300 limiter: self.limiter.clone(),
301 trusted_proxies: self.trusted_proxies.clone(),
302 }
303 }
304}
305
306#[derive(Clone)]
308pub struct RateLimitService<S> {
309 inner: S,
310 limiter: std::sync::Arc<crate::security::rate_limit::WebSocketRateLimiter>,
311 trusted_proxies: Option<TrustedProxyConfig>,
312}
313
314impl<S> Service<Request> for RateLimitService<S>
315where
316 S: Service<Request, Response = Response> + Clone + Send + 'static,
317 S::Future: Send + 'static,
318{
319 type Response = Response;
320 type Error = S::Error;
321 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
322
323 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
324 self.inner.poll_ready(cx)
325 }
326
327 fn call(&mut self, request: Request) -> Self::Future {
328 let limiter = self.limiter.clone();
329 let trusted_proxies = self.trusted_proxies.clone();
330 let mut inner = self.inner.clone();
331
332 Box::pin(async move {
333 let client_ip = extract_client_ip(&request, trusted_proxies.as_ref());
334
335 match limiter.check_request(client_ip) {
337 Ok(()) => {
338 let response = inner.call(request).await?;
340
341 let mut response = response;
343 add_rate_limit_headers(&mut response, &limiter, client_ip);
344
345 Ok(response)
346 }
347 Err(err) => {
348 let (status, retry_after, error_label) =
349 rate_limit_error_response_parts(&err, &limiter, client_ip);
350
351 let error_body = serde_json::json!({
352 "error": error_label,
353 "message": err.to_string(),
354 "retry_after": retry_after
355 })
356 .to_string();
357
358 let mut response = Response::builder()
359 .status(status)
360 .header(header::CONTENT_TYPE, "application/json")
361 .header("Retry-After", retry_after.to_string())
362 .body(error_body.into())
363 .unwrap_or_else(|_| Response::new(error_label.into()));
364
365 if status != StatusCode::SERVICE_UNAVAILABLE {
372 add_rate_limit_headers(&mut response, &limiter, client_ip);
373 }
374
375 Ok(response)
376 }
377 }
378 })
379 }
380}
381
382fn rate_limit_error_response_parts(
401 err: &crate::security::rate_limit::RateLimitError,
402 limiter: &crate::security::rate_limit::WebSocketRateLimiter,
403 client_ip: std::net::IpAddr,
404) -> (StatusCode, u64, &'static str) {
405 if matches!(
406 err,
407 crate::security::rate_limit::RateLimitError::CapacityExceeded { .. }
408 ) {
409 (
410 StatusCode::SERVICE_UNAVAILABLE,
411 crate::security::rate_limit::DEFAULT_CLEANUP_INTERVAL.as_secs(),
412 "Service Unavailable",
413 )
414 } else {
415 let retry_after = duration_secs_ceil(limiter.reset_after(client_ip)).max(1);
416 (
417 StatusCode::TOO_MANY_REQUESTS,
418 retry_after,
419 "Too Many Requests",
420 )
421 }
422}
423
424fn duration_secs_ceil(d: Duration) -> u64 {
433 d.as_secs().saturating_add(u64::from(d.subsec_nanos() > 0))
434}
435
436fn extract_client_ip(
453 request: &Request,
454 trusted_proxies: Option<&TrustedProxyConfig>,
455) -> std::net::IpAddr {
456 use std::net::{IpAddr, Ipv4Addr};
457
458 let Some(peer) = request
459 .extensions()
460 .get::<ConnectInfo<SocketAddr>>()
461 .map(|ConnectInfo(addr)| addr.ip().to_canonical())
462 else {
463 warn_missing_connect_info();
464 return IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
465 };
466
467 if let Some(proxies) = trusted_proxies
468 && proxies.contains(peer)
469 && let Some(forwarded_ip) = extract_forwarded_ip(request.headers(), proxies)
470 {
471 return forwarded_ip;
472 }
473
474 peer
475}
476
477fn warn_missing_connect_info() {
481 static WARNED: std::sync::Once = std::sync::Once::new();
482 WARNED.call_once(|| {
483 tracing::warn!(
484 "RateLimitMiddleware: request has no ConnectInfo<SocketAddr> extension; \
485 serve the router with `.into_make_service_with_connect_info::<SocketAddr>()` \
486 or every client will share a single rate-limit bucket keyed on 127.0.0.1 \
487 (logged once)"
488 );
489 });
490}
491
492fn extract_forwarded_ip(
513 headers: &HeaderMap,
514 trusted_proxies: &TrustedProxyConfig,
515) -> Option<std::net::IpAddr> {
516 let entries: Vec<&str> = headers
517 .get_all("x-forwarded-for")
518 .iter()
519 .filter_map(|h| h.to_str().ok())
520 .flat_map(|s| s.split(','))
521 .collect();
522
523 for entry in entries.into_iter().rev() {
524 let Ok(ip) = entry.trim().parse::<std::net::IpAddr>() else {
525 break;
526 };
527 let canonical = ip.to_canonical();
528 if !trusted_proxies.contains(canonical) {
529 return Some(canonical);
530 }
531 }
532
533 headers
534 .get("x-real-ip")
535 .and_then(|h| h.to_str().ok())
536 .and_then(|s| s.trim().parse::<std::net::IpAddr>().ok())
537 .map(|ip| ip.to_canonical())
538}
539
540fn add_rate_limit_headers(
542 response: &mut Response,
543 limiter: &crate::security::rate_limit::WebSocketRateLimiter,
544 client_ip: std::net::IpAddr,
545) {
546 use std::time::SystemTime;
547
548 let config = limiter.config();
549
550 response.headers_mut().insert(
551 "X-RateLimit-Limit",
552 HeaderValue::from(config.max_requests_per_window),
553 );
554
555 let remaining = limiter.remaining_for(client_ip);
556 response
557 .headers_mut()
558 .insert("X-RateLimit-Remaining", HeaderValue::from(remaining));
559
560 let reset_after_secs = duration_secs_ceil(limiter.reset_after(client_ip));
564 if let Some(reset_time) = SystemTime::now()
565 .duration_since(SystemTime::UNIX_EPOCH)
566 .ok()
567 .map(|d| d.as_secs().saturating_add(reset_after_secs))
568 {
569 response
570 .headers_mut()
571 .insert("X-RateLimit-Reset", HeaderValue::from(reset_time));
572 }
573}
574
575pub async fn websocket_upgrade_middleware(
577 headers: HeaderMap,
578 request: Request,
579 next: Next,
580) -> Result<Response, StatusCode> {
581 if headers
583 .get(header::UPGRADE)
584 .and_then(|h| h.to_str().ok())
585 .map(|s| s.to_lowercase())
586 == Some("websocket".to_string())
587 {
588 return handle_websocket_upgrade(request).await;
591 }
592
593 Ok(next.run(request).await)
595}
596
597async fn handle_websocket_upgrade(_request: Request) -> Result<Response, StatusCode> {
599 Response::builder()
602 .status(StatusCode::NOT_IMPLEMENTED)
603 .body("WebSocket support coming soon".into())
604 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
605}
606
607pub async fn compression_middleware(headers: HeaderMap, request: Request, next: Next) -> Response {
609 let accepts_compression = headers
610 .get(header::ACCEPT_ENCODING)
611 .and_then(|h| h.to_str().ok())
612 .map(|s| s.contains("gzip") || s.contains("deflate"))
613 .unwrap_or(false);
614
615 let mut response = next.run(request).await;
616
617 if accepts_compression {
619 response.headers_mut().insert(
620 "X-PJS-Compression-Available",
621 HeaderValue::from_static("gzip,deflate"),
622 );
623
624 }
627
628 response
629}
630
631pub async fn security_middleware(request: Request, next: Next) -> Response {
633 let mut response = next.run(request).await;
634
635 let headers = response.headers_mut();
637 headers.insert(
638 "X-Content-Type-Options",
639 HeaderValue::from_static("nosniff"),
640 );
641 headers.insert("X-Frame-Options", HeaderValue::from_static("DENY"));
642 headers.insert(
643 "Content-Security-Policy",
644 HeaderValue::from_static("default-src 'self'"),
645 );
646
647 response
648}
649
650#[derive(Clone)]
652pub struct CircuitBreakerMiddleware {
653 failure_threshold: usize,
654 recovery_timeout_seconds: u64,
655}
656
657impl CircuitBreakerMiddleware {
658 pub fn new() -> Self {
660 Self {
661 failure_threshold: 5,
662 recovery_timeout_seconds: 30,
663 }
664 }
665
666 pub fn with_failure_threshold(mut self, threshold: usize) -> Self {
668 self.failure_threshold = threshold;
669 self
670 }
671
672 pub fn with_recovery_timeout(mut self, seconds: u64) -> Self {
674 self.recovery_timeout_seconds = seconds;
675 self
676 }
677}
678
679impl Default for CircuitBreakerMiddleware {
680 fn default() -> Self {
681 Self::new()
682 }
683}
684
685pub async fn health_check_middleware(request: Request, next: Next) -> Response {
687 let mut response = next.run(request).await;
689
690 response
692 .headers_mut()
693 .insert("X-PJS-Health", HeaderValue::from_static("healthy"));
694
695 response
696}
697
698#[derive(Debug, Clone)]
700pub struct ContentValidationConfig {
701 pub max_content_length: usize,
703
704 pub allowed_content_types: Vec<String>,
706
707 pub require_content_type: bool,
709}
710
711impl Default for ContentValidationConfig {
712 fn default() -> Self {
713 Self {
714 max_content_length: 10 * 1024 * 1024, allowed_content_types: vec![
716 "application/json".to_string(),
717 "application/pjs+json".to_string(),
718 ],
719 require_content_type: true,
720 }
721 }
722}
723
724pub async fn content_validation_middleware(
731 config: ContentValidationConfig,
732 req: Request,
733 next: Next,
734) -> Response {
735 let method = req.method().clone();
737 let headers = req.headers();
738
739 if let Some(content_length_header) = headers.get(header::CONTENT_LENGTH) {
741 match content_length_header.to_str() {
742 Ok(content_length_str) => match content_length_str.parse::<usize>() {
743 Ok(content_length) => {
744 if content_length > config.max_content_length {
745 let error_body = serde_json::json!({
746 "error": "Payload Too Large",
747 "max_size": config.max_content_length,
748 "received_size": content_length
749 })
750 .to_string();
751
752 return Response::builder()
753 .status(StatusCode::PAYLOAD_TOO_LARGE)
754 .header(header::CONTENT_TYPE, "application/json")
755 .body(error_body.into())
756 .unwrap_or_else(|_| Response::new("Payload Too Large".into()));
757 }
758 }
759 Err(_) => {
760 let error_body = serde_json::json!({
761 "error": "Invalid Content-Length header"
762 })
763 .to_string();
764
765 return Response::builder()
766 .status(StatusCode::BAD_REQUEST)
767 .header(header::CONTENT_TYPE, "application/json")
768 .body(error_body.into())
769 .unwrap_or_else(|_| Response::new("Bad Request".into()));
770 }
771 },
772 Err(_) => {
773 let error_body = serde_json::json!({
774 "error": "Invalid Content-Length header encoding"
775 })
776 .to_string();
777
778 return Response::builder()
779 .status(StatusCode::BAD_REQUEST)
780 .header(header::CONTENT_TYPE, "application/json")
781 .body(error_body.into())
782 .unwrap_or_else(|_| Response::new("Bad Request".into()));
783 }
784 }
785 }
786
787 if config.require_content_type && (method == "POST" || method == "PUT" || method == "PATCH") {
789 match headers.get(header::CONTENT_TYPE) {
790 Some(content_type_header) => {
791 let content_type = content_type_header.to_str().unwrap_or("");
792
793 let base_content_type = content_type.split(';').next().unwrap_or("").trim();
795
796 if !config
797 .allowed_content_types
798 .iter()
799 .any(|allowed| base_content_type.eq_ignore_ascii_case(allowed))
800 {
801 let error_body = serde_json::json!({
802 "error": "Unsupported Media Type",
803 "accepted": config.allowed_content_types,
804 "received": content_type
805 })
806 .to_string();
807
808 return Response::builder()
809 .status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
810 .header(header::CONTENT_TYPE, "application/json")
811 .body(error_body.into())
812 .unwrap_or_else(|_| Response::new("Unsupported Media Type".into()));
813 }
814 }
815 None => {
816 let error_body = serde_json::json!({
817 "error": "Unsupported Media Type",
818 "message": "Content-Type header is required for POST/PUT/PATCH requests",
819 "accepted": config.allowed_content_types
820 })
821 .to_string();
822
823 return Response::builder()
824 .status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
825 .header(header::CONTENT_TYPE, "application/json")
826 .body(error_body.into())
827 .unwrap_or_else(|_| Response::new("Unsupported Media Type".into()));
828 }
829 }
830 }
831
832 next.run(req).await
834}
835
836#[cfg(test)]
837mod tests {
838 use super::*;
839
840 #[tokio::test]
841 async fn test_pjs_middleware_creation() {
842 let middleware = PjsMiddleware::new()
843 .with_compression(true)
844 .with_metrics(true)
845 .with_max_request_size(5 * 1024 * 1024);
846
847 assert!(middleware.enable_compression);
848 assert!(middleware.enable_metrics);
849 assert_eq!(middleware.max_request_size, 5 * 1024 * 1024);
850 }
851
852 #[test]
853 fn test_rate_limit_config_default() {
854 let config = RateLimitConfig::default();
855 assert_eq!(config.max_requests_per_window, 100);
856 assert_eq!(config.window_duration, std::time::Duration::from_secs(60));
857 }
858
859 #[test]
860 fn test_rate_limit_config_new() {
861 let config = RateLimitConfig::new(50);
862 assert_eq!(config.max_requests_per_window, 50);
863 }
864
865 #[test]
866 fn test_rate_limit_config_with_window() {
867 let config = RateLimitConfig::new(100).with_window(std::time::Duration::from_secs(30));
868 assert_eq!(config.window_duration, std::time::Duration::from_secs(30));
869 }
870
871 #[tokio::test]
872 async fn test_rate_limit_middleware_creation() {
873 let config = RateLimitConfig::default();
874 let _middleware = RateLimitMiddleware::new(config);
875 }
876
877 #[tokio::test]
878 async fn test_from_limiter_claims_cleanup_spawn() {
879 let limiter = std::sync::Arc::new(crate::security::rate_limit::WebSocketRateLimiter::new(
889 crate::security::rate_limit::RateLimitConfig::default(),
890 ));
891 assert!(!limiter.is_cleanup_task_spawned());
892
893 let _middleware = RateLimitMiddleware::from_limiter(limiter.clone());
894
895 assert!(
896 limiter.is_cleanup_task_spawned(),
897 "RateLimitMiddleware::from_limiter must wire up periodic cleanup"
898 );
899 }
900
901 #[tokio::test]
902 async fn test_new_claims_cleanup_spawn() {
903 let middleware = RateLimitMiddleware::new(RateLimitConfig::default());
904
905 assert!(
906 middleware.limiter.is_cleanup_task_spawned(),
907 "RateLimitMiddleware::new must wire up periodic cleanup"
908 );
909 }
910
911 #[test]
912 fn test_capacity_exceeded_maps_to_503_with_sweep_interval_retry_after() {
913 let err = crate::security::rate_limit::RateLimitError::CapacityExceeded {
914 max: crate::security::rate_limit::MAX_TRACKED_CLIENTS,
915 };
916 let limiter = crate::security::rate_limit::WebSocketRateLimiter::default();
917 let ip = std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1));
918 let (status, retry_after, label) = rate_limit_error_response_parts(&err, &limiter, ip);
919
920 assert_eq!(status, StatusCode::SERVICE_UNAVAILABLE);
921 assert_eq!(
922 retry_after,
923 crate::security::rate_limit::DEFAULT_CLEANUP_INTERVAL.as_secs()
924 );
925 assert_eq!(label, "Service Unavailable");
926 }
927
928 #[test]
929 fn test_other_rate_limit_errors_map_to_429_with_real_reset_derived_retry_after() {
930 let config = crate::security::rate_limit::RateLimitConfig {
931 max_requests_per_window: 1,
932 window_duration: std::time::Duration::from_secs(30),
933 ..Default::default()
934 };
935 let limiter = crate::security::rate_limit::WebSocketRateLimiter::new(config);
936 let ip = std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1));
937
938 limiter.check_request(ip).unwrap();
939 let err = limiter.check_request(ip).unwrap_err();
940
941 let (status, retry_after, label) = rate_limit_error_response_parts(&err, &limiter, ip);
942
943 assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
944 assert_eq!(retry_after, 30);
947 assert_eq!(label, "Too Many Requests");
948 }
949
950 #[test]
951 fn test_duration_secs_ceil_rounds_up_any_positive_remainder() {
952 assert_eq!(duration_secs_ceil(Duration::ZERO), 0);
953 assert_eq!(duration_secs_ceil(Duration::from_secs(1)), 1);
954 assert_eq!(duration_secs_ceil(Duration::from_millis(1)), 1);
955 assert_eq!(duration_secs_ceil(Duration::from_millis(500)), 1);
956 assert_eq!(duration_secs_ceil(Duration::from_millis(1500)), 2);
957 assert_eq!(duration_secs_ceil(Duration::from_nanos(1_000_000_001)), 2);
958 }
959
960 #[test]
961 fn test_duration_secs_ceil_saturates_instead_of_panicking_near_u64_max() {
962 assert_eq!(duration_secs_ceil(Duration::MAX), u64::MAX);
966 }
967
968 #[test]
969 fn test_add_rate_limit_headers_never_panics_with_extreme_window() {
970 let config = crate::security::rate_limit::RateLimitConfig {
976 window_duration: Duration::from_secs(u64::MAX),
977 ..Default::default()
978 };
979 let limiter = crate::security::rate_limit::WebSocketRateLimiter::new(config);
980 let ip = std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1));
981 limiter.check_request(ip).unwrap();
982
983 let mut response = Response::new(axum::body::Body::empty());
984 add_rate_limit_headers(&mut response, &limiter, ip); let reset = response
987 .headers()
988 .get("X-RateLimit-Reset")
989 .and_then(|v| v.to_str().ok())
990 .and_then(|s| s.parse::<u64>().ok())
991 .unwrap();
992 assert_eq!(reset, u64::MAX);
993 }
994
995 #[test]
996 fn test_rate_limit_error_response_parts_sub_second_window_never_reports_zero_retry_after() {
997 let config = crate::security::rate_limit::RateLimitConfig {
1003 max_requests_per_window: 1,
1004 window_duration: Duration::from_millis(200),
1005 ..Default::default()
1006 };
1007 let limiter = crate::security::rate_limit::WebSocketRateLimiter::new(config);
1008 let ip = std::net::IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1));
1009
1010 limiter.check_request(ip).unwrap();
1011 let err = limiter.check_request(ip).unwrap_err();
1012
1013 let (status, retry_after, _label) = rate_limit_error_response_parts(&err, &limiter, ip);
1014
1015 assert_eq!(status, StatusCode::TOO_MANY_REQUESTS);
1016 assert!(retry_after >= 1, "Retry-After must never be 0 on a 429");
1017 }
1018
1019 #[tokio::test]
1020 async fn test_from_limiter_on_already_spawned_limiter_does_not_reclaim() {
1021 let limiter = std::sync::Arc::new(crate::security::rate_limit::WebSocketRateLimiter::new(
1027 crate::security::rate_limit::RateLimitConfig::default(),
1028 ));
1029 limiter.spawn_cleanup_task(crate::security::rate_limit::DEFAULT_CLEANUP_INTERVAL);
1030 assert!(limiter.is_cleanup_task_spawned());
1031
1032 let _middleware = RateLimitMiddleware::from_limiter(limiter.clone());
1033
1034 assert!(limiter.is_cleanup_task_spawned());
1035 }
1036
1037 #[test]
1038 fn test_content_validation_config_default() {
1039 let config = ContentValidationConfig::default();
1040 assert_eq!(config.max_content_length, 10 * 1024 * 1024);
1041 assert_eq!(config.allowed_content_types.len(), 2);
1042 assert!(config.require_content_type);
1043 }
1044}