use std::collections::{HashMap, HashSet, VecDeque};
use super::types::PeerSession;
pub const DEFAULT_SESSION_IDLE_TTL_MS: u64 = 15 * 60 * 1000;
pub const DEFAULT_SEEN_NONCE_CAP: usize = 10_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MarkSeen {
Fresh,
Replay,
SessionGone,
}
struct SessionMeta {
last_used_ms: u64,
seen: HashSet<String>,
order: VecDeque<String>,
}
pub struct SessionManager {
nonce_to_session: HashMap<String, PeerSession>,
identity_to_nonces: HashMap<String, HashSet<String>>,
session_meta: HashMap<String, SessionMeta>,
idle_ttl_ms: u64,
seen_nonce_cap: usize,
}
impl SessionManager {
pub fn new() -> Self {
Self::with_config(DEFAULT_SESSION_IDLE_TTL_MS, DEFAULT_SEEN_NONCE_CAP)
}
pub fn with_config(idle_ttl_ms: u64, seen_nonce_cap: usize) -> Self {
SessionManager {
nonce_to_session: HashMap::new(),
identity_to_nonces: HashMap::new(),
session_meta: HashMap::new(),
idle_ttl_ms,
seen_nonce_cap,
}
}
pub fn idle_ttl_ms(&self) -> u64 {
self.idle_ttl_ms
}
pub fn add_session(&mut self, session: PeerSession) {
let nonce = session.session_nonce.clone();
let identity = session.peer_identity_key.clone();
self.nonce_to_session.insert(nonce.clone(), session);
self.identity_to_nonces
.entry(identity)
.or_default()
.insert(nonce);
}
pub fn get_session(&self, nonce: &str) -> Option<&PeerSession> {
self.nonce_to_session.get(nonce)
}
pub fn get_session_mut(&mut self, nonce: &str) -> Option<&mut PeerSession> {
self.nonce_to_session.get_mut(nonce)
}
pub fn get_sessions_for_identity(&self, identity_key: &str) -> Vec<&PeerSession> {
match self.identity_to_nonces.get(identity_key) {
Some(nonces) => nonces
.iter()
.filter_map(|n| self.nonce_to_session.get(n))
.collect(),
None => Vec::new(),
}
}
pub fn get_session_by_identifier(&self, identifier: &str) -> Option<&PeerSession> {
if let Some(session) = self.nonce_to_session.get(identifier) {
return Some(session);
}
let nonces = self.identity_to_nonces.get(identifier)?;
let mut best: Option<&PeerSession> = None;
for nonce in nonces {
if let Some(session) = self.nonce_to_session.get(nonce) {
match best {
None => best = Some(session),
Some(b) => {
if session.is_authenticated && !b.is_authenticated {
best = Some(session);
}
}
}
}
}
best
}
pub fn has_session(&self, nonce: &str) -> bool {
self.nonce_to_session.contains_key(nonce)
}
pub fn has_session_by_identifier(&self, identifier: &str) -> bool {
if self.nonce_to_session.contains_key(identifier) {
return true;
}
match self.identity_to_nonces.get(identifier) {
Some(nonces) => !nonces.is_empty(),
None => false,
}
}
pub fn update_session(&mut self, nonce: &str, session: PeerSession) {
if let Some(old_session) = self.nonce_to_session.get(nonce) {
let old_identity = old_session.peer_identity_key.clone();
if old_identity != session.peer_identity_key {
if let Some(nonces) = self.identity_to_nonces.get_mut(&old_identity) {
nonces.remove(nonce);
if nonces.is_empty() {
self.identity_to_nonces.remove(&old_identity);
}
}
}
}
let new_identity = session.peer_identity_key.clone();
self.nonce_to_session.insert(nonce.to_string(), session);
self.identity_to_nonces
.entry(new_identity)
.or_default()
.insert(nonce.to_string());
}
pub fn remove_session(&mut self, nonce: &str) -> Option<PeerSession> {
self.session_meta.remove(nonce);
if let Some(session) = self.nonce_to_session.remove(nonce) {
if let Some(nonces) = self.identity_to_nonces.get_mut(&session.peer_identity_key) {
nonces.remove(nonce);
if nonces.is_empty() {
self.identity_to_nonces.remove(&session.peer_identity_key);
}
}
Some(session)
} else {
None
}
}
pub fn touch(&mut self, session_nonce: &str, now_ms: u64) {
if !self.nonce_to_session.contains_key(session_nonce) {
return;
}
self.session_meta
.entry(session_nonce.to_string())
.or_insert_with(|| SessionMeta {
last_used_ms: now_ms,
seen: HashSet::new(),
order: VecDeque::new(),
})
.last_used_ms = now_ms;
}
pub fn is_expired(&self, session_nonce: &str, now_ms: u64) -> bool {
match self.session_meta.get(session_nonce) {
Some(m) => now_ms.saturating_sub(m.last_used_ms) > self.idle_ttl_ms,
None => false,
}
}
pub fn get_active_session(&self, identifier: &str, now_ms: u64) -> Option<&PeerSession> {
let session = self.get_session_by_identifier(identifier)?;
if self.is_expired(&session.session_nonce, now_ms) {
return None;
}
Some(session)
}
pub fn mark_message_seen(
&mut self,
session_nonce: &str,
message_nonce: &str,
now_ms: u64,
) -> MarkSeen {
if !self.nonce_to_session.contains_key(session_nonce) {
return MarkSeen::SessionGone;
}
let cap = self.seen_nonce_cap;
let meta = self
.session_meta
.entry(session_nonce.to_string())
.or_insert_with(|| SessionMeta {
last_used_ms: now_ms,
seen: HashSet::new(),
order: VecDeque::new(),
});
meta.last_used_ms = now_ms;
if !meta.seen.insert(message_nonce.to_string()) {
return MarkSeen::Replay;
}
meta.order.push_back(message_nonce.to_string());
while meta.order.len() > cap {
if let Some(old) = meta.order.pop_front() {
meta.seen.remove(&old);
}
}
MarkSeen::Fresh
}
pub fn reap_idle(&mut self, now_ms: u64) -> usize {
let ttl = self.idle_ttl_ms;
let expired: Vec<String> = self
.session_meta
.iter()
.filter(|(_, m)| now_ms.saturating_sub(m.last_used_ms) > ttl)
.map(|(nonce, _)| nonce.clone())
.collect();
let mut reaped = 0;
for nonce in expired {
if self.remove_session(&nonce).is_some() {
reaped += 1;
}
}
reaped
}
#[doc(hidden)]
pub fn seen_nonce_count(&self, session_nonce: &str) -> usize {
self.session_meta
.get(session_nonce)
.map(|m| m.seen.len())
.unwrap_or(0)
}
}
impl Default for SessionManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_session(nonce: &str, identity: &str, authenticated: bool) -> PeerSession {
PeerSession {
session_nonce: nonce.to_string(),
peer_identity_key: identity.to_string(),
peer_nonce: format!("peer_{}", nonce),
is_authenticated: authenticated,
}
}
#[test]
fn test_add_and_get_session() {
let mut mgr = SessionManager::new();
let session = make_session("nonce1", "id_key_A", true);
mgr.add_session(session.clone());
let retrieved = mgr.get_session("nonce1").unwrap();
assert_eq!(retrieved.session_nonce, "nonce1");
assert_eq!(retrieved.peer_identity_key, "id_key_A");
assert!(retrieved.is_authenticated);
}
#[test]
fn test_has_session() {
let mut mgr = SessionManager::new();
assert!(!mgr.has_session("nonce1"));
mgr.add_session(make_session("nonce1", "id_key_A", true));
assert!(mgr.has_session("nonce1"));
assert!(!mgr.has_session("nonce2"));
}
#[test]
fn test_remove_session() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", true));
let removed = mgr.remove_session("nonce1").unwrap();
assert_eq!(removed.session_nonce, "nonce1");
assert!(!mgr.has_session("nonce1"));
let sessions = mgr.get_sessions_for_identity("id_key_A");
assert!(sessions.is_empty());
}
#[test]
fn test_get_sessions_for_identity() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", true));
mgr.add_session(make_session("nonce2", "id_key_A", false));
mgr.add_session(make_session("nonce3", "id_key_B", true));
let a_sessions = mgr.get_sessions_for_identity("id_key_A");
assert_eq!(a_sessions.len(), 2);
let b_sessions = mgr.get_sessions_for_identity("id_key_B");
assert_eq!(b_sessions.len(), 1);
let c_sessions = mgr.get_sessions_for_identity("id_key_C");
assert!(c_sessions.is_empty());
}
#[test]
fn test_get_session_by_identifier() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", false));
mgr.add_session(make_session("nonce2", "id_key_A", true));
let s = mgr.get_session_by_identifier("nonce1").unwrap();
assert_eq!(s.session_nonce, "nonce1");
let best = mgr.get_session_by_identifier("id_key_A").unwrap();
assert!(best.is_authenticated);
}
#[test]
fn test_has_session_by_identifier() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", true));
assert!(mgr.has_session_by_identifier("nonce1"));
assert!(mgr.has_session_by_identifier("id_key_A"));
assert!(!mgr.has_session_by_identifier("unknown"));
}
#[test]
fn test_update_session() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", false));
let updated = make_session("nonce1", "id_key_A", true);
mgr.update_session("nonce1", updated);
let s = mgr.get_session("nonce1").unwrap();
assert!(s.is_authenticated);
}
#[test]
fn test_get_session_mut() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", false));
let s = mgr.get_session_mut("nonce1").unwrap();
s.is_authenticated = true;
let s2 = mgr.get_session("nonce1").unwrap();
assert!(s2.is_authenticated);
}
#[test]
fn test_remove_nonexistent() {
let mut mgr = SessionManager::new();
assert!(mgr.remove_session("nonexistent").is_none());
}
#[test]
fn test_identity_cleanup_on_remove_last_session() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("nonce1", "id_key_A", true));
mgr.add_session(make_session("nonce2", "id_key_A", true));
mgr.remove_session("nonce1");
assert!(mgr.has_session_by_identifier("id_key_A"));
mgr.remove_session("nonce2");
assert!(!mgr.has_session_by_identifier("id_key_A"));
}
#[test]
fn test_replay_seen_set_rejects_duplicate_but_accepts_fresh() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("sess1", "id_key_A", true));
mgr.touch("sess1", 1_000);
assert_eq!(mgr.mark_message_seen("sess1", "m1", 1_000), MarkSeen::Fresh);
assert_eq!(mgr.mark_message_seen("sess1", "m1", 1_001), MarkSeen::Replay);
assert_eq!(mgr.mark_message_seen("sess1", "m2", 1_002), MarkSeen::Fresh);
assert_eq!(
mgr.mark_message_seen("ghost", "m1", 1_003),
MarkSeen::SessionGone
);
}
#[test]
fn test_reap_idle_evicts_expired_and_frees_seen_set() {
let ttl = 60_000; let mut mgr = SessionManager::with_config(ttl, DEFAULT_SEEN_NONCE_CAP);
mgr.add_session(make_session("sess1", "id_key_A", true));
mgr.touch("sess1", 10_000);
mgr.mark_message_seen("sess1", "m1", 10_000);
mgr.mark_message_seen("sess1", "m2", 10_000);
assert_eq!(mgr.seen_nonce_count("sess1"), 2);
assert!(!mgr.is_expired("sess1", 10_000 + ttl));
assert_eq!(mgr.reap_idle(10_000 + ttl), 0);
assert!(mgr.has_session("sess1"));
assert!(mgr.is_expired("sess1", 10_000 + ttl + 1));
assert_eq!(mgr.reap_idle(10_000 + ttl + 1), 1);
assert!(!mgr.has_session("sess1"));
assert_eq!(mgr.seen_nonce_count("sess1"), 0);
assert!(mgr.get_active_session("sess1", 10_000 + ttl + 2).is_none());
}
#[test]
fn test_two_sessions_do_not_cross_contaminate() {
let mut mgr = SessionManager::new();
mgr.add_session(make_session("sessA", "id_key_A", true));
mgr.add_session(make_session("sessB", "id_key_B", true));
assert_eq!(
mgr.mark_message_seen("sessA", "shared", 1_000),
MarkSeen::Fresh
);
assert_eq!(
mgr.mark_message_seen("sessB", "shared", 1_000),
MarkSeen::Fresh
);
assert_eq!(
mgr.mark_message_seen("sessA", "shared", 1_001),
MarkSeen::Replay
);
assert_eq!(
mgr.mark_message_seen("sessB", "shared", 1_001),
MarkSeen::Replay
);
}
#[test]
fn test_seen_set_is_bounded_by_cap() {
let cap = 4;
let mut mgr = SessionManager::with_config(DEFAULT_SESSION_IDLE_TTL_MS, cap);
mgr.add_session(make_session("sess1", "id_key_A", true));
for i in 0..6 {
let n = format!("m{i}");
assert_eq!(mgr.mark_message_seen("sess1", &n, 1_000), MarkSeen::Fresh);
}
assert_eq!(mgr.seen_nonce_count("sess1"), cap);
assert_eq!(
mgr.mark_message_seen("sess1", "m5", 1_000),
MarkSeen::Replay
);
assert_eq!(mgr.mark_message_seen("sess1", "m0", 1_000), MarkSeen::Fresh);
}
#[test]
fn test_new_session_without_activity_is_not_expired() {
let mut mgr = SessionManager::with_config(1, DEFAULT_SEEN_NONCE_CAP);
mgr.add_session(make_session("sess1", "id_key_A", true));
assert!(!mgr.is_expired("sess1", u64::MAX));
assert!(mgr.get_active_session("sess1", u64::MAX).is_some());
assert_eq!(mgr.reap_idle(u64::MAX), 0);
}
}