1use crate::ctx::Ctx;
6use crate::middleware::{Flow, Middleware, Next};
7use async_trait::async_trait;
8use nagisa_types::id::Peer;
9use std::collections::HashMap;
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, Instant};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum RateLimitScope {
16 PerPeer,
18 Global,
20}
21
22#[derive(Clone)]
24pub struct RateLimit {
25 scope: RateLimitScope,
26 max: f64,
28 per: Duration,
30 buckets: Arc<Mutex<HashMap<Option<Peer>, Bucket>>>,
32}
33
34#[derive(Clone, Copy)]
35struct Bucket {
36 tokens: f64,
37 last: Instant,
38}
39
40impl RateLimit {
41 pub fn per_peer(max: u32, per: Duration) -> Self {
43 RateLimit {
44 scope: RateLimitScope::PerPeer,
45 max: max as f64,
46 per,
47 buckets: Arc::new(Mutex::new(HashMap::new())),
48 }
49 }
50 pub fn global(max: u32, per: Duration) -> Self {
52 RateLimit { scope: RateLimitScope::Global, max: max as f64, per, buckets: Arc::new(Mutex::new(HashMap::new())) }
53 }
54
55 pub fn check(&self, peer: Option<Peer>) -> bool {
58 let key = match self.scope {
59 RateLimitScope::PerPeer => match peer {
60 Some(p) => Some(p),
61 None => return true, },
63 RateLimitScope::Global => None,
64 };
65 let refill_per_sec = self.max / self.per.as_secs_f64();
66 let now = Instant::now();
67 let mut map = self.buckets.lock().expect("ratelimit buckets poisoned");
68 let bucket = map.entry(key).or_insert(Bucket { tokens: self.max, last: now });
69 let elapsed = now.saturating_duration_since(bucket.last).as_secs_f64();
71 bucket.tokens = (bucket.tokens + elapsed * refill_per_sec).min(self.max);
72 bucket.last = now;
73 if bucket.tokens >= 1.0 {
74 bucket.tokens -= 1.0;
75 true
76 } else {
77 false
78 }
79 }
80}
81
82#[async_trait]
83impl Middleware for RateLimit {
84 async fn handle(&self, ctx: Arc<Ctx>, next: Next<'_>) -> Flow {
85 let peer = ctx.event().peer();
86 if self.check(peer) {
87 next.run(ctx).await
88 } else {
89 tracing::debug!(?peer, "rate limited; dropping event");
90 Flow::Stop
91 }
92 }
93}