use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
#[derive(Clone)]
pub struct LoginGuard {
inner: Arc<Mutex<HashMap<String, Vec<Instant>>>>,
}
const MAX_FAILURES: usize = 5;
const WINDOW: Duration = Duration::from_secs(300);
impl LoginGuard {
pub fn new() -> Self {
Self { inner: Arc::new(Mutex::new(HashMap::new())) }
}
pub fn is_blocked(&self, ip: &str) -> bool {
let now = Instant::now();
let mut map = self.inner.lock().unwrap();
let hits = map.entry(ip.to_string()).or_default();
hits.retain(|t| now.duration_since(*t) < WINDOW);
hits.len() >= MAX_FAILURES
}
pub fn record_failure(&self, ip: &str) {
let now = Instant::now();
let mut map = self.inner.lock().unwrap();
let hits = map.entry(ip.to_string()).or_default();
hits.retain(|t| now.duration_since(*t) < WINDOW);
hits.push(now);
}
pub fn record_success(&self, ip: &str) {
self.inner.lock().unwrap().remove(ip);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_after_max_failures() {
let g = LoginGuard::new();
for _ in 0..MAX_FAILURES {
assert!(!g.is_blocked("1.2.3.4")); g.record_failure("1.2.3.4");
}
assert!(g.is_blocked("1.2.3.4"));
assert!(g.is_blocked("1.2.3.4"));
}
#[test]
fn success_resets_counter() {
let g = LoginGuard::new();
for _ in 0..MAX_FAILURES {
g.record_failure("1.2.3.4");
}
assert!(g.is_blocked("1.2.3.4"));
g.record_success("1.2.3.4");
assert!(!g.is_blocked("1.2.3.4"));
}
#[test]
fn window_expiry_resets_counter() {
let g = LoginGuard::new();
for _ in 0..MAX_FAILURES {
g.record_failure("1.2.3.4");
}
assert!(g.is_blocked("1.2.3.4"));
{
let mut map = g.inner.lock().unwrap();
let hits = map.get_mut("1.2.3.4").unwrap();
for t in hits.iter_mut() {
*t = Instant::now() - WINDOW - Duration::from_secs(1);
}
}
assert!(!g.is_blocked("1.2.3.4"));
}
#[test]
fn ips_are_isolated() {
let g = LoginGuard::new();
for _ in 0..MAX_FAILURES {
g.record_failure("1.2.3.4");
}
assert!(g.is_blocked("1.2.3.4"));
assert!(!g.is_blocked("5.6.7.8"));
}
}