eoka 0.3.15

Stealth browser automation for Rust. Puppeteer/Playwright alternative with anti-bot bypass.
Documentation
//! Browser fingerprint generation
//!
//! Generates realistic, randomized browser fingerprints.

/// Chrome versions (recent, realistic)
const CHROME_VERSIONS: &[&str] = &[
    "120.0.0.0",
    "121.0.0.0",
    "122.0.0.0",
    "123.0.0.0",
    "124.0.0.0",
    "125.0.0.0",
    "126.0.0.0",
    "127.0.0.0",
    "128.0.0.0",
    "129.0.0.0",
    "130.0.0.0",
    "131.0.0.0",
    "132.0.0.0",
    "133.0.0.0",
    "134.0.0.0",
];

/// macOS versions
const MACOS_VERSIONS: &[&str] = &[
    "10_15_7", "11_0_0", "11_6_0", "12_0_0", "12_6_0", "13_0_0", "13_4_0", "14_0_0", "14_2_0",
    "14_4_0",
];

/// Windows versions
const WINDOWS_VERSIONS: &[&str] = &["10.0"];

/// WebGL renderers for Mac
const WEBGL_RENDERERS_MAC: &[&str] = &[
    "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)",
    "ANGLE (Apple, Apple M1, OpenGL 4.1)",
    "ANGLE (Apple, Apple M2, OpenGL 4.1)",
    "ANGLE (Apple, Apple M3, OpenGL 4.1)",
    "ANGLE (Intel, Intel Iris Pro Graphics 6200, OpenGL 4.1)",
    "ANGLE (Intel, Intel UHD Graphics 630, OpenGL 4.1)",
];

/// WebGL renderers for Windows
const WEBGL_RENDERERS_WINDOWS: &[&str] = &[
    "ANGLE (NVIDIA, NVIDIA GeForce RTX 3080, Direct3D11)",
    "ANGLE (NVIDIA, NVIDIA GeForce RTX 4070, Direct3D11)",
    "ANGLE (NVIDIA, NVIDIA GeForce GTX 1080, Direct3D11)",
    "ANGLE (AMD, AMD Radeon RX 6800 XT, Direct3D11)",
    "ANGLE (Intel, Intel UHD Graphics 770, Direct3D11)",
];

/// Screen resolutions
const SCREEN_RESOLUTIONS: &[(u32, u32)] = &[
    (1920, 1080),
    (2560, 1440),
    (3840, 2160),
    (1440, 900),
    (1680, 1050),
    (2560, 1600),
    (3024, 1964), // MacBook Pro 14"
    (3456, 2234), // MacBook Pro 16"
];

/// Generate a random realistic user agent
/// Pick a random element from a slice
fn choose<T>(slice: &[T]) -> &T {
    &slice[fastrand::usize(..slice.len())]
}

pub fn random_user_agent() -> String {
    let chrome_version = choose(CHROME_VERSIONS);

    // 30% Mac, 70% Windows (matches real internet traffic)
    if fastrand::f64() < 0.3 {
        let macos = choose(MACOS_VERSIONS);
        format!(
            "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
            macos, chrome_version
        )
    } else {
        let windows = choose(WINDOWS_VERSIONS);
        format!(
            "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
            windows, chrome_version
        )
    }
}

/// Browser fingerprint data
#[derive(Debug, Clone)]
pub struct Fingerprint {
    pub user_agent: String,
    pub platform: Platform,
    pub screen_width: u32,
    pub screen_height: u32,
    pub color_depth: u8,
    pub hardware_concurrency: u8,
    pub device_memory: u8,
    pub timezone: String,
    pub languages: Vec<String>,
    pub webgl_vendor: String,
    pub webgl_renderer: String,
}

#[derive(Debug, Clone, Copy)]
pub enum Platform {
    MacOS,
    Windows,
}

impl Fingerprint {
    /// Generate a random consistent fingerprint
    pub fn random() -> Self {
        let platform = if fastrand::f64() < 0.3 {
            Platform::MacOS
        } else {
            Platform::Windows
        };

        let (screen_width, screen_height) = *choose(SCREEN_RESOLUTIONS);

        let (webgl_vendor, webgl_renderer) = match platform {
            Platform::MacOS => ("Google Inc. (Apple)", *choose(WEBGL_RENDERERS_MAC)),
            Platform::Windows => (
                "Google Inc. (NVIDIA Corporation)",
                *choose(WEBGL_RENDERERS_WINDOWS),
            ),
        };

        let hardware_concurrency = *choose(&[4, 8, 10, 12, 16]);
        let device_memory = *choose(&[8, 16, 32]);

        Self {
            user_agent: random_user_agent(),
            platform,
            screen_width,
            screen_height,
            color_depth: 24,
            hardware_concurrency,
            device_memory,
            timezone: choose(super::evasions::COMMON_TIMEZONES).to_string(),
            languages: vec!["en-US".to_string(), "en".to_string()],
            webgl_vendor: webgl_vendor.to_string(),
            webgl_renderer: webgl_renderer.to_string(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_random_user_agent_format() {
        for _ in 0..20 {
            let ua = random_user_agent();
            assert!(ua.starts_with("Mozilla/5.0"));
            assert!(ua.contains("Chrome/"));
            assert!(ua.contains("Safari/537.36"));
        }
    }

    #[test]
    fn test_fingerprint_random() {
        let fp = Fingerprint::random();
        assert!(!fp.user_agent.is_empty());
        assert!(fp.screen_width > 0);
        assert!(fp.screen_height > 0);
        assert!([4, 8, 10, 12, 16].contains(&fp.hardware_concurrency));
        assert!([8, 16, 32].contains(&fp.device_memory));
    }
}