1use std::collections::{HashMap, VecDeque};
130use std::net::IpAddr;
131use std::num::NonZeroUsize;
132use std::sync::{Arc, Mutex};
133
134use lru::LruCache;
135
136use crate::ip_gate::canonical;
137
138pub const DEFAULT_RATE_LIMIT: u32 = 10;
140pub const DEFAULT_RATE_LIMIT_WINDOW: u64 = 60;
142pub const DEFAULT_ROUTE_RATE_LIMIT_WINDOW: u64 = 60;
145pub const MAX_TRACKED_RATE_LIMIT_KEYS: usize = 10_000;
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub struct RateLimitEntry {
158 pub requests: u32,
160 pub window: u64,
162}
163
164impl RateLimitEntry {
165 pub fn new(requests: u32, window: u64) -> Result<Self, RateLimitConfigError> {
172 if requests == 0 {
173 return Err(RateLimitConfigError {
174 field: "requests".into(),
175 reason: "must be at least 1 request per window",
176 });
177 }
178 if window == 0 {
179 return Err(RateLimitConfigError {
180 field: "window".into(),
181 reason: "must be at least 1 second",
182 });
183 }
184 Ok(Self { requests, window })
185 }
186}
187
188#[derive(Debug, Clone, Default, PartialEq, Eq)]
197pub struct RouteRateLimits {
198 rate_limit: Option<u32>,
200 rate_limit_window: Option<u64>,
203 geo_rate_limits: Option<HashMap<String, RateLimitEntry>>,
207}
208
209impl RouteRateLimits {
210 pub fn new(
220 rate_limit: Option<u32>,
221 rate_limit_window: Option<u64>,
222 geo_rate_limits: Option<HashMap<String, RateLimitEntry>>,
223 ) -> Result<Self, RateLimitConfigError> {
224 if rate_limit.is_some_and(|limit| limit == 0) {
225 return Err(RateLimitConfigError {
226 field: "rate_limit".into(),
227 reason: "must be at least 1 request per window",
228 });
229 }
230 if rate_limit.is_some() && rate_limit_window.is_some_and(|window| window == 0) {
231 return Err(RateLimitConfigError {
232 field: "rate_limit_window".into(),
233 reason: "must be at least 1 second",
234 });
235 }
236 if let Some(geo) = &geo_rate_limits {
237 for (country, entry) in geo {
238 if entry.requests == 0 || entry.window == 0 {
239 return Err(RateLimitConfigError {
240 field: format!("geo_rate_limits[{country}]").into(),
241 reason: "every entry needs at least 1 request per at least 1 second",
242 });
243 }
244 }
245 }
246 Ok(Self {
247 rate_limit,
248 rate_limit_window,
249 geo_rate_limits,
250 })
251 }
252
253 #[must_use]
255 pub const fn rate_limit(&self) -> Option<u32> {
256 self.rate_limit
257 }
258
259 #[must_use]
262 pub const fn rate_limit_window(&self) -> Option<u64> {
263 self.rate_limit_window
264 }
265
266 #[must_use]
268 pub const fn geo_rate_limits(&self) -> Option<&HashMap<String, RateLimitEntry>> {
269 self.geo_rate_limits.as_ref()
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq)]
280pub struct RateLimitConfig {
281 pub enable_rate_limiting: bool,
284 pub rate_limit: u32,
287 pub rate_limit_window: u64,
289 pub enable_rate_limit_auto_ban: bool,
293 pub endpoint_rate_limits: HashMap<String, RateLimitEntry>,
298}
299
300impl Default for RateLimitConfig {
301 fn default() -> Self {
302 Self {
303 enable_rate_limiting: true,
304 rate_limit: DEFAULT_RATE_LIMIT,
305 rate_limit_window: DEFAULT_RATE_LIMIT_WINDOW,
306 enable_rate_limit_auto_ban: false,
307 endpoint_rate_limits: HashMap::new(),
308 }
309 }
310}
311
312#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct RateLimitConfigError {
316 pub field: std::borrow::Cow<'static, str>,
319 pub reason: &'static str,
321}
322
323impl core::fmt::Display for RateLimitConfigError {
324 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
325 write!(f, "invalid {}: {}", self.field, self.reason)
326 }
327}
328
329impl std::error::Error for RateLimitConfigError {}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
333pub struct RateLimitDecision {
334 pub allowed: bool,
338 pub count: u64,
341 pub window: u64,
344}
345
346impl RateLimitDecision {
347 #[must_use]
350 pub const fn retry_after(self) -> u64 {
351 self.window
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum RateLimitTier {
359 Endpoint,
361 Route,
363 Geo,
365 Global,
367}
368
369impl RateLimitTier {
370 #[must_use]
375 pub const fn name(self) -> &'static str {
376 match self {
377 Self::Endpoint => "endpoint",
378 Self::Route => "route",
379 Self::Geo => "geo",
380 Self::Global => "global",
381 }
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub struct TierDecision {
389 allowed: bool,
393 count: u64,
397 tier: RateLimitTier,
399 window: u64,
401}
402
403impl TierDecision {
404 #[must_use]
406 pub const fn allowed(&self) -> bool {
407 self.allowed
408 }
409
410 #[must_use]
412 pub const fn count(&self) -> u64 {
413 self.count
414 }
415
416 #[must_use]
418 pub const fn tier(&self) -> RateLimitTier {
419 self.tier
420 }
421
422 #[must_use]
424 pub const fn tier_name(&self) -> &'static str {
425 self.tier.name()
426 }
427
428 #[must_use]
431 pub const fn retry_after(&self) -> u64 {
432 self.window
433 }
434}
435
436pub type Clock = Arc<dyn Fn() -> f64 + Send + Sync>;
439
440#[derive(Debug, PartialEq, Eq, Hash)]
441struct WindowKey {
442 ip: IpAddr,
443 endpoint: Option<String>,
444}
445
446pub struct RateLimiter {
452 config: RateLimitConfig,
453 windows: Arc<Mutex<LruCache<WindowKey, VecDeque<f64>>>>,
454 clock: Clock,
455 store: Option<Arc<dyn crate::distributed::SlidingWindowStore>>,
456 redis_prefix: String,
457 redis_fail_open: bool,
458}
459
460impl Clone for RateLimiter {
461 fn clone(&self) -> Self {
465 Self {
466 config: self.config.clone(),
467 windows: Arc::clone(&self.windows),
468 clock: Arc::clone(&self.clock),
469 store: self.store.clone(),
470 redis_prefix: self.redis_prefix.clone(),
471 redis_fail_open: self.redis_fail_open,
472 }
473 }
474}
475
476impl core::fmt::Debug for RateLimiter {
477 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
478 f.debug_struct("RateLimiter")
479 .field("config", &self.config)
480 .finish_non_exhaustive()
481 }
482}
483
484#[must_use]
488pub fn system_clock() -> f64 {
489 std::time::SystemTime::now()
490 .duration_since(std::time::UNIX_EPOCH)
491 .map_or(0.0, |d| d.as_secs_f64())
492}
493
494fn new_window_store() -> Arc<Mutex<LruCache<WindowKey, VecDeque<f64>>>> {
497 Arc::new(Mutex::new(LruCache::new(
498 NonZeroUsize::new(MAX_TRACKED_RATE_LIMIT_KEYS).expect("constant above zero"),
499 )))
500}
501
502impl RateLimiter {
503 pub fn new(config: RateLimitConfig) -> Result<Self, RateLimitConfigError> {
512 Self::with_config_and_clock(config, Arc::new(system_clock))
513 }
514
515 pub fn with_config_and_clock(
526 config: RateLimitConfig,
527 clock: Clock,
528 ) -> Result<Self, RateLimitConfigError> {
529 if config.rate_limit == 0 {
530 return Err(RateLimitConfigError {
531 field: "rate_limit".into(),
532 reason: "must be at least 1 request per window",
533 });
534 }
535 if config.rate_limit_window == 0 {
536 return Err(RateLimitConfigError {
537 field: "rate_limit_window".into(),
538 reason: "must be at least 1 second",
539 });
540 }
541 for (path, entry) in &config.endpoint_rate_limits {
545 if entry.requests == 0 || entry.window == 0 {
546 return Err(RateLimitConfigError {
547 field: format!("endpoint_rate_limits[{path}]").into(),
548 reason: "every entry needs at least 1 request per at least 1 second",
549 });
550 }
551 }
552 Ok(Self {
553 config,
554 windows: new_window_store(),
555 clock,
556 store: None,
557 redis_prefix: String::from("guard_core:"),
558 redis_fail_open: false,
559 })
560 }
561
562 #[must_use]
578 pub fn with_distributed_store(
579 mut self,
580 store: Arc<dyn crate::distributed::SlidingWindowStore>,
581 redis_prefix: &str,
582 redis_fail_open: bool,
583 ) -> Self {
584 self.store = Some(store);
585 redis_prefix.clone_into(&mut self.redis_prefix);
586 self.redis_fail_open = redis_fail_open;
587 self
588 }
589
590 pub fn check_distributed(
598 &self,
599 ip: IpAddr,
600 endpoint: Option<&str>,
601 ) -> Result<RateLimitDecision, crate::distributed::StoreError> {
602 if !self.config.enable_rate_limiting {
603 return Ok(RateLimitDecision {
604 allowed: true,
605 count: 0,
606 window: self.config.rate_limit_window,
607 });
608 }
609 let (allowed, count, window) = self.record_and_decide_distributed(
610 ip,
611 endpoint,
612 self.config.rate_limit,
613 self.config.rate_limit_window,
614 )?;
615 Ok(RateLimitDecision {
616 allowed,
617 count,
618 window,
619 })
620 }
621
622 pub fn check_tiers_distributed(
631 &self,
632 ip: IpAddr,
633 url_path: Option<&str>,
634 route: Option<&RouteRateLimits>,
635 country_of_ip: Option<&dyn Fn(IpAddr) -> Option<String>>,
636 ) -> Result<TierDecision, crate::distributed::StoreError> {
637 let inert = TierDecision {
638 allowed: true,
639 count: 0,
640 tier: RateLimitTier::Global,
641 window: self.config.rate_limit_window,
642 };
643 if !self.config.enable_rate_limiting {
644 return Ok(inert);
645 }
646 if let Some(path) = url_path {
647 if let Some(entry) = self.config.endpoint_rate_limits.get(path) {
648 let (allowed, count, window) = self.record_and_decide_distributed(
649 ip,
650 Some(path),
651 entry.requests,
652 entry.window,
653 )?;
654 if !allowed {
655 return Ok(TierDecision {
656 allowed,
657 count,
658 tier: RateLimitTier::Endpoint,
659 window,
660 });
661 }
662 }
663 if let Some(route) = route {
664 if let Some(limit) = route.rate_limit() {
665 let window = route
666 .rate_limit_window()
667 .unwrap_or(DEFAULT_ROUTE_RATE_LIMIT_WINDOW);
668 let (allowed, count, window) =
669 self.record_and_decide_distributed(ip, Some(path), limit, window)?;
670 if !allowed {
671 return Ok(TierDecision {
672 allowed,
673 count,
674 tier: RateLimitTier::Route,
675 window,
676 });
677 }
678 }
679 if let (Some(limits), Some(country_of_ip)) =
680 (route.geo_rate_limits(), country_of_ip)
681 {
682 let country = country_of_ip(ip);
683 let entry = country
684 .as_ref()
685 .and_then(|code| limits.get(code))
686 .or_else(|| limits.get("*"));
687 if let Some(entry) = entry {
688 let (allowed, count, window) = self.record_and_decide_distributed(
689 ip,
690 Some(path),
691 entry.requests,
692 entry.window,
693 )?;
694 if !allowed {
695 return Ok(TierDecision {
696 allowed,
697 count,
698 tier: RateLimitTier::Geo,
699 window,
700 });
701 }
702 }
703 }
704 }
705 }
706 let (allowed, count, window) = self.record_and_decide_distributed(
707 ip,
708 None,
709 self.config.rate_limit,
710 self.config.rate_limit_window,
711 )?;
712 Ok(TierDecision {
713 allowed,
714 count,
715 tier: RateLimitTier::Global,
716 window,
717 })
718 }
719
720 pub fn with_clock(clock: Clock) -> Self {
721 Self {
722 config: RateLimitConfig::default(),
723 windows: new_window_store(),
724 store: None,
725 redis_prefix: String::from("guard_core:"),
726 redis_fail_open: false,
727 clock,
728 }
729 }
730
731 #[must_use]
733 pub const fn config(&self) -> &RateLimitConfig {
734 &self.config
735 }
736
737 #[allow(clippy::significant_drop_tightening)]
745 fn record_and_decide_distributed(
750 &self,
751 ip: IpAddr,
752 endpoint: Option<&str>,
753 limit: u32,
754 window_seconds: u64,
755 ) -> Result<(bool, u64, u64), crate::distributed::StoreError> {
756 let Some(store) = &self.store else {
757 return Ok(self.record_and_decide(ip, endpoint, limit, window_seconds));
758 };
759 let now = (self.clock)();
760 let key = endpoint.map_or_else(
761 || crate::distributed::rate_window_key(&self.redis_prefix, ip),
762 |path| crate::distributed::rate_window_key_endpoint(&self.redis_prefix, ip, path),
763 );
764 match store.record_hit(&key, now, window_seconds) {
765 Ok(count) => {
766 let allowed = count <= u64::from(limit);
767 Ok((allowed, count, window_seconds))
768 }
769 Err(error) if self.redis_fail_open => {
770 let _ = error;
775 Ok(self.record_and_decide(ip, endpoint, limit, window_seconds))
776 }
777 Err(error) => Err(error),
778 }
779 }
780
781 fn record_and_decide(
782 &self,
783 ip: IpAddr,
784 endpoint: Option<&str>,
785 limit: u32,
786 window_seconds: u64,
787 ) -> (bool, u64, u64) {
788 #[allow(clippy::cast_precision_loss)]
791 let window = window_seconds as f64;
792 let now = (self.clock)();
793 let window_start = now - window;
794 let key = WindowKey {
795 ip: canonical(ip),
796 endpoint: endpoint.map(str::to_owned),
797 };
798 let inside = {
800 let mut windows = self.windows.lock().expect("rate window store");
801 let timestamps = windows
802 .try_get_or_insert_mut(key, || Ok::<_, core::convert::Infallible>(VecDeque::new()))
803 .expect("key capacity just reserved");
804 while timestamps
805 .front()
806 .is_some_and(|&recorded| recorded <= window_start)
807 {
808 timestamps.pop_front();
809 }
810 let inside = timestamps.len();
811 timestamps.push_back(now);
812 drop(windows);
813 inside
814 };
815 let allowed = inside < usize::try_from(limit).unwrap_or(usize::MAX);
816 let count = u64::try_from(inside + 1).unwrap_or(u64::MAX);
817 (allowed, count, window_seconds)
818 }
819
820 #[must_use]
828 pub fn check(&self, ip: IpAddr, endpoint: Option<&str>) -> RateLimitDecision {
829 if !self.config.enable_rate_limiting {
830 return RateLimitDecision {
831 allowed: true,
832 count: 0,
833 window: self.config.rate_limit_window,
834 };
835 }
836 let (allowed, count, window) = self.record_and_decide(
837 ip,
838 endpoint,
839 self.config.rate_limit,
840 self.config.rate_limit_window,
841 );
842 RateLimitDecision {
843 allowed,
844 count,
845 window,
846 }
847 }
848
849 #[must_use]
867 pub fn check_tiers(
868 &self,
869 ip: IpAddr,
870 url_path: Option<&str>,
871 route: Option<&RouteRateLimits>,
872 country_of_ip: Option<&dyn Fn(IpAddr) -> Option<String>>,
873 ) -> TierDecision {
874 let inert = TierDecision {
875 allowed: true,
876 count: 0,
877 tier: RateLimitTier::Global,
878 window: self.config.rate_limit_window,
879 };
880 if !self.config.enable_rate_limiting {
881 return inert;
882 }
883
884 if let Some(path) = url_path {
887 if let Some(entry) = self.config.endpoint_rate_limits.get(path) {
888 let (allowed, count, window) =
889 self.record_and_decide(ip, Some(path), entry.requests, entry.window);
890 if !allowed {
891 return TierDecision {
892 allowed,
893 count,
894 tier: RateLimitTier::Endpoint,
895 window,
896 };
897 }
898 }
899
900 if let Some(route) = route {
903 if let Some(limit) = route.rate_limit() {
904 let window = route
905 .rate_limit_window()
906 .unwrap_or(DEFAULT_ROUTE_RATE_LIMIT_WINDOW);
907 let (allowed, count, window) =
908 self.record_and_decide(ip, Some(path), limit, window);
909 if !allowed {
910 return TierDecision {
911 allowed,
912 count,
913 tier: RateLimitTier::Route,
914 window,
915 };
916 }
917 }
918
919 if let (Some(limits), Some(country_of_ip)) =
923 (route.geo_rate_limits(), country_of_ip)
924 {
925 let country = country_of_ip(ip);
926 let entry = country
927 .as_ref()
928 .and_then(|code| limits.get(code))
929 .or_else(|| limits.get("*"));
930 if let Some(entry) = entry {
931 let (allowed, count, window) =
932 self.record_and_decide(ip, Some(path), entry.requests, entry.window);
933 if !allowed {
934 return TierDecision {
935 allowed,
936 count,
937 tier: RateLimitTier::Geo,
938 window,
939 };
940 }
941 }
942 }
943 }
944 }
945
946 let (allowed, count, window) = self.record_and_decide(
949 ip,
950 None,
951 self.config.rate_limit,
952 self.config.rate_limit_window,
953 );
954 TierDecision {
955 allowed,
956 count,
957 tier: RateLimitTier::Global,
958 window,
959 }
960 }
961
962 pub fn reset(&self) {
965 self.windows.lock().expect("rate window store").clear();
966 }
967
968 #[must_use]
971 pub fn tracked_windows(&self) -> usize {
972 self.windows.lock().expect("rate window store").len()
973 }
974}
975
976#[cfg(test)]
977mod tests {
978 use super::*;
979 use std::collections::HashMap;
980 use std::str::FromStr;
981 use std::sync::Arc;
982 use std::sync::atomic::{AtomicU64, Ordering};
983
984 use crate::distributed::MemoryStore;
985
986 #[derive(Clone, Default)]
989 struct FakeClock(Arc<AtomicU64>);
990
991 impl FakeClock {
992 fn advance(&self, seconds: u64) {
993 self.0.fetch_add(seconds, Ordering::Relaxed);
994 }
995
996 fn clock(&self) -> Clock {
997 let state = self.0.clone();
998 #[allow(clippy::cast_precision_loss)]
999 Arc::new(move || state.load(Ordering::Relaxed) as f64)
1000 }
1001 }
1002
1003 fn ip(text: &str) -> IpAddr {
1004 IpAddr::from_str(text).expect("test address")
1005 }
1006
1007 fn enabled_config(rate_limit: u32) -> RateLimitConfig {
1008 RateLimitConfig {
1009 enable_rate_limiting: true,
1010 rate_limit,
1011 ..RateLimitConfig::default()
1012 }
1013 }
1014
1015 #[test]
1016 fn default_config_matches_the_reference() {
1017 let config = RateLimitConfig::default();
1018 assert!(config.enable_rate_limiting);
1019 assert_eq!(config.rate_limit, 10);
1020 assert_eq!(config.rate_limit_window, 60);
1021 assert!(!config.enable_rate_limit_auto_ban);
1022 }
1023
1024 #[test]
1025 fn new_fails_closed_on_zero_limit_or_window() {
1026 let error = RateLimiter::new(RateLimitConfig {
1027 rate_limit: 0,
1028 ..RateLimitConfig::default()
1029 })
1030 .unwrap_err();
1031 assert_eq!(error.field, "rate_limit");
1032 assert_eq!(error.field, std::borrow::Cow::Borrowed("rate_limit"));
1034 assert_eq!(
1035 error.to_string(),
1036 "invalid rate_limit: must be at least 1 request per window"
1037 );
1038
1039 let error = RateLimiter::new(RateLimitConfig {
1040 enable_rate_limiting: true,
1041 rate_limit: 5,
1042 rate_limit_window: 0,
1043 enable_rate_limit_auto_ban: false,
1044 endpoint_rate_limits: std::collections::HashMap::new(),
1045 })
1046 .unwrap_err();
1047 assert_eq!(error.field, "rate_limit_window");
1048 }
1049
1050 #[test]
1051 fn new_fails_closed_on_invalid_endpoint_entries() {
1052 let error = RateLimiter::new(RateLimitConfig {
1053 endpoint_rate_limits: std::iter::once((
1054 "/login".to_owned(),
1055 RateLimitEntry {
1056 requests: 0,
1057 window: 60,
1058 },
1059 ))
1060 .collect(),
1061 ..RateLimitConfig::default()
1062 })
1063 .unwrap_err();
1064 assert_eq!(error.field, "endpoint_rate_limits[/login]");
1065 assert_eq!(
1066 error.to_string(),
1067 "invalid endpoint_rate_limits[/login]: every entry needs at least 1 request \
1068 per at least 1 second"
1069 );
1070
1071 let error = RateLimitEntry::new(1, 0).unwrap_err();
1072 assert_eq!(error.field, "window");
1073
1074 let error = RouteRateLimits::new(Some(0), Some(60), None).unwrap_err();
1075 assert_eq!(error.field, "rate_limit");
1076
1077 let error = RouteRateLimits::new(Some(10), Some(0), None).unwrap_err();
1078 assert_eq!(error.field, "rate_limit_window");
1079
1080 let error = RouteRateLimits::new(
1081 None,
1082 None,
1083 Some(
1084 std::iter::once((
1085 "RU".to_owned(),
1086 RateLimitEntry {
1087 requests: 1,
1088 window: 0,
1089 },
1090 ))
1091 .collect(),
1092 ),
1093 )
1094 .unwrap_err();
1095 assert_eq!(error.field, "geo_rate_limits[RU]");
1096
1097 assert!(RouteRateLimits::new(None, None, None).is_ok());
1100 assert!(RouteRateLimits::new(None, Some(30), None).is_ok());
1101 }
1102
1103 #[test]
1104 fn disabled_limiter_allows_and_records_nothing() {
1105 let limiter = RateLimiter::new(RateLimitConfig {
1106 enable_rate_limiting: false,
1107 ..RateLimitConfig::default()
1108 })
1109 .expect("valid config");
1110 for _ in 0..50 {
1111 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1112 }
1113 assert_eq!(limiter.tracked_windows(), 0, "no window was recorded");
1114 }
1115
1116 #[test]
1117 fn blocks_at_the_crossing_and_reports_the_crossing_count() {
1118 let limiter = RateLimiter::new(enabled_config(3)).expect("valid config");
1119 for expected in 1..=3 {
1120 let decision = limiter.check(ip("192.0.2.1"), None);
1121 assert!(decision.allowed, "request {expected} must pass");
1122 assert_eq!(decision.count, expected);
1123 }
1124 let decision = limiter.check(ip("192.0.2.1"), None);
1125 assert!(
1126 !decision.allowed,
1127 "the 4th request inside the window blocks"
1128 );
1129 assert_eq!(decision.count, 4);
1130 assert_eq!(decision.retry_after(), 60, "Retry-After is the window");
1131 }
1132
1133 #[test]
1134 fn window_slide_restores_the_budget() {
1135 let fake = FakeClock::default();
1136 let limiter = RateLimiter {
1137 config: enabled_config(2),
1138 windows: Arc::new(Mutex::new(LruCache::new(
1139 NonZeroUsize::new(16).expect("above zero"),
1140 ))),
1141 clock: fake.clock(),
1142 store: None,
1143 redis_prefix: String::from("guard_core:"),
1144 redis_fail_open: false,
1145 };
1146 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1147 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1148 assert!(!limiter.check(ip("192.0.2.1"), None).allowed);
1149
1150 fake.advance(30);
1152 assert!(!limiter.check(ip("192.0.2.1"), None).allowed);
1153 fake.advance(31);
1156 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1157 }
1158
1159 #[test]
1160 fn windows_are_per_ip() {
1161 let limiter = RateLimiter::new(enabled_config(1)).expect("valid config");
1162 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1163 assert!(!limiter.check(ip("192.0.2.1"), None).allowed);
1164 assert!(
1165 limiter.check(ip("192.0.2.2"), None).allowed,
1166 "another IP has its own budget"
1167 );
1168 }
1169
1170 #[test]
1171 fn endpoint_scope_is_isolated_from_the_global_scope() {
1172 let limiter = RateLimiter::new(enabled_config(1)).expect("valid config");
1173 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1174 assert!(
1175 limiter.check(ip("192.0.2.1"), Some("/login")).allowed,
1176 "the endpoint window is a separate budget"
1177 );
1178 assert!(!limiter.check(ip("192.0.2.1"), Some("/login")).allowed);
1179 assert!(
1180 limiter.check(ip("192.0.2.1"), Some("/signup")).allowed,
1181 "another endpoint has its own budget"
1182 );
1183 assert_eq!(limiter.tracked_windows(), 3);
1184 }
1185
1186 #[test]
1187 fn ipv4_mapped_requests_share_the_ipv4_budget() {
1188 let limiter = RateLimiter::new(enabled_config(1)).expect("valid config");
1189 assert!(limiter.check(ip("::ffff:192.0.2.1"), None).allowed);
1190 assert!(
1191 !limiter.check(ip("192.0.2.1"), None).allowed,
1192 "the mapped form must count toward the same window"
1193 );
1194 }
1195
1196 #[test]
1197 fn reset_drops_every_window() {
1198 let limiter = RateLimiter::new(enabled_config(1)).expect("valid config");
1199 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1200 assert!(!limiter.check(ip("192.0.2.1"), None).allowed);
1201 limiter.reset();
1202 assert_eq!(limiter.tracked_windows(), 0);
1203 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1204 }
1205
1206 #[test]
1207 fn large_windows_keep_their_exact_length() {
1208 let limiter = RateLimiter::new(RateLimitConfig {
1211 enable_rate_limiting: true,
1212 rate_limit: 1,
1213 rate_limit_window: u64::from(u32::MAX) + 1,
1214 ..RateLimitConfig::default()
1215 })
1216 .expect("valid config");
1217 assert!(limiter.check(ip("192.0.2.1"), None).allowed);
1218 let decision = limiter.check(ip("192.0.2.1"), None);
1219 assert!(!decision.allowed);
1220 assert_eq!(decision.retry_after(), u64::from(u32::MAX) + 1);
1221 }
1222
1223 fn country_of(code: &'static str) -> impl Fn(IpAddr) -> Option<String> {
1225 move |_ip| Some(code.to_owned())
1226 }
1227
1228 #[test]
1229 fn tierless_check_tiers_collapses_to_the_global_tier() {
1230 let limiter = RateLimiter::new(enabled_config(2)).expect("valid config");
1234 let visitor = ip("192.0.2.40");
1235 assert!(
1236 limiter
1237 .check_tiers(visitor, Some("/x"), None, None)
1238 .allowed()
1239 );
1240 assert!(
1241 limiter
1242 .check_tiers(visitor, Some("/x"), None, None)
1243 .allowed()
1244 );
1245 let blocked = limiter.check_tiers(visitor, Some("/x"), None, None);
1246 assert!(!blocked.allowed());
1247 assert_eq!(blocked.tier(), RateLimitTier::Global);
1248 assert_eq!(blocked.tier_name(), "global");
1249 assert_eq!(blocked.retry_after(), 60);
1250 assert_eq!(limiter.tracked_windows(), 1, "one global window");
1251 }
1252
1253 #[test]
1254 fn endpoint_tier_matches_the_exact_path_and_keys_its_own_window() {
1255 let limiter = RateLimiter::new(RateLimitConfig {
1256 endpoint_rate_limits: std::iter::once((
1257 "/login".to_owned(),
1258 RateLimitEntry::new(1, 60).expect("valid entry"),
1259 ))
1260 .collect(),
1261 ..RateLimitConfig::default()
1262 })
1263 .expect("valid config");
1264 let visitor = ip("192.0.2.41");
1265
1266 assert!(
1269 limiter
1270 .check_tiers(visitor, Some("/login"), None, None)
1271 .allowed()
1272 );
1273 let blocked = limiter.check_tiers(visitor, Some("/login"), None, None);
1274 assert!(!blocked.allowed());
1275 assert_eq!(blocked.tier(), RateLimitTier::Endpoint);
1276 assert_eq!(blocked.retry_after(), 60);
1277
1278 assert!(
1281 limiter
1282 .check_tiers(visitor, Some("/other"), None, None)
1283 .allowed()
1284 );
1285 assert_eq!(
1286 limiter.tracked_windows(),
1287 2,
1288 "the login endpoint window + the shared global window"
1289 );
1290 }
1291
1292 #[test]
1293 fn route_tier_uses_the_decorator_window_default_of_60() {
1294 let fake = FakeClock::default();
1295 let limiter = RateLimiter {
1296 config: enabled_config(100),
1297 windows: Arc::new(Mutex::new(LruCache::new(
1298 NonZeroUsize::new(16).expect("above zero"),
1299 ))),
1300 clock: fake.clock(),
1301 store: None,
1302 redis_prefix: String::from("guard_core:"),
1303 redis_fail_open: false,
1304 };
1305 let route = RouteRateLimits::new(Some(1), None, None).expect("valid route");
1306 let visitor = ip("192.0.2.42");
1307
1308 assert!(
1309 limiter
1310 .check_tiers(visitor, Some("/x"), Some(&route), None)
1311 .allowed()
1312 );
1313 let blocked = limiter.check_tiers(visitor, Some("/x"), Some(&route), None);
1314 assert!(!blocked.allowed());
1315 assert_eq!(blocked.tier(), RateLimitTier::Route);
1316 assert_eq!(
1317 blocked.retry_after(),
1318 60,
1319 "the reference default window (rate_limit_window or 60)"
1320 );
1321
1322 let route = RouteRateLimits::new(Some(1), Some(30), None).expect("valid route");
1324 let blocked = limiter.check_tiers(visitor, Some("/x"), Some(&route), None);
1325 assert!(!blocked.allowed());
1326 assert_eq!(blocked.retry_after(), 30);
1327 }
1328
1329 #[test]
1330 fn geo_tier_resolves_the_country_then_the_star_fallback() {
1331 let limiter = RateLimiter::new(RateLimitConfig {
1332 rate_limit: 100,
1333 ..RateLimitConfig::default()
1334 })
1335 .expect("valid config");
1336 let route = RouteRateLimits::new(
1337 None,
1338 None,
1339 Some(
1340 [
1341 ("RU".to_owned(), RateLimitEntry::new(1, 60).expect("valid")),
1342 ("*".to_owned(), RateLimitEntry::new(2, 45).expect("valid")),
1343 ]
1344 .into_iter()
1345 .collect(),
1346 ),
1347 )
1348 .expect("valid route");
1349
1350 let russian = ip("192.0.2.43");
1352 assert!(
1353 limiter
1354 .check_tiers(russian, Some("/x"), Some(&route), Some(&country_of("RU")))
1355 .allowed()
1356 );
1357 let blocked =
1358 limiter.check_tiers(russian, Some("/x"), Some(&route), Some(&country_of("RU")));
1359 assert!(!blocked.allowed());
1360 assert_eq!(blocked.tier(), RateLimitTier::Geo);
1361 assert_eq!(blocked.retry_after(), 60);
1362
1363 let other = ip("192.0.2.44");
1365 let resolver = country_of("DE");
1366 assert!(
1367 limiter
1368 .check_tiers(other, Some("/x"), Some(&route), Some(&resolver))
1369 .allowed()
1370 );
1371 assert!(
1372 limiter
1373 .check_tiers(other, Some("/x"), Some(&route), Some(&resolver))
1374 .allowed()
1375 );
1376 let blocked = limiter.check_tiers(other, Some("/x"), Some(&route), Some(&resolver));
1377 assert!(!blocked.allowed());
1378 assert_eq!(blocked.tier(), RateLimitTier::Geo);
1379 assert_eq!(blocked.retry_after(), 45);
1380
1381 let unknown: fn(IpAddr) -> Option<String> = |_ip| None;
1384 let third = ip("192.0.2.45");
1385 assert!(
1386 limiter
1387 .check_tiers(third, Some("/x"), Some(&route), Some(&unknown))
1388 .allowed()
1389 );
1390 assert!(
1391 limiter
1392 .check_tiers(third, Some("/x"), Some(&route), Some(&unknown))
1393 .allowed()
1394 );
1395 let blocked = limiter.check_tiers(third, Some("/x"), Some(&route), Some(&unknown));
1396 assert!(!blocked.allowed());
1397 assert_eq!(blocked.tier(), RateLimitTier::Geo);
1398 }
1399
1400 #[test]
1401 fn geo_tier_needs_a_configured_map_and_country_lookup() {
1402 let limiter = RateLimiter::new(RateLimitConfig {
1404 rate_limit: 1,
1405 ..RateLimitConfig::default()
1406 })
1407 .expect("valid config");
1408 let route = RouteRateLimits::new(None, None, Some(std::collections::HashMap::new()))
1409 .expect("valid route");
1410 let visitor = ip("192.0.2.46");
1411 assert!(
1412 limiter
1413 .check_tiers(visitor, Some("/x"), Some(&route), Some(&country_of("RU")))
1414 .allowed()
1415 );
1416 let blocked =
1417 limiter.check_tiers(visitor, Some("/x"), Some(&route), Some(&country_of("RU")));
1418 assert!(!blocked.allowed());
1419 assert_eq!(blocked.tier(), RateLimitTier::Global);
1420
1421 let route = RouteRateLimits::new(
1425 None,
1426 None,
1427 Some(
1428 std::iter::once(("*".to_owned(), RateLimitEntry::new(1, 60).expect("valid")))
1429 .collect(),
1430 ),
1431 )
1432 .expect("valid route");
1433 let fresh = ip("192.0.2.51");
1434 assert!(
1435 limiter
1436 .check_tiers(fresh, Some("/x"), Some(&route), None)
1437 .allowed()
1438 );
1439 let blocked = limiter.check_tiers(fresh, Some("/x"), Some(&route), None);
1440 assert!(!blocked.allowed());
1441 assert_eq!(
1442 blocked.tier(),
1443 RateLimitTier::Global,
1444 "no handler, no geo tier"
1445 );
1446 }
1447
1448 #[test]
1449 fn every_configured_tier_records_so_the_budgets_stay_independent() {
1450 let limiter = RateLimiter::new(RateLimitConfig {
1455 rate_limit: 2,
1456 ..RateLimitConfig::default()
1457 })
1458 .expect("valid config");
1459 let route = RouteRateLimits::new(Some(1_000), Some(60), None).expect("valid route");
1460 let visitor = ip("192.0.2.47");
1461
1462 assert!(
1463 limiter
1464 .check_tiers(visitor, Some("/x"), Some(&route), None)
1465 .allowed()
1466 );
1467 assert!(
1468 limiter
1469 .check_tiers(visitor, Some("/x"), Some(&route), None)
1470 .allowed()
1471 );
1472 let blocked = limiter.check_tiers(visitor, Some("/x"), Some(&route), None);
1473 assert!(!blocked.allowed());
1474 assert_eq!(
1475 blocked.tier(),
1476 RateLimitTier::Global,
1477 "the global tier crossed first"
1478 );
1479 assert_eq!(blocked.count(), 3);
1480 assert_eq!(limiter.tracked_windows(), 2, "route + global windows");
1481
1482 }
1487
1488 #[test]
1489 fn first_blocked_tier_wins_the_response_shape() {
1490 let limiter = RateLimiter::new(RateLimitConfig {
1494 rate_limit: 1,
1495 rate_limit_window: 120,
1496 endpoint_rate_limits: std::iter::once((
1497 "/login".to_owned(),
1498 RateLimitEntry::new(1, 30).expect("valid"),
1499 ))
1500 .collect(),
1501 ..RateLimitConfig::default()
1502 })
1503 .expect("valid config");
1504 let visitor = ip("192.0.2.48");
1505 assert!(
1506 limiter
1507 .check_tiers(visitor, Some("/login"), None, None)
1508 .allowed()
1509 );
1510 let blocked = limiter.check_tiers(visitor, Some("/login"), None, None);
1511 assert!(!blocked.allowed());
1512 assert_eq!(blocked.tier(), RateLimitTier::Endpoint);
1513 assert_eq!(blocked.retry_after(), 30, "the endpoint tier's window");
1514
1515 let blocked = limiter.check_tiers(visitor, Some("/other"), None, None);
1517 assert!(!blocked.allowed());
1518 assert_eq!(blocked.tier(), RateLimitTier::Global);
1519 assert_eq!(blocked.retry_after(), 120);
1520 }
1521
1522 #[test]
1523 fn tiers_without_a_request_path_skip_the_path_keyed_tiers() {
1524 let limiter = RateLimiter::new(RateLimitConfig {
1528 rate_limit: 1,
1529 endpoint_rate_limits: std::iter::once((
1530 "/login".to_owned(),
1531 RateLimitEntry::new(1, 60).expect("valid"),
1532 ))
1533 .collect(),
1534 ..RateLimitConfig::default()
1535 })
1536 .expect("valid config");
1537 let route = RouteRateLimits::new(Some(1), Some(60), None).expect("valid route");
1538 let visitor = ip("192.0.2.49");
1539 assert!(
1540 limiter
1541 .check_tiers(visitor, None, Some(&route), Some(&country_of("RU")))
1542 .allowed()
1543 );
1544 let blocked = limiter.check_tiers(visitor, None, Some(&route), Some(&country_of("RU")));
1545 assert!(!blocked.allowed());
1546 assert_eq!(blocked.tier(), RateLimitTier::Global);
1547 }
1548
1549 #[test]
1550 fn disabled_limiter_skips_the_tiers_without_recording() {
1551 let limiter = RateLimiter::new(RateLimitConfig {
1552 enable_rate_limiting: false,
1553 endpoint_rate_limits: std::iter::once((
1554 "/login".to_owned(),
1555 RateLimitEntry::new(1, 60).expect("valid"),
1556 ))
1557 .collect(),
1558 ..RateLimitConfig::default()
1559 })
1560 .expect("valid config");
1561 let route = RouteRateLimits::new(Some(1), Some(60), None).expect("valid route");
1562 let visitor = ip("192.0.2.50");
1563 for _ in 0..10 {
1564 let decision = limiter.check_tiers(visitor, Some("/login"), Some(&route), None);
1565 assert!(decision.allowed());
1566 }
1567 assert_eq!(limiter.tracked_windows(), 0, "nothing was recorded");
1568 }
1569
1570 #[test]
1571 fn zero_request_entries_are_rejected() {
1572 assert_eq!(RateLimitEntry::new(0, 60).err().unwrap().field, "requests");
1573 }
1574
1575 #[test]
1576 fn tier_names_read_like_the_reference_reasons() {
1577 assert_eq!(RateLimitTier::Endpoint.name(), "endpoint");
1578 assert_eq!(RateLimitTier::Route.name(), "route");
1579 assert_eq!(RateLimitTier::Geo.name(), "geo");
1580 assert_eq!(RateLimitTier::Global.name(), "global");
1581 }
1582
1583 #[test]
1584 fn limiter_debug_carries_the_config() {
1585 let limiter = RateLimiter::with_clock(Arc::new(system_clock));
1586 assert!(format!("{limiter:?}").contains("RateLimiter"));
1587 assert_eq!(limiter.config().rate_limit_window, 60);
1588 }
1589
1590 #[test]
1591 fn distributed_checks_short_circuit_when_disabled() {
1592 let limiter = RateLimiter::with_config_and_clock(
1593 RateLimitConfig {
1594 enable_rate_limiting: false,
1595 ..RateLimitConfig::default()
1596 },
1597 Arc::new(system_clock),
1598 )
1599 .expect("config");
1600 let decision = limiter
1601 .check_distributed(ip("192.0.2.51"), None)
1602 .expect("inert");
1603 assert!(decision.allowed);
1604
1605 let tiers = limiter
1606 .check_tiers_distributed(ip("192.0.2.51"), Some("/x"), None, None)
1607 .expect("inert");
1608 assert!(tiers.allowed());
1609 }
1610
1611 #[test]
1612 fn distributed_tiers_cover_endpoint_route_and_geo_windows() {
1613 let fake = FakeClock::default();
1614 let store = Arc::new(MemoryStore::default());
1615 let geo_limits = HashMap::from([
1616 ("RU".to_owned(), RateLimitEntry::new(2, 60).unwrap()),
1617 ("*".to_owned(), RateLimitEntry::new(5, 60).unwrap()),
1618 ]);
1619 let route = RouteRateLimits::new(Some(3), Some(30), Some(geo_limits)).expect("valid route");
1620 let limiter = RateLimiter::with_config_and_clock(
1621 RateLimitConfig {
1622 enable_rate_limiting: true,
1623 rate_limit: 100,
1624 endpoint_rate_limits: std::iter::once((
1625 "/api".to_owned(),
1626 RateLimitEntry::new(2, 60).unwrap(),
1627 ))
1628 .collect(),
1629 ..RateLimitConfig::default()
1630 },
1631 fake.clock(),
1632 )
1633 .expect("config")
1634 .with_distributed_store(
1635 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1636 "gc:",
1637 false,
1638 );
1639
1640 let country_ru = |probe: IpAddr| -> Option<String> {
1643 if probe == ip("192.0.2.52") {
1644 Some("RU".to_owned())
1645 } else {
1646 None
1647 }
1648 };
1649 let first = limiter
1653 .check_tiers_distributed(
1654 ip("192.0.2.52"),
1655 Some("/api"),
1656 Some(&route),
1657 Some(&country_ru),
1658 )
1659 .expect("store");
1660 assert!(!first.allowed());
1661 assert_eq!(first.tier(), RateLimitTier::Geo);
1662 assert_eq!(first.count(), 3);
1663
1664 let second = limiter
1667 .check_tiers_distributed(
1668 ip("192.0.2.53"),
1669 Some("/other"),
1670 Some(&route),
1671 Some(&country_ru),
1672 )
1673 .expect("store");
1674 assert!(second.allowed());
1675
1676 let third = limiter
1679 .check_tiers_distributed(
1680 ip("192.0.2.52"),
1681 Some("/other"),
1682 Some(&route),
1683 Some(&country_ru),
1684 )
1685 .expect("store");
1686 assert!(third.allowed());
1687 }
1688
1689 #[test]
1690 fn a_store_error_propagates_from_the_endpoint_tier() {
1691 let store = Arc::new(MemoryStore::default());
1692 let limiter = RateLimiter::with_config_and_clock(
1693 RateLimitConfig {
1694 enable_rate_limiting: true,
1695 rate_limit: 100,
1696 endpoint_rate_limits: std::iter::once((
1697 "/api".to_owned(),
1698 RateLimitEntry::new(2, 60).unwrap(),
1699 ))
1700 .collect(),
1701 ..RateLimitConfig::default()
1702 },
1703 FakeClock::default().clock(),
1704 )
1705 .expect("config")
1706 .with_distributed_store(
1707 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1708 "gc:",
1709 false,
1710 );
1711 store.fail.store(true, std::sync::atomic::Ordering::Relaxed);
1712 assert!(
1713 limiter
1714 .check_tiers_distributed(ip("192.0.2.60"), Some("/api"), None, None)
1715 .is_err()
1716 );
1717 }
1718
1719 #[test]
1720 fn a_store_error_propagates_from_the_route_tier() {
1721 let store = Arc::new(MemoryStore::default());
1722 let route = RouteRateLimits::new(Some(3), Some(60), None).expect("valid route");
1723 let limiter = RateLimiter::with_config_and_clock(
1724 RateLimitConfig {
1725 enable_rate_limiting: true,
1726 ..RateLimitConfig::default()
1727 },
1728 FakeClock::default().clock(),
1729 )
1730 .expect("config")
1731 .with_distributed_store(
1732 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1733 "gc:",
1734 false,
1735 );
1736 store.fail.store(true, std::sync::atomic::Ordering::Relaxed);
1737 assert!(
1740 limiter
1741 .check_tiers_distributed(ip("192.0.2.59"), Some("/r"), Some(&route), None)
1742 .is_err()
1743 );
1744 }
1745
1746 #[test]
1747 fn a_store_error_propagates_from_the_geo_tier() {
1748 let store = Arc::new(MemoryStore::default());
1749 let geo_limits = HashMap::from([("RU".to_owned(), RateLimitEntry::new(2, 60).unwrap())]);
1750 let route = RouteRateLimits::new(None, None, Some(geo_limits)).expect("valid route");
1751 let limiter = RateLimiter::with_config_and_clock(
1752 RateLimitConfig {
1753 enable_rate_limiting: true,
1754 ..RateLimitConfig::default()
1755 },
1756 FakeClock::default().clock(),
1757 )
1758 .expect("config")
1759 .with_distributed_store(
1760 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1761 "gc:",
1762 false,
1763 );
1764 store.fail.store(true, std::sync::atomic::Ordering::Relaxed);
1765 assert!(
1768 limiter
1769 .check_tiers_distributed(
1770 ip("192.0.2.61"),
1771 Some("/other"),
1772 Some(&route),
1773 Some(&|_| Some("RU".to_owned())),
1774 )
1775 .is_err()
1776 );
1777 }
1778
1779 #[test]
1780 fn a_geo_limit_miss_falls_through_to_the_global_tier() {
1781 let store = Arc::new(MemoryStore::default());
1782 let geo_limits = HashMap::from([("CN".to_owned(), RateLimitEntry::new(2, 60).unwrap())]);
1783 let route = RouteRateLimits::new(None, None, Some(geo_limits)).expect("valid route");
1784 let limiter = RateLimiter::with_config_and_clock(
1785 RateLimitConfig {
1786 enable_rate_limiting: true,
1787 ..RateLimitConfig::default()
1788 },
1789 FakeClock::default().clock(),
1790 )
1791 .expect("config")
1792 .with_distributed_store(
1793 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1794 "gc:",
1795 false,
1796 );
1797 let decision = limiter
1799 .check_tiers_distributed(
1800 ip("192.0.2.62"),
1801 Some("/other"),
1802 Some(&route),
1803 Some(&|_| Some("US".to_owned())),
1804 )
1805 .expect("store");
1806 assert!(decision.allowed());
1807 assert_eq!(decision.tier(), RateLimitTier::Global);
1808 }
1809}
1810
1811#[cfg(test)]
1812mod unit_twins {
1813 use super::*;
1814 use crate::distributed::MemoryStore;
1815 use std::str::FromStr;
1816
1817 fn ip(text: &str) -> IpAddr {
1818 IpAddr::from_str(text).expect("test address")
1819 }
1820
1821 fn failing_store_limiter(endpoint: Option<(&str, RateLimitEntry)>) -> RateLimiter {
1824 let store = MemoryStore::default();
1825 store.fail.store(true, std::sync::atomic::Ordering::Relaxed);
1826 let mut endpoint_rate_limits = HashMap::new();
1827 if let Some((path, entry)) = endpoint {
1828 endpoint_rate_limits.insert(path.to_owned(), entry);
1829 }
1830 RateLimiter::with_config_and_clock(
1831 RateLimitConfig {
1832 enable_rate_limiting: true,
1833 rate_limit: 100,
1834 endpoint_rate_limits,
1835 ..RateLimitConfig::default()
1836 },
1837 Arc::new(system_clock),
1838 )
1839 .expect("config")
1840 .with_distributed_store(
1841 Arc::new(store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1842 "gc:",
1843 false,
1844 )
1845 }
1846
1847 #[test]
1848 fn the_endpoint_tier_surfaces_a_failing_backend() {
1849 let limiter = failing_store_limiter(Some((
1850 "/api",
1851 RateLimitEntry::new(2, 60).expect("valid entry"),
1852 )));
1853 assert!(
1854 limiter
1855 .check_tiers_distributed(ip("192.0.2.60"), Some("/api"), None, None)
1856 .is_err()
1857 );
1858 }
1859
1860 #[test]
1861 fn the_geo_tier_surfaces_a_failing_backend() {
1862 let geo_limits = HashMap::from([(
1863 "RU".to_owned(),
1864 RateLimitEntry::new(2, 60).expect("valid entry"),
1865 )]);
1866 let route = RouteRateLimits::new(None, None, Some(geo_limits)).expect("valid route");
1868 let limiter = failing_store_limiter(None);
1869 let country = |probe: IpAddr| -> Option<String> {
1870 if probe == ip("192.0.2.61") {
1871 Some("RU".to_owned())
1872 } else {
1873 None
1874 }
1875 };
1876 assert!(
1879 limiter
1880 .check_tiers_distributed(
1881 ip("192.0.2.61"),
1882 Some("/other"),
1883 Some(&route),
1884 Some(&country),
1885 )
1886 .is_err()
1887 );
1888 assert!(
1891 limiter
1892 .check_tiers_distributed(
1893 ip("192.0.2.62"),
1894 Some("/other"),
1895 Some(&route),
1896 Some(&country),
1897 )
1898 .is_err()
1899 );
1900 }
1901
1902 #[test]
1903 fn exhausted_tiers_answer_their_denials() {
1904 let store = Arc::new(MemoryStore::default());
1906 let geo_limits = HashMap::from([(
1907 "RU".to_owned(),
1908 RateLimitEntry::new(1, 60).expect("valid entry"),
1909 )]);
1910 let limiter = RateLimiter::with_config_and_clock(
1911 RateLimitConfig {
1912 enable_rate_limiting: true,
1913 rate_limit: 1,
1914 endpoint_rate_limits: std::iter::once((
1915 "/api".to_owned(),
1916 RateLimitEntry::new(1, 60).expect("valid entry"),
1917 ))
1918 .collect(),
1919 ..RateLimitConfig::default()
1920 },
1921 Arc::new(system_clock),
1922 )
1923 .expect("config")
1924 .with_distributed_store(
1925 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
1926 "gc:",
1927 false,
1928 );
1929 let country_ru = |probe: IpAddr| -> Option<String> {
1930 if probe == ip("192.0.2.63") || probe == ip("192.0.2.67") {
1931 Some("RU".to_owned())
1932 } else {
1933 None
1934 }
1935 };
1936 let route = RouteRateLimits::new(Some(1), Some(30), None).expect("valid route");
1937 let geo_route = RouteRateLimits::new(None, None, Some(geo_limits)).expect("valid route");
1940
1941 let endpoint = limiter
1943 .check_tiers_distributed(ip("192.0.2.63"), Some("/api"), None, None)
1944 .expect("store");
1945 assert!(endpoint.allowed());
1946 let endpoint = limiter
1947 .check_tiers_distributed(ip("192.0.2.63"), Some("/api"), None, None)
1948 .expect("store");
1949 assert!(!endpoint.allowed());
1950
1951 let route_hit = limiter
1953 .check_tiers_distributed(ip("192.0.2.64"), Some("/route"), Some(&route), None)
1954 .expect("store");
1955 assert!(route_hit.allowed());
1956 let route_hit = limiter
1957 .check_tiers_distributed(ip("192.0.2.64"), Some("/route"), Some(&route), None)
1958 .expect("store");
1959 assert!(!route_hit.allowed());
1960
1961 let geo = limiter
1963 .check_tiers_distributed(
1964 ip("192.0.2.67"),
1965 Some("/geo"),
1966 Some(&geo_route),
1967 Some(&country_ru),
1968 )
1969 .expect("store");
1970 assert!(geo.allowed(), "first geo: {geo:?}");
1971 let skipped = limiter
1973 .check_tiers_distributed(
1974 ip("192.0.2.68"),
1975 Some("/geo"),
1976 Some(&geo_route),
1977 Some(&country_ru),
1978 )
1979 .expect("store");
1980 assert!(skipped.allowed(), "skipped geo: {skipped:?}");
1981 let geo = limiter
1982 .check_tiers_distributed(
1983 ip("192.0.2.67"),
1984 Some("/geo"),
1985 Some(&geo_route),
1986 Some(&country_ru),
1987 )
1988 .expect("store");
1989 assert!(!geo.allowed(), "second geo: {geo:?}");
1990 assert_eq!(geo.tier(), RateLimitTier::Geo);
1991
1992 let global = limiter
1994 .check_tiers_distributed(ip("192.0.2.65"), Some("/other"), None, None)
1995 .expect("store");
1996 assert!(global.allowed());
1997 let global = limiter
1998 .check_tiers_distributed(ip("192.0.2.65"), Some("/other"), None, None)
1999 .expect("store");
2000 assert!(!global.allowed());
2001 assert_eq!(global.tier(), RateLimitTier::Global);
2002 }
2003
2004 #[test]
2005 fn the_global_tier_surfaces_a_failing_backend() {
2006 let limiter = failing_store_limiter(None);
2009 assert!(
2010 limiter
2011 .check_tiers_distributed(ip("192.0.2.66"), Some("/other"), None, None)
2012 .is_err()
2013 );
2014 assert!(
2015 limiter
2016 .check_tiers_distributed(ip("192.0.2.66"), None, None, None)
2017 .is_err()
2018 );
2019 }
2020
2021 #[test]
2022 fn a_geo_tier_without_a_matching_country_falls_through_to_the_global_tier() {
2023 let store = Arc::new(MemoryStore::default());
2026 let geo_limits = HashMap::from([(
2027 "RU".to_owned(),
2028 RateLimitEntry::new(2, 60).expect("valid entry"),
2029 )]);
2030 let route = RouteRateLimits::new(Some(3), Some(30), Some(geo_limits)).expect("valid route");
2031 let limiter = RateLimiter::with_config_and_clock(
2032 RateLimitConfig {
2033 enable_rate_limiting: true,
2034 rate_limit: 100,
2035 ..RateLimitConfig::default()
2036 },
2037 Arc::new(system_clock),
2038 )
2039 .expect("config")
2040 .with_distributed_store(
2041 Arc::clone(&store) as Arc<dyn crate::distributed::SlidingWindowStore>,
2042 "gc:",
2043 false,
2044 );
2045 let no_country = |_: IpAddr| -> Option<String> { None };
2046 let decision = limiter
2047 .check_tiers_distributed(
2048 ip("192.0.2.62"),
2049 Some("/other"),
2050 Some(&route),
2051 Some(&no_country),
2052 )
2053 .expect("store");
2054 assert!(decision.allowed());
2055 assert_eq!(decision.tier(), RateLimitTier::Global);
2056 }
2057}