use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Duration;
use lru::LruCache;
use serde::Serialize;
use std::num::NonZeroUsize;
use tracing::{info, warn};
pub const LOGIN_RATE_LIMIT_ENV: &str = "RUNEGATE_LOGIN_RATE_LIMIT";
pub const EMAIL_COOLDOWN_ENV: &str = "RUNEGATE_EMAIL_COOLDOWN";
pub const TOKEN_RATE_LIMIT_ENV: &str = "RUNEGATE_TOKEN_RATE_LIMIT";
pub const RATE_LIMIT_ENABLED_ENV: &str = "RUNEGATE_RATE_LIMIT_ENABLED";
pub const DEFAULT_LOGIN_RATE_LIMIT: u32 = 5; pub const DEFAULT_EMAIL_COOLDOWN: u64 = 300; pub const DEFAULT_TOKEN_RATE_LIMIT: u32 = 10;
#[derive(Debug, Clone, Serialize)]
pub struct RateLimitConfig {
pub login_rate_limit: u32,
pub email_cooldown: u64,
pub token_rate_limit: u32,
pub enabled: bool,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
login_rate_limit: DEFAULT_LOGIN_RATE_LIMIT,
email_cooldown: DEFAULT_EMAIL_COOLDOWN,
token_rate_limit: DEFAULT_TOKEN_RATE_LIMIT,
enabled: true,
}
}
}
impl RateLimitConfig {
pub fn from_env() -> Self {
let login_rate_limit = std::env::var(LOGIN_RATE_LIMIT_ENV)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_LOGIN_RATE_LIMIT);
let email_cooldown = std::env::var(EMAIL_COOLDOWN_ENV)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_EMAIL_COOLDOWN);
let token_rate_limit = std::env::var(TOKEN_RATE_LIMIT_ENV)
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(DEFAULT_TOKEN_RATE_LIMIT);
let enabled = std::env::var(RATE_LIMIT_ENABLED_ENV)
.map(|v| v.to_lowercase() != "false" && v != "0")
.unwrap_or(true);
Self {
login_rate_limit,
email_cooldown,
token_rate_limit,
enabled,
}
}
}
#[derive(Debug, Clone, Copy)]
struct Timestamp(std::time::SystemTime);
impl Timestamp {
fn now() -> Self {
Timestamp(std::time::SystemTime::now())
}
fn elapsed(&self) -> Duration {
self.0.elapsed().unwrap_or_else(|_| Duration::from_secs(0))
}
}
#[derive(Debug)]
pub struct EmailRateLimiter {
cache: Mutex<LruCache<String, Timestamp>>,
cooldown: Duration,
enabled: bool,
}
impl EmailRateLimiter {
pub fn new(config: &RateLimitConfig) -> Self {
let cache = Mutex::new(LruCache::new(NonZeroUsize::new(1000).unwrap()));
let cooldown = Duration::from_secs(config.email_cooldown);
Self {
cache,
cooldown,
enabled: config.enabled,
}
}
pub fn check_email(&self, email: &str) -> Option<u64> {
if !self.enabled {
return None;
}
let now = Timestamp::now();
let mut cache = self.cache.lock().unwrap();
if let Some(last_time) = cache.get(email) {
let elapsed = last_time.elapsed();
if elapsed < self.cooldown {
let remaining = self.cooldown.saturating_sub(elapsed);
return Some(remaining.as_secs());
}
}
cache.put(email.to_string(), now);
None
}
}
#[derive(Debug)]
pub struct LoginRateLimiter {
attempts: Mutex<HashMap<String, (u32, Timestamp)>>,
max_attempts: u32,
period: Duration,
enabled: bool,
}
impl LoginRateLimiter {
pub fn new(config: &RateLimitConfig) -> Self {
Self {
attempts: Mutex::new(HashMap::new()),
max_attempts: config.login_rate_limit,
period: Duration::from_secs(60), enabled: config.enabled,
}
}
pub fn check_ip(&self, ip: &str) -> bool {
if !self.enabled {
return true;
}
let now = Timestamp::now();
let mut attempts = self.attempts.lock().unwrap();
let entry = attempts.entry(ip.to_string()).or_insert((0, now));
if entry.1.elapsed() >= self.period {
*entry = (1, now); return true;
}
if entry.0 < self.max_attempts {
entry.0 += 1;
true
} else {
warn!("Rate limited login attempt from IP: {}", ip);
false
}
}
}
#[derive(Debug)]
pub struct TokenRateLimiter {
attempts: Mutex<HashMap<String, (u32, Timestamp)>>,
max_attempts: u32,
period: Duration,
enabled: bool,
}
impl TokenRateLimiter {
pub fn new(config: &RateLimitConfig) -> Self {
Self {
attempts: Mutex::new(HashMap::new()),
max_attempts: config.token_rate_limit,
period: Duration::from_secs(60), enabled: config.enabled,
}
}
pub fn check_ip(&self, ip: &str) -> bool {
if !self.enabled {
return true;
}
let now = Timestamp::now();
let mut attempts = self.attempts.lock().unwrap();
let entry = attempts.entry(ip.to_string()).or_insert((0, now));
if entry.1.elapsed() >= self.period {
*entry = (1, now); return true;
}
if entry.0 < self.max_attempts {
entry.0 += 1;
true
} else {
warn!("Rate limited token verification attempt from IP: {}", ip);
false
}
}
}
#[derive(Debug)]
pub struct RateLimiters {
pub email_limiter: EmailRateLimiter,
pub login_limiter: LoginRateLimiter,
pub token_limiter: TokenRateLimiter,
pub config: RateLimitConfig,
}
impl Default for RateLimiters {
fn default() -> Self {
Self::new()
}
}
impl RateLimiters {
pub fn new() -> Self {
let config = RateLimitConfig::from_env();
info!("Rate limiting configuration:");
info!(" Enabled: {}", config.enabled);
info!(
" Login rate limit: {} per minute per IP",
config.login_rate_limit
);
info!(
" Email cooldown: {} seconds per email",
config.email_cooldown
);
info!(
" Token rate limit: {} per minute per IP",
config.token_rate_limit
);
let email_limiter = EmailRateLimiter::new(&config);
let login_limiter = LoginRateLimiter::new(&config);
let token_limiter = TokenRateLimiter::new(&config);
Self {
email_limiter,
login_limiter,
token_limiter,
config,
}
}
}