mod account;
mod client_ip;
#[cfg(test)]
mod tests;
use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use axum::Json;
use axum::body::{Body, to_bytes};
use axum::extract::{ConnectInfo, Request};
use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde_json::json;
use tokio::sync::Mutex;
use tracing::warn;
use uuid::Uuid;
use ironflow_auth::cookies::AUTH_COOKIE_NAME;
use ironflow_auth::extractor::{API_KEY_PREFIX, API_KEY_SUFFIX_LEN};
use ironflow_auth::jwt::{AccessToken, JwtConfig, token_hash};
use ironflow_auth::password;
use ironflow_store::store::Store;
use self::account::{ACCOUNT_BODY_LIMIT, account_key};
use self::client_ip::client_ip;
pub use self::account::AccountLimit;
pub use self::client_ip::{
ClientIpHeader, InvalidClientIpHeader, InvalidTrustedProxy, TrustedProxies,
};
const SWEEP_THRESHOLD: usize = 10_000;
const VERIFIED_KEY_TTL_SECS: u64 = 60;
const KEY_CHECKS_PER_MINUTE: u32 = 10;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RateLimitKey {
ApiKey(Uuid),
User(Uuid),
Ip(IpAddr),
Account {
route: String,
email: String,
},
KeyCheck {
ip: Option<IpAddr>,
api_key: Uuid,
},
}
struct WindowEntry {
count: AtomicU32,
window_start: AtomicU64,
}
struct CachedVerification {
identity: Option<(Uuid, Option<u32>)>,
valid_until: u64,
}
#[derive(Clone)]
pub struct RateLimitState {
counters: Arc<DashMap<RateLimitKey, WindowEntry>>,
verified_keys: Arc<DashMap<String, CachedVerification>>,
pending_checks: Arc<DashMap<String, Arc<Mutex<()>>>>,
burst: u32,
}
#[derive(Clone)]
pub struct RateLimitContext {
pub store: Arc<dyn Store>,
pub jwt_config: Arc<JwtConfig>,
pub limiter: RateLimitState,
pub trusted_proxies: TrustedProxies,
pub account_limit: AccountLimit,
}
struct RateLimitResult {
limit: u32,
remaining: u32,
reset: u64,
allowed: bool,
}
pub fn per_minute(requests_per_minute: u32) -> RateLimitState {
assert!(requests_per_minute > 0, "burst must be > 0");
RateLimitState {
counters: Arc::new(DashMap::new()),
verified_keys: Arc::new(DashMap::new()),
pending_checks: Arc::new(DashMap::new()),
burst: requests_per_minute,
}
}
const WINDOW_SECS: u64 = 60;
fn now_epoch() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_secs()
}
fn check_rate_limit(limiter: &RateLimitState, key: RateLimitKey, limit: u32) -> RateLimitResult {
let now = now_epoch();
if limiter.counters.len() >= SWEEP_THRESHOLD && !limiter.counters.contains_key(&key) {
limiter
.counters
.retain(|_, entry| entry.window_start.load(Ordering::Acquire) + WINDOW_SECS > now);
}
let entry = limiter.counters.entry(key).or_insert_with(|| WindowEntry {
count: AtomicU32::new(0),
window_start: AtomicU64::new(now),
});
let window_start = entry.window_start.load(Ordering::Acquire);
if now >= window_start + WINDOW_SECS
&& entry
.window_start
.compare_exchange(window_start, now, Ordering::AcqRel, Ordering::Relaxed)
.is_ok()
{
entry.count.store(0, Ordering::Release);
}
let count = entry.count.fetch_add(1, Ordering::AcqRel) + 1;
let reset = entry.window_start.load(Ordering::Acquire) + WINDOW_SECS;
if count > limit {
entry.count.fetch_sub(1, Ordering::AcqRel);
RateLimitResult {
limit,
remaining: 0,
reset,
allowed: false,
}
} else {
RateLimitResult {
limit,
remaining: limit.saturating_sub(count),
reset,
allowed: true,
}
}
}
pub async fn rate_limit(mut req: Request, next: Next) -> Response {
let ctx = req.extensions_mut().remove::<RateLimitContext>();
let Some(ctx) = ctx else {
return next.run(req).await;
};
let peer = req
.extensions()
.get::<ConnectInfo<SocketAddr>>()
.map(|ConnectInfo(addr)| addr.ip());
let identified = match extract_rate_limit_key(req.headers(), peer, &ctx).await {
Ok(identified) => identified,
Err(result) => return too_many_requests(&result),
};
let caller = match identified {
Some((key, override_limit)) => {
let limit = override_limit.unwrap_or(ctx.limiter.burst);
if limit == 0 {
return next.run(req).await;
}
let result = check_rate_limit(&ctx.limiter, key, limit);
if !result.allowed {
return too_many_requests(&result);
}
Some(result)
}
None => None,
};
let (req, result) = match ctx.account_limit {
AccountLimit::Off => (req, caller),
AccountLimit::ByEmail => {
let (parts, body) = req.into_parts();
let Ok(bytes) = to_bytes(body, ACCOUNT_BODY_LIMIT).await else {
return payload_too_large();
};
let account = account_key(parts.uri.path(), &bytes);
let req = Request::from_parts(parts, Body::from(bytes));
match account {
None => (req, caller),
Some(key) => {
let account = check_rate_limit(&ctx.limiter, key, ctx.limiter.burst);
if !account.allowed {
return too_many_requests(&account);
}
let tightest = match caller {
Some(caller) if caller.remaining <= account.remaining => caller,
_ => account,
};
(req, Some(tightest))
}
}
}
};
let mut resp = next.run(req).await;
if let Some(result) = &result {
insert_rate_limit_headers(resp.headers_mut(), result);
}
resp
}
fn too_many_requests(result: &RateLimitResult) -> Response {
let retry_after = result.reset.saturating_sub(now_epoch()).max(1);
let body = json!({
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests, please try again later",
"retry_after_secs": retry_after,
}
});
let mut resp = (StatusCode::TOO_MANY_REQUESTS, Json(body)).into_response();
resp.headers_mut()
.insert("retry-after", HeaderValue::from(retry_after));
insert_rate_limit_headers(resp.headers_mut(), result);
resp
}
fn payload_too_large() -> Response {
let body = json!({
"error": {
"code": "PAYLOAD_TOO_LARGE",
"message": "Request body too large or unreadable",
}
});
(StatusCode::PAYLOAD_TOO_LARGE, Json(body)).into_response()
}
fn insert_rate_limit_headers(headers: &mut HeaderMap, result: &RateLimitResult) {
headers.insert("x-ratelimit-limit", HeaderValue::from(result.limit));
headers.insert("x-ratelimit-remaining", HeaderValue::from(result.remaining));
headers.insert("x-ratelimit-reset", HeaderValue::from(result.reset));
}
async fn extract_rate_limit_key(
headers: &HeaderMap,
peer: Option<IpAddr>,
ctx: &RateLimitContext,
) -> Result<Option<(RateLimitKey, Option<u32>)>, RateLimitResult> {
let ip = client_ip(peer, headers, &ctx.trusted_proxies);
let bearer = headers
.get("authorization")
.and_then(|v| v.to_str().ok())
.and_then(|v| v.strip_prefix("Bearer "))
.map(|s| s.to_string());
if let Some(ref token) = bearer {
if token.starts_with(API_KEY_PREFIX) {
if let Some(found) = try_api_key_identity(token, ip, ctx).await? {
return Ok(Some(found));
}
} else if let Some(key) = try_jwt_identity(token, ctx) {
return Ok(Some((key, None)));
}
}
if let Some(cookie_header) = headers.get("cookie").and_then(|v| v.to_str().ok()) {
for part in cookie_header.split(';') {
let cookie = part.trim().strip_prefix(AUTH_COOKIE_NAME);
if let Some(value) = cookie.and_then(|rest| rest.strip_prefix('='))
&& let Some(key) = try_jwt_identity(value, ctx)
{
return Ok(Some((key, None)));
}
}
}
Ok(ip.map(|ip| (RateLimitKey::Ip(ip), None)))
}
enum Verification {
Valid {
id: Uuid,
rate_limit_override: Option<u32>,
expires_at: Option<DateTime<Utc>>,
},
Unknown,
WrongSecret,
Unavailable,
Throttled(RateLimitResult),
}
async fn try_api_key_identity(
token: &str,
ip: Option<IpAddr>,
ctx: &RateLimitContext,
) -> Result<Option<(RateLimitKey, Option<u32>)>, RateLimitResult> {
let now = now_epoch();
let hash = token_hash(token);
let as_key = |(id, limit): (Uuid, Option<u32>)| (RateLimitKey::ApiKey(id), limit);
if let Some(identity) = cached_verdict(ctx, &hash, now) {
return Ok(identity.map(as_key));
}
let lock = Arc::clone(
ctx.limiter
.pending_checks
.entry(hash.clone())
.or_default()
.value(),
);
let _pending = PendingCheck {
pending_checks: &ctx.limiter.pending_checks,
hash: &hash,
lock: Arc::clone(&lock),
};
let _turn = lock.lock().await;
if let Some(identity) = cached_verdict(ctx, &hash, now) {
return Ok(identity.map(as_key));
}
let ttl_end = now + VERIFIED_KEY_TTL_SECS;
let (identity, valid_until) = match verify_api_key(token, ip, ctx).await {
Verification::Valid {
id,
rate_limit_override,
expires_at,
} => {
let expiry = expires_at.map_or(u64::MAX, |at| at.timestamp().max(0) as u64);
(Some((id, rate_limit_override)), ttl_end.min(expiry))
}
Verification::WrongSecret => (None, ttl_end),
Verification::Unknown | Verification::Unavailable => return Ok(None),
Verification::Throttled(result) => return Err(result),
};
let verified_keys = &ctx.limiter.verified_keys;
if verified_keys.len() >= SWEEP_THRESHOLD && !verified_keys.contains_key(&hash) {
verified_keys.retain(|_, cached| cached.valid_until > now);
}
let verdict = CachedVerification {
identity,
valid_until,
};
verified_keys.insert(hash.clone(), verdict);
Ok(identity.map(as_key))
}
fn cached_verdict(
ctx: &RateLimitContext,
hash: &str,
now: u64,
) -> Option<Option<(Uuid, Option<u32>)>> {
ctx.limiter
.verified_keys
.get(hash)
.filter(|cached| cached.valid_until > now)
.map(|cached| cached.identity)
}
struct PendingCheck<'a> {
pending_checks: &'a DashMap<String, Arc<Mutex<()>>>,
hash: &'a str,
lock: Arc<Mutex<()>>,
}
impl Drop for PendingCheck<'_> {
fn drop(&mut self) {
self.pending_checks
.remove_if(self.hash, |_, lock| Arc::ptr_eq(lock, &self.lock));
}
}
async fn verify_api_key(token: &str, ip: Option<IpAddr>, ctx: &RateLimitContext) -> Verification {
let suffix_len = (token.len() - API_KEY_PREFIX.len()).min(API_KEY_SUFFIX_LEN);
let prefix = &token[..API_KEY_PREFIX.len() + suffix_len];
let api_key = match ctx.store.find_api_key_by_prefix(prefix).await {
Ok(Some(api_key)) => api_key,
Ok(None) => return Verification::Unknown,
Err(err) => {
warn!(error = %err, "rate limiter could not look up an API key");
return Verification::Unavailable;
}
};
if !api_key.is_active || api_key.expires_at.is_some_and(|at| at <= Utc::now()) {
return Verification::Unknown;
}
let check = check_rate_limit(
&ctx.limiter,
RateLimitKey::KeyCheck {
ip,
api_key: api_key.id,
},
KEY_CHECKS_PER_MINUTE,
);
if !check.allowed {
return Verification::Throttled(check);
}
match password::verify(token, &api_key.key_hash) {
Ok(true) => {}
Ok(false) => return Verification::WrongSecret,
Err(err) => {
warn!(error = %err, "rate limiter could not verify an API key");
return Verification::Unavailable;
}
}
let rate_limit_override = match ctx.store.find_user_by_id(api_key.user_id).await {
Ok(Some(owner)) if owner.is_admin => api_key.rate_limit_override,
Ok(_) => None,
Err(err) => {
warn!(error = %err, "rate limiter could not look up an API key owner");
return Verification::Unavailable;
}
};
Verification::Valid {
id: api_key.id,
rate_limit_override,
expires_at: api_key.expires_at,
}
}
fn try_jwt_identity(token: &str, ctx: &RateLimitContext) -> Option<RateLimitKey> {
let claims = AccessToken::decode(token, &ctx.jwt_config).ok()?;
Some(RateLimitKey::User(claims.user_id))
}