1mod account;
28mod client_ip;
29#[cfg(test)]
30mod tests;
31
32use std::net::{IpAddr, SocketAddr};
33use std::sync::Arc;
34use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
35use std::time::{SystemTime, UNIX_EPOCH};
36
37use axum::Json;
38use axum::body::{Body, to_bytes};
39use axum::extract::{ConnectInfo, Request};
40use axum::http::{HeaderMap, HeaderValue, StatusCode};
41use axum::middleware::Next;
42use axum::response::{IntoResponse, Response};
43use chrono::{DateTime, Utc};
44use dashmap::DashMap;
45use serde_json::json;
46use tokio::sync::Mutex;
47use tracing::warn;
48use uuid::Uuid;
49
50use ironflow_auth::cookies::AUTH_COOKIE_NAME;
51use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
52use ironflow_auth::jwt::{AccessToken, JwtConfig, token_hash};
53use ironflow_auth::password;
54use ironflow_store::store::Store;
55
56use self::account::{ACCOUNT_BODY_LIMIT, account_key};
57use self::client_ip::client_ip;
58
59pub use self::account::AccountLimit;
60pub use self::client_ip::{
61 ClientIpHeader, InvalidClientIpHeader, InvalidTrustedProxy, TrustedProxies,
62};
63
64const SWEEP_THRESHOLD: usize = 10_000;
67
68const VERIFIED_KEY_TTL_SECS: u64 = 60;
72
73const KEY_CHECKS_PER_MINUTE: u32 = 10;
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub enum RateLimitKey {
103 ApiKey(Uuid),
105 User(Uuid),
107 Ip(IpAddr),
109 Account {
111 route: String,
114 email: String,
116 },
117 KeyCheck {
124 ip: Option<IpAddr>,
126 api_key: Uuid,
128 },
129}
130
131struct WindowEntry {
132 count: AtomicU32,
133 window_start: AtomicU64,
134}
135
136struct CachedVerification {
138 identity: Option<(Uuid, Option<u32>)>,
142 valid_until: u64,
143}
144
145#[derive(Clone)]
155pub struct RateLimitState {
156 counters: Arc<DashMap<RateLimitKey, WindowEntry>>,
157 verified_keys: Arc<DashMap<String, CachedVerification>>,
159 pending_checks: Arc<DashMap<String, Arc<Mutex<()>>>>,
163 burst: u32,
164}
165
166#[derive(Clone)]
196pub struct RateLimitContext {
197 pub store: Arc<dyn Store>,
199 pub jwt_config: Arc<JwtConfig>,
201 pub limiter: RateLimitState,
203 pub trusted_proxies: TrustedProxies,
205 pub account_limit: AccountLimit,
207}
208
209struct RateLimitResult {
210 limit: u32,
211 remaining: u32,
212 reset: u64,
213 allowed: bool,
214}
215
216pub fn per_minute(requests_per_minute: u32) -> RateLimitState {
232 assert!(requests_per_minute > 0, "burst must be > 0");
233 RateLimitState {
234 counters: Arc::new(DashMap::new()),
235 verified_keys: Arc::new(DashMap::new()),
236 pending_checks: Arc::new(DashMap::new()),
237 burst: requests_per_minute,
238 }
239}
240
241const WINDOW_SECS: u64 = 60;
242
243fn now_epoch() -> u64 {
244 SystemTime::now()
245 .duration_since(UNIX_EPOCH)
246 .expect("system clock before UNIX epoch")
247 .as_secs()
248}
249
250fn check_rate_limit(limiter: &RateLimitState, key: RateLimitKey, limit: u32) -> RateLimitResult {
251 let now = now_epoch();
252
253 if limiter.counters.len() >= SWEEP_THRESHOLD && !limiter.counters.contains_key(&key) {
254 limiter
255 .counters
256 .retain(|_, entry| entry.window_start.load(Ordering::Acquire) + WINDOW_SECS > now);
257 }
258
259 let entry = limiter.counters.entry(key).or_insert_with(|| WindowEntry {
260 count: AtomicU32::new(0),
261 window_start: AtomicU64::new(now),
262 });
263
264 let window_start = entry.window_start.load(Ordering::Acquire);
265 if now >= window_start + WINDOW_SECS
266 && entry
267 .window_start
268 .compare_exchange(window_start, now, Ordering::AcqRel, Ordering::Relaxed)
269 .is_ok()
270 {
271 entry.count.store(0, Ordering::Release);
272 }
273
274 let count = entry.count.fetch_add(1, Ordering::AcqRel) + 1;
275 let reset = entry.window_start.load(Ordering::Acquire) + WINDOW_SECS;
276
277 if count > limit {
278 entry.count.fetch_sub(1, Ordering::AcqRel);
279 RateLimitResult {
280 limit,
281 remaining: 0,
282 reset,
283 allowed: false,
284 }
285 } else {
286 RateLimitResult {
287 limit,
288 remaining: limit.saturating_sub(count),
289 reset,
290 allowed: true,
291 }
292 }
293}
294
295pub async fn rate_limit(mut req: Request, next: Next) -> Response {
312 let ctx = req.extensions_mut().remove::<RateLimitContext>();
313 let Some(ctx) = ctx else {
314 return next.run(req).await;
315 };
316 let peer = req
317 .extensions()
318 .get::<ConnectInfo<SocketAddr>>()
319 .map(|ConnectInfo(addr)| addr.ip());
320 let identified = match extract_rate_limit_key(req.headers(), peer, &ctx).await {
321 Ok(identified) => identified,
322 Err(result) => return too_many_requests(&result),
323 };
324
325 let caller = match identified {
326 Some((key, override_limit)) => {
327 let limit = override_limit.unwrap_or(ctx.limiter.burst);
328 if limit == 0 {
329 return next.run(req).await;
330 }
331 let result = check_rate_limit(&ctx.limiter, key, limit);
332 if !result.allowed {
333 return too_many_requests(&result);
334 }
335 Some(result)
336 }
337 None => None,
338 };
339
340 let (req, result) = match ctx.account_limit {
341 AccountLimit::Off => (req, caller),
342 AccountLimit::ByEmail => {
343 let (parts, body) = req.into_parts();
344 let Ok(bytes) = to_bytes(body, ACCOUNT_BODY_LIMIT).await else {
345 return payload_too_large();
346 };
347 let account = account_key(parts.uri.path(), &bytes);
351 let req = Request::from_parts(parts, Body::from(bytes));
352 match account {
353 None => (req, caller),
354 Some(key) => {
355 let account = check_rate_limit(&ctx.limiter, key, ctx.limiter.burst);
356 if !account.allowed {
357 return too_many_requests(&account);
358 }
359 let tightest = match caller {
360 Some(caller) if caller.remaining <= account.remaining => caller,
361 _ => account,
362 };
363 (req, Some(tightest))
364 }
365 }
366 }
367 };
368
369 let mut resp = next.run(req).await;
370 if let Some(result) = &result {
371 insert_rate_limit_headers(resp.headers_mut(), result);
372 }
373 resp
374}
375
376fn too_many_requests(result: &RateLimitResult) -> Response {
377 let retry_after = result.reset.saturating_sub(now_epoch()).max(1);
378
379 let body = json!({
380 "error": {
381 "code": "RATE_LIMIT_EXCEEDED",
382 "message": "Too many requests, please try again later",
383 "retry_after_secs": retry_after,
384 }
385 });
386
387 let mut resp = (StatusCode::TOO_MANY_REQUESTS, Json(body)).into_response();
388 resp.headers_mut()
389 .insert("retry-after", HeaderValue::from(retry_after));
390 insert_rate_limit_headers(resp.headers_mut(), result);
391 resp
392}
393
394fn payload_too_large() -> Response {
395 let body = json!({
396 "error": {
397 "code": "PAYLOAD_TOO_LARGE",
398 "message": "Request body too large or unreadable",
399 }
400 });
401 (StatusCode::PAYLOAD_TOO_LARGE, Json(body)).into_response()
402}
403
404fn insert_rate_limit_headers(headers: &mut HeaderMap, result: &RateLimitResult) {
405 headers.insert("x-ratelimit-limit", HeaderValue::from(result.limit));
406 headers.insert("x-ratelimit-remaining", HeaderValue::from(result.remaining));
407 headers.insert("x-ratelimit-reset", HeaderValue::from(result.reset));
408}
409
410async fn extract_rate_limit_key(
421 headers: &HeaderMap,
422 peer: Option<IpAddr>,
423 ctx: &RateLimitContext,
424) -> Result<Option<(RateLimitKey, Option<u32>)>, RateLimitResult> {
425 let ip = client_ip(peer, headers, &ctx.trusted_proxies);
426 let bearer = headers
427 .get("authorization")
428 .and_then(|v| v.to_str().ok())
429 .and_then(|v| v.strip_prefix("Bearer "))
430 .map(|s| s.to_string());
431
432 if let Some(ref token) = bearer {
433 if token.starts_with(API_KEY_PREFIX) {
434 if let Some(found) = try_api_key_identity(token, ip, ctx).await? {
435 return Ok(Some(found));
436 }
437 } else if let Some(key) = try_jwt_identity(token, ctx) {
438 return Ok(Some((key, None)));
439 }
440 }
441
442 if let Some(cookie_header) = headers.get("cookie").and_then(|v| v.to_str().ok()) {
444 for part in cookie_header.split(';') {
445 let cookie = part.trim().strip_prefix(AUTH_COOKIE_NAME);
446 if let Some(value) = cookie.and_then(|rest| rest.strip_prefix('='))
447 && let Some(key) = try_jwt_identity(value, ctx)
448 {
449 return Ok(Some((key, None)));
450 }
451 }
452 }
453
454 Ok(ip.map(|ip| (RateLimitKey::Ip(ip), None)))
455}
456
457enum Verification {
459 Valid {
461 id: Uuid,
462 rate_limit_override: Option<u32>,
464 expires_at: Option<DateTime<Utc>>,
465 },
466 Unknown,
470 WrongSecret,
472 Unavailable,
474 Throttled(RateLimitResult),
476}
477
478async fn try_api_key_identity(
484 token: &str,
485 ip: Option<IpAddr>,
486 ctx: &RateLimitContext,
487) -> Result<Option<(RateLimitKey, Option<u32>)>, RateLimitResult> {
488 let now = now_epoch();
489 let hash = token_hash(token);
490 let as_key = |(id, limit): (Uuid, Option<u32>)| (RateLimitKey::ApiKey(id), limit);
491
492 if let Some(identity) = cached_verdict(ctx, &hash, now) {
493 return Ok(identity.map(as_key));
494 }
495
496 let lock = Arc::clone(
498 ctx.limiter
499 .pending_checks
500 .entry(hash.clone())
501 .or_default()
502 .value(),
503 );
504 let _pending = PendingCheck {
505 pending_checks: &ctx.limiter.pending_checks,
506 hash: &hash,
507 lock: Arc::clone(&lock),
508 };
509 let _turn = lock.lock().await;
510 if let Some(identity) = cached_verdict(ctx, &hash, now) {
512 return Ok(identity.map(as_key));
513 }
514
515 let ttl_end = now + VERIFIED_KEY_TTL_SECS;
516 let (identity, valid_until) = match verify_api_key(token, ip, ctx).await {
517 Verification::Valid {
518 id,
519 rate_limit_override,
520 expires_at,
521 } => {
522 let expiry = expires_at.map_or(u64::MAX, |at| at.timestamp().max(0) as u64);
523 (Some((id, rate_limit_override)), ttl_end.min(expiry))
524 }
525 Verification::WrongSecret => (None, ttl_end),
526 Verification::Unknown | Verification::Unavailable => return Ok(None),
527 Verification::Throttled(result) => return Err(result),
528 };
529
530 let verified_keys = &ctx.limiter.verified_keys;
531 if verified_keys.len() >= SWEEP_THRESHOLD && !verified_keys.contains_key(&hash) {
532 verified_keys.retain(|_, cached| cached.valid_until > now);
533 }
534 let verdict = CachedVerification {
535 identity,
536 valid_until,
537 };
538 verified_keys.insert(hash.clone(), verdict);
539 Ok(identity.map(as_key))
540}
541
542fn cached_verdict(
544 ctx: &RateLimitContext,
545 hash: &str,
546 now: u64,
547) -> Option<Option<(Uuid, Option<u32>)>> {
548 ctx.limiter
550 .verified_keys
551 .get(hash)
552 .filter(|cached| cached.valid_until > now)
553 .map(|cached| cached.identity)
554}
555
556struct PendingCheck<'a> {
559 pending_checks: &'a DashMap<String, Arc<Mutex<()>>>,
560 hash: &'a str,
561 lock: Arc<Mutex<()>>,
562}
563
564impl Drop for PendingCheck<'_> {
565 fn drop(&mut self) {
566 self.pending_checks
569 .remove_if(self.hash, |_, lock| Arc::ptr_eq(lock, &self.lock));
570 }
571}
572
573async fn verify_api_key(token: &str, ip: Option<IpAddr>, ctx: &RateLimitContext) -> Verification {
576 let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(API_KEY_SUFFIX_LEN);
577 let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
578
579 let api_key = match ctx.store.find_api_key_by_prefix(prefix).await {
580 Ok(Some(api_key)) => api_key,
581 Ok(None) => return Verification::Unknown,
582 Err(err) => {
583 warn!(error = %err, "rate limiter could not look up an API key");
584 return Verification::Unavailable;
585 }
586 };
587
588 if !api_key.is_active || api_key.expires_at.is_some_and(|at| at <= Utc::now()) {
589 return Verification::Unknown;
590 }
591
592 let check = check_rate_limit(
595 &ctx.limiter,
596 RateLimitKey::KeyCheck {
597 ip,
598 api_key: api_key.id,
599 },
600 KEY_CHECKS_PER_MINUTE,
601 );
602 if !check.allowed {
603 return Verification::Throttled(check);
604 }
605
606 match password::verify(token, &api_key.key_hash) {
607 Ok(true) => {}
608 Ok(false) => return Verification::WrongSecret,
609 Err(err) => {
610 warn!(error = %err, "rate limiter could not verify an API key");
611 return Verification::Unavailable;
612 }
613 }
614
615 let rate_limit_override = match ctx.store.find_user_by_id(api_key.user_id).await {
618 Ok(Some(owner)) if owner.is_admin => api_key.rate_limit_override,
619 Ok(_) => None,
620 Err(err) => {
621 warn!(error = %err, "rate limiter could not look up an API key owner");
622 return Verification::Unavailable;
623 }
624 };
625
626 Verification::Valid {
627 id: api_key.id,
628 rate_limit_override,
629 expires_at: api_key.expires_at,
630 }
631}
632
633fn try_jwt_identity(token: &str, ctx: &RateLimitContext) -> Option<RateLimitKey> {
634 let claims = AccessToken::decode(token, &ctx.jwt_config).ok()?;
635 Some(RateLimitKey::User(claims.user_id))
636}