1use std::collections::HashMap;
4use std::net::IpAddr;
5use std::sync::RwLock;
6use std::time::{Duration, Instant};
7
8use axum::{
9 extract::{ConnectInfo, Request, State},
10 http::StatusCode,
11 middleware::Next,
12 response::{IntoResponse, Response},
13};
14
15#[derive(Debug, Clone)]
17pub struct RateLimitConfig {
18 pub max_requests: u32,
20 pub window: Duration,
22 pub enabled: bool,
24 pub max_tracked_ips: usize,
26}
27
28impl Default for RateLimitConfig {
29 fn default() -> Self {
30 Self {
31 max_requests: 100,
32 window: Duration::from_secs(60),
33 enabled: true,
34 max_tracked_ips: 10000,
35 }
36 }
37}
38
39impl RateLimitConfig {
40 pub fn disabled() -> Self {
42 Self {
43 enabled: false,
44 ..Default::default()
45 }
46 }
47
48 pub fn strict() -> Self {
50 Self {
51 max_requests: 10,
52 window: Duration::from_secs(60),
53 ..Default::default()
54 }
55 }
56
57 pub fn relaxed() -> Self {
59 Self {
60 max_requests: 1000,
61 window: Duration::from_secs(60),
62 ..Default::default()
63 }
64 }
65
66 pub fn custom(max_requests: u32, window_secs: u64) -> Self {
68 Self {
69 max_requests,
70 window: Duration::from_secs(window_secs),
71 ..Default::default()
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78struct RequestRecord {
79 timestamps: Vec<Instant>,
81}
82
83impl RequestRecord {
84 fn new() -> Self {
85 Self {
86 timestamps: Vec::new(),
87 }
88 }
89
90 fn clean_and_count(&mut self, window: Duration) -> u32 {
92 let now = Instant::now();
93 let cutoff = now - window;
94
95 self.timestamps.retain(|&t| t > cutoff);
97
98 self.timestamps.len() as u32
99 }
100
101 fn record(&mut self) -> Instant {
103 let now = Instant::now();
104 self.timestamps.push(now);
105 now
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum RateLimitDecision {
118 Unlimited,
120 Allowed {
122 remaining: u32,
124 charge: RateLimitCharge,
126 },
127 Limited { retry_after: Duration },
129}
130
131impl RateLimitDecision {
132 pub fn remaining(&self) -> Option<u32> {
134 match self {
135 Self::Allowed { remaining, .. } => Some(*remaining),
136 _ => None,
137 }
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct RateLimitCharge(Instant);
150
151#[derive(Debug)]
153pub struct RateLimiter {
154 records: RwLock<HashMap<IpAddr, RequestRecord>>,
155 config: RateLimitConfig,
156 last_cleanup: RwLock<Instant>,
157}
158
159impl RateLimiter {
160 pub fn new(config: RateLimitConfig) -> Self {
162 Self {
163 records: RwLock::new(HashMap::new()),
164 config,
165 last_cleanup: RwLock::new(Instant::now()),
166 }
167 }
168
169 pub fn disabled() -> Self {
171 Self::new(RateLimitConfig::disabled())
172 }
173
174 pub fn is_enabled(&self) -> bool {
176 self.config.enabled
177 }
178
179 pub fn check(&self, ip: IpAddr) -> RateLimitDecision {
181 if !self.config.enabled {
182 return RateLimitDecision::Unlimited;
183 }
184
185 self.maybe_cleanup();
187
188 let mut records = match self.records.write() {
189 Ok(r) => r,
190 Err(_) => return RateLimitDecision::Unlimited,
194 };
195
196 let record = records.entry(ip).or_insert_with(RequestRecord::new);
197 let current_count = record.clean_and_count(self.config.window);
198
199 if current_count >= self.config.max_requests {
200 let oldest = record.timestamps.first().copied();
202 let retry_after = oldest
203 .map(|t| self.config.window.saturating_sub(t.elapsed()))
204 .unwrap_or(self.config.window);
205 return RateLimitDecision::Limited { retry_after };
206 }
207
208 let charge = RateLimitCharge(record.record());
210 let remaining = self.config.max_requests - current_count - 1;
211
212 RateLimitDecision::Allowed { remaining, charge }
213 }
214
215 pub fn refund(&self, ip: IpAddr, charge: RateLimitCharge) {
234 if !self.config.enabled {
235 return;
236 }
237
238 let Ok(mut records) = self.records.write() else {
239 return;
240 };
241
242 if let Some(record) = records.get_mut(&ip) {
243 if let Some(at) = record.timestamps.iter().position(|t| *t == charge.0) {
244 record.timestamps.remove(at);
245 }
246 }
247 }
248
249 fn maybe_cleanup(&self) {
251 let should_cleanup = self
252 .last_cleanup
253 .read()
254 .map(|t| t.elapsed() > self.config.window * 2)
255 .unwrap_or(false);
256
257 if !should_cleanup {
258 return;
259 }
260
261 if let Ok(mut last) = self.last_cleanup.write() {
263 if last.elapsed() <= self.config.window * 2 {
265 return;
266 }
267
268 *last = Instant::now();
269
270 if let Ok(mut records) = self.records.write() {
271 let cutoff = Instant::now() - self.config.window * 2;
272
273 records.retain(|_, record| {
275 record
276 .timestamps
277 .last()
278 .map(|&t| t > cutoff)
279 .unwrap_or(false)
280 });
281
282 if records.len() > self.config.max_tracked_ips {
284 let mut entries: Vec<_> = records
285 .iter()
286 .map(|(ip, r)| (*ip, r.timestamps.last().copied()))
287 .collect();
288
289 entries.sort_by_key(|(_, t)| *t);
290
291 let to_remove = records.len() - self.config.max_tracked_ips;
292 for (ip, _) in entries.into_iter().take(to_remove) {
293 records.remove(&ip);
294 }
295 }
296 }
297 }
298 }
299
300 pub fn stats(&self) -> RateLimitStats {
302 let tracked_ips = self.records.read().map(|r| r.len()).unwrap_or(0);
303 RateLimitStats {
304 tracked_ips,
305 max_requests: self.config.max_requests,
306 window_secs: self.config.window.as_secs(),
307 enabled: self.config.enabled,
308 }
309 }
310}
311
312impl Default for RateLimiter {
313 fn default() -> Self {
314 Self::new(RateLimitConfig::default())
315 }
316}
317
318#[derive(Debug, Clone)]
320pub struct RateLimitStats {
321 pub tracked_ips: usize,
322 pub max_requests: u32,
323 pub window_secs: u64,
324 pub enabled: bool,
325}
326
327pub async fn rate_limit_middleware(
329 State(limiter): State<std::sync::Arc<RateLimiter>>,
330 ConnectInfo(addr): ConnectInfo<std::net::SocketAddr>,
331 mut request: Request,
332 next: Next,
333) -> Response {
334 if request.uri().path() == "/health" {
336 return next.run(request).await;
337 }
338
339 match limiter.check(addr.ip()) {
340 RateLimitDecision::Unlimited => next.run(request).await,
342 RateLimitDecision::Allowed { remaining, charge } => {
343 request.extensions_mut().insert(charge);
348
349 let mut response = next.run(request).await;
350
351 let refused_elsewhere = response.status() == StatusCode::TOO_MANY_REQUESTS;
368 let headers = response.headers_mut();
369 if !refused_elsewhere
370 && !headers.contains_key("X-RateLimit-Limit")
371 && !headers.contains_key("X-RateLimit-Remaining")
372 {
373 headers.insert(
374 "X-RateLimit-Limit",
375 limiter.config.max_requests.to_string().parse().unwrap(),
376 );
377 headers.insert(
378 "X-RateLimit-Remaining",
379 remaining.to_string().parse().unwrap(),
380 );
381 }
382
383 response
384 }
385 RateLimitDecision::Limited { retry_after } => {
386 let mut response = (
387 StatusCode::TOO_MANY_REQUESTS,
388 "Rate limit exceeded. Please try again later.",
389 )
390 .into_response();
391
392 response.headers_mut().insert(
393 "Retry-After",
394 retry_after.as_secs().to_string().parse().unwrap(),
395 );
396 response.headers_mut().insert(
397 "X-RateLimit-Limit",
398 limiter.config.max_requests.to_string().parse().unwrap(),
399 );
400 response
401 .headers_mut()
402 .insert("X-RateLimit-Remaining", "0".parse().unwrap());
403
404 response
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use super::*;
412 use std::net::{Ipv4Addr, Ipv6Addr};
413
414 #[test]
415 fn test_rate_limit_config_default() {
416 let config = RateLimitConfig::default();
417 assert_eq!(config.max_requests, 100);
418 assert_eq!(config.window, Duration::from_secs(60));
419 assert!(config.enabled);
420 }
421
422 #[test]
423 fn test_rate_limit_config_disabled() {
424 let config = RateLimitConfig::disabled();
425 assert!(!config.enabled);
426 }
427
428 #[test]
429 fn test_rate_limit_config_custom() {
430 let config = RateLimitConfig::custom(50, 30);
431 assert_eq!(config.max_requests, 50);
432 assert_eq!(config.window, Duration::from_secs(30));
433 }
434
435 fn allowed(decision: RateLimitDecision) -> bool {
436 matches!(decision, RateLimitDecision::Allowed { .. })
437 }
438
439 fn limited(decision: RateLimitDecision) -> bool {
440 matches!(decision, RateLimitDecision::Limited { .. })
441 }
442
443 #[test]
444 fn test_rate_limiter_allows_requests() {
445 let limiter = RateLimiter::new(RateLimitConfig::custom(5, 60));
446 let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
447
448 for i in 0..5 {
450 let result = limiter.check(ip);
451 assert!(allowed(result), "Request {} should be allowed", i);
452 }
453 }
454
455 #[test]
456 fn test_rate_limiter_blocks_excess() {
457 let limiter = RateLimiter::new(RateLimitConfig::custom(3, 60));
458 let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
459
460 assert!(allowed(limiter.check(ip)));
462 assert!(allowed(limiter.check(ip)));
463 assert!(allowed(limiter.check(ip)));
464
465 assert!(limited(limiter.check(ip)));
467 }
468
469 #[test]
470 fn test_rate_limiter_different_ips() {
471 let limiter = RateLimiter::new(RateLimitConfig::custom(2, 60));
472 let ip1 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
473 let ip2 = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 2));
474
475 assert!(allowed(limiter.check(ip1)));
477 assert!(allowed(limiter.check(ip1)));
478 assert!(limited(limiter.check(ip1))); assert!(allowed(limiter.check(ip2))); assert!(allowed(limiter.check(ip2)));
482 assert!(limited(limiter.check(ip2))); }
484
485 #[test]
493 fn test_rate_limiter_disabled() {
494 let limiter = RateLimiter::disabled();
495 let ip = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
496
497 for _ in 0..100 {
498 assert_eq!(limiter.check(ip), RateLimitDecision::Unlimited);
499 }
500 }
501
502 fn charge_of(decision: RateLimitDecision) -> RateLimitCharge {
504 match decision {
505 RateLimitDecision::Allowed { charge, .. } => charge,
506 other => panic!("expected an allowed decision, got {other:?}"),
507 }
508 }
509
510 #[test]
512 fn a_refund_returns_the_slot_it_was_charged() {
513 let limiter = RateLimiter::new(RateLimitConfig::custom(2, 60));
514 let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7));
515
516 let first = limiter.check(ip);
517 assert_eq!(first.remaining(), Some(1));
518 limiter.refund(ip, charge_of(first));
519
520 assert_eq!(
521 limiter.check(ip).remaining(),
522 Some(1),
523 "the refunded slot is available again"
524 );
525
526 assert!(allowed(limiter.check(ip)));
528 assert!(limited(limiter.check(ip)));
529 }
530
531 #[test]
540 fn a_refund_of_an_expired_charge_takes_nothing_from_anyone_else() {
541 let limiter = RateLimiter::new(RateLimitConfig::custom(2, 1));
543 let ip = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 9));
544
545 let stale = charge_of(limiter.check(ip));
546 std::thread::sleep(Duration::from_millis(1100));
547
548 assert_eq!(limiter.check(ip).remaining(), Some(1));
550
551 limiter.refund(ip, stale);
552
553 assert!(allowed(limiter.check(ip)), "the second slot is still free");
555 assert!(
556 limited(limiter.check(ip)),
557 "an expired charge must not have bought a third"
558 );
559 }
560
561 #[test]
567 fn a_refund_without_a_charge_creates_nothing() {
568 let limiter = RateLimiter::new(RateLimitConfig::custom(1, 60));
569 let unseen = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 8));
570 let elsewhere = charge_of(
571 RateLimiter::new(RateLimitConfig::custom(9, 60))
572 .check(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1))),
573 );
574
575 for _ in 0..5 {
576 limiter.refund(unseen, elsewhere);
577 }
578
579 assert!(allowed(limiter.check(unseen)), "one request is the budget");
580 limiter.refund(unseen, elsewhere);
581 assert!(
582 limited(limiter.check(unseen)),
583 "a charge this limiter never issued banked nothing"
584 );
585 }
586
587 #[test]
588 fn test_rate_limiter_ipv6() {
589 let limiter = RateLimiter::new(RateLimitConfig::custom(2, 60));
590 let ip = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
591
592 assert!(allowed(limiter.check(ip)));
593 assert!(allowed(limiter.check(ip)));
594 assert!(limited(limiter.check(ip)));
595 }
596
597 #[test]
598 fn test_rate_limiter_stats() {
599 let limiter = RateLimiter::new(RateLimitConfig::custom(10, 30));
600 let ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
601
602 limiter.check(ip);
603
604 let stats = limiter.stats();
605 assert_eq!(stats.tracked_ips, 1);
606 assert_eq!(stats.max_requests, 10);
607 assert_eq!(stats.window_secs, 30);
608 assert!(stats.enabled);
609 }
610
611 #[test]
612 fn test_rate_limiter_remaining_count() {
613 let limiter = RateLimiter::new(RateLimitConfig::custom(5, 60));
614 let ip = IpAddr::V4(Ipv4Addr::new(172, 16, 0, 1));
615
616 for expected in [4, 3, 2, 1, 0] {
617 assert_eq!(limiter.check(ip).remaining(), Some(expected));
618 }
619 assert!(limited(limiter.check(ip))); }
621}