mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-22 20:06:18 +02:00
feat: partially add wayfern
This commit is contained in:
Generated
+53
-1
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -1999,7 +1999,8 @@ export function ProfilesDataTable({
|
||||
>
|
||||
Assign to Group
|
||||
</DropdownMenuItem>
|
||||
{profile.browser === "camoufox" &&
|
||||
{(profile.browser === "camoufox" ||
|
||||
profile.browser === "wayfern") &&
|
||||
meta.onConfigureCamoufox && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user