rustenium-identity 0.1.7

A versatile stealth overlay for rustenium
Documentation
use crate::error::IdentityError;
use crate::identity::{Browser, Identity, Os};

use crate::script;
use crate::ua;

use rustenium::browsers::chrome::browser::ChromeBrowser;
use rustenium::browsers::cdp_browser::CdpBrowser;
use rustenium_cdp_definitions::browser_protocol::emulation::commands::{
    SetDeviceMetricsOverride, SetHardwareConcurrencyOverride, SetLocaleOverride,
    SetTimezoneOverride, SetTouchEmulationEnabled, SetUserAgentOverride,
};
use rustenium_cdp_definitions::browser_protocol::emulation::types::{
    UserAgentBrandVersion, UserAgentMetadata,
};
use rustenium_cdp_definitions::browser_protocol::network::commands::SetUserAgentOverride as NetworkSetUserAgentOverride;
use rustenium_cdp_definitions::browser_protocol::page::commands::{
    AddScriptToEvaluateOnNewDocument, AddScriptToEvaluateOnNewDocumentMethod,
    AddScriptToEvaluateOnNewDocumentParams,
};

/// Build UserAgentMetadata (Client Hints) for Chrome/Edge identities.
fn build_client_hints(identity: &Identity) -> UserAgentMetadata {
    let major = identity.browser_version.first().copied().unwrap_or(0).to_string();
    let full_version = identity
        .browser_version
        .iter()
        .map(|v| v.to_string())
        .collect::<Vec<_>>()
        .join(".");
    let is_mobile = identity.device_model.is_some();

    let (brand_name, brand_name_full) = match identity.browser {
        Browser::Edge => ("Microsoft Edge", "Microsoft Edge"),
        _ => ("Google Chrome", "Google Chrome"),
    };

    let grease_brand = match identity.os {
        Os::Android => "Not-A.Brand",
        _ => "Not;A=Brand",
    };

    let brands = vec![
        UserAgentBrandVersion::new("Chromium", &major),
        UserAgentBrandVersion::new(grease_brand, "24"),
        UserAgentBrandVersion::new(brand_name, &major),
    ];

    let full_version_list = vec![
        UserAgentBrandVersion::new("Chromium", &full_version),
        UserAgentBrandVersion::new(grease_brand, "24.0.0.0"),
        UserAgentBrandVersion::new(brand_name_full, &full_version),
    ];

    let platform = match identity.os {
        Os::Windows => "Windows",
        Os::Macintosh => "macOS",
        Os::Linux => "Linux",
        Os::Android => "Android",
        Os::Ios => "iOS",
    };

    // On mobile, architecture and bitness should be empty
    let (architecture, bitness) = if is_mobile {
        (String::new(), String::new())
    } else {
        (
            identity.platform.architecture.clone().unwrap_or_else(|| "x86".into()),
            identity.platform.bitness.clone().unwrap_or_else(|| "64".into()),
        )
    };
    let model = identity.device_model.clone().unwrap_or_default();

    UserAgentMetadata::builder()
        .brands(brands)
        .full_version_lists(full_version_list)
        .platform(platform)
        .platform_version(&identity.platform.version)
        .architecture(&architecture)
        .model(&model)
        .mobile(is_mobile)
        .bitness(bitness)
        .build()
        .unwrap()
}

/// Apply all CDP emulation commands and register the stealth script.
pub async fn apply_identity(
    browser: &mut ChromeBrowser,
    identity: &Identity,
    timezone: &str,
) -> Result<(), IdentityError> {
    let user_agent = ua::build_user_agent(identity)?;
    let lang0 = identity
        .language
        .first()
        .cloned()
        .unwrap_or_else(|| "en-US".into());
    let is_mobile = identity.device_model.is_some();

    // Build client hints for Chrome/Edge (skip iOS — all iOS browsers are WebKit-based)
    let client_hints = if matches!(identity.browser, Browser::Chrome | Browser::Edge)
        && !matches!(identity.os, Os::Ios)
    {
        Some(build_client_hints(identity))
    } else {
        None
    };

    // Emulation.setUserAgentOverride
    let mut ua_builder = SetUserAgentOverride::builder()
        .user_agent(&user_agent)
        .accept_language(&lang0)
        .platform(identity.platform.navigator_platform.as_str());
    if let Some(ref metadata) = client_hints {
        ua_builder = ua_builder.user_agent_metadata(metadata.clone());
    }
    let ua_cmd = ua_builder.build().unwrap();
    browser
        .send_command(ua_cmd.into())
        .await
        .map_err(|e| IdentityError::CdpError(e.to_string()))?;

    // Network.setUserAgentOverride (belt-and-suspenders, also with client hints)
    let mut net_builder = NetworkSetUserAgentOverride::builder()
        .user_agent(&user_agent)
        .accept_language(&lang0);
    if let Some(ref metadata) = client_hints {
        net_builder = net_builder.user_agent_metadata(metadata.clone());
    }
    let net_ua_cmd = net_builder.build().unwrap();
    browser
        .send_command(net_ua_cmd.into())
        .await
        .map_err(|e| IdentityError::CdpError(e.to_string()))?;

    // Emulation.setDeviceMetricsOverride
    let screen_width = (identity.screen.logical_width as f64 * identity.screen.density_pixel_ratio as f64) as i64;
    let screen_height = (identity.screen.logical_height as f64 * identity.screen.density_pixel_ratio as f64) as i64;
    let device_cmd = SetDeviceMetricsOverride::builder()
        .width(identity.screen.logical_width as i64)
        .height(identity.screen.logical_height as i64)
        .device_scale_factor(identity.screen.density_pixel_ratio as f64)
        .mobile(is_mobile)
        .screen_width(screen_width)
        .screen_height(screen_height)
        .build()
        .unwrap();
    browser
        .send_command(device_cmd.into())
        .await
        .map_err(|e| IdentityError::CdpError(e.to_string()))?;

    // Emulation.setTouchEmulationEnabled
    if identity.has_touch {
        let touch_cmd = SetTouchEmulationEnabled::builder()
            .enabled(true)
            .max_touch_points(5i64)
            .build()
            .unwrap();
        browser
            .send_command(touch_cmd.into())
            .await
            .map_err(|e| IdentityError::CdpError(e.to_string()))?;
    }

    // Emulation.setLocaleOverride
    let locale_cmd = SetLocaleOverride::builder()
        .locale(&lang0)
        .build();
    browser
        .send_command(locale_cmd.into())
        .await
        .map_err(|e| IdentityError::CdpError(e.to_string()))?;

    // Emulation.setTimezoneOverride
    let tz_cmd = SetTimezoneOverride::builder()
        .timezone_id(timezone)
        .build()
        .unwrap();
    browser
        .send_command(tz_cmd.into())
        .await
        .map_err(|e| IdentityError::CdpError(e.to_string()))?;

    // Emulation.setHardwareConcurrencyOverride
    let hc_cmd = SetHardwareConcurrencyOverride::builder()
        .hardware_concurrency(identity.hardware_concurrency as i64)
        .build()
        .unwrap();
    browser
        .send_command(hc_cmd.into())
        .await
        .map_err(|e| IdentityError::CdpError(e.to_string()))?;

    // Page.addScriptToEvaluateOnNewDocument — stealth bootstrap
    let stealth_script = script::build_stealth_script(identity);
    let cmd = AddScriptToEvaluateOnNewDocument {
        method: AddScriptToEvaluateOnNewDocumentMethod::AddScriptToEvaluateOnNewDocument,
        params: AddScriptToEvaluateOnNewDocumentParams::new(stealth_script),
    };
    browser
        .send_command(cmd.into())
        .await
        .map_err(|e| IdentityError::InjectionError(e.to_string()))?;

    Ok(())
}