1use std::net::IpAddr;
6use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
7use std::sync::Arc;
8use std::time::{Duration, SystemTime, UNIX_EPOCH};
9
10use dashmap::DashMap;
11use serde::{Deserialize, Serialize};
12use tokio::sync::Notify;
13
14#[derive(Debug, Clone, PartialEq)]
20pub enum SessionDecision {
21 Valid,
23 New,
25 Suspicious(HijackAlert),
27 Expired,
29 Invalid(String),
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
39pub struct HijackAlert {
40 pub session_id: String,
42 pub alert_type: HijackType,
44 pub original_value: String,
46 pub new_value: String,
48 pub timestamp: u64,
50 pub confidence: f64,
52}
53
54#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
56pub enum HijackType {
57 Ja4Mismatch,
59 IpChange,
61 ImpossibleTravel,
63 TokenRotation,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct SessionState {
74 pub session_id: String,
76 pub token_hash: String,
78 pub actor_id: Option<String>,
80 pub creation_time: u64,
82 pub last_activity: u64,
84 pub request_count: u64,
86 pub bound_ja4: Option<String>,
88 pub bound_ip: Option<IpAddr>,
90 pub is_suspicious: bool,
92 pub hijack_alerts: Vec<HijackAlert>,
94}
95
96impl SessionState {
97 pub fn new(session_id: String, token_hash: String) -> Self {
99 let now = now_ms();
100 Self {
101 session_id,
102 token_hash,
103 actor_id: None,
104 creation_time: now,
105 last_activity: now,
106 request_count: 0,
107 bound_ja4: None,
108 bound_ip: None,
109 is_suspicious: false,
110 hijack_alerts: Vec::new(),
111 }
112 }
113
114 pub fn touch(&mut self) {
116 self.last_activity = now_ms();
117 self.request_count += 1;
118 }
119
120 pub fn bind_ja4(&mut self, ja4: String) {
122 if self.bound_ja4.is_none() && !ja4.is_empty() {
123 self.bound_ja4 = Some(ja4);
124 }
125 }
126
127 pub fn bind_ip(&mut self, ip: IpAddr) {
129 if self.bound_ip.is_none() {
130 self.bound_ip = Some(ip);
131 }
132 }
133
134 pub fn add_alert(&mut self, alert: HijackAlert) {
136 self.is_suspicious = true;
137 self.hijack_alerts.push(alert);
138 }
139}
140
141#[derive(Debug, Clone)]
147pub struct SessionConfig {
148 pub max_sessions: usize,
151
152 pub session_ttl_secs: u64,
155
156 pub idle_timeout_secs: u64,
159
160 pub cleanup_interval_secs: u64,
163
164 pub enable_ja4_binding: bool,
167
168 pub enable_ip_binding: bool,
171
172 pub ja4_mismatch_threshold: u32,
175
176 pub ip_change_window_secs: u64,
179
180 pub max_alerts_per_session: usize,
183
184 pub enabled: bool,
187}
188
189impl Default for SessionConfig {
190 fn default() -> Self {
191 Self {
192 max_sessions: 50_000,
193 session_ttl_secs: 3600,
194 idle_timeout_secs: 900,
195 cleanup_interval_secs: 300,
196 enable_ja4_binding: true,
197 enable_ip_binding: false,
198 ja4_mismatch_threshold: 1,
199 ip_change_window_secs: 60,
200 max_alerts_per_session: 10,
201 enabled: true,
202 }
203 }
204}
205
206#[derive(Debug, Default)]
212pub struct SessionStats {
213 pub total_sessions: AtomicU64,
215 pub active_sessions: AtomicU64,
217 pub suspicious_sessions: AtomicU64,
219 pub hijack_alerts: AtomicU64,
221 pub expired_sessions: AtomicU64,
223 pub evictions: AtomicU64,
225 pub total_created: AtomicU64,
227 pub total_invalidated: AtomicU64,
229}
230
231impl SessionStats {
232 pub fn new() -> Self {
234 Self::default()
235 }
236
237 pub fn snapshot(&self) -> SessionStatsSnapshot {
239 SessionStatsSnapshot {
240 total_sessions: self.total_sessions.load(Ordering::Relaxed),
241 active_sessions: self.active_sessions.load(Ordering::Relaxed),
242 suspicious_sessions: self.suspicious_sessions.load(Ordering::Relaxed),
243 hijack_alerts: self.hijack_alerts.load(Ordering::Relaxed),
244 expired_sessions: self.expired_sessions.load(Ordering::Relaxed),
245 evictions: self.evictions.load(Ordering::Relaxed),
246 total_created: self.total_created.load(Ordering::Relaxed),
247 total_invalidated: self.total_invalidated.load(Ordering::Relaxed),
248 }
249 }
250}
251
252#[derive(Debug, Clone, Serialize)]
254pub struct SessionStatsSnapshot {
255 pub total_sessions: u64,
256 pub active_sessions: u64,
257 pub suspicious_sessions: u64,
258 pub hijack_alerts: u64,
259 pub expired_sessions: u64,
260 pub evictions: u64,
261 pub total_created: u64,
262 pub total_invalidated: u64,
263}
264
265pub struct SessionManager {
273 sessions: DashMap<String, SessionState>,
275
276 session_by_id: DashMap<String, String>,
278
279 actor_sessions: DashMap<String, Vec<String>>,
281
282 config: SessionConfig,
284
285 stats: Arc<SessionStats>,
287
288 shutdown: Arc<Notify>,
290
291 touch_counter: AtomicU32,
293}
294
295impl SessionManager {
296 pub fn new(config: SessionConfig) -> Self {
298 Self {
299 sessions: DashMap::with_capacity(config.max_sessions),
300 session_by_id: DashMap::with_capacity(config.max_sessions),
301 actor_sessions: DashMap::with_capacity(config.max_sessions / 10),
302 config,
303 stats: Arc::new(SessionStats::new()),
304 shutdown: Arc::new(Notify::new()),
305 touch_counter: AtomicU32::new(0),
306 }
307 }
308
309 pub fn config(&self) -> &SessionConfig {
311 &self.config
312 }
313
314 pub fn is_enabled(&self) -> bool {
316 self.config.enabled
317 }
318
319 pub fn len(&self) -> usize {
321 self.sessions.len()
322 }
323
324 pub fn is_empty(&self) -> bool {
326 self.sessions.is_empty()
327 }
328
329 pub fn validate_request(
345 &self,
346 token_hash: &str,
347 ip: IpAddr,
348 ja4: Option<&str>,
349 ) -> SessionDecision {
350 if !self.config.enabled {
351 return SessionDecision::Valid;
352 }
353
354 self.maybe_evict();
356
357 match self.sessions.entry(token_hash.to_string()) {
360 dashmap::mapref::entry::Entry::Occupied(mut entry) => {
361 let session = entry.get_mut();
362
363 if self.is_session_expired(session) {
365 let session_id = session.session_id.clone();
366 let actor_id = session.actor_id.clone();
367 let was_suspicious = session.is_suspicious;
368
369 entry.remove();
371
372 self.session_by_id.remove(&session_id);
374 if let Some(aid) = actor_id {
375 if let Some(mut actor_entry) = self.actor_sessions.get_mut(&aid) {
376 actor_entry.retain(|id| id != &session_id);
377 }
378 }
379
380 self.stats.total_sessions.fetch_sub(1, Ordering::Relaxed);
382 self.stats.active_sessions.fetch_sub(1, Ordering::Relaxed);
383 self.stats.expired_sessions.fetch_add(1, Ordering::Relaxed);
384 if was_suspicious {
385 self.stats
386 .suspicious_sessions
387 .fetch_sub(1, Ordering::Relaxed);
388 }
389
390 return SessionDecision::Expired;
391 }
392
393 if let Some(alert) = self.detect_hijack(session, ip, ja4) {
395 let was_suspicious = session.is_suspicious;
396 session.add_alert(alert.clone());
397 session.touch();
398
399 if session.hijack_alerts.len() > self.config.max_alerts_per_session {
401 let excess =
402 session.hijack_alerts.len() - self.config.max_alerts_per_session;
403 session.hijack_alerts.drain(0..excess);
404 }
405
406 self.stats.hijack_alerts.fetch_add(1, Ordering::Relaxed);
407
408 if !was_suspicious {
410 self.stats
411 .suspicious_sessions
412 .fetch_add(1, Ordering::Relaxed);
413 }
414
415 return SessionDecision::Suspicious(alert);
416 }
417
418 session.touch();
420
421 if let Some(ja4_str) = ja4 {
423 session.bind_ja4(ja4_str.to_string());
424 }
425
426 if self.config.enable_ip_binding {
427 session.bind_ip(ip);
428 }
429
430 SessionDecision::Valid
431 }
432 dashmap::mapref::entry::Entry::Vacant(entry) => {
433 let session_id = generate_session_id();
435 let mut session = SessionState::new(session_id.clone(), token_hash.to_string());
436 session.touch();
437
438 if let Some(ja4_str) = ja4 {
440 session.bind_ja4(ja4_str.to_string());
441 }
442
443 if self.config.enable_ip_binding {
444 session.bind_ip(ip);
445 }
446
447 entry.insert(session);
449
450 self.session_by_id
452 .insert(session_id, token_hash.to_string());
453
454 self.stats.total_sessions.fetch_add(1, Ordering::Relaxed);
456 self.stats.active_sessions.fetch_add(1, Ordering::Relaxed);
457 self.stats.total_created.fetch_add(1, Ordering::Relaxed);
458
459 SessionDecision::New
460 }
461 }
462 }
463
464 pub fn create_session(&self, token_hash: &str, ip: IpAddr, ja4: Option<&str>) -> SessionState {
474 if !self.config.enabled {
475 return SessionState::new(generate_session_id(), token_hash.to_string());
476 }
477
478 self.maybe_evict();
480
481 let session_id = generate_session_id();
482 let mut session = SessionState::new(session_id.clone(), token_hash.to_string());
483 session.touch();
484
485 if let Some(ja4_str) = ja4 {
487 session.bind_ja4(ja4_str.to_string());
488 }
489
490 if self.config.enable_ip_binding {
491 session.bind_ip(ip);
492 }
493
494 self.session_by_id
496 .insert(session_id.clone(), token_hash.to_string());
497 self.sessions
498 .insert(token_hash.to_string(), session.clone());
499
500 self.stats.total_sessions.fetch_add(1, Ordering::Relaxed);
502 self.stats.active_sessions.fetch_add(1, Ordering::Relaxed);
503 self.stats.total_created.fetch_add(1, Ordering::Relaxed);
504
505 session
506 }
507
508 pub fn get_session(&self, token_hash: &str) -> Option<SessionState> {
510 self.sessions
511 .get(token_hash)
512 .map(|entry| entry.value().clone())
513 }
514
515 pub fn get_session_by_id(&self, session_id: &str) -> Option<SessionState> {
517 self.session_by_id.get(session_id).and_then(|token_hash| {
518 self.sessions
519 .get(token_hash.value())
520 .map(|e| e.value().clone())
521 })
522 }
523
524 pub fn touch_session(&self, token_hash: &str) {
526 if let Some(mut entry) = self.sessions.get_mut(token_hash) {
527 entry.value_mut().touch();
528 }
529 }
530
531 pub fn bind_to_actor(&self, token_hash: &str, actor_id: &str) -> bool {
543 match self.sessions.entry(token_hash.to_string()) {
545 dashmap::mapref::entry::Entry::Occupied(mut entry) => {
546 let session = entry.get_mut();
547
548 if session.actor_id.as_deref() == Some(actor_id) {
550 return true;
551 }
552
553 if let Some(ref old_actor_id) = session.actor_id {
555 if let Some(mut old_actor_entry) = self.actor_sessions.get_mut(old_actor_id) {
556 old_actor_entry.retain(|id| id != &session.session_id);
557 }
558 }
559
560 let session_id = session.session_id.clone();
561
562 session.actor_id = Some(actor_id.to_string());
564
565 self.actor_sessions
567 .entry(actor_id.to_string())
568 .or_insert_with(Vec::new)
569 .push(session_id);
570
571 true
572 }
573 dashmap::mapref::entry::Entry::Vacant(_) => false,
574 }
575 }
576
577 pub fn get_actor_sessions(&self, actor_id: &str) -> Vec<SessionState> {
585 self.actor_sessions
586 .get(actor_id)
587 .map(|session_ids| {
588 session_ids
589 .iter()
590 .filter_map(|session_id| self.get_session_by_id(session_id))
591 .collect()
592 })
593 .unwrap_or_default()
594 }
595
596 pub fn list_sessions_by_actor(
600 &self,
601 actor_id: &str,
602 limit: usize,
603 offset: usize,
604 ) -> Vec<SessionState> {
605 let mut sessions = self.get_actor_sessions(actor_id);
606 sessions.sort_by_key(|s| std::cmp::Reverse(s.last_activity));
607 sessions.into_iter().skip(offset).take(limit).collect()
608 }
609
610 pub fn invalidate_session(&self, token_hash: &str) -> bool {
618 if self.remove_session(token_hash) {
619 self.stats.total_invalidated.fetch_add(1, Ordering::Relaxed);
620 true
621 } else {
622 false
623 }
624 }
625
626 pub fn mark_suspicious(&self, token_hash: &str, alert: HijackAlert) -> bool {
637 match self.sessions.entry(token_hash.to_string()) {
639 dashmap::mapref::entry::Entry::Occupied(mut entry) => {
640 let session = entry.get_mut();
641 let was_suspicious = session.is_suspicious;
642 session.add_alert(alert);
643
644 if session.hijack_alerts.len() > self.config.max_alerts_per_session {
646 let excess = session.hijack_alerts.len() - self.config.max_alerts_per_session;
647 session.hijack_alerts.drain(0..excess);
648 }
649
650 self.stats.hijack_alerts.fetch_add(1, Ordering::Relaxed);
651
652 if !was_suspicious {
654 self.stats
655 .suspicious_sessions
656 .fetch_add(1, Ordering::Relaxed);
657 }
658
659 true
660 }
661 dashmap::mapref::entry::Entry::Vacant(_) => false,
662 }
663 }
664
665 pub fn list_sessions(&self, limit: usize, offset: usize) -> Vec<SessionState> {
674 let mut sessions: Vec<SessionState> = self
675 .sessions
676 .iter()
677 .map(|entry| entry.value().clone())
678 .collect();
679
680 sessions.sort_by_key(|s| std::cmp::Reverse(s.last_activity));
682
683 sessions.into_iter().skip(offset).take(limit).collect()
685 }
686
687 pub fn list_suspicious_sessions(&self) -> Vec<SessionState> {
692 self.sessions
693 .iter()
694 .filter(|entry| entry.value().is_suspicious)
695 .map(|entry| entry.value().clone())
696 .collect()
697 }
698
699 pub fn list_suspicious_sessions_paginated(
703 &self,
704 limit: usize,
705 offset: usize,
706 ) -> Vec<SessionState> {
707 let mut sessions = self.list_suspicious_sessions();
708 sessions.sort_by_key(|s| std::cmp::Reverse(s.last_activity));
709 sessions.into_iter().skip(offset).take(limit).collect()
710 }
711
712 pub fn start_background_tasks(self: Arc<Self>) {
718 let manager = self;
719 let cleanup_interval = Duration::from_secs(manager.config.cleanup_interval_secs);
720
721 tokio::spawn(async move {
722 let mut interval = tokio::time::interval(cleanup_interval);
723
724 loop {
725 tokio::select! {
726 _ = interval.tick() => {
727 if Arc::strong_count(&manager.shutdown) == 1 {
729 break;
731 }
732
733 manager.cleanup_expired_sessions();
735
736 manager.evict_if_needed();
738 }
739 _ = manager.shutdown.notified() => {
740 log::info!("Session manager background tasks shutting down");
741 break;
742 }
743 }
744 }
745 });
746 }
747
748 pub fn shutdown(&self) {
750 self.shutdown.notify_one();
751 }
752
753 pub fn stats(&self) -> &SessionStats {
755 &self.stats
756 }
757
758 pub fn clear(&self) {
760 self.sessions.clear();
761 self.session_by_id.clear();
762 self.actor_sessions.clear();
763 self.stats.total_sessions.store(0, Ordering::Relaxed);
764 self.stats.active_sessions.store(0, Ordering::Relaxed);
765 self.stats.suspicious_sessions.store(0, Ordering::Relaxed);
766 }
767
768 fn detect_hijack(
782 &self,
783 session: &SessionState,
784 ip: IpAddr,
785 ja4: Option<&str>,
786 ) -> Option<HijackAlert> {
787 let now = now_ms();
788
789 if self.config.enable_ja4_binding {
791 if let (Some(bound_ja4), Some(current_ja4)) = (&session.bound_ja4, ja4) {
792 if bound_ja4 != current_ja4 {
793 return Some(HijackAlert {
794 session_id: session.session_id.clone(),
795 alert_type: HijackType::Ja4Mismatch,
796 original_value: bound_ja4.clone(),
797 new_value: current_ja4.to_string(),
798 timestamp: now,
799 confidence: 0.9, });
801 }
802 }
803 }
804
805 if self.config.enable_ip_binding {
807 if let Some(bound_ip) = session.bound_ip {
808 if bound_ip != ip {
809 let time_since_last = now.saturating_sub(session.last_activity);
812 let window_ms = self.config.ip_change_window_secs * 1000;
813
814 if time_since_last >= window_ms {
815 return Some(HijackAlert {
816 session_id: session.session_id.clone(),
817 alert_type: HijackType::IpChange,
818 original_value: bound_ip.to_string(),
819 new_value: ip.to_string(),
820 timestamp: now,
821 confidence: 0.7, });
823 }
824 }
825 }
826 }
827
828 None
829 }
830
831 fn is_session_expired(&self, session: &SessionState) -> bool {
839 let now = now_ms();
840
841 let ttl_ms = self.config.session_ttl_secs * 1000;
843 if now.saturating_sub(session.creation_time) > ttl_ms {
844 return true;
845 }
846
847 let idle_ms = self.config.idle_timeout_secs * 1000;
849 if now.saturating_sub(session.last_activity) > idle_ms {
850 return true;
851 }
852
853 false
854 }
855
856 fn cleanup_expired_sessions(&self) {
858 let mut to_remove = Vec::new();
859
860 for entry in self.sessions.iter() {
861 if self.is_session_expired(entry.value()) {
862 to_remove.push(entry.key().clone());
863 }
864 }
865
866 for token_hash in to_remove {
867 self.remove_session(&token_hash);
868 self.stats.expired_sessions.fetch_add(1, Ordering::Relaxed);
869 }
870 }
871
872 fn evict_if_needed(&self) {
874 let current_len = self.sessions.len();
875 if current_len <= self.config.max_sessions {
876 return;
877 }
878
879 let evict_count = (self.config.max_sessions / 100).max(1);
881 self.evict_oldest(evict_count);
882 }
883
884 fn maybe_evict(&self) {
888 let count = self.touch_counter.fetch_add(1, Ordering::Relaxed);
889 if !count.is_multiple_of(100) {
890 return;
891 }
892
893 if self.sessions.len() < self.config.max_sessions {
894 return;
895 }
896
897 let evict_count = (self.config.max_sessions / 100).max(1);
899 self.evict_oldest(evict_count);
900 }
901
902 fn evict_oldest(&self, count: usize) {
906 let sample_size = (count * 10).min(1000).min(self.sessions.len());
907
908 if sample_size == 0 {
909 return;
910 }
911
912 let mut candidates: Vec<(String, u64)> = Vec::with_capacity(sample_size);
914 for entry in self.sessions.iter().take(sample_size) {
915 candidates.push((entry.key().clone(), entry.value().last_activity));
916 }
917
918 candidates.sort_unstable_by_key(|(_, ts)| *ts);
920
921 for (token_hash, _) in candidates.into_iter().take(count) {
923 self.remove_session(&token_hash);
924 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
925 }
926 }
927
928 fn remove_session(&self, token_hash: &str) -> bool {
930 if let Some((_, session)) = self.sessions.remove(token_hash) {
931 self.session_by_id.remove(&session.session_id);
933
934 if let Some(actor_id) = &session.actor_id {
936 if let Some(mut entry) = self.actor_sessions.get_mut(actor_id) {
937 entry.retain(|id| id != &session.session_id);
938 if entry.is_empty() {
939 drop(entry);
940 self.actor_sessions.remove(actor_id);
941 }
942 }
943 }
944
945 self.stats.total_sessions.fetch_sub(1, Ordering::Relaxed);
947 self.stats.active_sessions.fetch_sub(1, Ordering::Relaxed);
948
949 if session.is_suspicious {
950 self.stats
951 .suspicious_sessions
952 .fetch_sub(1, Ordering::Relaxed);
953 }
954
955 return true;
956 }
957
958 false
959 }
960}
961
962impl Default for SessionManager {
963 fn default() -> Self {
964 Self::new(SessionConfig::default())
965 }
966}
967
968fn generate_session_id() -> String {
974 let mut bytes = [0u8; 16];
976 if let Err(err) = getrandom::getrandom(&mut bytes) {
977 log::error!("Failed to get random bytes for session id: {}", err);
978 for byte in bytes.iter_mut() {
979 *byte = fastrand::u8(..);
980 }
981 }
982
983 bytes[6] = (bytes[6] & 0x0F) | 0x40; bytes[8] = (bytes[8] & 0x3F) | 0x80; format!(
988 "sess-{:08x}-{:04x}-{:04x}-{:04x}-{:012x}",
989 u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
990 u16::from_be_bytes([bytes[4], bytes[5]]),
991 u16::from_be_bytes([bytes[6], bytes[7]]),
992 u16::from_be_bytes([bytes[8], bytes[9]]),
993 u64::from_be_bytes([
994 0, 0, bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15]
995 ])
996 )
997}
998
999#[inline]
1001fn now_ms() -> u64 {
1002 SystemTime::now()
1003 .duration_since(UNIX_EPOCH)
1004 .map(|d| d.as_millis() as u64)
1005 .unwrap_or(0)
1006}
1007
1008#[cfg(test)]
1013mod tests {
1014 use super::*;
1015 use std::thread;
1016
1017 fn create_test_manager() -> SessionManager {
1022 SessionManager::new(SessionConfig {
1023 max_sessions: 1000,
1024 session_ttl_secs: 3600,
1025 idle_timeout_secs: 900,
1026 ..Default::default()
1027 })
1028 }
1029
1030 fn create_test_ip(last_octet: u8) -> IpAddr {
1031 format!("192.168.1.{}", last_octet).parse().unwrap()
1032 }
1033
1034 #[test]
1039 fn test_session_creation() {
1040 let manager = create_test_manager();
1041 let ip = create_test_ip(1);
1042
1043 let session = manager.create_session("token_hash_1", ip, None);
1044
1045 assert!(!session.session_id.is_empty());
1046 assert!(session.session_id.starts_with("sess-"));
1047 assert_eq!(session.token_hash, "token_hash_1");
1048 assert_eq!(manager.len(), 1);
1049 }
1050
1051 #[test]
1052 fn test_session_retrieval_by_token_hash() {
1053 let manager = create_test_manager();
1054 let ip = create_test_ip(1);
1055
1056 manager.create_session("token_hash_1", ip, None);
1057
1058 let retrieved = manager.get_session("token_hash_1").unwrap();
1059 assert_eq!(retrieved.token_hash, "token_hash_1");
1060 }
1061
1062 #[test]
1063 fn test_session_retrieval_by_id() {
1064 let manager = create_test_manager();
1065 let ip = create_test_ip(1);
1066
1067 let session = manager.create_session("token_hash_1", ip, None);
1068 let retrieved = manager.get_session_by_id(&session.session_id).unwrap();
1069
1070 assert_eq!(retrieved.token_hash, "token_hash_1");
1071 }
1072
1073 #[test]
1074 fn test_session_nonexistent() {
1075 let manager = create_test_manager();
1076
1077 assert!(manager.get_session("nonexistent").is_none());
1078 assert!(manager.get_session_by_id("nonexistent").is_none());
1079 }
1080
1081 #[test]
1086 fn test_validate_new_session() {
1087 let manager = create_test_manager();
1088 let ip = create_test_ip(1);
1089
1090 let decision = manager.validate_request("token_hash_1", ip, None);
1091
1092 assert_eq!(decision, SessionDecision::New);
1093 assert_eq!(manager.len(), 1);
1094 }
1095
1096 #[test]
1097 fn test_validate_existing_session() {
1098 let manager = create_test_manager();
1099 let ip = create_test_ip(1);
1100
1101 manager.create_session("token_hash_1", ip, Some("ja4_fingerprint"));
1103
1104 let decision = manager.validate_request("token_hash_1", ip, Some("ja4_fingerprint"));
1106
1107 assert_eq!(decision, SessionDecision::Valid);
1108 assert_eq!(manager.len(), 1);
1109 }
1110
1111 #[test]
1112 fn test_validate_increments_request_count() {
1113 let manager = create_test_manager();
1114 let ip = create_test_ip(1);
1115
1116 manager.validate_request("token_hash_1", ip, None);
1117 manager.validate_request("token_hash_1", ip, None);
1118 manager.validate_request("token_hash_1", ip, None);
1119
1120 let session = manager.get_session("token_hash_1").unwrap();
1121 assert_eq!(session.request_count, 3);
1122 }
1123
1124 #[test]
1129 fn test_ja4_binding() {
1130 let manager = create_test_manager();
1131 let ip = create_test_ip(1);
1132
1133 manager.create_session("token_hash_1", ip, Some("ja4_fingerprint_1"));
1134
1135 let session = manager.get_session("token_hash_1").unwrap();
1136 assert_eq!(session.bound_ja4, Some("ja4_fingerprint_1".to_string()));
1137 }
1138
1139 #[test]
1140 fn test_ja4_mismatch_detection() {
1141 let manager = create_test_manager();
1142 let ip = create_test_ip(1);
1143
1144 manager.create_session("token_hash_1", ip, Some("ja4_fingerprint_1"));
1146
1147 let decision = manager.validate_request("token_hash_1", ip, Some("ja4_fingerprint_2"));
1149
1150 match decision {
1151 SessionDecision::Suspicious(alert) => {
1152 assert_eq!(alert.alert_type, HijackType::Ja4Mismatch);
1153 assert_eq!(alert.original_value, "ja4_fingerprint_1");
1154 assert_eq!(alert.new_value, "ja4_fingerprint_2");
1155 assert!(alert.confidence >= 0.9);
1156 }
1157 _ => panic!("Expected Suspicious decision, got {:?}", decision),
1158 }
1159 }
1160
1161 #[test]
1162 fn test_ja4_binding_first_value_only() {
1163 let manager = create_test_manager();
1164 let ip = create_test_ip(1);
1165
1166 manager.create_session("token_hash_1", ip, None);
1168
1169 manager.validate_request("token_hash_1", ip, Some("ja4_fingerprint_1"));
1171
1172 let session = manager.get_session("token_hash_1").unwrap();
1173 assert_eq!(session.bound_ja4, Some("ja4_fingerprint_1".to_string()));
1174 }
1175
1176 #[test]
1177 fn test_ja4_binding_disabled() {
1178 let config = SessionConfig {
1179 enable_ja4_binding: false,
1180 ..Default::default()
1181 };
1182 let manager = SessionManager::new(config);
1183 let ip = create_test_ip(1);
1184
1185 manager.create_session("token_hash_1", ip, Some("ja4_fingerprint_1"));
1187
1188 let decision = manager.validate_request("token_hash_1", ip, Some("ja4_fingerprint_2"));
1190
1191 assert_eq!(decision, SessionDecision::Valid);
1192 }
1193
1194 #[test]
1199 fn test_ip_binding_strict_mode_within_window() {
1200 let config = SessionConfig {
1201 enable_ip_binding: true,
1202 ip_change_window_secs: 60,
1203 ..Default::default()
1204 };
1205 let manager = SessionManager::new(config);
1206 let ip1 = create_test_ip(1);
1207 let ip2 = create_test_ip(2);
1208
1209 manager.create_session("token_hash_1", ip1, None);
1211
1212 let decision = manager.validate_request("token_hash_1", ip2, None);
1214
1215 assert_eq!(decision, SessionDecision::Valid);
1217 }
1218
1219 #[test]
1220 fn test_ip_binding_strict_mode_outside_window() {
1221 let config = SessionConfig {
1222 enable_ip_binding: true,
1223 ip_change_window_secs: 0, ..Default::default()
1225 };
1226 let manager = SessionManager::new(config);
1227 let ip1 = create_test_ip(1);
1228 let ip2 = create_test_ip(2);
1229
1230 manager.create_session("token_hash_1", ip1, None);
1232
1233 std::thread::sleep(std::time::Duration::from_millis(10));
1235
1236 let decision = manager.validate_request("token_hash_1", ip2, None);
1238
1239 match decision {
1240 SessionDecision::Suspicious(alert) => {
1241 assert_eq!(alert.alert_type, HijackType::IpChange);
1242 assert!(alert.confidence >= 0.5 && alert.confidence < 0.9);
1243 }
1244 _ => panic!("Expected Suspicious decision, got {:?}", decision),
1245 }
1246 }
1247
1248 #[test]
1249 fn test_ip_binding_disabled_by_default() {
1250 let manager = create_test_manager();
1251 let ip1 = create_test_ip(1);
1252 let ip2 = create_test_ip(2);
1253
1254 manager.create_session("token_hash_1", ip1, None);
1256
1257 let decision = manager.validate_request("token_hash_1", ip2, None);
1259
1260 assert_eq!(decision, SessionDecision::Valid);
1261 }
1262
1263 #[test]
1268 fn test_session_ttl_expiration() {
1269 let config = SessionConfig {
1270 session_ttl_secs: 0, idle_timeout_secs: 3600,
1272 ..Default::default()
1273 };
1274 let manager = SessionManager::new(config);
1275 let ip = create_test_ip(1);
1276
1277 manager.create_session("token_hash_1", ip, None);
1278
1279 std::thread::sleep(std::time::Duration::from_millis(10));
1281
1282 let decision = manager.validate_request("token_hash_1", ip, None);
1283 assert_eq!(decision, SessionDecision::Expired);
1284 }
1285
1286 #[test]
1287 fn test_session_idle_expiration() {
1288 let config = SessionConfig {
1289 session_ttl_secs: 3600,
1290 idle_timeout_secs: 0, ..Default::default()
1292 };
1293 let manager = SessionManager::new(config);
1294 let ip = create_test_ip(1);
1295
1296 manager.create_session("token_hash_1", ip, None);
1297
1298 std::thread::sleep(std::time::Duration::from_millis(10));
1300
1301 let decision = manager.validate_request("token_hash_1", ip, None);
1302 assert_eq!(decision, SessionDecision::Expired);
1303 }
1304
1305 #[test]
1310 fn test_bind_to_actor() {
1311 let manager = create_test_manager();
1312 let ip = create_test_ip(1);
1313
1314 manager.create_session("token_hash_1", ip, None);
1315 let result = manager.bind_to_actor("token_hash_1", "actor_123");
1316
1317 assert!(result);
1318 let session = manager.get_session("token_hash_1").unwrap();
1319 assert_eq!(session.actor_id, Some("actor_123".to_string()));
1320 }
1321
1322 #[test]
1323 fn test_bind_to_actor_nonexistent() {
1324 let manager = create_test_manager();
1325
1326 let result = manager.bind_to_actor("nonexistent", "actor_123");
1327 assert!(!result);
1328 }
1329
1330 #[test]
1331 fn test_bind_to_actor_idempotent() {
1332 let manager = create_test_manager();
1333 let ip = create_test_ip(1);
1334
1335 manager.create_session("token_hash_1", ip, None);
1336
1337 assert!(manager.bind_to_actor("token_hash_1", "actor_123"));
1339 assert!(manager.bind_to_actor("token_hash_1", "actor_123"));
1340
1341 let sessions = manager.get_actor_sessions("actor_123");
1343 assert_eq!(sessions.len(), 1);
1344 }
1345
1346 #[test]
1347 fn test_bind_to_actor_rebind() {
1348 let manager = create_test_manager();
1349 let ip = create_test_ip(1);
1350
1351 manager.create_session("token_hash_1", ip, None);
1352
1353 assert!(manager.bind_to_actor("token_hash_1", "actor_123"));
1355 assert_eq!(manager.get_actor_sessions("actor_123").len(), 1);
1356
1357 assert!(manager.bind_to_actor("token_hash_1", "actor_456"));
1359
1360 assert_eq!(manager.get_actor_sessions("actor_123").len(), 0);
1362 assert_eq!(manager.get_actor_sessions("actor_456").len(), 1);
1364
1365 let session = manager.get_session("token_hash_1").unwrap();
1366 assert_eq!(session.actor_id, Some("actor_456".to_string()));
1367 }
1368
1369 #[test]
1370 fn test_remove_session_cleans_actor_sessions() {
1371 let manager = create_test_manager();
1372 let ip = create_test_ip(1);
1373
1374 manager.create_session("token_hash_1", ip, None);
1375 assert!(manager.bind_to_actor("token_hash_1", "actor_cleanup"));
1376 assert!(manager.actor_sessions.contains_key("actor_cleanup"));
1377
1378 assert!(manager.remove_session("token_hash_1"));
1379 assert!(!manager.actor_sessions.contains_key("actor_cleanup"));
1380 }
1381
1382 #[test]
1383 fn test_get_actor_sessions() {
1384 let manager = create_test_manager();
1385 let ip = create_test_ip(1);
1386
1387 manager.create_session("token_1", ip, None);
1389 manager.create_session("token_2", ip, None);
1390 manager.create_session("token_3", ip, None);
1391
1392 assert!(manager.bind_to_actor("token_1", "actor_123"));
1393 assert!(manager.bind_to_actor("token_2", "actor_123"));
1394 assert!(manager.bind_to_actor("token_3", "actor_456"));
1395
1396 let actor_sessions = manager.get_actor_sessions("actor_123");
1397 assert_eq!(actor_sessions.len(), 2);
1398 }
1399
1400 #[test]
1405 fn test_lru_eviction() {
1406 let config = SessionConfig {
1407 max_sessions: 100,
1408 ..Default::default()
1409 };
1410 let manager = SessionManager::new(config);
1411
1412 for i in 0..150 {
1414 let ip = create_test_ip((i % 256) as u8);
1415 manager.create_session(&format!("token_{}", i), ip, None);
1416 }
1417
1418 assert!(manager.len() <= 150);
1420
1421 for i in 150..300 {
1423 let ip = create_test_ip((i % 256) as u8);
1424 manager.create_session(&format!("token_{}", i), ip, None);
1425 }
1426
1427 let evictions = manager.stats().evictions.load(Ordering::Relaxed);
1429 assert!(evictions > 0);
1430 }
1431
1432 #[test]
1437 fn test_invalidate_session() {
1438 let manager = create_test_manager();
1439 let ip = create_test_ip(1);
1440
1441 manager.create_session("token_hash_1", ip, None);
1442 assert_eq!(manager.len(), 1);
1443
1444 let result = manager.invalidate_session("token_hash_1");
1445 assert!(result);
1446 assert_eq!(manager.len(), 0);
1447 }
1448
1449 #[test]
1450 fn test_invalidate_nonexistent_session() {
1451 let manager = create_test_manager();
1452
1453 let result = manager.invalidate_session("nonexistent");
1454 assert!(!result);
1455 }
1456
1457 #[test]
1462 fn test_mark_suspicious() {
1463 let manager = create_test_manager();
1464 let ip = create_test_ip(1);
1465
1466 manager.create_session("token_hash_1", ip, None);
1467
1468 let alert = HijackAlert {
1469 session_id: "test".to_string(),
1470 alert_type: HijackType::Ja4Mismatch,
1471 original_value: "old".to_string(),
1472 new_value: "new".to_string(),
1473 timestamp: now_ms(),
1474 confidence: 0.9,
1475 };
1476
1477 let result = manager.mark_suspicious("token_hash_1", alert);
1478 assert!(result);
1479
1480 let session = manager.get_session("token_hash_1").unwrap();
1481 assert!(session.is_suspicious);
1482 assert_eq!(session.hijack_alerts.len(), 1);
1483 }
1484
1485 #[test]
1486 fn test_mark_suspicious_nonexistent() {
1487 let manager = create_test_manager();
1488
1489 let alert = HijackAlert {
1490 session_id: "test".to_string(),
1491 alert_type: HijackType::Ja4Mismatch,
1492 original_value: "old".to_string(),
1493 new_value: "new".to_string(),
1494 timestamp: now_ms(),
1495 confidence: 0.9,
1496 };
1497
1498 let result = manager.mark_suspicious("nonexistent", alert);
1499 assert!(!result);
1500 }
1501
1502 #[test]
1503 fn test_list_suspicious_sessions() {
1504 let manager = create_test_manager();
1505 let ip = create_test_ip(1);
1506
1507 for i in 0..10 {
1509 manager.create_session(&format!("token_{}", i), ip, None);
1510 }
1511
1512 let alert = HijackAlert {
1513 session_id: "test".to_string(),
1514 alert_type: HijackType::Ja4Mismatch,
1515 original_value: "old".to_string(),
1516 new_value: "new".to_string(),
1517 timestamp: now_ms(),
1518 confidence: 0.9,
1519 };
1520
1521 assert!(manager.mark_suspicious("token_0", alert.clone()));
1522 assert!(manager.mark_suspicious("token_2", alert.clone()));
1523 assert!(manager.mark_suspicious("token_4", alert));
1524
1525 let suspicious = manager.list_suspicious_sessions();
1526 assert_eq!(suspicious.len(), 3);
1527 }
1528
1529 #[test]
1534 fn test_list_sessions() {
1535 let manager = create_test_manager();
1536
1537 for i in 0..10 {
1538 let ip = create_test_ip(i);
1539 manager.create_session(&format!("token_{}", i), ip, None);
1540 std::thread::sleep(std::time::Duration::from_millis(1));
1541 }
1542
1543 let first_page = manager.list_sessions(5, 0);
1545 assert_eq!(first_page.len(), 5);
1546
1547 let second_page = manager.list_sessions(5, 5);
1548 assert_eq!(second_page.len(), 5);
1549
1550 for window in first_page.windows(2) {
1552 assert!(window[0].last_activity >= window[1].last_activity);
1553 }
1554 }
1555
1556 #[test]
1561 fn test_concurrent_access() {
1562 let manager = Arc::new(create_test_manager());
1563 let mut handles = vec![];
1564
1565 for thread_id in 0..10 {
1567 let manager = Arc::clone(&manager);
1568 handles.push(thread::spawn(move || {
1569 for i in 0..100 {
1570 let ip: IpAddr = format!("10.{}.0.{}", thread_id, i % 256).parse().unwrap();
1571 let token = format!("token_t{}_{}", thread_id, i);
1572 let ja4 = format!("ja4_t{}_{}", thread_id, i % 5);
1573
1574 manager.validate_request(&token, ip, Some(&ja4));
1575 }
1576 }));
1577 }
1578
1579 for handle in handles {
1580 handle.join().unwrap();
1581 }
1582
1583 assert!(manager.len() > 0);
1585 assert!(manager.stats().total_created.load(Ordering::Relaxed) > 0);
1586 }
1587
1588 #[test]
1589 fn test_stress_concurrent_sessions() {
1590 let manager = Arc::new(SessionManager::new(SessionConfig {
1591 max_sessions: 10_000,
1592 session_ttl_secs: 86_400,
1593 idle_timeout_secs: 86_400,
1594 ..Default::default()
1595 }));
1596 let mut handles = vec![];
1597
1598 for thread_id in 0..16 {
1599 let manager = Arc::clone(&manager);
1600 handles.push(thread::spawn(move || {
1601 let actor_id = format!("actor_{}", thread_id);
1602 for i in 0..300 {
1603 let ip: IpAddr = format!("10.{}.{}.{}", thread_id, i / 256, i % 256)
1604 .parse()
1605 .unwrap();
1606 let token = format!("token_t{}_{}", thread_id, i);
1607 let ja4 = format!("ja4_t{}_{}", thread_id, i % 10);
1608
1609 manager.validate_request(&token, ip, Some(&ja4));
1610
1611 if i % 3 == 0 {
1612 let _ = manager.bind_to_actor(&token, &actor_id);
1613 }
1614 if i % 2 == 0 {
1615 manager.touch_session(&token);
1616 }
1617 }
1618 }));
1619 }
1620
1621 for handle in handles {
1622 handle.join().unwrap();
1623 }
1624
1625 let stats = manager.stats();
1626 assert!(manager.len() > 0);
1627 assert!(stats.total_created.load(Ordering::Relaxed) > 0);
1628 assert!(!manager.get_actor_sessions("actor_0").is_empty());
1629 }
1630
1631 #[test]
1636 fn test_stats() {
1637 let manager = create_test_manager();
1638
1639 let stats = manager.stats().snapshot();
1641 assert_eq!(stats.total_sessions, 0);
1642 assert_eq!(stats.suspicious_sessions, 0);
1643
1644 for i in 0..5 {
1646 let ip = create_test_ip(i);
1647 manager.create_session(&format!("token_{}", i), ip, Some(&format!("ja4_{}", i)));
1648 }
1649
1650 let stats = manager.stats().snapshot();
1651 assert_eq!(stats.total_sessions, 5);
1652 assert_eq!(stats.active_sessions, 5);
1653 assert_eq!(stats.total_created, 5);
1654 }
1655
1656 #[test]
1661 fn test_clear() {
1662 let manager = create_test_manager();
1663
1664 for i in 0..10 {
1665 let ip = create_test_ip(i);
1666 manager.create_session(&format!("token_{}", i), ip, None);
1667 }
1668
1669 assert_eq!(manager.len(), 10);
1670
1671 manager.clear();
1672
1673 assert_eq!(manager.len(), 0);
1674 assert!(manager.session_by_id.is_empty());
1675 assert!(manager.actor_sessions.is_empty());
1676 }
1677
1678 #[test]
1683 fn test_default() {
1684 let manager = SessionManager::default();
1685
1686 assert!(manager.is_enabled());
1687 assert!(manager.is_empty());
1688 assert_eq!(manager.config().max_sessions, 50_000);
1689 }
1690
1691 #[test]
1696 fn test_session_id_uniqueness() {
1697 let mut ids = std::collections::HashSet::new();
1698 for _ in 0..1000 {
1699 let id = generate_session_id();
1700 assert!(!ids.contains(&id), "Duplicate ID generated: {}", id);
1701 ids.insert(id);
1702 }
1703 }
1704
1705 #[test]
1706 fn test_session_id_format() {
1707 let id = generate_session_id();
1708
1709 assert!(id.starts_with("sess-"));
1711 assert_eq!(id.len(), 41); }
1713
1714 #[test]
1719 fn test_empty_ja4_fingerprint() {
1720 let manager = create_test_manager();
1721 let ip = create_test_ip(1);
1722
1723 manager.create_session("token_hash_1", ip, Some(""));
1724
1725 let session = manager.get_session("token_hash_1").unwrap();
1726 assert!(session.bound_ja4.is_none());
1727 }
1728
1729 #[test]
1730 fn test_ipv6_addresses() {
1731 let manager = create_test_manager();
1732
1733 let ipv6: IpAddr = "2001:db8::1".parse().unwrap();
1734
1735 let session = manager.create_session("token_hash_1", ipv6, None);
1736 assert_eq!(session.request_count, 1);
1737
1738 let decision = manager.validate_request("token_hash_1", ipv6, None);
1739 assert_eq!(decision, SessionDecision::Valid);
1740 }
1741
1742 #[test]
1743 fn test_disabled_manager() {
1744 let config = SessionConfig {
1745 enabled: false,
1746 ..Default::default()
1747 };
1748 let manager = SessionManager::new(config);
1749
1750 assert!(!manager.is_enabled());
1751
1752 let ip = create_test_ip(1);
1753 let decision = manager.validate_request("token_hash_1", ip, None);
1754
1755 assert_eq!(decision, SessionDecision::Valid);
1757 assert!(manager.is_empty());
1758 }
1759
1760 #[test]
1765 fn test_alert_trimming() {
1766 let config = SessionConfig {
1767 max_alerts_per_session: 3,
1768 ..Default::default()
1769 };
1770 let manager = SessionManager::new(config);
1771 let ip = create_test_ip(1);
1772
1773 manager.create_session("token_hash_1", ip, Some("ja4_original"));
1774
1775 for i in 0..10 {
1777 let alert = HijackAlert {
1778 session_id: "test".to_string(),
1779 alert_type: HijackType::Ja4Mismatch,
1780 original_value: "old".to_string(),
1781 new_value: format!("new_{}", i),
1782 timestamp: now_ms(),
1783 confidence: 0.9,
1784 };
1785 assert!(manager.mark_suspicious("token_hash_1", alert));
1786 }
1787
1788 let session = manager.get_session("token_hash_1").unwrap();
1789 assert_eq!(session.hijack_alerts.len(), 3);
1790
1791 assert_eq!(session.hijack_alerts[2].new_value, "new_9");
1793 }
1794
1795 #[test]
1800 fn test_touch_session() {
1801 let manager = create_test_manager();
1802 let ip = create_test_ip(1);
1803
1804 manager.create_session("token_hash_1", ip, None);
1805
1806 let before = manager.get_session("token_hash_1").unwrap().last_activity;
1807
1808 std::thread::sleep(std::time::Duration::from_millis(10));
1809
1810 manager.touch_session("token_hash_1");
1811
1812 let after = manager.get_session("token_hash_1").unwrap().last_activity;
1813 assert!(after > before);
1814 }
1815}