use chromiumoxide::Page;
use crate::error::{Result, ScrapeError};
use rand::Rng;
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StealthMode {
None,
Basic,
Advanced,
Auto,
}
impl StealthMode {
pub fn from_env() -> Self {
std::env::var("BROWSER_STEALTH_MODE")
.ok()
.and_then(|mode| match mode.to_lowercase().as_str() {
"none" => Some(StealthMode::None),
"basic" => Some(StealthMode::Basic),
"advanced" => Some(StealthMode::Advanced),
"auto" => Some(StealthMode::Auto),
_ => {
warn!("Invalid BROWSER_STEALTH_MODE '{}', using Basic", mode);
None
}
})
.unwrap_or(StealthMode::Basic) }
pub fn timeout_ms(&self) -> u64 {
match self {
StealthMode::None | StealthMode::Basic => 30_000, StealthMode::Advanced | StealthMode::Auto => {
std::env::var("BROWSER_STEALTH_TIMEOUT_MS")
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(120_000)
}
}
}
}
impl std::fmt::Display for StealthMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StealthMode::None => write!(f, "none"),
StealthMode::Basic => write!(f, "basic"),
StealthMode::Advanced => write!(f, "advanced"),
StealthMode::Auto => write!(f, "auto"),
}
}
}
pub async fn apply_stealth_techniques(page: &Page, mode: StealthMode) -> Result<()> {
if mode == StealthMode::None {
debug!("Stealth mode is None, skipping techniques");
return Ok(());
}
info!("Applying stealth mode: {}", mode);
let stealth_js = include_str!("stealth.js");
page.evaluate(stealth_js)
.await
.map_err(|e| ScrapeError::BrowserError(format!("Failed to inject stealth JavaScript: {}", e)))?;
debug!("JavaScript stealth injection successful");
if matches!(mode, StealthMode::Advanced | StealthMode::Auto) {
apply_advanced_stealth(page).await?;
}
info!("Stealth techniques applied successfully (mode: {})", mode);
Ok(())
}
async fn apply_advanced_stealth(page: &Page) -> Result<()> {
debug!("Applying advanced fingerprint randomization");
let (width, height, cores, memory) = {
let mut rng = rand::thread_rng();
let resolutions = [
(1920, 1080), (1366, 768), (1440, 900), (1536, 864), (1280, 720), (1600, 900), ];
let (w, h) = resolutions[rng.gen_range(0..resolutions.len())];
let c = rng.gen_range(4..17);
let memory_options = [4, 8, 16, 32];
let m = memory_options[rng.gen_range(0..memory_options.len())];
(w, h, c, m)
};
let fingerprint_js = format!(
r#"
// Screen dimensions
Object.defineProperty(screen, 'width', {{ get: () => {}, configurable: true }});
Object.defineProperty(screen, 'height', {{ get: () => {}, configurable: true }});
Object.defineProperty(screen, 'availWidth', {{ get: () => {}, configurable: true }});
Object.defineProperty(screen, 'availHeight', {{ get: () => {}, configurable: true }});
// Hardware
Object.defineProperty(navigator, 'hardwareConcurrency', {{ get: () => {}, configurable: true }});
Object.defineProperty(navigator, 'deviceMemory', {{ get: () => {}, configurable: true }});
// Log fingerprint (for debugging)
console.log('[Stealth] Fingerprint: {{width}}x{{height}}, {{cores}} cores, {{memory}}GB RAM');
"#,
width, height, width, height - 40, cores, memory,
);
page.evaluate(fingerprint_js)
.await
.map_err(|e| ScrapeError::BrowserError(format!("Failed to apply fingerprint randomization: {}", e)))?;
info!(
"Advanced stealth applied: {}x{} screen, {} cores, {}GB RAM",
width, height, cores, memory
);
Ok(())
}
pub fn should_escalate_to_stealth(status_code: u16, current_mode: StealthMode) -> bool {
if current_mode != StealthMode::Auto {
return false;
}
let should_escalate = [401, 403, 429].contains(&status_code);
if should_escalate {
warn!(
"Detected status code {} - will auto-escalate to advanced stealth",
status_code
);
}
should_escalate
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stealth_mode_from_env() {
std::env::set_var("BROWSER_STEALTH_MODE", "none");
assert_eq!(StealthMode::from_env(), StealthMode::None);
std::env::set_var("BROWSER_STEALTH_MODE", "basic");
assert_eq!(StealthMode::from_env(), StealthMode::Basic);
std::env::set_var("BROWSER_STEALTH_MODE", "advanced");
assert_eq!(StealthMode::from_env(), StealthMode::Advanced);
std::env::set_var("BROWSER_STEALTH_MODE", "auto");
assert_eq!(StealthMode::from_env(), StealthMode::Auto);
std::env::set_var("BROWSER_STEALTH_MODE", "invalid");
assert_eq!(StealthMode::from_env(), StealthMode::Basic);
std::env::remove_var("BROWSER_STEALTH_MODE");
assert_eq!(StealthMode::from_env(), StealthMode::Basic);
}
#[test]
fn test_stealth_mode_timeout() {
assert_eq!(StealthMode::None.timeout_ms(), 30_000);
assert_eq!(StealthMode::Basic.timeout_ms(), 30_000);
assert_eq!(StealthMode::Advanced.timeout_ms(), 120_000);
assert_eq!(StealthMode::Auto.timeout_ms(), 120_000);
}
#[test]
fn test_should_escalate_to_stealth() {
assert!(should_escalate_to_stealth(401, StealthMode::Auto));
assert!(should_escalate_to_stealth(403, StealthMode::Auto));
assert!(should_escalate_to_stealth(429, StealthMode::Auto));
assert!(!should_escalate_to_stealth(200, StealthMode::Auto));
assert!(!should_escalate_to_stealth(404, StealthMode::Auto));
assert!(!should_escalate_to_stealth(500, StealthMode::Auto));
assert!(!should_escalate_to_stealth(403, StealthMode::None));
assert!(!should_escalate_to_stealth(403, StealthMode::Basic));
assert!(!should_escalate_to_stealth(403, StealthMode::Advanced));
}
#[test]
fn test_stealth_mode_display() {
assert_eq!(format!("{}", StealthMode::None), "none");
assert_eq!(format!("{}", StealthMode::Basic), "basic");
assert_eq!(format!("{}", StealthMode::Advanced), "advanced");
assert_eq!(format!("{}", StealthMode::Auto), "auto");
}
}