use std::collections::HashMap;
use std::sync::Mutex;
pub trait ReplayCache: Send + Sync {
fn check_and_remember(&self, key_id: &str, nonce: &str, now_unix: i64) -> bool;
}
pub struct InMemoryReplayCache {
window_secs: i64,
seen: Mutex<HashMap<(String, String), i64>>, }
impl InMemoryReplayCache {
pub fn new(window_secs: i64) -> Self {
InMemoryReplayCache {
window_secs,
seen: Mutex::new(HashMap::new()),
}
}
pub fn len(&self) -> usize {
self.seen.lock().unwrap().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
impl ReplayCache for InMemoryReplayCache {
fn check_and_remember(&self, key_id: &str, nonce: &str, now_unix: i64) -> bool {
let mut map = self.seen.lock().unwrap();
map.retain(|_, &mut expiry| expiry > now_unix);
let key = (key_id.to_string(), nonce.to_string());
if map.contains_key(&key) {
return false; }
map.insert(key, now_unix + self.window_secs);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_use_accepted_replay_rejected() {
let c = InMemoryReplayCache::new(300);
assert!(c.check_and_remember("k", "nonce-1", 1000));
assert!(!c.check_and_remember("k", "nonce-1", 1100));
assert!(c.check_and_remember("k", "nonce-2", 1100));
assert!(c.check_and_remember("k2", "nonce-1", 1100));
}
#[test]
fn expired_entries_pruned_and_reusable() {
let c = InMemoryReplayCache::new(300);
assert!(c.check_and_remember("k", "n", 1000));
assert!(c.check_and_remember("k", "n2", 2000));
assert_eq!(c.len(), 1, "old entry should have been pruned at t=2000");
}
}