Skip to main content

stygian_browser/validation/
validators.rs

1//! Individual anti-bot validator implementations.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::Instant;
6
7use tracing::debug;
8
9use crate::page::WaitUntil;
10use crate::pool::BrowserPool;
11
12use super::{ValidationResult, ValidationTarget};
13
14// ───────────────────────────────────────────────────────────────────────────
15// Tier 1: Open-Source Observatories (no rate limits)
16// ───────────────────────────────────────────────────────────────────────────
17
18/// Run the `CreepJS` observatory validator.
19///
20/// Navigates to `CreepJS`, waits for results, extracts the trust score, and
21/// checks if it is > 50%.
22pub fn run_creepjs(pool: &Arc<BrowserPool>) -> ValidationResult {
23    let start = Instant::now();
24    let result = creepjs_impl(pool);
25    ValidationResult {
26        elapsed: start.elapsed(),
27        ..result
28    }
29}
30
31fn creepjs_impl(_pool: &Arc<BrowserPool>) -> ValidationResult {
32    // NOTE: Real implementation would:
33    // 1. Acquire a session from the pool
34    // 2. Navigate to CreepJS URL
35    // 3. Wait for results-loaded signal
36    // 4. Extract trust score from JSON in window.result or DOM element
37    // 5. Check score > 50%
38    // 6. Return with score and details
39    //
40    // For now, return a stub "not yet implemented" result that keeps CI
41    // deterministic while documenting the need for a live browser environment.
42
43    ValidationResult {
44        target: ValidationTarget::CreepJs,
45        passed: false,
46        score: None,
47        details: HashMap::from([("phase".to_string(), "stub-not-yet-implemented".to_string())]),
48        screenshot: None,
49        elapsed: std::time::Duration::ZERO,
50    }
51}
52
53/// Run the `BrowserScan` validator.
54///
55/// Navigates to `BrowserScan`, waits for scan completion, and extracts the
56/// authenticity percentage.
57pub fn run_browserscan(pool: &Arc<BrowserPool>) -> ValidationResult {
58    let start = Instant::now();
59    let result = browserscan_impl(pool);
60    ValidationResult {
61        elapsed: start.elapsed(),
62        ..result
63    }
64}
65
66fn browserscan_impl(_pool: &Arc<BrowserPool>) -> ValidationResult {
67    // NOTE: Real implementation would:
68    // 1. Acquire a session from the pool
69    // 2. Navigate to BrowserScan URL
70    // 3. Wait for scan-complete signal
71    // 4. Extract authenticity percentage from JSON or DOM
72    // 5. Check score > 90%
73    // 6. Return with score and details
74
75    ValidationResult {
76        target: ValidationTarget::BrowserScan,
77        passed: false,
78        score: None,
79        details: HashMap::from([("phase".to_string(), "stub-not-yet-implemented".to_string())]),
80        screenshot: None,
81        elapsed: std::time::Duration::ZERO,
82    }
83}
84
85// ───────────────────────────────────────────────────────────────────────────
86// Tier 2: Anti-Bot Protected Sites (may rate-limit, use #[ignore])
87// ───────────────────────────────────────────────────────────────────────────
88
89/// Run the Kasada validator against `WizzAir` booking page.
90///
91/// Navigates to a Kasada-protected page, waits for page load, and checks
92/// whether a 429/403 block page is returned or the page loads normally.
93pub async fn run_kasada(pool: &Arc<BrowserPool>) -> ValidationResult {
94    let start = Instant::now();
95    let result = kasada_impl(pool).await;
96    ValidationResult {
97        elapsed: start.elapsed(),
98        ..result
99    }
100}
101
102async fn kasada_impl(pool: &Arc<BrowserPool>) -> ValidationResult {
103    let url = ValidationTarget::Kasada.url();
104    debug!("Kasada validator: navigating to {url}");
105
106    match pool.acquire().await {
107        Ok(session) => {
108            match session.browser() {
109                Some(browser) => {
110                    match browser.new_page().await {
111                        Ok(mut page) => {
112                            // Try to navigate with a generous timeout
113                            let navigate_result = page
114                                .navigate(
115                                    url,
116                                    WaitUntil::DomContentLoaded,
117                                    std::time::Duration::from_secs(20),
118                                )
119                                .await;
120
121                            let passed = match navigate_result {
122                                Ok(()) => {
123                                    // Check HTTP status code — 200 OK is a pass
124                                    true
125                                }
126                                Err(e) => {
127                                    // Navigation timeout or network error typically means blocked
128                                    debug!("Kasada: navigation failed: {}", e);
129                                    false
130                                }
131                            };
132
133                            page.close().await.ok();
134
135                            ValidationResult {
136                                target: ValidationTarget::Kasada,
137                                passed,
138                                score: None,
139                                details: HashMap::from([(
140                                    "phase".to_string(),
141                                    "load-check".to_string(),
142                                )]),
143                                screenshot: None,
144                                elapsed: std::time::Duration::ZERO,
145                            }
146                        }
147                        Err(e) => {
148                            ValidationResult::failed(ValidationTarget::Kasada, &e.to_string())
149                        }
150                    }
151                }
152                None => ValidationResult::failed(ValidationTarget::Kasada, "browser handle lost"),
153            }
154        }
155        Err(e) => ValidationResult::failed(ValidationTarget::Kasada, &e.to_string()),
156    }
157}
158
159/// Run the Cloudflare validator on a CF-protected site.
160///
161/// Navigates to a Cloudflare-protected page and checks if the page loads
162/// without a challenge block.
163pub async fn run_cloudflare(pool: &Arc<BrowserPool>) -> ValidationResult {
164    let start = Instant::now();
165    let result = cloudflare_impl(pool).await;
166    ValidationResult {
167        elapsed: start.elapsed(),
168        ..result
169    }
170}
171
172async fn cloudflare_impl(pool: &Arc<BrowserPool>) -> ValidationResult {
173    let url = ValidationTarget::Cloudflare.url();
174    debug!("Cloudflare validator: navigating to {url}");
175
176    match pool.acquire().await {
177        Ok(session) => match session.browser() {
178            Some(browser) => match browser.new_page().await {
179                Ok(mut page) => {
180                    let navigate_result = page
181                        .navigate(
182                            url,
183                            WaitUntil::DomContentLoaded,
184                            std::time::Duration::from_secs(20),
185                        )
186                        .await;
187
188                    let passed = navigate_result.is_ok();
189
190                    page.close().await.ok();
191
192                    ValidationResult {
193                        target: ValidationTarget::Cloudflare,
194                        passed,
195                        score: None,
196                        details: HashMap::from([("phase".to_string(), "load-check".to_string())]),
197                        screenshot: None,
198                        elapsed: std::time::Duration::ZERO,
199                    }
200                }
201                Err(e) => ValidationResult::failed(ValidationTarget::Cloudflare, &e.to_string()),
202            },
203            None => ValidationResult::failed(ValidationTarget::Cloudflare, "browser handle lost"),
204        },
205        Err(e) => ValidationResult::failed(ValidationTarget::Cloudflare, &e.to_string()),
206    }
207}
208
209/// Run the Akamai validator on an Akamai-protected site (e.g., `FedEx`).
210///
211/// Navigates to the `FedEx` tracking page and checks if the page loads
212/// without bot detection.
213pub async fn run_akamai(pool: &Arc<BrowserPool>) -> ValidationResult {
214    let start = Instant::now();
215    let result = akamai_impl(pool).await;
216    ValidationResult {
217        elapsed: start.elapsed(),
218        ..result
219    }
220}
221
222async fn akamai_impl(pool: &Arc<BrowserPool>) -> ValidationResult {
223    let url = ValidationTarget::Akamai.url();
224    debug!("Akamai validator: navigating to {url}");
225
226    match pool.acquire().await {
227        Ok(session) => match session.browser() {
228            Some(browser) => match browser.new_page().await {
229                Ok(mut page) => {
230                    let navigate_result = page
231                        .navigate(
232                            url,
233                            WaitUntil::DomContentLoaded,
234                            std::time::Duration::from_secs(20),
235                        )
236                        .await;
237
238                    let passed = navigate_result.is_ok();
239
240                    page.close().await.ok();
241
242                    ValidationResult {
243                        target: ValidationTarget::Akamai,
244                        passed,
245                        score: None,
246                        details: HashMap::from([("phase".to_string(), "load-check".to_string())]),
247                        screenshot: None,
248                        elapsed: std::time::Duration::ZERO,
249                    }
250                }
251                Err(e) => ValidationResult::failed(ValidationTarget::Akamai, &e.to_string()),
252            },
253            None => ValidationResult::failed(ValidationTarget::Akamai, "browser handle lost"),
254        },
255        Err(e) => ValidationResult::failed(ValidationTarget::Akamai, &e.to_string()),
256    }
257}