1use std::collections::{HashMap, HashSet, VecDeque};
10
11use super::types::PeerSession;
12
13pub const DEFAULT_SESSION_IDLE_TTL_MS: u64 = 15 * 60 * 1000;
22
23pub const DEFAULT_SEEN_NONCE_CAP: usize = 10_000;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum MarkSeen {
34 Fresh,
36 Replay,
38 SessionGone,
40}
41
42struct SessionMeta {
48 last_used_ms: u64,
50 seen: HashSet<String>,
52 order: VecDeque<String>,
54}
55
56pub struct SessionManager {
65 nonce_to_session: HashMap<String, PeerSession>,
67 identity_to_nonces: HashMap<String, HashSet<String>>,
69 session_meta: HashMap<String, SessionMeta>,
73 idle_ttl_ms: u64,
75 seen_nonce_cap: usize,
77}
78
79impl SessionManager {
80 pub fn new() -> Self {
82 Self::with_config(DEFAULT_SESSION_IDLE_TTL_MS, DEFAULT_SEEN_NONCE_CAP)
83 }
84
85 pub fn with_config(idle_ttl_ms: u64, seen_nonce_cap: usize) -> Self {
89 SessionManager {
90 nonce_to_session: HashMap::new(),
91 identity_to_nonces: HashMap::new(),
92 session_meta: HashMap::new(),
93 idle_ttl_ms,
94 seen_nonce_cap,
95 }
96 }
97
98 pub fn idle_ttl_ms(&self) -> u64 {
100 self.idle_ttl_ms
101 }
102
103 pub fn add_session(&mut self, session: PeerSession) {
109 let nonce = session.session_nonce.clone();
110 let identity = session.peer_identity_key.clone();
111
112 self.nonce_to_session.insert(nonce.clone(), session);
113
114 self.identity_to_nonces
115 .entry(identity)
116 .or_default()
117 .insert(nonce);
118 }
119
120 pub fn get_session(&self, nonce: &str) -> Option<&PeerSession> {
122 self.nonce_to_session.get(nonce)
123 }
124
125 pub fn get_session_mut(&mut self, nonce: &str) -> Option<&mut PeerSession> {
127 self.nonce_to_session.get_mut(nonce)
128 }
129
130 pub fn get_sessions_for_identity(&self, identity_key: &str) -> Vec<&PeerSession> {
132 match self.identity_to_nonces.get(identity_key) {
133 Some(nonces) => nonces
134 .iter()
135 .filter_map(|n| self.nonce_to_session.get(n))
136 .collect(),
137 None => Vec::new(),
138 }
139 }
140
141 pub fn get_session_by_identifier(&self, identifier: &str) -> Option<&PeerSession> {
147 if let Some(session) = self.nonce_to_session.get(identifier) {
149 return Some(session);
150 }
151
152 let nonces = self.identity_to_nonces.get(identifier)?;
154 let mut best: Option<&PeerSession> = None;
155 for nonce in nonces {
156 if let Some(session) = self.nonce_to_session.get(nonce) {
157 match best {
158 None => best = Some(session),
159 Some(b) => {
160 if session.is_authenticated && !b.is_authenticated {
162 best = Some(session);
163 }
164 }
165 }
166 }
167 }
168 best
169 }
170
171 pub fn has_session(&self, nonce: &str) -> bool {
173 self.nonce_to_session.contains_key(nonce)
174 }
175
176 pub fn has_session_by_identifier(&self, identifier: &str) -> bool {
178 if self.nonce_to_session.contains_key(identifier) {
179 return true;
180 }
181 match self.identity_to_nonces.get(identifier) {
182 Some(nonces) => !nonces.is_empty(),
183 None => false,
184 }
185 }
186
187 pub fn update_session(&mut self, nonce: &str, session: PeerSession) {
189 if let Some(old_session) = self.nonce_to_session.get(nonce) {
191 let old_identity = old_session.peer_identity_key.clone();
192 if old_identity != session.peer_identity_key {
193 if let Some(nonces) = self.identity_to_nonces.get_mut(&old_identity) {
194 nonces.remove(nonce);
195 if nonces.is_empty() {
196 self.identity_to_nonces.remove(&old_identity);
197 }
198 }
199 }
200 }
201
202 let new_identity = session.peer_identity_key.clone();
203 self.nonce_to_session.insert(nonce.to_string(), session);
204 self.identity_to_nonces
205 .entry(new_identity)
206 .or_default()
207 .insert(nonce.to_string());
208 }
209
210 pub fn remove_session(&mut self, nonce: &str) -> Option<PeerSession> {
214 self.session_meta.remove(nonce);
216 if let Some(session) = self.nonce_to_session.remove(nonce) {
217 if let Some(nonces) = self.identity_to_nonces.get_mut(&session.peer_identity_key) {
219 nonces.remove(nonce);
220 if nonces.is_empty() {
221 self.identity_to_nonces.remove(&session.peer_identity_key);
222 }
223 }
224 Some(session)
225 } else {
226 None
227 }
228 }
229
230 pub fn touch(&mut self, session_nonce: &str, now_ms: u64) {
245 if !self.nonce_to_session.contains_key(session_nonce) {
246 return;
247 }
248 self.session_meta
249 .entry(session_nonce.to_string())
250 .or_insert_with(|| SessionMeta {
251 last_used_ms: now_ms,
252 seen: HashSet::new(),
253 order: VecDeque::new(),
254 })
255 .last_used_ms = now_ms;
256 }
257
258 pub fn is_expired(&self, session_nonce: &str, now_ms: u64) -> bool {
262 match self.session_meta.get(session_nonce) {
263 Some(m) => now_ms.saturating_sub(m.last_used_ms) > self.idle_ttl_ms,
264 None => false,
265 }
266 }
267
268 pub fn get_active_session(&self, identifier: &str, now_ms: u64) -> Option<&PeerSession> {
273 let session = self.get_session_by_identifier(identifier)?;
274 if self.is_expired(&session.session_nonce, now_ms) {
275 return None;
276 }
277 Some(session)
278 }
279
280 pub fn mark_message_seen(
295 &mut self,
296 session_nonce: &str,
297 message_nonce: &str,
298 now_ms: u64,
299 ) -> MarkSeen {
300 if !self.nonce_to_session.contains_key(session_nonce) {
301 return MarkSeen::SessionGone;
302 }
303 let cap = self.seen_nonce_cap;
304 let meta = self
305 .session_meta
306 .entry(session_nonce.to_string())
307 .or_insert_with(|| SessionMeta {
308 last_used_ms: now_ms,
309 seen: HashSet::new(),
310 order: VecDeque::new(),
311 });
312 meta.last_used_ms = now_ms;
313
314 if !meta.seen.insert(message_nonce.to_string()) {
315 return MarkSeen::Replay;
316 }
317 meta.order.push_back(message_nonce.to_string());
318 while meta.order.len() > cap {
319 if let Some(old) = meta.order.pop_front() {
320 meta.seen.remove(&old);
321 }
322 }
323 MarkSeen::Fresh
324 }
325
326 pub fn reap_idle(&mut self, now_ms: u64) -> usize {
332 let ttl = self.idle_ttl_ms;
333 let expired: Vec<String> = self
334 .session_meta
335 .iter()
336 .filter(|(_, m)| now_ms.saturating_sub(m.last_used_ms) > ttl)
337 .map(|(nonce, _)| nonce.clone())
338 .collect();
339 let mut reaped = 0;
340 for nonce in expired {
341 if self.remove_session(&nonce).is_some() {
342 reaped += 1;
343 }
344 }
345 reaped
346 }
347
348 #[doc(hidden)]
351 pub fn seen_nonce_count(&self, session_nonce: &str) -> usize {
352 self.session_meta
353 .get(session_nonce)
354 .map(|m| m.seen.len())
355 .unwrap_or(0)
356 }
357}
358
359impl Default for SessionManager {
360 fn default() -> Self {
361 Self::new()
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 fn make_session(nonce: &str, identity: &str, authenticated: bool) -> PeerSession {
370 PeerSession {
371 session_nonce: nonce.to_string(),
372 peer_identity_key: identity.to_string(),
373 peer_nonce: format!("peer_{}", nonce),
374 is_authenticated: authenticated,
375 }
376 }
377
378 #[test]
379 fn test_add_and_get_session() {
380 let mut mgr = SessionManager::new();
381 let session = make_session("nonce1", "id_key_A", true);
382 mgr.add_session(session.clone());
383
384 let retrieved = mgr.get_session("nonce1").unwrap();
385 assert_eq!(retrieved.session_nonce, "nonce1");
386 assert_eq!(retrieved.peer_identity_key, "id_key_A");
387 assert!(retrieved.is_authenticated);
388 }
389
390 #[test]
391 fn test_has_session() {
392 let mut mgr = SessionManager::new();
393 assert!(!mgr.has_session("nonce1"));
394
395 mgr.add_session(make_session("nonce1", "id_key_A", true));
396 assert!(mgr.has_session("nonce1"));
397 assert!(!mgr.has_session("nonce2"));
398 }
399
400 #[test]
401 fn test_remove_session() {
402 let mut mgr = SessionManager::new();
403 mgr.add_session(make_session("nonce1", "id_key_A", true));
404
405 let removed = mgr.remove_session("nonce1").unwrap();
406 assert_eq!(removed.session_nonce, "nonce1");
407 assert!(!mgr.has_session("nonce1"));
408
409 let sessions = mgr.get_sessions_for_identity("id_key_A");
411 assert!(sessions.is_empty());
412 }
413
414 #[test]
415 fn test_get_sessions_for_identity() {
416 let mut mgr = SessionManager::new();
417 mgr.add_session(make_session("nonce1", "id_key_A", true));
418 mgr.add_session(make_session("nonce2", "id_key_A", false));
419 mgr.add_session(make_session("nonce3", "id_key_B", true));
420
421 let a_sessions = mgr.get_sessions_for_identity("id_key_A");
422 assert_eq!(a_sessions.len(), 2);
423
424 let b_sessions = mgr.get_sessions_for_identity("id_key_B");
425 assert_eq!(b_sessions.len(), 1);
426
427 let c_sessions = mgr.get_sessions_for_identity("id_key_C");
428 assert!(c_sessions.is_empty());
429 }
430
431 #[test]
432 fn test_get_session_by_identifier() {
433 let mut mgr = SessionManager::new();
434 mgr.add_session(make_session("nonce1", "id_key_A", false));
435 mgr.add_session(make_session("nonce2", "id_key_A", true));
436
437 let s = mgr.get_session_by_identifier("nonce1").unwrap();
439 assert_eq!(s.session_nonce, "nonce1");
440
441 let best = mgr.get_session_by_identifier("id_key_A").unwrap();
443 assert!(best.is_authenticated);
444 }
445
446 #[test]
447 fn test_has_session_by_identifier() {
448 let mut mgr = SessionManager::new();
449 mgr.add_session(make_session("nonce1", "id_key_A", true));
450
451 assert!(mgr.has_session_by_identifier("nonce1"));
452 assert!(mgr.has_session_by_identifier("id_key_A"));
453 assert!(!mgr.has_session_by_identifier("unknown"));
454 }
455
456 #[test]
457 fn test_update_session() {
458 let mut mgr = SessionManager::new();
459 mgr.add_session(make_session("nonce1", "id_key_A", false));
460
461 let updated = make_session("nonce1", "id_key_A", true);
463 mgr.update_session("nonce1", updated);
464
465 let s = mgr.get_session("nonce1").unwrap();
466 assert!(s.is_authenticated);
467 }
468
469 #[test]
470 fn test_get_session_mut() {
471 let mut mgr = SessionManager::new();
472 mgr.add_session(make_session("nonce1", "id_key_A", false));
473
474 let s = mgr.get_session_mut("nonce1").unwrap();
475 s.is_authenticated = true;
476
477 let s2 = mgr.get_session("nonce1").unwrap();
478 assert!(s2.is_authenticated);
479 }
480
481 #[test]
482 fn test_remove_nonexistent() {
483 let mut mgr = SessionManager::new();
484 assert!(mgr.remove_session("nonexistent").is_none());
485 }
486
487 #[test]
488 fn test_identity_cleanup_on_remove_last_session() {
489 let mut mgr = SessionManager::new();
490 mgr.add_session(make_session("nonce1", "id_key_A", true));
491 mgr.add_session(make_session("nonce2", "id_key_A", true));
492
493 mgr.remove_session("nonce1");
494 assert!(mgr.has_session_by_identifier("id_key_A"));
496
497 mgr.remove_session("nonce2");
498 assert!(!mgr.has_session_by_identifier("id_key_A"));
500 }
501
502 #[test]
509 fn test_replay_seen_set_rejects_duplicate_but_accepts_fresh() {
510 let mut mgr = SessionManager::new();
511 mgr.add_session(make_session("sess1", "id_key_A", true));
512 mgr.touch("sess1", 1_000);
513
514 assert_eq!(mgr.mark_message_seen("sess1", "m1", 1_000), MarkSeen::Fresh);
516 assert_eq!(mgr.mark_message_seen("sess1", "m1", 1_001), MarkSeen::Replay);
518 assert_eq!(mgr.mark_message_seen("sess1", "m2", 1_002), MarkSeen::Fresh);
520 assert_eq!(
522 mgr.mark_message_seen("ghost", "m1", 1_003),
523 MarkSeen::SessionGone
524 );
525 }
526
527 #[test]
529 fn test_reap_idle_evicts_expired_and_frees_seen_set() {
530 let ttl = 60_000; let mut mgr = SessionManager::with_config(ttl, DEFAULT_SEEN_NONCE_CAP);
532 mgr.add_session(make_session("sess1", "id_key_A", true));
533 mgr.touch("sess1", 10_000);
534 mgr.mark_message_seen("sess1", "m1", 10_000);
535 mgr.mark_message_seen("sess1", "m2", 10_000);
536 assert_eq!(mgr.seen_nonce_count("sess1"), 2);
537
538 assert!(!mgr.is_expired("sess1", 10_000 + ttl));
540 assert_eq!(mgr.reap_idle(10_000 + ttl), 0);
541 assert!(mgr.has_session("sess1"));
542
543 assert!(mgr.is_expired("sess1", 10_000 + ttl + 1));
545 assert_eq!(mgr.reap_idle(10_000 + ttl + 1), 1);
546 assert!(!mgr.has_session("sess1"));
547 assert_eq!(mgr.seen_nonce_count("sess1"), 0);
548 assert!(mgr.get_active_session("sess1", 10_000 + ttl + 2).is_none());
550 }
551
552 #[test]
554 fn test_two_sessions_do_not_cross_contaminate() {
555 let mut mgr = SessionManager::new();
556 mgr.add_session(make_session("sessA", "id_key_A", true));
557 mgr.add_session(make_session("sessB", "id_key_B", true));
558
559 assert_eq!(
561 mgr.mark_message_seen("sessA", "shared", 1_000),
562 MarkSeen::Fresh
563 );
564 assert_eq!(
565 mgr.mark_message_seen("sessB", "shared", 1_000),
566 MarkSeen::Fresh
567 );
568 assert_eq!(
570 mgr.mark_message_seen("sessA", "shared", 1_001),
571 MarkSeen::Replay
572 );
573 assert_eq!(
574 mgr.mark_message_seen("sessB", "shared", 1_001),
575 MarkSeen::Replay
576 );
577 }
578
579 #[test]
582 fn test_seen_set_is_bounded_by_cap() {
583 let cap = 4;
584 let mut mgr = SessionManager::with_config(DEFAULT_SESSION_IDLE_TTL_MS, cap);
585 mgr.add_session(make_session("sess1", "id_key_A", true));
586
587 for i in 0..6 {
588 let n = format!("m{i}");
589 assert_eq!(mgr.mark_message_seen("sess1", &n, 1_000), MarkSeen::Fresh);
590 }
591 assert_eq!(mgr.seen_nonce_count("sess1"), cap);
593 assert_eq!(
595 mgr.mark_message_seen("sess1", "m5", 1_000),
596 MarkSeen::Replay
597 );
598 assert_eq!(mgr.mark_message_seen("sess1", "m0", 1_000), MarkSeen::Fresh);
600 }
601
602 #[test]
605 fn test_new_session_without_activity_is_not_expired() {
606 let mut mgr = SessionManager::with_config(1, DEFAULT_SEEN_NONCE_CAP);
607 mgr.add_session(make_session("sess1", "id_key_A", true));
608 assert!(!mgr.is_expired("sess1", u64::MAX));
610 assert!(mgr.get_active_session("sess1", u64::MAX).is_some());
611 assert_eq!(mgr.reap_idle(u64::MAX), 0);
612 }
613}