1use crate::clock::Stamp;
10use std::collections::HashMap;
11use std::sync::Mutex;
12use std::time::Duration;
13
14use axum::extract::Request;
15use axum::http::HeaderValue;
16use axum::middleware::Next;
17use axum::response::{IntoResponse, Response};
18
19use crate::Error;
20use crate::auth::CurrentUser;
21
22const SWEEP_AT: usize = 10_000;
24
25pub(crate) struct Limiter {
26 id: String,
29 max: u32,
30 window: Duration,
31 hits: Mutex<HashMap<String, (u32, Stamp)>>,
32}
33
34pub(crate) enum Verdict {
35 Allowed { remaining: u32 },
36 Limited { retry_after: u64 },
37}
38
39impl Limiter {
40 pub fn new(id: String, max: u32, window: Duration) -> Self {
41 Self {
42 id,
43 max: max.max(1),
44 window,
45 hits: Mutex::new(HashMap::new()),
46 }
47 }
48
49 pub fn hit(&self, key: &str) -> Verdict {
50 let mut hits = self.hits.lock().unwrap_or_else(|e| e.into_inner());
51 if hits.len() >= SWEEP_AT {
52 hits.retain(|_, (_, start)| start.elapsed() < self.window);
53 }
54 let entry = hits.entry(key.to_owned()).or_insert((0, Stamp::now()));
55 if entry.1.elapsed() >= self.window {
56 *entry = (0, Stamp::now());
57 }
58 if entry.0 >= self.max {
59 let left = self.window.saturating_sub(entry.1.elapsed());
60 return Verdict::Limited {
61 retry_after: left.as_secs().max(1),
62 };
63 }
64 entry.0 += 1;
65 Verdict::Allowed {
66 remaining: self.max - entry.0,
67 }
68 }
69
70 pub fn max(&self) -> u32 {
71 self.max
72 }
73}
74
75fn key(req: &Request) -> String {
76 let user = req
77 .extensions()
78 .get::<CurrentUser>()
79 .and_then(|c| c.user.as_ref().map(|u| u.id));
80 let device = req
81 .extensions()
82 .get::<CurrentUser>()
83 .and_then(|c| c.device.as_ref().map(|d| d.key().to_owned()));
84 match (user, device) {
85 (Some(id), _) => format!("user:{id}"),
86 (None, Some(device)) => format!("device:{device}"),
87 (None, None) => match crate::ClientIp::of(req) {
88 Some(ip) => format!("ip:{ip}"),
89 None => "ip:unknown".to_owned(),
90 },
91 }
92}
93
94async fn shared_hit(limiter: &Limiter, db: &crate::db::Db, key: &str) -> Verdict {
97 let key = format!("throttle:{}:{key}", limiter.id);
98 match crate::counters::increment(db, &key, limiter.window).await {
99 Ok((count, ends)) if count > limiter.max => Verdict::Limited {
100 retry_after: crate::counters::seconds_until(ends),
101 },
102 Ok((count, _)) => Verdict::Allowed {
103 remaining: limiter.max - count,
104 },
105 Err(err) => {
106 tracing::warn!(error = ?err, "could not count a rate-limited request");
107 Verdict::Allowed {
108 remaining: limiter.max,
109 }
110 }
111 }
112}
113
114pub(crate) async fn check(limiter: &Limiter, req: Request, next: Next) -> Response {
115 let shared = req
116 .extensions()
117 .get::<crate::AppState>()
118 .filter(|state| state.config.cache_store == crate::CacheStore::Database)
119 .map(|state| state.db.clone());
120 let verdict = match &shared {
121 Some(db) => shared_hit(limiter, db, &key(&req)).await,
122 None => limiter.hit(&key(&req)),
123 };
124 respond(verdict, limiter.max(), req, next).await
125}
126
127#[non_exhaustive]
129pub struct LimitRequest<'a> {
130 pub user: Option<&'a crate::auth::User>,
132 pub device: Option<&'a crate::auth::Device>,
134 pub ip: Option<std::net::IpAddr>,
136 pub method: &'a axum::http::Method,
138 pub path: &'a str,
140 pub headers: &'a axum::http::HeaderMap,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct Limit {
162 max: u32,
163 per: Duration,
164 key: Option<String>,
165 unlimited: bool,
166}
167
168impl Limit {
169 pub fn per_minute(max: u32) -> Self {
171 Self::per(max, Duration::from_secs(60))
172 }
173
174 pub fn per_hour(max: u32) -> Self {
176 Self::per(max, Duration::from_secs(3600))
177 }
178
179 pub fn per(max: u32, per: Duration) -> Self {
181 Self {
182 max: max.max(1),
183 per: per.max(Duration::from_secs(1)),
184 key: None,
185 unlimited: false,
186 }
187 }
188
189 pub fn none() -> Self {
191 Self {
192 max: u32::MAX,
193 per: Duration::from_secs(1),
194 key: None,
195 unlimited: true,
196 }
197 }
198
199 pub fn by(mut self, key: impl Into<String>) -> Self {
202 self.key = Some(key.into());
203 self
204 }
205}
206
207pub(crate) type LimitRule = std::sync::Arc<dyn Fn(&LimitRequest) -> Limit + Send + Sync>;
208
209pub(crate) struct NamedLimiter {
211 pub rule: LimitRule,
212 hits: Mutex<HashMap<String, (u32, Stamp)>>,
213}
214
215impl NamedLimiter {
216 pub fn new(rule: LimitRule) -> Self {
217 Self {
218 rule,
219 hits: Mutex::new(HashMap::new()),
220 }
221 }
222
223 fn hit(&self, key: &str, limit: &Limit) -> Verdict {
224 let mut hits = self.hits.lock().unwrap_or_else(|e| e.into_inner());
225 if hits.len() >= SWEEP_AT {
226 hits.retain(|_, (_, start)| start.elapsed() < Duration::from_secs(24 * 60 * 60));
227 }
228 let entry = hits.entry(key.to_owned()).or_insert((0, Stamp::now()));
229 if entry.1.elapsed() >= limit.per {
230 *entry = (0, Stamp::now());
231 }
232 if entry.0 >= limit.max {
233 let left = limit.per.saturating_sub(entry.1.elapsed());
234 return Verdict::Limited {
235 retry_after: left.as_secs().max(1),
236 };
237 }
238 entry.0 += 1;
239 Verdict::Allowed {
240 remaining: limit.max - entry.0,
241 }
242 }
243}
244
245pub(crate) async fn check_named(name: &str, req: Request, next: Next) -> Response {
247 let Some(state) = req.extensions().get::<crate::AppState>().cloned() else {
248 return next.run(req).await;
249 };
250 let Some(limiter) = state.limiters.get(name) else {
251 return Error::Internal(anyhow::anyhow!(
252 "no rate limiter `{name}`: define it with App::rate_limiter"
253 ))
254 .into_response();
255 };
256 let user = req
257 .extensions()
258 .get::<CurrentUser>()
259 .and_then(|c| c.user.clone());
260 let device = req
261 .extensions()
262 .get::<CurrentUser>()
263 .and_then(|c| c.device.clone());
264 let limit = (limiter.rule)(&LimitRequest {
265 user: user.as_deref(),
266 device: device.as_deref(),
267 ip: crate::ClientIp::of(&req),
268 method: req.method(),
269 path: req.uri().path(),
270 headers: req.headers(),
271 });
272 if limit.unlimited {
273 return next.run(req).await;
274 }
275 let key = format!(
276 "{name}:{}:{}",
277 limit.key.clone().unwrap_or_else(|| key(&req)),
278 limit.per.as_secs()
279 );
280 let verdict = if state.config.cache_store == crate::CacheStore::Database {
281 let shared = Limiter::new(format!("named:{name}"), limit.max, limit.per);
282 shared_hit(&shared, &state.db, &key).await
283 } else {
284 limiter.hit(&key, &limit)
285 };
286 respond(verdict, limit.max, req, next).await
287}
288
289async fn respond(verdict: Verdict, max: u32, req: Request, next: Next) -> Response {
290 match verdict {
291 Verdict::Limited { retry_after } => {
292 let mut res = Error::TooManyRequests.into_response();
293 if let Ok(value) = HeaderValue::from_str(&retry_after.to_string()) {
294 res.headers_mut().insert("retry-after", value);
295 }
296 res
297 }
298 Verdict::Allowed { remaining } => {
299 let mut res = next.run(req).await;
300 let headers = res.headers_mut();
301 headers.insert("x-ratelimit-limit", max.into());
302 headers.insert("x-ratelimit-remaining", remaining.into());
303 res
304 }
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn counts_per_key_and_window() {
314 let limiter = Limiter::new("t".into(), 2, Duration::from_millis(50));
315 assert!(matches!(
316 limiter.hit("a"),
317 Verdict::Allowed { remaining: 1 }
318 ));
319 assert!(matches!(
320 limiter.hit("a"),
321 Verdict::Allowed { remaining: 0 }
322 ));
323 assert!(matches!(
324 limiter.hit("a"),
325 Verdict::Limited { retry_after: 1 }
326 ));
327 assert!(matches!(limiter.hit("b"), Verdict::Allowed { .. }));
328 std::thread::sleep(Duration::from_millis(60));
329 assert!(matches!(
330 limiter.hit("a"),
331 Verdict::Allowed { remaining: 1 }
332 ));
333 }
334
335 #[test]
338 fn old_counters_are_swept_once_there_are_many() {
339 let limiter = Limiter::new("t".into(), 5, Duration::from_secs(60));
340 let named = NamedLimiter::new(std::sync::Arc::new(|_: &LimitRequest| Limit::per_hour(5)));
341 let limit = Limit::per_hour(5);
342 for i in 0..SWEEP_AT {
343 limiter.hit(&format!("ip:{i}"));
344 named.hit(&format!("ip:{i}"), &limit);
345 }
346 crate::clock::with_offset_sync(25 * 60 * 60, || {
349 limiter.hit("late");
350 named.hit("late", &limit);
351 });
352 assert_eq!(limiter.hits.lock().unwrap().len(), 1);
353 assert_eq!(named.hits.lock().unwrap().len(), 1);
354 crate::clock::with_offset_sync(-10, || {
356 assert!(matches!(
357 limiter.hit("late"),
358 Verdict::Allowed { remaining: 3 }
359 ));
360 });
361 }
362
363 #[tokio::test]
364 async fn a_named_limit_outside_the_app_lets_requests_through_and_an_unknown_one_is_a_500() {
365 use tower::ServiceExt;
366 let router = |name: &'static str| {
367 axum::Router::new()
368 .route("/", axum::routing::get(|| async { "ok" }))
369 .layer(axum::middleware::from_fn(move |req, next| {
370 check_named(name, req, next)
371 }))
372 };
373 let request = || {
374 Request::builder()
375 .uri("/")
376 .body(axum::body::Body::empty())
377 .unwrap()
378 };
379 let res = router("api").oneshot(request()).await.unwrap();
381 assert_eq!(res.status(), axum::http::StatusCode::OK);
382 let app = crate::testing::TestApp::new(crate::App::new()).await;
385 let mut req = request();
386 req.extensions_mut().insert(app.state().clone());
387 let res = router("nowhere").oneshot(req).await.unwrap();
388 assert_eq!(res.status(), axum::http::StatusCode::INTERNAL_SERVER_ERROR);
389 }
390}