feat: partially add wayfern

This commit is contained in:
zhom
2026-01-09 09:50:07 +04:00
parent fdd921c6bb
commit e9c084d6a4
22 changed files with 2313 additions and 127 deletions
+53 -1
View File
@@ -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",
+6
View File
@@ -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"
+106
View File
@@ -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<String, Option<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
struct CachedVersionData {
releases: Vec<BrowserRelease>,
@@ -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<WayfernVersionInfo> {
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<dyn std::error::Error + Send + Sync>> {
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<WayfernVersionInfo, Box<dyn std::error::Error + Send + Sync>> {
// 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<String> {
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,
+10
View File
@@ -60,6 +60,8 @@ pub struct CreateProfileRequest {
pub release_type: Option<String>,
#[schema(value_type = Object)]
pub camoufox_config: Option<serde_json::Value>,
#[schema(value_type = Object)]
pub wayfern_config: Option<serde_json::Value>,
pub group_id: Option<String>,
pub tags: Option<Vec<String>>,
}
@@ -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
+1
View File
@@ -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,
+209 -3
View File
@@ -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<PathBuf, Box<dyn std::error::Error>> {
// 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<PathBuf, Box<dyn std::error::Error>> {
#[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<String>,
remote_debugging_port: Option<u16>,
headless: bool,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
#[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");
+555 -4
View File
@@ -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<dyn std::error::Error + Send + Sync> {
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
+75
View File
@@ -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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
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]
+37
View File
@@ -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::<Vec<_>>()
.join(", ")
)
})?;
Ok(download_url)
}
_ => {
// For other browsers, use the provided URL
Ok(download_info.url.clone())
+12 -3
View File
@@ -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/<version>/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/<version>/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!(
+3 -1
View File
@@ -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,
+282
View File
@@ -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<String>,
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
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<dyn std::error::Error + Send + Sync>> {
// Find the profile by ID
let profile_uuid = uuid::Uuid::parse_str(profile_id).map_err(
|_| -> Box<dyn std::error::Error + Send + Sync> {
format!("Invalid profile ID: {profile_id}").into()
},
)?;
let profiles =
self
.list_profiles()
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
format!("Failed to list profiles: {e}").into()
})?;
let mut profile = profiles
.into_iter()
.find(|p| p.id == profile_uuid)
.ok_or_else(|| -> Box<dyn std::error::Error + Send + Sync> {
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<dyn std::error::Error + Send + Sync> {
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<bool, Box<dyn std::error::Error + Send + Sync>> {
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<String> {
vec![
// Disable default browser check
@@ -1453,6 +1718,7 @@ pub async fn create_browser_profile_with_group(
release_type: String,
proxy_id: Option<String>,
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
) -> Result<BrowserProfile, String> {
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<String>,
camoufox_config: Option<CamoufoxConfig>,
wayfern_config: Option<WayfernConfig>,
group_id: Option<String>,
) -> Result<BrowserProfile, String> {
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> {
+3
View File
@@ -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<CamoufoxConfig>, // Camoufox configuration
#[serde(default)]
pub wayfern_config: Option<WayfernConfig>, // Wayfern configuration
#[serde(default)]
pub group_id: Option<String>, // Reference to profile group
#[serde(default)]
pub tags: Vec<String>, // Free-form tags
+1
View File
@@ -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,
+615
View File
@@ -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<String>,
#[serde(default)]
pub randomize_fingerprint_on_launch: Option<bool>,
#[serde(default)]
pub os: Option<String>,
#[serde(default)]
pub screen_max_width: Option<u32>,
#[serde(default)]
pub screen_max_height: Option<u32>,
#[serde(default)]
pub screen_min_width: Option<u32>,
#[serde(default)]
pub screen_min_height: Option<u32>,
#[serde(default)]
pub geoip: Option<serde_json::Value>, // For compatibility with shared config form
#[serde(default)]
pub block_images: Option<bool>, // For compatibility with shared config form
#[serde(default)]
pub block_webrtc: Option<bool>,
#[serde(default)]
pub block_webgl: Option<bool>,
#[serde(default)]
pub executable_path: Option<String>,
#[serde(default, skip_serializing)]
pub proxy: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(non_snake_case)]
pub struct WayfernLaunchResult {
pub id: String,
#[serde(alias = "process_id")]
pub processId: Option<u32>,
#[serde(alias = "profile_path")]
pub profilePath: Option<String>,
pub url: Option<String>,
pub cdp_port: Option<u16>,
}
#[derive(Debug)]
struct WayfernInstance {
#[allow(dead_code)]
id: String,
process_id: Option<u32>,
profile_path: Option<String>,
url: Option<String>,
cdp_port: Option<u16>,
}
struct WayfernManagerInner {
instances: HashMap<String, WayfernInstance>,
}
pub struct WayfernManager {
inner: Arc<AsyncMutex<WayfernManagerInner>>,
#[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<String>,
}
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<u16, Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<Vec<CdpTarget>, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("http://127.0.0.1:{port}/json");
let resp = self.http_client.get(&url).send().await?;
let targets: Vec<CdpTarget> = resp.json().await?;
Ok(targets)
}
async fn send_cdp_command(
&self,
ws_url: &str,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
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<String, Box<dyn std::error::Error + Send + Sync>> {
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::<Vec<_>>())
);
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<WayfernLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
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::<Vec<_>>())
);
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<dyn std::error::Error + Send + Sync>> {
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<WayfernLaunchResult> {
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<String> = 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<WayfernLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
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();
}
+26 -2
View File
@@ -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)
+33 -10
View File
@@ -28,6 +28,10 @@ interface CamoufoxConfigDialogProps {
onClose: () => void;
profile: BrowserProfile | null;
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
onSaveWayfern?: (
profile: BrowserProfile,
config: CamoufoxConfig,
) => Promise<void>;
isRunning?: boolean;
}
@@ -36,6 +40,7 @@ export function CamoufoxConfigDialog({
onClose,
profile,
onSave,
onSaveWayfern,
isRunning = false,
}: CamoufoxConfigDialogProps) {
const [config, setConfig] = useState<CamoufoxConfig>(() => ({
@@ -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 (
<Dialog open={isOpen} onOpenChange={handleClose}>
@@ -118,7 +138,7 @@ export function CamoufoxConfigDialog({
<DialogHeader className="shrink-0">
<DialogTitle>
{isRunning ? "View" : "Configure"} Fingerprint Settings -{" "}
{profile.name}
{profile.name} ({browserName})
</DialogTitle>
</DialogHeader>
@@ -129,6 +149,9 @@ export function CamoufoxConfigDialog({
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
browserType={
profile.browser === "wayfern" ? "wayfern" : "camoufox"
}
/>
</div>
</ScrollArea>
+209 -53
View File
@@ -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<void>;
selectedGroupId?: string;
@@ -115,6 +123,11 @@ export function CreateProfileDialog({
os: getCurrentOS(), // Default to current OS
}));
// Wayfern anti-detect states
const [wayfernConfig, setWayfernConfig] = useState<WayfernConfig>(() => ({
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"
>
<TabsList
className="grid flex-shrink-0 grid-cols-2 w-full"
defaultValue="anti-detect"
>
<TabsTrigger value="anti-detect">Anti-Detect</TabsTrigger>
<TabsTrigger value="regular">Regular</TabsTrigger>
</TabsList>
{/* Tab list hidden - only anti-detect browsers are supported */}
<ScrollArea className="overflow-y-auto flex-1">
<div className="flex flex-col justify-center items-center w-full">
@@ -482,30 +520,60 @@ export function CreateProfileDialog({
Anti-Detect Browser
</h3>
<p className="mt-2 text-sm text-muted-foreground">
Choose Firefox for anti-detection capabilities
Choose a browser with anti-detection capabilities
</p>
</div>
<Button
onClick={() => handleBrowserSelect("camoufox")}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
variant="outline"
>
<div className="flex justify-center items-center w-8 h-8">
{(() => {
const IconComponent = getBrowserIcon("firefox");
return IconComponent ? (
<IconComponent className="w-6 h-6" />
) : null;
})()}
</div>
<div className="text-left">
<div className="font-medium">Firefox</div>
<div className="text-sm text-muted-foreground">
Anti-Detect Browser
<div className="space-y-3">
{/* Wayfern (Chromium) - First */}
<Button
onClick={() => handleBrowserSelect("wayfern")}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
variant="outline"
>
<div className="flex justify-center items-center w-8 h-8">
{(() => {
const IconComponent = getBrowserIcon("wayfern");
return IconComponent ? (
<IconComponent className="w-6 h-6" />
) : null;
})()}
</div>
</div>
</Button>
<div className="text-left">
<div className="font-medium">
Chromium (Wayfern)
</div>
<div className="text-sm text-muted-foreground">
Anti-Detect Browser
</div>
</div>
</Button>
{/* Camoufox (Firefox) - Second */}
<Button
onClick={() => handleBrowserSelect("camoufox")}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
variant="outline"
>
<div className="flex justify-center items-center w-8 h-8">
{(() => {
const IconComponent =
getBrowserIcon("camoufox");
return IconComponent ? (
<IconComponent className="w-6 h-6" />
) : null;
})()}
</div>
<div className="text-left">
<div className="font-medium">
Firefox (Camoufox)
</div>
<div className="text-sm text-muted-foreground">
Anti-Detect Browser
</div>
</div>
</Button>
</div>
</div>
</TabsContent>
@@ -570,7 +638,94 @@ export function CreateProfileDialog({
/>
</div>
{selectedBrowser === "camoufox" ? (
{selectedBrowser === "wayfern" ? (
// Wayfern Configuration
<div className="space-y-6">
{/* Wayfern Download Status */}
{isLoadingReleaseTypes && (
<div className="flex gap-3 items-center p-3 rounded-md border">
<div className="w-4 h-4 rounded-full border-2 animate-spin border-muted/40 border-t-primary" />
<p className="text-sm text-muted-foreground">
Fetching available versions...
</p>
</div>
)}
{!isLoadingReleaseTypes && releaseTypesError && (
<div className="flex gap-3 items-center p-3 rounded-md border border-destructive/50 bg-destructive/10">
<p className="flex-1 text-sm text-destructive">
{releaseTypesError}
</p>
<RippleButton
onClick={() =>
selectedBrowser &&
loadReleaseTypes(selectedBrowser)
}
size="sm"
variant="outline"
>
Retry
</RippleButton>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!isBrowserCurrentlyDownloading("wayfern") &&
!isBrowserVersionAvailable("wayfern") &&
getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border">
<p className="text-sm text-muted-foreground">
{(() => {
const bestVersion =
getBestAvailableVersion("wayfern");
return `Wayfern version (${bestVersion?.version}) needs to be downloaded`;
})()}
</p>
<LoadingButton
onClick={() => handleDownload("wayfern")}
isLoading={isBrowserCurrentlyDownloading(
"wayfern",
)}
size="sm"
disabled={isBrowserCurrentlyDownloading(
"wayfern",
)}
>
{isBrowserCurrentlyDownloading("wayfern")
? "Downloading..."
: "Download"}
</LoadingButton>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!isBrowserCurrentlyDownloading("wayfern") &&
isBrowserVersionAvailable("wayfern") && (
<div className="p-3 text-sm rounded-md border text-muted-foreground">
{(() => {
const bestVersion =
getBestAvailableVersion("wayfern");
return `✓ Wayfern version (${bestVersion?.version}) is available`;
})()}
</div>
)}
{isBrowserCurrentlyDownloading("wayfern") && (
<div className="p-3 text-sm rounded-md border text-muted-foreground">
{(() => {
const bestVersion =
getBestAvailableVersion("wayfern");
return `Downloading Wayfern version (${bestVersion?.version})...`;
})()}
</div>
)}
<SharedCamoufoxConfigForm
config={wayfernConfig}
onConfigChange={updateWayfernConfig}
isCreating
browserType="wayfern"
/>
</div>
) : selectedBrowser === "camoufox" ? (
// Camoufox Configuration
<div className="space-y-6">
{/* Camoufox Download Status */}
@@ -654,6 +809,7 @@ export function CreateProfileDialog({
config={camoufoxConfig}
onConfigChange={updateCamoufoxConfig}
isCreating
browserType="camoufox"
/>
</div>
) : (
+2 -1
View File
@@ -1999,7 +1999,8 @@ export function ProfilesDataTable({
>
Assign to Group
</DropdownMenuItem>
{profile.browser === "camoufox" &&
{(profile.browser === "camoufox" ||
profile.browser === "wayfern") &&
meta.onConfigureCamoufox && (
<DropdownMenuItem
onClick={() => {
+38 -34
View File
@@ -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({
)}
<fieldset disabled={isEditingDisabled} className="space-y-6">
{/* Blocking Options */}
<div className="space-y-3">
<Label>Blocking Options</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
onConfigChange("block_images", checked)
}
/>
<Label htmlFor="block-images">Block Images</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
onConfigChange("block_webrtc", checked)
}
/>
<Label htmlFor="block-webrtc">Block WebRTC</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
onConfigChange("block_webgl", checked)
}
/>
<Label htmlFor="block-webgl">Block WebGL</Label>
{/* Blocking Options - Only available for Camoufox */}
{browserType === "camoufox" && (
<div className="space-y-3">
<Label>Blocking Options</Label>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Checkbox
id="block-images"
checked={config.block_images || false}
onCheckedChange={(checked) =>
onConfigChange("block_images", checked)
}
/>
<Label htmlFor="block-images">Block Images</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webrtc"
checked={config.block_webrtc || false}
onCheckedChange={(checked) =>
onConfigChange("block_webrtc", checked)
}
/>
<Label htmlFor="block-webrtc">Block WebRTC</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="block-webgl"
checked={config.block_webgl || false}
onCheckedChange={(checked) =>
onConfigChange("block_webgl", checked)
}
/>
<Label htmlFor="block-webgl">Block WebGL</Label>
</div>
</div>
</div>
</div>
)}
{/* Navigator Properties */}
<div className="space-y-3">
+10 -15
View File
@@ -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;
}
}
+27
View File
@@ -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;