use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
use tracing::warn;
#[derive(Debug, Clone)]
pub struct AdminRateLimitConfig {
pub max_failures: u32,
pub window_duration: Duration,
pub block_duration: Duration,
}
impl Default for AdminRateLimitConfig {
fn default() -> Self {
Self {
max_failures: 5,
window_duration: Duration::from_secs(5 * 60), block_duration: Duration::from_secs(15 * 60), }
}
}
#[derive(Debug, Clone)]
struct IpAttemptRecord {
failure_count: u32,
window_start: Instant,
blocked_until: Option<Instant>,
}
impl IpAttemptRecord {
fn new() -> Self {
Self {
failure_count: 0,
window_start: Instant::now(),
blocked_until: None,
}
}
fn is_blocked(&self) -> bool {
if let Some(blocked_until) = self.blocked_until {
Instant::now() < blocked_until
} else {
false
}
}
fn maybe_reset_window(&mut self, window_duration: Duration) {
if self.window_start.elapsed() > window_duration {
self.failure_count = 0;
self.window_start = Instant::now();
}
}
}
#[derive(Clone)]
pub struct AdminRateLimiter {
config: AdminRateLimitConfig,
attempts: Arc<RwLock<HashMap<IpAddr, IpAttemptRecord>>>,
}
impl AdminRateLimiter {
pub fn new() -> Self {
Self::with_config(AdminRateLimitConfig::default())
}
pub fn with_config(config: AdminRateLimitConfig) -> Self {
Self {
config,
attempts: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn check_allowed(&self, ip: IpAddr) -> Result<(), u64> {
let attempts = self.attempts.read().await;
if let Some(record) = attempts.get(&ip) {
if let Some(blocked_until) = record.blocked_until {
let now = Instant::now();
if now < blocked_until {
let remaining = blocked_until.duration_since(now);
return Err(remaining.as_secs());
}
}
}
Ok(())
}
pub async fn record_failure(&self, ip: IpAddr) -> bool {
let mut attempts = self.attempts.write().await;
let record = attempts.entry(ip).or_insert_with(IpAttemptRecord::new);
record.maybe_reset_window(self.config.window_duration);
record.failure_count += 1;
if record.failure_count >= self.config.max_failures {
record.blocked_until = Some(Instant::now() + self.config.block_duration);
warn!(
ip = %ip,
failures = record.failure_count,
block_minutes = self.config.block_duration.as_secs() / 60,
"Admin API: IP blocked due to too many failed authentication attempts"
);
true
} else {
warn!(
ip = %ip,
failures = record.failure_count,
max = self.config.max_failures,
"Admin API: failed authentication attempt"
);
false
}
}
pub async fn record_success(&self, ip: IpAddr) {
let mut attempts = self.attempts.write().await;
attempts.remove(&ip);
}
#[allow(dead_code)]
pub async fn cleanup_expired(&self) {
let mut attempts = self.attempts.write().await;
let now = Instant::now();
attempts.retain(|_, record| {
if let Some(blocked_until) = record.blocked_until {
if now < blocked_until {
return true;
}
}
record.window_start.elapsed() < self.config.window_duration
});
}
#[allow(dead_code)]
pub async fn tracked_ip_count(&self) -> usize {
self.attempts.read().await.len()
}
#[allow(dead_code)]
pub async fn blocked_ip_count(&self) -> usize {
let attempts = self.attempts.read().await;
attempts.values().filter(|r| r.is_blocked()).count()
}
}
impl Default for AdminRateLimiter {
fn default() -> Self {
Self::new()
}
}