use crate::config::ipc_security::ReplayProtectionConfig;
use crate::dashboard::error::DashboardError;
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub struct ReplayWindow {
entries: HashMap<String, Instant>,
max_size: usize,
ttl: Duration,
}
impl ReplayWindow {
pub fn new(max_size: usize, ttl: Duration) -> Self {
Self {
entries: HashMap::with_capacity(max_size.min(64)),
max_size,
ttl,
}
}
pub fn from_config(config: &ReplayProtectionConfig) -> Self {
Self::new(config.window_size, Duration::from_secs(config.ttl_seconds))
}
pub fn check_and_record(&mut self, request_id: &str) -> Result<(), DashboardError> {
self.purge_expired();
if self.entries.contains_key(request_id) {
return Err(DashboardError::replay_detected(request_id));
}
if self.entries.len() >= self.max_size
&& let Some(oldest_key) = self
.entries
.iter()
.min_by_key(|(_, t)| **t)
.map(|(k, _)| k.clone())
{
self.entries.remove(&oldest_key);
}
self.entries.insert(request_id.to_string(), Instant::now());
Ok(())
}
pub fn is_replay(&self, request_id: &str) -> bool {
self.entries.contains_key(request_id)
}
pub fn record(&mut self, request_id: String) {
if self.entries.len() >= self.max_size
&& let Some(oldest_key) = self
.entries
.iter()
.min_by_key(|(_, t)| **t)
.map(|(k, _)| k.clone())
{
self.entries.remove(&oldest_key);
}
self.entries.insert(request_id, Instant::now());
}
pub fn purge_expired(&mut self) {
let now = Instant::now();
self.entries
.retain(|_, inserted| now.duration_since(*inserted) < self.ttl);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}