mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-07 23:43:57 +02:00
feat: fully implement happy flow for persistant fingerprint generation
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user