use axum::extract::Request;
use axum::http::StatusCode;
use axum::middleware::Next;
use axum::response::Response;
use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, Mutex};
use std::time::Instant;
#[derive(Clone)]
pub struct RateLimiter {
inner: Arc<Mutex<HashMap<IpAddr, Bucket>>>,
capacity: f64,
refill_per_sec: f64,
}
struct Bucket {
tokens: f64,
last: Instant,
}
impl RateLimiter {
pub fn new(capacity_per_min: u32) -> Self {
let capacity = capacity_per_min as f64;
RateLimiter {
inner: Arc::new(Mutex::new(HashMap::new())),
capacity,
refill_per_sec: capacity / 60.0,
}
}
fn allow(&self, ip: IpAddr) -> bool {
let mut map = self.inner.lock().unwrap();
let now = Instant::now();
let bucket = map.entry(ip).or_insert(Bucket {
tokens: self.capacity,
last: now,
});
let elapsed = now.duration_since(bucket.last).as_secs_f64();
bucket.tokens = (bucket.tokens + elapsed * self.refill_per_sec).min(self.capacity);
bucket.last = now;
if bucket.tokens >= 1.0 {
bucket.tokens -= 1.0;
true
} else {
false
}
}
}
pub async fn rate_limit_middleware(
axum::extract::State(limiter): axum::extract::State<RateLimiter>,
req: Request,
next: Next,
) -> Result<Response, StatusCode> {
if let Some(ip) = client_ip(&req)
&& !limiter.allow(ip)
{
return Err(StatusCode::TOO_MANY_REQUESTS);
}
Ok(next.run(req).await)
}
fn client_ip(req: &Request) -> Option<IpAddr> {
if let Some(xff) = req.headers().get("x-forwarded-for")
&& let Ok(s) = xff.to_str()
&& let Some(first) = s.split(',').next()
&& let Ok(ip) = first.trim().parse::<IpAddr>()
{
return Some(ip);
}
req.extensions()
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
.map(|ci| ci.0.ip())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allows_until_capacity_then_denies() {
let limiter = RateLimiter::new(3);
let ip: IpAddr = "127.0.0.1".parse().unwrap();
assert!(limiter.allow(ip));
assert!(limiter.allow(ip));
assert!(limiter.allow(ip));
assert!(!limiter.allow(ip));
}
#[test]
fn different_ips_independent() {
let limiter = RateLimiter::new(1);
let a: IpAddr = "127.0.0.1".parse().unwrap();
let b: IpAddr = "127.0.0.2".parse().unwrap();
assert!(limiter.allow(a));
assert!(limiter.allow(b));
assert!(!limiter.allow(a));
}
}