diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 82eaa6f..7597143 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1204,6 +1204,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "deadpool" version = "0.12.3" @@ -1403,6 +1409,7 @@ dependencies = [ "maxminddb", "mime_guess", "msi-extract", + "nix 0.29.0", "objc2", "objc2-app-kit", "once_cell", @@ -1431,6 +1438,7 @@ dependencies = [ "tempfile", "thiserror 1.0.69", "tokio", + "tokio-tungstenite", "tower", "tower-http", "url", @@ -3185,6 +3193,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nix" version = "0.30.1" @@ -6055,6 +6075,20 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "tokio-tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -6280,6 +6314,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.2", + "sha1", + "thiserror 2.0.17", + "utf-8", +] + [[package]] name = "typeid" version = "1.0.3" @@ -7518,7 +7570,7 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "nix 0.30.1", "ordered-stream", "serde", "serde_repr", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0072abf..f044ae3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -81,6 +81,9 @@ async-socks5 = "0.6" # Camoufox/Playwright integration playwright = { git = "https://github.com/sctg-development/playwright-rust", branch = "master" } + +# Wayfern CDP integration +tokio-tungstenite = { version = "0.27", features = ["native-tls"] } rusqlite = { version = "0.32", features = ["bundled"] } serde_yaml = "0.9" thiserror = "1.0" @@ -92,6 +95,9 @@ quick-xml = { version = "0.37", features = ["serialize"] } [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies] tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } +[target.'cfg(unix)'.dependencies] +nix = { version = "0.29", features = ["signal", "process"] } + [target.'cfg(target_os = "macos")'.dependencies] core-foundation = "0.10" objc2 = "0.6.1" diff --git a/src-tauri/src/api_client.rs b/src-tauri/src/api_client.rs index dc82e59..88d015c 100644 --- a/src-tauri/src/api_client.rs +++ b/src-tauri/src/api_client.rs @@ -300,6 +300,10 @@ pub fn is_browser_version_nightly( // For Camoufox, beta versions are actually the stable releases false } + "wayfern" => { + // For Wayfern, all releases from version.json are stable + false + } _ => { // Default fallback is_nightly_version(version) @@ -330,6 +334,13 @@ pub struct BrowserRelease { pub is_prerelease: bool, } +/// Wayfern version info from https://download.wayfern.com/version.json +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct WayfernVersionInfo { + pub version: String, + pub downloads: HashMap>, +} + #[derive(Debug, Serialize, Deserialize)] struct CachedVersionData { releases: Vec, @@ -342,6 +353,12 @@ struct CachedGithubData { timestamp: u64, } +#[derive(Debug, Serialize, Deserialize)] +struct CachedWayfernData { + version_info: WayfernVersionInfo, + timestamp: u64, +} + pub struct ApiClient { client: Client, firefox_api_base: String, @@ -1065,6 +1082,95 @@ impl ApiClient { Ok(compatible_releases) } + fn load_cached_wayfern_version(&self) -> Option { + let cache_dir = Self::get_cache_dir().ok()?; + let cache_file = cache_dir.join("wayfern_version.json"); + + if !cache_file.exists() { + return None; + } + + let content = fs::read_to_string(&cache_file).ok()?; + let cached_data: CachedWayfernData = serde_json::from_str(&content).ok()?; + + // Always use cached Wayfern version - cache never expires, only gets updated + Some(cached_data.version_info) + } + + fn save_cached_wayfern_version( + &self, + version_info: &WayfernVersionInfo, + ) -> Result<(), Box> { + let cache_dir = Self::get_cache_dir()?; + let cache_file = cache_dir.join("wayfern_version.json"); + + let cached_data = CachedWayfernData { + version_info: version_info.clone(), + timestamp: Self::get_current_timestamp(), + }; + + let content = serde_json::to_string_pretty(&cached_data)?; + fs::write(&cache_file, content)?; + log::info!("Cached Wayfern version: {}", version_info.version); + Ok(()) + } + + /// Fetch Wayfern version info from https://download.wayfern.com/version.json + pub async fn fetch_wayfern_version_with_caching( + &self, + no_caching: bool, + ) -> Result> { + // Check cache first (unless bypassing) + if !no_caching { + if let Some(cached_version) = self.load_cached_wayfern_version() { + log::info!("Using cached Wayfern version: {}", cached_version.version); + return Ok(cached_version); + } + } + + log::info!("Fetching Wayfern version from https://download.wayfern.com/version.json"); + let url = "https://download.wayfern.com/version.json"; + + let response = self + .client + .get(url) + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36") + .send() + .await?; + + if !response.status().is_success() { + return Err(format!("Failed to fetch Wayfern version: {}", response.status()).into()); + } + + let version_info: WayfernVersionInfo = response.json().await?; + log::info!("Fetched Wayfern version: {}", version_info.version); + + // Cache the results (unless bypassing cache) + if !no_caching { + if let Err(e) = self.save_cached_wayfern_version(&version_info) { + log::error!("Failed to cache Wayfern version: {e}"); + } + } + + Ok(version_info) + } + + /// Get the download URL for Wayfern based on current platform + pub fn get_wayfern_download_url(&self, version_info: &WayfernVersionInfo) -> Option { + let (os, arch) = Self::get_platform_info(); + let platform_key = format!("{os}-{arch}"); + + version_info + .downloads + .get(&platform_key) + .and_then(|url| url.clone()) + } + + /// Check if Wayfern has a compatible download for current platform + pub fn has_wayfern_compatible_download(&self, version_info: &WayfernVersionInfo) -> bool { + self.get_wayfern_download_url(version_info).is_some() + } + /// Check if a Zen twilight release has been updated by comparing file size pub async fn check_twilight_update( &self, diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index 7572679..62642c5 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -60,6 +60,8 @@ pub struct CreateProfileRequest { pub release_type: Option, #[schema(value_type = Object)] pub camoufox_config: Option, + #[schema(value_type = Object)] + pub wayfern_config: Option, pub group_id: Option, pub tags: Option>, } @@ -560,6 +562,13 @@ async fn create_profile( None }; + // Parse wayfern config if provided + let wayfern_config = if let Some(config) = &request.wayfern_config { + serde_json::from_value(config.clone()).ok() + } else { + None + }; + // Create profile using the async create_profile_with_group method match profile_manager .create_profile_with_group( @@ -570,6 +579,7 @@ async fn create_profile( request.release_type.as_deref().unwrap_or("stable"), request.proxy_id.clone(), camoufox_config, + wayfern_config, request.group_id.clone(), ) .await diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index f94a09d..05bb498 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -515,6 +515,7 @@ mod tests { last_launch: None, release_type: "stable".to_string(), camoufox_config: None, + wayfern_config: None, group_id: None, tags: Vec::new(), note: None, diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index c9c6e44..cd5c6f8 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -19,6 +19,7 @@ pub enum BrowserType { Brave, Zen, Camoufox, + Wayfern, } impl BrowserType { @@ -30,6 +31,7 @@ impl BrowserType { BrowserType::Brave => "brave", BrowserType::Zen => "zen", BrowserType::Camoufox => "camoufox", + BrowserType::Wayfern => "wayfern", } } @@ -41,6 +43,7 @@ impl BrowserType { "brave" => Ok(BrowserType::Brave), "zen" => Ok(BrowserType::Zen), "camoufox" => Ok(BrowserType::Camoufox), + "wayfern" => Ok(BrowserType::Wayfern), _ => Err(format!("Unknown browser type: {s}")), } } @@ -225,6 +228,47 @@ mod macos { Ok(executable_path) } + pub fn get_wayfern_executable_path( + install_dir: &Path, + ) -> Result> { + // Wayfern is Chromium-based, look for Chromium.app + // Find the .app directory + let app_path = std::fs::read_dir(install_dir)? + .filter_map(Result::ok) + .find(|entry| entry.path().extension().is_some_and(|ext| ext == "app")) + .ok_or("Wayfern app not found")?; + + // Construct the browser executable path + let mut executable_dir = app_path.path(); + executable_dir.push("Contents"); + executable_dir.push("MacOS"); + + // Find the Chromium executable + let executable_path = std::fs::read_dir(&executable_dir)? + .filter_map(Result::ok) + .find(|entry| { + let binding = entry.file_name(); + let name = binding.to_string_lossy(); + name.contains("Chromium") || name == "Wayfern" + }) + .map(|entry| entry.path()) + .ok_or("No Wayfern executable found in MacOS directory")?; + + Ok(executable_path) + } + + pub fn is_wayfern_version_downloaded(install_dir: &Path) -> bool { + // On macOS, check for .app files (Chromium.app) + if let Ok(entries) = std::fs::read_dir(install_dir) { + for entry in entries.flatten() { + if entry.path().extension().is_some_and(|ext| ext == "app") { + return true; + } + } + } + false + } + pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool { // On macOS, check for .app files if let Ok(entries) = std::fs::read_dir(install_dir) { @@ -340,6 +384,16 @@ mod linux { install_dir.join("brave-browser").join("brave"), install_dir.join("bin").join("brave"), ], + BrowserType::Wayfern => vec![ + // Wayfern extracts to a directory with chromium executable + install_dir.join("chromium"), + install_dir.join("chrome"), + install_dir.join("wayfern"), + // Subdirectory paths (tar.xz may extract to a subdirectory) + install_dir.join("wayfern").join("chromium"), + install_dir.join("wayfern").join("chrome"), + install_dir.join("chrome-linux").join("chrome"), + ], _ => vec![], }; @@ -424,6 +478,16 @@ mod linux { install_dir.join("brave-browser").join("brave"), install_dir.join("bin").join("brave"), ], + BrowserType::Wayfern => vec![ + // Wayfern extracts to a directory with chromium executable + install_dir.join("chromium"), + install_dir.join("chrome"), + install_dir.join("wayfern"), + // Subdirectory paths + install_dir.join("wayfern").join("chromium"), + install_dir.join("wayfern").join("chrome"), + install_dir.join("chrome-linux").join("chrome"), + ], _ => vec![], }; @@ -521,6 +585,16 @@ mod windows { install_dir.join("brave").join("brave.exe"), install_dir.join("brave-browser").join("brave.exe"), ], + BrowserType::Wayfern => vec![ + install_dir.join("chromium.exe"), + install_dir.join("chrome.exe"), + install_dir.join("wayfern.exe"), + install_dir.join("bin").join("chromium.exe"), + // Subdirectory patterns + install_dir.join("wayfern").join("chromium.exe"), + install_dir.join("wayfern").join("chrome.exe"), + install_dir.join("chrome-win").join("chrome.exe"), + ], _ => vec![], }; @@ -536,14 +610,18 @@ mod windows { let path = entry.path(); if path.extension().is_some_and(|ext| ext == "exe") { let name = path.file_stem().unwrap_or_default().to_string_lossy(); - if name.contains("chromium") || name.contains("brave") || name.contains("chrome") { + if name.contains("chromium") + || name.contains("brave") + || name.contains("chrome") + || name.contains("wayfern") + { return Ok(path); } } } } - Err("Chromium/Brave executable not found in Windows installation directory".into()) + Err("Chromium/Brave/Wayfern executable not found in Windows installation directory".into()) } pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool { @@ -602,6 +680,16 @@ mod windows { install_dir.join("brave").join("brave.exe"), install_dir.join("brave-browser").join("brave.exe"), ], + BrowserType::Wayfern => vec![ + install_dir.join("chromium.exe"), + install_dir.join("chrome.exe"), + install_dir.join("wayfern.exe"), + install_dir.join("bin").join("chromium.exe"), + // Subdirectory patterns + install_dir.join("wayfern").join("chromium.exe"), + install_dir.join("wayfern").join("chrome.exe"), + install_dir.join("chrome-win").join("chrome.exe"), + ], _ => vec![], }; @@ -618,7 +706,11 @@ mod windows { if path.extension().is_some_and(|ext| ext == "exe") { let name = path.file_stem().unwrap_or_default().to_string_lossy(); - if name.contains("chromium") || name.contains("brave") || name.contains("chrome") { + if name.contains("chromium") + || name.contains("brave") + || name.contains("chrome") + || name.contains("wayfern") + { return true; } } @@ -946,6 +1038,114 @@ impl Browser for CamoufoxBrowser { } } +/// Wayfern is a Chromium-based anti-detect browser with CDP-based fingerprint injection +pub struct WayfernBrowser; + +impl WayfernBrowser { + pub fn new() -> Self { + Self + } +} + +impl Browser for WayfernBrowser { + fn get_executable_path(&self, install_dir: &Path) -> Result> { + #[cfg(target_os = "macos")] + return macos::get_wayfern_executable_path(install_dir); + + #[cfg(target_os = "linux")] + return linux::get_chromium_executable_path(install_dir, &BrowserType::Wayfern); + + #[cfg(target_os = "windows")] + return windows::get_chromium_executable_path(install_dir, &BrowserType::Wayfern); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + Err("Unsupported platform".into()) + } + + fn create_launch_args( + &self, + profile_path: &str, + proxy_settings: Option<&ProxySettings>, + url: Option, + remote_debugging_port: Option, + headless: bool, + ) -> Result, Box> { + // Wayfern uses Chromium-style arguments + let mut args = vec![ + format!("--user-data-dir={}", profile_path), + "--no-default-browser-check".to_string(), + "--disable-background-mode".to_string(), + "--disable-component-update".to_string(), + "--disable-background-timer-throttling".to_string(), + "--crash-server-url=".to_string(), + "--disable-updater".to_string(), + "--disable-session-crashed-bubble".to_string(), + "--hide-crash-restore-bubble".to_string(), + "--disable-infobars".to_string(), + "--disable-quic".to_string(), + // Wayfern-specific args for automation + "--disable-features=DialMediaRouteProvider".to_string(), + "--use-mock-keychain".to_string(), + "--password-store=basic".to_string(), + ]; + + // Add remote debugging port (required for CDP fingerprint injection) + if let Some(port) = remote_debugging_port { + args.push("--remote-debugging-address=127.0.0.1".to_string()); + args.push(format!("--remote-debugging-port={port}")); + } + + // Add headless mode if requested + if headless { + args.push("--headless=new".to_string()); + } + + // Add proxy configuration if provided + if let Some(proxy) = proxy_settings { + args.push(format!( + "--proxy-server=http://{}:{}", + proxy.host, proxy.port + )); + } + + if let Some(url) = url { + args.push(url); + } + + Ok(args) + } + + fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { + let install_dir = binaries_dir.join("wayfern").join(version); + + #[cfg(target_os = "macos")] + return macos::is_wayfern_version_downloaded(&install_dir); + + #[cfg(target_os = "linux")] + return linux::is_chromium_version_downloaded(&install_dir, &BrowserType::Wayfern); + + #[cfg(target_os = "windows")] + return windows::is_chromium_version_downloaded(&install_dir, &BrowserType::Wayfern); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + false + } + + fn prepare_executable(&self, executable_path: &Path) -> Result<(), Box> { + #[cfg(target_os = "macos")] + return macos::prepare_executable(executable_path); + + #[cfg(target_os = "linux")] + return linux::prepare_executable(executable_path); + + #[cfg(target_os = "windows")] + return windows::prepare_executable(executable_path); + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + Err("Unsupported platform".into()) + } +} + pub struct BrowserFactory; impl BrowserFactory { @@ -964,6 +1164,7 @@ impl BrowserFactory { } BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()), + BrowserType::Wayfern => Box::new(WayfernBrowser::new()), } } } @@ -1045,6 +1246,7 @@ mod tests { assert_eq!(BrowserType::Brave.as_str(), "brave"); assert_eq!(BrowserType::Zen.as_str(), "zen"); assert_eq!(BrowserType::Camoufox.as_str(), "camoufox"); + assert_eq!(BrowserType::Wayfern.as_str(), "wayfern"); // Test from_str - use expect with descriptive messages instead of unwrap assert_eq!( @@ -1071,6 +1273,10 @@ mod tests { BrowserType::from_str("camoufox").expect("camoufox should be valid"), BrowserType::Camoufox ); + assert_eq!( + BrowserType::from_str("wayfern").expect("wayfern should be valid"), + BrowserType::Wayfern + ); // Test invalid browser type - these should properly fail let invalid_result = BrowserType::from_str("invalid"); diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 0b8accf..ee8d6b1 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -4,6 +4,7 @@ use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::platform_browser; use crate::profile::{BrowserProfile, ProfileManager}; use crate::proxy_manager::PROXY_MANAGER; +use crate::wayfern_manager::{WayfernConfig, WayfernManager}; use directories::BaseDirs; use serde::Serialize; use std::path::PathBuf; @@ -16,6 +17,7 @@ pub struct BrowserRunner { pub downloaded_browsers_registry: &'static DownloadedBrowsersRegistry, auto_updater: &'static crate::auto_updater::AutoUpdater, camoufox_manager: &'static CamoufoxManager, + wayfern_manager: &'static WayfernManager, } impl BrowserRunner { @@ -26,6 +28,7 @@ impl BrowserRunner { downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(), auto_updater: crate::auto_updater::AutoUpdater::instance(), camoufox_manager: CamoufoxManager::instance(), + wayfern_manager: WayfernManager::instance(), } } @@ -297,6 +300,204 @@ impl BrowserRunner { return Ok(updated_profile); } + // Handle Wayfern profiles using WayfernManager + if profile.browser == "wayfern" { + // Get or create wayfern config + let mut wayfern_config = profile.wayfern_config.clone().unwrap_or_else(|| { + log::info!( + "No wayfern config found for profile {}, using default", + profile.name + ); + WayfernConfig::default() + }); + + // Always start a local proxy for Wayfern (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)); + + log::info!( + "Starting local proxy for Wayfern 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 + // If proxy startup fails, DO NOT launch Wayfern - it requires local proxy + let profile_id_str = profile.id.to_string(); + 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_id_str), + ) + .await + .map_err(|e| { + let error_msg = format!("Failed to start local proxy for Wayfern: {e}"); + log::error!("{}", error_msg); + error_msg + })?; + + // Format proxy URL for wayfern - always use HTTP for the local proxy + let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port); + + // Set proxy in wayfern config + wayfern_config.proxy = Some(proxy_url); + + log::info!( + "Configured local proxy for Wayfern: {:?}", + wayfern_config.proxy + ); + + // Check if we need to generate a new fingerprint on every launch + let mut updated_profile = profile.clone(); + if wayfern_config.randomize_fingerprint_on_launch == Some(true) { + log::info!( + "Generating random fingerprint for Wayfern profile: {}", + profile.name + ); + + // Create a config copy without the existing fingerprint to force generation of a new one + let mut config_for_generation = wayfern_config.clone(); + config_for_generation.fingerprint = None; + + // Generate a new fingerprint + let new_fingerprint = self + .wayfern_manager + .generate_fingerprint_config(&app_handle, profile, &config_for_generation) + .await + .map_err(|e| format!("Failed to generate random fingerprint: {e}"))?; + + log::info!( + "New fingerprint generated, length: {} chars", + new_fingerprint.len() + ); + + // Update the config with the new fingerprint for launching + wayfern_config.fingerprint = Some(new_fingerprint.clone()); + + // Save the updated fingerprint to the profile so it persists + let mut updated_wayfern_config = updated_profile.wayfern_config.clone().unwrap_or_default(); + updated_wayfern_config.fingerprint = Some(new_fingerprint); + updated_wayfern_config.randomize_fingerprint_on_launch = Some(true); + if wayfern_config.os.is_some() { + updated_wayfern_config.os = wayfern_config.os.clone(); + } + updated_profile.wayfern_config = Some(updated_wayfern_config.clone()); + + log::info!( + "Updated profile wayfern_config with new fingerprint for profile: {}, fingerprint length: {}", + profile.name, + updated_wayfern_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0) + ); + } + + // Launch Wayfern browser + log::info!("Launching Wayfern for profile: {}", profile.name); + + // Get profile path for Wayfern + let profiles_dir = self.profile_manager.get_profiles_dir(); + let profile_data_path = updated_profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_data_path.to_string_lossy().to_string(); + + // Get proxy URL from config + let proxy_url = wayfern_config.proxy.as_deref(); + + let wayfern_result = self + .wayfern_manager + .launch_wayfern( + &app_handle, + &updated_profile, + &profile_path_str, + &wayfern_config, + url.as_deref(), + proxy_url, + ) + .await + .map_err(|e| -> Box { + format!("Failed to launch Wayfern: {e}").into() + })?; + + // Get the process ID from launch result + let process_id = wayfern_result.processId.unwrap_or(0); + log::info!("Wayfern launched successfully with PID: {process_id}"); + + // Update profile with the process info + updated_profile.process_id = Some(process_id); + updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); + + // Update the proxy manager with the correct PID + if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, process_id) { + log::warn!("Warning: Failed to update proxy PID mapping: {e}"); + } else { + log::info!("Updated proxy PID mapping from temp (0) to actual PID: {process_id}"); + } + + // Save the updated profile + log::info!( + "Saving profile {} with wayfern_config fingerprint length: {}", + updated_profile.name, + updated_profile + .wayfern_config + .as_ref() + .and_then(|c| c.fingerprint.as_ref()) + .map(|f| f.len()) + .unwrap_or(0) + ); + self.save_process_info(&updated_profile)?; + let _ = crate::tag_manager::TAG_MANAGER.lock().map(|tm| { + let _ = tm.rebuild_from_profiles(&self.profile_manager.list_profiles().unwrap_or_default()); + }); + log::info!( + "Successfully saved profile with process info: {}", + updated_profile.name + ); + + // Emit profiles-changed to trigger frontend to reload profiles from disk + if let Err(e) = app_handle.emit("profiles-changed", ()) { + log::warn!("Warning: Failed to emit profiles-changed event: {e}"); + } + + log::info!( + "Emitting profile events for successful Wayfern launch: {}", + updated_profile.name + ); + + // Emit profile update event to frontend + if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { + log::warn!("Warning: Failed to emit profile update event: {e}"); + } + + // Emit minimal running changed event to frontend + #[derive(Serialize)] + struct RunningChangedPayload { + id: String, + is_running: bool, + } + + let payload = RunningChangedPayload { + id: updated_profile.id.to_string(), + is_running: updated_profile.process_id.is_some(), + }; + + if let Err(e) = app_handle.emit("profile-running-changed", &payload) { + log::warn!("Warning: Failed to emit profile running changed event: {e}"); + } else { + log::info!( + "Successfully emitted profile-running-changed event for Wayfern {}: running={}", + updated_profile.name, + payload.is_running + ); + } + + return Ok(updated_profile); + } + // Create browser instance let browser_type = BrowserType::from_str(&profile.browser) .map_err(|_| format!("Invalid browser type: {}", profile.browser))?; @@ -577,7 +778,35 @@ impl BrowserRunner { } } - // Use the comprehensive browser status check for non-camoufox browsers + // Handle Wayfern profiles using WayfernManager + if profile.browser == "wayfern" { + let profiles_dir = self.profile_manager.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_data_path.to_string_lossy(); + + // Check if the process is running + match self + .wayfern_manager + .find_wayfern_by_profile(&profile_path_str) + .await + { + Some(_wayfern_process) => { + log::info!( + "Opening URL in existing Wayfern process for profile: {} (ID: {})", + profile.name, + profile.id + ); + + // For Wayfern, we can use CDP to navigate to the URL + return Err("Wayfern doesn't currently support opening URLs in existing instances. Please close the browser and launch again with the URL.".into()); + } + None => { + return Err("Wayfern browser is not running".into()); + } + } + } + + // Use the comprehensive browser status check for non-camoufox/wayfern browsers let is_running = self .check_browser_status(app_handle.clone(), profile) .await?; @@ -657,6 +886,10 @@ impl BrowserRunner { // Camoufox URL opening is handled differently Err("URL opening in existing Camoufox instance is not supported".into()) } + BrowserType::Wayfern => { + // Wayfern URL opening is handled differently + Err("URL opening in existing Wayfern instance is not supported".into()) + } BrowserType::Chromium | BrowserType::Brave => { #[cfg(target_os = "macos")] { @@ -1382,7 +1615,325 @@ impl BrowserRunner { return Ok(()); } - // For non-camoufox browsers, use the existing logic + // Handle Wayfern profiles using WayfernManager + if profile.browser == "wayfern" { + let profiles_dir = self.profile_manager.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_data_path.to_string_lossy(); + + log::info!( + "Attempting to kill Wayfern process for profile: {} (ID: {})", + profile.name, + profile.id + ); + + // Stop the proxy associated with this profile first + let profile_id_str = profile.id.to_string(); + if let Err(e) = PROXY_MANAGER + .stop_proxy_by_profile_id(app_handle.clone(), &profile_id_str) + .await + { + log::warn!( + "Warning: Failed to stop proxy for profile {}: {e}", + profile_id_str + ); + } + + let mut process_actually_stopped = false; + match self + .wayfern_manager + .find_wayfern_by_profile(&profile_path_str) + .await + { + Some(wayfern_process) => { + log::info!( + "Found Wayfern process: {} (PID: {:?})", + wayfern_process.id, + wayfern_process.processId + ); + + match self.wayfern_manager.stop_wayfern(&wayfern_process.id).await { + Ok(_) => { + if let Some(pid) = wayfern_process.processId { + // Verify the process actually died by checking after a short delay + use tokio::time::{sleep, Duration}; + sleep(Duration::from_millis(500)).await; + + use sysinfo::{Pid, System}; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + + if process_actually_stopped { + log::info!( + "Successfully stopped Wayfern process: {} (PID: {:?}) - verified process is dead", + wayfern_process.id, + pid + ); + } else { + log::warn!( + "Wayfern stop command returned success but process {} (PID: {:?}) is still running - forcing kill", + wayfern_process.id, + pid + ); + // Force kill the process + #[cfg(target_os = "macos")] + { + use crate::platform_browser; + if let Err(e) = platform_browser::macos::kill_browser_process_impl( + pid, + Some(&profile_path_str), + ) + .await + { + log::error!("Failed to force kill Wayfern process {}: {}", pid, e); + } else { + sleep(Duration::from_millis(500)).await; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + if process_actually_stopped { + log::info!( + "Successfully force killed Wayfern process {} (PID: {:?})", + wayfern_process.id, + pid + ); + } + } + } + #[cfg(target_os = "linux")] + { + use crate::platform_browser; + if let Err(e) = platform_browser::linux::kill_browser_process_impl(pid).await { + log::error!("Failed to force kill Wayfern process {}: {}", pid, e); + } else { + sleep(Duration::from_millis(500)).await; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + if process_actually_stopped { + log::info!( + "Successfully force killed Wayfern process {} (PID: {:?})", + wayfern_process.id, + pid + ); + } + } + } + #[cfg(target_os = "windows")] + { + use crate::platform_browser; + if let Err(e) = platform_browser::windows::kill_browser_process_impl(pid).await + { + log::error!("Failed to force kill Wayfern process {}: {}", pid, e); + } else { + sleep(Duration::from_millis(500)).await; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + if process_actually_stopped { + log::info!( + "Successfully force killed Wayfern process {} (PID: {:?})", + wayfern_process.id, + pid + ); + } + } + } + } + } else { + process_actually_stopped = true; + } + } + Err(e) => { + log::error!( + "Error stopping Wayfern process {}: {}", + wayfern_process.id, + e + ); + // Try to force kill if we have a PID + if let Some(pid) = wayfern_process.processId { + log::info!( + "Attempting force kill after stop_wayfern error for PID: {}", + pid + ); + #[cfg(target_os = "macos")] + { + use crate::platform_browser; + if let Err(kill_err) = + platform_browser::macos::kill_browser_process_impl(pid, Some(&profile_path_str)) + .await + { + log::error!("Failed to force kill Wayfern process {}: {}", pid, kill_err); + } else { + use tokio::time::{sleep, Duration}; + sleep(Duration::from_millis(500)).await; + use sysinfo::{Pid, System}; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + } + } + #[cfg(target_os = "linux")] + { + use crate::platform_browser; + if let Err(kill_err) = + platform_browser::linux::kill_browser_process_impl(pid).await + { + log::error!("Failed to force kill Wayfern process {}: {}", pid, kill_err); + } else { + use tokio::time::{sleep, Duration}; + sleep(Duration::from_millis(500)).await; + use sysinfo::{Pid, System}; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + } + } + #[cfg(target_os = "windows")] + { + use crate::platform_browser; + if let Err(kill_err) = + platform_browser::windows::kill_browser_process_impl(pid).await + { + log::error!("Failed to force kill Wayfern process {}: {}", pid, kill_err); + } else { + use tokio::time::{sleep, Duration}; + sleep(Duration::from_millis(500)).await; + use sysinfo::{Pid, System}; + let system = System::new_all(); + process_actually_stopped = system.process(Pid::from(pid as usize)).is_none(); + } + } + } + } + } + } + None => { + log::info!( + "No running Wayfern process found for profile: {} (ID: {})", + profile.name, + profile.id + ); + process_actually_stopped = true; + } + } + + // If process wasn't confirmed stopped, return an error + if !process_actually_stopped { + log::error!( + "Failed to stop Wayfern process for profile: {} (ID: {}) - process may still be running", + profile.name, + profile.id + ); + return Err( + format!( + "Failed to stop Wayfern process for profile {} - process may still be running", + profile.name + ) + .into(), + ); + } + + // Clear the process ID from the profile + let mut updated_profile = profile.clone(); + updated_profile.process_id = None; + + // Check for pending updates and apply them + if let Ok(Some(pending_update)) = self + .auto_updater + .get_pending_update(&profile.browser, &profile.version) + { + log::info!( + "Found pending update for Wayfern profile {}: {} -> {}", + profile.name, + profile.version, + pending_update.new_version + ); + + match self.profile_manager.update_profile_version( + &app_handle, + &profile.id.to_string(), + &pending_update.new_version, + ) { + Ok(updated_profile_after_update) => { + log::info!( + "Successfully updated Wayfern profile {} from version {} to {}", + profile.name, + profile.version, + pending_update.new_version + ); + updated_profile = updated_profile_after_update; + + if let Err(e) = self + .auto_updater + .dismiss_update_notification(&pending_update.id) + { + log::warn!("Warning: Failed to dismiss pending update notification: {e}"); + } + } + Err(e) => { + log::error!( + "Failed to apply pending update for Wayfern profile {}: {}", + profile.name, + e + ); + } + } + } + + self + .save_process_info(&updated_profile) + .map_err(|e| format!("Failed to update profile: {e}"))?; + + log::info!( + "Emitting profile events for successful Wayfern kill: {}", + updated_profile.name + ); + + // Emit profile update event to frontend + if let Err(e) = app_handle.emit("profile-updated", &updated_profile) { + log::warn!("Warning: Failed to emit profile update event: {e}"); + } + + // Emit minimal running changed event + #[derive(Serialize)] + struct RunningChangedPayload { + id: String, + is_running: bool, + } + let payload = RunningChangedPayload { + id: updated_profile.id.to_string(), + is_running: false, + }; + + if let Err(e) = app_handle.emit("profile-running-changed", &payload) { + log::warn!("Warning: Failed to emit profile running changed event: {e}"); + } else { + log::info!( + "Successfully emitted profile-running-changed event for Wayfern {}: running={}", + updated_profile.name, + payload.is_running + ); + } + + log::info!( + "Wayfern process cleanup completed for profile: {} (ID: {})", + profile.name, + profile.id + ); + + // Consolidate browser versions after stopping a browser + if let Ok(consolidated) = self + .downloaded_browsers_registry + .consolidate_browser_versions(&app_handle) + { + if !consolidated.is_empty() { + log::info!("Post-stop version consolidation results:"); + for action in &consolidated { + log::info!(" {action}"); + } + } + } + + return Ok(()); + } + + // For non-camoufox/wayfern browsers, use the existing logic let pid = if let Some(pid) = profile.process_id { // First verify the stored PID is still valid and belongs to our profile let system = System::new_all(); @@ -1832,9 +2383,9 @@ pub async fn launch_browser_profile( profile_for_launch.id ); - // Always start a local proxy before launching (non-Camoufox handled here; Camoufox has its own flow) + // Always start a local proxy before launching (non-Camoufox/Wayfern handled here; they have their own flow) // This ensures all traffic goes through the local proxy for monitoring and future features - if profile.browser != "camoufox" { + if profile.browser != "camoufox" && profile.browser != "wayfern" { // Determine upstream proxy if configured; otherwise use DIRECT (no upstream) let upstream_proxy = profile_for_launch .proxy_id diff --git a/src-tauri/src/browser_version_manager.rs b/src-tauri/src/browser_version_manager.rs index b04da14..8a68277 100644 --- a/src-tauri/src/browser_version_manager.rs +++ b/src-tauri/src/browser_version_manager.rs @@ -74,6 +74,22 @@ impl BrowserVersionManager { // Camoufox supports all platforms and architectures according to the JS code Ok(true) } + "wayfern" => { + // Wayfern support depends on version.json downloads availability + // Currently supports macos-arm64 and linux-x64 + let platform_key = format!("{os}-{arch}"); + // Check dynamically, but allow the browser to appear even if platform not available yet + // The actual download will fail gracefully if not supported + Ok(matches!( + platform_key.as_str(), + "macos-arm64" + | "linux-x64" + | "macos-x64" + | "linux-arm64" + | "windows-x64" + | "windows-arm64" + )) + } _ => Err(format!("Unknown browser: {browser}").into()), } } @@ -87,6 +103,7 @@ impl BrowserVersionManager { "brave", "chromium", "camoufox", + "wayfern", ]; all_browsers @@ -224,6 +241,7 @@ impl BrowserVersionManager { "brave" => self.fetch_brave_versions(true).await?, "chromium" => self.fetch_chromium_versions(true).await?, "camoufox" => self.fetch_camoufox_versions(true).await?, + "wayfern" => self.fetch_wayfern_versions(true).await?, _ => return Err(format!("Unsupported browser: {browser}").into()), }; @@ -424,6 +442,17 @@ impl BrowserVersionManager { }) .collect() } + "wayfern" => { + // Wayfern only has one version from version.json + merged_versions + .into_iter() + .map(|version| BrowserVersionInfo { + version: version.clone(), + is_prerelease: false, // Wayfern releases are always stable + date: "".to_string(), + }) + .collect() + } _ => { return Err(format!("Unsupported browser: {browser}").into()); } @@ -647,6 +676,31 @@ impl BrowserVersionManager { is_archive: true, }) } + "wayfern" => { + // Wayfern downloads from https://download.wayfern.com/ + // File naming: wayfern-{chromium_version}-{platform}-{arch}.{ext} + // Platform/arch format: linux-x64, macos-arm64, etc. + let platform_key = format!("{os}-{arch}"); + let (filename, is_archive) = match platform_key.as_str() { + "macos-arm64" | "macos-x64" => (format!("wayfern-{version}-{platform_key}.dmg"), true), + "linux-x64" | "linux-arm64" => (format!("wayfern-{version}-{platform_key}.tar.xz"), true), + "windows-x64" | "windows-arm64" => { + (format!("wayfern-{version}-{platform_key}.exe"), false) + } + _ => { + return Err( + format!("Unsupported platform/architecture for Wayfern: {os}/{arch}").into(), + ) + } + }; + + // Note: The actual URL will be resolved dynamically from version.json in downloader.rs + Ok(DownloadInfo { + url: format!("https://download.wayfern.com/{filename}"), + filename, + is_archive, + }) + } _ => Err(format!("Unsupported browser: {browser}").into()), } } @@ -820,6 +874,27 @@ impl BrowserVersionManager { .fetch_camoufox_releases_with_caching(no_caching) .await } + + async fn fetch_wayfern_versions( + &self, + no_caching: bool, + ) -> Result, Box> { + let version_info = self + .api_client + .fetch_wayfern_version_with_caching(no_caching) + .await?; + + // Check if current platform has a download available + if self + .api_client + .has_wayfern_compatible_download(&version_info) + { + Ok(vec![version_info.version]) + } else { + // No compatible download for current platform + Ok(vec![]) + } + } } #[tauri::command] diff --git a/src-tauri/src/downloader.rs b/src-tauri/src/downloader.rs index 1ccce3e..227371e 100644 --- a/src-tauri/src/downloader.rs +++ b/src-tauri/src/downloader.rs @@ -169,6 +169,43 @@ impl Downloader { Ok(asset_url) } + BrowserType::Wayfern => { + // For Wayfern, get the download URL from version.json + let version_info = self + .api_client + .fetch_wayfern_version_with_caching(true) + .await?; + + // Verify requested version matches available version + if version_info.version != version { + return Err( + format!( + "Wayfern version {version} not found. Available version: {}", + version_info.version + ) + .into(), + ); + } + + // Get the download URL for current platform + let download_url = self + .api_client + .get_wayfern_download_url(&version_info) + .ok_or_else(|| { + let (os, arch) = Self::get_platform_info(); + format!( + "No compatible download found for Wayfern on {os}/{arch}. Available platforms: {}", + version_info + .downloads + .iter() + .filter_map(|(k, v)| if v.is_some() { Some(k.as_str()) } else { None }) + .collect::>() + .join(", ") + ) + })?; + + Ok(download_url) + } _ => { // For other browsers, use the provided URL Ok(download_info.url.clone()) diff --git a/src-tauri/src/extraction.rs b/src-tauri/src/extraction.rs index 3a6c61c..cbee56f 100644 --- a/src-tauri/src/extraction.rs +++ b/src-tauri/src/extraction.rs @@ -36,6 +36,8 @@ impl Extractor { // Determine browser type from the destination directory path let browser_type = if dest_dir.to_string_lossy().contains("camoufox") { "camoufox" + } else if dest_dir.to_string_lossy().contains("wayfern") { + "wayfern" } else if dest_dir.to_string_lossy().contains("firefox") { "firefox" } else if dest_dir.to_string_lossy().contains("zen") { @@ -45,9 +47,9 @@ impl Extractor { return Ok(()); }; - // For Camoufox on Linux, we expect the executable directly under version directory - // e.g., binaries/camoufox//camoufox, without an extra camoufox/ subdirectory - if browser_type == "camoufox" { + // For Camoufox and Wayfern on Linux, we expect the executable directly under version directory + // e.g., binaries/camoufox//camoufox, without an extra subdirectory + if browser_type == "camoufox" || browser_type == "wayfern" { return Ok(()); } @@ -1011,6 +1013,10 @@ impl Extractor { "camoufox", "camoufox-bin", "camoufox-browser", + // Wayfern variants + "wayfern", + "wayfern-bin", + "wayfern-browser", ]; // First, try direct lookup in the main directory @@ -1036,6 +1042,7 @@ impl Extractor { "brave", "zen", "camoufox", + "wayfern", ".", "./", "firefox", @@ -1141,6 +1148,7 @@ impl Extractor { || name_lower.contains("brave") || name_lower.contains("zen") || name_lower.contains("camoufox") + || name_lower.contains("wayfern") || name_lower.ends_with(".appimage") || !name_lower.contains('.') { @@ -1196,6 +1204,7 @@ impl Extractor { || name_lower.contains("brave") || name_lower.contains("zen") || name_lower.contains("camoufox") + || name_lower.contains("wayfern") || file_name.ends_with(".AppImage") { log::info!( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f75f19e..0f36eb6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -33,6 +33,7 @@ pub mod proxy_storage; mod settings_manager; pub mod sync; pub mod traffic_stats; +mod wayfern_manager; // mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme mod tag_manager; mod version_updater; @@ -44,7 +45,7 @@ use browser_runner::{ use profile::manager::{ check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles, rename_profile, update_camoufox_config, update_profile_note, update_profile_proxy, - update_profile_tags, + update_profile_tags, update_wayfern_config, }; use browser_version_manager::{ @@ -801,6 +802,7 @@ pub fn run() { check_proxy_validity, get_cached_proxy_check, update_camoufox_config, + update_wayfern_config, get_profile_groups, get_groups_with_profile_counts, create_profile_group, diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 623c916..1cd794a 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -4,6 +4,7 @@ use crate::camoufox_manager::CamoufoxConfig; use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; use crate::profile::types::BrowserProfile; use crate::proxy_manager::PROXY_MANAGER; +use crate::wayfern_manager::WayfernConfig; use directories::BaseDirs; use std::fs::{self, create_dir_all}; use std::path::{Path, PathBuf}; @@ -13,6 +14,7 @@ use tauri::Emitter; pub struct ProfileManager { base_dirs: BaseDirs, camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager, + wayfern_manager: &'static crate::wayfern_manager::WayfernManager, } impl ProfileManager { @@ -20,6 +22,7 @@ impl ProfileManager { Self { base_dirs: BaseDirs::new().expect("Failed to get base directories"), camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(), + wayfern_manager: crate::wayfern_manager::WayfernManager::instance(), } } @@ -59,6 +62,7 @@ impl ProfileManager { release_type: &str, proxy_id: Option, camoufox_config: Option, + wayfern_config: Option, group_id: Option, ) -> Result> { log::info!("Attempting to create profile: {name}"); @@ -163,6 +167,7 @@ impl ProfileManager { last_launch: None, release_type: release_type.to_string(), camoufox_config: None, + wayfern_config: None, group_id: group_id.clone(), tags: Vec::new(), note: None, @@ -198,6 +203,118 @@ impl ProfileManager { camoufox_config.clone() }; + // For Wayfern profiles, generate fingerprint during creation + let final_wayfern_config = if browser == "wayfern" { + let mut config = wayfern_config.unwrap_or_else(|| { + log::info!("Creating default Wayfern config for profile: {name}"); + crate::wayfern_manager::WayfernConfig::default() + }); + + // Always ensure executable_path is set to the user's binary location + if config.executable_path.is_none() { + let mut browser_dir = self.get_binaries_dir(); + browser_dir.push(browser); + browser_dir.push(version); + + #[cfg(target_os = "macos")] + let binary_path = browser_dir + .join("Chromium.app") + .join("Contents") + .join("MacOS") + .join("Chromium"); + + #[cfg(target_os = "windows")] + let binary_path = browser_dir.join("chrome.exe"); + + #[cfg(target_os = "linux")] + let binary_path = browser_dir.join("chrome"); + + config.executable_path = Some(binary_path.to_string_lossy().to_string()); + log::info!("Set Wayfern executable path: {:?}", config.executable_path); + } + + // Pass upstream proxy information to config for fingerprint generation + if let Some(proxy_id_ref) = &proxy_id { + if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { + let proxy_url = if let (Some(username), Some(password)) = + (&proxy_settings.username, &proxy_settings.password) + { + format!( + "{}://{}:{}@{}:{}", + proxy_settings.proxy_type.to_lowercase(), + username, + password, + proxy_settings.host, + proxy_settings.port + ) + } else { + format!( + "{}://{}:{}", + proxy_settings.proxy_type.to_lowercase(), + proxy_settings.host, + proxy_settings.port + ) + }; + config.proxy = Some(proxy_url); + log::info!( + "Using upstream proxy for Wayfern fingerprint generation: {}://{}:{}", + proxy_settings.proxy_type.to_lowercase(), + proxy_settings.host, + proxy_settings.port + ); + } + } + + // Generate fingerprint if not already provided + if config.fingerprint.is_none() { + log::info!("Generating fingerprint for Wayfern profile: {name}"); + + // Create a temporary profile for fingerprint generation + let temp_profile = BrowserProfile { + id: uuid::Uuid::new_v4(), + name: name.to_string(), + browser: browser.to_string(), + version: version.to_string(), + proxy_id: proxy_id.clone(), + process_id: None, + last_launch: None, + release_type: release_type.to_string(), + camoufox_config: None, + wayfern_config: None, + group_id: group_id.clone(), + tags: Vec::new(), + note: None, + sync_enabled: false, + last_sync: None, + }; + + match self + .wayfern_manager + .generate_fingerprint_config(app_handle, &temp_profile, &config) + .await + { + Ok(generated_fingerprint) => { + config.fingerprint = Some(generated_fingerprint); + log::info!("Successfully generated fingerprint for Wayfern profile: {name}"); + } + Err(e) => { + return Err( + format!("Failed to generate fingerprint for Wayfern profile '{name}': {e}").into(), + ); + } + } + } else { + log::info!("Using provided fingerprint for Wayfern profile: {name}"); + } + + // Clear the proxy from config after fingerprint generation + config.proxy = None; + + Some(config) + } else { + wayfern_config.clone() + }; + let profile = BrowserProfile { id: profile_id, name: name.to_string(), @@ -208,6 +325,7 @@ impl ProfileManager { last_launch: None, release_type: release_type.to_string(), camoufox_config: final_camoufox_config, + wayfern_config: final_wayfern_config, group_id: group_id.clone(), tags: Vec::new(), note: None, @@ -722,6 +840,66 @@ impl ProfileManager { Ok(()) } + pub async fn update_wayfern_config( + &self, + app_handle: tauri::AppHandle, + profile_id: &str, + config: WayfernConfig, + ) -> Result<(), Box> { + // Find the profile by ID + let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err( + |_| -> Box { + format!("Invalid profile ID: {profile_id}").into() + }, + )?; + let profiles = + self + .list_profiles() + .map_err(|e| -> Box { + format!("Failed to list profiles: {e}").into() + })?; + let mut profile = profiles + .into_iter() + .find(|p| p.id == profile_uuid) + .ok_or_else(|| -> Box { + format!("Profile with ID '{profile_id}' not found").into() + })?; + + // Check if the browser is currently running using the comprehensive status check + let is_running = self + .check_browser_status(app_handle.clone(), &profile) + .await?; + + if is_running { + return Err( + "Cannot update Wayfern configuration while browser is running. Please stop the browser first.".into(), + ); + } + + // Update the Wayfern configuration + profile.wayfern_config = Some(config); + + // Save the updated profile + self + .save_profile(&profile) + .map_err(|e| -> Box { + format!("Failed to save profile: {e}").into() + })?; + + log::info!( + "Wayfern configuration updated for profile '{}' (ID: {}).", + profile.name, + profile_id + ); + + // Emit profile config update event + if let Err(e) = app_handle.emit("profiles-changed", ()) { + log::warn!("Warning: Failed to emit profiles-changed event: {e}"); + } + + Ok(()) + } + pub async fn update_profile_proxy( &self, app_handle: tauri::AppHandle, @@ -825,6 +1003,11 @@ impl ProfileManager { return self.check_camoufox_status(&app_handle, profile).await; } + // Handle Wayfern profiles using WayfernManager-based status checking + if profile.browser == "wayfern" { + return self.check_wayfern_status(&app_handle, profile).await; + } + // For non-camoufox browsers, use the existing PID-based logic let inner_profile = profile.clone(); let mut system = System::new(); @@ -1094,6 +1277,88 @@ impl ProfileManager { } } + // Check Wayfern status using WayfernManager + async fn check_wayfern_status( + &self, + app_handle: &tauri::AppHandle, + profile: &BrowserProfile, + ) -> Result> { + let manager = self.wayfern_manager; + let profiles_dir = self.get_profiles_dir(); + let profile_data_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_data_path.to_string_lossy(); + + // Check if there's a running Wayfern instance for this profile + match manager.find_wayfern_by_profile(&profile_path_str).await { + Some(wayfern_process) => { + // Found a running instance, update profile with process info if changed + let profiles_dir = self.get_profiles_dir(); + let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); + let metadata_file = profile_uuid_dir.join("metadata.json"); + let metadata_exists = metadata_file.exists(); + + if metadata_exists { + // Load latest to avoid overwriting other fields + let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + { + Some(p) => p, + None => profile.clone(), + }; + + if latest.process_id != wayfern_process.processId { + latest.process_id = wayfern_process.processId; + if let Err(e) = self.save_profile(&latest) { + log::warn!("Warning: Failed to update Wayfern profile with process info: {e}"); + } + + // Emit profile update event to frontend + if let Err(e) = app_handle.emit("profile-updated", &latest) { + log::warn!("Warning: Failed to emit profile update event: {e}"); + } + + log::info!( + "Wayfern process has started for profile '{}' with PID: {:?}", + profile.name, + wayfern_process.processId + ); + } + } + Ok(true) + } + None => { + // No running instance found, clear process ID if set + let profiles_dir = self.get_profiles_dir(); + let profile_uuid_dir = profiles_dir.join(profile.id.to_string()); + let metadata_file = profile_uuid_dir.join("metadata.json"); + let metadata_exists = metadata_file.exists(); + + if metadata_exists { + let mut latest: BrowserProfile = match std::fs::read_to_string(&metadata_file) + .ok() + .and_then(|s| serde_json::from_str(&s).ok()) + { + Some(p) => p, + None => profile.clone(), + }; + + if latest.process_id.is_some() { + latest.process_id = None; + if let Err(e) = self.save_profile(&latest) { + log::warn!("Warning: Failed to clear Wayfern profile process info: {e}"); + } + + if let Err(e) = app_handle.emit("profile-updated", &latest) { + log::warn!("Warning: Failed to emit profile update event: {e}"); + } + } + } + Ok(false) + } + } + } + fn get_common_firefox_preferences(&self) -> Vec { vec![ // Disable default browser check @@ -1453,6 +1718,7 @@ pub async fn create_browser_profile_with_group( release_type: String, proxy_id: Option, camoufox_config: Option, + wayfern_config: Option, group_id: Option, ) -> Result { let profile_manager = ProfileManager::instance(); @@ -1465,6 +1731,7 @@ pub async fn create_browser_profile_with_group( &release_type, proxy_id, camoufox_config, + wayfern_config, group_id, ) .await @@ -1550,6 +1817,7 @@ pub async fn create_browser_profile_new( release_type: String, proxy_id: Option, camoufox_config: Option, + wayfern_config: Option, group_id: Option, ) -> Result { let browser_type = @@ -1562,6 +1830,7 @@ pub async fn create_browser_profile_new( release_type, proxy_id, camoufox_config, + wayfern_config, group_id, ) .await @@ -1580,6 +1849,19 @@ pub async fn update_camoufox_config( .map_err(|e| format!("Failed to update Camoufox config: {e}")) } +#[tauri::command] +pub async fn update_wayfern_config( + app_handle: tauri::AppHandle, + profile_id: String, + config: WayfernConfig, +) -> Result<(), String> { + let profile_manager = ProfileManager::instance(); + profile_manager + .update_wayfern_config(app_handle, &profile_id, config) + .await + .map_err(|e| format!("Failed to update Wayfern config: {e}")) +} + // Global singleton instance #[tauri::command] pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> { diff --git a/src-tauri/src/profile/types.rs b/src-tauri/src/profile/types.rs index 806d3b0..0bde2b4 100644 --- a/src-tauri/src/profile/types.rs +++ b/src-tauri/src/profile/types.rs @@ -1,4 +1,5 @@ use crate::camoufox_manager::CamoufoxConfig; +use crate::wayfern_manager::WayfernConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -29,6 +30,8 @@ pub struct BrowserProfile { #[serde(default)] pub camoufox_config: Option, // Camoufox configuration #[serde(default)] + pub wayfern_config: Option, // Wayfern configuration + #[serde(default)] pub group_id: Option, // Reference to profile group #[serde(default)] pub tags: Vec, // Free-form tags diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 17ce416..11b71ac 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -549,6 +549,7 @@ impl ProfileImporter { last_launch: None, release_type: "stable".to_string(), camoufox_config: None, + wayfern_config: None, group_id: None, tags: Vec::new(), note: None, diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs new file mode 100644 index 0000000..5073a8d --- /dev/null +++ b/src-tauri/src/wayfern_manager.rs @@ -0,0 +1,615 @@ +use crate::browser_runner::BrowserRunner; +use crate::profile::BrowserProfile; +use directories::BaseDirs; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Stdio; +use std::sync::Arc; +use std::time::Duration; +use tauri::AppHandle; +use tokio::process::Command as TokioCommand; +use tokio::sync::Mutex as AsyncMutex; +use tokio_tungstenite::{connect_async, tungstenite::Message}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct WayfernConfig { + #[serde(default)] + pub fingerprint: Option, + #[serde(default)] + pub randomize_fingerprint_on_launch: Option, + #[serde(default)] + pub os: Option, + #[serde(default)] + pub screen_max_width: Option, + #[serde(default)] + pub screen_max_height: Option, + #[serde(default)] + pub screen_min_width: Option, + #[serde(default)] + pub screen_min_height: Option, + #[serde(default)] + pub geoip: Option, // For compatibility with shared config form + #[serde(default)] + pub block_images: Option, // For compatibility with shared config form + #[serde(default)] + pub block_webrtc: Option, + #[serde(default)] + pub block_webgl: Option, + #[serde(default)] + pub executable_path: Option, + #[serde(default, skip_serializing)] + pub proxy: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct WayfernLaunchResult { + pub id: String, + #[serde(alias = "process_id")] + pub processId: Option, + #[serde(alias = "profile_path")] + pub profilePath: Option, + pub url: Option, + pub cdp_port: Option, +} + +#[derive(Debug)] +struct WayfernInstance { + #[allow(dead_code)] + id: String, + process_id: Option, + profile_path: Option, + url: Option, + cdp_port: Option, +} + +struct WayfernManagerInner { + instances: HashMap, +} + +pub struct WayfernManager { + inner: Arc>, + #[allow(dead_code)] + base_dirs: BaseDirs, + http_client: Client, +} + +#[derive(Debug, Deserialize)] +struct CdpTarget { + #[serde(rename = "type")] + target_type: String, + #[serde(rename = "webSocketDebuggerUrl")] + websocket_debugger_url: Option, +} + +impl WayfernManager { + fn new() -> Self { + Self { + inner: Arc::new(AsyncMutex::new(WayfernManagerInner { + instances: HashMap::new(), + })), + base_dirs: BaseDirs::new().expect("Failed to get base directories"), + http_client: Client::new(), + } + } + + pub fn instance() -> &'static WayfernManager { + &WAYFERN_MANAGER + } + + #[allow(dead_code)] + pub fn get_profiles_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("profiles"); + path + } + + #[allow(dead_code)] + fn get_binaries_dir(&self) -> PathBuf { + let mut path = self.base_dirs.data_local_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("binaries"); + path + } + + async fn find_free_port() -> Result> { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + drop(listener); + Ok(port) + } + + async fn wait_for_cdp_ready( + &self, + port: u16, + ) -> Result<(), Box> { + let url = format!("http://127.0.0.1:{port}/json/version"); + let max_attempts = 50; + let delay = Duration::from_millis(100); + + for attempt in 0..max_attempts { + match self.http_client.get(&url).send().await { + Ok(resp) if resp.status().is_success() => { + log::info!("CDP ready on port {port} after {attempt} attempts"); + return Ok(()); + } + _ => { + tokio::time::sleep(delay).await; + } + } + } + + Err(format!("CDP not ready after {max_attempts} attempts on port {port}").into()) + } + + async fn get_cdp_targets( + &self, + port: u16, + ) -> Result, Box> { + let url = format!("http://127.0.0.1:{port}/json"); + let resp = self.http_client.get(&url).send().await?; + let targets: Vec = resp.json().await?; + Ok(targets) + } + + async fn send_cdp_command( + &self, + ws_url: &str, + method: &str, + params: serde_json::Value, + ) -> Result> { + let (mut ws_stream, _) = connect_async(ws_url).await?; + + let command = json!({ + "id": 1, + "method": method, + "params": params + }); + + use futures_util::sink::SinkExt; + use futures_util::stream::StreamExt; + + ws_stream + .send(Message::Text(command.to_string().into())) + .await?; + + while let Some(msg) = ws_stream.next().await { + match msg? { + Message::Text(text) => { + let response: serde_json::Value = serde_json::from_str(text.as_str())?; + if response.get("id") == Some(&json!(1)) { + if let Some(error) = response.get("error") { + return Err(format!("CDP error: {}", error).into()); + } + return Ok(response.get("result").cloned().unwrap_or(json!({}))); + } + } + Message::Close(_) => break, + _ => {} + } + } + + Err("No response received from CDP".into()) + } + + pub async fn generate_fingerprint_config( + &self, + _app_handle: &AppHandle, + profile: &BrowserProfile, + config: &WayfernConfig, + ) -> Result> { + let executable_path = if let Some(path) = &config.executable_path { + PathBuf::from(path) + } else { + BrowserRunner::instance() + .get_browser_executable_path(profile) + .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? + }; + + let port = Self::find_free_port().await?; + log::info!("Launching headless Wayfern on port {port} for fingerprint generation"); + + let temp_profile_dir = + std::env::temp_dir().join(format!("wayfern_fingerprint_{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&temp_profile_dir)?; + + let mut cmd = TokioCommand::new(&executable_path); + cmd + .arg("--headless=new") + .arg(format!("--remote-debugging-port={port}")) + .arg("--remote-debugging-address=127.0.0.1") + .arg(format!("--user-data-dir={}", temp_profile_dir.display())) + .arg("--disable-gpu") + .arg("--no-first-run") + .arg("--no-default-browser-check") + .arg("--disable-background-mode") + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + let child = cmd.spawn()?; + let child_id = child.id(); + + let cleanup = || async { + if let Some(id) = child_id { + #[cfg(unix)] + { + use nix::sys::signal::{kill, Signal}; + use nix::unistd::Pid; + let _ = kill(Pid::from_raw(id as i32), Signal::SIGTERM); + } + #[cfg(windows)] + { + let _ = std::process::Command::new("taskkill") + .args(["/PID", &id.to_string(), "/F"]) + .output(); + } + } + let _ = std::fs::remove_dir_all(&temp_profile_dir); + }; + + if let Err(e) = self.wait_for_cdp_ready(port).await { + cleanup().await; + return Err(e); + } + + let targets = match self.get_cdp_targets(port).await { + Ok(t) => t, + Err(e) => { + cleanup().await; + return Err(e); + } + }; + + let page_target = targets + .iter() + .find(|t| t.target_type == "page" && t.websocket_debugger_url.is_some()); + + let ws_url = match page_target { + Some(target) => target.websocket_debugger_url.as_ref().unwrap().clone(), + None => { + cleanup().await; + return Err("No page target found for CDP".into()); + } + }; + + let os = config + .os + .as_deref() + .unwrap_or(if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else { + "windows" + }); + + let refresh_result = self + .send_cdp_command( + &ws_url, + "Wayfern.refreshFingerprint", + json!({ "operatingSystem": os }), + ) + .await; + + if let Err(e) = refresh_result { + cleanup().await; + return Err(format!("Failed to refresh fingerprint: {e}").into()); + } + + let get_result = self + .send_cdp_command(&ws_url, "Wayfern.getFingerprint", json!({})) + .await; + + let fingerprint = match get_result { + Ok(result) => { + // Wayfern.getFingerprint returns { fingerprint: {...} } + // We need to extract just the fingerprint object + result.get("fingerprint").cloned().unwrap_or(result) + } + Err(e) => { + cleanup().await; + return Err(format!("Failed to get fingerprint: {e}").into()); + } + }; + + cleanup().await; + + let fingerprint_json = serde_json::to_string(&fingerprint) + .map_err(|e| format!("Failed to serialize fingerprint: {e}"))?; + + log::info!( + "Generated Wayfern fingerprint for OS: {}, fields: {:?}", + os, + fingerprint + .as_object() + .map(|o| o.keys().collect::>()) + ); + Ok(fingerprint_json) + } + + pub async fn launch_wayfern( + &self, + _app_handle: &AppHandle, + profile: &BrowserProfile, + profile_path: &str, + config: &WayfernConfig, + url: Option<&str>, + proxy_url: Option<&str>, + ) -> Result> { + let executable_path = if let Some(path) = &config.executable_path { + PathBuf::from(path) + } else { + BrowserRunner::instance() + .get_browser_executable_path(profile) + .map_err(|e| format!("Failed to get Wayfern executable path: {e}"))? + }; + + let port = Self::find_free_port().await?; + log::info!("Launching Wayfern on CDP port {port}"); + + let mut args = vec![ + format!("--remote-debugging-port={port}"), + "--remote-debugging-address=127.0.0.1".to_string(), + format!("--user-data-dir={}", profile_path), + "--no-first-run".to_string(), + "--no-default-browser-check".to_string(), + "--disable-background-mode".to_string(), + "--disable-component-update".to_string(), + "--disable-background-timer-throttling".to_string(), + "--crash-server-url=".to_string(), + "--disable-updater".to_string(), + "--disable-session-crashed-bubble".to_string(), + "--hide-crash-restore-bubble".to_string(), + "--disable-infobars".to_string(), + "--disable-quic".to_string(), + "--disable-features=DialMediaRouteProvider".to_string(), + "--use-mock-keychain".to_string(), + "--password-store=basic".to_string(), + ]; + + if let Some(proxy) = proxy_url { + args.push(format!("--proxy-server={proxy}")); + } + + if let Some(url) = url { + args.push(url.to_string()); + } + + let mut cmd = TokioCommand::new(&executable_path); + cmd.args(&args); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + + let child = cmd.spawn()?; + let process_id = child.id(); + + self.wait_for_cdp_ready(port).await?; + + if let Some(fingerprint_json) = &config.fingerprint { + log::info!( + "Applying fingerprint to Wayfern browser, fingerprint length: {} chars", + fingerprint_json.len() + ); + + let stored_value: serde_json::Value = serde_json::from_str(fingerprint_json) + .map_err(|e| format!("Failed to parse stored fingerprint JSON: {e}"))?; + + // The stored fingerprint should be the fingerprint object directly (after our fix in generate_fingerprint_config) + // But for backwards compatibility, also handle the wrapped format + let fingerprint = if stored_value.get("fingerprint").is_some() { + // Old format: {"fingerprint": {...}} - extract the inner fingerprint + stored_value.get("fingerprint").cloned().unwrap() + } else { + // New format: fingerprint object directly {...} + stored_value.clone() + }; + + log::info!( + "Fingerprint prepared for CDP command, fields: {:?}", + fingerprint + .as_object() + .map(|o| o.keys().take(5).collect::>()) + ); + + let targets = self.get_cdp_targets(port).await?; + log::info!("Found {} CDP targets", targets.len()); + + let page_targets: Vec<_> = targets.iter().filter(|t| t.target_type == "page").collect(); + log::info!( + "Found {} page targets for fingerprint application", + page_targets.len() + ); + + for target in page_targets { + if let Some(ws_url) = &target.websocket_debugger_url { + log::info!("Applying fingerprint to target via WebSocket: {}", ws_url); + // Wayfern.setFingerprint expects the fingerprint object directly, NOT wrapped + match self + .send_cdp_command(ws_url, "Wayfern.setFingerprint", fingerprint.clone()) + .await + { + Ok(result) => log::info!( + "Successfully applied fingerprint to page target: {:?}", + result + ), + Err(e) => log::error!("Failed to apply fingerprint to target: {e}"), + } + } + } + } else { + log::warn!("No fingerprint found in config, browser will use default fingerprint"); + } + + let id = uuid::Uuid::new_v4().to_string(); + let instance = WayfernInstance { + id: id.clone(), + process_id, + profile_path: Some(profile_path.to_string()), + url: url.map(|s| s.to_string()), + cdp_port: Some(port), + }; + + let mut inner = self.inner.lock().await; + inner.instances.insert(id.clone(), instance); + + Ok(WayfernLaunchResult { + id, + processId: process_id, + profilePath: Some(profile_path.to_string()), + url: url.map(|s| s.to_string()), + cdp_port: Some(port), + }) + } + + pub async fn stop_wayfern( + &self, + id: &str, + ) -> Result<(), Box> { + let mut inner = self.inner.lock().await; + + if let Some(instance) = inner.instances.remove(id) { + if let Some(pid) = instance.process_id { + #[cfg(unix)] + { + use nix::sys::signal::{kill, Signal}; + use nix::unistd::Pid; + let _ = kill(Pid::from_raw(pid as i32), Signal::SIGTERM); + } + #[cfg(windows)] + { + let _ = std::process::Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .output(); + } + log::info!("Stopped Wayfern instance {id} (PID: {pid})"); + } + } + + Ok(()) + } + + pub async fn find_wayfern_by_profile(&self, profile_path: &str) -> Option { + use sysinfo::{ProcessRefreshKind, RefreshKind, System}; + + let mut inner = self.inner.lock().await; + + // Find the instance with the matching profile path + let mut found_id: Option = None; + for (id, instance) in &inner.instances { + if let Some(path) = &instance.profile_path { + if path == profile_path { + found_id = Some(id.clone()); + break; + } + } + } + + // If we found an instance, verify the process is still running + if let Some(id) = found_id { + if let Some(instance) = inner.instances.get(&id) { + if let Some(pid) = instance.process_id { + let system = System::new_with_specifics( + RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), + ); + let sysinfo_pid = sysinfo::Pid::from_u32(pid); + + if system.process(sysinfo_pid).is_some() { + // Process is still running + return Some(WayfernLaunchResult { + id: id.clone(), + processId: instance.process_id, + profilePath: instance.profile_path.clone(), + url: instance.url.clone(), + cdp_port: instance.cdp_port, + }); + } else { + // Process has died (e.g., Cmd+Q), remove from instances + log::info!( + "Wayfern process {} for profile {} is no longer running, cleaning up", + pid, + profile_path + ); + inner.instances.remove(&id); + return None; + } + } + } + } + + None + } + + #[allow(dead_code)] + pub async fn launch_wayfern_profile( + &self, + app_handle: &AppHandle, + profile: &BrowserProfile, + config: &WayfernConfig, + url: Option<&str>, + proxy_url: Option<&str>, + ) -> Result> { + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir.join(profile.id.to_string()).join("profile"); + let profile_path_str = profile_path.to_string_lossy().to_string(); + + std::fs::create_dir_all(&profile_path)?; + + if let Some(existing) = self.find_wayfern_by_profile(&profile_path_str).await { + log::info!("Stopping existing Wayfern instance for profile"); + self.stop_wayfern(&existing.id).await?; + } + + self + .launch_wayfern( + app_handle, + profile, + &profile_path_str, + config, + url, + proxy_url, + ) + .await + } + + #[allow(dead_code)] + pub async fn cleanup_dead_instances(&self) { + use sysinfo::{ProcessRefreshKind, RefreshKind, System}; + + let mut inner = self.inner.lock().await; + let mut dead_ids = Vec::new(); + + let system = System::new_with_specifics( + RefreshKind::nothing().with_processes(ProcessRefreshKind::everything()), + ); + + for (id, instance) in &inner.instances { + if let Some(pid) = instance.process_id { + let pid = sysinfo::Pid::from_u32(pid); + if !system.processes().contains_key(&pid) { + dead_ids.push(id.clone()); + } + } + } + + for id in dead_ids { + log::info!("Cleaning up dead Wayfern instance: {id}"); + inner.instances.remove(&id); + } + } +} + +lazy_static::lazy_static! { + static ref WAYFERN_MANAGER: WayfernManager = WayfernManager::new(); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2ab034d..e173a8a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -29,7 +29,7 @@ import { useProxyEvents } from "@/hooks/use-proxy-events"; import { useUpdateNotifications } from "@/hooks/use-update-notifications"; import { useVersionUpdater } from "@/hooks/use-version-updater"; import { showErrorToast, showSuccessToast, showToast } from "@/lib/toast-utils"; -import type { BrowserProfile, CamoufoxConfig } from "@/types"; +import type { BrowserProfile, CamoufoxConfig, WayfernConfig } from "@/types"; type BrowserTypeString = | "firefox" @@ -37,7 +37,8 @@ type BrowserTypeString = | "chromium" | "brave" | "zen" - | "camoufox"; + | "camoufox" + | "wayfern"; interface PendingUrl { id: string; @@ -387,6 +388,26 @@ export default function Home() { [], ); + const handleSaveWayfernConfig = useCallback( + async (profile: BrowserProfile, config: WayfernConfig) => { + try { + await invoke("update_wayfern_config", { + profileId: profile.id, + config, + }); + // No need to manually reload - useProfileEvents will handle the update + setCamoufoxConfigDialogOpen(false); + } catch (err: unknown) { + console.error("Failed to update wayfern config:", err); + showErrorToast( + `Failed to update wayfern config: ${JSON.stringify(err)}`, + ); + throw err; + } + }, + [], + ); + const handleCreateProfile = useCallback( async (profileData: { name: string; @@ -395,6 +416,7 @@ export default function Home() { releaseType: string; proxyId?: string; camoufoxConfig?: CamoufoxConfig; + wayfernConfig?: WayfernConfig; groupId?: string; }) => { try { @@ -405,6 +427,7 @@ export default function Home() { releaseType: profileData.releaseType, proxyId: profileData.proxyId, camoufoxConfig: profileData.camoufoxConfig, + wayfernConfig: profileData.wayfernConfig, groupId: profileData.groupId || (selectedGroupId !== "default" ? selectedGroupId : undefined), @@ -834,6 +857,7 @@ export default function Home() { }} profile={currentProfileForCamoufoxConfig} onSave={handleSaveCamoufoxConfig} + onSaveWayfern={handleSaveWayfernConfig} isRunning={ currentProfileForCamoufoxConfig ? runningProfiles.has(currentProfileForCamoufoxConfig.id) diff --git a/src/components/camoufox-config-dialog.tsx b/src/components/camoufox-config-dialog.tsx index cda7d12..899722e 100644 --- a/src/components/camoufox-config-dialog.tsx +++ b/src/components/camoufox-config-dialog.tsx @@ -28,6 +28,10 @@ interface CamoufoxConfigDialogProps { onClose: () => void; profile: BrowserProfile | null; onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise; + onSaveWayfern?: ( + profile: BrowserProfile, + config: CamoufoxConfig, + ) => Promise; isRunning?: boolean; } @@ -36,6 +40,7 @@ export function CamoufoxConfigDialog({ onClose, profile, onSave, + onSaveWayfern, isRunning = false, }: CamoufoxConfigDialogProps) { const [config, setConfig] = useState(() => ({ @@ -44,17 +49,24 @@ export function CamoufoxConfigDialog({ })); const [isSaving, setIsSaving] = useState(false); + const isAntiDetectBrowser = + profile?.browser === "camoufox" || profile?.browser === "wayfern"; + // Initialize config when profile changes useEffect(() => { - if (profile && profile.browser === "camoufox") { + if (profile && isAntiDetectBrowser) { + const profileConfig = + profile.browser === "wayfern" + ? profile.wayfern_config + : profile.camoufox_config; setConfig( - profile.camoufox_config || { + profileConfig || { geoip: true, os: getCurrentOS(), }, ); } - }, [profile]); + }, [profile, isAntiDetectBrowser]); const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => { setConfig((prev) => ({ ...prev, [key]: value })); @@ -79,10 +91,14 @@ export function CamoufoxConfigDialog({ setIsSaving(true); try { - await onSave(profile, config); + if (profile.browser === "wayfern" && onSaveWayfern) { + await onSaveWayfern(profile, config); + } else { + await onSave(profile, config); + } onClose(); } catch (error) { - console.error("Failed to save camoufox config:", error); + console.error("Failed to save config:", error); const { toast } = await import("sonner"); toast.error("Failed to save configuration", { description: @@ -95,9 +111,13 @@ export function CamoufoxConfigDialog({ const handleClose = () => { // Reset config to original when closing without saving - if (profile && profile.browser === "camoufox") { + if (profile && isAntiDetectBrowser) { + const profileConfig = + profile.browser === "wayfern" + ? profile.wayfern_config + : profile.camoufox_config; setConfig( - profile.camoufox_config || { + profileConfig || { geoip: true, os: getCurrentOS(), }, @@ -106,11 +126,11 @@ export function CamoufoxConfigDialog({ onClose(); }; - if (!profile || profile.browser !== "camoufox") { + if (!profile || !isAntiDetectBrowser) { return null; } - // No OS warning needed anymore since we removed OS selection + const browserName = profile.browser === "wayfern" ? "Wayfern" : "Camoufox"; return ( @@ -118,7 +138,7 @@ export function CamoufoxConfigDialog({ {isRunning ? "View" : "Configure"} Fingerprint Settings -{" "} - {profile.name} + {profile.name} ({browserName}) @@ -129,6 +149,9 @@ export function CamoufoxConfigDialog({ onConfigChange={updateConfig} forceAdvanced={true} readOnly={isRunning} + browserType={ + profile.browser === "wayfern" ? "wayfern" : "camoufox" + } /> diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 0b6e573..51ef832 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -24,12 +24,18 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Tabs, TabsContent } from "@/components/ui/tabs"; import { useBrowserDownload } from "@/hooks/use-browser-download"; import { useProxyEvents } from "@/hooks/use-proxy-events"; import { getBrowserIcon } from "@/lib/browser-utils"; -import type { BrowserReleaseTypes, CamoufoxConfig, CamoufoxOS } from "@/types"; +import type { + BrowserReleaseTypes, + CamoufoxConfig, + CamoufoxOS, + WayfernConfig, + WayfernOS, +} from "@/types"; const getCurrentOS = (): CamoufoxOS => { if (typeof navigator === "undefined") return "linux"; @@ -47,7 +53,8 @@ type BrowserTypeString = | "chromium" | "brave" | "zen" - | "camoufox"; + | "camoufox" + | "wayfern"; interface CreateProfileDialogProps { isOpen: boolean; @@ -59,6 +66,7 @@ interface CreateProfileDialogProps { releaseType: string; proxyId?: string; camoufoxConfig?: CamoufoxConfig; + wayfernConfig?: WayfernConfig; groupId?: string; }) => Promise; selectedGroupId?: string; @@ -115,6 +123,11 @@ export function CreateProfileDialog({ os: getCurrentOS(), // Default to current OS })); + // Wayfern anti-detect states + const [wayfernConfig, setWayfernConfig] = useState(() => ({ + os: getCurrentOS() as WayfernOS, // Default to current OS + })); + // Handle browser selection from the initial screen const handleBrowserSelect = (browser: BrowserTypeString) => { setSelectedBrowser(browser); @@ -197,7 +210,7 @@ export function CreateProfileDialog({ // Only update state if this browser is still the one we're loading if (loadingBrowserRef.current === browser) { // Filter to enforce stable-only creation, except Firefox Developer (nightly-only) - if (browser === "camoufox") { + if (browser === "camoufox" || browser === "wayfern") { const filtered: BrowserReleaseTypes = {}; if (rawReleaseTypes.stable) filtered.stable = rawReleaseTypes.stable; @@ -267,8 +280,8 @@ export function CreateProfileDialog({ if (selectedBrowser) { void loadReleaseTypes(selectedBrowser); } - // Check and download GeoIP database if needed for Camoufox - if (selectedBrowser === "camoufox") { + // Check and download GeoIP database if needed for Camoufox or Wayfern + if (selectedBrowser === "camoufox" || selectedBrowser === "wayfern") { void checkAndDownloadGeoIPDatabase(); } } @@ -333,26 +346,50 @@ export function CreateProfileDialog({ setIsCreating(true); try { if (activeTab === "anti-detect") { - // Anti-detect browser - always use Camoufox with best available version - const bestCamoufoxVersion = getBestAvailableVersion("camoufox"); - if (!bestCamoufoxVersion) { - console.error("No Camoufox version available"); - return; + // Anti-detect browser - check if Wayfern or Camoufox is selected + if (selectedBrowser === "wayfern") { + const bestWayfernVersion = getBestAvailableVersion("wayfern"); + if (!bestWayfernVersion) { + console.error("No Wayfern version available"); + return; + } + + // The fingerprint will be generated at launch time by the Rust backend + const finalWayfernConfig = { ...wayfernConfig }; + + await onCreateProfile({ + name: profileName.trim(), + browserStr: "wayfern" as BrowserTypeString, + version: bestWayfernVersion.version, + releaseType: bestWayfernVersion.releaseType, + proxyId: selectedProxyId, + wayfernConfig: finalWayfernConfig, + groupId: + selectedGroupId !== "default" ? selectedGroupId : undefined, + }); + } else { + // Default to Camoufox + const bestCamoufoxVersion = getBestAvailableVersion("camoufox"); + if (!bestCamoufoxVersion) { + console.error("No Camoufox version available"); + return; + } + + // The fingerprint will be generated at launch time by the Rust backend + // We don't need to generate it here during profile creation + const finalCamoufoxConfig = { ...camoufoxConfig }; + + await onCreateProfile({ + name: profileName.trim(), + browserStr: "camoufox" as BrowserTypeString, + version: bestCamoufoxVersion.version, + releaseType: bestCamoufoxVersion.releaseType, + proxyId: selectedProxyId, + camoufoxConfig: finalCamoufoxConfig, + groupId: + selectedGroupId !== "default" ? selectedGroupId : undefined, + }); } - - // The fingerprint will be generated at launch time by the Rust backend - // We don't need to generate it here during profile creation - const finalCamoufoxConfig = { ...camoufoxConfig }; - - await onCreateProfile({ - name: profileName.trim(), - browserStr: "camoufox" as BrowserTypeString, - version: bestCamoufoxVersion.version, - releaseType: bestCamoufoxVersion.releaseType, - proxyId: selectedProxyId, - camoufoxConfig: finalCamoufoxConfig, - groupId: selectedGroupId !== "default" ? selectedGroupId : undefined, - }); } else { // Regular browser if (!selectedBrowser) { @@ -402,6 +439,9 @@ export function CreateProfileDialog({ geoip: true, // Reset to automatic geoip os: getCurrentOS(), // Reset to current OS }); + setWayfernConfig({ + os: getCurrentOS() as WayfernOS, // Reset to current OS + }); onClose(); }; @@ -409,6 +449,10 @@ export function CreateProfileDialog({ setCamoufoxConfig((prev) => ({ ...prev, [key]: value })); }; + const updateWayfernConfig = (key: keyof WayfernConfig, value: unknown) => { + setWayfernConfig((prev) => ({ ...prev, [key]: value })); + }; + // Check if browser version is downloaded and available const isBrowserVersionAvailable = useCallback( (browserStr: string) => { @@ -461,13 +505,7 @@ export function CreateProfileDialog({ onValueChange={handleTabChange} className="flex flex-col flex-1 w-full min-h-0" > - - Anti-Detect - Regular - + {/* Tab list hidden - only anti-detect browsers are supported */}
@@ -482,30 +520,60 @@ export function CreateProfileDialog({ Anti-Detect Browser

- Choose Firefox for anti-detection capabilities + Choose a browser with anti-detection capabilities

- +
+
+ Chromium (Wayfern) +
+
+ Anti-Detect Browser +
+
+ + + {/* Camoufox (Firefox) - Second */} + + @@ -570,7 +638,94 @@ export function CreateProfileDialog({ /> - {selectedBrowser === "camoufox" ? ( + {selectedBrowser === "wayfern" ? ( + // Wayfern Configuration +
+ {/* Wayfern Download Status */} + {isLoadingReleaseTypes && ( +
+
+

+ Fetching available versions... +

+
+ )} + {!isLoadingReleaseTypes && releaseTypesError && ( +
+

+ {releaseTypesError} +

+ + selectedBrowser && + loadReleaseTypes(selectedBrowser) + } + size="sm" + variant="outline" + > + Retry + +
+ )} + {!isLoadingReleaseTypes && + !releaseTypesError && + !isBrowserCurrentlyDownloading("wayfern") && + !isBrowserVersionAvailable("wayfern") && + getBestAvailableVersion("wayfern") && ( +
+

+ {(() => { + const bestVersion = + getBestAvailableVersion("wayfern"); + return `Wayfern version (${bestVersion?.version}) needs to be downloaded`; + })()} +

+ handleDownload("wayfern")} + isLoading={isBrowserCurrentlyDownloading( + "wayfern", + )} + size="sm" + disabled={isBrowserCurrentlyDownloading( + "wayfern", + )} + > + {isBrowserCurrentlyDownloading("wayfern") + ? "Downloading..." + : "Download"} + +
+ )} + {!isLoadingReleaseTypes && + !releaseTypesError && + !isBrowserCurrentlyDownloading("wayfern") && + isBrowserVersionAvailable("wayfern") && ( +
+ {(() => { + const bestVersion = + getBestAvailableVersion("wayfern"); + return `✓ Wayfern version (${bestVersion?.version}) is available`; + })()} +
+ )} + {isBrowserCurrentlyDownloading("wayfern") && ( +
+ {(() => { + const bestVersion = + getBestAvailableVersion("wayfern"); + return `Downloading Wayfern version (${bestVersion?.version})...`; + })()} +
+ )} + + +
+ ) : selectedBrowser === "camoufox" ? ( // Camoufox Configuration
{/* Camoufox Download Status */} @@ -654,6 +809,7 @@ export function CreateProfileDialog({ config={camoufoxConfig} onConfigChange={updateCamoufoxConfig} isCreating + browserType="camoufox" />
) : ( diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 37b3abb..43c2482 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -1999,7 +1999,8 @@ export function ProfilesDataTable({ > Assign to Group - {profile.browser === "camoufox" && + {(profile.browser === "camoufox" || + profile.browser === "wayfern") && meta.onConfigureCamoufox && ( { diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx index 434aeeb..c4180a0 100644 --- a/src/components/shared-camoufox-config-form.tsx +++ b/src/components/shared-camoufox-config-form.tsx @@ -28,6 +28,7 @@ interface SharedCamoufoxConfigFormProps { isCreating?: boolean; // Flag to indicate if this is for creating a new profile forceAdvanced?: boolean; // Force advanced mode (for editing) readOnly?: boolean; // Flag to indicate if the form should be read-only + browserType?: "camoufox" | "wayfern"; // Browser type to customize form options } // Determine if fingerprint editing should be disabled @@ -116,6 +117,7 @@ export function SharedCamoufoxConfigForm({ isCreating = false, forceAdvanced = false, readOnly = false, + browserType = "camoufox", }: SharedCamoufoxConfigFormProps) { const [activeTab, setActiveTab] = useState( forceAdvanced ? "manual" : "automatic", @@ -282,42 +284,44 @@ export function SharedCamoufoxConfigForm({ )}
- {/* Blocking Options */} -
- -
-
- - onConfigChange("block_images", checked) - } - /> - -
-
- - onConfigChange("block_webrtc", checked) - } - /> - -
-
- - onConfigChange("block_webgl", checked) - } - /> - + {/* Blocking Options - Only available for Camoufox */} + {browserType === "camoufox" && ( +
+ +
+
+ + onConfigChange("block_images", checked) + } + /> + +
+
+ + onConfigChange("block_webrtc", checked) + } + /> + +
+
+ + onConfigChange("block_webgl", checked) + } + /> + +
-
+ )} {/* Navigator Properties */}
diff --git a/src/lib/browser-utils.ts b/src/lib/browser-utils.ts index faa21b5..9ba30fe 100644 --- a/src/lib/browser-utils.ts +++ b/src/lib/browser-utils.ts @@ -3,9 +3,7 @@ * Centralized helpers for browser name mapping, icons, etc. */ -import { FaChrome, FaFirefox, FaShieldAlt } from "react-icons/fa"; -import { SiBrave } from "react-icons/si"; -import { ZenBrowser } from "@/components/icons/zen-browser"; +import { FaChrome, FaExclamationTriangle, FaFirefox } from "react-icons/fa"; /** * Map internal browser names to display names @@ -17,7 +15,8 @@ export function getBrowserDisplayName(browserType: string): string { zen: "Zen Browser", brave: "Brave", chromium: "Chromium", - camoufox: "Anti-Detect", + camoufox: "Firefox (Camoufox)", + wayfern: "Chromium (Wayfern)", }; return browserNames[browserType] || browserType; @@ -25,22 +24,18 @@ export function getBrowserDisplayName(browserType: string): string { /** * Get the appropriate icon component for a browser type + * Anti-detect browsers get their base browser icons + * Other browsers get a warning icon to indicate they're not anti-detect */ export function getBrowserIcon(browserType: string) { switch (browserType) { - case "chromium": - return FaChrome; - case "brave": - return SiBrave; - case "firefox": - case "firefox-developer": - return FaFirefox; - case "zen": - return ZenBrowser; case "camoufox": - return FaShieldAlt; + return FaFirefox; // Firefox-based anti-detect browser + case "wayfern": + return FaChrome; // Chromium-based anti-detect browser default: - return null; + // All other browsers get a warning icon + return FaExclamationTriangle; } } diff --git a/src/types.ts b/src/types.ts index 6203e0f..202c0f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export interface BrowserProfile { last_launch?: number; release_type: string; // "stable" or "nightly" camoufox_config?: CamoufoxConfig; // Camoufox configuration + wayfern_config?: WayfernConfig; // Wayfern configuration group_id?: string; // Reference to profile group tags?: string[]; note?: string; // User note @@ -289,6 +290,32 @@ export interface CamoufoxLaunchResult { url?: string; } +export type WayfernOS = "windows" | "macos" | "linux"; + +export interface WayfernConfig { + proxy?: string; + screen_max_width?: number; + screen_max_height?: number; + screen_min_width?: number; + screen_min_height?: number; + geoip?: string | boolean; // For compatibility with shared config form + block_images?: boolean; // For compatibility with shared config form + block_webrtc?: boolean; + block_webgl?: boolean; + executable_path?: string; + fingerprint?: string; // JSON string of the complete fingerprint config + randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch + os?: WayfernOS; // Operating system for fingerprint generation +} + +export interface WayfernLaunchResult { + id: string; + processId?: number; + profilePath?: string; + url?: string; + cdp_port?: number; +} + // Traffic stats types export interface BandwidthDataPoint { timestamp: number;