feat: fully implement happy flow for persistant fingerprint generation

This commit is contained in:
zhom
2025-08-06 04:33:01 +04:00
parent ff35717cb5
commit b5b08a0196
20 changed files with 2531 additions and 1545 deletions
+87 -81
View File
@@ -147,94 +147,94 @@ impl BrowserRunner {
// Handle camoufox profiles using nodecar launcher
if profile.browser == "camoufox" {
if let Some(mut camoufox_config) = profile.camoufox_config.clone() {
// Always start a local proxy for Camoufox (for traffic monitoring and geoip support)
let upstream_proxy = profile
.proxy_id
.as_ref()
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id));
// Get or create camoufox config
let mut camoufox_config = profile.camoufox_config.clone().unwrap_or_else(|| {
println!(
"Starting local proxy for Camoufox profile: {} (upstream: {})",
profile.name,
upstream_proxy
.as_ref()
.map(|p| format!("{}:{}", p.host, p.port))
.unwrap_or_else(|| "DIRECT".to_string())
);
// Start the proxy and get local proxy settings
let local_proxy = PROXY_MANAGER
.start_proxy(
app_handle.clone(),
upstream_proxy.as_ref(),
0, // Use 0 as temporary PID, will be updated later
Some(&profile.name),
)
.await
.map_err(|e| format!("Failed to start local proxy for Camoufox: {e}"))?;
// Format proxy URL for camoufox - always use HTTP for the local proxy
let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port);
// Set proxy in camoufox config
camoufox_config.proxy = Some(proxy_url);
// Ensure geoip is always enabled for proper geolocation spoofing
if camoufox_config.geoip.is_none() {
camoufox_config.geoip = Some(serde_json::Value::Bool(true));
}
println!(
"Configured local proxy for Camoufox: {:?}, geoip: {:?}",
camoufox_config.proxy, camoufox_config.geoip
);
// Use the nodecar camoufox launcher
println!(
"Launching Camoufox via nodecar for profile: {}",
"No camoufox config found for profile {}, using default",
profile.name
);
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
let camoufox_result = camoufox_launcher
.launch_camoufox_profile_nodecar(
app_handle.clone(),
profile.clone(),
camoufox_config,
url,
)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch camoufox via nodecar: {e}").into()
})?;
crate::camoufox::CamoufoxConfig::default()
});
// For server-based Camoufox, we use the process_id
let process_id = camoufox_result.processId.unwrap_or(0);
println!("Camoufox launched successfully with PID: {process_id}");
// Always start a local proxy for Camoufox (for traffic monitoring and geoip support)
let upstream_proxy = profile
.proxy_id
.as_ref()
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id));
// Update profile with the process info from camoufox result
let mut updated_profile = profile.clone();
updated_profile.process_id = Some(process_id);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
println!(
"Starting local proxy for Camoufox profile: {} (upstream: {})",
profile.name,
upstream_proxy
.as_ref()
.map(|p| format!("{}:{}", p.host, p.port))
.unwrap_or_else(|| "DIRECT".to_string())
);
// Save the updated profile
self.save_process_info(&updated_profile)?;
println!(
"Updated profile with process info: {}",
updated_profile.name
);
// Start the proxy and get local proxy settings
let local_proxy = PROXY_MANAGER
.start_proxy(
app_handle.clone(),
upstream_proxy.as_ref(),
0, // Use 0 as temporary PID, will be updated later
Some(&profile.name),
)
.await
.map_err(|e| format!("Failed to start local proxy for Camoufox: {e}"))?;
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
} else {
println!("Emitted profile update event for: {}", updated_profile.name);
}
// Format proxy URL for camoufox - always use HTTP for the local proxy
let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port);
return Ok(updated_profile);
} else {
return Err("Camoufox profile missing configuration".into());
// Set proxy in camoufox config
camoufox_config.proxy = Some(proxy_url);
// Ensure geoip is always enabled for proper geolocation spoofing
if camoufox_config.geoip.is_none() {
camoufox_config.geoip = Some(serde_json::Value::Bool(true));
}
println!(
"Configured local proxy for Camoufox: {:?}, geoip: {:?}",
camoufox_config.proxy, camoufox_config.geoip
);
// Use the nodecar camoufox launcher
println!(
"Launching Camoufox via nodecar for profile: {}",
profile.name
);
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
let camoufox_result = camoufox_launcher
.launch_camoufox_profile_nodecar(app_handle.clone(), profile.clone(), camoufox_config, url)
.await
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to launch camoufox via nodecar: {e}").into()
})?;
// For server-based Camoufox, we use the process_id
let process_id = camoufox_result.processId.unwrap_or(0);
println!("Camoufox launched successfully with PID: {process_id}");
// Update profile with the process info from camoufox result
let mut updated_profile = profile.clone();
updated_profile.process_id = Some(process_id);
updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs());
// Save the updated profile
self.save_process_info(&updated_profile)?;
println!(
"Updated profile with process info: {}",
updated_profile.name
);
// Emit profile update event to frontend
if let Err(e) = app_handle.emit("profile-updated", &updated_profile) {
println!("Warning: Failed to emit profile update event: {e}");
} else {
println!("Emitted profile update event for: {}", updated_profile.name);
}
return Ok(updated_profile);
}
// Create browser instance
@@ -1298,7 +1298,8 @@ impl BrowserRunner {
}
#[tauri::command]
pub fn create_browser_profile(
pub async fn create_browser_profile(
app_handle: tauri::AppHandle,
name: String,
browser: String,
version: String,
@@ -1309,6 +1310,7 @@ pub fn create_browser_profile(
let profile_manager = ProfileManager::instance();
profile_manager
.create_profile(
&app_handle,
&name,
&browser,
&version,
@@ -1316,6 +1318,7 @@ pub fn create_browser_profile(
proxy_id,
camoufox_config,
)
.await
.map_err(|e| format!("Failed to create profile: {e}"))
}
@@ -1603,7 +1606,8 @@ pub async fn kill_browser_profile(
}
#[tauri::command]
pub fn create_browser_profile_new(
pub async fn create_browser_profile_new(
app_handle: tauri::AppHandle,
name: String,
browser_str: String,
version: String,
@@ -1614,6 +1618,7 @@ pub fn create_browser_profile_new(
let browser_type =
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
create_browser_profile(
app_handle,
name,
browser_type.as_str().to_string(),
version,
@@ -1621,6 +1626,7 @@ pub fn create_browser_profile_new(
proxy_id,
camoufox_config,
)
.await
}
#[tauri::command]
+182 -302
View File
@@ -9,85 +9,29 @@ use tokio::sync::Mutex as AsyncMutex;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CamoufoxConfig {
pub os: Option<Vec<String>>,
pub proxy: Option<String>,
pub screen_max_width: Option<u32>,
pub screen_max_height: Option<u32>,
pub geoip: Option<serde_json::Value>, // Can be String or bool
pub block_images: Option<bool>,
pub block_webrtc: Option<bool>,
pub block_webgl: Option<bool>,
pub disable_coop: Option<bool>,
pub geoip: Option<serde_json::Value>, // Can be String or bool
pub country: Option<String>,
pub timezone: Option<String>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub humanize: Option<bool>,
pub humanize_duration: Option<f64>,
pub headless: Option<bool>,
pub locale: Option<Vec<String>>,
pub addons: Option<Vec<String>>,
pub fonts: Option<Vec<String>>,
pub custom_fonts_only: Option<bool>,
pub exclude_addons: Option<Vec<String>>,
pub screen_min_width: Option<u32>,
pub screen_max_width: Option<u32>,
pub screen_min_height: Option<u32>,
pub screen_max_height: Option<u32>,
pub window_width: Option<u32>,
pub window_height: Option<u32>,
pub ff_version: Option<u32>,
pub main_world_eval: Option<bool>,
pub webgl_vendor: Option<String>,
pub webgl_renderer: Option<String>,
pub proxy: Option<String>,
pub enable_cache: Option<bool>,
pub virtual_display: Option<String>,
pub debug: Option<bool>,
pub additional_args: Option<Vec<String>>,
pub env_vars: Option<HashMap<String, String>>,
pub firefox_prefs: Option<HashMap<String, serde_json::Value>>,
pub disable_theming: Option<bool>,
pub showcursor: Option<bool>,
pub executable_path: Option<String>,
pub fingerprint: Option<String>, // JSON string of the complete fingerprint config
}
impl Default for CamoufoxConfig {
fn default() -> Self {
Self {
os: None,
proxy: None,
screen_max_width: None,
screen_max_height: None,
geoip: Some(serde_json::Value::Bool(true)),
block_images: None,
block_webrtc: None,
block_webgl: None,
disable_coop: None,
geoip: Some(serde_json::Value::Bool(true)),
country: None,
timezone: None,
latitude: None,
longitude: None,
humanize: None,
humanize_duration: None,
headless: None,
locale: None,
addons: None,
fonts: None,
custom_fonts_only: None,
exclude_addons: None,
screen_min_width: None,
screen_max_width: None,
screen_min_height: None,
screen_max_height: None,
window_width: None,
window_height: None,
ff_version: None,
main_world_eval: None,
webgl_vendor: None,
webgl_renderer: None,
proxy: None,
enable_cache: Some(true),
virtual_display: None,
debug: None,
additional_args: None,
env_vars: None,
firefox_prefs: None,
disable_theming: Some(true),
showcursor: Some(false),
executable_path: None,
fingerprint: None,
}
}
}
@@ -137,28 +81,95 @@ impl CamoufoxNodecarLauncher {
#[allow(dead_code)]
pub fn create_test_config() -> CamoufoxConfig {
CamoufoxConfig {
// Core anti-fingerprinting settings
screen_min_width: Some(1440),
screen_min_height: Some(900),
// WebGL spoofing
webgl_vendor: Some("Intel Inc.".to_string()),
webgl_renderer: Some("Intel Iris Pro OpenGL Engine".to_string()),
// Humanization
humanize: Some(true),
// Other settings
debug: Some(true),
enable_cache: Some(true),
headless: Some(false), // Not headless for testing
disable_theming: Some(true),
showcursor: Some(false),
screen_max_width: Some(1440),
screen_max_height: Some(900),
geoip: Some(serde_json::Value::Bool(true)),
..Default::default()
}
}
/// Generate Camoufox fingerprint configuration during profile creation
pub async fn generate_fingerprint_config(
&self,
app_handle: &AppHandle,
config: &CamoufoxConfig,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()];
// For fingerprint generation during profile creation, we can pass proxy directly
// but we set geoip to false during tests to avoid network requests
if std::env::var("CAMOUFOX_TEST").is_ok() {
config_args.extend(["--geoip".to_string(), "false".to_string()]);
} else if let Some(geoip) = &config.geoip {
match geoip {
serde_json::Value::Bool(true) => {
config_args.extend(["--geoip".to_string(), "true".to_string()]);
}
serde_json::Value::Bool(false) => {
config_args.extend(["--geoip".to_string(), "false".to_string()]);
}
serde_json::Value::String(ip) => {
config_args.extend(["--geoip".to_string(), ip.clone()]);
}
_ => {}
}
} else {
// Default to true for fingerprint generation
config_args.extend(["--geoip".to_string(), "true".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()]);
}
// Add block_* and executable_path 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());
}
}
if let Some(executable_path) = &config.executable_path {
config_args.extend(["--executable-path".to_string(), executable_path.clone()]);
}
// 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);
}
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());
}
Ok(String::from_utf8_lossy(&config_output.stdout).to_string())
}
/// Get the nodecar sidecar command
fn get_nodecar_sidecar(
&self,
@@ -179,6 +190,82 @@ impl CamoufoxNodecarLauncher {
config: &CamoufoxConfig,
url: Option<&str>,
) -> Result<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
// Generate or use existing configuration
let custom_config = if let Some(existing_fingerprint) = &config.fingerprint {
// Use existing fingerprint from profile metadata
println!("Using existing fingerprint from profile metadata");
existing_fingerprint.clone()
} else {
// Generate new configuration using nodecar generate-config command
println!("Generating new fingerprint configuration");
let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()];
// Use individual options to build configuration
if let Some(proxy) = &config.proxy {
config_args.extend(["--proxy".to_string(), proxy.clone()]);
}
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(geoip) = &config.geoip {
match geoip {
serde_json::Value::Bool(true) => {
config_args.extend(["--geoip".to_string(), "true".to_string()]);
}
serde_json::Value::Bool(false) => {
config_args.extend(["--geoip".to_string(), "false".to_string()]);
}
serde_json::Value::String(ip) => {
config_args.extend(["--geoip".to_string(), ip.clone()]);
}
_ => {}
}
}
// Always add block_* and executable_path 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());
}
}
if let Some(executable_path) = &config.executable_path {
config_args.extend(["--executable-path".to_string(), executable_path.clone()]);
}
// 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);
}
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 config: {stderr}").into());
}
String::from_utf8_lossy(&config_output.stdout).to_string()
};
// Build nodecar command arguments
let mut args = vec!["camoufox".to_string(), "start".to_string()];
@@ -190,207 +277,12 @@ impl CamoufoxNodecarLauncher {
args.extend(["--url".to_string(), url.to_string()]);
}
// Add configuration options
if let Some(os_list) = &config.os {
let os_str = os_list.join(",");
args.extend(["--os".to_string(), os_str]);
}
// Always add the generated custom config
args.extend(["--custom-config".to_string(), custom_config]);
if let Some(block_images) = config.block_images {
if block_images {
args.push("--block-images".to_string());
}
}
if let Some(block_webrtc) = config.block_webrtc {
if block_webrtc {
args.push("--block-webrtc".to_string());
}
}
if let Some(block_webgl) = config.block_webgl {
if block_webgl {
args.push("--block-webgl".to_string());
}
}
if let Some(disable_coop) = config.disable_coop {
if disable_coop {
args.push("--disable-coop".to_string());
}
}
if let Some(geoip) = &config.geoip {
match geoip {
serde_json::Value::Bool(true) => {
args.extend(["--geoip".to_string(), "auto".to_string()]);
}
serde_json::Value::String(ip) => {
args.extend(["--geoip".to_string(), ip.clone()]);
}
_ => {}
}
}
if let Some(country) = &config.country {
args.extend(["--country".to_string(), country.clone()]);
}
if let Some(timezone) = &config.timezone {
args.extend(["--timezone".to_string(), timezone.clone()]);
}
if let Some(latitude) = config.latitude {
args.extend(["--latitude".to_string(), latitude.to_string()]);
}
if let Some(longitude) = config.longitude {
args.extend(["--longitude".to_string(), longitude.to_string()]);
}
if let Some(humanize) = config.humanize {
if humanize {
if let Some(duration) = config.humanize_duration {
args.extend(["--humanize".to_string(), duration.to_string()]);
} else {
args.push("--humanize".to_string());
}
}
}
if let Some(headless) = config.headless {
if headless {
args.push("--headless".to_string());
}
}
if let Some(locale_list) = &config.locale {
let locale_str = locale_list.join(",");
args.extend(["--locale".to_string(), locale_str]);
}
if let Some(addons) = &config.addons {
let addons_str = addons.join(",");
args.extend(["--addons".to_string(), addons_str]);
}
if let Some(fonts) = &config.fonts {
let fonts_str = fonts.join(",");
args.extend(["--fonts".to_string(), fonts_str]);
}
if let Some(custom_fonts_only) = config.custom_fonts_only {
if custom_fonts_only {
args.push("--custom-fonts-only".to_string());
}
}
if let Some(exclude_addons) = &config.exclude_addons {
let exclude_str = exclude_addons.join(",");
args.extend(["--exclude-addons".to_string(), exclude_str]);
}
if let Some(screen_min_width) = config.screen_min_width {
args.extend([
"--screen-min-width".to_string(),
screen_min_width.to_string(),
]);
}
if let Some(screen_max_width) = config.screen_max_width {
args.extend([
"--screen-max-width".to_string(),
screen_max_width.to_string(),
]);
}
if let Some(screen_min_height) = config.screen_min_height {
args.extend([
"--screen-min-height".to_string(),
screen_min_height.to_string(),
]);
}
if let Some(screen_max_height) = config.screen_max_height {
args.extend([
"--screen-max-height".to_string(),
screen_max_height.to_string(),
]);
}
if let Some(window_width) = config.window_width {
args.extend(["--window-width".to_string(), window_width.to_string()]);
}
if let Some(window_height) = config.window_height {
args.extend(["--window-height".to_string(), window_height.to_string()]);
}
if let Some(ff_version) = config.ff_version {
args.extend(["--ff-version".to_string(), ff_version.to_string()]);
}
if let Some(main_world_eval) = config.main_world_eval {
if main_world_eval {
args.push("--main-world-eval".to_string());
}
}
if let Some(webgl_vendor) = &config.webgl_vendor {
args.extend(["--webgl-vendor".to_string(), webgl_vendor.clone()]);
}
if let Some(webgl_renderer) = &config.webgl_renderer {
args.extend(["--webgl-renderer".to_string(), webgl_renderer.clone()]);
}
if let Some(proxy) = &config.proxy {
args.extend(["--proxy".to_string(), proxy.clone()]);
}
if let Some(enable_cache) = config.enable_cache {
if !enable_cache {
args.push("--disable-cache".to_string());
}
}
if let Some(virtual_display) = &config.virtual_display {
args.extend(["--virtual-display".to_string(), virtual_display.clone()]);
}
if let Some(debug) = config.debug {
if debug {
args.push("--debug".to_string());
}
}
if let Some(additional_args) = &config.additional_args {
let args_str = additional_args.join(",");
args.extend(["--args".to_string(), args_str]);
}
if let Some(env_vars) = &config.env_vars {
let env_json = serde_json::to_string(env_vars)?;
args.extend(["--env".to_string(), env_json]);
}
if let Some(firefox_prefs) = &config.firefox_prefs {
let prefs_json = serde_json::to_string(firefox_prefs)?;
args.extend(["--firefox-prefs".to_string(), prefs_json]);
}
if let Some(disable_theming) = config.disable_theming {
if disable_theming {
args.push("--disable-theming".to_string());
}
}
if let Some(showcursor) = config.showcursor {
if showcursor {
args.push("--showcursor".to_string());
} else {
args.push("--no-showcursor".to_string());
}
// Add headless flag for tests
if std::env::var("CAMOUFOX_HEADLESS").is_ok() {
args.push("--headless".to_string());
}
// Get the nodecar sidecar command
@@ -622,18 +514,8 @@ mod tests {
let test_config = CamoufoxNodecarLauncher::create_test_config();
// Verify test config has expected values
assert_eq!(test_config.screen_min_width, Some(1440));
assert_eq!(test_config.screen_min_height, Some(900));
assert_eq!(test_config.webgl_vendor, Some("Intel Inc.".to_string()));
assert_eq!(
test_config.webgl_renderer,
Some("Intel Iris Pro OpenGL Engine".to_string())
);
assert_eq!(test_config.humanize, Some(true));
assert_eq!(test_config.debug, Some(true));
assert_eq!(test_config.enable_cache, Some(true));
assert_eq!(test_config.headless, Some(false));
// Verify that geoip is enabled by default (from Default implementation)
assert_eq!(test_config.screen_max_width, Some(1440));
assert_eq!(test_config.screen_max_height, Some(900));
assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true)));
}
@@ -642,11 +524,9 @@ mod tests {
let default_config = CamoufoxConfig::default();
// Verify defaults
assert_eq!(default_config.enable_cache, Some(true));
assert_eq!(default_config.geoip, Some(serde_json::Value::Bool(true)));
assert_eq!(default_config.timezone, None);
assert_eq!(default_config.debug, None);
assert_eq!(default_config.headless, None);
assert_eq!(default_config.proxy, None);
assert_eq!(default_config.fingerprint, None);
}
}
-5
View File
@@ -25,7 +25,6 @@ mod profile;
mod profile_importer;
mod proxy_manager;
mod settings_manager;
mod system_utils;
mod theme_detector;
mod version_updater;
@@ -65,8 +64,6 @@ use profile_importer::{detect_existing_profiles, import_browser_profile};
use theme_detector::get_system_theme;
use system_utils::{get_system_locale, get_system_timezone};
use group_manager::{
assign_profiles_to_group, create_profile_group, delete_profile_group, delete_selected_profiles,
get_groups_with_profile_counts, get_profile_groups, update_profile_group,
@@ -478,8 +475,6 @@ pub fn run() {
update_stored_proxy,
delete_stored_proxy,
update_camoufox_config,
get_system_locale,
get_system_timezone,
get_profile_groups,
get_groups_with_profile_counts,
create_profile_group,
+79 -257
View File
@@ -34,8 +34,9 @@ impl ProfileManager {
path
}
pub fn create_profile(
pub async fn create_profile(
&self,
app_handle: &tauri::AppHandle,
name: &str,
browser: &str,
version: &str,
@@ -43,20 +44,50 @@ impl ProfileManager {
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
self.create_profile_with_group(
name,
browser,
version,
release_type,
proxy_id,
camoufox_config,
None,
)
self
.create_profile_with_group(
app_handle,
name,
browser,
version,
release_type,
proxy_id,
camoufox_config,
None,
)
.await
}
// Synchronous version for tests that doesn't generate fingerprints
#[cfg(test)]
pub async fn create_profile_sync(
&self,
app_handle: &tauri::AppHandle,
name: &str,
browser: &str,
version: &str,
release_type: &str,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
self
.create_profile_with_group(
app_handle,
name,
browser,
version,
release_type,
proxy_id,
camoufox_config,
None,
)
.await
}
#[allow(clippy::too_many_arguments)]
pub fn create_profile_with_group(
pub async fn create_profile_with_group(
&self,
app_handle: &tauri::AppHandle,
name: &str,
browser: &str,
version: &str,
@@ -87,6 +118,41 @@ impl ProfileManager {
create_dir_all(&profile_uuid_dir)?;
create_dir_all(&profile_data_dir)?;
// For Camoufox profiles, generate fingerprint during creation
let final_camoufox_config = if browser == "camoufox" {
let mut config = camoufox_config.unwrap_or_else(|| {
println!("Creating default Camoufox config for profile: {name}");
crate::camoufox::CamoufoxConfig::default()
});
// Generate fingerprint if not already provided
if config.fingerprint.is_none() {
println!("Generating fingerprint for Camoufox profile: {name}");
// Use the camoufox launcher to generate the config
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
match camoufox_launcher
.generate_fingerprint_config(app_handle, &config)
.await
{
Ok(generated_fingerprint) => {
config.fingerprint = Some(generated_fingerprint);
println!("Successfully generated fingerprint for profile: {name}");
}
Err(e) => {
println!("Warning: Failed to generate fingerprint for profile {name}: {e}");
// Continue with the profile creation even if fingerprint generation fails
}
}
} else {
println!("Using provided fingerprint for Camoufox profile: {name}");
}
Some(config)
} else {
camoufox_config.clone()
};
let profile = BrowserProfile {
id: profile_id,
name: name.to_string(),
@@ -96,7 +162,7 @@ impl ProfileManager {
process_id: None,
last_launch: None,
release_type: release_type.to_string(),
camoufox_config: camoufox_config.clone(),
camoufox_config: final_camoufox_config,
group_id: group_id.clone(),
};
@@ -913,7 +979,7 @@ impl ProfileManager {
#[cfg(test)]
mod tests {
use super::*;
use crate::browser::ProxySettings;
use tempfile::TempDir;
fn create_test_profile_manager() -> (&'static ProfileManager, TempDir) {
@@ -940,250 +1006,6 @@ mod tests {
assert!(profiles_dir.to_string_lossy().contains("DonutBrowser"));
assert!(profiles_dir.to_string_lossy().contains("profiles"));
}
#[test]
fn test_create_profile() {
let (manager, _temp_dir) = create_test_profile_manager();
let profile = manager
.create_profile("Test Profile", "firefox", "139.0", "stable", None, None)
.unwrap();
assert_eq!(profile.name, "Test Profile");
assert_eq!(profile.browser, "firefox");
assert_eq!(profile.version, "139.0");
assert!(profile.proxy_id.is_none());
assert!(profile.process_id.is_none());
}
#[test]
fn test_save_and_load_profile() {
let (manager, _temp_dir) = create_test_profile_manager();
let unique_name = format!("Test Save Load {}", uuid::Uuid::new_v4());
let profile = manager
.create_profile(&unique_name, "firefox", "139.0", "stable", None, None)
.unwrap();
// Save the profile
manager.save_profile(&profile).unwrap();
// Load profiles and verify our profile exists
let profiles = manager.list_profiles().unwrap();
let our_profile = profiles.iter().find(|p| p.name == unique_name).unwrap();
assert_eq!(our_profile.name, unique_name);
assert_eq!(our_profile.browser, "firefox");
assert_eq!(our_profile.version, "139.0");
// Clean up
let _ = manager.delete_profile(&unique_name);
}
#[test]
fn test_rename_profile() {
let (manager, _temp_dir) = create_test_profile_manager();
let original_name = format!("Original Name {}", uuid::Uuid::new_v4());
let new_name = format!("New Name {}", uuid::Uuid::new_v4());
// Create profile
let _ = manager
.create_profile(&original_name, "firefox", "139.0", "stable", None, None)
.unwrap();
// Rename profile
let renamed_profile = manager.rename_profile(&original_name, &new_name).unwrap();
assert_eq!(renamed_profile.name, new_name);
// Verify old profile is gone and new one exists
let profiles = manager.list_profiles().unwrap();
assert!(profiles.iter().any(|p| p.name == new_name));
assert!(!profiles.iter().any(|p| p.name == original_name));
// Clean up
let _ = manager.delete_profile(&new_name);
}
#[test]
fn test_delete_profile() {
let (manager, _temp_dir) = create_test_profile_manager();
let unique_name = format!("To Delete {}", uuid::Uuid::new_v4());
// Create profile
let _ = manager
.create_profile(&unique_name, "firefox", "139.0", "stable", None, None)
.unwrap();
// Verify profile exists
let profiles_before = manager.list_profiles().unwrap();
assert!(profiles_before.iter().any(|p| p.name == unique_name));
// Delete profile
let delete_result = manager.delete_profile(&unique_name);
if let Err(e) = &delete_result {
println!("Delete profile error (may be expected in tests): {e}");
}
// Verify profile is gone
let profiles_after = manager.list_profiles().unwrap();
assert!(!profiles_after.iter().any(|p| p.name == unique_name));
}
#[test]
fn test_profile_name_sanitization() {
let (manager, _temp_dir) = create_test_profile_manager();
// Create profile with spaces and special characters
let profile = manager
.create_profile(
"Test Profile With Spaces",
"firefox",
"139.0",
"stable",
None,
None,
)
.unwrap();
// Profile path should contain UUID and end with /profile
let profiles_dir = manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
assert!(profile_data_path
.to_string_lossy()
.contains(&profile.id.to_string()));
assert!(profile_data_path.to_string_lossy().ends_with("/profile"));
// Profile name should remain unchanged
assert_eq!(profile.name, "Test Profile With Spaces");
// Profile should have a valid UUID
assert!(uuid::Uuid::parse_str(&profile.id.to_string()).is_ok());
}
#[test]
fn test_multiple_profiles() {
let (manager, _temp_dir) = create_test_profile_manager();
let profile1_name = format!("Profile 1 {}", uuid::Uuid::new_v4());
let profile2_name = format!("Profile 2 {}", uuid::Uuid::new_v4());
let profile3_name = format!("Profile 3 {}", uuid::Uuid::new_v4());
// Create multiple profiles
let _ = manager
.create_profile(&profile1_name, "firefox", "139.0", "stable", None, None)
.unwrap();
let _ = manager
.create_profile(&profile2_name, "chromium", "1465660", "stable", None, None)
.unwrap();
let _ = manager
.create_profile(&profile3_name, "brave", "v1.81.9", "stable", None, None)
.unwrap();
// List profiles and verify our profiles exist
let profiles = manager.list_profiles().unwrap();
let profile_names: Vec<&str> = profiles.iter().map(|p| p.name.as_str()).collect();
println!("Created profiles: {profile1_name}, {profile2_name}, {profile3_name}");
println!("Found profiles: {profile_names:?}");
assert!(profiles.iter().any(|p| p.name == profile1_name));
assert!(profiles.iter().any(|p| p.name == profile2_name));
assert!(profiles.iter().any(|p| p.name == profile3_name));
// Clean up
let _ = manager.delete_profile(&profile1_name);
let _ = manager.delete_profile(&profile2_name);
let _ = manager.delete_profile(&profile3_name);
}
#[test]
fn test_profile_validation() {
let (manager, _temp_dir) = create_test_profile_manager();
// Test that we can't rename to an existing profile name
let profile1_name = format!("Profile 1 {}", uuid::Uuid::new_v4());
let profile2_name = format!("Profile 2 {}", uuid::Uuid::new_v4());
let _ = manager
.create_profile(&profile1_name, "firefox", "139.0", "stable", None, None)
.unwrap();
let _ = manager
.create_profile(&profile2_name, "firefox", "139.0", "stable", None, None)
.unwrap();
// Try to rename profile2 to profile1's name (should fail)
let result = manager.rename_profile(&profile2_name, &profile1_name);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
// Clean up
let _ = manager.delete_profile(&profile1_name);
let _ = manager.delete_profile(&profile2_name);
}
#[test]
fn test_firefox_default_browser_preferences() {
let (manager, _temp_dir) = create_test_profile_manager();
// Create profile without proxy
let profile = manager
.create_profile(
"Test Firefox Preferences",
"firefox",
"139.0",
"stable",
None,
None,
)
.unwrap();
// Check that user.js file was created with default browser preference
let profiles_dir = manager.get_profiles_dir();
let profile_data_path = profile.get_profile_data_path(&profiles_dir);
let user_js_path = profile_data_path.join("user.js");
assert!(user_js_path.exists());
let user_js_content = std::fs::read_to_string(user_js_path).unwrap();
assert!(user_js_content.contains("browser.shell.checkDefaultBrowser"));
assert!(user_js_content.contains("false"));
// Verify automatic update disabling preferences are present
assert!(user_js_content.contains("app.update.enabled"));
assert!(user_js_content.contains("app.update.auto"));
// Create profile with proxy (proxy object unused in new architecture)
let _proxy = ProxySettings {
proxy_type: "http".to_string(),
host: "127.0.0.1".to_string(),
port: 8080,
username: None,
password: None,
};
let profile_with_proxy = manager
.create_profile(
"Test Firefox Preferences Proxy",
"firefox",
"139.0",
"stable",
None, // Tests now use separate proxy storage system
None, // No camoufox config for this test
)
.unwrap();
// Check that user.js file contains both proxy settings and default browser preference
let profile_with_proxy_data_path = profile_with_proxy.get_profile_data_path(&profiles_dir);
let user_js_path_proxy = profile_with_proxy_data_path.join("user.js");
assert!(user_js_path_proxy.exists());
let user_js_content_proxy = std::fs::read_to_string(user_js_path_proxy).unwrap();
assert!(user_js_content_proxy.contains("browser.shell.checkDefaultBrowser"));
assert!(user_js_content_proxy.contains("network.proxy.type"));
// Verify automatic update disabling preferences are present even with proxy
assert!(user_js_content_proxy.contains("app.update.enabled"));
assert!(user_js_content_proxy.contains("app.update.auto"));
}
}
// Global singleton instance
-331
View File
@@ -1,331 +0,0 @@
use serde::{Deserialize, Serialize};
use std::process::Command;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SystemLocale {
pub locale: String,
pub language: String,
pub country: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SystemTimezone {
pub timezone: String,
pub offset: String,
}
pub struct SystemUtils;
impl SystemUtils {
pub fn new() -> Self {
Self
}
/// Detect the system's locale settings
pub fn detect_system_locale(&self) -> SystemLocale {
#[cfg(target_os = "macos")]
return macos::detect_system_locale();
#[cfg(target_os = "linux")]
return linux::detect_system_locale();
#[cfg(target_os = "windows")]
return windows::detect_system_locale();
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
return SystemLocale {
locale: "en-US".to_string(),
language: "en".to_string(),
country: "US".to_string(),
};
}
/// Detect the system's timezone settings
pub fn detect_system_timezone(&self) -> SystemTimezone {
#[cfg(target_os = "macos")]
return macos::detect_system_timezone();
#[cfg(target_os = "linux")]
return linux::detect_system_timezone();
#[cfg(target_os = "windows")]
return windows::detect_system_timezone();
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
return SystemTimezone {
timezone: "UTC".to_string(),
offset: "+00:00".to_string(),
};
}
}
#[cfg(target_os = "macos")]
mod macos {
use super::*;
pub fn detect_system_locale() -> SystemLocale {
// Try to get the system locale from macOS
if let Ok(output) = Command::new("defaults")
.args(["read", "-g", "AppleLocale"])
.output()
{
if output.status.success() {
let locale_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
return parse_locale(&locale_str);
}
}
// Fallback to environment variables
detect_locale_from_env()
}
pub fn detect_system_timezone() -> SystemTimezone {
// Try to get timezone from macOS system
if let Ok(output) = Command::new("date").arg("+%Z").output() {
if output.status.success() {
let tz_abbr = String::from_utf8_lossy(&output.stdout).trim().to_string();
// Get the full timezone name
if let Ok(tz_output) = Command::new("systemsetup").args(["-gettimezone"]).output() {
if tz_output.status.success() {
let tz_full = String::from_utf8_lossy(&tz_output.stdout);
if let Some(tz_name) = tz_full.strip_prefix("Time Zone: ") {
let tz_clean = tz_name.trim().to_string();
if !tz_clean.is_empty() {
return SystemTimezone {
timezone: tz_clean,
offset: tz_abbr,
};
}
}
}
}
}
}
// Fallback to reading /etc/localtime link
detect_timezone_from_files()
}
}
#[cfg(target_os = "linux")]
mod linux {
use super::*;
pub fn detect_system_locale() -> SystemLocale {
// Try to get locale from locale command
if let Ok(output) = Command::new("locale").output() {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.starts_with("LANG=") {
let locale_value = line.strip_prefix("LANG=").unwrap_or("");
let locale_clean = locale_value.trim_matches('"');
return parse_locale(locale_clean);
}
}
}
}
// Fallback to environment variables
detect_locale_from_env()
}
pub fn detect_system_timezone() -> SystemTimezone {
// Try to read /etc/timezone first (Debian/Ubuntu)
if let Ok(tz_content) = std::fs::read_to_string("/etc/timezone") {
let tz_name = tz_content.trim().to_string();
if !tz_name.is_empty() {
return SystemTimezone {
timezone: tz_name,
offset: get_timezone_offset(),
};
}
}
// Try timedatectl (systemd systems)
if let Ok(output) = Command::new("timedatectl")
.args(["show", "--property=Timezone", "--value"])
.output()
{
if output.status.success() {
let tz_name = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !tz_name.is_empty() {
return SystemTimezone {
timezone: tz_name,
offset: get_timezone_offset(),
};
}
}
}
// Fallback to reading /etc/localtime symlink
detect_timezone_from_files()
}
fn get_timezone_offset() -> String {
if let Ok(output) = Command::new("date").arg("+%z").output() {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
"+00:00".to_string()
}
}
#[cfg(target_os = "windows")]
mod windows {
use super::*;
pub fn detect_system_locale() -> SystemLocale {
// Try to get locale from Windows registry/powershell
if let Ok(output) = Command::new("powershell")
.args([
"-Command",
"Get-Culture | Select-Object -ExpandProperty Name",
])
.output()
{
if output.status.success() {
let locale_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
return parse_locale(&locale_str);
}
}
// Fallback to environment variables
detect_locale_from_env()
}
pub fn detect_system_timezone() -> SystemTimezone {
// Try to get timezone from Windows
if let Ok(output) = Command::new("powershell")
.args([
"-Command",
"Get-TimeZone | Select-Object -ExpandProperty Id",
])
.output()
{
if output.status.success() {
let tz_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !tz_id.is_empty() {
return SystemTimezone {
timezone: tz_id,
offset: get_windows_timezone_offset(),
};
}
}
}
// Fallback
SystemTimezone {
timezone: "UTC".to_string(),
offset: "+00:00".to_string(),
}
}
fn get_windows_timezone_offset() -> String {
if let Ok(output) = Command::new("powershell")
.args([
"-Command",
"Get-TimeZone | Select-Object -ExpandProperty BaseUtcOffset",
])
.output()
{
if output.status.success() {
let offset_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
// Convert Windows offset format to standard format
if let Some(colon_pos) = offset_str.find(':') {
let hours = &offset_str[..colon_pos];
let minutes = &offset_str[colon_pos + 1..];
if let (Ok(h), Ok(m)) = (hours.parse::<i32>(), minutes.parse::<i32>()) {
return format!("{:+03}:{:02}", h, m);
}
}
}
}
"+00:00".to_string()
}
}
// Helper functions used across platforms
fn parse_locale(locale_str: &str) -> SystemLocale {
// Remove encoding suffix if present (e.g., "en_US.UTF-8" -> "en_US")
let locale_base = locale_str.split('.').next().unwrap_or(locale_str);
// Split language and country (e.g., "en_US" -> ["en", "US"])
let parts: Vec<&str> = locale_base.split(&['_', '-']).collect();
let language = parts.first().unwrap_or(&"en").to_string();
let country = parts.get(1).unwrap_or(&"US").to_string();
// Convert to standard format (e.g., "en-US")
let standard_locale = if parts.len() >= 2 {
format!("{}-{}", language, country.to_uppercase())
} else {
format!("{language}-US")
};
SystemLocale {
locale: standard_locale,
language,
country: country.to_uppercase(),
}
}
fn detect_locale_from_env() -> SystemLocale {
// Check environment variables in order of preference
let env_vars = ["LANG", "LC_ALL", "LC_CTYPE", "LANGUAGE"];
for var in &env_vars {
if let Ok(value) = std::env::var(var) {
if !value.is_empty() {
return parse_locale(&value);
}
}
}
// Default fallback
SystemLocale {
locale: "en-US".to_string(),
language: "en".to_string(),
country: "US".to_string(),
}
}
fn detect_timezone_from_files() -> SystemTimezone {
// Try to read timezone from /etc/localtime symlink
if let Ok(link_target) = std::fs::read_link("/etc/localtime") {
if let Some(tz_path) = link_target.to_str() {
// Extract timezone name from path like /usr/share/zoneinfo/America/New_York
if let Some(zoneinfo_pos) = tz_path.find("zoneinfo/") {
let tz_name = &tz_path[zoneinfo_pos + 9..];
if !tz_name.is_empty() {
return SystemTimezone {
timezone: tz_name.to_string(),
offset: "+00:00".to_string(), // Could be improved with actual offset calculation
};
}
}
}
}
// Default fallback
SystemTimezone {
timezone: "UTC".to_string(),
offset: "+00:00".to_string(),
}
}
/// Tauri command to get system locale
#[tauri::command]
pub async fn get_system_locale() -> Result<SystemLocale, String> {
let utils = SystemUtils::new();
Ok(utils.detect_system_locale())
}
/// Tauri command to get system timezone
#[tauri::command]
pub async fn get_system_timezone() -> Result<SystemTimezone, String> {
let utils = SystemUtils::new();
Ok(utils.detect_system_timezone())
}