1use dashmap::DashMap;
38use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
39use std::sync::Arc;
40use std::time::{Duration, SystemTime, UNIX_EPOCH};
41use tokio::sync::Notify;
42
43use super::{
44 CaptchaManager, ChallengeResponse, CookieManager, JsChallengeManager, ValidationResult,
45};
46use crate::tarpit::TarpitManager;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
50#[repr(u8)]
51pub enum ChallengeLevel {
52 None = 0,
53 Cookie = 1,
54 JsChallenge = 2,
55 Captcha = 3,
56 Tarpit = 4,
57 Block = 5,
58}
59
60impl ChallengeLevel {
61 pub fn from_u8(value: u8) -> Self {
63 match value {
64 0 => ChallengeLevel::None,
65 1 => ChallengeLevel::Cookie,
66 2 => ChallengeLevel::JsChallenge,
67 3 => ChallengeLevel::Captcha,
68 4 => ChallengeLevel::Tarpit,
69 _ => ChallengeLevel::Block,
70 }
71 }
72
73 pub fn name(&self) -> &'static str {
75 match self {
76 ChallengeLevel::None => "none",
77 ChallengeLevel::Cookie => "cookie",
78 ChallengeLevel::JsChallenge => "js_challenge",
79 ChallengeLevel::Captcha => "captcha",
80 ChallengeLevel::Tarpit => "tarpit",
81 ChallengeLevel::Block => "block",
82 }
83 }
84}
85
86impl std::fmt::Display for ChallengeLevel {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.name())
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct ActorChallengeState {
95 pub actor_id: String,
97 pub current_level: ChallengeLevel,
99 pub failures_at_level: u32,
101 pub last_challenge_time: u64,
103 pub total_failures: u32,
105 pub escalation_history: Vec<(ChallengeLevel, u64)>,
107 pub last_success_time: Option<u64>,
109}
110
111impl ActorChallengeState {
112 pub fn new(actor_id: String) -> Self {
114 Self {
115 actor_id,
116 current_level: ChallengeLevel::None,
117 failures_at_level: 0,
118 last_challenge_time: 0,
119 total_failures: 0,
120 escalation_history: Vec::new(),
121 last_success_time: None,
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct ProgressionConfig {
129 pub failures_before_escalate: u32,
131 pub escalation_cooldown_secs: u64,
133 pub auto_de_escalate_secs: u64,
135 pub skip_to_block_threshold: u32,
137 pub enable_cookie: bool,
139 pub enable_js_challenge: bool,
141 pub enable_captcha: bool,
143 pub enable_tarpit: bool,
145 pub risk_threshold_cookie: f64,
147 pub risk_threshold_js: f64,
149 pub risk_threshold_captcha: f64,
151 pub risk_threshold_block: f64,
153 pub block_page_html: String,
155 pub block_status_code: u16,
157 pub captcha_page_html: String,
159 pub cleanup_interval_secs: u64,
161 pub max_states: usize,
163 pub max_escalation_history: usize,
166}
167
168impl Default for ProgressionConfig {
169 fn default() -> Self {
170 Self {
171 failures_before_escalate: 3,
172 escalation_cooldown_secs: 60,
173 auto_de_escalate_secs: 3600, skip_to_block_threshold: 10,
175 enable_cookie: true,
176 enable_js_challenge: true,
177 enable_captcha: false, enable_tarpit: true,
179 risk_threshold_cookie: 0.2,
180 risk_threshold_js: 0.4,
181 risk_threshold_captcha: 0.6,
182 risk_threshold_block: 0.8,
183 block_page_html: DEFAULT_BLOCK_PAGE.to_string(),
184 block_status_code: 403,
185 captcha_page_html: DEFAULT_CAPTCHA_PAGE.to_string(),
186 cleanup_interval_secs: 300,
187 max_states: 100_000,
188 max_escalation_history: 100, }
190 }
191}
192
193#[derive(Debug, Default)]
195pub struct ProgressionStats {
196 pub actors_tracked: AtomicU64,
198 pub escalations: AtomicU64,
200 pub de_escalations: AtomicU64,
202 pub direct_blocks: AtomicU64,
204 pub challenges_issued: AtomicU64,
206 pub successes: AtomicU64,
208 pub failures: AtomicU64,
210}
211
212impl ProgressionStats {
213 pub fn snapshot(&self) -> ProgressionStatsSnapshot {
215 ProgressionStatsSnapshot {
216 actors_tracked: self.actors_tracked.load(Ordering::Relaxed),
217 escalations: self.escalations.load(Ordering::Relaxed),
218 de_escalations: self.de_escalations.load(Ordering::Relaxed),
219 direct_blocks: self.direct_blocks.load(Ordering::Relaxed),
220 challenges_issued: self.challenges_issued.load(Ordering::Relaxed),
221 successes: self.successes.load(Ordering::Relaxed),
222 failures: self.failures.load(Ordering::Relaxed),
223 }
224 }
225}
226
227#[derive(Debug, Clone, serde::Serialize)]
229pub struct ProgressionStatsSnapshot {
230 pub actors_tracked: u64,
231 pub escalations: u64,
232 pub de_escalations: u64,
233 pub direct_blocks: u64,
234 pub challenges_issued: u64,
235 pub successes: u64,
236 pub failures: u64,
237}
238
239pub struct ProgressionManager {
241 actor_states: DashMap<String, ActorChallengeState>,
243 cookie_manager: Arc<CookieManager>,
245 js_manager: Arc<JsChallengeManager>,
247 captcha_manager: Arc<CaptchaManager>,
249 tarpit_manager: Arc<TarpitManager>,
251 config: ProgressionConfig,
253 stats: ProgressionStats,
255 shutdown: Arc<Notify>,
257 shutdown_flag: Arc<AtomicBool>,
259}
260
261impl ProgressionManager {
262 pub fn new(
264 cookie_manager: Arc<CookieManager>,
265 js_manager: Arc<JsChallengeManager>,
266 captcha_manager: Arc<CaptchaManager>,
267 tarpit_manager: Arc<TarpitManager>,
268 config: ProgressionConfig,
269 ) -> Self {
270 Self {
271 actor_states: DashMap::with_capacity(config.max_states.min(10_000)),
272 cookie_manager,
273 js_manager,
274 captcha_manager,
275 tarpit_manager,
276 config,
277 stats: ProgressionStats::default(),
278 shutdown: Arc::new(Notify::new()),
279 shutdown_flag: Arc::new(AtomicBool::new(false)),
280 }
281 }
282
283 pub fn config(&self) -> &ProgressionConfig {
285 &self.config
286 }
287
288 pub fn stats(&self) -> &ProgressionStats {
290 &self.stats
291 }
292
293 fn push_escalation_history(
297 &self,
298 state: &mut ActorChallengeState,
299 level: ChallengeLevel,
300 timestamp: u64,
301 ) {
302 if state.escalation_history.len() >= self.config.max_escalation_history {
304 state.escalation_history.remove(0);
305 }
306 state.escalation_history.push((level, timestamp));
307 }
308
309 pub fn get_challenge(&self, actor_id: &str, risk_score: f64) -> ChallengeResponse {
311 let now = now_ms();
312
313 let mut state = self.get_or_create_state(actor_id);
315
316 self.check_auto_de_escalate(&mut state, now);
318
319 let effective_level = self.determine_effective_level(&state, risk_score);
321
322 state.last_challenge_time = now;
324 state.current_level = effective_level;
325
326 self.actor_states.insert(actor_id.to_string(), state);
328
329 self.stats.challenges_issued.fetch_add(1, Ordering::Relaxed);
330
331 self.get_challenge_for_level(actor_id, effective_level)
333 }
334
335 pub fn record_failure(&self, actor_id: &str) {
337 let now = now_ms();
338
339 let mut state = self.get_or_create_state(actor_id);
340 state.failures_at_level += 1;
341 state.total_failures += 1;
342 state.last_challenge_time = now;
343
344 self.stats.failures.fetch_add(1, Ordering::Relaxed);
345
346 if state.total_failures >= self.config.skip_to_block_threshold {
348 if state.current_level != ChallengeLevel::Block {
350 self.push_escalation_history(&mut state, ChallengeLevel::Block, now);
351 state.current_level = ChallengeLevel::Block;
352 state.failures_at_level = 0;
353 self.stats.direct_blocks.fetch_add(1, Ordering::Relaxed);
354 }
355 } else if state.failures_at_level >= self.config.failures_before_escalate {
356 let next_level = self.next_level(state.current_level);
358 if next_level != state.current_level {
359 self.push_escalation_history(&mut state, next_level, now);
360 state.current_level = next_level;
361 state.failures_at_level = 0;
362 self.stats.escalations.fetch_add(1, Ordering::Relaxed);
363 }
364 }
365
366 self.actor_states.insert(actor_id.to_string(), state);
367 }
368
369 pub fn record_success(&self, actor_id: &str) {
371 let now = now_ms();
372
373 if let Some(mut entry) = self.actor_states.get_mut(actor_id) {
374 entry.failures_at_level = 0;
375 entry.last_success_time = Some(now);
376 self.stats.successes.fetch_add(1, Ordering::Relaxed);
377 }
378 }
379
380 pub fn escalate(&self, actor_id: &str) -> ChallengeLevel {
382 let now = now_ms();
383
384 let mut state = self.get_or_create_state(actor_id);
385 let next_level = self.next_level(state.current_level);
386
387 if next_level != state.current_level {
388 self.push_escalation_history(&mut state, next_level, now);
389 state.current_level = next_level;
390 state.failures_at_level = 0;
391 self.stats.escalations.fetch_add(1, Ordering::Relaxed);
392 }
393
394 let level = state.current_level;
395 self.actor_states.insert(actor_id.to_string(), state);
396 level
397 }
398
399 pub fn de_escalate(&self, actor_id: &str) -> ChallengeLevel {
401 let now = now_ms();
402
403 let mut state = self.get_or_create_state(actor_id);
404 let prev_level = self.prev_level(state.current_level);
405
406 if prev_level != state.current_level {
407 self.push_escalation_history(&mut state, prev_level, now);
408 state.current_level = prev_level;
409 state.failures_at_level = 0;
410 self.stats.de_escalations.fetch_add(1, Ordering::Relaxed);
411 }
412
413 let level = state.current_level;
414 self.actor_states.insert(actor_id.to_string(), state);
415 level
416 }
417
418 pub fn reset(&self, actor_id: &str) {
420 self.actor_states.remove(actor_id);
421 }
422
423 pub fn get_level(&self, actor_id: &str) -> ChallengeLevel {
425 self.actor_states
426 .get(actor_id)
427 .map(|s| s.current_level)
428 .unwrap_or(ChallengeLevel::None)
429 }
430
431 pub fn get_actor_state(&self, actor_id: &str) -> Option<ActorChallengeState> {
433 self.actor_states.get(actor_id).map(|s| s.clone())
434 }
435
436 pub fn list_actors_at_level(&self, level: ChallengeLevel) -> Vec<ActorChallengeState> {
438 self.actor_states
439 .iter()
440 .filter(|e| e.value().current_level == level)
441 .map(|e| e.value().clone())
442 .collect()
443 }
444
445 pub fn list_all_actors(&self) -> Vec<ActorChallengeState> {
447 self.actor_states
448 .iter()
449 .map(|e| e.value().clone())
450 .collect()
451 }
452
453 pub fn start_background_tasks(self: Arc<Self>) {
458 let manager = self.clone();
459 let interval = Duration::from_secs(self.config.cleanup_interval_secs);
460 let shutdown = self.shutdown.clone();
461 let shutdown_flag = self.shutdown_flag.clone();
462
463 tokio::spawn(async move {
464 let mut interval_timer = tokio::time::interval(interval);
465
466 loop {
467 tokio::select! {
468 _ = interval_timer.tick() => {
469 if shutdown_flag.load(Ordering::Relaxed) {
471 log::info!("Progression manager background tasks shutting down (flag)");
472 break;
473 }
474 manager.run_maintenance();
475 }
476 _ = shutdown.notified() => {
477 log::info!("Progression manager background tasks shutting down");
478 break;
479 }
480 }
481 }
482 });
483 }
484
485 pub fn shutdown(&self) {
490 self.shutdown_flag.store(true, Ordering::Relaxed);
491 self.shutdown.notify_one();
492 }
493
494 pub fn run_maintenance(&self) {
496 let now = now_ms();
497
498 let mut to_de_escalate = Vec::new();
500 for entry in self.actor_states.iter() {
501 let state = entry.value();
502 let idle_time = now.saturating_sub(state.last_challenge_time);
503 let de_escalate_threshold_ms = self.config.auto_de_escalate_secs * 1000;
504
505 if idle_time > de_escalate_threshold_ms && state.current_level > ChallengeLevel::None {
506 to_de_escalate.push(entry.key().clone());
507 }
508 }
509
510 for actor_id in to_de_escalate {
511 self.de_escalate(&actor_id);
512 }
513
514 if self.actor_states.len() > self.config.max_states {
516 let mut actors: Vec<_> = self
518 .actor_states
519 .iter()
520 .map(|e| (e.key().clone(), e.value().last_challenge_time))
521 .collect();
522 actors.sort_by_key(|(_, time)| *time);
523
524 let to_remove = self.config.max_states / 10;
526 for (actor_id, _) in actors.into_iter().take(to_remove) {
527 self.actor_states.remove(&actor_id);
528 }
529 }
530 }
531
532 pub fn len(&self) -> usize {
534 self.actor_states.len()
535 }
536
537 pub fn cookie_name(&self) -> &str {
539 self.cookie_manager.config().cookie_name.as_str()
540 }
541
542 pub fn is_empty(&self) -> bool {
544 self.actor_states.is_empty()
545 }
546
547 pub fn clear(&self) {
549 self.actor_states.clear();
550 }
551
552 pub fn validate_challenge(&self, actor_id: &str, response: &str) -> ValidationResult {
554 let level = self.get_level(actor_id);
555
556 let result = match level {
557 ChallengeLevel::Cookie => self.cookie_manager.validate_cookie(actor_id, response),
558 ChallengeLevel::JsChallenge => self.js_manager.validate_pow(actor_id, response),
559 ChallengeLevel::Captcha => self.captcha_manager.validate_response(actor_id, response),
560 _ => ValidationResult::NotFound,
561 };
562
563 match &result {
564 ValidationResult::Valid => self.record_success(actor_id),
565 ValidationResult::Invalid(_) | ValidationResult::Expired => {
566 self.record_failure(actor_id)
567 }
568 ValidationResult::NotFound => {}
569 }
570
571 result
572 }
573
574 fn get_or_create_state(&self, actor_id: &str) -> ActorChallengeState {
581 let entry = self.actor_states.entry(actor_id.to_string());
583 match entry {
584 dashmap::mapref::entry::Entry::Occupied(occupied) => occupied.get().clone(),
585 dashmap::mapref::entry::Entry::Vacant(vacant) => {
586 self.stats.actors_tracked.fetch_add(1, Ordering::Relaxed);
587 let state = ActorChallengeState::new(actor_id.to_string());
588 vacant.insert(state.clone());
589 state
590 }
591 }
592 }
593
594 fn check_auto_de_escalate(&self, state: &mut ActorChallengeState, now: u64) {
596 if state.current_level == ChallengeLevel::None {
597 return;
598 }
599
600 let idle_time = now.saturating_sub(state.last_challenge_time);
601 let de_escalate_threshold_ms = self.config.auto_de_escalate_secs * 1000;
602
603 if idle_time > de_escalate_threshold_ms {
604 let prev_level = self.prev_level(state.current_level);
605 if prev_level != state.current_level {
606 self.push_escalation_history(state, prev_level, now);
607 state.current_level = prev_level;
608 state.failures_at_level = 0;
609 self.stats.de_escalations.fetch_add(1, Ordering::Relaxed);
610 }
611 }
612 }
613
614 fn determine_effective_level(
616 &self,
617 state: &ActorChallengeState,
618 risk_score: f64,
619 ) -> ChallengeLevel {
620 let risk_level = self.determine_initial_level(risk_score);
622
623 std::cmp::max(state.current_level, risk_level)
626 }
627
628 fn determine_initial_level(&self, risk_score: f64) -> ChallengeLevel {
630 if risk_score >= self.config.risk_threshold_block {
631 ChallengeLevel::Block
632 } else if risk_score >= self.config.risk_threshold_captcha {
633 if self.config.enable_captcha {
634 ChallengeLevel::Captcha
635 } else if self.config.enable_tarpit {
636 ChallengeLevel::Tarpit
637 } else {
638 ChallengeLevel::JsChallenge
639 }
640 } else if risk_score >= self.config.risk_threshold_js {
641 if self.config.enable_js_challenge {
642 ChallengeLevel::JsChallenge
643 } else if self.config.enable_cookie {
644 ChallengeLevel::Cookie
645 } else {
646 ChallengeLevel::None
647 }
648 } else if risk_score >= self.config.risk_threshold_cookie {
649 if self.config.enable_cookie {
650 ChallengeLevel::Cookie
651 } else {
652 ChallengeLevel::None
653 }
654 } else {
655 ChallengeLevel::None
656 }
657 }
658
659 fn next_level(&self, current: ChallengeLevel) -> ChallengeLevel {
661 match current {
662 ChallengeLevel::None => {
663 if self.config.enable_cookie {
664 ChallengeLevel::Cookie
665 } else if self.config.enable_js_challenge {
666 ChallengeLevel::JsChallenge
667 } else if self.config.enable_captcha {
668 ChallengeLevel::Captcha
669 } else if self.config.enable_tarpit {
670 ChallengeLevel::Tarpit
671 } else {
672 ChallengeLevel::Block
673 }
674 }
675 ChallengeLevel::Cookie => {
676 if self.config.enable_js_challenge {
677 ChallengeLevel::JsChallenge
678 } else if self.config.enable_captcha {
679 ChallengeLevel::Captcha
680 } else if self.config.enable_tarpit {
681 ChallengeLevel::Tarpit
682 } else {
683 ChallengeLevel::Block
684 }
685 }
686 ChallengeLevel::JsChallenge => {
687 if self.config.enable_captcha {
688 ChallengeLevel::Captcha
689 } else if self.config.enable_tarpit {
690 ChallengeLevel::Tarpit
691 } else {
692 ChallengeLevel::Block
693 }
694 }
695 ChallengeLevel::Captcha => {
696 if self.config.enable_tarpit {
697 ChallengeLevel::Tarpit
698 } else {
699 ChallengeLevel::Block
700 }
701 }
702 ChallengeLevel::Tarpit => ChallengeLevel::Block,
703 ChallengeLevel::Block => ChallengeLevel::Block, }
705 }
706
707 fn prev_level(&self, current: ChallengeLevel) -> ChallengeLevel {
709 match current {
710 ChallengeLevel::Block => {
711 if self.config.enable_tarpit {
712 ChallengeLevel::Tarpit
713 } else if self.config.enable_captcha {
714 ChallengeLevel::Captcha
715 } else if self.config.enable_js_challenge {
716 ChallengeLevel::JsChallenge
717 } else if self.config.enable_cookie {
718 ChallengeLevel::Cookie
719 } else {
720 ChallengeLevel::None
721 }
722 }
723 ChallengeLevel::Tarpit => {
724 if self.config.enable_captcha {
725 ChallengeLevel::Captcha
726 } else if self.config.enable_js_challenge {
727 ChallengeLevel::JsChallenge
728 } else if self.config.enable_cookie {
729 ChallengeLevel::Cookie
730 } else {
731 ChallengeLevel::None
732 }
733 }
734 ChallengeLevel::Captcha => {
735 if self.config.enable_js_challenge {
736 ChallengeLevel::JsChallenge
737 } else if self.config.enable_cookie {
738 ChallengeLevel::Cookie
739 } else {
740 ChallengeLevel::None
741 }
742 }
743 ChallengeLevel::JsChallenge => {
744 if self.config.enable_cookie {
745 ChallengeLevel::Cookie
746 } else {
747 ChallengeLevel::None
748 }
749 }
750 ChallengeLevel::Cookie => ChallengeLevel::None,
751 ChallengeLevel::None => ChallengeLevel::None, }
753 }
754
755 fn get_challenge_for_level(&self, actor_id: &str, level: ChallengeLevel) -> ChallengeResponse {
757 match level {
758 ChallengeLevel::None => ChallengeResponse::Allow,
759
760 ChallengeLevel::Cookie => {
761 let challenge = self.cookie_manager.generate_tracking_cookie(actor_id);
762 ChallengeResponse::Cookie {
763 name: challenge.cookie_name,
764 value: challenge.cookie_value,
765 max_age: self.cookie_manager.config().cookie_max_age_secs,
766 http_only: self.cookie_manager.config().http_only,
767 secure: self.cookie_manager.config().secure_only,
768 }
769 }
770
771 ChallengeLevel::JsChallenge => {
772 let challenge = self.js_manager.generate_pow_challenge(actor_id);
773 let html = self.js_manager.generate_challenge_page(&challenge);
774 ChallengeResponse::JsChallenge {
775 html,
776 expected_solution: challenge.expected_hash_prefix,
777 expires_at: challenge.expires_at,
778 }
779 }
780
781 ChallengeLevel::Captcha => {
782 let challenge = self.captcha_manager.issue_challenge(actor_id);
783 ChallengeResponse::Captcha {
784 html: challenge.html,
785 session_id: challenge.session_id,
786 }
787 }
788
789 ChallengeLevel::Tarpit => {
790 let decision = self.tarpit_manager.tarpit(actor_id);
791 ChallengeResponse::Tarpit {
792 delay_ms: decision.delay_ms,
793 }
794 }
795
796 ChallengeLevel::Block => ChallengeResponse::Block {
797 html: self.config.block_page_html.clone(),
798 status_code: self.config.block_status_code,
799 },
800 }
801 }
802}
803
804#[inline]
806fn now_ms() -> u64 {
807 SystemTime::now()
808 .duration_since(UNIX_EPOCH)
809 .map(|d| d.as_millis() as u64)
810 .unwrap_or(0)
811}
812
813const DEFAULT_BLOCK_PAGE: &str = r#"<!DOCTYPE html>
815<html>
816<head>
817 <meta charset="UTF-8">
818 <meta name="viewport" content="width=device-width, initial-scale=1.0">
819 <title>Access Denied</title>
820 <style>
821 body {
822 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
823 display: flex;
824 justify-content: center;
825 align-items: center;
826 min-height: 100vh;
827 margin: 0;
828 background: linear-gradient(135deg, #e53e3e 0%, #9b2c2c 100%);
829 }
830 .container {
831 background: white;
832 padding: 2rem;
833 border-radius: 8px;
834 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
835 text-align: center;
836 max-width: 400px;
837 }
838 h1 { color: #e53e3e; margin-bottom: 1rem; }
839 p { color: #666; }
840 .icon {
841 font-size: 4rem;
842 margin-bottom: 1rem;
843 }
844 </style>
845</head>
846<body>
847 <div class="container">
848 <div class="icon">🚫</div>
849 <h1>Access Denied</h1>
850 <p>Your request has been blocked due to suspicious activity.</p>
851 <p>If you believe this is an error, please contact support.</p>
852 </div>
853</body>
854</html>"#;
855
856const DEFAULT_CAPTCHA_PAGE: &str = r#"<!DOCTYPE html>
858<html>
859<head>
860 <meta charset="UTF-8">
861 <meta name="viewport" content="width=device-width, initial-scale=1.0">
862 <title>Verification Required</title>
863 <style>
864 body {
865 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
866 display: flex;
867 justify-content: center;
868 align-items: center;
869 min-height: 100vh;
870 margin: 0;
871 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
872 }
873 .container {
874 background: white;
875 padding: 2rem;
876 border-radius: 8px;
877 box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
878 text-align: center;
879 max-width: 400px;
880 }
881 h2 { color: #333; }
882 p { color: #666; }
883 .placeholder {
884 background: #f0f0f0;
885 padding: 2rem;
886 margin: 1rem 0;
887 border-radius: 4px;
888 color: #999;
889 }
890 </style>
891</head>
892<body>
893 <div class="container">
894 <h2>Human Verification Required</h2>
895 <p>Please complete the verification below to continue.</p>
896 <div class="placeholder">
897 [CAPTCHA Placeholder - Integration Required]
898 </div>
899 <p><small>This is a stub implementation.</small></p>
900 </div>
901</body>
902</html>"#;
903
904#[cfg(test)]
905mod tests {
906 use super::*;
907 use crate::interrogator::{CaptchaConfig, CookieConfig, JsChallengeConfig};
908 use crate::tarpit::TarpitConfig;
909
910 fn test_managers() -> (
911 Arc<CookieManager>,
912 Arc<JsChallengeManager>,
913 Arc<CaptchaManager>,
914 Arc<TarpitManager>,
915 ) {
916 let cookie_config = CookieConfig {
917 cookie_name: "__test".to_string(),
918 cookie_max_age_secs: 3600,
919 secret_key: [0x01; 32],
920 secure_only: true,
921 http_only: true,
922 same_site: "Strict".to_string(),
923 };
924 let js_config = JsChallengeConfig {
925 difficulty: 1, ..Default::default()
927 };
928 let captcha_config = CaptchaConfig {
929 secret: "test_captcha_secret".to_string(),
930 expiry_secs: 300,
931 max_challenges: 100,
932 cleanup_interval_secs: 60,
933 };
934 let tarpit_config = TarpitConfig {
935 base_delay_ms: 10, ..Default::default()
937 };
938
939 (
940 Arc::new(CookieManager::new(cookie_config).expect("valid test config")),
941 Arc::new(JsChallengeManager::new(js_config)),
942 Arc::new(CaptchaManager::new(captcha_config)),
943 Arc::new(TarpitManager::new(tarpit_config)),
944 )
945 }
946
947 fn test_config() -> ProgressionConfig {
948 ProgressionConfig {
949 failures_before_escalate: 3,
950 auto_de_escalate_secs: 1, skip_to_block_threshold: 10,
952 ..Default::default()
953 }
954 }
955
956 #[test]
957 fn test_level_from_risk_score() {
958 let (cookie, js, captcha, tarpit) = test_managers();
959 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
960
961 assert_eq!(manager.determine_initial_level(0.1), ChallengeLevel::None);
963
964 assert_eq!(manager.determine_initial_level(0.3), ChallengeLevel::Cookie);
966
967 assert_eq!(
969 manager.determine_initial_level(0.5),
970 ChallengeLevel::JsChallenge
971 );
972
973 assert_eq!(manager.determine_initial_level(0.7), ChallengeLevel::Tarpit);
975
976 assert_eq!(manager.determine_initial_level(0.9), ChallengeLevel::Block);
978 }
979
980 #[test]
981 fn test_escalation() {
982 let (cookie, js, captcha, tarpit) = test_managers();
983 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
984
985 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::None);
986
987 let level = manager.escalate("actor_123");
989 assert_eq!(level, ChallengeLevel::Cookie);
990
991 let level = manager.escalate("actor_123");
992 assert_eq!(level, ChallengeLevel::JsChallenge);
993
994 let level = manager.escalate("actor_123");
995 assert_eq!(level, ChallengeLevel::Tarpit); let level = manager.escalate("actor_123");
998 assert_eq!(level, ChallengeLevel::Block);
999
1000 let level = manager.escalate("actor_123");
1002 assert_eq!(level, ChallengeLevel::Block);
1003 }
1004
1005 #[test]
1006 fn test_de_escalation() {
1007 let (cookie, js, captcha, tarpit) = test_managers();
1008 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1009
1010 for _ in 0..5 {
1012 manager.escalate("actor_123");
1013 }
1014 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Block);
1015
1016 let level = manager.de_escalate("actor_123");
1018 assert_eq!(level, ChallengeLevel::Tarpit);
1019
1020 let level = manager.de_escalate("actor_123");
1021 assert_eq!(level, ChallengeLevel::JsChallenge);
1022
1023 let level = manager.de_escalate("actor_123");
1024 assert_eq!(level, ChallengeLevel::Cookie);
1025
1026 let level = manager.de_escalate("actor_123");
1027 assert_eq!(level, ChallengeLevel::None);
1028
1029 let level = manager.de_escalate("actor_123");
1031 assert_eq!(level, ChallengeLevel::None);
1032 }
1033
1034 #[test]
1035 fn test_failure_escalation() {
1036 let (cookie, js, captcha, tarpit) = test_managers();
1037 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1038
1039 manager.get_challenge("actor_123", 0.3);
1041 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Cookie);
1042
1043 manager.record_failure("actor_123");
1045 manager.record_failure("actor_123");
1046 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Cookie);
1047
1048 manager.record_failure("actor_123"); assert_eq!(manager.get_level("actor_123"), ChallengeLevel::JsChallenge);
1050 }
1051
1052 #[test]
1053 fn test_skip_to_block() {
1054 let (cookie, js, captcha, tarpit) = test_managers();
1055 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1056
1057 manager.get_challenge("actor_123", 0.3);
1059
1060 for _ in 0..10 {
1062 manager.record_failure("actor_123");
1063 }
1064
1065 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Block);
1066 }
1067
1068 #[test]
1069 fn test_get_challenge_response() {
1070 let (cookie, js, captcha, tarpit) = test_managers();
1071 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1072
1073 let response = manager.get_challenge("actor_1", 0.1);
1075 assert!(matches!(response, ChallengeResponse::Allow));
1076
1077 let response = manager.get_challenge("actor_2", 0.3);
1079 assert!(matches!(response, ChallengeResponse::Cookie { .. }));
1080
1081 let response = manager.get_challenge("actor_3", 0.5);
1083 assert!(matches!(response, ChallengeResponse::JsChallenge { .. }));
1084
1085 let response = manager.get_challenge("actor_4", 0.9);
1087 assert!(matches!(response, ChallengeResponse::Block { .. }));
1088 }
1089
1090 #[test]
1091 fn test_actor_state_tracking() {
1092 let (cookie, js, captcha, tarpit) = test_managers();
1093 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1094
1095 manager.get_challenge("actor_123", 0.5);
1096
1097 let state = manager.get_actor_state("actor_123").unwrap();
1098 assert_eq!(state.actor_id, "actor_123");
1099 assert_eq!(state.current_level, ChallengeLevel::JsChallenge);
1100 assert_eq!(state.total_failures, 0);
1101
1102 manager.record_failure("actor_123");
1103 let state = manager.get_actor_state("actor_123").unwrap();
1104 assert_eq!(state.total_failures, 1);
1105 assert_eq!(state.failures_at_level, 1);
1106 }
1107
1108 #[test]
1109 fn test_list_actors_at_level() {
1110 let (cookie, js, captcha, tarpit) = test_managers();
1111 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1112
1113 manager.get_challenge("actor_1", 0.3); manager.get_challenge("actor_2", 0.5); manager.get_challenge("actor_3", 0.5); manager.get_challenge("actor_4", 0.9); let cookie_actors = manager.list_actors_at_level(ChallengeLevel::Cookie);
1119 assert_eq!(cookie_actors.len(), 1);
1120
1121 let js_actors = manager.list_actors_at_level(ChallengeLevel::JsChallenge);
1122 assert_eq!(js_actors.len(), 2);
1123
1124 let block_actors = manager.list_actors_at_level(ChallengeLevel::Block);
1125 assert_eq!(block_actors.len(), 1);
1126 }
1127
1128 #[test]
1129 fn test_reset() {
1130 let (cookie, js, captcha, tarpit) = test_managers();
1131 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1132
1133 manager.get_challenge("actor_123", 0.5);
1134 assert!(manager.get_actor_state("actor_123").is_some());
1135
1136 manager.reset("actor_123");
1137 assert!(manager.get_actor_state("actor_123").is_none());
1138 }
1139
1140 #[test]
1141 fn test_stats_tracking() {
1142 let (cookie, js, captcha, tarpit) = test_managers();
1143 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1144
1145 manager.get_challenge("actor_1", 0.5);
1146 manager.get_challenge("actor_2", 0.5);
1147
1148 let stats = manager.stats().snapshot();
1149 assert_eq!(stats.actors_tracked, 2);
1150 assert_eq!(stats.challenges_issued, 2);
1151
1152 manager.record_failure("actor_1");
1153 let stats = manager.stats().snapshot();
1154 assert_eq!(stats.failures, 1);
1155
1156 manager.record_success("actor_1");
1157 let stats = manager.stats().snapshot();
1158 assert_eq!(stats.successes, 1);
1159 }
1160
1161 #[test]
1162 fn test_challenge_level_display() {
1163 assert_eq!(ChallengeLevel::None.name(), "none");
1164 assert_eq!(ChallengeLevel::Cookie.name(), "cookie");
1165 assert_eq!(ChallengeLevel::JsChallenge.name(), "js_challenge");
1166 assert_eq!(ChallengeLevel::Captcha.name(), "captcha");
1167 assert_eq!(ChallengeLevel::Tarpit.name(), "tarpit");
1168 assert_eq!(ChallengeLevel::Block.name(), "block");
1169
1170 assert_eq!(format!("{}", ChallengeLevel::Cookie), "cookie");
1171 }
1172
1173 #[test]
1174 fn test_challenge_level_from_u8() {
1175 assert_eq!(ChallengeLevel::from_u8(0), ChallengeLevel::None);
1176 assert_eq!(ChallengeLevel::from_u8(1), ChallengeLevel::Cookie);
1177 assert_eq!(ChallengeLevel::from_u8(2), ChallengeLevel::JsChallenge);
1178 assert_eq!(ChallengeLevel::from_u8(3), ChallengeLevel::Captcha);
1179 assert_eq!(ChallengeLevel::from_u8(4), ChallengeLevel::Tarpit);
1180 assert_eq!(ChallengeLevel::from_u8(5), ChallengeLevel::Block);
1181 assert_eq!(ChallengeLevel::from_u8(100), ChallengeLevel::Block); }
1183
1184 #[test]
1185 fn test_risk_higher_than_current_level() {
1186 let (cookie, js, captcha, tarpit) = test_managers();
1187 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1188
1189 manager.get_challenge("actor_123", 0.3);
1191 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Cookie);
1192
1193 let response = manager.get_challenge("actor_123", 0.9);
1195 assert!(matches!(response, ChallengeResponse::Block { .. }));
1196 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Block);
1197 }
1198
1199 #[test]
1200 fn test_behavior_escalates_above_risk() {
1201 let (cookie, js, captcha, tarpit) = test_managers();
1202 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1203
1204 manager.get_challenge("actor_123", 0.1);
1206 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::None);
1207
1208 manager.escalate("actor_123");
1209 manager.escalate("actor_123");
1210 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::JsChallenge);
1211
1212 let response = manager.get_challenge("actor_123", 0.1);
1214 assert!(matches!(response, ChallengeResponse::JsChallenge { .. }));
1215 }
1216
1217 #[test]
1218 fn test_escalation_history() {
1219 let (cookie, js, captcha, tarpit) = test_managers();
1220 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1221
1222 manager.get_challenge("actor_123", 0.3);
1223 manager.escalate("actor_123");
1224 manager.escalate("actor_123");
1225
1226 let state = manager.get_actor_state("actor_123").unwrap();
1227 assert_eq!(state.escalation_history.len(), 2);
1228 assert_eq!(state.escalation_history[0].0, ChallengeLevel::JsChallenge);
1229 assert_eq!(state.escalation_history[1].0, ChallengeLevel::Tarpit);
1230 }
1231
1232 #[test]
1233 fn test_clear() {
1234 let (cookie, js, captcha, tarpit) = test_managers();
1235 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1236
1237 manager.get_challenge("actor_1", 0.5);
1238 manager.get_challenge("actor_2", 0.5);
1239 assert_eq!(manager.len(), 2);
1240
1241 manager.clear();
1242 assert!(manager.is_empty());
1243 }
1244
1245 #[test]
1246 fn test_disabled_levels_skipped() {
1247 let (cookie, js, captcha, tarpit) = test_managers();
1248 let config = ProgressionConfig {
1249 enable_cookie: false,
1250 enable_js_challenge: false,
1251 enable_captcha: false,
1252 enable_tarpit: true,
1253 ..test_config()
1254 };
1255 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, config);
1256
1257 let level = manager.escalate("actor_123");
1259 assert_eq!(level, ChallengeLevel::Tarpit);
1260
1261 let level = manager.escalate("actor_123");
1262 assert_eq!(level, ChallengeLevel::Block);
1263 }
1264
1265 #[test]
1266 fn test_tarpit_challenge() {
1267 let (cookie, js, captcha, tarpit) = test_managers();
1268 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, test_config());
1269
1270 let response = manager.get_challenge("actor_123", 0.7);
1272 assert!(matches!(response, ChallengeResponse::Tarpit { delay_ms } if delay_ms > 0));
1273 }
1274
1275 #[test]
1276 fn test_captcha_challenge() {
1277 let (cookie, js, captcha, tarpit) = test_managers();
1278 let config = ProgressionConfig {
1279 enable_captcha: true,
1280 ..test_config()
1281 };
1282 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, config);
1283
1284 let response = manager.get_challenge("actor_123", 0.65);
1286 match response {
1287 ChallengeResponse::Captcha { html, session_id } => {
1288 assert!(html.contains("Human Verification Required"));
1289 assert!(html.contains("What is"));
1290 assert!(!session_id.is_empty());
1291 }
1292 _ => panic!("Expected CAPTCHA challenge, got {:?}", response),
1293 }
1294 }
1295
1296 #[test]
1297 fn test_captcha_validation() {
1298 let (cookie, js, captcha, tarpit) = test_managers();
1299 let config = ProgressionConfig {
1300 enable_captcha: true,
1301 ..test_config()
1302 };
1303 let manager = ProgressionManager::new(cookie, js, captcha.clone(), tarpit, config);
1304
1305 let response = manager.get_challenge("actor_123", 0.65);
1307 let session_id = match response {
1308 ChallengeResponse::Captcha { session_id, .. } => session_id,
1309 _ => panic!("Expected CAPTCHA challenge"),
1310 };
1311
1312 let challenge = captcha.issue_challenge("actor_123");
1314 let parts: Vec<&str> = challenge.question.split_whitespace().collect();
1316 let a: i32 = parts[2].parse().unwrap();
1317 let b: i32 = parts[4].trim_end_matches('?').parse().unwrap();
1318 let answer = a + b;
1319
1320 let validation_response = format!("{}:{}", challenge.session_id, answer);
1322 let result = captcha.validate_response("actor_123", &validation_response);
1323 assert_eq!(result, ValidationResult::Valid);
1324 }
1325
1326 #[test]
1327 fn test_captcha_escalation_with_enabled() {
1328 let (cookie, js, captcha, tarpit) = test_managers();
1329 let config = ProgressionConfig {
1330 enable_captcha: true,
1331 ..test_config()
1332 };
1333 let manager = ProgressionManager::new(cookie, js, captcha, tarpit, config);
1334
1335 manager.get_challenge("actor_123", 0.3);
1337 assert_eq!(manager.get_level("actor_123"), ChallengeLevel::Cookie);
1338
1339 let level = manager.escalate("actor_123");
1341 assert_eq!(level, ChallengeLevel::JsChallenge);
1342
1343 let level = manager.escalate("actor_123");
1344 assert_eq!(level, ChallengeLevel::Captcha); let level = manager.escalate("actor_123");
1347 assert_eq!(level, ChallengeLevel::Tarpit);
1348
1349 let level = manager.escalate("actor_123");
1350 assert_eq!(level, ChallengeLevel::Block);
1351 }
1352}