bimp-persona 0.0.1

Browser persona configuration and resolved fingerprint values for Bimp.
Documentation
use serde::{Deserialize, Serialize};

use crate::options::Viewport;
use crate::seed::{BrowserPersonaPreset, PersonaSeed};

/// Network-visible browser fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NetworkFingerprint {
    pub user_agent: String,
    pub accept_language: String,
    pub accept_header: String,
    pub sec_ch_ua: String,
    pub sec_ch_ua_mobile: String,
    pub sec_ch_ua_platform: String,
}

/// Automation exposure policy for WebDriver-related JS surfaces.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AutomationPolicy {
    pub webdriver: bool,
    pub expose_webdriver_helpers: bool,
    pub expose_automation_globals: bool,
}

impl Default for AutomationPolicy {
    fn default() -> Self {
        Self {
            webdriver: false,
            expose_webdriver_helpers: false,
            expose_automation_globals: false,
        }
    }
}

/// Resolved screen fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScreenFingerprint {
    pub width: u32,
    pub height: u32,
    pub avail_width: u32,
    pub avail_height: u32,
    pub avail_left: i32,
    pub avail_top: i32,
    pub left: i32,
    pub top: i32,
    pub color_depth: u32,
    pub pixel_depth: u32,
    pub device_scale_factor: u32,
    pub is_extended: bool,
    pub orientation_type: String,
    pub orientation_angle: u16,
}

/// Resolved window fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WindowFingerprint {
    pub outer_width: u32,
    pub outer_height: u32,
    pub screen_x: i32,
    pub screen_y: i32,
}

/// Resolved CSS feature-detection fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CssFingerprint {
    pub moz_prefix_enabled: bool,
    pub webkit_prefix_enabled: bool,
}

impl WindowFingerprint {
    pub fn from_viewport(viewport: &Viewport) -> Self {
        Self {
            outer_width: viewport.width,
            outer_height: viewport.height,
            screen_x: 0,
            screen_y: 0,
        }
    }
}

impl ScreenFingerprint {
    pub fn from_viewport(viewport: &Viewport) -> Self {
        Self {
            width: viewport.width,
            height: viewport.height,
            avail_width: viewport.width,
            avail_height: viewport.height,
            avail_left: 0,
            avail_top: 0,
            left: 0,
            top: 0,
            color_depth: 24,
            pixel_depth: 24,
            device_scale_factor: viewport.device_scale_factor,
            is_extended: false,
            orientation_type: if viewport.width >= viewport.height {
                "landscape-primary"
            } else {
                "portrait-primary"
            }
            .to_string(),
            orientation_angle: 0,
        }
    }
}

/// Resolved WebGL and graphics fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphicsFingerprint {
    pub webgl_vendor: String,
    pub webgl_renderer: String,
    pub webgl_masked_vendor: String,
    pub webgl_masked_renderer: String,
    pub webgl_seed: PersonaSeed,
}

/// Resolved JavaScript environment fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsFingerprint {
    pub app_version: String,
    pub platform: String,
    pub language: String,
    pub languages: Vec<String>,
    pub hardware_concurrency: u32,
    pub device_memory_gb: u32,
    pub max_touch_points: u32,
    pub do_not_track: String,
    pub expose_global_privacy_control: bool,
    pub global_privacy_control: bool,
    pub permissions_enabled: bool,
    pub bluetooth_enabled: bool,
    pub bluetooth_available: bool,
    pub media_devices_enabled: bool,
    pub webgpu_enabled: bool,
    pub offscreen_canvas_enabled: bool,
    pub service_worker_enabled: bool,
    pub ua_platform_version: String,
    pub ua_architecture: String,
    pub ua_bitness: String,
    pub ua_model: String,
    pub timezone: String,
    pub vendor: String,
    pub product_sub: String,
    pub pdf_viewer_enabled: bool,
}

/// Resolved locale, timezone, and geolocation fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GeoFingerprint {
    pub timezone: String,
    pub locale: String,
    pub latitude: Option<String>,
    pub longitude: Option<String>,
    pub accuracy_meters: Option<u32>,
}

/// Resolved WebRTC address fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WebRtcFingerprint {
    pub ipv4: Option<String>,
    pub ipv6: Option<String>,
}

/// Resolved canvas fingerprint behavior.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CanvasFingerprint {
    pub seed: PersonaSeed,
    pub noise_enabled: bool,
    pub noise_amplitude: u8,
    pub blink_low_entropy_probe: bool,
}

/// Resolved DOMRect/SVGRect fingerprint behavior.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DomRectFingerprint {
    pub seed: PersonaSeed,
    pub enabled: bool,
    pub quantization_steps_per_px: u32,
    pub fill_empty_client_rects: bool,
}

/// Resolved JavaScript engine fingerprint behavior.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EngineFingerprint {
    pub enabled: bool,
    pub to_fixed_range_error_message: String,
    pub array_constructor_source: String,
}

/// Resolved audio fingerprint behavior.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AudioFingerprint {
    pub seed: PersonaSeed,
    pub sample_rate: u32,
    pub output_latency_ms: u32,
    pub max_channel_count: u32,
}

/// Resolved font fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FontFingerprint {
    pub seed: PersonaSeed,
    pub families: Vec<String>,
}

/// Resolved media devices and speech synthesis fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaFingerprint {
    pub speech_voices: Vec<String>,
    pub audio_inputs: u32,
    pub video_inputs: u32,
    pub audio_outputs: u32,
}

/// Resolved battery fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BatteryFingerprint {
    pub charging: bool,
    pub charging_time_seconds: u32,
    pub discharging_time_seconds: Option<u32>,
    pub level_percent: u32,
}

/// Resolved storage quota fingerprint values.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StorageFingerprint {
    pub quota_bytes: Option<u64>,
}

/// Complete resolved persona consumed by the runtime and Servo preferences.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResolvedPersona {
    pub seed: PersonaSeed,
    pub preset: BrowserPersonaPreset,
    pub startup_url: Option<String>,
    pub browser_name: String,
    pub browser_version: String,
    pub platform_name: String,
    pub viewport: Viewport,
    pub screen: ScreenFingerprint,
    pub window: WindowFingerprint,
    pub css: CssFingerprint,
    pub network: NetworkFingerprint,
    pub graphics: GraphicsFingerprint,
    pub js: JsFingerprint,
    pub geo: GeoFingerprint,
    pub webrtc: WebRtcFingerprint,
    pub canvas: CanvasFingerprint,
    pub domrect: DomRectFingerprint,
    pub engine: EngineFingerprint,
    pub audio: AudioFingerprint,
    pub fonts: FontFingerprint,
    pub media: MediaFingerprint,
    pub battery: BatteryFingerprint,
    pub storage: StorageFingerprint,
    pub automation: AutomationPolicy,
}

pub type BrowserPersona = ResolvedPersona;

impl ResolvedPersona {
    pub fn from_preset(preset: BrowserPersonaPreset) -> Self {
        Self::from_preset_and_seed(
            preset,
            PersonaSeed::from_stable_input(format!("preset:{preset:?}")),
        )
    }

    pub fn from_preset_and_seed(preset: BrowserPersonaPreset, seed: PersonaSeed) -> Self {
        match preset {
            BrowserPersonaPreset::ChromeStable => Self::chrome_stable(seed),
        }
    }

    pub fn chrome_stable(seed: PersonaSeed) -> Self {
        let viewport = Viewport::default();
        Self {
            seed: seed.clone(),
            preset: BrowserPersonaPreset::ChromeStable,
            startup_url: None,
            browser_name: "Chrome".to_string(),
            browser_version: "136.0.7103.49".to_string(),
            platform_name: "macOS".to_string(),
            viewport: viewport.clone(),
            screen: ScreenFingerprint::from_viewport(&viewport),
            window: WindowFingerprint::from_viewport(&viewport),
            css: CssFingerprint {
                moz_prefix_enabled: false,
                webkit_prefix_enabled: true,
            },
            network: NetworkFingerprint {
                user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".to_string(),
                accept_language: "en-US,en;q=0.9".to_string(),
                accept_header: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8".to_string(),
                sec_ch_ua: "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"".to_string(),
                sec_ch_ua_mobile: "?0".to_string(),
                sec_ch_ua_platform: "\"macOS\"".to_string(),
            },
            graphics: GraphicsFingerprint {
                webgl_vendor: "Intel Inc.".to_string(),
                webgl_renderer: "Intel(R) Iris(TM) Plus Graphics OpenGL Engine".to_string(),
                webgl_masked_vendor: "WebKit".to_string(),
                webgl_masked_renderer: "WebKit WebGL".to_string(),
                webgl_seed: PersonaSeed::from_stable_input(format!("{seed}:webgl")),
            },
            js: JsFingerprint {
                app_version: "5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36".to_string(),
                platform: "MacIntel".to_string(),
                language: "en-US".to_string(),
                languages: vec!["en-US".to_string(), "en".to_string()],
                hardware_concurrency: 8,
                device_memory_gb: 8,
                max_touch_points: 0,
                do_not_track: String::new(),
                expose_global_privacy_control: false,
                global_privacy_control: false,
                permissions_enabled: true,
                bluetooth_enabled: true,
                bluetooth_available: true,
                media_devices_enabled: true,
                webgpu_enabled: true,
                offscreen_canvas_enabled: true,
                service_worker_enabled: true,
                ua_platform_version: "15.7.0".to_string(),
                ua_architecture: "x86".to_string(),
                ua_bitness: "64".to_string(),
                ua_model: String::new(),
                timezone: "America/Los_Angeles".to_string(),
                vendor: "Google Inc.".to_string(),
                product_sub: "20030107".to_string(),
                pdf_viewer_enabled: true,
            },
            geo: GeoFingerprint {
                timezone: "America/Los_Angeles".to_string(),
                locale: "en-US".to_string(),
                latitude: None,
                longitude: None,
                accuracy_meters: None,
            },
            webrtc: WebRtcFingerprint {
                ipv4: None,
                ipv6: None,
            },
            canvas: CanvasFingerprint {
                seed: PersonaSeed::from_stable_input(format!("{seed}:canvas")),
                noise_enabled: true,
                noise_amplitude: 1,
                blink_low_entropy_probe: true,
            },
            domrect: DomRectFingerprint {
                seed: PersonaSeed::from_stable_input(format!("{seed}:domrect")),
                enabled: true,
                quantization_steps_per_px: 64,
                fill_empty_client_rects: true,
            },
            engine: EngineFingerprint {
                enabled: true,
                to_fixed_range_error_message: "toFixed() digits argument must be between 0-100"
                    .to_string(),
                array_constructor_source: "function Array() { [native code] }".to_string(),
            },
            audio: AudioFingerprint {
                seed: PersonaSeed::from_stable_input(format!("{seed}:audio")),
                sample_rate: 48_000,
                output_latency_ms: 20,
                max_channel_count: 2,
            },
            fonts: FontFingerprint {
                seed: PersonaSeed::from_stable_input(format!("{seed}:fonts")),
                families: vec![
                    "Arial".to_string(),
                    "Helvetica".to_string(),
                    "Times New Roman".to_string(),
                    "Courier New".to_string(),
                ],
            },
            media: MediaFingerprint {
                speech_voices: vec!["Samantha".to_string(), "Alex".to_string()],
                audio_inputs: 1,
                video_inputs: 1,
                audio_outputs: 1,
            },
            battery: BatteryFingerprint {
                charging: true,
                charging_time_seconds: 0,
                discharging_time_seconds: None,
                level_percent: 100,
            },
            storage: StorageFingerprint { quota_bytes: None },
            automation: AutomationPolicy::default(),
        }
    }
}