//! Camoufox configuration builder. //! //! Converts fingerprints to Camoufox configuration format and builds launch options. use rand::RngExt; use serde_yaml; use std::collections::HashMap; use std::path::Path; use crate::camoufox::data; use crate::camoufox::env_vars; use crate::camoufox::fingerprint::types::*; use crate::camoufox::fonts; use crate::camoufox::geolocation; use crate::camoufox::presets; use crate::camoufox::webgl; /// Browserforge mapping from YAML. type BrowserforgeMapping = HashMap; /// Load the browserforge mapping from embedded YAML. fn load_browserforge_mapping() -> BrowserforgeMapping { serde_yaml::from_str(data::BROWSERFORGE_YML).unwrap_or_default() } /// Convert a fingerprint to Camoufox configuration. pub fn from_browserforge( fingerprint: &Fingerprint, ff_version: Option, ) -> HashMap { let mapping = load_browserforge_mapping(); let mut config = HashMap::new(); // Convert fingerprint to a JSON value for easier traversal let fp_json = serde_json::to_value(fingerprint).unwrap_or_default(); // Apply mappings recursively cast_to_properties(&mut config, &mapping, &fp_json, ff_version); // Handle window.screenX and window.screenY handle_screen_xy(&mut config, &fingerprint.screen); config } /// Recursively cast fingerprint properties to Camoufox config format. fn cast_to_properties( config: &mut HashMap, mapping: &BrowserforgeMapping, fingerprint: &serde_json::Value, ff_version: Option, ) { if let serde_json::Value::Object(fp_obj) = fingerprint { for (key, mapping_value) in mapping { let fp_value = fp_obj.get(key); match mapping_value { serde_yaml::Value::String(target_key) => { if let Some(value) = fp_value { let mut final_value = value.clone(); // Handle negative screen values if target_key.starts_with("screen.") { if let Some(num) = final_value.as_i64() { if num < 0 { final_value = serde_json::json!(0); } } } // Replace Firefox version in user agent strings if let (Some(version), Some(s)) = (ff_version, final_value.as_str()) { let replaced = replace_ff_version(s, version); final_value = serde_json::json!(replaced); } config.insert(target_key.clone(), final_value); } } serde_yaml::Value::Mapping(nested_mapping) => { if let Some(nested_fp) = fp_value { let nested: BrowserforgeMapping = nested_mapping .iter() .filter_map(|(k, v)| k.as_str().map(|ks| (ks.to_string(), v.clone()))) .collect(); cast_to_properties(config, &nested, nested_fp, ff_version); } } _ => {} } } } } /// Replace Firefox version in user agent and related strings. fn replace_ff_version(s: &str, version: u32) -> String { // Match patterns like "135.0" (Firefox version) and replace with new version let re = regex_lite::Regex::new(r"(?, screen: &ScreenFingerprint) { if config.contains_key("window.screenY") { return; } let screen_x = screen.screen_x; if screen_x == 0 { config.insert("window.screenX".to_string(), serde_json::json!(0)); config.insert("window.screenY".to_string(), serde_json::json!(0)); return; } if (-50..=50).contains(&screen_x) { config.insert("window.screenY".to_string(), serde_json::json!(screen_x)); return; } let screen_y = screen.avail_height as i32 - screen.outer_height as i32; let mut rng = rand::rng(); let y = if screen_y == 0 { 0 } else if screen_y > 0 { rng.random_range(0..=screen_y) } else { rng.random_range(screen_y..=0) }; config.insert("window.screenY".to_string(), serde_json::json!(y)); } /// GeoIP option - can be an IP address string or auto-detect. #[derive(Debug, Clone)] pub enum GeoIPOption { /// Auto-detect IP (fetch public IP, optionally through proxy) Auto, /// Use a specific IP address IP(String), } /// Configuration builder for Camoufox launch. #[derive(Debug, Clone)] pub struct CamoufoxConfigBuilder { fingerprint: Option, operating_system: Option, screen_constraints: Option, block_images: bool, block_webrtc: bool, block_webgl: bool, custom_fonts: Option>, custom_fonts_only: bool, firefox_prefs: HashMap, proxy: Option, headless: bool, ff_version: Option, extra_config: HashMap, geoip: Option, } /// Proxy configuration. #[derive(Debug, Clone)] pub struct ProxyConfig { pub server: String, pub username: Option, pub password: Option, pub bypass: Option, } impl ProxyConfig { /// Parse a proxy URL string into ProxyConfig. /// Supports formats like: /// - "http://host:port" /// - "http://user:pass@host:port" /// - "socks5://user:pass@host:port" pub fn from_url(url: &str) -> Result { let parsed = url::Url::parse(url).map_err(|e| ConfigError::InvalidProxy(e.to_string()))?; let host = parsed .host_str() .ok_or_else(|| ConfigError::InvalidProxy("Missing host".to_string()))?; let port = parsed.port().unwrap_or(8080); let scheme = parsed.scheme(); let server = format!("{scheme}://{host}:{port}"); let username = if !parsed.username().is_empty() { Some(parsed.username().to_string()) } else { None }; let password = parsed.password().map(String::from); Ok(Self { server, username, password, bypass: None, }) } } impl Default for CamoufoxConfigBuilder { fn default() -> Self { Self::new() } } impl CamoufoxConfigBuilder { pub fn new() -> Self { Self { fingerprint: None, operating_system: None, screen_constraints: None, block_images: false, block_webrtc: false, block_webgl: false, custom_fonts: None, custom_fonts_only: false, firefox_prefs: HashMap::new(), proxy: None, headless: false, ff_version: None, extra_config: HashMap::new(), geoip: None, } } pub fn fingerprint(mut self, fp: Fingerprint) -> Self { self.fingerprint = Some(fp); self } pub fn operating_system(mut self, os: &str) -> Self { self.operating_system = Some(os.to_string()); self } pub fn screen_constraints(mut self, constraints: ScreenConstraints) -> Self { self.screen_constraints = Some(constraints); self } pub fn block_images(mut self, block: bool) -> Self { self.block_images = block; self } pub fn block_webrtc(mut self, block: bool) -> Self { self.block_webrtc = block; self } pub fn block_webgl(mut self, block: bool) -> Self { self.block_webgl = block; self } pub fn custom_fonts(mut self, fonts: Vec) -> Self { self.custom_fonts = Some(fonts); self } pub fn custom_fonts_only(mut self, only: bool) -> Self { self.custom_fonts_only = only; self } pub fn firefox_pref>(mut self, key: &str, value: V) -> Self { self.firefox_prefs.insert(key.to_string(), value.into()); self } pub fn proxy(mut self, proxy: ProxyConfig) -> Self { self.proxy = Some(proxy); self } pub fn headless(mut self, headless: bool) -> Self { self.headless = headless; self } pub fn ff_version(mut self, version: u32) -> Self { self.ff_version = Some(version); self } pub fn extra_config>(mut self, key: &str, value: V) -> Self { self.extra_config.insert(key.to_string(), value.into()); self } /// Set GeoIP option for geolocation-based fingerprinting. /// Use `GeoIPOption::Auto` to auto-detect public IP (optionally through proxy). /// Use `GeoIPOption::IP(ip_string)` to use a specific IP address. pub fn geoip(mut self, option: GeoIPOption) -> Self { self.geoip = Some(option); self } /// Build the complete Camoufox launch configuration. /// /// Prefers a real-fingerprint preset (matched against the Camoufox build's /// Firefox version via `presets::preset_line_for`) when no explicit /// fingerprint was passed. Falls back to the Bayesian network-based /// synthesizer when presets are unavailable, so callers without a known /// Firefox version (or with no preset for the requested OS) still get a /// valid config — matching pre-v150 behaviour byte-for-byte. pub fn build(self) -> Result { let mut rng = rand::rng(); let ff_version = self.ff_version; // 1) The caller supplied a fingerprint outright — honour it and skip // presets entirely. This is the path tests and advanced consumers // use to inject deterministic fixtures. // 2) Otherwise, try a bundled preset for the requested OS / FF line. // 3) Fall back to the Bayesian generator. This is also the path that // runs for users whose Camoufox binary has no readable `version.json` // (`ff_version == None`), or whose OS has no presets bundled. let (mut config, target_os) = if let Some(fp) = self.fingerprint { let target_os = env_vars::determine_ua_os(&fp.navigator.user_agent); // `from_browserforge` already runs `handle_screen_xy` internally. let config = from_browserforge(&fp, ff_version); (config, target_os) } else if let Some(preset) = presets::get_random_preset(self.operating_system.as_deref(), ff_version) { let mut config = presets::from_preset(&preset, ff_version); let target_os = config .get("navigator.userAgent") .and_then(|v| v.as_str()) .map(env_vars::determine_ua_os) .or_else(|| { // Last-resort heuristic from the platform string — keeps target_os // sensible even if a preset somehow omits the user agent. config .get("navigator.platform") .and_then(|v| v.as_str()) .map(|p| match p { "Win32" => "windows", "MacIntel" => "macos", _ => "linux", }) }) .unwrap_or("macos"); // Presets don't carry multi-monitor offsets, so default screenX/Y to // (0, 0) — matches what real single-display users send. config .entry("window.screenX".to_string()) .or_insert(serde_json::json!(0)); config .entry("window.screenY".to_string()) .or_insert(serde_json::json!(0)); (config, target_os) } else { let generator = crate::camoufox::fingerprint::FingerprintGenerator::new()?; let options = FingerprintOptions { operating_system: self.operating_system.clone(), browsers: Some(vec!["firefox".to_string()]), devices: Some(vec!["desktop".to_string()]), screen: self.screen_constraints, ..Default::default() }; let fingerprint = generator.get_fingerprint(&options)?.fingerprint; let target_os = env_vars::determine_ua_os(&fingerprint.navigator.user_agent); let config = from_browserforge(&fingerprint, ff_version); (config, target_os) }; // Add random window history length config.insert( "window.history.length".to_string(), serde_json::json!(rng.random_range(1..=5)), ); // Add fonts if !self.custom_fonts_only { let system_fonts = fonts::get_fonts_for_os(target_os); let fonts = if let Some(custom) = &self.custom_fonts { let mut all_fonts = system_fonts; for font in custom { if !all_fonts.contains(font) { all_fonts.push(font.clone()); } } all_fonts } else { system_fonts }; config.insert("fonts".to_string(), serde_json::json!(fonts)); } else if let Some(custom) = &self.custom_fonts { config.insert("fonts".to_string(), serde_json::json!(custom)); } // Add font spacing seed config.insert( "fonts:spacing_seed".to_string(), serde_json::json!(rng.random_range(0..1_073_741_824u32)), ); // Build Firefox preferences let mut firefox_prefs = self.firefox_prefs; if self.block_images { firefox_prefs.insert( "permissions.default.image".to_string(), serde_json::json!(2), ); } if self.block_webrtc { firefox_prefs.insert( "media.peerconnection.enabled".to_string(), serde_json::json!(false), ); } if self.block_webgl { firefox_prefs.insert("webgl.disabled".to_string(), serde_json::json!(true)); } else { // Sample and add WebGL configuration match webgl::sample_webgl(target_os, None, None) { Ok(webgl_data) => { for (key, value) in webgl_data.config { config.insert(key, value); } firefox_prefs.insert("webgl.force-enabled".to_string(), serde_json::json!(true)); } Err(e) => { log::warn!("Failed to sample WebGL config: {}", e); } } } // Canvas anti-fingerprinting config.insert( "canvas:aaOffset".to_string(), serde_json::json!(rng.random_range(-50..=50)), ); config.insert("canvas:aaCapOffset".to_string(), serde_json::json!(true)); // Add extra config (user-provided) for (key, value) in self.extra_config { config.insert(key, value); } // Hardcoded Camoufox settings (cannot be overridden) // Disable theming to prevent fingerprinting via browser theme config.insert("disableTheming".to_string(), serde_json::json!(true)); // Hide cursor in headless mode config.insert("showcursor".to_string(), serde_json::json!(false)); Ok(CamoufoxLaunchConfig { fingerprint_config: config, firefox_prefs, proxy: self.proxy, headless: self.headless, target_os: target_os.to_string(), }) } /// Build the complete Camoufox launch configuration with async geolocation support. /// This method should be used when geoip option is set to Auto. pub async fn build_async(self) -> Result { // Get full proxy URL (with credentials) for IP detection let proxy_url = self.proxy.as_ref().map(|p| { if let (Some(user), Some(pass)) = (&p.username, &p.password) { // Reconstruct URL with credentials: scheme://user:pass@host:port if let Ok(mut parsed) = url::Url::parse(&p.server) { let _ = parsed.set_username(user); let _ = parsed.set_password(Some(pass)); parsed.to_string() } else { p.server.clone() } } else if let Some(user) = &p.username { if let Ok(mut parsed) = url::Url::parse(&p.server) { let _ = parsed.set_username(user); parsed.to_string() } else { p.server.clone() } } else { p.server.clone() } }); let geoip_option = self.geoip.clone(); let block_webrtc = self.block_webrtc; // Build base config first let mut launch_config = self.build()?; // Handle geolocation if geoip option is set if let Some(geoip) = geoip_option { let ip = match geoip { GeoIPOption::Auto => { // Fetch public IP, optionally through proxy geolocation::fetch_public_ip(proxy_url.as_deref()) .await .map_err(geolocation::GeolocationError::from)? } GeoIPOption::IP(ip_str) => { if !geolocation::validate_ip(&ip_str) { return Err(ConfigError::Geolocation( geolocation::GeolocationError::InvalidIP(ip_str), )); } ip_str } }; // Get geolocation from IP match geolocation::get_geolocation(&ip) { Ok(geo) => { // Add geolocation config for (key, value) in geo.as_config() { launch_config.fingerprint_config.insert(key, value); } // Add WebRTC IP spoofing if not blocked if !block_webrtc { if geolocation::is_ipv4(&ip) { launch_config .fingerprint_config .insert("webrtc:ipv4".to_string(), serde_json::json!(ip)); } else if geolocation::is_ipv6(&ip) { launch_config .fingerprint_config .insert("webrtc:ipv6".to_string(), serde_json::json!(ip)); } } log::info!( "Applied geolocation from IP {}: {} ({})", ip, geo.locale.as_string(), geo.timezone ); } Err(e) => { log::warn!("Failed to get geolocation for IP {}: {}", ip, e); // Continue without geolocation rather than failing } } } Ok(launch_config) } } /// Complete Camoufox launch configuration. #[derive(Debug, Clone)] pub struct CamoufoxLaunchConfig { pub fingerprint_config: HashMap, pub firefox_prefs: HashMap, pub proxy: Option, pub headless: bool, pub target_os: String, } impl CamoufoxLaunchConfig { /// Get environment variables for launching Camoufox. pub fn get_env_vars(&self) -> Result, serde_json::Error> { env_vars::config_to_env_vars(&self.fingerprint_config) } /// Get the config as JSON string. pub fn config_json(&self) -> Result { serde_json::to_string(&self.fingerprint_config) } } /// Error type for configuration operations. #[derive(Debug, thiserror::Error)] pub enum ConfigError { #[error("Fingerprint generation error: {0}")] Fingerprint(#[from] crate::camoufox::fingerprint::FingerprintError), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), #[error("WebGL error: {0}")] WebGL(#[from] webgl::WebGLError), #[error("Invalid proxy configuration: {0}")] InvalidProxy(String), #[error("Geolocation error: {0}")] Geolocation(#[from] crate::camoufox::geolocation::GeolocationError), } /// Get Firefox version from executable path. pub fn get_firefox_version(executable_path: &Path) -> Option { // Try to read version.json from the same directory let version_path = executable_path.parent()?.join("version.json"); if let Ok(content) = std::fs::read_to_string(&version_path) { if let Ok(json) = serde_json::from_str::(&content) { if let Some(version_str) = json.get("version").and_then(|v| v.as_str()) { // Parse major version from "135.0" or similar let major: u32 = version_str.split('.').next()?.parse().ok()?; return Some(major); } } } None } #[cfg(test)] mod tests { use super::*; #[test] fn test_config_builder() { let config = CamoufoxConfigBuilder::new() .operating_system("windows") .block_images(true) .build(); assert!(config.is_ok()); let config = config.unwrap(); assert!(config .firefox_prefs .contains_key("permissions.default.image")); } #[test] fn test_replace_ff_version() { let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0"; let replaced = replace_ff_version(ua, 140); assert!(replaced.contains("140.0")); } #[test] fn test_from_browserforge() { let fingerprint = Fingerprint { screen: ScreenFingerprint { width: 1920, height: 1080, avail_width: 1920, avail_height: 1040, color_depth: 24, pixel_depth: 24, inner_width: 1903, inner_height: 969, outer_width: 1920, outer_height: 1040, ..Default::default() }, navigator: NavigatorFingerprint { user_agent: "Mozilla/5.0 Firefox/135.0".to_string(), platform: "Win32".to_string(), language: "en-US".to_string(), languages: vec!["en-US".to_string()], hardware_concurrency: 8, ..Default::default() }, ..Default::default() }; let config = from_browserforge(&fingerprint, Some(140)); assert!(config.contains_key("navigator.userAgent")); assert!(config.contains_key("screen.width")); } }