1use std::collections::{HashMap, HashSet, VecDeque};
10
11use super::types::PeerSession;
12
13pub const DEFAULT_SESSION_IDLE_TTL_MS: u64 = 15 * 60 * 1000;
32
33pub const DEFAULT_SEEN_NONCE_CAP: usize = 10_000;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum MarkSeen {
44 Fresh,
46 Replay,
48 SessionGone,
50}
51
52struct SessionMeta {
58 last_used_ms: u64,
60 seen: HashSet<String>,
62 order: VecDeque<String>,
64}
65
66pub struct SessionManager {
75 nonce_to_session: HashMap<String, PeerSession>,
77 identity_to_nonces: HashMap<String, HashSet<String>>,
79 session_meta: HashMap<String, SessionMeta>,
83 idle_ttl_ms: u64,
85 seen_nonce_cap: usize,
87}
88
89impl SessionManager {
90 pub fn new() -> Self {
92 Self::with_config(DEFAULT_SESSION_IDLE_TTL_MS, DEFAULT_SEEN_NONCE_CAP)
93 }
94
95 pub fn with_config(idle_ttl_ms: u64, seen_nonce_cap: usize) -> Self {
99 SessionManager {
100 nonce_to_session: HashMap::new(),
101 identity_to_nonces: HashMap::new(),
102 session_meta: HashMap::new(),
103 idle_ttl_ms,
104 seen_nonce_cap,
105 }
106 }
107
108 pub fn idle_ttl_ms(&self) -> u64 {
110 self.idle_ttl_ms
111 }
112
113 pub fn add_session(&mut self, session: PeerSession) {
119 let nonce = session.session_nonce.clone();
120 let identity = session.peer_identity_key.clone();
121
122 self.nonce_to_session.insert(nonce.clone(), session);
123
124 self.identity_to_nonces
125 .entry(identity)
126 .or_default()
127 .insert(nonce);
128 }
129
130 pub fn get_session(&self, nonce: &str) -> Option<&PeerSession> {
132 self.nonce_to_session.get(nonce)
133 }
134
135 pub fn get_session_mut(&mut self, nonce: &str) -> Option<&mut PeerSession> {
137 self.nonce_to_session.get_mut(nonce)
138 }
139
140 pub fn get_sessions_for_identity(&self, identity_key: &str) -> Vec<&PeerSession> {
142 match self.identity_to_nonces.get(identity_key) {
143 Some(nonces) => nonces
144 .iter()
145 .filter_map(|n| self.nonce_to_session.get(n))
146 .collect(),
147 None => Vec::new(),
148 }
149 }
150
151 pub fn get_session_by_identifier(&self, identifier: &str) -> Option<&PeerSession> {
157 if let Some(session) = self.nonce_to_session.get(identifier) {
159 return Some(session);
160 }
161
162 let nonces = self.identity_to_nonces.get(identifier)?;
164 let mut best: Option<&PeerSession> = None;
165 for nonce in nonces {
166 if let Some(session) = self.nonce_to_session.get(nonce) {
167 match best {
168 None => best = Some(session),
169 Some(b) => {
170 if session.is_authenticated && !b.is_authenticated {
172 best = Some(session);
173 }
174 }
175 }
176 }
177 }
178 best
179 }
180
181 pub fn has_session(&self, nonce: &str) -> bool {
183 self.nonce_to_session.contains_key(nonce)
184 }
185
186 pub fn has_session_by_identifier(&self, identifier: &str) -> bool {
188 if self.nonce_to_session.contains_key(identifier) {
189 return true;
190 }
191 match self.identity_to_nonces.get(identifier) {
192 Some(nonces) => !nonces.is_empty(),
193 None => false,
194 }
195 }
196
197 pub fn update_session(&mut self, nonce: &str, session: PeerSession) {
199 if let Some(old_session) = self.nonce_to_session.get(nonce) {
201 let old_identity = old_session.peer_identity_key.clone();
202 if old_identity != session.peer_identity_key {
203 if let Some(nonces) = self.identity_to_nonces.get_mut(&old_identity) {
204 nonces.remove(nonce);
205 if nonces.is_empty() {
206 self.identity_to_nonces.remove(&old_identity);
207 }
208 }
209 }
210 }
211
212 let new_identity = session.peer_identity_key.clone();
213 self.nonce_to_session.insert(nonce.to_string(), session);
214 self.identity_to_nonces
215 .entry(new_identity)
216 .or_default()
217 .insert(nonce.to_string());
218 }
219
220 pub fn remove_session(&mut self, nonce: &str) -> Option<PeerSession> {
224 self.session_meta.remove(nonce);
226 if let Some(session) = self.nonce_to_session.remove(nonce) {
227 if let Some(nonces) = self.identity_to_nonces.get_mut(&session.peer_identity_key) {
229 nonces.remove(nonce);
230 if nonces.is_empty() {
231 self.identity_to_nonces.remove(&session.peer_identity_key);
232 }
233 }
234 Some(session)
235 } else {
236 None
237 }
238 }
239
240 pub fn touch(&mut self, session_nonce: &str, now_ms: u64) {
255 if !self.nonce_to_session.contains_key(session_nonce) {
256 return;
257 }
258 self.session_meta
259 .entry(session_nonce.to_string())
260 .or_insert_with(|| SessionMeta {
261 last_used_ms: now_ms,
262 seen: HashSet::new(),
263 order: VecDeque::new(),
264 })
265 .last_used_ms = now_ms;
266 }
267
268 pub fn is_expired(&self, session_nonce: &str, now_ms: u64) -> bool {
272 match self.session_meta.get(session_nonce) {
273 Some(m) => now_ms.saturating_sub(m.last_used_ms) > self.idle_ttl_ms,
274 None => false,
275 }
276 }
277
278 pub fn get_active_session(&self, identifier: &str, now_ms: u64) -> Option<&PeerSession> {
283 let session = self.get_session_by_identifier(identifier)?;
284 if self.is_expired(&session.session_nonce, now_ms) {
285 return None;
286 }
287 Some(session)
288 }
289
290 pub fn mark_message_seen(
305 &mut self,
306 session_nonce: &str,
307 message_nonce: &str,
308 now_ms: u64,
309 ) -> MarkSeen {
310 if !self.nonce_to_session.contains_key(session_nonce) {
311 return MarkSeen::SessionGone;
312 }
313 let cap = self.seen_nonce_cap;
314 let meta = self
315 .session_meta
316 .entry(session_nonce.to_string())
317 .or_insert_with(|| SessionMeta {
318 last_used_ms: now_ms,
319 seen: HashSet::new(),
320 order: VecDeque::new(),
321 });
322 meta.last_used_ms = now_ms;
323
324 if !meta.seen.insert(message_nonce.to_string()) {
325 return MarkSeen::Replay;
326 }
327 meta.order.push_back(message_nonce.to_string());
328 while meta.order.len() > cap {
329 if let Some(old) = meta.order.pop_front() {
330 meta.seen.remove(&old);
331 }
332 }
333 MarkSeen::Fresh
334 }
335
336 pub fn reap_idle(&mut self, now_ms: u64) -> usize {
342 let ttl = self.idle_ttl_ms;
343 let expired: Vec<String> = self
344 .session_meta
345 .iter()
346 .filter(|(_, m)| now_ms.saturating_sub(m.last_used_ms) > ttl)
347 .map(|(nonce, _)| nonce.clone())
348 .collect();
349 let mut reaped = 0;
350 for nonce in expired {
351 if self.remove_session(&nonce).is_some() {
352 reaped += 1;
353 }
354 }
355 reaped
356 }
357
358 #[doc(hidden)]
361 pub fn seen_nonce_count(&self, session_nonce: &str) -> usize {
362 self.session_meta
363 .get(session_nonce)
364 .map(|m| m.seen.len())
365 .unwrap_or(0)
366 }
367}
368
369impl Default for SessionManager {
370 fn default() -> Self {
371 Self::new()
372 }
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378
379 fn make_session(nonce: &str, identity: &str, authenticated: bool) -> PeerSession {
380 PeerSession {
381 session_nonce: nonce.to_string(),
382 peer_identity_key: identity.to_string(),
383 peer_nonce: format!("peer_{nonce}"),
384 is_authenticated: authenticated,
385 }
386 }
387
388 #[test]
389 fn test_add_and_get_session() {
390 let mut mgr = SessionManager::new();
391 let session = make_session("nonce1", "id_key_A", true);
392 mgr.add_session(session.clone());
393
394 let retrieved = mgr.get_session("nonce1").unwrap();
395 assert_eq!(retrieved.session_nonce, "nonce1");
396 assert_eq!(retrieved.peer_identity_key, "id_key_A");
397 assert!(retrieved.is_authenticated);
398 }
399
400 #[test]
401 fn test_has_session() {
402 let mut mgr = SessionManager::new();
403 assert!(!mgr.has_session("nonce1"));
404
405 mgr.add_session(make_session("nonce1", "id_key_A", true));
406 assert!(mgr.has_session("nonce1"));
407 assert!(!mgr.has_session("nonce2"));
408 }
409
410 #[test]
411 fn test_remove_session() {
412 let mut mgr = SessionManager::new();
413 mgr.add_session(make_session("nonce1", "id_key_A", true));
414
415 let removed = mgr.remove_session("nonce1").unwrap();
416 assert_eq!(removed.session_nonce, "nonce1");
417 assert!(!mgr.has_session("nonce1"));
418
419 let sessions = mgr.get_sessions_for_identity("id_key_A");
421 assert!(sessions.is_empty());
422 }
423
424 #[test]
425 fn test_get_sessions_for_identity() {
426 let mut mgr = SessionManager::new();
427 mgr.add_session(make_session("nonce1", "id_key_A", true));
428 mgr.add_session(make_session("nonce2", "id_key_A", false));
429 mgr.add_session(make_session("nonce3", "id_key_B", true));
430
431 let a_sessions = mgr.get_sessions_for_identity("id_key_A");
432 assert_eq!(a_sessions.len(), 2);
433
434 let b_sessions = mgr.get_sessions_for_identity("id_key_B");
435 assert_eq!(b_sessions.len(), 1);
436
437 let c_sessions = mgr.get_sessions_for_identity("id_key_C");
438 assert!(c_sessions.is_empty());
439 }
440
441 #[test]
442 fn test_get_session_by_identifier() {
443 let mut mgr = SessionManager::new();
444 mgr.add_session(make_session("nonce1", "id_key_A", false));
445 mgr.add_session(make_session("nonce2", "id_key_A", true));
446
447 let s = mgr.get_session_by_identifier("nonce1").unwrap();
449 assert_eq!(s.session_nonce, "nonce1");
450
451 let best = mgr.get_session_by_identifier("id_key_A").unwrap();
453 assert!(best.is_authenticated);
454 }
455
456 #[test]
457 fn test_has_session_by_identifier() {
458 let mut mgr = SessionManager::new();
459 mgr.add_session(make_session("nonce1", "id_key_A", true));
460
461 assert!(mgr.has_session_by_identifier("nonce1"));
462 assert!(mgr.has_session_by_identifier("id_key_A"));
463 assert!(!mgr.has_session_by_identifier("unknown"));
464 }
465
466 #[test]
467 fn test_update_session() {
468 let mut mgr = SessionManager::new();
469 mgr.add_session(make_session("nonce1", "id_key_A", false));
470
471 let updated = make_session("nonce1", "id_key_A", true);
473 mgr.update_session("nonce1", updated);
474
475 let s = mgr.get_session("nonce1").unwrap();
476 assert!(s.is_authenticated);
477 }
478
479 #[test]
480 fn test_get_session_mut() {
481 let mut mgr = SessionManager::new();
482 mgr.add_session(make_session("nonce1", "id_key_A", false));
483
484 let s = mgr.get_session_mut("nonce1").unwrap();
485 s.is_authenticated = true;
486
487 let s2 = mgr.get_session("nonce1").unwrap();
488 assert!(s2.is_authenticated);
489 }
490
491 #[test]
492 fn test_remove_nonexistent() {
493 let mut mgr = SessionManager::new();
494 assert!(mgr.remove_session("nonexistent").is_none());
495 }
496
497 #[test]
498 fn test_identity_cleanup_on_remove_last_session() {
499 let mut mgr = SessionManager::new();
500 mgr.add_session(make_session("nonce1", "id_key_A", true));
501 mgr.add_session(make_session("nonce2", "id_key_A", true));
502
503 mgr.remove_session("nonce1");
504 assert!(mgr.has_session_by_identifier("id_key_A"));
506
507 mgr.remove_session("nonce2");
508 assert!(!mgr.has_session_by_identifier("id_key_A"));
510 }
511
512 #[test]
519 fn test_replay_seen_set_rejects_duplicate_but_accepts_fresh() {
520 let mut mgr = SessionManager::new();
521 mgr.add_session(make_session("sess1", "id_key_A", true));
522 mgr.touch("sess1", 1_000);
523
524 assert_eq!(mgr.mark_message_seen("sess1", "m1", 1_000), MarkSeen::Fresh);
526 assert_eq!(
528 mgr.mark_message_seen("sess1", "m1", 1_001),
529 MarkSeen::Replay
530 );
531 assert_eq!(mgr.mark_message_seen("sess1", "m2", 1_002), MarkSeen::Fresh);
533 assert_eq!(
535 mgr.mark_message_seen("ghost", "m1", 1_003),
536 MarkSeen::SessionGone
537 );
538 }
539
540 #[test]
542 fn test_reap_idle_evicts_expired_and_frees_seen_set() {
543 let ttl = 60_000; let mut mgr = SessionManager::with_config(ttl, DEFAULT_SEEN_NONCE_CAP);
545 mgr.add_session(make_session("sess1", "id_key_A", true));
546 mgr.touch("sess1", 10_000);
547 mgr.mark_message_seen("sess1", "m1", 10_000);
548 mgr.mark_message_seen("sess1", "m2", 10_000);
549 assert_eq!(mgr.seen_nonce_count("sess1"), 2);
550
551 assert!(!mgr.is_expired("sess1", 10_000 + ttl));
553 assert_eq!(mgr.reap_idle(10_000 + ttl), 0);
554 assert!(mgr.has_session("sess1"));
555
556 assert!(mgr.is_expired("sess1", 10_000 + ttl + 1));
558 assert_eq!(mgr.reap_idle(10_000 + ttl + 1), 1);
559 assert!(!mgr.has_session("sess1"));
560 assert_eq!(mgr.seen_nonce_count("sess1"), 0);
561 assert!(mgr.get_active_session("sess1", 10_000 + ttl + 2).is_none());
563 }
564
565 #[test]
567 fn test_two_sessions_do_not_cross_contaminate() {
568 let mut mgr = SessionManager::new();
569 mgr.add_session(make_session("sessA", "id_key_A", true));
570 mgr.add_session(make_session("sessB", "id_key_B", true));
571
572 assert_eq!(
574 mgr.mark_message_seen("sessA", "shared", 1_000),
575 MarkSeen::Fresh
576 );
577 assert_eq!(
578 mgr.mark_message_seen("sessB", "shared", 1_000),
579 MarkSeen::Fresh
580 );
581 assert_eq!(
583 mgr.mark_message_seen("sessA", "shared", 1_001),
584 MarkSeen::Replay
585 );
586 assert_eq!(
587 mgr.mark_message_seen("sessB", "shared", 1_001),
588 MarkSeen::Replay
589 );
590 }
591
592 #[test]
595 fn test_seen_set_is_bounded_by_cap() {
596 let cap = 4;
597 let mut mgr = SessionManager::with_config(DEFAULT_SESSION_IDLE_TTL_MS, cap);
598 mgr.add_session(make_session("sess1", "id_key_A", true));
599
600 for i in 0..6 {
601 let n = format!("m{i}");
602 assert_eq!(mgr.mark_message_seen("sess1", &n, 1_000), MarkSeen::Fresh);
603 }
604 assert_eq!(mgr.seen_nonce_count("sess1"), cap);
606 assert_eq!(
608 mgr.mark_message_seen("sess1", "m5", 1_000),
609 MarkSeen::Replay
610 );
611 assert_eq!(mgr.mark_message_seen("sess1", "m0", 1_000), MarkSeen::Fresh);
613 }
614
615 #[test]
618 fn test_new_session_without_activity_is_not_expired() {
619 let mut mgr = SessionManager::with_config(1, DEFAULT_SEEN_NONCE_CAP);
620 mgr.add_session(make_session("sess1", "id_key_A", true));
621 assert!(!mgr.is_expired("sess1", u64::MAX));
623 assert!(mgr.get_active_session("sess1", u64::MAX).is_some());
624 assert_eq!(mgr.reap_idle(u64::MAX), 0);
625 }
626}