use anyhow::Result;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub enabled: bool,
pub default_rpm: u32,
pub burst_capacity: u32,
pub method_limits: HashMap<String, MethodLimit>,
pub client_limits: HashMap<String, ClientLimit>,
pub cleanup_interval_secs: u64,
pub adaptive: bool,
pub threat_penalty_multiplier: f32,
#[serde(default)]
pub whitelist: HashSet<String>,
#[serde(default)]
pub blacklist: HashSet<String>,
#[serde(default)]
pub ip_limits: HashMap<String, IpLimit>,
pub global_rpm: Option<u32>,
#[serde(default)]
pub track_by: TrackingMethod,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IpLimit {
pub rpm: u32,
pub burst: u32,
pub block: bool,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TrackingMethod {
ClientId,
IpAddress,
Combined,
}
impl Default for TrackingMethod {
fn default() -> Self {
Self::ClientId
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MethodLimit {
pub rpm: u32,
pub burst: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientLimit {
pub rpm: u32,
pub burst: u32,
pub priority: ClientPriority,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum ClientPriority {
Low = 0,
Normal = 1,
High = 2,
Premium = 3,
}
impl Default for RateLimitConfig {
fn default() -> Self {
let mut method_limits = HashMap::new();
method_limits.insert("tools/list".to_string(), MethodLimit { rpm: 60, burst: 10 });
method_limits.insert(
"resources/list".to_string(),
MethodLimit { rpm: 60, burst: 10 },
);
method_limits.insert("tools/call".to_string(), MethodLimit { rpm: 30, burst: 5 });
method_limits.insert(
"security/threats".to_string(),
MethodLimit { rpm: 10, burst: 2 },
);
Self {
enabled: false,
default_rpm: 60,
burst_capacity: 10,
method_limits,
client_limits: HashMap::new(),
cleanup_interval_secs: 300, adaptive: false,
threat_penalty_multiplier: 0.5, whitelist: HashSet::new(),
blacklist: HashSet::new(),
ip_limits: HashMap::new(),
global_rpm: None,
track_by: TrackingMethod::default(),
}
}
}
#[derive(Debug)]
struct TokenBucket {
capacity: f64,
tokens: f64,
refill_rate: f64,
last_refill: Instant,
penalty_factor: f64,
}
impl TokenBucket {
fn new(rpm: u32, burst: u32) -> Self {
let capacity = f64::from(burst);
let refill_rate = f64::from(rpm) / 60.0;
Self {
capacity,
tokens: capacity, refill_rate,
last_refill: Instant::now(),
penalty_factor: 1.0,
}
}
fn try_consume(&mut self, tokens: f64) -> bool {
self.refill();
if self.tokens >= tokens {
self.tokens -= tokens;
true
} else {
false
}
}
fn refill(&mut self) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_refill).as_secs_f64();
let new_tokens = elapsed * self.refill_rate * self.penalty_factor;
self.tokens = (self.tokens + new_tokens).min(self.capacity);
self.last_refill = now;
}
fn apply_penalty(&mut self, factor: f64) {
self.penalty_factor = (self.penalty_factor * factor).max(0.1); }
fn time_until_available(&self, tokens: f64) -> Duration {
if self.tokens >= tokens {
Duration::ZERO
} else {
let needed = tokens - self.tokens;
let seconds = needed / (self.refill_rate * self.penalty_factor);
Duration::from_secs_f64(seconds)
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct RateLimitKey {
client_id: String,
method: Option<String>,
}
pub struct RateLimiter {
config: RateLimitConfig,
buckets: Arc<RwLock<HashMap<RateLimitKey, Arc<Mutex<TokenBucket>>>>>,
#[allow(dead_code)] last_cleanup: Arc<Mutex<Instant>>,
}
#[derive(Debug)]
pub struct RateLimitResult {
pub allowed: bool,
pub remaining: u32,
pub reset_after: Duration,
pub limit: u32,
}
impl RateLimiter {
pub fn new(config: RateLimitConfig) -> Self {
let limiter = Self {
config,
buckets: Arc::new(RwLock::new(HashMap::new())),
last_cleanup: Arc::new(Mutex::new(Instant::now())),
};
if limiter.config.enabled && limiter.config.cleanup_interval_secs > 0 {
let buckets = limiter.buckets.clone();
let interval = limiter.config.cleanup_interval_secs;
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(interval));
loop {
interval.tick().await;
Self::cleanup_buckets(buckets.clone()).await;
}
});
}
limiter
}
pub async fn check_limit(
&self,
client_id: &str,
method: Option<&str>,
tokens: f64,
) -> Result<RateLimitResult> {
if !self.config.enabled {
return Ok(RateLimitResult {
allowed: true,
remaining: u32::MAX,
reset_after: Duration::ZERO,
limit: u32::MAX,
});
}
if self.config.blacklist.contains(client_id) {
return Ok(RateLimitResult {
allowed: false,
remaining: 0,
reset_after: Duration::from_secs(3600), limit: 0,
});
}
if self.config.whitelist.contains(client_id) {
return Ok(RateLimitResult {
allowed: true,
remaining: u32::MAX,
reset_after: Duration::ZERO,
limit: u32::MAX,
});
}
let (rpm, burst) = self.get_limits(client_id, method);
let key = RateLimitKey {
client_id: client_id.to_string(),
method: method.map(String::from),
};
let bucket = self.get_or_create_bucket(&key, rpm, burst).await;
let mut bucket = bucket.lock();
let allowed = bucket.try_consume(tokens);
let remaining = bucket.tokens as u32;
let reset_after = bucket.time_until_available(1.0);
Ok(RateLimitResult {
allowed,
remaining,
reset_after,
limit: rpm,
})
}
pub async fn apply_penalty(&self, client_id: &str, factor: f64) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
let buckets = self.buckets.read().await;
for (key, bucket) in buckets.iter() {
if key.client_id == client_id {
bucket.lock().apply_penalty(factor);
}
}
Ok(())
}
pub async fn get_status(&self, client_id: &str) -> Result<HashMap<String, RateLimitResult>> {
let mut status = HashMap::new();
if !self.config.enabled {
return Ok(status);
}
let buckets = self.buckets.read().await;
for (key, bucket) in buckets.iter() {
if key.client_id == client_id {
let bucket = bucket.lock();
let method = key.method.as_deref().unwrap_or("default");
status.insert(
method.to_string(),
RateLimitResult {
allowed: bucket.tokens >= 1.0,
remaining: bucket.tokens as u32,
reset_after: bucket.time_until_available(1.0),
limit: (bucket.refill_rate * 60.0) as u32,
},
);
}
}
Ok(status)
}
async fn get_or_create_bucket(
&self,
key: &RateLimitKey,
rpm: u32,
burst: u32,
) -> Arc<Mutex<TokenBucket>> {
let mut buckets = self.buckets.write().await;
buckets
.entry(key.clone())
.or_insert_with(|| Arc::new(Mutex::new(TokenBucket::new(rpm, burst))))
.clone()
}
fn get_limits(&self, client_id: &str, method: Option<&str>) -> (u32, u32) {
if let Some(client_limit) = self.config.client_limits.get(client_id) {
return (client_limit.rpm, client_limit.burst);
}
if let Some(method) = method {
if let Some(method_limit) = self.config.method_limits.get(method) {
return (method_limit.rpm, method_limit.burst);
}
}
(self.config.default_rpm, self.config.burst_capacity)
}
async fn cleanup_buckets(buckets: Arc<RwLock<HashMap<RateLimitKey, Arc<Mutex<TokenBucket>>>>>) {
let mut buckets = buckets.write().await;
let now = Instant::now();
buckets.retain(|_, bucket| {
let bucket = bucket.lock();
now.duration_since(bucket.last_refill) < Duration::from_secs(600)
});
}
pub fn create_headers(result: &RateLimitResult) -> HashMap<String, String> {
let mut headers = HashMap::new();
headers.insert("X-RateLimit-Limit".to_string(), result.limit.to_string());
headers.insert(
"X-RateLimit-Remaining".to_string(),
result.remaining.to_string(),
);
headers.insert(
"X-RateLimit-Reset".to_string(),
(Instant::now() + result.reset_after)
.duration_since(Instant::now())
.as_secs()
.to_string(),
);
if !result.allowed {
headers.insert(
"Retry-After".to_string(),
result.reset_after.as_secs().to_string(),
);
}
headers
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_bucket() {
let mut bucket = TokenBucket::new(60, 10);
assert!(bucket.try_consume(10.0));
assert!(!bucket.try_consume(1.0));
std::thread::sleep(Duration::from_millis(1100)); bucket.refill();
assert!(bucket.tokens > 0.0); }
#[tokio::test]
async fn test_rate_limiter() {
let config = RateLimitConfig {
enabled: true,
default_rpm: 60,
burst_capacity: 10,
..Default::default()
};
let limiter = RateLimiter::new(config);
for _ in 0..10 {
let result = limiter.check_limit("test-client", None, 1.0).await.unwrap();
assert!(result.allowed);
}
let result = limiter.check_limit("test-client", None, 1.0).await.unwrap();
assert!(!result.allowed);
assert!(result.reset_after > Duration::ZERO);
}
#[test]
fn test_penalty_application() {
let mut bucket = TokenBucket::new(60, 10);
bucket.apply_penalty(0.5);
assert_eq!(bucket.penalty_factor, 0.5);
}
}