mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-12 23:56:34 +02:00
refactor: don't bundle node backend
This commit is contained in:
@@ -98,7 +98,7 @@ impl BrowserRunner {
|
||||
);
|
||||
}
|
||||
|
||||
// Handle camoufox profiles using nodecar launcher
|
||||
// Handle Camoufox profiles using CamoufoxManager
|
||||
if profile.browser == "camoufox" {
|
||||
// Get or create camoufox config
|
||||
let mut camoufox_config = profile.camoufox_config.clone().unwrap_or_else(|| {
|
||||
@@ -205,14 +205,11 @@ impl BrowserRunner {
|
||||
);
|
||||
}
|
||||
|
||||
// Use the nodecar camoufox launcher
|
||||
log::info!(
|
||||
"Launching Camoufox via nodecar for profile: {}",
|
||||
profile.name
|
||||
);
|
||||
// Launch Camoufox browser
|
||||
log::info!("Launching Camoufox for profile: {}", profile.name);
|
||||
let camoufox_result = self
|
||||
.camoufox_manager
|
||||
.launch_camoufox_profile_nodecar(
|
||||
.launch_camoufox_profile(
|
||||
app_handle.clone(),
|
||||
updated_profile.clone(),
|
||||
camoufox_config,
|
||||
@@ -220,7 +217,7 @@ impl BrowserRunner {
|
||||
)
|
||||
.await
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to launch camoufox via nodecar: {e}").into()
|
||||
format!("Failed to launch Camoufox: {e}").into()
|
||||
})?;
|
||||
|
||||
// For server-based Camoufox, we use the process_id
|
||||
@@ -547,7 +544,7 @@ impl BrowserRunner {
|
||||
url: &str,
|
||||
_internal_proxy_settings: Option<&ProxySettings>,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Handle camoufox profiles using nodecar launcher
|
||||
// Handle Camoufox profiles using CamoufoxManager
|
||||
if profile.browser == "camoufox" {
|
||||
// Get the profile path based on the UUID
|
||||
let profiles_dir = self.profile_manager.get_profiles_dir();
|
||||
@@ -657,7 +654,7 @@ impl BrowserRunner {
|
||||
return Err("Unsupported platform".into());
|
||||
}
|
||||
BrowserType::Camoufox => {
|
||||
// Camoufox uses nodecar for launching, URL opening is handled differently
|
||||
// Camoufox URL opening is handled differently
|
||||
Err("URL opening in existing Camoufox instance is not supported".into())
|
||||
}
|
||||
BrowserType::Chromium | BrowserType::Brave => {
|
||||
@@ -941,7 +938,7 @@ impl BrowserRunner {
|
||||
app_handle: tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Handle camoufox profiles using nodecar launcher
|
||||
// Handle Camoufox profiles using CamoufoxManager
|
||||
if profile.browser == "camoufox" {
|
||||
// Search by profile path to find the running Camoufox instance
|
||||
let profiles_dir = self.profile_manager.get_profiles_dir();
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
//! Camoufox configuration builder.
|
||||
//!
|
||||
//! Converts fingerprints to Camoufox configuration format and builds launch options.
|
||||
|
||||
use rand::Rng;
|
||||
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::webgl;
|
||||
|
||||
/// Browserforge mapping from YAML.
|
||||
type BrowserforgeMapping = HashMap<String, serde_yaml::Value>;
|
||||
|
||||
/// 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<u32>,
|
||||
) -> HashMap<String, serde_json::Value> {
|
||||
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<String, serde_json::Value>,
|
||||
mapping: &BrowserforgeMapping,
|
||||
fingerprint: &serde_json::Value,
|
||||
ff_version: Option<u32>,
|
||||
) {
|
||||
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"(?<!\d)(1[0-9]{2})(\.0)(?!\d)").unwrap_or_else(|_| {
|
||||
// Fallback - just do simple replacement
|
||||
regex_lite::Regex::new(r"Firefox/\d+").unwrap()
|
||||
});
|
||||
|
||||
re.replace_all(s, format!("{}.0", version).as_str())
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Handle window.screenX and window.screenY generation.
|
||||
fn handle_screen_xy(config: &mut HashMap<String, serde_json::Value>, 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<Fingerprint>,
|
||||
operating_system: Option<String>,
|
||||
screen_constraints: Option<ScreenConstraints>,
|
||||
block_images: bool,
|
||||
block_webrtc: bool,
|
||||
block_webgl: bool,
|
||||
custom_fonts: Option<Vec<String>>,
|
||||
custom_fonts_only: bool,
|
||||
firefox_prefs: HashMap<String, serde_json::Value>,
|
||||
proxy: Option<ProxyConfig>,
|
||||
headless: bool,
|
||||
ff_version: Option<u32>,
|
||||
extra_config: HashMap<String, serde_json::Value>,
|
||||
geoip: Option<GeoIPOption>,
|
||||
}
|
||||
|
||||
/// Proxy configuration.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyConfig {
|
||||
pub server: String,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub bypass: Option<String>,
|
||||
}
|
||||
|
||||
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<Self, ConfigError> {
|
||||
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<String>) -> 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<V: Into<serde_json::Value>>(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<V: Into<serde_json::Value>>(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.
|
||||
pub fn build(self) -> Result<CamoufoxLaunchConfig, ConfigError> {
|
||||
// Generate or use provided fingerprint
|
||||
let fingerprint = if let Some(fp) = self.fingerprint {
|
||||
fp
|
||||
} 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()
|
||||
};
|
||||
generator.get_fingerprint(&options)?.fingerprint
|
||||
};
|
||||
|
||||
// Determine target OS from user agent
|
||||
let target_os = env_vars::determine_ua_os(&fingerprint.navigator.user_agent);
|
||||
|
||||
// Convert fingerprint to config
|
||||
let mut config = from_browserforge(&fingerprint, self.ff_version);
|
||||
|
||||
// Add random window history length
|
||||
let mut rng = rand::rng();
|
||||
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<CamoufoxLaunchConfig, ConfigError> {
|
||||
// Get proxy URL for IP detection if set
|
||||
let proxy_url = self.proxy.as_ref().map(|p| 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?
|
||||
}
|
||||
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<String, serde_json::Value>,
|
||||
pub firefox_prefs: HashMap<String, serde_json::Value>,
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
pub headless: bool,
|
||||
pub target_os: String,
|
||||
}
|
||||
|
||||
impl CamoufoxLaunchConfig {
|
||||
/// Get environment variables for launching Camoufox.
|
||||
pub fn get_env_vars(&self) -> Result<HashMap<String, String>, serde_json::Error> {
|
||||
env_vars::config_to_env_vars(&self.fingerprint_config)
|
||||
}
|
||||
|
||||
/// Get the config as JSON string.
|
||||
pub fn config_json(&self) -> Result<String, serde_json::Error> {
|
||||
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<u32> {
|
||||
// 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::<serde_json::Value>(&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"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
[
|
||||
"chrome/143.0.0.0|2",
|
||||
"safari/18.3.1|2",
|
||||
"chrome/101.0.4951.54|2",
|
||||
"chrome/139.0.0.0|2",
|
||||
"safari/16.6|2",
|
||||
"safari/26.2|2",
|
||||
"safari/18.6|2",
|
||||
"safari/26.1|2",
|
||||
"chrome/142.0.0.0|2",
|
||||
"chrome/141.0.0.0|2",
|
||||
"safari/18.7.3|2",
|
||||
"edge/143.0.0.0|2",
|
||||
"safari/18.4|2",
|
||||
"safari/17.3.1|2",
|
||||
"chrome/135.0.0.0|2",
|
||||
"safari/18.5|2",
|
||||
"safari/18.7.2|2",
|
||||
"chrome/143.0.0.0|1",
|
||||
"chrome/128.0.0.0|2",
|
||||
"chrome/131.0.0.0|2",
|
||||
"safari/26.3|2",
|
||||
"safari/26.0.1|2",
|
||||
"chrome/114.0.0.0|2",
|
||||
"safari/18.1|2",
|
||||
"firefox/147.0|2",
|
||||
"safari/17.5|2",
|
||||
"chrome/140.0.0.0|2",
|
||||
"safari/16.6.1|2",
|
||||
"firefox/146.0|2",
|
||||
"chrome/124.0.0.0|1",
|
||||
"chrome/34.0.1847.114|2",
|
||||
"chrome/130.0.0.0|2",
|
||||
"safari/15.6.7|2",
|
||||
"chrome/144.0.0.0|2",
|
||||
"safari/18.3|2",
|
||||
"safari/16.4|2",
|
||||
"chrome/141.0.7390.122|1",
|
||||
"firefox/140.0|2",
|
||||
"chrome/138.0.0.0|2",
|
||||
"firefox/135.0|2",
|
||||
"safari/17.6|2",
|
||||
"chrome/132.0.0.0|2",
|
||||
"chrome/109.0.0.0|2",
|
||||
"chrome/92.0.4515.131|2",
|
||||
"chrome/136.0.0.0|2",
|
||||
"edge/142.0.0.0|2",
|
||||
"chrome/125.0.0.0|2",
|
||||
"safari/17.8|2",
|
||||
"edge/143.0.0.0|1",
|
||||
"chrome/123.0.0.0|2",
|
||||
"chrome/137.0.0.0|2",
|
||||
"chrome/129.0.0.0|2",
|
||||
"chrome/126.0.0.0|2",
|
||||
"safari/26.0|2",
|
||||
"chrome/133.0.0.0|2",
|
||||
"chrome/119.0.0.0|2",
|
||||
"chrome/145.0.0.0|2",
|
||||
"firefox/145.0|2",
|
||||
"safari/17.3|2",
|
||||
"safari/18.2|2",
|
||||
"safari/16.5.2|2",
|
||||
"safari/17.4|2",
|
||||
"chrome/120.0.0.0|2",
|
||||
"chrome/116.0.0.0|2",
|
||||
"firefox/141.0|2",
|
||||
"safari/17.4.1|2",
|
||||
"chrome/134.0.0.0|2",
|
||||
"safari/15.4|2",
|
||||
"safari/18.1.1|2",
|
||||
"edge/144.0.0.0|2",
|
||||
"firefox/144.0|2",
|
||||
"safari/16.3|2",
|
||||
"safari/13.0.3|2",
|
||||
"chrome/131.0.6778.33|2",
|
||||
"edge/145.0.0.0|2",
|
||||
"edge/139.0.0.0|2",
|
||||
"safari/17.1|2",
|
||||
"chrome/133.0.0.0|1",
|
||||
"chrome/121.0.0.0|2",
|
||||
"chrome/124.0.0.0|2",
|
||||
"chrome/127.0.0.0|2",
|
||||
"chrome/122.0.6261.95|2",
|
||||
"chrome/91.0.4450.0|2",
|
||||
"edge/134.0.0.0|2",
|
||||
"chrome/134.0.6998.179|2",
|
||||
"chrome/122.0.0.0|2",
|
||||
"firefox/128.0|2",
|
||||
"chrome/142.0.0.0|1",
|
||||
"safari/18.7|2",
|
||||
"safari/17.8.1|2",
|
||||
"firefox/115.0|2",
|
||||
"safari/17.2|2",
|
||||
"chrome/117.0.0.0|2",
|
||||
"safari/18.0.1|2",
|
||||
"chrome/139.0.7258.5|2",
|
||||
"edge/140.0.0.0|2",
|
||||
"safari/16.5|2",
|
||||
"safari/18.6.2|2",
|
||||
"firefox/136.0|2",
|
||||
"safari/17.2.1|2",
|
||||
"safari/18.0|2",
|
||||
"safari/15.6.1|2",
|
||||
"safari/26.2|1",
|
||||
"safari/17.1.2|2",
|
||||
"safari/17.7|2",
|
||||
"safari/16.2|2",
|
||||
"edge/122.0.0.0|2",
|
||||
"chrome/139.0.0.0|1",
|
||||
"safari/17.0|2",
|
||||
"firefox/139.0|2",
|
||||
"chrome/101.0.9316.173|2",
|
||||
"chrome/101.0.4951.64|2",
|
||||
"chrome/141.0.0.0|1",
|
||||
"safari/15.5|2",
|
||||
"safari/18.6|1",
|
||||
"chrome/112.0.0.0|2",
|
||||
"edge/135.0.0.0|2",
|
||||
"chrome/140.0.0.0|1"
|
||||
]
|
||||
@@ -0,0 +1,68 @@
|
||||
# Mappings of Browserforge fingerprints to Camoufox config properties.
|
||||
|
||||
navigator:
|
||||
# Note: Browserforge tends to have outdated UAs.
|
||||
# The version will be replaced in Camoufox.
|
||||
userAgent: navigator.userAgent
|
||||
# userAgentData not in Firefox
|
||||
doNotTrack: navigator.doNotTrack
|
||||
appCodeName: navigator.appCodeName
|
||||
appName: navigator.appName
|
||||
appVersion: navigator.appVersion
|
||||
oscpu: navigator.oscpu
|
||||
# webdriver is always True
|
||||
# Locale is now implemented separately:
|
||||
# language: navigator.language
|
||||
# languages: navigator.languages
|
||||
platform: navigator.platform
|
||||
# deviceMemory not in Firefox
|
||||
hardwareConcurrency: navigator.hardwareConcurrency
|
||||
product: navigator.product
|
||||
# Never override productSub #105
|
||||
# productSub: navigator.productSub
|
||||
# vendor is not necessary
|
||||
# vendorSub is not necessary
|
||||
maxTouchPoints: navigator.maxTouchPoints
|
||||
extraProperties:
|
||||
# Note: Changing pdfViewerEnabled is not recommended. This will be kept to True.
|
||||
globalPrivacyControl: navigator.globalPrivacyControl
|
||||
|
||||
screen:
|
||||
# hasHDR is not implemented in Camoufox
|
||||
availLeft: screen.availLeft
|
||||
availTop: screen.availTop
|
||||
availWidth: screen.availWidth
|
||||
availHeight: screen.availHeight
|
||||
height: screen.height
|
||||
width: screen.width
|
||||
colorDepth: screen.colorDepth
|
||||
pixelDepth: screen.pixelDepth
|
||||
# devicePixelRatio is not recommended. Any value other than 1.0 is suspicious.
|
||||
pageXOffset: screen.pageXOffset
|
||||
pageYOffset: screen.pageYOffset
|
||||
outerHeight: window.outerHeight
|
||||
outerWidth: window.outerWidth
|
||||
innerHeight: window.innerHeight
|
||||
innerWidth: window.innerWidth
|
||||
screenX: window.screenX
|
||||
screenY: window.screenY
|
||||
# Tends to generate out of bounds (network inconsistencies):
|
||||
# clientWidth: document.body.clientWidth
|
||||
# clientHeight: document.body.clientHeight
|
||||
|
||||
# videoCard:
|
||||
# renderer: webgl:renderer
|
||||
# vendor: webgl:vendor
|
||||
|
||||
headers:
|
||||
# headers.User-Agent is redundant with navigator.userAgent
|
||||
# headers.Accept-Language is redundant with locale:*
|
||||
Accept-Encoding: headers.Accept-Encoding
|
||||
|
||||
battery:
|
||||
charging: battery:charging
|
||||
chargingTime: battery:chargingTime
|
||||
dischargingTime: battery:dischargingTime
|
||||
|
||||
# Unsupported: videoCodecs, audioCodecs, pluginsData, multimediaDevices
|
||||
# Fonts are listed through the launcher.
|
||||
Binary file not shown.
@@ -0,0 +1,822 @@
|
||||
{
|
||||
"win": [
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Bahnschrift",
|
||||
"Calibri",
|
||||
"Calibri Light",
|
||||
"Cambria",
|
||||
"Cambria Math",
|
||||
"Candara",
|
||||
"Candara Light",
|
||||
"Comic Sans MS",
|
||||
"Consolas",
|
||||
"Constantia",
|
||||
"Corbel",
|
||||
"Corbel Light",
|
||||
"Courier New",
|
||||
"Ebrima",
|
||||
"Franklin Gothic Medium",
|
||||
"Gabriola",
|
||||
"Gadugi",
|
||||
"Georgia",
|
||||
"HoloLens MDL2 Assets",
|
||||
"Impact",
|
||||
"Ink Free",
|
||||
"Javanese Text",
|
||||
"Leelawadee UI",
|
||||
"Leelawadee UI Semilight",
|
||||
"Lucida Console",
|
||||
"Lucida Sans Unicode",
|
||||
"MS Gothic",
|
||||
"MS PGothic",
|
||||
"MS UI Gothic",
|
||||
"MV Boli",
|
||||
"Malgun Gothic",
|
||||
"Malgun Gothic Semilight",
|
||||
"Marlett",
|
||||
"Microsoft Himalaya",
|
||||
"Microsoft JhengHei",
|
||||
"Microsoft JhengHei Light",
|
||||
"Microsoft JhengHei UI",
|
||||
"Microsoft JhengHei UI Light",
|
||||
"Microsoft New Tai Lue",
|
||||
"Microsoft PhagsPa",
|
||||
"Microsoft Sans Serif",
|
||||
"Microsoft Tai Le",
|
||||
"Microsoft YaHei",
|
||||
"Microsoft YaHei Light",
|
||||
"Microsoft YaHei UI",
|
||||
"Microsoft YaHei UI Light",
|
||||
"Microsoft Yi Baiti",
|
||||
"MingLiU-ExtB",
|
||||
"MingLiU_HKSCS-ExtB",
|
||||
"Mongolian Baiti",
|
||||
"Myanmar Text",
|
||||
"NSimSun",
|
||||
"Nirmala UI",
|
||||
"Nirmala UI Semilight",
|
||||
"PMingLiU-ExtB",
|
||||
"Palatino Linotype",
|
||||
"Segoe Fluent Icons",
|
||||
"Segoe MDL2 Assets",
|
||||
"Segoe Print",
|
||||
"Segoe Script",
|
||||
"Segoe UI",
|
||||
"Segoe UI Black",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Historic",
|
||||
"Segoe UI Light",
|
||||
"Segoe UI Semibold",
|
||||
"Segoe UI Semilight",
|
||||
"Segoe UI Symbol",
|
||||
"Segoe UI Variable",
|
||||
"SimSun",
|
||||
"SimSun-ExtB",
|
||||
"Sitka",
|
||||
"Sitka Text",
|
||||
"Sylfaen",
|
||||
"Symbol",
|
||||
"Tahoma",
|
||||
"Times New Roman",
|
||||
"Trebuchet MS",
|
||||
"Twemoji Mozilla",
|
||||
"Verdana",
|
||||
"Webdings",
|
||||
"Wingdings",
|
||||
"Yu Gothic",
|
||||
"Yu Gothic Light",
|
||||
"Yu Gothic Medium",
|
||||
"Yu Gothic UI",
|
||||
"Yu Gothic UI Light",
|
||||
"Yu Gothic UI Semibold",
|
||||
"Yu Gothic UI Semilight",
|
||||
"\u5b8b\u4f53",
|
||||
"\u5fae\u8edf\u6b63\u9ed1\u9ad4",
|
||||
"\u5fae\u8edf\u6b63\u9ed1\u9ad4 Light",
|
||||
"\u5fae\u8f6f\u96c5\u9ed1",
|
||||
"\u5fae\u8f6f\u96c5\u9ed1 Light",
|
||||
"\u65b0\u5b8b\u4f53",
|
||||
"\u65b0\u7d30\u660e\u9ad4-ExtB",
|
||||
"\u6e38\u30b4\u30b7\u30c3\u30af",
|
||||
"\u6e38\u30b4\u30b7\u30c3\u30af Light",
|
||||
"\u6e38\u30b4\u30b7\u30c3\u30af Medium",
|
||||
"\u7d30\u660e\u9ad4-ExtB",
|
||||
"\u7d30\u660e\u9ad4_HKSCS-ExtB",
|
||||
"\ub9d1\uc740 \uace0\ub515",
|
||||
"\ub9d1\uc740 \uace0\ub515 Semilight",
|
||||
"\uff2d\uff33 \u30b4\u30b7\u30c3\u30af",
|
||||
"\uff2d\uff33 \uff30\u30b4\u30b7\u30c3\u30af"
|
||||
],
|
||||
"mac": [
|
||||
".Al Bayan PUA",
|
||||
".Al Nile PUA",
|
||||
".Al Tarikh PUA",
|
||||
".Apple Color Emoji UI",
|
||||
".Apple SD Gothic NeoI",
|
||||
".Aqua Kana",
|
||||
".Aqua Kana Bold",
|
||||
".Aqua \u304b\u306a",
|
||||
".Aqua \u304b\u306a \u30dc\u30fc\u30eb\u30c9",
|
||||
".Arial Hebrew Desk Interface",
|
||||
".Baghdad PUA",
|
||||
".Beirut PUA",
|
||||
".Damascus PUA",
|
||||
".DecoType Naskh PUA",
|
||||
".Diwan Kufi PUA",
|
||||
".Farah PUA",
|
||||
".Geeza Pro Interface",
|
||||
".Geeza Pro PUA",
|
||||
".Helvetica LT MM",
|
||||
".Hiragino Kaku Gothic Interface",
|
||||
".Hiragino Sans GB Interface",
|
||||
".Keyboard",
|
||||
".KufiStandardGK PUA",
|
||||
".LastResort",
|
||||
".Lucida Grande UI",
|
||||
".Muna PUA",
|
||||
".Nadeem PUA",
|
||||
".New York",
|
||||
".Noto Nastaliq Urdu UI",
|
||||
".PingFang HK",
|
||||
".PingFang SC",
|
||||
".PingFang TC",
|
||||
".SF Arabic",
|
||||
".SF Arabic Rounded",
|
||||
".SF Compact",
|
||||
".SF Compact Rounded",
|
||||
".SF NS",
|
||||
".SF NS Mono",
|
||||
".SF NS Rounded",
|
||||
".Sana PUA",
|
||||
".Savoye LET CC.",
|
||||
".ThonburiUI",
|
||||
".ThonburiUIWatch",
|
||||
".\u82f9\u65b9-\u6e2f",
|
||||
".\u82f9\u65b9-\u7b80",
|
||||
".\u82f9\u65b9-\u7e41",
|
||||
".\u860b\u65b9-\u6e2f",
|
||||
".\u860b\u65b9-\u7c21",
|
||||
".\u860b\u65b9-\u7e41",
|
||||
"Academy Engraved LET",
|
||||
"Al Bayan",
|
||||
"Al Nile",
|
||||
"Al Tarikh",
|
||||
"American Typewriter",
|
||||
"Andale Mono",
|
||||
"Apple Braille",
|
||||
"Apple Chancery",
|
||||
"Apple Color Emoji",
|
||||
"Apple SD Gothic Neo",
|
||||
"Apple SD \uc0b0\ub3cc\uace0\ub515 Neo",
|
||||
"Apple Symbols",
|
||||
"AppleGothic",
|
||||
"AppleMyungjo",
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Arial Hebrew",
|
||||
"Arial Hebrew Scholar",
|
||||
"Arial Narrow",
|
||||
"Arial Rounded MT Bold",
|
||||
"Arial Unicode MS",
|
||||
"Athelas",
|
||||
"Avenir",
|
||||
"Avenir Black",
|
||||
"Avenir Black Oblique",
|
||||
"Avenir Book",
|
||||
"Avenir Heavy",
|
||||
"Avenir Light",
|
||||
"Avenir Medium",
|
||||
"Avenir Next",
|
||||
"Avenir Next Condensed",
|
||||
"Avenir Next Condensed Demi Bold",
|
||||
"Avenir Next Condensed Heavy",
|
||||
"Avenir Next Condensed Medium",
|
||||
"Avenir Next Condensed Ultra Light",
|
||||
"Avenir Next Demi Bold",
|
||||
"Avenir Next Heavy",
|
||||
"Avenir Next Medium",
|
||||
"Avenir Next Ultra Light",
|
||||
"Ayuthaya",
|
||||
"Baghdad",
|
||||
"Bangla MN",
|
||||
"Bangla Sangam MN",
|
||||
"Baskerville",
|
||||
"Beirut",
|
||||
"Big Caslon",
|
||||
"Bodoni 72",
|
||||
"Bodoni 72 Oldstyle",
|
||||
"Bodoni 72 Smallcaps",
|
||||
"Bodoni Ornaments",
|
||||
"Bradley Hand",
|
||||
"Brush Script MT",
|
||||
"Chalkboard",
|
||||
"Chalkboard SE",
|
||||
"Chalkduster",
|
||||
"Charter",
|
||||
"Charter Black",
|
||||
"Cochin",
|
||||
"Comic Sans MS",
|
||||
"Copperplate",
|
||||
"Corsiva Hebrew",
|
||||
"Courier",
|
||||
"Courier New",
|
||||
"Czcionka systemowa",
|
||||
"DIN Alternate",
|
||||
"DIN Condensed",
|
||||
"Damascus",
|
||||
"DecoType Naskh",
|
||||
"Devanagari MT",
|
||||
"Devanagari Sangam MN",
|
||||
"Didot",
|
||||
"Diwan Kufi",
|
||||
"Diwan Thuluth",
|
||||
"Euphemia UCAS",
|
||||
"Farah",
|
||||
"Farisi",
|
||||
"Font Sistem",
|
||||
"Font de sistem",
|
||||
"Font di sistema",
|
||||
"Font sustava",
|
||||
"Fonte do Sistema",
|
||||
"Futura",
|
||||
"GB18030 Bitmap",
|
||||
"Galvji",
|
||||
"Geeza Pro",
|
||||
"Geneva",
|
||||
"Georgia",
|
||||
"Gill Sans",
|
||||
"Grantha Sangam MN",
|
||||
"Gujarati MT",
|
||||
"Gujarati Sangam MN",
|
||||
"Gurmukhi MN",
|
||||
"Gurmukhi MT",
|
||||
"Gurmukhi Sangam MN",
|
||||
"Heiti SC",
|
||||
"Heiti TC",
|
||||
"Heiti-\uac04\uccb4",
|
||||
"Heiti-\ubc88\uccb4",
|
||||
"Helvetica",
|
||||
"Helvetica Neue",
|
||||
"Herculanum",
|
||||
"Hiragino Kaku Gothic Pro",
|
||||
"Hiragino Kaku Gothic Pro W3",
|
||||
"Hiragino Kaku Gothic Pro W6",
|
||||
"Hiragino Kaku Gothic ProN",
|
||||
"Hiragino Kaku Gothic ProN W3",
|
||||
"Hiragino Kaku Gothic ProN W6",
|
||||
"Hiragino Kaku Gothic Std",
|
||||
"Hiragino Kaku Gothic Std W8",
|
||||
"Hiragino Kaku Gothic StdN",
|
||||
"Hiragino Kaku Gothic StdN W8",
|
||||
"Hiragino Maru Gothic Pro",
|
||||
"Hiragino Maru Gothic Pro W4",
|
||||
"Hiragino Maru Gothic ProN",
|
||||
"Hiragino Maru Gothic ProN W4",
|
||||
"Hiragino Mincho Pro",
|
||||
"Hiragino Mincho Pro W3",
|
||||
"Hiragino Mincho Pro W6",
|
||||
"Hiragino Mincho ProN",
|
||||
"Hiragino Mincho ProN W3",
|
||||
"Hiragino Mincho ProN W6",
|
||||
"Hiragino Sans",
|
||||
"Hiragino Sans GB",
|
||||
"Hiragino Sans GB W3",
|
||||
"Hiragino Sans GB W6",
|
||||
"Hiragino Sans W0",
|
||||
"Hiragino Sans W1",
|
||||
"Hiragino Sans W2",
|
||||
"Hiragino Sans W3",
|
||||
"Hiragino Sans W4",
|
||||
"Hiragino Sans W5",
|
||||
"Hiragino Sans W6",
|
||||
"Hiragino Sans W7",
|
||||
"Hiragino Sans W8",
|
||||
"Hiragino Sans W9",
|
||||
"Hoefler Text",
|
||||
"Hoefler Text Ornaments",
|
||||
"ITF Devanagari",
|
||||
"ITF Devanagari Marathi",
|
||||
"Impact",
|
||||
"InaiMathi",
|
||||
"Iowan Old Style",
|
||||
"Iowan Old Style Black",
|
||||
"J\u00e4rjestelm\u00e4fontti",
|
||||
"Kailasa",
|
||||
"Kannada MN",
|
||||
"Kannada Sangam MN",
|
||||
"Kefa",
|
||||
"Khmer MN",
|
||||
"Khmer Sangam MN",
|
||||
"Kohinoor Bangla",
|
||||
"Kohinoor Devanagari",
|
||||
"Kohinoor Gujarati",
|
||||
"Kohinoor Telugu",
|
||||
"Kokonor",
|
||||
"Krungthep",
|
||||
"KufiStandardGK",
|
||||
"Lao MN",
|
||||
"Lao Sangam MN",
|
||||
"Lucida Grande",
|
||||
"Luminari",
|
||||
"Malayalam MN",
|
||||
"Malayalam Sangam MN",
|
||||
"Marion",
|
||||
"Marker Felt",
|
||||
"Menlo",
|
||||
"Microsoft Sans Serif",
|
||||
"Mishafi",
|
||||
"Mishafi Gold",
|
||||
"Monaco",
|
||||
"Mshtakan",
|
||||
"Mukta Mahee",
|
||||
"MuktaMahee Bold",
|
||||
"MuktaMahee ExtraBold",
|
||||
"MuktaMahee ExtraLight",
|
||||
"MuktaMahee Light",
|
||||
"MuktaMahee Medium",
|
||||
"MuktaMahee Regular",
|
||||
"MuktaMahee SemiBold",
|
||||
"Muna",
|
||||
"Myanmar MN",
|
||||
"Myanmar Sangam MN",
|
||||
"Nadeem",
|
||||
"New Peninim MT",
|
||||
"Noteworthy",
|
||||
"Noto Nastaliq Urdu",
|
||||
"Noto Sans Adlam",
|
||||
"Noto Sans Armenian",
|
||||
"Noto Sans Armenian Blk",
|
||||
"Noto Sans Armenian ExtBd",
|
||||
"Noto Sans Armenian ExtLt",
|
||||
"Noto Sans Armenian Light",
|
||||
"Noto Sans Armenian Med",
|
||||
"Noto Sans Armenian SemBd",
|
||||
"Noto Sans Armenian Thin",
|
||||
"Noto Sans Avestan",
|
||||
"Noto Sans Bamum",
|
||||
"Noto Sans Bassa Vah",
|
||||
"Noto Sans Batak",
|
||||
"Noto Sans Bhaiksuki",
|
||||
"Noto Sans Brahmi",
|
||||
"Noto Sans Buginese",
|
||||
"Noto Sans Buhid",
|
||||
"Noto Sans CanAborig",
|
||||
"Noto Sans Canadian Aboriginal",
|
||||
"Noto Sans Carian",
|
||||
"Noto Sans CaucAlban",
|
||||
"Noto Sans Caucasian Albanian",
|
||||
"Noto Sans Chakma",
|
||||
"Noto Sans Cham",
|
||||
"Noto Sans Coptic",
|
||||
"Noto Sans Cuneiform",
|
||||
"Noto Sans Cypriot",
|
||||
"Noto Sans Duployan",
|
||||
"Noto Sans EgyptHiero",
|
||||
"Noto Sans Egyptian Hieroglyphs",
|
||||
"Noto Sans Elbasan",
|
||||
"Noto Sans Glagolitic",
|
||||
"Noto Sans Gothic",
|
||||
"Noto Sans Gunjala Gondi",
|
||||
"Noto Sans Hanifi Rohingya",
|
||||
"Noto Sans HanifiRohg",
|
||||
"Noto Sans Hanunoo",
|
||||
"Noto Sans Hatran",
|
||||
"Noto Sans ImpAramaic",
|
||||
"Noto Sans Imperial Aramaic",
|
||||
"Noto Sans InsPahlavi",
|
||||
"Noto Sans InsParthi",
|
||||
"Noto Sans Inscriptional Pahlavi",
|
||||
"Noto Sans Inscriptional Parthian",
|
||||
"Noto Sans Javanese",
|
||||
"Noto Sans Kaithi",
|
||||
"Noto Sans Kannada",
|
||||
"Noto Sans Kannada Black",
|
||||
"Noto Sans Kannada ExtraBold",
|
||||
"Noto Sans Kannada ExtraLight",
|
||||
"Noto Sans Kannada Light",
|
||||
"Noto Sans Kannada Medium",
|
||||
"Noto Sans Kannada SemiBold",
|
||||
"Noto Sans Kannada Thin",
|
||||
"Noto Sans Kayah Li",
|
||||
"Noto Sans Kharoshthi",
|
||||
"Noto Sans Khojki",
|
||||
"Noto Sans Khudawadi",
|
||||
"Noto Sans Lepcha",
|
||||
"Noto Sans Limbu",
|
||||
"Noto Sans Linear A",
|
||||
"Noto Sans Linear B",
|
||||
"Noto Sans Lisu",
|
||||
"Noto Sans Lycian",
|
||||
"Noto Sans Lydian",
|
||||
"Noto Sans Mahajani",
|
||||
"Noto Sans Mandaic",
|
||||
"Noto Sans Manichaean",
|
||||
"Noto Sans Marchen",
|
||||
"Noto Sans Masaram Gondi",
|
||||
"Noto Sans Meetei Mayek",
|
||||
"Noto Sans Mende Kikakui",
|
||||
"Noto Sans Meroitic",
|
||||
"Noto Sans Miao",
|
||||
"Noto Sans Modi",
|
||||
"Noto Sans Mongolian",
|
||||
"Noto Sans Mro",
|
||||
"Noto Sans Multani",
|
||||
"Noto Sans Myanmar",
|
||||
"Noto Sans Myanmar Blk",
|
||||
"Noto Sans Myanmar ExtBd",
|
||||
"Noto Sans Myanmar ExtLt",
|
||||
"Noto Sans Myanmar Light",
|
||||
"Noto Sans Myanmar Med",
|
||||
"Noto Sans Myanmar SemBd",
|
||||
"Noto Sans Myanmar Thin",
|
||||
"Noto Sans NKo",
|
||||
"Noto Sans Nabataean",
|
||||
"Noto Sans New Tai Lue",
|
||||
"Noto Sans Newa",
|
||||
"Noto Sans Ol Chiki",
|
||||
"Noto Sans Old Hungarian",
|
||||
"Noto Sans Old Italic",
|
||||
"Noto Sans Old North Arabian",
|
||||
"Noto Sans Old Permic",
|
||||
"Noto Sans Old Persian",
|
||||
"Noto Sans Old South Arabian",
|
||||
"Noto Sans Old Turkic",
|
||||
"Noto Sans OldHung",
|
||||
"Noto Sans OldNorArab",
|
||||
"Noto Sans OldSouArab",
|
||||
"Noto Sans Oriya",
|
||||
"Noto Sans Osage",
|
||||
"Noto Sans Osmanya",
|
||||
"Noto Sans Pahawh Hmong",
|
||||
"Noto Sans Palmyrene",
|
||||
"Noto Sans Pau Cin Hau",
|
||||
"Noto Sans PhagsPa",
|
||||
"Noto Sans Phoenician",
|
||||
"Noto Sans PsaPahlavi",
|
||||
"Noto Sans Psalter Pahlavi",
|
||||
"Noto Sans Rejang",
|
||||
"Noto Sans Samaritan",
|
||||
"Noto Sans Saurashtra",
|
||||
"Noto Sans Sharada",
|
||||
"Noto Sans Siddham",
|
||||
"Noto Sans Sora Sompeng",
|
||||
"Noto Sans SoraSomp",
|
||||
"Noto Sans Sundanese",
|
||||
"Noto Sans Syloti Nagri",
|
||||
"Noto Sans Syriac",
|
||||
"Noto Sans Tagalog",
|
||||
"Noto Sans Tagbanwa",
|
||||
"Noto Sans Tai Le",
|
||||
"Noto Sans Tai Tham",
|
||||
"Noto Sans Tai Viet",
|
||||
"Noto Sans Takri",
|
||||
"Noto Sans Thaana",
|
||||
"Noto Sans Tifinagh",
|
||||
"Noto Sans Tirhuta",
|
||||
"Noto Sans Ugaritic",
|
||||
"Noto Sans Vai",
|
||||
"Noto Sans Wancho",
|
||||
"Noto Sans Warang Citi",
|
||||
"Noto Sans Yi",
|
||||
"Noto Sans Zawgyi",
|
||||
"Noto Sans Zawgyi Blk",
|
||||
"Noto Sans Zawgyi ExtBd",
|
||||
"Noto Sans Zawgyi ExtLt",
|
||||
"Noto Sans Zawgyi Light",
|
||||
"Noto Sans Zawgyi Med",
|
||||
"Noto Sans Zawgyi SemBd",
|
||||
"Noto Sans Zawgyi Thin",
|
||||
"Noto Serif Ahom",
|
||||
"Noto Serif Balinese",
|
||||
"Noto Serif Hmong Nyiakeng",
|
||||
"Noto Serif Myanmar",
|
||||
"Noto Serif Myanmar Blk",
|
||||
"Noto Serif Myanmar ExtBd",
|
||||
"Noto Serif Myanmar ExtLt",
|
||||
"Noto Serif Myanmar Light",
|
||||
"Noto Serif Myanmar Med",
|
||||
"Noto Serif Myanmar SemBd",
|
||||
"Noto Serif Myanmar Thin",
|
||||
"Noto Serif Yezidi",
|
||||
"Optima",
|
||||
"Oriya MN",
|
||||
"Oriya Sangam MN",
|
||||
"PT Mono",
|
||||
"PT Sans",
|
||||
"PT Sans Caption",
|
||||
"PT Sans Narrow",
|
||||
"PT Serif",
|
||||
"PT Serif Caption",
|
||||
"Palatino",
|
||||
"Papyrus",
|
||||
"Party LET",
|
||||
"Phosphate",
|
||||
"Ph\u00f4ng ch\u1eef H\u1ec7 th\u1ed1ng",
|
||||
"PingFang HK",
|
||||
"PingFang SC",
|
||||
"PingFang TC",
|
||||
"Plantagenet Cherokee",
|
||||
"Police syst\u00e8me",
|
||||
"Raanana",
|
||||
"Rendszerbet\u0171t\u00edpus",
|
||||
"Rockwell",
|
||||
"STIX Two Math",
|
||||
"STIX Two Text",
|
||||
"STIXGeneral",
|
||||
"STIXIntegralsD",
|
||||
"STIXIntegralsSm",
|
||||
"STIXIntegralsUp",
|
||||
"STIXIntegralsUpD",
|
||||
"STIXIntegralsUpSm",
|
||||
"STIXNonUnicode",
|
||||
"STIXSizeFiveSym",
|
||||
"STIXSizeFourSym",
|
||||
"STIXSizeOneSym",
|
||||
"STIXSizeThreeSym",
|
||||
"STIXSizeTwoSym",
|
||||
"STIXVariants",
|
||||
"STSong",
|
||||
"Sana",
|
||||
"Sathu",
|
||||
"Savoye LET",
|
||||
"Seravek",
|
||||
"Seravek ExtraLight",
|
||||
"Seravek Light",
|
||||
"Seravek Medium",
|
||||
"Shree Devanagari 714",
|
||||
"SignPainter",
|
||||
"SignPainter-HouseScript",
|
||||
"Silom",
|
||||
"Sinhala MN",
|
||||
"Sinhala Sangam MN",
|
||||
"Sistem Fontu",
|
||||
"Skia",
|
||||
"Snell Roundhand",
|
||||
"Songti SC",
|
||||
"Songti TC",
|
||||
"Sukhumvit Set",
|
||||
"Superclarendon",
|
||||
"Symbol",
|
||||
"Systeemlettertype",
|
||||
"System Font",
|
||||
"Systemschrift",
|
||||
"Systemskrift",
|
||||
"Systemtypsnitt",
|
||||
"Syst\u00e9mov\u00e9 p\u00edsmo",
|
||||
"Tahoma",
|
||||
"Tamil MN",
|
||||
"Tamil Sangam MN",
|
||||
"Telugu MN",
|
||||
"Telugu Sangam MN",
|
||||
"Thonburi",
|
||||
"Times",
|
||||
"Times New Roman",
|
||||
"Tipo de letra del sistema",
|
||||
"Tipo de letra do sistema",
|
||||
"Tipus de lletra del sistema",
|
||||
"Trattatello",
|
||||
"Trebuchet MS",
|
||||
"Verdana",
|
||||
"Waseem",
|
||||
"Webdings",
|
||||
"Wingdings",
|
||||
"Wingdings 2",
|
||||
"Wingdings 3",
|
||||
"Zapf Dingbats",
|
||||
"Zapfino",
|
||||
"\u0393\u03c1\u03b1\u03bc\u03bc\u03b1\u03c4\u03bf\u03c3\u03b5\u03b9\u03c1\u03ac \u03c3\u03c5\u03c3\u03c4\u03ae\u03bc\u03b1\u03c4\u03bf\u03c2",
|
||||
"\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u0448\u0440\u0438\u0444\u0442",
|
||||
"\u0421\u0438\u0441\u0442\u0435\u043c\u043d\u044b\u0439 \u0448\u0440\u0438\u0444\u0442",
|
||||
"\u05d2\u05d5\u05e4\u05df \u05de\u05e2\u05e8\u05db\u05ea",
|
||||
"\u0627\u0644\u0628\u064a\u0627\u0646",
|
||||
"\u0627\u0644\u062a\u0627\u0631\u064a\u062e",
|
||||
"\u0627\u0644\u0646\u064a\u0644",
|
||||
"\u0628\u063a\u062f\u0627\u062f",
|
||||
"\u0628\u064a\u0631\u0648\u062a",
|
||||
"\u062c\u064a\u0632\u0629",
|
||||
"\u062e\u0637 \u0627\u0644\u0646\u0638\u0627\u0645",
|
||||
"\u062f\u0645\u0634\u0642",
|
||||
"\u062f\u064a\u0648\u0627\u0646 \u062b\u0644\u062b",
|
||||
"\u062f\u064a\u0648\u0627\u0646 \u0643\u0648\u0641\u064a",
|
||||
"\u0635\u0646\u0639\u0627\u0621",
|
||||
"\u0641\u0627\u0631\u0633\u064a",
|
||||
"\u0641\u0631\u062d",
|
||||
"\u0643\u0648\u0641\u064a",
|
||||
"\u0645\u0646\u0649",
|
||||
"\u0645\u0650\u0635\u062d\u0641\u064a",
|
||||
"\u0645\u0650\u0635\u062d\u0641\u064a \u0630\u0647\u0628\u064a",
|
||||
"\u0646\u062f\u064a\u0645",
|
||||
"\u0646\u0633\u062e",
|
||||
"\u0648\u0633\u064a\u0645",
|
||||
"\u0906\u0908\u0970\u091f\u0940\u0970\u090f\u092b\u093c\u0970 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940",
|
||||
"\u0906\u0908\u0970\u091f\u0940\u0970\u090f\u092b\u093c\u0970 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u092e\u0930\u093e\u0920\u0940",
|
||||
"\u0915\u094b\u0939\u093f\u0928\u0942\u0930 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940",
|
||||
"\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u090f\u092e\u0970\u091f\u0940\u0970",
|
||||
"\u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u0938\u0902\u0917\u092e \u090f\u092e\u0970\u090f\u0928\u0970",
|
||||
"\u0936\u094d\u0930\u0940 \u0926\u0947\u0935\u0928\u093e\u0917\u0930\u0940 \u096d\u0967\u096a",
|
||||
"\u0e41\u0e1a\u0e1a\u0e2d\u0e31\u0e01\u0e29\u0e23\u0e23\u0e30\u0e1a\u0e1a",
|
||||
"\u2e41\u7175\u6120\u82a9\u82c8",
|
||||
"\u30b7\u30b9\u30c6\u30e0\u30d5\u30a9\u30f3\u30c8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 Pro",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 Pro W4",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 ProN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u4e38\u30b4 ProN W4",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d Pro W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u660e\u671d ProN W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Pro W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 ProN W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Std",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 Std W8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 StdN",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 StdN W8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587 W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4 \u7c21\u4f53\u4e2d\u6587 W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W0",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W1",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W2",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W3",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W4",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W5",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W6",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W7",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W8",
|
||||
"\u30d2\u30e9\u30ae\u30ce\u89d2\u30b4\u30b7\u30c3\u30af W9",
|
||||
"\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587",
|
||||
"\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587 W3",
|
||||
"\u51ac\u9752\u9ed1\u4f53\u7b80\u4f53\u4e2d\u6587 W6",
|
||||
"\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587",
|
||||
"\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587 W3",
|
||||
"\u51ac\u9752\u9ed1\u9ad4\u7c21\u9ad4\u4e2d\u6587 W6",
|
||||
"\u5b8b\u4f53-\u7b80",
|
||||
"\u5b8b\u4f53-\u7e41",
|
||||
"\u5b8b\u9ad4-\u7c21",
|
||||
"\u5b8b\u9ad4-\u7e41",
|
||||
"\u7cfb\u7d71\u5b57\u9ad4",
|
||||
"\u7cfb\u7edf\u5b57\u4f53",
|
||||
"\u82f9\u65b9-\u6e2f",
|
||||
"\u82f9\u65b9-\u7b80",
|
||||
"\u82f9\u65b9-\u7e41",
|
||||
"\u8371\u8389\u834d\u836d\u8a70\u8353\u2050\u726f",
|
||||
"\u8371\u8389\u834d\u836d\u8a70\u8353\u2053\u7464",
|
||||
"\u8371\u8389\u834d\u836d\u8a70\u8353\u8356\u8362\u834e",
|
||||
"\u8371\u8389\u834d\u836d\u8adb\u8353\u2050\u726f",
|
||||
"\u8371\u8389\u834d\u836d\u96be\u92a9\u2050\u726f",
|
||||
"\u860b\u65b9-\u6e2f",
|
||||
"\u860b\u65b9-\u7c21",
|
||||
"\u860b\u65b9-\u7e41",
|
||||
"\u9ed1\u4f53-\u7b80",
|
||||
"\u9ed1\u4f53-\u7e41",
|
||||
"\u9ed1\u9ad4-\u7c21",
|
||||
"\u9ed1\u9ad4-\u7e41",
|
||||
"\u9ed2\u4f53-\u7c21",
|
||||
"\u9ed2\u4f53-\u7e41",
|
||||
"\uc2dc\uc2a4\ud15c \uc11c\uccb4"
|
||||
],
|
||||
"lin": [
|
||||
"Arimo",
|
||||
"Cousine",
|
||||
"Noto Naskh Arabic",
|
||||
"Noto Sans Adlam",
|
||||
"Noto Sans Armenian",
|
||||
"Noto Sans Balinese",
|
||||
"Noto Sans Bamum",
|
||||
"Noto Sans Bassa Vah",
|
||||
"Noto Sans Batak",
|
||||
"Noto Sans Bengali",
|
||||
"Noto Sans Buginese",
|
||||
"Noto Sans Buhid",
|
||||
"Noto Sans Canadian Aboriginal",
|
||||
"Noto Sans Chakma",
|
||||
"Noto Sans Cham",
|
||||
"Noto Sans Cherokee",
|
||||
"Noto Sans Coptic",
|
||||
"Noto Sans Deseret",
|
||||
"Noto Sans Devanagari",
|
||||
"Noto Sans Elbasan",
|
||||
"Noto Sans Ethiopic",
|
||||
"Noto Sans Georgian",
|
||||
"Noto Sans Grantha",
|
||||
"Noto Sans Gujarati",
|
||||
"Noto Sans Gunjala Gondi",
|
||||
"Noto Sans Gurmukhi",
|
||||
"Noto Sans Hanifi Rohingya",
|
||||
"Noto Sans Hanunoo",
|
||||
"Noto Sans Hebrew",
|
||||
"Noto Sans JP",
|
||||
"Noto Sans Javanese",
|
||||
"Noto Sans KR",
|
||||
"Noto Sans Kannada",
|
||||
"Noto Sans Kayah Li",
|
||||
"Noto Sans Khmer",
|
||||
"Noto Sans Khojki",
|
||||
"Noto Sans Khudawadi",
|
||||
"Noto Sans Lao",
|
||||
"Noto Sans Lepcha",
|
||||
"Noto Sans Limbu",
|
||||
"Noto Sans Lisu",
|
||||
"Noto Sans Mahajani",
|
||||
"Noto Sans Malayalam",
|
||||
"Noto Sans Mandaic",
|
||||
"Noto Sans Masaram Gondi",
|
||||
"Noto Sans Medefaidrin",
|
||||
"Noto Sans Meetei Mayek",
|
||||
"Noto Sans Mende Kikakui",
|
||||
"Noto Sans Miao",
|
||||
"Noto Sans Modi",
|
||||
"Noto Sans Mongolian",
|
||||
"Noto Sans Mro",
|
||||
"Noto Sans Multani",
|
||||
"Noto Sans Myanmar",
|
||||
"Noto Sans NKo",
|
||||
"Noto Sans New Tai Lue",
|
||||
"Noto Sans Newa",
|
||||
"Noto Sans Ol Chiki",
|
||||
"Noto Sans Oriya",
|
||||
"Noto Sans Osage",
|
||||
"Noto Sans Osmanya",
|
||||
"Noto Sans Pahawh Hmong",
|
||||
"Noto Sans Pau Cin Hau",
|
||||
"Noto Sans Rejang",
|
||||
"Noto Sans Runic",
|
||||
"Noto Sans SC",
|
||||
"Noto Sans Samaritan",
|
||||
"Noto Sans Saurashtra",
|
||||
"Noto Sans Sharada",
|
||||
"Noto Sans Shavian",
|
||||
"Noto Sans Sinhala",
|
||||
"Noto Sans Sora Sompeng",
|
||||
"Noto Sans Soyombo",
|
||||
"Noto Sans Sundanese",
|
||||
"Noto Sans Syloti Nagri",
|
||||
"Noto Sans Symbols",
|
||||
"Noto Sans Symbols 2",
|
||||
"Noto Sans Syriac",
|
||||
"Noto Sans TC",
|
||||
"Noto Sans Tagalog",
|
||||
"Noto Sans Tagbanwa",
|
||||
"Noto Sans Tai Le",
|
||||
"Noto Sans Tai Tham",
|
||||
"Noto Sans Tai Viet",
|
||||
"Noto Sans Takri",
|
||||
"Noto Sans Tamil",
|
||||
"Noto Sans Telugu",
|
||||
"Noto Sans Thaana",
|
||||
"Noto Sans Thai",
|
||||
"Noto Sans Tifinagh",
|
||||
"Noto Sans Tifinagh APT",
|
||||
"Noto Sans Tifinagh Adrar",
|
||||
"Noto Sans Tifinagh Agraw Imazighen",
|
||||
"Noto Sans Tifinagh Ahaggar",
|
||||
"Noto Sans Tifinagh Air",
|
||||
"Noto Sans Tifinagh Azawagh",
|
||||
"Noto Sans Tifinagh Ghat",
|
||||
"Noto Sans Tifinagh Hawad",
|
||||
"Noto Sans Tifinagh Rhissa Ixa",
|
||||
"Noto Sans Tifinagh SIL",
|
||||
"Noto Sans Tifinagh Tawellemmet",
|
||||
"Noto Sans Tirhuta",
|
||||
"Noto Sans Vai",
|
||||
"Noto Sans Wancho",
|
||||
"Noto Sans Warang Citi",
|
||||
"Noto Sans Yi",
|
||||
"Noto Sans Zanabazar Square",
|
||||
"Noto Serif Armenian",
|
||||
"Noto Serif Balinese",
|
||||
"Noto Serif Bengali",
|
||||
"Noto Serif Devanagari",
|
||||
"Noto Serif Dogra",
|
||||
"Noto Serif Ethiopic",
|
||||
"Noto Serif Georgian",
|
||||
"Noto Serif Grantha",
|
||||
"Noto Serif Gujarati",
|
||||
"Noto Serif Gurmukhi",
|
||||
"Noto Serif Hebrew",
|
||||
"Noto Serif Kannada",
|
||||
"Noto Serif Khmer",
|
||||
"Noto Serif Khojki",
|
||||
"Noto Serif Lao",
|
||||
"Noto Serif Malayalam",
|
||||
"Noto Serif Myanmar",
|
||||
"Noto Serif NP Hmong",
|
||||
"Noto Serif Sinhala",
|
||||
"Noto Serif Tamil",
|
||||
"Noto Serif Telugu",
|
||||
"Noto Serif Thai",
|
||||
"Noto Serif Tibetan",
|
||||
"Noto Serif Yezidi",
|
||||
"STIX Two Math",
|
||||
"Tinos",
|
||||
"Twemoji Mozilla"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"safari": [
|
||||
"Referer",
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Accept",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"User-Agent",
|
||||
"Content-Length",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Connection",
|
||||
"Host",
|
||||
"Cookie",
|
||||
"Sec-Fetch-Dest",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-Site",
|
||||
":method",
|
||||
":scheme",
|
||||
":authority",
|
||||
":path",
|
||||
"referer",
|
||||
"origin",
|
||||
"content-type",
|
||||
"accept",
|
||||
"user-agent",
|
||||
"content-length",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"cookie",
|
||||
"sec-fetch-dest",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-site"
|
||||
],
|
||||
"chrome": [
|
||||
"Host",
|
||||
"Connection",
|
||||
"Content-Length",
|
||||
"Cache-Control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"User-Agent",
|
||||
"Accept",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-User",
|
||||
"Sec-Fetch-Dest",
|
||||
"Referer",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Cookie",
|
||||
":method",
|
||||
":authority",
|
||||
":scheme",
|
||||
":path",
|
||||
"content-length",
|
||||
"cache-control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"origin",
|
||||
"content-type",
|
||||
"upgrade-insecure-requests",
|
||||
"user-agent",
|
||||
"accept",
|
||||
"sec-fetch-site",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-user",
|
||||
"sec-fetch-dest",
|
||||
"referer",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"cookie",
|
||||
"priority"
|
||||
],
|
||||
"firefox": [
|
||||
"Host",
|
||||
"User-Agent",
|
||||
"Accept",
|
||||
"Accept-Language",
|
||||
"Accept-Encoding",
|
||||
"Content-Type",
|
||||
"Content-Length",
|
||||
"Origin",
|
||||
"Connection",
|
||||
"Referer",
|
||||
"Cookie",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"Sec-Fetch-Dest",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-User",
|
||||
"Priority",
|
||||
":method",
|
||||
":path",
|
||||
":authority",
|
||||
":scheme",
|
||||
"user-agent",
|
||||
"accept",
|
||||
"accept-language",
|
||||
"accept-encoding",
|
||||
"content-type",
|
||||
"content-length",
|
||||
"origin",
|
||||
"referer",
|
||||
"cookie",
|
||||
"upgrade-insecure-requests",
|
||||
"sec-fetch-dest",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-site",
|
||||
"sec-fetch-user",
|
||||
"priority",
|
||||
"te"
|
||||
],
|
||||
"edge": [
|
||||
"Host",
|
||||
"Connection",
|
||||
"Content-Length",
|
||||
"Cache-Control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"Origin",
|
||||
"Content-Type",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"User-Agent",
|
||||
"Accept",
|
||||
"Sec-Fetch-Site",
|
||||
"Sec-Fetch-Mode",
|
||||
"Sec-Fetch-User",
|
||||
"Sec-Fetch-Dest",
|
||||
"Referer",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Cookie",
|
||||
":method",
|
||||
":authority",
|
||||
":scheme",
|
||||
":path",
|
||||
"content-length",
|
||||
"cache-control",
|
||||
"sec-ch-ua",
|
||||
"sec-ch-ua-mobile",
|
||||
"sec-ch-ua-platform",
|
||||
"origin",
|
||||
"content-type",
|
||||
"upgrade-insecure-requests",
|
||||
"user-agent",
|
||||
"accept",
|
||||
"sec-fetch-site",
|
||||
"sec-fetch-mode",
|
||||
"sec-fetch-user",
|
||||
"sec-fetch-dest",
|
||||
"referer",
|
||||
"accept-encoding",
|
||||
"accept-language",
|
||||
"cookie",
|
||||
"priority"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
||||
pub const FINGERPRINT_NETWORK_ZIP: &[u8] = include_bytes!("fingerprint-network-definition.zip");
|
||||
pub const INPUT_NETWORK_ZIP: &[u8] = include_bytes!("input-network-definition.zip");
|
||||
pub const HEADER_NETWORK_ZIP: &[u8] = include_bytes!("header-network-definition.zip");
|
||||
pub const BROWSER_HELPER_JSON: &str = include_str!("browser-helper-file.json");
|
||||
pub const HEADERS_ORDER_JSON: &str = include_str!("headers-order.json");
|
||||
pub const FONTS_JSON: &str = include_str!("fonts.json");
|
||||
pub const BROWSERFORGE_YML: &str = include_str!("browserforge.yml");
|
||||
pub const WEBGL_DATA_DB: &[u8] = include_bytes!("webgl_data.db");
|
||||
pub const TERRITORY_INFO_XML: &str = include_str!("territoryInfo.xml");
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,142 @@
|
||||
//! Environment variable handling for Camoufox configuration.
|
||||
//!
|
||||
//! Camoufox reads its configuration from environment variables named CAMOU_CONFIG_1, CAMOU_CONFIG_2, etc.
|
||||
//! The configuration JSON is chunked to fit within environment variable size limits.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Maximum chunk size for environment variables on Windows.
|
||||
const CHUNK_SIZE_WINDOWS: usize = 2047;
|
||||
|
||||
/// Maximum chunk size for environment variables on Unix systems.
|
||||
const CHUNK_SIZE_UNIX: usize = 32767;
|
||||
|
||||
/// Get the chunk size for the current platform.
|
||||
fn get_chunk_size() -> usize {
|
||||
if cfg!(windows) {
|
||||
CHUNK_SIZE_WINDOWS
|
||||
} else {
|
||||
CHUNK_SIZE_UNIX
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Camoufox config map to environment variables.
|
||||
///
|
||||
/// The config is serialized to JSON and split into chunks that fit within
|
||||
/// environment variable size limits. Each chunk is stored in a variable
|
||||
/// named CAMOU_CONFIG_1, CAMOU_CONFIG_2, etc.
|
||||
pub fn config_to_env_vars(
|
||||
config: &HashMap<String, serde_json::Value>,
|
||||
) -> Result<HashMap<String, String>, serde_json::Error> {
|
||||
let config_json = serde_json::to_string(config)?;
|
||||
Ok(chunk_config_string(&config_json))
|
||||
}
|
||||
|
||||
/// Split a config string into chunks and create environment variable map.
|
||||
pub fn chunk_config_string(config_str: &str) -> HashMap<String, String> {
|
||||
let chunk_size = get_chunk_size();
|
||||
let mut env_vars = HashMap::new();
|
||||
|
||||
for (i, chunk) in config_str.as_bytes().chunks(chunk_size).enumerate() {
|
||||
let chunk_str = String::from_utf8_lossy(chunk).to_string();
|
||||
let env_name = format!("CAMOU_CONFIG_{}", i + 1);
|
||||
env_vars.insert(env_name, chunk_str);
|
||||
}
|
||||
|
||||
env_vars
|
||||
}
|
||||
|
||||
/// Determine the target OS from a user agent string.
|
||||
pub fn determine_ua_os(user_agent: &str) -> &'static str {
|
||||
let ua_lower = user_agent.to_lowercase();
|
||||
|
||||
if ua_lower.contains("mac os") || ua_lower.contains("macos") || ua_lower.contains("macintosh") {
|
||||
"mac"
|
||||
} else if ua_lower.contains("windows") {
|
||||
"win"
|
||||
} else {
|
||||
"lin"
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the fontconfig path environment variable for Linux.
|
||||
pub fn get_fontconfig_env(target_os: &str, camoufox_path: &std::path::Path) -> Option<String> {
|
||||
if cfg!(target_os = "linux") {
|
||||
let fontconfig_dir = camoufox_path.join("fontconfig").join(target_os);
|
||||
if fontconfig_dir.exists() {
|
||||
return Some(fontconfig_dir.to_string_lossy().to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chunk_small_config() {
|
||||
let config = r#"{"navigator.userAgent": "Mozilla/5.0"}"#;
|
||||
let env_vars = chunk_config_string(config);
|
||||
|
||||
assert_eq!(env_vars.len(), 1);
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
assert_eq!(env_vars.get("CAMOU_CONFIG_1").unwrap(), config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_chunk_large_config() {
|
||||
// Create a config string larger than the chunk size
|
||||
let chunk_size = get_chunk_size();
|
||||
let large_value = "x".repeat(chunk_size * 2 + 100);
|
||||
let config = format!(r#"{{"key": "{}"}}"#, large_value);
|
||||
|
||||
let env_vars = chunk_config_string(&config);
|
||||
|
||||
// Should have at least 2 chunks
|
||||
assert!(env_vars.len() >= 2);
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_2"));
|
||||
|
||||
// Reconstruct and verify
|
||||
let mut reconstructed = String::new();
|
||||
let mut i = 1;
|
||||
while let Some(chunk) = env_vars.get(&format!("CAMOU_CONFIG_{}", i)) {
|
||||
reconstructed.push_str(chunk);
|
||||
i += 1;
|
||||
}
|
||||
assert_eq!(reconstructed, config);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_ua_os_windows() {
|
||||
let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
assert_eq!(determine_ua_os(ua), "win");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_ua_os_macos() {
|
||||
let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
assert_eq!(determine_ua_os(ua), "mac");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_determine_ua_os_linux() {
|
||||
let ua = "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0";
|
||||
assert_eq!(determine_ua_os(ua), "lin");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_to_env_vars() {
|
||||
let mut config = HashMap::new();
|
||||
config.insert(
|
||||
"navigator.userAgent".to_string(),
|
||||
serde_json::json!("Mozilla/5.0 Firefox/135.0"),
|
||||
);
|
||||
config.insert("screen.width".to_string(), serde_json::json!(1920));
|
||||
|
||||
let env_vars = config_to_env_vars(&config).unwrap();
|
||||
assert!(!env_vars.is_empty());
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
//! Bayesian network for fingerprint generation.
|
||||
//!
|
||||
//! Loads pre-trained probability distributions from ZIP files and samples fingerprints.
|
||||
|
||||
use super::bayesian_node::{BayesianNode, NodeDefinition};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Cursor, Read};
|
||||
use zip::ZipArchive;
|
||||
|
||||
/// Network definition structure from the ZIP file.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct NetworkDefinition {
|
||||
pub nodes: Vec<NodeDefinition>,
|
||||
}
|
||||
|
||||
/// A Bayesian network for generating consistent fingerprints.
|
||||
pub struct BayesianNetwork {
|
||||
nodes_in_sampling_order: Vec<BayesianNode>,
|
||||
nodes_by_name: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
impl BayesianNetwork {
|
||||
/// Load a Bayesian network from embedded ZIP file bytes.
|
||||
pub fn from_zip_bytes(zip_bytes: &[u8]) -> Result<Self, BayesianNetworkError> {
|
||||
let cursor = Cursor::new(zip_bytes);
|
||||
let mut archive = ZipArchive::new(cursor)?;
|
||||
|
||||
// Find and read the JSON file from the ZIP
|
||||
let mut json_content = String::new();
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i)?;
|
||||
if file.name().ends_with(".json") {
|
||||
file.read_to_string(&mut json_content)?;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if json_content.is_empty() {
|
||||
return Err(BayesianNetworkError::NoJsonInZip);
|
||||
}
|
||||
|
||||
let definition: NetworkDefinition = serde_json::from_str(&json_content)?;
|
||||
|
||||
let mut nodes_in_sampling_order = Vec::with_capacity(definition.nodes.len());
|
||||
let mut nodes_by_name = HashMap::with_capacity(definition.nodes.len());
|
||||
|
||||
for (i, node_def) in definition.nodes.into_iter().enumerate() {
|
||||
nodes_by_name.insert(node_def.name.clone(), i);
|
||||
nodes_in_sampling_order.push(BayesianNode::new(node_def));
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
nodes_in_sampling_order,
|
||||
nodes_by_name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a node by name.
|
||||
pub fn get_node(&self, name: &str) -> Option<&BayesianNode> {
|
||||
self
|
||||
.nodes_by_name
|
||||
.get(name)
|
||||
.map(|&i| &self.nodes_in_sampling_order[i])
|
||||
}
|
||||
|
||||
/// Get possible values for a node.
|
||||
pub fn get_possible_values(&self, name: &str) -> Option<Vec<String>> {
|
||||
self
|
||||
.get_node(name)
|
||||
.map(|node| node.possible_values().to_vec())
|
||||
}
|
||||
|
||||
/// Generate a random sample from the network.
|
||||
///
|
||||
/// `input_values` contains already known node values that should not be overwritten.
|
||||
pub fn generate_sample(&self, input_values: &HashMap<String, String>) -> HashMap<String, String> {
|
||||
let mut sample = input_values.clone();
|
||||
|
||||
for node in &self.nodes_in_sampling_order {
|
||||
if !sample.contains_key(node.name()) {
|
||||
let value = node.sample(&sample);
|
||||
sample.insert(node.name().to_string(), value);
|
||||
}
|
||||
}
|
||||
|
||||
sample
|
||||
}
|
||||
|
||||
/// Generate a random sample consistent with the given value restrictions.
|
||||
///
|
||||
/// Uses backtracking to find a valid configuration.
|
||||
/// Returns `None` if no consistent sample can be generated.
|
||||
pub fn generate_consistent_sample_when_possible(
|
||||
&self,
|
||||
value_possibilities: &HashMap<String, Vec<String>>,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
self.recursively_generate_consistent_sample(HashMap::new(), value_possibilities, 0)
|
||||
}
|
||||
|
||||
fn recursively_generate_consistent_sample(
|
||||
&self,
|
||||
sample_so_far: HashMap<String, String>,
|
||||
value_possibilities: &HashMap<String, Vec<String>>,
|
||||
depth: usize,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
if depth >= self.nodes_in_sampling_order.len() {
|
||||
return Some(sample_so_far);
|
||||
}
|
||||
|
||||
let node = &self.nodes_in_sampling_order[depth];
|
||||
let mut banned_values: Vec<String> = Vec::new();
|
||||
let mut sample_so_far = sample_so_far;
|
||||
|
||||
loop {
|
||||
let sample_value = node.sample_according_to_restrictions(
|
||||
&sample_so_far,
|
||||
value_possibilities.get(node.name()).map(|v| v.as_slice()),
|
||||
&banned_values,
|
||||
);
|
||||
|
||||
let Some(value) = sample_value else {
|
||||
break;
|
||||
};
|
||||
|
||||
sample_so_far.insert(node.name().to_string(), value.clone());
|
||||
|
||||
if let Some(complete_sample) = self.recursively_generate_consistent_sample(
|
||||
sample_so_far.clone(),
|
||||
value_possibilities,
|
||||
depth + 1,
|
||||
) {
|
||||
return Some(complete_sample);
|
||||
}
|
||||
|
||||
banned_values.push(value);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors that can occur when working with Bayesian networks.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BayesianNetworkError {
|
||||
#[error("ZIP file error: {0}")]
|
||||
Zip(#[from] zip::result::ZipError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("No JSON file found in ZIP archive")]
|
||||
NoJsonInZip,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_load_input_network() {
|
||||
let zip_bytes = include_bytes!("../data/input-network-definition.zip");
|
||||
let network = BayesianNetwork::from_zip_bytes(zip_bytes);
|
||||
assert!(
|
||||
network.is_ok(),
|
||||
"Failed to load input network: {:?}",
|
||||
network.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_sample_from_input_network() {
|
||||
let zip_bytes = include_bytes!("../data/input-network-definition.zip");
|
||||
let network = BayesianNetwork::from_zip_bytes(zip_bytes).unwrap();
|
||||
|
||||
let sample = network.generate_sample(&HashMap::new());
|
||||
assert!(!sample.is_empty(), "Sample should not be empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_consistent_sample() {
|
||||
let zip_bytes = include_bytes!("../data/input-network-definition.zip");
|
||||
let network = BayesianNetwork::from_zip_bytes(zip_bytes).unwrap();
|
||||
|
||||
let mut constraints = HashMap::new();
|
||||
constraints.insert("*OPERATING_SYSTEM".to_string(), vec!["windows".to_string()]);
|
||||
|
||||
let sample = network.generate_consistent_sample_when_possible(&constraints);
|
||||
assert!(sample.is_some(), "Should generate a consistent sample");
|
||||
|
||||
if let Some(s) = sample {
|
||||
assert_eq!(s.get("*OPERATING_SYSTEM"), Some(&"windows".to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
//! Bayesian network node implementation for fingerprint generation.
|
||||
//!
|
||||
//! Implements weighted random sampling from conditional probability distributions.
|
||||
|
||||
use rand::Rng;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Node definition from the network JSON file.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NodeDefinition {
|
||||
pub name: String,
|
||||
pub parent_names: Vec<String>,
|
||||
pub possible_values: Vec<String>,
|
||||
pub conditional_probabilities: ConditionalProbabilities,
|
||||
}
|
||||
|
||||
/// Conditional probability structure - can be nested or terminal.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ConditionalProbabilities {
|
||||
#[serde(default)]
|
||||
pub deeper: Option<HashMap<String, ConditionalProbabilities>>,
|
||||
#[serde(default)]
|
||||
pub skip: Option<Box<ConditionalProbabilities>>,
|
||||
#[serde(flatten)]
|
||||
pub probabilities: HashMap<String, f64>,
|
||||
}
|
||||
|
||||
impl ConditionalProbabilities {
|
||||
/// Check if this is a terminal node (has actual probabilities, not deeper nesting)
|
||||
pub fn is_terminal(&self) -> bool {
|
||||
self.deeper.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
/// A single node in the Bayesian network.
|
||||
pub struct BayesianNode {
|
||||
definition: NodeDefinition,
|
||||
}
|
||||
|
||||
impl BayesianNode {
|
||||
pub fn new(definition: NodeDefinition) -> Self {
|
||||
Self { definition }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.definition.name
|
||||
}
|
||||
|
||||
pub fn parent_names(&self) -> &[String] {
|
||||
&self.definition.parent_names
|
||||
}
|
||||
|
||||
pub fn possible_values(&self) -> &[String] {
|
||||
&self.definition.possible_values
|
||||
}
|
||||
|
||||
/// Get the probability distribution given parent node values.
|
||||
fn get_probabilities_given_known_values(
|
||||
&self,
|
||||
parent_values: &HashMap<String, String>,
|
||||
) -> HashMap<String, f64> {
|
||||
let mut probabilities = &self.definition.conditional_probabilities;
|
||||
|
||||
for parent_name in &self.definition.parent_names {
|
||||
if let Some(deeper) = &probabilities.deeper {
|
||||
if let Some(parent_value) = parent_values.get(parent_name) {
|
||||
if let Some(next_level) = deeper.get(parent_value) {
|
||||
probabilities = next_level;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Use skip if parent value not found in deeper
|
||||
if let Some(skip) = &probabilities.skip {
|
||||
probabilities = skip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
probabilities.probabilities.clone()
|
||||
}
|
||||
|
||||
/// Randomly sample from the given values using the given probabilities.
|
||||
fn sample_random_value_from_possibilities(
|
||||
possible_values: &[String],
|
||||
total_probability: f64,
|
||||
probabilities: &HashMap<String, f64>,
|
||||
) -> String {
|
||||
if possible_values.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let mut rng = rand::rng();
|
||||
let anchor = rng.random::<f64>() * total_probability;
|
||||
let mut cumulative = 0.0;
|
||||
|
||||
for value in possible_values {
|
||||
if let Some(&prob) = probabilities.get(value) {
|
||||
cumulative += prob;
|
||||
if cumulative > anchor {
|
||||
return value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
possible_values.first().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Sample a value from the conditional distribution given parent values.
|
||||
pub fn sample(&self, parent_values: &HashMap<String, String>) -> String {
|
||||
let probabilities = self.get_probabilities_given_known_values(parent_values);
|
||||
let possible_values: Vec<String> = probabilities.keys().cloned().collect();
|
||||
|
||||
Self::sample_random_value_from_possibilities(&possible_values, 1.0, &probabilities)
|
||||
}
|
||||
|
||||
/// Sample according to restrictions on possible values.
|
||||
///
|
||||
/// Returns `None` if no valid value can be sampled.
|
||||
pub fn sample_according_to_restrictions(
|
||||
&self,
|
||||
parent_values: &HashMap<String, String>,
|
||||
value_possibilities: Option<&[String]>,
|
||||
banned_values: &[String],
|
||||
) -> Option<String> {
|
||||
let probabilities = self.get_probabilities_given_known_values(parent_values);
|
||||
let values_in_distribution: Vec<String> = probabilities.keys().cloned().collect();
|
||||
|
||||
let possible_values = value_possibilities.unwrap_or(&values_in_distribution);
|
||||
|
||||
let mut valid_values = Vec::new();
|
||||
let mut total_probability = 0.0;
|
||||
|
||||
for value in possible_values {
|
||||
if !banned_values.contains(value) && values_in_distribution.contains(value) {
|
||||
if let Some(&prob) = probabilities.get(value) {
|
||||
valid_values.push(value.clone());
|
||||
total_probability += prob;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if valid_values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Self::sample_random_value_from_possibilities(
|
||||
&valid_values,
|
||||
total_probability,
|
||||
&probabilities,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_test_node() -> BayesianNode {
|
||||
let mut probs = HashMap::new();
|
||||
probs.insert("1920".to_string(), 0.5);
|
||||
probs.insert("1366".to_string(), 0.3);
|
||||
probs.insert("1536".to_string(), 0.2);
|
||||
|
||||
let definition = NodeDefinition {
|
||||
name: "screen.width".to_string(),
|
||||
parent_names: vec![],
|
||||
possible_values: vec!["1920".to_string(), "1366".to_string(), "1536".to_string()],
|
||||
conditional_probabilities: ConditionalProbabilities {
|
||||
deeper: None,
|
||||
skip: None,
|
||||
probabilities: probs,
|
||||
},
|
||||
};
|
||||
|
||||
BayesianNode::new(definition)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_returns_valid_value() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
for _ in 0..100 {
|
||||
let value = node.sample(&parent_values);
|
||||
assert!(
|
||||
node.possible_values().contains(&value),
|
||||
"Sampled value '{}' not in possible values",
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_with_restrictions() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
let allowed = vec!["1920".to_string()];
|
||||
let banned = vec![];
|
||||
|
||||
let value = node.sample_according_to_restrictions(&parent_values, Some(&allowed), &banned);
|
||||
|
||||
assert_eq!(value, Some("1920".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_with_banned_values() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
let banned = vec!["1920".to_string(), "1366".to_string()];
|
||||
|
||||
for _ in 0..100 {
|
||||
let value = node.sample_according_to_restrictions(&parent_values, None, &banned);
|
||||
assert_eq!(value, Some("1536".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_returns_none_when_all_banned() {
|
||||
let node = create_test_node();
|
||||
let parent_values = HashMap::new();
|
||||
|
||||
let banned = vec!["1920".to_string(), "1366".to_string(), "1536".to_string()];
|
||||
|
||||
let value = node.sample_according_to_restrictions(&parent_values, None, &banned);
|
||||
assert!(value.is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,569 @@
|
||||
//! Fingerprint generation module.
|
||||
//!
|
||||
//! Generates realistic browser fingerprints using Bayesian networks trained on real browser data.
|
||||
|
||||
pub mod bayesian_network;
|
||||
pub mod bayesian_node;
|
||||
pub mod types;
|
||||
|
||||
use bayesian_network::{BayesianNetwork, BayesianNetworkError};
|
||||
use std::collections::HashMap;
|
||||
use types::*;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
/// Fingerprint generator using Bayesian networks.
|
||||
pub struct FingerprintGenerator {
|
||||
fingerprint_network: BayesianNetwork,
|
||||
input_network: BayesianNetwork,
|
||||
header_network: BayesianNetwork,
|
||||
browser_helper: Vec<BrowserHttpInfo>,
|
||||
headers_order: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Parsed browser/HTTP version info.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrowserHttpInfo {
|
||||
pub name: String,
|
||||
pub version: Vec<u32>,
|
||||
pub http_version: String,
|
||||
pub complete_string: String,
|
||||
}
|
||||
|
||||
impl BrowserHttpInfo {
|
||||
fn parse(s: &str) -> Option<Self> {
|
||||
if s == MISSING_VALUE_DATASET_TOKEN {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = s.split('|').collect();
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let browser_string = parts[0];
|
||||
let http_version = parts[1].to_string();
|
||||
|
||||
let browser_parts: Vec<&str> = browser_string.split('/').collect();
|
||||
if browser_parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = browser_parts[0].to_string();
|
||||
let version: Vec<u32> = browser_parts[1]
|
||||
.split('.')
|
||||
.filter_map(|v| v.parse().ok())
|
||||
.collect();
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
version,
|
||||
http_version,
|
||||
complete_string: s.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn major_version(&self) -> u32 {
|
||||
self.version.first().copied().unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for fingerprint generation.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum FingerprintError {
|
||||
#[error("Bayesian network error: {0}")]
|
||||
Network(#[from] BayesianNetworkError),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("Failed to generate consistent fingerprint after {0} attempts")]
|
||||
GenerationFailed(u32),
|
||||
|
||||
#[error("No valid fingerprint generated")]
|
||||
NoValidFingerprint,
|
||||
}
|
||||
|
||||
impl FingerprintGenerator {
|
||||
/// Create a new fingerprint generator.
|
||||
pub fn new() -> Result<Self, FingerprintError> {
|
||||
let fingerprint_network = BayesianNetwork::from_zip_bytes(data::FINGERPRINT_NETWORK_ZIP)?;
|
||||
let input_network = BayesianNetwork::from_zip_bytes(data::INPUT_NETWORK_ZIP)?;
|
||||
let header_network = BayesianNetwork::from_zip_bytes(data::HEADER_NETWORK_ZIP)?;
|
||||
|
||||
let browser_strings: Vec<String> = serde_json::from_str(data::BROWSER_HELPER_JSON)?;
|
||||
let browser_helper: Vec<BrowserHttpInfo> = browser_strings
|
||||
.iter()
|
||||
.filter_map(|s| BrowserHttpInfo::parse(s))
|
||||
.collect();
|
||||
|
||||
let headers_order: HashMap<String, Vec<String>> =
|
||||
serde_json::from_str(data::HEADERS_ORDER_JSON)?;
|
||||
|
||||
Ok(Self {
|
||||
fingerprint_network,
|
||||
input_network,
|
||||
header_network,
|
||||
browser_helper,
|
||||
headers_order,
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate a fingerprint with matching headers.
|
||||
pub fn get_fingerprint(
|
||||
&self,
|
||||
options: &FingerprintOptions,
|
||||
) -> Result<FingerprintWithHeaders, FingerprintError> {
|
||||
const MAX_RETRIES: u32 = 10;
|
||||
|
||||
// Build constraints from options
|
||||
let mut value_possibilities = self.build_constraints(options);
|
||||
|
||||
// Handle screen constraints
|
||||
let screen_values = if let Some(screen_constraints) = &options.screen {
|
||||
self.filter_screen_values(screen_constraints)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(sv) = screen_values {
|
||||
value_possibilities.insert("screen".to_string(), sv);
|
||||
}
|
||||
|
||||
for attempt in 0..MAX_RETRIES {
|
||||
// Generate input sample consistent with constraints
|
||||
let input_sample = self
|
||||
.input_network
|
||||
.generate_consistent_sample_when_possible(&value_possibilities);
|
||||
|
||||
let Some(input_sample) = input_sample else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Generate header sample from input
|
||||
let header_sample = self.header_network.generate_sample(&input_sample);
|
||||
|
||||
// Extract user agent
|
||||
let user_agent = header_sample
|
||||
.get("user-agent")
|
||||
.or_else(|| header_sample.get("User-Agent"))
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build fingerprint constraints with the generated user agent
|
||||
let mut fp_constraints = value_possibilities.clone();
|
||||
fp_constraints.insert("userAgent".to_string(), vec![user_agent.clone()]);
|
||||
|
||||
// Generate fingerprint sample
|
||||
let fingerprint_sample = self
|
||||
.fingerprint_network
|
||||
.generate_consistent_sample_when_possible(&fp_constraints);
|
||||
|
||||
let Some(fp_sample) = fingerprint_sample else {
|
||||
log::debug!(
|
||||
"Failed to generate fingerprint on attempt {}, retrying",
|
||||
attempt + 1
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
// Transform the sample to a Fingerprint struct
|
||||
match self.transform_sample(&fp_sample, &header_sample, options) {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(e) => {
|
||||
log::debug!(
|
||||
"Failed to transform fingerprint on attempt {}: {}",
|
||||
attempt + 1,
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(FingerprintError::GenerationFailed(MAX_RETRIES))
|
||||
}
|
||||
|
||||
/// Build constraint map from options.
|
||||
fn build_constraints(&self, options: &FingerprintOptions) -> HashMap<String, Vec<String>> {
|
||||
let mut constraints = HashMap::new();
|
||||
|
||||
// Operating system constraint
|
||||
if let Some(os) = &options.operating_system {
|
||||
constraints.insert(OPERATING_SYSTEM_NODE_NAME.to_string(), vec![os.clone()]);
|
||||
}
|
||||
|
||||
// Device constraint (default to desktop)
|
||||
let devices = options
|
||||
.devices
|
||||
.clone()
|
||||
.unwrap_or_else(|| vec!["desktop".to_string()]);
|
||||
constraints.insert(DEVICE_NODE_NAME.to_string(), devices);
|
||||
|
||||
// Browser constraint
|
||||
let browsers = options
|
||||
.browsers
|
||||
.clone()
|
||||
.unwrap_or_else(|| SUPPORTED_BROWSERS.iter().map(|s| s.to_string()).collect());
|
||||
|
||||
let http_version = options
|
||||
.http_version
|
||||
.clone()
|
||||
.unwrap_or_else(|| "2".to_string());
|
||||
|
||||
// Filter browser helper entries by browser names and HTTP version
|
||||
let browser_http_values: Vec<String> = self
|
||||
.browser_helper
|
||||
.iter()
|
||||
.filter(|bh| browsers.contains(&bh.name) && bh.http_version == http_version)
|
||||
.map(|bh| bh.complete_string.clone())
|
||||
.collect();
|
||||
|
||||
if !browser_http_values.is_empty() {
|
||||
constraints.insert(BROWSER_HTTP_NODE_NAME.to_string(), browser_http_values);
|
||||
}
|
||||
|
||||
constraints
|
||||
}
|
||||
|
||||
/// Filter screen values based on constraints.
|
||||
fn filter_screen_values(&self, constraints: &ScreenConstraints) -> Option<Vec<String>> {
|
||||
let possible_values = self.fingerprint_network.get_possible_values("screen")?;
|
||||
|
||||
let filtered: Vec<String> = possible_values
|
||||
.into_iter()
|
||||
.filter(|screen_str| {
|
||||
// Screen values are stored as "*STRINGIFIED*{...json...}"
|
||||
if let Some(json_str) = screen_str.strip_prefix(STRINGIFIED_PREFIX) {
|
||||
if let Ok(screen) = serde_json::from_str::<serde_json::Value>(json_str) {
|
||||
let width = screen["width"].as_u64().unwrap_or(0) as u32;
|
||||
let height = screen["height"].as_u64().unwrap_or(0) as u32;
|
||||
return constraints.matches(width, height);
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
if filtered.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform raw sample data into a Fingerprint struct.
|
||||
fn transform_sample(
|
||||
&self,
|
||||
fp_sample: &HashMap<String, String>,
|
||||
header_sample: &HashMap<String, String>,
|
||||
options: &FingerprintOptions,
|
||||
) -> Result<FingerprintWithHeaders, FingerprintError> {
|
||||
// Parse values, handling STRINGIFIED prefix and MISSING_VALUE token
|
||||
let mut parsed: HashMap<String, serde_json::Value> = HashMap::new();
|
||||
|
||||
for (key, value) in fp_sample {
|
||||
if value == MISSING_VALUE_DATASET_TOKEN {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parsed_value = if let Some(json_str) = value.strip_prefix(STRINGIFIED_PREFIX) {
|
||||
serde_json::from_str(json_str)?
|
||||
} else {
|
||||
serde_json::Value::String(value.clone())
|
||||
};
|
||||
|
||||
parsed.insert(key.clone(), parsed_value);
|
||||
}
|
||||
|
||||
// Check if screen was generated
|
||||
let screen_value = parsed.get("screen");
|
||||
if screen_value.is_none() {
|
||||
return Err(FingerprintError::NoValidFingerprint);
|
||||
}
|
||||
|
||||
// Extract screen fingerprint
|
||||
let screen = if let Some(screen_val) = screen_value {
|
||||
serde_json::from_value(screen_val.clone()).unwrap_or_default()
|
||||
} else {
|
||||
ScreenFingerprint::default()
|
||||
};
|
||||
|
||||
// Build languages from Accept-Language header
|
||||
let accept_language = header_sample
|
||||
.get("accept-language")
|
||||
.or_else(|| header_sample.get("Accept-Language"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "en-US".to_string());
|
||||
|
||||
let languages: Vec<String> = accept_language
|
||||
.split(',')
|
||||
.map(|s| s.split(';').next().unwrap_or(s).trim().to_string())
|
||||
.collect();
|
||||
|
||||
let language = languages
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "en-US".to_string());
|
||||
|
||||
// Build navigator fingerprint
|
||||
let navigator = NavigatorFingerprint {
|
||||
user_agent: get_string(&parsed, "userAgent"),
|
||||
user_agent_data: parsed
|
||||
.get("userAgentData")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok()),
|
||||
do_not_track: parsed
|
||||
.get("doNotTrack")
|
||||
.and_then(|v| v.as_str().map(String::from)),
|
||||
app_code_name: get_string_or(&parsed, "appCodeName", "Mozilla"),
|
||||
app_name: get_string_or(&parsed, "appName", "Netscape"),
|
||||
app_version: get_string(&parsed, "appVersion"),
|
||||
oscpu: parsed
|
||||
.get("oscpu")
|
||||
.and_then(|v| v.as_str().map(String::from)),
|
||||
webdriver: parsed
|
||||
.get("webdriver")
|
||||
.and_then(|v| v.as_str().map(String::from)),
|
||||
language,
|
||||
languages,
|
||||
platform: get_string(&parsed, "platform"),
|
||||
device_memory: parsed
|
||||
.get("deviceMemory")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok()),
|
||||
hardware_concurrency: parsed
|
||||
.get("hardwareConcurrency")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(4),
|
||||
product: get_string_or(&parsed, "product", "Gecko"),
|
||||
product_sub: get_string(&parsed, "productSub"),
|
||||
vendor: get_string(&parsed, "vendor"),
|
||||
vendor_sub: get_string(&parsed, "vendorSub"),
|
||||
max_touch_points: parsed
|
||||
.get("maxTouchPoints")
|
||||
.and_then(|v| v.as_str())
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0),
|
||||
extra_properties: parsed
|
||||
.get("extraProperties")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok()),
|
||||
};
|
||||
|
||||
// Build video card (will be filled later by WebGL sampler)
|
||||
let video_card = parsed
|
||||
.get("videoCard")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Build other components
|
||||
let audio_codecs = parsed
|
||||
.get("audioCodecs")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let video_codecs = parsed
|
||||
.get("videoCodecs")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let plugins_data = parsed
|
||||
.get("pluginsData")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let battery = parsed
|
||||
.get("battery")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok());
|
||||
|
||||
let multimedia_devices = parsed
|
||||
.get("multimediaDevices")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let fonts = parsed
|
||||
.get("fonts")
|
||||
.and_then(|v| serde_json::from_value(v.clone()).ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let fingerprint = Fingerprint {
|
||||
screen,
|
||||
navigator,
|
||||
video_codecs,
|
||||
audio_codecs,
|
||||
plugins_data,
|
||||
battery,
|
||||
video_card,
|
||||
multimedia_devices,
|
||||
fonts,
|
||||
mock_web_rtc: options.mock_web_rtc,
|
||||
slim: options.slim,
|
||||
};
|
||||
|
||||
// Build headers (filter out internal nodes and missing values)
|
||||
let headers: Headers = header_sample
|
||||
.iter()
|
||||
.filter(|(k, v)| !k.starts_with('*') && *v != MISSING_VALUE_DATASET_TOKEN)
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
// Order headers
|
||||
let ordered_headers = self.order_headers(&headers, &fingerprint.navigator.user_agent);
|
||||
|
||||
Ok(FingerprintWithHeaders {
|
||||
fingerprint,
|
||||
headers: ordered_headers,
|
||||
})
|
||||
}
|
||||
|
||||
/// Order headers according to browser-specific ordering.
|
||||
fn order_headers(&self, headers: &Headers, user_agent: &str) -> Headers {
|
||||
let browser = detect_browser_from_ua(user_agent);
|
||||
let order = self.headers_order.get(browser).cloned().unwrap_or_default();
|
||||
|
||||
let mut ordered = HashMap::new();
|
||||
|
||||
// Add headers in order
|
||||
for header_name in &order {
|
||||
if let Some(value) = headers.get(header_name) {
|
||||
ordered.insert(header_name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining headers not in order
|
||||
for (key, value) in headers {
|
||||
if !order.contains(key) {
|
||||
ordered.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
ordered
|
||||
}
|
||||
}
|
||||
|
||||
fn get_string(map: &HashMap<String, serde_json::Value>, key: &str) -> String {
|
||||
map
|
||||
.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn get_string_or(map: &HashMap<String, serde_json::Value>, key: &str, default: &str) -> String {
|
||||
map
|
||||
.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
fn detect_browser_from_ua(user_agent: &str) -> &str {
|
||||
let ua_lower = user_agent.to_lowercase();
|
||||
if ua_lower.contains("firefox") {
|
||||
"firefox"
|
||||
} else if ua_lower.contains("edg/") || ua_lower.contains("edge") {
|
||||
"edge"
|
||||
} else if ua_lower.contains("chrome") {
|
||||
"chrome"
|
||||
} else if ua_lower.contains("safari") {
|
||||
"safari"
|
||||
} else {
|
||||
"chrome"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_create_generator() {
|
||||
let generator = FingerprintGenerator::new();
|
||||
assert!(
|
||||
generator.is_ok(),
|
||||
"Failed to create generator: {:?}",
|
||||
generator.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_fingerprint() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions::default();
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate fingerprint: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
if let Ok(fp) = result {
|
||||
assert!(!fp.fingerprint.navigator.user_agent.is_empty());
|
||||
assert!(fp.fingerprint.screen.width > 0);
|
||||
assert!(fp.fingerprint.screen.height > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_firefox_fingerprint() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions {
|
||||
browsers: Some(vec!["firefox".to_string()]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(result.is_ok(), "Failed to generate Firefox fingerprint");
|
||||
|
||||
if let Ok(fp) = result {
|
||||
assert!(
|
||||
fp.fingerprint
|
||||
.navigator
|
||||
.user_agent
|
||||
.to_lowercase()
|
||||
.contains("firefox"),
|
||||
"User agent should contain Firefox: {}",
|
||||
fp.fingerprint.navigator.user_agent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_with_screen_constraints() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions {
|
||||
screen: Some(ScreenConstraints {
|
||||
min_width: Some(1900),
|
||||
max_width: Some(1920),
|
||||
min_height: Some(1000),
|
||||
max_height: Some(1100),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to generate fingerprint with screen constraints"
|
||||
);
|
||||
|
||||
if let Ok(fp) = result {
|
||||
assert!(
|
||||
fp.fingerprint.screen.width >= 1900 && fp.fingerprint.screen.width <= 1920,
|
||||
"Screen width {} should be between 1900 and 1920",
|
||||
fp.fingerprint.screen.width
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_browser_http_info_parse() {
|
||||
let info = BrowserHttpInfo::parse("chrome/143.0.0.0|2");
|
||||
assert!(info.is_some());
|
||||
let info = info.unwrap();
|
||||
assert_eq!(info.name, "chrome");
|
||||
assert_eq!(info.major_version(), 143);
|
||||
assert_eq!(info.http_version, "2");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
//! Fingerprint type definitions.
|
||||
//!
|
||||
//! These types represent browser fingerprints that can be injected into Camoufox.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A complete browser fingerprint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Fingerprint {
|
||||
pub screen: ScreenFingerprint,
|
||||
pub navigator: NavigatorFingerprint,
|
||||
#[serde(default)]
|
||||
pub video_codecs: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub audio_codecs: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub plugins_data: HashMap<String, String>,
|
||||
#[serde(default)]
|
||||
pub battery: Option<BatteryFingerprint>,
|
||||
pub video_card: VideoCard,
|
||||
#[serde(default)]
|
||||
pub multimedia_devices: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub fonts: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mock_web_rtc: bool,
|
||||
#[serde(default)]
|
||||
pub slim: bool,
|
||||
}
|
||||
|
||||
/// Screen-related fingerprint properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ScreenFingerprint {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub avail_width: u32,
|
||||
pub avail_height: u32,
|
||||
#[serde(default)]
|
||||
pub avail_top: u32,
|
||||
#[serde(default)]
|
||||
pub avail_left: u32,
|
||||
pub color_depth: u32,
|
||||
pub pixel_depth: u32,
|
||||
#[serde(default = "default_device_pixel_ratio")]
|
||||
pub device_pixel_ratio: f64,
|
||||
#[serde(default)]
|
||||
pub page_x_offset: f64,
|
||||
#[serde(default)]
|
||||
pub page_y_offset: f64,
|
||||
pub inner_width: u32,
|
||||
pub inner_height: u32,
|
||||
pub outer_width: u32,
|
||||
pub outer_height: u32,
|
||||
#[serde(default)]
|
||||
pub screen_x: i32,
|
||||
#[serde(default)]
|
||||
pub screen_y: i32,
|
||||
#[serde(default)]
|
||||
pub client_width: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub client_height: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub has_hdr: bool,
|
||||
}
|
||||
|
||||
fn default_device_pixel_ratio() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Brand information for User-Agent Client Hints.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Brand {
|
||||
pub brand: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
/// User-Agent Client Hints data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UserAgentData {
|
||||
#[serde(default)]
|
||||
pub brands: Vec<Brand>,
|
||||
#[serde(default)]
|
||||
pub mobile: bool,
|
||||
#[serde(default)]
|
||||
pub platform: String,
|
||||
#[serde(default)]
|
||||
pub architecture: String,
|
||||
#[serde(default)]
|
||||
pub bitness: String,
|
||||
#[serde(default)]
|
||||
pub full_version_list: Vec<Brand>,
|
||||
#[serde(default)]
|
||||
pub model: String,
|
||||
#[serde(default)]
|
||||
pub platform_version: String,
|
||||
#[serde(default)]
|
||||
pub ua_full_version: String,
|
||||
}
|
||||
|
||||
/// Extra navigator properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExtraProperties {
|
||||
#[serde(default)]
|
||||
pub vendor_flavors: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub is_bluetooth_supported: bool,
|
||||
#[serde(default)]
|
||||
pub global_privacy_control: Option<bool>,
|
||||
#[serde(default = "default_pdf_viewer_enabled")]
|
||||
pub pdf_viewer_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub installed_apps: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn default_pdf_viewer_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Navigator-related fingerprint properties.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NavigatorFingerprint {
|
||||
pub user_agent: String,
|
||||
#[serde(default)]
|
||||
pub user_agent_data: Option<UserAgentData>,
|
||||
#[serde(default)]
|
||||
pub do_not_track: Option<String>,
|
||||
#[serde(default = "default_app_code_name")]
|
||||
pub app_code_name: String,
|
||||
#[serde(default = "default_app_name")]
|
||||
pub app_name: String,
|
||||
#[serde(default)]
|
||||
pub app_version: String,
|
||||
#[serde(default)]
|
||||
pub oscpu: Option<String>,
|
||||
#[serde(default)]
|
||||
pub webdriver: Option<String>,
|
||||
pub language: String,
|
||||
pub languages: Vec<String>,
|
||||
pub platform: String,
|
||||
#[serde(default)]
|
||||
pub device_memory: Option<u32>,
|
||||
pub hardware_concurrency: u32,
|
||||
#[serde(default = "default_product")]
|
||||
pub product: String,
|
||||
#[serde(default)]
|
||||
pub product_sub: String,
|
||||
#[serde(default)]
|
||||
pub vendor: String,
|
||||
#[serde(default)]
|
||||
pub vendor_sub: String,
|
||||
#[serde(default)]
|
||||
pub max_touch_points: u32,
|
||||
#[serde(default)]
|
||||
pub extra_properties: Option<ExtraProperties>,
|
||||
}
|
||||
|
||||
fn default_app_code_name() -> String {
|
||||
"Mozilla".to_string()
|
||||
}
|
||||
|
||||
fn default_app_name() -> String {
|
||||
"Netscape".to_string()
|
||||
}
|
||||
|
||||
fn default_product() -> String {
|
||||
"Gecko".to_string()
|
||||
}
|
||||
|
||||
/// WebGL video card information.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct VideoCard {
|
||||
pub vendor: String,
|
||||
pub renderer: String,
|
||||
}
|
||||
|
||||
/// Battery status fingerprint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BatteryFingerprint {
|
||||
pub charging: bool,
|
||||
pub charging_time: f64,
|
||||
pub discharging_time: f64,
|
||||
pub level: f64,
|
||||
}
|
||||
|
||||
/// HTTP headers for a fingerprint.
|
||||
pub type Headers = HashMap<String, String>;
|
||||
|
||||
/// A fingerprint combined with matching HTTP headers.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FingerprintWithHeaders {
|
||||
pub fingerprint: Fingerprint,
|
||||
pub headers: Headers,
|
||||
}
|
||||
|
||||
/// Options for generating fingerprints.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FingerprintOptions {
|
||||
/// Target operating system: "windows", "macos", "linux"
|
||||
pub operating_system: Option<String>,
|
||||
/// Target browser: "firefox", "chrome", "safari", "edge"
|
||||
pub browsers: Option<Vec<String>>,
|
||||
/// Target device type: "desktop", "mobile"
|
||||
pub devices: Option<Vec<String>>,
|
||||
/// Locales for Accept-Language header
|
||||
pub locales: Option<Vec<String>>,
|
||||
/// HTTP version: "1" or "2"
|
||||
pub http_version: Option<String>,
|
||||
/// Screen dimension constraints
|
||||
pub screen: Option<ScreenConstraints>,
|
||||
/// Whether to mock WebRTC
|
||||
pub mock_web_rtc: bool,
|
||||
/// Slim mode (fewer evasions)
|
||||
pub slim: bool,
|
||||
}
|
||||
|
||||
/// Constraints for screen dimensions.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ScreenConstraints {
|
||||
pub min_width: Option<u32>,
|
||||
pub max_width: Option<u32>,
|
||||
pub min_height: Option<u32>,
|
||||
pub max_height: Option<u32>,
|
||||
}
|
||||
|
||||
impl ScreenConstraints {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_min_width(mut self, width: u32) -> Self {
|
||||
self.min_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_width(mut self, width: u32) -> Self {
|
||||
self.max_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_min_height(mut self, height: u32) -> Self {
|
||||
self.min_height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_height(mut self, height: u32) -> Self {
|
||||
self.max_height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if a screen size matches these constraints.
|
||||
pub fn matches(&self, width: u32, height: u32) -> bool {
|
||||
if let Some(min_w) = self.min_width {
|
||||
if width < min_w {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(max_w) = self.max_width {
|
||||
if width > max_w {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(min_h) = self.min_height {
|
||||
if height < min_h {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(max_h) = self.max_height {
|
||||
if height > max_h {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants used in fingerprint generation.
|
||||
pub const MISSING_VALUE_DATASET_TOKEN: &str = "*MISSING_VALUE*";
|
||||
pub const STRINGIFIED_PREFIX: &str = "*STRINGIFIED*";
|
||||
|
||||
/// Special node names in the Bayesian networks.
|
||||
pub const BROWSER_HTTP_NODE_NAME: &str = "*BROWSER_HTTP";
|
||||
pub const OPERATING_SYSTEM_NODE_NAME: &str = "*OPERATING_SYSTEM";
|
||||
pub const DEVICE_NODE_NAME: &str = "*DEVICE";
|
||||
|
||||
/// Supported browsers.
|
||||
pub const SUPPORTED_BROWSERS: &[&str] = &["chrome", "firefox", "safari", "edge"];
|
||||
|
||||
/// Supported operating systems.
|
||||
pub const SUPPORTED_OPERATING_SYSTEMS: &[&str] = &["windows", "macos", "linux", "android", "ios"];
|
||||
|
||||
/// Supported devices.
|
||||
pub const SUPPORTED_DEVICES: &[&str] = &["desktop", "mobile"];
|
||||
|
||||
/// Supported HTTP versions.
|
||||
pub const SUPPORTED_HTTP_VERSIONS: &[&str] = &["1", "2"];
|
||||
@@ -0,0 +1,83 @@
|
||||
//! OS-specific font lists for Camoufox.
|
||||
//!
|
||||
//! Provides default system fonts for Windows, macOS, and Linux.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
/// Get fonts for the target OS.
|
||||
pub fn get_fonts_for_os(target_os: &str) -> Vec<String> {
|
||||
let fonts_map: HashMap<String, Vec<String>> =
|
||||
serde_json::from_str(data::FONTS_JSON).unwrap_or_default();
|
||||
|
||||
let os_key = match target_os {
|
||||
"win" | "windows" => "win",
|
||||
"mac" | "macos" => "mac",
|
||||
"lin" | "linux" => "lin",
|
||||
_ => "win", // Default to Windows fonts
|
||||
};
|
||||
|
||||
fonts_map.get(os_key).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get fonts for the target OS with additional custom fonts.
|
||||
pub fn get_fonts_with_custom(target_os: &str, custom_fonts: Option<&[String]>) -> Vec<String> {
|
||||
let mut fonts = get_fonts_for_os(target_os);
|
||||
|
||||
if let Some(custom) = custom_fonts {
|
||||
// Add custom fonts, avoiding duplicates
|
||||
for font in custom {
|
||||
if !fonts.contains(font) {
|
||||
fonts.push(font.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fonts
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_for_windows() {
|
||||
let fonts = get_fonts_for_os("win");
|
||||
assert!(!fonts.is_empty());
|
||||
assert!(fonts.contains(&"Arial".to_string()));
|
||||
assert!(fonts.contains(&"Calibri".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_for_macos() {
|
||||
let fonts = get_fonts_for_os("mac");
|
||||
assert!(!fonts.is_empty());
|
||||
assert!(fonts.contains(&"Helvetica".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_for_linux() {
|
||||
let fonts = get_fonts_for_os("lin");
|
||||
assert!(!fonts.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_fonts_with_custom() {
|
||||
let custom = vec!["MyCustomFont".to_string()];
|
||||
let fonts = get_fonts_with_custom("win", Some(&custom));
|
||||
|
||||
assert!(fonts.contains(&"MyCustomFont".to_string()));
|
||||
assert!(fonts.contains(&"Arial".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fonts_no_duplicates() {
|
||||
let custom = vec!["Arial".to_string()]; // Arial already exists in Windows fonts
|
||||
let fonts = get_fonts_with_custom("win", Some(&custom));
|
||||
|
||||
// Count occurrences of Arial
|
||||
let arial_count = fonts.iter().filter(|f| *f == "Arial").count();
|
||||
assert_eq!(arial_count, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
//! Geolocation support for Camoufox fingerprinting.
|
||||
//!
|
||||
//! This module provides IP-based geolocation lookup using the MaxMind GeoLite2 database,
|
||||
//! and locale generation based on country/territory information.
|
||||
|
||||
use crate::camoufox::data;
|
||||
use crate::geoip_downloader::GeoIPDownloader;
|
||||
use directories::BaseDirs;
|
||||
use maxminddb::{geoip2, Reader};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::Reader as XmlReader;
|
||||
use rand::Rng;
|
||||
use std::collections::HashMap;
|
||||
use std::net::IpAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Geolocation error type.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum GeolocationError {
|
||||
#[error("GeoIP database not found. Please download it first.")]
|
||||
DatabaseNotFound,
|
||||
|
||||
#[error("Failed to open GeoIP database: {0}")]
|
||||
DatabaseOpen(String),
|
||||
|
||||
#[error("Invalid IP address: {0}")]
|
||||
InvalidIP(String),
|
||||
|
||||
#[error("IP location not found: {0}")]
|
||||
LocationNotFound(String),
|
||||
|
||||
#[error("Unknown territory: {0}")]
|
||||
UnknownTerritory(String),
|
||||
|
||||
#[error("No language data for territory: {0}")]
|
||||
NoLanguageData(String),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
}
|
||||
|
||||
/// Locale information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Locale {
|
||||
pub language: String,
|
||||
pub region: Option<String>,
|
||||
pub script: Option<String>,
|
||||
}
|
||||
|
||||
impl Locale {
|
||||
/// Format locale as a string (e.g., "en-US").
|
||||
pub fn as_string(&self) -> String {
|
||||
if let Some(region) = &self.region {
|
||||
format!("{}-{}", self.language, region)
|
||||
} else {
|
||||
self.language.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to config format for Camoufox.
|
||||
pub fn as_config(&self) -> HashMap<String, serde_json::Value> {
|
||||
let mut config = HashMap::new();
|
||||
|
||||
if let Some(region) = &self.region {
|
||||
config.insert(
|
||||
"locale:region".to_string(),
|
||||
serde_json::json!(region.to_uppercase()),
|
||||
);
|
||||
}
|
||||
|
||||
config.insert(
|
||||
"locale:language".to_string(),
|
||||
serde_json::json!(self.language.clone()),
|
||||
);
|
||||
|
||||
if let Some(script) = &self.script {
|
||||
config.insert("locale:script".to_string(), serde_json::json!(script));
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Geolocation information.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Geolocation {
|
||||
pub locale: Locale,
|
||||
pub longitude: f64,
|
||||
pub latitude: f64,
|
||||
pub timezone: String,
|
||||
pub accuracy: Option<f64>,
|
||||
}
|
||||
|
||||
impl Geolocation {
|
||||
/// Convert to config format for Camoufox.
|
||||
pub fn as_config(&self) -> HashMap<String, serde_json::Value> {
|
||||
let mut config = self.locale.as_config();
|
||||
|
||||
config.insert(
|
||||
"geolocation:longitude".to_string(),
|
||||
serde_json::json!(self.longitude),
|
||||
);
|
||||
config.insert(
|
||||
"geolocation:latitude".to_string(),
|
||||
serde_json::json!(self.latitude),
|
||||
);
|
||||
config.insert("timezone".to_string(), serde_json::json!(self.timezone));
|
||||
|
||||
if let Some(accuracy) = self.accuracy {
|
||||
config.insert(
|
||||
"geolocation:accuracy".to_string(),
|
||||
serde_json::json!(accuracy),
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
/// Territory language population data.
|
||||
struct LanguagePopulation {
|
||||
language: String,
|
||||
population_percent: f64,
|
||||
}
|
||||
|
||||
/// Statistical locale selector based on territory language populations.
|
||||
pub struct LocaleSelector {
|
||||
territories: HashMap<String, Vec<LanguagePopulation>>,
|
||||
}
|
||||
|
||||
impl LocaleSelector {
|
||||
/// Create a new locale selector by parsing territory info XML.
|
||||
pub fn new() -> Result<Self, GeolocationError> {
|
||||
let mut territories: HashMap<String, Vec<LanguagePopulation>> = HashMap::new();
|
||||
|
||||
let mut reader = XmlReader::from_str(data::TERRITORY_INFO_XML);
|
||||
reader.config_mut().trim_text(true);
|
||||
|
||||
let mut current_territory: Option<String> = None;
|
||||
let mut current_languages: Vec<LanguagePopulation> = Vec::new();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
if name_str == "territory" {
|
||||
// Save previous territory if exists
|
||||
if let Some(code) = current_territory.take() {
|
||||
if !current_languages.is_empty() {
|
||||
territories.insert(code, std::mem::take(&mut current_languages));
|
||||
}
|
||||
}
|
||||
|
||||
// Get territory type attribute
|
||||
for attr in e.attributes().flatten() {
|
||||
if attr.key.as_ref() == b"type" {
|
||||
current_territory = Some(String::from_utf8_lossy(&attr.value).to_uppercase());
|
||||
}
|
||||
}
|
||||
} else if name_str == "languagePopulation" && current_territory.is_some() {
|
||||
let mut lang_type = None;
|
||||
let mut pop_percent = 0.0;
|
||||
|
||||
for attr in e.attributes().flatten() {
|
||||
match attr.key.as_ref() {
|
||||
b"type" => {
|
||||
lang_type = Some(String::from_utf8_lossy(&attr.value).to_string());
|
||||
}
|
||||
b"populationPercent" => {
|
||||
pop_percent = String::from_utf8_lossy(&attr.value).parse().unwrap_or(0.0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(lang) = lang_type {
|
||||
current_languages.push(LanguagePopulation {
|
||||
language: lang.replace('_', "-"),
|
||||
population_percent: pop_percent,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
let name_ref = e.name();
|
||||
let name = std::str::from_utf8(name_ref.as_ref()).unwrap_or("");
|
||||
if name == "territory" {
|
||||
// Save territory
|
||||
if let Some(code) = current_territory.take() {
|
||||
if !current_languages.is_empty() {
|
||||
territories.insert(code, std::mem::take(&mut current_languages));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => {
|
||||
log::warn!("Error parsing territory XML: {}", e);
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
Ok(Self { territories })
|
||||
}
|
||||
|
||||
/// Get a locale for a given region/country code.
|
||||
pub fn from_region(&self, region: &str) -> Result<Locale, GeolocationError> {
|
||||
let region_upper = region.to_uppercase();
|
||||
|
||||
let languages = self
|
||||
.territories
|
||||
.get(®ion_upper)
|
||||
.ok_or_else(|| GeolocationError::UnknownTerritory(region.to_string()))?;
|
||||
|
||||
if languages.is_empty() {
|
||||
return Err(GeolocationError::NoLanguageData(region.to_string()));
|
||||
}
|
||||
|
||||
// Weighted random selection based on population percentage
|
||||
let total: f64 = languages.iter().map(|l| l.population_percent).sum();
|
||||
let mut rng = rand::rng();
|
||||
let target = rng.random::<f64>() * total;
|
||||
let mut cumulative = 0.0;
|
||||
|
||||
for lang in languages {
|
||||
cumulative += lang.population_percent;
|
||||
if cumulative >= target {
|
||||
return Ok(normalize_locale(&format!(
|
||||
"{}-{}",
|
||||
lang.language, region_upper
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first language
|
||||
let first_lang = &languages[0].language;
|
||||
Ok(normalize_locale(&format!(
|
||||
"{}-{}",
|
||||
first_lang, region_upper
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LocaleSelector {
|
||||
fn default() -> Self {
|
||||
Self::new().unwrap_or(Self {
|
||||
territories: HashMap::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Normalize a locale string to standard format.
|
||||
fn normalize_locale(locale: &str) -> Locale {
|
||||
let parts: Vec<&str> = locale.split('-').collect();
|
||||
|
||||
let language = parts
|
||||
.first()
|
||||
.map(|s| s.to_lowercase())
|
||||
.unwrap_or_else(|| "en".to_string());
|
||||
|
||||
let region = parts.get(1).map(|s| s.to_uppercase());
|
||||
|
||||
// Determine script based on language if needed
|
||||
let script = match language.as_str() {
|
||||
"zh" => {
|
||||
// Chinese - Traditional for TW/HK, Simplified otherwise
|
||||
if region.as_deref() == Some("TW") || region.as_deref() == Some("HK") {
|
||||
Some("Hant".to_string())
|
||||
} else {
|
||||
Some("Hans".to_string())
|
||||
}
|
||||
}
|
||||
"sr" => {
|
||||
// Serbian - can be Cyrillic or Latin
|
||||
Some("Cyrl".to_string())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Locale {
|
||||
language,
|
||||
region,
|
||||
script,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to the GeoIP MMDB file.
|
||||
fn get_mmdb_path() -> Result<PathBuf, GeolocationError> {
|
||||
let base_dirs = BaseDirs::new().ok_or(GeolocationError::DatabaseNotFound)?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let cache_dir = base_dirs
|
||||
.data_local_dir()
|
||||
.join("camoufox")
|
||||
.join("camoufox")
|
||||
.join("Cache");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
let cache_dir = base_dirs.cache_dir().join("camoufox");
|
||||
|
||||
Ok(cache_dir.join("GeoLite2-City.mmdb"))
|
||||
}
|
||||
|
||||
/// Check if the GeoIP database is available.
|
||||
pub fn is_geoip_available() -> bool {
|
||||
GeoIPDownloader::is_geoip_database_available()
|
||||
}
|
||||
|
||||
/// Get geolocation information for an IP address.
|
||||
pub fn get_geolocation(ip: &str) -> Result<Geolocation, GeolocationError> {
|
||||
let mmdb_path = get_mmdb_path()?;
|
||||
|
||||
if !mmdb_path.exists() {
|
||||
return Err(GeolocationError::DatabaseNotFound);
|
||||
}
|
||||
|
||||
let reader =
|
||||
Reader::open_readfile(&mmdb_path).map_err(|e| GeolocationError::DatabaseOpen(e.to_string()))?;
|
||||
|
||||
let ip_addr: IpAddr =
|
||||
IpAddr::from_str(ip).map_err(|_| GeolocationError::InvalidIP(ip.to_string()))?;
|
||||
|
||||
let city: geoip2::City = reader
|
||||
.lookup(ip_addr)
|
||||
.map_err(|e| GeolocationError::LocationNotFound(e.to_string()))?;
|
||||
|
||||
// Extract location data
|
||||
let location = city
|
||||
.location
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound(ip.to_string()))?;
|
||||
|
||||
let longitude = location
|
||||
.longitude
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No longitude".to_string()))?;
|
||||
let latitude = location
|
||||
.latitude
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No latitude".to_string()))?;
|
||||
let timezone = location
|
||||
.time_zone
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No timezone".to_string()))?
|
||||
.to_string();
|
||||
|
||||
// Get country code
|
||||
let country = city
|
||||
.country
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No country".to_string()))?;
|
||||
let iso_code = country
|
||||
.iso_code
|
||||
.ok_or_else(|| GeolocationError::LocationNotFound("No country code".to_string()))?
|
||||
.to_uppercase();
|
||||
|
||||
// Get locale from territory data
|
||||
let selector = LocaleSelector::new()?;
|
||||
let locale = selector.from_region(&iso_code)?;
|
||||
|
||||
Ok(Geolocation {
|
||||
locale,
|
||||
longitude,
|
||||
latitude,
|
||||
timezone,
|
||||
accuracy: location.accuracy_radius.map(|r| r as f64),
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate an IP address (IPv4 or IPv6).
|
||||
pub fn validate_ip(ip: &str) -> bool {
|
||||
IpAddr::from_str(ip).is_ok()
|
||||
}
|
||||
|
||||
/// Check if an IP is IPv4.
|
||||
pub fn is_ipv4(ip: &str) -> bool {
|
||||
if let Ok(addr) = IpAddr::from_str(ip) {
|
||||
addr.is_ipv4()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an IP is IPv6.
|
||||
pub fn is_ipv6(ip: &str) -> bool {
|
||||
if let Ok(addr) = IpAddr::from_str(ip) {
|
||||
addr.is_ipv6()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch public IP address, optionally through a proxy.
|
||||
pub async fn fetch_public_ip(proxy: Option<&str>) -> Result<String, GeolocationError> {
|
||||
let urls = [
|
||||
"https://api.ipify.org",
|
||||
"https://checkip.amazonaws.com",
|
||||
"https://ipinfo.io/ip",
|
||||
"https://icanhazip.com",
|
||||
"https://ifconfig.co/ip",
|
||||
"https://ipecho.net/plain",
|
||||
];
|
||||
|
||||
let client_builder = reqwest::Client::builder().timeout(std::time::Duration::from_secs(5));
|
||||
|
||||
let client = if let Some(proxy_url) = proxy {
|
||||
let proxy = reqwest::Proxy::all(proxy_url)
|
||||
.map_err(|e| GeolocationError::Network(format!("Invalid proxy: {}", e)))?;
|
||||
client_builder
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
.map_err(|e| GeolocationError::Network(e.to_string()))?
|
||||
} else {
|
||||
client_builder
|
||||
.build()
|
||||
.map_err(|e| GeolocationError::Network(e.to_string()))?
|
||||
};
|
||||
|
||||
let mut last_error = None;
|
||||
|
||||
for url in &urls {
|
||||
match client.get(*url).send().await {
|
||||
Ok(response) if response.status().is_success() => match response.text().await {
|
||||
Ok(text) => {
|
||||
let ip = text.trim().to_string();
|
||||
if validate_ip(&ip) {
|
||||
return Ok(ip);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = Some(format!("Failed to read response from {}: {}", url, e));
|
||||
}
|
||||
},
|
||||
Ok(response) => {
|
||||
last_error = Some(format!("HTTP {} from {}", response.status(), url));
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = Some(format!("Request to {} failed: {}", url, e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(GeolocationError::Network(last_error.unwrap_or_else(|| {
|
||||
"Failed to fetch public IP from any endpoint".to_string()
|
||||
})))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_locale_selector_creation() {
|
||||
let selector = LocaleSelector::new();
|
||||
assert!(selector.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locale_from_region() {
|
||||
let selector = LocaleSelector::new().unwrap();
|
||||
|
||||
// Test common regions
|
||||
let us_locale = selector.from_region("US");
|
||||
assert!(us_locale.is_ok());
|
||||
let us = us_locale.unwrap();
|
||||
assert_eq!(us.region, Some("US".to_string()));
|
||||
|
||||
let de_locale = selector.from_region("DE");
|
||||
assert!(de_locale.is_ok());
|
||||
let de = de_locale.unwrap();
|
||||
assert_eq!(de.region, Some("DE".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_locale_as_string() {
|
||||
let locale = Locale {
|
||||
language: "en".to_string(),
|
||||
region: Some("US".to_string()),
|
||||
script: None,
|
||||
};
|
||||
assert_eq!(locale.as_string(), "en-US");
|
||||
|
||||
let locale_no_region = Locale {
|
||||
language: "en".to_string(),
|
||||
region: None,
|
||||
script: None,
|
||||
};
|
||||
assert_eq!(locale_no_region.as_string(), "en");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_ip() {
|
||||
assert!(validate_ip("8.8.8.8"));
|
||||
assert!(validate_ip("192.168.1.1"));
|
||||
assert!(validate_ip("2001:4860:4860::8888"));
|
||||
assert!(!validate_ip("invalid"));
|
||||
assert!(!validate_ip("256.256.256.256"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_ipv4() {
|
||||
assert!(is_ipv4("8.8.8.8"));
|
||||
assert!(!is_ipv4("2001:4860:4860::8888"));
|
||||
assert!(!is_ipv4("invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_ipv6() {
|
||||
assert!(is_ipv6("2001:4860:4860::8888"));
|
||||
assert!(!is_ipv6("8.8.8.8"));
|
||||
assert!(!is_ipv6("invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_locale() {
|
||||
let locale = normalize_locale("en-US");
|
||||
assert_eq!(locale.language, "en");
|
||||
assert_eq!(locale.region, Some("US".to_string()));
|
||||
assert!(locale.script.is_none());
|
||||
|
||||
let zh_tw = normalize_locale("zh-TW");
|
||||
assert_eq!(zh_tw.language, "zh");
|
||||
assert_eq!(zh_tw.region, Some("TW".to_string()));
|
||||
assert_eq!(zh_tw.script, Some("Hant".to_string()));
|
||||
|
||||
let zh_cn = normalize_locale("zh-CN");
|
||||
assert_eq!(zh_cn.script, Some("Hans".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
//! Camoufox browser launcher using playwright-rust.
|
||||
//!
|
||||
//! Provides functionality to launch Camoufox browser instances with fingerprint injection.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use playwright::api::{Browser, BrowserContext, Playwright, ProxySettings};
|
||||
use playwright::Error as PlaywrightError;
|
||||
|
||||
use crate::camoufox::config::{CamoufoxConfigBuilder, CamoufoxLaunchConfig, ProxyConfig};
|
||||
use crate::camoufox::fingerprint::types::{Fingerprint, ScreenConstraints};
|
||||
|
||||
/// Camoufox launcher for creating browser instances.
|
||||
pub struct CamoufoxLauncher {
|
||||
playwright: Arc<Playwright>,
|
||||
executable_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Error type for launcher operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LauncherError {
|
||||
#[error("Playwright error: {0}")]
|
||||
Playwright(PlaywrightError),
|
||||
|
||||
#[error("Playwright Arc error: {0}")]
|
||||
PlaywrightArc(#[from] Arc<PlaywrightError>),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(#[from] crate::camoufox::config::ConfigError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Camoufox executable not found at: {0}")]
|
||||
ExecutableNotFound(PathBuf),
|
||||
|
||||
#[error("Failed to generate environment variables: {0}")]
|
||||
EnvVars(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
/// Options for launching Camoufox.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LaunchOptions {
|
||||
/// Operating system to spoof: "windows", "macos", "linux"
|
||||
pub os: Option<String>,
|
||||
/// Block all images
|
||||
pub block_images: bool,
|
||||
/// Block WebRTC entirely
|
||||
pub block_webrtc: bool,
|
||||
/// Block WebGL (not recommended unless necessary)
|
||||
pub block_webgl: bool,
|
||||
/// Screen dimension constraints
|
||||
pub screen: Option<ScreenConstraints>,
|
||||
/// Fixed window size [width, height]
|
||||
pub window: Option<(u32, u32)>,
|
||||
/// Custom fingerprint (if not provided, one will be generated)
|
||||
pub fingerprint: Option<Fingerprint>,
|
||||
/// Run in headless mode
|
||||
pub headless: bool,
|
||||
/// Custom fonts to load
|
||||
pub fonts: Option<Vec<String>>,
|
||||
/// Only use custom fonts (disable OS fonts)
|
||||
pub custom_fonts_only: bool,
|
||||
/// Firefox user preferences
|
||||
pub firefox_user_prefs: Option<HashMap<String, serde_json::Value>>,
|
||||
/// Proxy configuration
|
||||
pub proxy: Option<ProxyConfig>,
|
||||
/// Additional browser arguments
|
||||
pub args: Option<Vec<String>>,
|
||||
/// Additional environment variables
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Profile/user data directory
|
||||
pub user_data_dir: Option<PathBuf>,
|
||||
/// Enable debug output
|
||||
pub debug: bool,
|
||||
}
|
||||
|
||||
impl CamoufoxLauncher {
|
||||
/// Create a new Camoufox launcher.
|
||||
pub async fn new(executable_path: impl AsRef<Path>) -> Result<Self, LauncherError> {
|
||||
let executable_path = executable_path.as_ref().to_path_buf();
|
||||
|
||||
if !executable_path.exists() {
|
||||
return Err(LauncherError::ExecutableNotFound(executable_path));
|
||||
}
|
||||
|
||||
let playwright = Playwright::initialize()
|
||||
.await
|
||||
.map_err(LauncherError::Playwright)?;
|
||||
|
||||
Ok(Self {
|
||||
playwright: Arc::new(playwright),
|
||||
executable_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Launch a new Camoufox browser instance.
|
||||
pub async fn launch(&self, options: LaunchOptions) -> Result<Browser, LauncherError> {
|
||||
let config = self.build_config(&options)?;
|
||||
|
||||
if options.debug {
|
||||
log::debug!("Camoufox config: {:?}", config.fingerprint_config);
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
let env_vars = config.get_env_vars()?;
|
||||
|
||||
// Build launch arguments
|
||||
let mut args = options.args.clone().unwrap_or_default();
|
||||
|
||||
// Add headless flag if needed
|
||||
if options.headless {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
// Merge environment variables
|
||||
let mut env = options.env.clone().unwrap_or_default();
|
||||
for (key, value) in env_vars {
|
||||
env.insert(key, value);
|
||||
}
|
||||
|
||||
// Handle fontconfig on Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(fontconfig_path) =
|
||||
crate::camoufox::env_vars::get_fontconfig_env(&config.target_os, &self.executable_path)
|
||||
{
|
||||
env.insert("FONTCONFIG_PATH".to_string(), fontconfig_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Build Firefox user prefs
|
||||
let mut firefox_prefs = config.firefox_prefs.clone();
|
||||
if let Some(user_prefs) = options.firefox_user_prefs {
|
||||
for (key, value) in user_prefs {
|
||||
firefox_prefs.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the Firefox browser type
|
||||
let firefox = self.playwright.firefox();
|
||||
|
||||
// Build launch options
|
||||
let mut launch_options = firefox.launcher();
|
||||
launch_options = launch_options.executable(&self.executable_path);
|
||||
launch_options = launch_options.headless(options.headless);
|
||||
|
||||
// Add args
|
||||
if !args.is_empty() {
|
||||
launch_options = launch_options.args(&args);
|
||||
}
|
||||
|
||||
// Add environment as serde_json::Map
|
||||
if !env.is_empty() {
|
||||
let env_map: serde_json::Map<String, serde_json::Value> = env
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, serde_json::Value::String(v)))
|
||||
.collect();
|
||||
launch_options = launch_options.env(env_map);
|
||||
}
|
||||
|
||||
// Add proxy if configured
|
||||
if let Some(proxy) = &config.proxy {
|
||||
let proxy_settings = ProxySettings {
|
||||
server: proxy.server.clone(),
|
||||
username: proxy.username.clone(),
|
||||
password: proxy.password.clone(),
|
||||
bypass: proxy.bypass.clone(),
|
||||
};
|
||||
launch_options = launch_options.proxy(proxy_settings);
|
||||
}
|
||||
|
||||
// Add Firefox preferences
|
||||
if !firefox_prefs.is_empty() {
|
||||
let prefs_map: serde_json::Map<String, serde_json::Value> =
|
||||
firefox_prefs.into_iter().collect();
|
||||
launch_options = launch_options.firefox_user_prefs(prefs_map);
|
||||
}
|
||||
|
||||
// Launch the browser
|
||||
let browser = launch_options.launch().await?;
|
||||
|
||||
Ok(browser)
|
||||
}
|
||||
|
||||
/// Launch a persistent browser context.
|
||||
pub async fn launch_persistent_context(
|
||||
&self,
|
||||
user_data_dir: impl AsRef<Path>,
|
||||
options: LaunchOptions,
|
||||
) -> Result<BrowserContext, LauncherError> {
|
||||
let config = self.build_config(&options)?;
|
||||
|
||||
if options.debug {
|
||||
log::debug!("Camoufox config: {:?}", config.fingerprint_config);
|
||||
}
|
||||
|
||||
// Get environment variables
|
||||
let env_vars = config.get_env_vars()?;
|
||||
|
||||
// Build launch arguments
|
||||
let mut args = options.args.clone().unwrap_or_default();
|
||||
|
||||
if options.headless {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
// Merge environment variables
|
||||
let mut env = options.env.clone().unwrap_or_default();
|
||||
for (key, value) in env_vars {
|
||||
env.insert(key, value);
|
||||
}
|
||||
|
||||
// Handle fontconfig on Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
if let Some(fontconfig_path) =
|
||||
crate::camoufox::env_vars::get_fontconfig_env(&config.target_os, &self.executable_path)
|
||||
{
|
||||
env.insert("FONTCONFIG_PATH".to_string(), fontconfig_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Build Firefox user prefs
|
||||
let mut firefox_prefs = config.firefox_prefs.clone();
|
||||
if let Some(user_prefs) = options.firefox_user_prefs {
|
||||
for (key, value) in user_prefs {
|
||||
firefox_prefs.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the Firefox browser type
|
||||
let firefox = self.playwright.firefox();
|
||||
|
||||
// Build persistent context options
|
||||
let mut context_options = firefox.persistent_context_launcher(user_data_dir.as_ref());
|
||||
context_options = context_options.executable(&self.executable_path);
|
||||
context_options = context_options.headless(options.headless);
|
||||
|
||||
// Add args
|
||||
if !args.is_empty() {
|
||||
context_options = context_options.args(&args);
|
||||
}
|
||||
|
||||
// Add environment as serde_json::Map
|
||||
if !env.is_empty() {
|
||||
let env_map: serde_json::Map<String, serde_json::Value> = env
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, serde_json::Value::String(v)))
|
||||
.collect();
|
||||
context_options = context_options.env(env_map);
|
||||
}
|
||||
|
||||
// Add proxy if configured
|
||||
if let Some(proxy) = &config.proxy {
|
||||
let proxy_settings = ProxySettings {
|
||||
server: proxy.server.clone(),
|
||||
username: proxy.username.clone(),
|
||||
password: proxy.password.clone(),
|
||||
bypass: proxy.bypass.clone(),
|
||||
};
|
||||
context_options = context_options.proxy(proxy_settings);
|
||||
}
|
||||
|
||||
// Note: PersistentContextLauncher doesn't support firefox_user_prefs
|
||||
// Firefox preferences should be set via about:config or prefs.js in the profile
|
||||
|
||||
// Launch the persistent context
|
||||
let context = context_options.launch().await?;
|
||||
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
/// Build Camoufox configuration from launch options.
|
||||
fn build_config(&self, options: &LaunchOptions) -> Result<CamoufoxLaunchConfig, LauncherError> {
|
||||
let mut builder = CamoufoxConfigBuilder::new();
|
||||
|
||||
if let Some(os) = &options.os {
|
||||
builder = builder.operating_system(os);
|
||||
}
|
||||
|
||||
if let Some(screen) = &options.screen {
|
||||
builder = builder.screen_constraints(screen.clone());
|
||||
}
|
||||
|
||||
if let Some(fingerprint) = &options.fingerprint {
|
||||
builder = builder.fingerprint(fingerprint.clone());
|
||||
}
|
||||
|
||||
builder = builder.block_images(options.block_images);
|
||||
builder = builder.block_webrtc(options.block_webrtc);
|
||||
builder = builder.block_webgl(options.block_webgl);
|
||||
builder = builder.headless(options.headless);
|
||||
|
||||
if let Some(fonts) = &options.fonts {
|
||||
builder = builder.custom_fonts(fonts.clone());
|
||||
}
|
||||
|
||||
builder = builder.custom_fonts_only(options.custom_fonts_only);
|
||||
|
||||
if let Some(proxy) = &options.proxy {
|
||||
builder = builder.proxy(proxy.clone());
|
||||
}
|
||||
|
||||
// Get Firefox version from executable
|
||||
if let Some(version) = crate::camoufox::config::get_firefox_version(&self.executable_path) {
|
||||
builder = builder.ff_version(version);
|
||||
}
|
||||
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
/// Get the executable path.
|
||||
pub fn executable_path(&self) -> &Path {
|
||||
&self.executable_path
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to launch Camoufox with default settings.
|
||||
pub async fn launch_camoufox(
|
||||
executable_path: impl AsRef<Path>,
|
||||
options: LaunchOptions,
|
||||
) -> Result<Browser, LauncherError> {
|
||||
let launcher = CamoufoxLauncher::new(executable_path).await?;
|
||||
launcher.launch(options).await
|
||||
}
|
||||
|
||||
/// Convenience function to launch a persistent Camoufox context.
|
||||
pub async fn launch_persistent_camoufox(
|
||||
executable_path: impl AsRef<Path>,
|
||||
user_data_dir: impl AsRef<Path>,
|
||||
options: LaunchOptions,
|
||||
) -> Result<BrowserContext, LauncherError> {
|
||||
let launcher = CamoufoxLauncher::new(executable_path).await?;
|
||||
launcher
|
||||
.launch_persistent_context(user_data_dir, options)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//! Camoufox browser integration module.
|
||||
//!
|
||||
//! Provides native Rust support for launching Camoufox browsers with realistic
|
||||
//! fingerprint injection using playwright-rust.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! This module replaces the previous Node.js-based nodecar implementation with
|
||||
//! a pure Rust solution. Key components:
|
||||
//!
|
||||
//! - **Fingerprint Generation**: Bayesian network-based fingerprint generation
|
||||
//! - **WebGL Sampling**: Realistic WebGL configurations from a SQLite database
|
||||
//! - **Configuration Builder**: Converts fingerprints to Camoufox config format
|
||||
//! - **Launcher**: playwright-rust integration for browser launching
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use donutbrowser_lib::camoufox::{CamoufoxLauncher, LaunchOptions};
|
||||
//!
|
||||
//! async fn launch_browser() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let launcher = CamoufoxLauncher::new("/path/to/camoufox").await?;
|
||||
//!
|
||||
//! let options = LaunchOptions {
|
||||
//! os: Some("windows".to_string()),
|
||||
//! headless: false,
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//!
|
||||
//! let browser = launcher.launch(options).await?;
|
||||
//!
|
||||
//! // Use the browser...
|
||||
//!
|
||||
//! browser.close().await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
pub mod config;
|
||||
pub mod data;
|
||||
pub mod env_vars;
|
||||
pub mod fingerprint;
|
||||
pub mod fonts;
|
||||
pub mod geolocation;
|
||||
pub mod launcher;
|
||||
pub mod webgl;
|
||||
|
||||
// Re-export main types for convenience
|
||||
pub use config::{
|
||||
CamoufoxConfigBuilder, CamoufoxLaunchConfig, ConfigError, GeoIPOption, ProxyConfig,
|
||||
};
|
||||
pub use fingerprint::types::{
|
||||
Fingerprint, FingerprintOptions, FingerprintWithHeaders, NavigatorFingerprint, ScreenConstraints,
|
||||
ScreenFingerprint, VideoCard,
|
||||
};
|
||||
pub use fingerprint::{FingerprintError, FingerprintGenerator};
|
||||
pub use geolocation::{
|
||||
fetch_public_ip, get_geolocation, is_geoip_available, is_ipv4, is_ipv6, validate_ip, Geolocation,
|
||||
GeolocationError, Locale, LocaleSelector,
|
||||
};
|
||||
pub use launcher::{
|
||||
launch_camoufox, launch_persistent_camoufox, CamoufoxLauncher, LaunchOptions, LauncherError,
|
||||
};
|
||||
pub use webgl::{sample_webgl, WebGLData, WebGLError};
|
||||
|
||||
/// Unified error type for all Camoufox operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CamoufoxError {
|
||||
#[error("Launcher error: {0}")]
|
||||
Launcher(#[from] LauncherError),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(#[from] ConfigError),
|
||||
|
||||
#[error("Fingerprint error: {0}")]
|
||||
Fingerprint(#[from] FingerprintError),
|
||||
|
||||
#[error("WebGL error: {0}")]
|
||||
WebGL(#[from] WebGLError),
|
||||
|
||||
#[error("Geolocation error: {0}")]
|
||||
Geolocation(#[from] GeolocationError),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_fingerprint_generation() {
|
||||
let generator = FingerprintGenerator::new().unwrap();
|
||||
let options = FingerprintOptions {
|
||||
browsers: Some(vec!["firefox".to_string()]),
|
||||
operating_system: Some("windows".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = generator.get_fingerprint(&options);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let fp = result.unwrap();
|
||||
assert!(!fp.fingerprint.navigator.user_agent.is_empty());
|
||||
assert!(fp.fingerprint.screen.width > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_builder() {
|
||||
let config = CamoufoxConfigBuilder::new()
|
||||
.operating_system("windows")
|
||||
.block_images(false)
|
||||
.build();
|
||||
|
||||
assert!(config.is_ok());
|
||||
|
||||
let config = config.unwrap();
|
||||
assert!(!config.fingerprint_config.is_empty());
|
||||
assert!(config
|
||||
.fingerprint_config
|
||||
.contains_key("navigator.userAgent"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_webgl_sampling() {
|
||||
let result = webgl::sample_webgl("win", None, None);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let webgl_data = result.unwrap();
|
||||
assert!(!webgl_data.vendor.is_empty());
|
||||
assert!(!webgl_data.renderer.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fonts() {
|
||||
let fonts = fonts::get_fonts_for_os("win");
|
||||
assert!(!fonts.is_empty());
|
||||
assert!(fonts.contains(&"Arial".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_env_vars() {
|
||||
let mut config = std::collections::HashMap::new();
|
||||
config.insert(
|
||||
"navigator.userAgent".to_string(),
|
||||
serde_json::json!("Mozilla/5.0"),
|
||||
);
|
||||
|
||||
let env_vars = env_vars::config_to_env_vars(&config).unwrap();
|
||||
assert!(!env_vars.is_empty());
|
||||
assert!(env_vars.contains_key("CAMOU_CONFIG_1"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
//! WebGL fingerprint sampling from SQLite database.
|
||||
//!
|
||||
//! Samples realistic WebGL configurations based on OS-specific probability distributions.
|
||||
|
||||
use rand::Rng;
|
||||
use rusqlite::{Connection, Result as SqliteResult};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::camoufox::data;
|
||||
|
||||
/// WebGL fingerprint data.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WebGLData {
|
||||
pub vendor: String,
|
||||
pub renderer: String,
|
||||
pub config: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Error type for WebGL operations.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum WebGLError {
|
||||
#[error("SQLite error: {0}")]
|
||||
Sqlite(#[from] rusqlite::Error),
|
||||
|
||||
#[error("JSON parsing error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("No WebGL data found for OS: {0}")]
|
||||
NoDataForOS(String),
|
||||
|
||||
#[error("Invalid vendor/renderer combination for OS {os}: {vendor}/{renderer}")]
|
||||
InvalidCombination {
|
||||
os: String,
|
||||
vendor: String,
|
||||
renderer: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Sample a WebGL configuration for the given OS.
|
||||
///
|
||||
/// If `vendor` and `renderer` are provided, returns the specific configuration.
|
||||
/// Otherwise, randomly samples based on OS-specific probability weights.
|
||||
pub fn sample_webgl(
|
||||
os: &str,
|
||||
vendor: Option<&str>,
|
||||
renderer: Option<&str>,
|
||||
) -> Result<WebGLData, WebGLError> {
|
||||
// Write embedded database to a temporary file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
temp_file.write_all(data::WEBGL_DATA_DB)?;
|
||||
let db_path = temp_file.path();
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
// Validate OS
|
||||
let os_column = match os {
|
||||
"win" | "windows" => "win",
|
||||
"mac" | "macos" => "mac",
|
||||
"lin" | "linux" => "lin",
|
||||
_ => return Err(WebGLError::NoDataForOS(os.to_string())),
|
||||
};
|
||||
|
||||
if let (Some(v), Some(r)) = (vendor, renderer) {
|
||||
sample_specific(&conn, os_column, v, r)
|
||||
} else {
|
||||
sample_random(&conn, os_column)
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_specific(
|
||||
conn: &Connection,
|
||||
os_column: &str,
|
||||
vendor: &str,
|
||||
renderer: &str,
|
||||
) -> Result<WebGLData, WebGLError> {
|
||||
let query = format!(
|
||||
"SELECT vendor, renderer, data, {} FROM webgl_fingerprints WHERE vendor = ?1 AND renderer = ?2",
|
||||
os_column
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let mut rows = stmt.query([vendor, renderer])?;
|
||||
|
||||
if let Some(row) = rows.next()? {
|
||||
let weight: f64 = row.get(3)?;
|
||||
if weight <= 0.0 {
|
||||
return Err(WebGLError::InvalidCombination {
|
||||
os: os_column.to_string(),
|
||||
vendor: vendor.to_string(),
|
||||
renderer: renderer.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let data_json: String = row.get(2)?;
|
||||
let config: HashMap<String, serde_json::Value> = serde_json::from_str(&data_json)?;
|
||||
|
||||
Ok(WebGLData {
|
||||
vendor: vendor.to_string(),
|
||||
renderer: renderer.to_string(),
|
||||
config,
|
||||
})
|
||||
} else {
|
||||
Err(WebGLError::InvalidCombination {
|
||||
os: os_column.to_string(),
|
||||
vendor: vendor.to_string(),
|
||||
renderer: renderer.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_random(conn: &Connection, os_column: &str) -> Result<WebGLData, WebGLError> {
|
||||
let query = format!(
|
||||
"SELECT vendor, renderer, data, {} FROM webgl_fingerprints WHERE {} > 0",
|
||||
os_column, os_column
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let rows: Vec<(String, String, String, f64)> = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, String>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, String>(2)?,
|
||||
row.get::<_, f64>(3)?,
|
||||
))
|
||||
})?
|
||||
.collect::<SqliteResult<Vec<_>>>()?;
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(WebGLError::NoDataForOS(os_column.to_string()));
|
||||
}
|
||||
|
||||
// Calculate total weight
|
||||
let total_weight: f64 = rows.iter().map(|(_, _, _, w)| w).sum();
|
||||
|
||||
// Weighted random selection
|
||||
let mut rng = rand::rng();
|
||||
let threshold = rng.random::<f64>() * total_weight;
|
||||
let mut cumulative = 0.0;
|
||||
|
||||
for (vendor, renderer, data_json, weight) in &rows {
|
||||
cumulative += *weight;
|
||||
if cumulative >= threshold {
|
||||
let config: HashMap<String, serde_json::Value> = serde_json::from_str(data_json)?;
|
||||
return Ok(WebGLData {
|
||||
vendor: vendor.clone(),
|
||||
renderer: renderer.clone(),
|
||||
config,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to last row
|
||||
let (vendor, renderer, data_json, _) = rows.last().unwrap();
|
||||
let config: HashMap<String, serde_json::Value> = serde_json::from_str(data_json)?;
|
||||
Ok(WebGLData {
|
||||
vendor: vendor.clone(),
|
||||
renderer: renderer.clone(),
|
||||
config,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get all possible vendor/renderer pairs for each OS.
|
||||
pub fn get_possible_pairs() -> Result<HashMap<String, Vec<(String, String)>>, WebGLError> {
|
||||
// Write embedded database to a temporary file
|
||||
let mut temp_file = NamedTempFile::new()?;
|
||||
temp_file.write_all(data::WEBGL_DATA_DB)?;
|
||||
let db_path = temp_file.path();
|
||||
|
||||
let conn = Connection::open(db_path)?;
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for os in &["win", "mac", "lin"] {
|
||||
let query = format!(
|
||||
"SELECT DISTINCT vendor, renderer FROM webgl_fingerprints WHERE {} > 0 ORDER BY {} DESC",
|
||||
os, os
|
||||
);
|
||||
|
||||
let mut stmt = conn.prepare(&query)?;
|
||||
let pairs: Vec<(String, String)> = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})?
|
||||
.collect::<SqliteResult<Vec<_>>>()?;
|
||||
|
||||
result.insert(os.to_string(), pairs);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_sample_webgl_windows() {
|
||||
let result = sample_webgl("win", None, None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to sample WebGL for Windows: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let data = result.unwrap();
|
||||
assert!(!data.vendor.is_empty());
|
||||
assert!(!data.renderer.is_empty());
|
||||
assert!(!data.config.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_webgl_macos() {
|
||||
let result = sample_webgl("mac", None, None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to sample WebGL for macOS: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sample_webgl_linux() {
|
||||
let result = sample_webgl("lin", None, None);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to sample WebGL for Linux: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_possible_pairs() {
|
||||
let result = get_possible_pairs();
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to get possible pairs: {:?}",
|
||||
result.err()
|
||||
);
|
||||
|
||||
let pairs = result.unwrap();
|
||||
assert!(pairs.contains_key("win"));
|
||||
assert!(pairs.contains_key("mac"));
|
||||
assert!(pairs.contains_key("lin"));
|
||||
assert!(!pairs.get("win").unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
+190
-166
@@ -1,12 +1,14 @@
|
||||
use crate::browser_runner::BrowserRunner;
|
||||
use crate::camoufox::{CamoufoxConfigBuilder, GeoIPOption, ScreenConstraints};
|
||||
use crate::profile::BrowserProfile;
|
||||
use directories::BaseDirs;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::process::Command as TokioCommand;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -86,7 +88,7 @@ impl CamoufoxManager {
|
||||
}
|
||||
|
||||
pub fn instance() -> &'static CamoufoxManager {
|
||||
&CAMOUFOX_NODECAR_LAUNCHER
|
||||
&CAMOUFOX_LAUNCHER
|
||||
}
|
||||
|
||||
pub fn get_profiles_dir(&self) -> PathBuf {
|
||||
@@ -103,112 +105,91 @@ impl CamoufoxManager {
|
||||
/// Generate Camoufox fingerprint configuration during profile creation
|
||||
pub async fn generate_fingerprint_config(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
_app_handle: &AppHandle,
|
||||
profile: &crate::profile::BrowserProfile,
|
||||
config: &CamoufoxConfig,
|
||||
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()];
|
||||
|
||||
// Always ensure executable_path is set to the user's binary location
|
||||
// Get executable path
|
||||
let executable_path = if let Some(path) = &config.executable_path {
|
||||
path.clone()
|
||||
PathBuf::from(path)
|
||||
} else {
|
||||
// Use the browser runner helper with the real profile
|
||||
// Use self.browser_runner instead of instance()
|
||||
BrowserRunner::instance()
|
||||
.get_browser_executable_path(profile)
|
||||
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
config_args.extend(["--executable-path".to_string(), executable_path]);
|
||||
|
||||
// Pass existing fingerprint if provided (for advanced form partial fingerprints)
|
||||
if let Some(fingerprint) = &config.fingerprint {
|
||||
config_args.extend(["--fingerprint".to_string(), fingerprint.clone()]);
|
||||
}
|
||||
// Build the config using CamoufoxConfigBuilder
|
||||
let mut builder = CamoufoxConfigBuilder::new()
|
||||
.block_images(config.block_images.unwrap_or(false))
|
||||
.block_webrtc(config.block_webrtc.unwrap_or(false))
|
||||
.block_webgl(config.block_webgl.unwrap_or(false));
|
||||
|
||||
if let Some(serde_json::Value::Bool(true)) = &config.geoip {
|
||||
config_args.push("--geoip".to_string());
|
||||
}
|
||||
|
||||
// Add proxy if provided (can be passed directly during fingerprint generation)
|
||||
if let Some(proxy) = &config.proxy {
|
||||
config_args.extend(["--proxy".to_string(), proxy.clone()]);
|
||||
}
|
||||
|
||||
// Add screen dimensions if provided
|
||||
if let Some(max_width) = config.screen_max_width {
|
||||
config_args.extend(["--max-width".to_string(), max_width.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(max_height) = config.screen_max_height {
|
||||
config_args.extend(["--max-height".to_string(), max_height.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(min_width) = config.screen_min_width {
|
||||
config_args.extend(["--min-width".to_string(), min_width.to_string()]);
|
||||
}
|
||||
|
||||
if let Some(min_height) = config.screen_min_height {
|
||||
config_args.extend(["--min-height".to_string(), min_height.to_string()]);
|
||||
}
|
||||
|
||||
// Add block_* options
|
||||
if let Some(block_images) = config.block_images {
|
||||
if block_images {
|
||||
config_args.push("--block-images".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(block_webrtc) = config.block_webrtc {
|
||||
if block_webrtc {
|
||||
config_args.push("--block-webrtc".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(block_webgl) = config.block_webgl {
|
||||
if block_webgl {
|
||||
config_args.push("--block-webgl".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Add OS option for fingerprint generation
|
||||
// Set operating system
|
||||
if let Some(os) = &config.os {
|
||||
config_args.extend(["--os".to_string(), os.clone()]);
|
||||
builder = builder.operating_system(os);
|
||||
}
|
||||
|
||||
// Execute config generation command
|
||||
let mut config_sidecar = self.get_nodecar_sidecar(app_handle)?;
|
||||
for arg in &config_args {
|
||||
config_sidecar = config_sidecar.arg(arg);
|
||||
// Build screen constraints if provided
|
||||
if config.screen_min_width.is_some()
|
||||
|| config.screen_max_width.is_some()
|
||||
|| config.screen_min_height.is_some()
|
||||
|| config.screen_max_height.is_some()
|
||||
{
|
||||
let screen_constraints = ScreenConstraints {
|
||||
min_width: config.screen_min_width,
|
||||
max_width: config.screen_max_width,
|
||||
min_height: config.screen_min_height,
|
||||
max_height: config.screen_max_height,
|
||||
};
|
||||
builder = builder.screen_constraints(screen_constraints);
|
||||
}
|
||||
|
||||
let config_output = config_sidecar.output().await?;
|
||||
if !config_output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&config_output.stderr);
|
||||
return Err(format!("Failed to generate camoufox fingerprint config: {stderr}").into());
|
||||
// Parse proxy if provided
|
||||
if let Some(proxy_str) = &config.proxy {
|
||||
let proxy_config = crate::camoufox::ProxyConfig::from_url(proxy_str)
|
||||
.map_err(|e| format!("Failed to parse proxy URL: {e}"))?;
|
||||
builder = builder.proxy(proxy_config);
|
||||
}
|
||||
|
||||
Ok(String::from_utf8_lossy(&config_output.stdout).to_string())
|
||||
// Set Firefox version from executable
|
||||
if let Some(version) = crate::camoufox::config::get_firefox_version(&executable_path) {
|
||||
builder = builder.ff_version(version);
|
||||
}
|
||||
|
||||
// Handle geoip option
|
||||
if let Some(geoip_value) = &config.geoip {
|
||||
match geoip_value {
|
||||
serde_json::Value::Bool(true) => {
|
||||
// Auto-detect IP (through proxy if set)
|
||||
builder = builder.geoip(GeoIPOption::Auto);
|
||||
}
|
||||
serde_json::Value::String(ip) => {
|
||||
// Use specific IP
|
||||
builder = builder.geoip(GeoIPOption::IP(ip.clone()));
|
||||
}
|
||||
_ => {
|
||||
// geoip: false or other values - don't apply geolocation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build the config (async to handle geoip)
|
||||
let launch_config = builder
|
||||
.build_async()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to build Camoufox config: {e}"))?;
|
||||
|
||||
// Return the fingerprint config as JSON
|
||||
let config_json = serde_json::to_string(&launch_config.fingerprint_config)
|
||||
.map_err(|e| format!("Failed to serialize config: {e}"))?;
|
||||
|
||||
Ok(config_json)
|
||||
}
|
||||
|
||||
/// Get the nodecar sidecar command
|
||||
fn get_nodecar_sidecar(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
) -> Result<tauri_plugin_shell::process::Command, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let shell = app_handle.shell();
|
||||
let sidecar_command = shell
|
||||
.sidecar("nodecar")
|
||||
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?;
|
||||
Ok(sidecar_command)
|
||||
}
|
||||
|
||||
/// Launch Camoufox browser using nodecar sidecar
|
||||
/// Launch Camoufox browser by directly spawning the process
|
||||
pub async fn launch_camoufox(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
_app_handle: &AppHandle,
|
||||
profile: &crate::profile::BrowserProfile,
|
||||
profile_path: &str,
|
||||
config: &CamoufoxConfig,
|
||||
@@ -221,44 +202,39 @@ impl CamoufoxManager {
|
||||
return Err("No fingerprint provided".into());
|
||||
};
|
||||
|
||||
// Always ensure executable_path is set to the user's binary location
|
||||
// Get executable path
|
||||
let executable_path = if let Some(path) = &config.executable_path {
|
||||
path.clone()
|
||||
PathBuf::from(path)
|
||||
} else {
|
||||
// Use the browser runner helper with the real profile
|
||||
// Use self.browser_runner instead of instance()
|
||||
BrowserRunner::instance()
|
||||
.get_browser_executable_path(profile)
|
||||
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
// Build nodecar command arguments
|
||||
let mut args = vec!["camoufox".to_string(), "start".to_string()];
|
||||
// Parse the fingerprint config JSON
|
||||
let fingerprint_config: HashMap<String, serde_json::Value> =
|
||||
serde_json::from_str(&custom_config)
|
||||
.map_err(|e| format!("Failed to parse fingerprint config: {e}"))?;
|
||||
|
||||
// Add profile path - ensure it's an absolute path
|
||||
let absolute_profile_path = std::path::Path::new(profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
args.extend(["--profile-path".to_string(), absolute_profile_path]);
|
||||
// Convert to environment variables using CAMOU_CONFIG chunking
|
||||
let env_vars = crate::camoufox::env_vars::config_to_env_vars(&fingerprint_config)
|
||||
.map_err(|e| format!("Failed to convert config to env vars: {e}"))?;
|
||||
|
||||
// Build command arguments
|
||||
let mut args = vec![
|
||||
"-profile".to_string(),
|
||||
std::path::Path::new(profile_path)
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf())
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
"-no-remote".to_string(),
|
||||
];
|
||||
|
||||
// Add URL if provided
|
||||
if let Some(url) = url {
|
||||
args.extend(["--url".to_string(), url.to_string()]);
|
||||
}
|
||||
|
||||
// Always add the executable path
|
||||
args.extend(["--executable-path".to_string(), executable_path]);
|
||||
|
||||
// Always add the generated custom config
|
||||
args.extend(["--custom-config".to_string(), custom_config]);
|
||||
|
||||
// Add proxy if provided
|
||||
if let Some(proxy) = &config.proxy {
|
||||
args.extend(["--proxy".to_string(), proxy.clone()]);
|
||||
args.push("-new-tab".to_string());
|
||||
args.push(url.to_string());
|
||||
}
|
||||
|
||||
// Add headless flag for tests
|
||||
@@ -266,43 +242,62 @@ impl CamoufoxManager {
|
||||
args.push("--headless".to_string());
|
||||
}
|
||||
|
||||
// Get the nodecar sidecar command
|
||||
let mut sidecar_command = self.get_nodecar_sidecar(app_handle)?;
|
||||
log::info!(
|
||||
"Launching Camoufox: {:?} with args: {:?}",
|
||||
executable_path,
|
||||
args
|
||||
);
|
||||
|
||||
// Add all arguments to the sidecar command
|
||||
for arg in &args {
|
||||
sidecar_command = sidecar_command.arg(arg);
|
||||
// Spawn the browser process
|
||||
let mut command = TokioCommand::new(&executable_path);
|
||||
command
|
||||
.args(&args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
// Add environment variables
|
||||
for (key, value) in &env_vars {
|
||||
command.env(key, value);
|
||||
}
|
||||
|
||||
// Execute nodecar sidecar command
|
||||
log::info!("Executing nodecar command with args: {args:?}");
|
||||
let output = sidecar_command.output().await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::info!("nodecar camoufox failed - stdout: {stdout}, stderr: {stderr}");
|
||||
return Err(format!("nodecar camoufox failed: {stderr}").into());
|
||||
// Handle fontconfig on Linux
|
||||
if cfg!(target_os = "linux") {
|
||||
let target_os = config.os.as_deref().unwrap_or("linux");
|
||||
if let Some(fontconfig_path) =
|
||||
crate::camoufox::env_vars::get_fontconfig_env(target_os, &executable_path)
|
||||
{
|
||||
command.env("FONTCONFIG_PATH", fontconfig_path);
|
||||
}
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
log::info!("nodecar camoufox output: {stdout}");
|
||||
let child = command
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to spawn Camoufox process: {e}"))?;
|
||||
|
||||
// Parse the JSON output
|
||||
let launch_result: CamoufoxLaunchResult = serde_json::from_str(&stdout)
|
||||
.map_err(|e| format!("Failed to parse nodecar output as JSON: {e}\nOutput was: {stdout}"))?;
|
||||
let process_id = child.id();
|
||||
let instance_id = format!("camoufox_{}", process_id.unwrap_or(0));
|
||||
|
||||
log::info!("Camoufox launched with PID: {:?}", process_id);
|
||||
|
||||
// Store the instance
|
||||
let instance = CamoufoxInstance {
|
||||
id: launch_result.id.clone(),
|
||||
process_id: launch_result.processId,
|
||||
profile_path: launch_result.profilePath.clone(),
|
||||
url: launch_result.url.clone(),
|
||||
id: instance_id.clone(),
|
||||
process_id,
|
||||
profile_path: Some(profile_path.to_string()),
|
||||
url: url.map(String::from),
|
||||
};
|
||||
|
||||
let launch_result = CamoufoxLaunchResult {
|
||||
id: instance_id.clone(),
|
||||
processId: process_id,
|
||||
profilePath: Some(profile_path.to_string()),
|
||||
url: url.map(String::from),
|
||||
};
|
||||
|
||||
{
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.insert(launch_result.id.clone(), instance);
|
||||
inner.instances.insert(instance_id, instance);
|
||||
}
|
||||
|
||||
Ok(launch_result)
|
||||
@@ -311,41 +306,70 @@ impl CamoufoxManager {
|
||||
/// Stop a Camoufox process by ID
|
||||
pub async fn stop_camoufox(
|
||||
&self,
|
||||
app_handle: &AppHandle,
|
||||
_app_handle: &AppHandle,
|
||||
id: &str,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Get the nodecar sidecar command
|
||||
let sidecar_command = self
|
||||
.get_nodecar_sidecar(app_handle)?
|
||||
.arg("camoufox")
|
||||
.arg("stop")
|
||||
.arg("--id")
|
||||
.arg(id);
|
||||
// Get the process ID from our tracking
|
||||
let process_id = {
|
||||
let inner = self.inner.lock().await;
|
||||
inner
|
||||
.instances
|
||||
.get(id)
|
||||
.and_then(|instance| instance.process_id)
|
||||
};
|
||||
|
||||
// Execute nodecar stop command
|
||||
let output = sidecar_command.output().await?;
|
||||
if let Some(pid) = process_id {
|
||||
// Kill the process
|
||||
let success = self.kill_process(pid);
|
||||
|
||||
if !output.status.success() {
|
||||
let _stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Ok(false);
|
||||
}
|
||||
if success {
|
||||
// Remove from our tracking
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.remove(id);
|
||||
log::info!("Stopped Camoufox instance {} (PID: {})", id, pid);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let result: serde_json::Value = serde_json::from_str(&stdout)
|
||||
.map_err(|e| format!("Failed to parse nodecar stop output: {e}"))?;
|
||||
|
||||
let success = result
|
||||
.get("success")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
if success {
|
||||
// Remove from our tracking
|
||||
Ok(success)
|
||||
} else {
|
||||
// No process ID found, just remove from tracking
|
||||
let mut inner = self.inner.lock().await;
|
||||
inner.instances.remove(id);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Kill a process by PID
|
||||
fn kill_process(&self, pid: u32) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::ExitStatusExt;
|
||||
let result = std::process::Command::new("kill")
|
||||
.args(["-TERM", &pid.to_string()])
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) => status.success() || status.signal() == Some(0),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to kill process {}: {}", pid, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(success)
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let result = std::process::Command::new("taskkill")
|
||||
.args(["/PID", &pid.to_string(), "/T"])
|
||||
.status();
|
||||
|
||||
match result {
|
||||
Ok(status) => status.success(),
|
||||
Err(e) => {
|
||||
log::warn!("Failed to kill process {}: {}", pid, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find Camoufox server by profile path (for integration with browser_runner)
|
||||
@@ -544,7 +568,7 @@ impl CamoufoxManager {
|
||||
}
|
||||
|
||||
impl CamoufoxManager {
|
||||
pub async fn launch_camoufox_profile_nodecar(
|
||||
pub async fn launch_camoufox_profile(
|
||||
&self,
|
||||
app_handle: AppHandle,
|
||||
profile: BrowserProfile,
|
||||
@@ -574,7 +598,7 @@ impl CamoufoxManager {
|
||||
url.as_deref(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}"))
|
||||
.map_err(|e| format!("Failed to launch Camoufox: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,5 +621,5 @@ mod tests {
|
||||
|
||||
// Global singleton instance
|
||||
lazy_static::lazy_static! {
|
||||
static ref CAMOUFOX_NODECAR_LAUNCHER: CamoufoxManager = CamoufoxManager::new();
|
||||
static ref CAMOUFOX_LAUNCHER: CamoufoxManager = CamoufoxManager::new();
|
||||
}
|
||||
|
||||
@@ -350,6 +350,50 @@ impl Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure version.json exists in the Camoufox installation directory.
|
||||
/// Creates the file if it doesn't exist, using the version from the tag name.
|
||||
async fn ensure_camoufox_version_json(
|
||||
&self,
|
||||
browser_dir: &Path,
|
||||
version: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// The browser_dir is typically: binaries/camoufox/<version>/
|
||||
// Find the executable directory within it
|
||||
let version_json_locations = vec![
|
||||
browser_dir.join("version.json"),
|
||||
browser_dir.join("camoufox").join("version.json"),
|
||||
];
|
||||
|
||||
// Check if version.json already exists in any expected location
|
||||
for location in &version_json_locations {
|
||||
if location.exists() {
|
||||
log::info!("version.json already exists at: {}", location.display());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the Firefox version from the Camoufox version tag
|
||||
// Format: "135.0.1-beta.24" -> Firefox version is "135.0.1" (or just "135.0")
|
||||
let firefox_version = version.split('-').next().unwrap_or(version);
|
||||
|
||||
// Create version.json in the browser directory
|
||||
let version_json_path = browser_dir.join("version.json");
|
||||
let version_data = serde_json::json!({
|
||||
"version": firefox_version
|
||||
});
|
||||
|
||||
let version_json_str = serde_json::to_string_pretty(&version_data)?;
|
||||
tokio::fs::write(&version_json_path, version_json_str).await?;
|
||||
|
||||
log::info!(
|
||||
"Created version.json at {} with Firefox version: {}",
|
||||
version_json_path.display(),
|
||||
firefox_version
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn download_browser<R: tauri::Runtime>(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle<R>,
|
||||
@@ -809,7 +853,7 @@ impl Downloader {
|
||||
}
|
||||
}
|
||||
|
||||
// If this is Camoufox, automatically download GeoIP database
|
||||
// If this is Camoufox, automatically download GeoIP database and create version.json
|
||||
if browser_str == "camoufox" {
|
||||
// Check if GeoIP database is already available
|
||||
if !crate::geoip_downloader::GeoIPDownloader::is_geoip_database_available() {
|
||||
@@ -831,6 +875,15 @@ impl Downloader {
|
||||
} else {
|
||||
log::info!("GeoIP database already available");
|
||||
}
|
||||
|
||||
// Create version.json if it doesn't exist
|
||||
if let Err(e) = self
|
||||
.ensure_camoufox_version_json(&browser_dir, &version)
|
||||
.await
|
||||
{
|
||||
log::warn!("Failed to create version.json for Camoufox: {e}");
|
||||
// Don't fail the download if version.json creation fails
|
||||
}
|
||||
}
|
||||
|
||||
// Emit completion
|
||||
|
||||
+1
-30
@@ -15,6 +15,7 @@ mod auto_updater;
|
||||
mod browser;
|
||||
mod browser_runner;
|
||||
mod browser_version_manager;
|
||||
pub mod camoufox;
|
||||
mod camoufox_manager;
|
||||
mod default_browser;
|
||||
mod downloaded_browsers_registry;
|
||||
@@ -143,35 +144,6 @@ impl<R: Runtime> WindowExt for WebviewWindow<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn warm_up_nodecar(app: tauri::AppHandle) -> Result<(), String> {
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
// Use sidecar to execute a fast, harmless command that ensures the binary is loaded
|
||||
let cmd = app
|
||||
.shell()
|
||||
.sidecar("nodecar")
|
||||
.map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?
|
||||
.arg("help");
|
||||
|
||||
let exec_future = async { cmd.output().await };
|
||||
match timeout(Duration::from_secs(120), exec_future).await {
|
||||
Ok(Ok(_output)) => {
|
||||
let duration = start_time.elapsed();
|
||||
log::info!(
|
||||
"Nodecar warm-up (frontend-triggered) completed in {:.2}s",
|
||||
duration.as_secs_f64()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Ok(Err(e)) => Err(format!("Failed to execute nodecar for warm-up: {e}")),
|
||||
Err(_) => Err("Nodecar warm-up timed out after 120s".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn handle_url_open(app: tauri::AppHandle, url: String) -> Result<(), String> {
|
||||
log::info!("handle_url_open called with URL: {url}");
|
||||
@@ -838,7 +810,6 @@ pub fn run() {
|
||||
delete_selected_profiles,
|
||||
is_geoip_database_available,
|
||||
download_geoip_database,
|
||||
warm_up_nodecar,
|
||||
start_api_server,
|
||||
stop_api_server,
|
||||
get_api_server_status,
|
||||
|
||||
@@ -820,11 +820,9 @@ impl ProfileManager {
|
||||
app_handle: tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// Handle Camoufox profiles using nodecar-based status checking
|
||||
// Handle Camoufox profiles using CamoufoxManager-based status checking
|
||||
if profile.browser == "camoufox" {
|
||||
return self
|
||||
.check_camoufox_status_via_nodecar(&app_handle, profile)
|
||||
.await;
|
||||
return self.check_camoufox_status(&app_handle, profile).await;
|
||||
}
|
||||
|
||||
// For non-camoufox browsers, use the existing PID-based logic
|
||||
@@ -888,7 +886,7 @@ impl ProfileManager {
|
||||
"zen" => exe_name.contains("zen"),
|
||||
"chromium" => exe_name.contains("chromium"),
|
||||
"brave" => exe_name.contains("brave"),
|
||||
// Camoufox is handled via nodecar, not PID-based checking
|
||||
// Camoufox is handled via CamoufoxManager, not PID-based checking
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -981,8 +979,8 @@ impl ProfileManager {
|
||||
Ok(is_running)
|
||||
}
|
||||
|
||||
// Check Camoufox status using nodecar-based approach
|
||||
async fn check_camoufox_status_via_nodecar(
|
||||
// Check Camoufox status using CamoufoxManager
|
||||
async fn check_camoufox_status(
|
||||
&self,
|
||||
app_handle: &tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
@@ -1062,7 +1060,7 @@ impl ProfileManager {
|
||||
}
|
||||
Err(e) => {
|
||||
// Error checking status, assume not running and clear process ID
|
||||
log::warn!("Warning: Failed to check Camoufox status via nodecar: {e}");
|
||||
log::warn!("Warning: Failed to check Camoufox status: {e}");
|
||||
let profiles_dir = self.get_profiles_dir();
|
||||
let profile_uuid_dir = profiles_dir.join(profile.id.to_string());
|
||||
let metadata_file = profile_uuid_dir.join("metadata.json");
|
||||
|
||||
Reference in New Issue
Block a user