mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-23 09:50:58 +02:00
feat: partially add wayfern
This commit is contained in:
Generated
+53
-1
@@ -1204,6 +1204,12 @@ dependencies = [
|
|||||||
"syn 2.0.111",
|
"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]]
|
[[package]]
|
||||||
name = "deadpool"
|
name = "deadpool"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1403,6 +1409,7 @@ dependencies = [
|
|||||||
"maxminddb",
|
"maxminddb",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"msi-extract",
|
"msi-extract",
|
||||||
|
"nix 0.29.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -1431,6 +1438,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"url",
|
"url",
|
||||||
@@ -3185,6 +3193,18 @@ version = "1.0.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
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]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.30.1"
|
version = "0.30.1"
|
||||||
@@ -6055,6 +6075,20 @@ dependencies = [
|
|||||||
"tokio-util",
|
"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]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.17"
|
version = "0.7.17"
|
||||||
@@ -6280,6 +6314,24 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
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]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -7518,7 +7570,7 @@ dependencies = [
|
|||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
"hex",
|
"hex",
|
||||||
"nix",
|
"nix 0.30.1",
|
||||||
"ordered-stream",
|
"ordered-stream",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ async-socks5 = "0.6"
|
|||||||
|
|
||||||
# Camoufox/Playwright integration
|
# Camoufox/Playwright integration
|
||||||
playwright = { git = "https://github.com/sctg-development/playwright-rust", branch = "master" }
|
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"] }
|
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
thiserror = "1.0"
|
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]
|
[target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\"))".dependencies]
|
||||||
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
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]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
core-foundation = "0.10"
|
core-foundation = "0.10"
|
||||||
objc2 = "0.6.1"
|
objc2 = "0.6.1"
|
||||||
|
|||||||
@@ -300,6 +300,10 @@ pub fn is_browser_version_nightly(
|
|||||||
// For Camoufox, beta versions are actually the stable releases
|
// For Camoufox, beta versions are actually the stable releases
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
"wayfern" => {
|
||||||
|
// For Wayfern, all releases from version.json are stable
|
||||||
|
false
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Default fallback
|
// Default fallback
|
||||||
is_nightly_version(version)
|
is_nightly_version(version)
|
||||||
@@ -330,6 +334,13 @@ pub struct BrowserRelease {
|
|||||||
pub is_prerelease: bool,
|
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)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct CachedVersionData {
|
struct CachedVersionData {
|
||||||
releases: Vec<BrowserRelease>,
|
releases: Vec<BrowserRelease>,
|
||||||
@@ -342,6 +353,12 @@ struct CachedGithubData {
|
|||||||
timestamp: u64,
|
timestamp: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct CachedWayfernData {
|
||||||
|
version_info: WayfernVersionInfo,
|
||||||
|
timestamp: u64,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ApiClient {
|
pub struct ApiClient {
|
||||||
client: Client,
|
client: Client,
|
||||||
firefox_api_base: String,
|
firefox_api_base: String,
|
||||||
@@ -1065,6 +1082,95 @@ impl ApiClient {
|
|||||||
Ok(compatible_releases)
|
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
|
/// Check if a Zen twilight release has been updated by comparing file size
|
||||||
pub async fn check_twilight_update(
|
pub async fn check_twilight_update(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ pub struct CreateProfileRequest {
|
|||||||
pub release_type: Option<String>,
|
pub release_type: Option<String>,
|
||||||
#[schema(value_type = Object)]
|
#[schema(value_type = Object)]
|
||||||
pub camoufox_config: Option<serde_json::Value>,
|
pub camoufox_config: Option<serde_json::Value>,
|
||||||
|
#[schema(value_type = Object)]
|
||||||
|
pub wayfern_config: Option<serde_json::Value>,
|
||||||
pub group_id: Option<String>,
|
pub group_id: Option<String>,
|
||||||
pub tags: Option<Vec<String>>,
|
pub tags: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
@@ -560,6 +562,13 @@ async fn create_profile(
|
|||||||
None
|
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
|
// Create profile using the async create_profile_with_group method
|
||||||
match profile_manager
|
match profile_manager
|
||||||
.create_profile_with_group(
|
.create_profile_with_group(
|
||||||
@@ -570,6 +579,7 @@ async fn create_profile(
|
|||||||
request.release_type.as_deref().unwrap_or("stable"),
|
request.release_type.as_deref().unwrap_or("stable"),
|
||||||
request.proxy_id.clone(),
|
request.proxy_id.clone(),
|
||||||
camoufox_config,
|
camoufox_config,
|
||||||
|
wayfern_config,
|
||||||
request.group_id.clone(),
|
request.group_id.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -515,6 +515,7 @@ mod tests {
|
|||||||
last_launch: None,
|
last_launch: None,
|
||||||
release_type: "stable".to_string(),
|
release_type: "stable".to_string(),
|
||||||
camoufox_config: None,
|
camoufox_config: None,
|
||||||
|
wayfern_config: None,
|
||||||
group_id: None,
|
group_id: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
|
|||||||
+209
-3
@@ -19,6 +19,7 @@ pub enum BrowserType {
|
|||||||
Brave,
|
Brave,
|
||||||
Zen,
|
Zen,
|
||||||
Camoufox,
|
Camoufox,
|
||||||
|
Wayfern,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserType {
|
impl BrowserType {
|
||||||
@@ -30,6 +31,7 @@ impl BrowserType {
|
|||||||
BrowserType::Brave => "brave",
|
BrowserType::Brave => "brave",
|
||||||
BrowserType::Zen => "zen",
|
BrowserType::Zen => "zen",
|
||||||
BrowserType::Camoufox => "camoufox",
|
BrowserType::Camoufox => "camoufox",
|
||||||
|
BrowserType::Wayfern => "wayfern",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +43,7 @@ impl BrowserType {
|
|||||||
"brave" => Ok(BrowserType::Brave),
|
"brave" => Ok(BrowserType::Brave),
|
||||||
"zen" => Ok(BrowserType::Zen),
|
"zen" => Ok(BrowserType::Zen),
|
||||||
"camoufox" => Ok(BrowserType::Camoufox),
|
"camoufox" => Ok(BrowserType::Camoufox),
|
||||||
|
"wayfern" => Ok(BrowserType::Wayfern),
|
||||||
_ => Err(format!("Unknown browser type: {s}")),
|
_ => Err(format!("Unknown browser type: {s}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +228,47 @@ mod macos {
|
|||||||
Ok(executable_path)
|
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 {
|
pub fn is_firefox_version_downloaded(install_dir: &Path) -> bool {
|
||||||
// On macOS, check for .app files
|
// On macOS, check for .app files
|
||||||
if let Ok(entries) = std::fs::read_dir(install_dir) {
|
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("brave-browser").join("brave"),
|
||||||
install_dir.join("bin").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![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -424,6 +478,16 @@ mod linux {
|
|||||||
install_dir.join("brave-browser").join("brave"),
|
install_dir.join("brave-browser").join("brave"),
|
||||||
install_dir.join("bin").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![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -521,6 +585,16 @@ mod windows {
|
|||||||
install_dir.join("brave").join("brave.exe"),
|
install_dir.join("brave").join("brave.exe"),
|
||||||
install_dir.join("brave-browser").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![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -536,14 +610,18 @@ mod windows {
|
|||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||||
let name = path.file_stem().unwrap_or_default().to_string_lossy();
|
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);
|
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 {
|
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").join("brave.exe"),
|
||||||
install_dir.join("brave-browser").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![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -618,7 +706,11 @@ mod windows {
|
|||||||
|
|
||||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
if path.extension().is_some_and(|ext| ext == "exe") {
|
||||||
let name = path.file_stem().unwrap_or_default().to_string_lossy();
|
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;
|
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;
|
pub struct BrowserFactory;
|
||||||
|
|
||||||
impl BrowserFactory {
|
impl BrowserFactory {
|
||||||
@@ -964,6 +1164,7 @@ impl BrowserFactory {
|
|||||||
}
|
}
|
||||||
BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)),
|
BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)),
|
||||||
BrowserType::Camoufox => Box::new(CamoufoxBrowser::new()),
|
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::Brave.as_str(), "brave");
|
||||||
assert_eq!(BrowserType::Zen.as_str(), "zen");
|
assert_eq!(BrowserType::Zen.as_str(), "zen");
|
||||||
assert_eq!(BrowserType::Camoufox.as_str(), "camoufox");
|
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
|
// Test from_str - use expect with descriptive messages instead of unwrap
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1071,6 +1273,10 @@ mod tests {
|
|||||||
BrowserType::from_str("camoufox").expect("camoufox should be valid"),
|
BrowserType::from_str("camoufox").expect("camoufox should be valid"),
|
||||||
BrowserType::Camoufox
|
BrowserType::Camoufox
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
BrowserType::from_str("wayfern").expect("wayfern should be valid"),
|
||||||
|
BrowserType::Wayfern
|
||||||
|
);
|
||||||
|
|
||||||
// Test invalid browser type - these should properly fail
|
// Test invalid browser type - these should properly fail
|
||||||
let invalid_result = BrowserType::from_str("invalid");
|
let invalid_result = BrowserType::from_str("invalid");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
|
|||||||
use crate::platform_browser;
|
use crate::platform_browser;
|
||||||
use crate::profile::{BrowserProfile, ProfileManager};
|
use crate::profile::{BrowserProfile, ProfileManager};
|
||||||
use crate::proxy_manager::PROXY_MANAGER;
|
use crate::proxy_manager::PROXY_MANAGER;
|
||||||
|
use crate::wayfern_manager::{WayfernConfig, WayfernManager};
|
||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -16,6 +17,7 @@ pub struct BrowserRunner {
|
|||||||
pub downloaded_browsers_registry: &'static DownloadedBrowsersRegistry,
|
pub downloaded_browsers_registry: &'static DownloadedBrowsersRegistry,
|
||||||
auto_updater: &'static crate::auto_updater::AutoUpdater,
|
auto_updater: &'static crate::auto_updater::AutoUpdater,
|
||||||
camoufox_manager: &'static CamoufoxManager,
|
camoufox_manager: &'static CamoufoxManager,
|
||||||
|
wayfern_manager: &'static WayfernManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BrowserRunner {
|
impl BrowserRunner {
|
||||||
@@ -26,6 +28,7 @@ impl BrowserRunner {
|
|||||||
downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(),
|
downloaded_browsers_registry: DownloadedBrowsersRegistry::instance(),
|
||||||
auto_updater: crate::auto_updater::AutoUpdater::instance(),
|
auto_updater: crate::auto_updater::AutoUpdater::instance(),
|
||||||
camoufox_manager: CamoufoxManager::instance(),
|
camoufox_manager: CamoufoxManager::instance(),
|
||||||
|
wayfern_manager: WayfernManager::instance(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,6 +300,204 @@ impl BrowserRunner {
|
|||||||
return Ok(updated_profile);
|
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
|
// Create browser instance
|
||||||
let browser_type = BrowserType::from_str(&profile.browser)
|
let browser_type = BrowserType::from_str(&profile.browser)
|
||||||
.map_err(|_| format!("Invalid browser type: {}", 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
|
let is_running = self
|
||||||
.check_browser_status(app_handle.clone(), profile)
|
.check_browser_status(app_handle.clone(), profile)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -657,6 +886,10 @@ impl BrowserRunner {
|
|||||||
// Camoufox URL opening is handled differently
|
// Camoufox URL opening is handled differently
|
||||||
Err("URL opening in existing Camoufox instance is not supported".into())
|
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 => {
|
BrowserType::Chromium | BrowserType::Brave => {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
@@ -1382,7 +1615,325 @@ impl BrowserRunner {
|
|||||||
return Ok(());
|
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 {
|
let pid = if let Some(pid) = profile.process_id {
|
||||||
// First verify the stored PID is still valid and belongs to our profile
|
// First verify the stored PID is still valid and belongs to our profile
|
||||||
let system = System::new_all();
|
let system = System::new_all();
|
||||||
@@ -1832,9 +2383,9 @@ pub async fn launch_browser_profile(
|
|||||||
profile_for_launch.id
|
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
|
// 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)
|
// Determine upstream proxy if configured; otherwise use DIRECT (no upstream)
|
||||||
let upstream_proxy = profile_for_launch
|
let upstream_proxy = profile_for_launch
|
||||||
.proxy_id
|
.proxy_id
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ impl BrowserVersionManager {
|
|||||||
// Camoufox supports all platforms and architectures according to the JS code
|
// Camoufox supports all platforms and architectures according to the JS code
|
||||||
Ok(true)
|
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()),
|
_ => Err(format!("Unknown browser: {browser}").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,6 +103,7 @@ impl BrowserVersionManager {
|
|||||||
"brave",
|
"brave",
|
||||||
"chromium",
|
"chromium",
|
||||||
"camoufox",
|
"camoufox",
|
||||||
|
"wayfern",
|
||||||
];
|
];
|
||||||
|
|
||||||
all_browsers
|
all_browsers
|
||||||
@@ -224,6 +241,7 @@ impl BrowserVersionManager {
|
|||||||
"brave" => self.fetch_brave_versions(true).await?,
|
"brave" => self.fetch_brave_versions(true).await?,
|
||||||
"chromium" => self.fetch_chromium_versions(true).await?,
|
"chromium" => self.fetch_chromium_versions(true).await?,
|
||||||
"camoufox" => self.fetch_camoufox_versions(true).await?,
|
"camoufox" => self.fetch_camoufox_versions(true).await?,
|
||||||
|
"wayfern" => self.fetch_wayfern_versions(true).await?,
|
||||||
_ => return Err(format!("Unsupported browser: {browser}").into()),
|
_ => return Err(format!("Unsupported browser: {browser}").into()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -424,6 +442,17 @@ impl BrowserVersionManager {
|
|||||||
})
|
})
|
||||||
.collect()
|
.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());
|
return Err(format!("Unsupported browser: {browser}").into());
|
||||||
}
|
}
|
||||||
@@ -647,6 +676,31 @@ impl BrowserVersionManager {
|
|||||||
is_archive: true,
|
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()),
|
_ => Err(format!("Unsupported browser: {browser}").into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -820,6 +874,27 @@ impl BrowserVersionManager {
|
|||||||
.fetch_camoufox_releases_with_caching(no_caching)
|
.fetch_camoufox_releases_with_caching(no_caching)
|
||||||
.await
|
.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]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -169,6 +169,43 @@ impl Downloader {
|
|||||||
|
|
||||||
Ok(asset_url)
|
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
|
// For other browsers, use the provided URL
|
||||||
Ok(download_info.url.clone())
|
Ok(download_info.url.clone())
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ impl Extractor {
|
|||||||
// Determine browser type from the destination directory path
|
// Determine browser type from the destination directory path
|
||||||
let browser_type = if dest_dir.to_string_lossy().contains("camoufox") {
|
let browser_type = if dest_dir.to_string_lossy().contains("camoufox") {
|
||||||
"camoufox"
|
"camoufox"
|
||||||
|
} else if dest_dir.to_string_lossy().contains("wayfern") {
|
||||||
|
"wayfern"
|
||||||
} else if dest_dir.to_string_lossy().contains("firefox") {
|
} else if dest_dir.to_string_lossy().contains("firefox") {
|
||||||
"firefox"
|
"firefox"
|
||||||
} else if dest_dir.to_string_lossy().contains("zen") {
|
} else if dest_dir.to_string_lossy().contains("zen") {
|
||||||
@@ -45,9 +47,9 @@ impl Extractor {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
// For Camoufox on Linux, we expect the executable directly under version directory
|
// For Camoufox and Wayfern on Linux, we expect the executable directly under version directory
|
||||||
// e.g., binaries/camoufox/<version>/camoufox, without an extra camoufox/ subdirectory
|
// e.g., binaries/camoufox/<version>/camoufox, without an extra subdirectory
|
||||||
if browser_type == "camoufox" {
|
if browser_type == "camoufox" || browser_type == "wayfern" {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,6 +1013,10 @@ impl Extractor {
|
|||||||
"camoufox",
|
"camoufox",
|
||||||
"camoufox-bin",
|
"camoufox-bin",
|
||||||
"camoufox-browser",
|
"camoufox-browser",
|
||||||
|
// Wayfern variants
|
||||||
|
"wayfern",
|
||||||
|
"wayfern-bin",
|
||||||
|
"wayfern-browser",
|
||||||
];
|
];
|
||||||
|
|
||||||
// First, try direct lookup in the main directory
|
// First, try direct lookup in the main directory
|
||||||
@@ -1036,6 +1042,7 @@ impl Extractor {
|
|||||||
"brave",
|
"brave",
|
||||||
"zen",
|
"zen",
|
||||||
"camoufox",
|
"camoufox",
|
||||||
|
"wayfern",
|
||||||
".",
|
".",
|
||||||
"./",
|
"./",
|
||||||
"firefox",
|
"firefox",
|
||||||
@@ -1141,6 +1148,7 @@ impl Extractor {
|
|||||||
|| name_lower.contains("brave")
|
|| name_lower.contains("brave")
|
||||||
|| name_lower.contains("zen")
|
|| name_lower.contains("zen")
|
||||||
|| name_lower.contains("camoufox")
|
|| name_lower.contains("camoufox")
|
||||||
|
|| name_lower.contains("wayfern")
|
||||||
|| name_lower.ends_with(".appimage")
|
|| name_lower.ends_with(".appimage")
|
||||||
|| !name_lower.contains('.')
|
|| !name_lower.contains('.')
|
||||||
{
|
{
|
||||||
@@ -1196,6 +1204,7 @@ impl Extractor {
|
|||||||
|| name_lower.contains("brave")
|
|| name_lower.contains("brave")
|
||||||
|| name_lower.contains("zen")
|
|| name_lower.contains("zen")
|
||||||
|| name_lower.contains("camoufox")
|
|| name_lower.contains("camoufox")
|
||||||
|
|| name_lower.contains("wayfern")
|
||||||
|| file_name.ends_with(".AppImage")
|
|| file_name.ends_with(".AppImage")
|
||||||
{
|
{
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub mod proxy_storage;
|
|||||||
mod settings_manager;
|
mod settings_manager;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
pub mod traffic_stats;
|
pub mod traffic_stats;
|
||||||
|
mod wayfern_manager;
|
||||||
// mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme
|
// mod theme_detector; // removed: theme detection handled in webview via CSS prefers-color-scheme
|
||||||
mod tag_manager;
|
mod tag_manager;
|
||||||
mod version_updater;
|
mod version_updater;
|
||||||
@@ -44,7 +45,7 @@ use browser_runner::{
|
|||||||
use profile::manager::{
|
use profile::manager::{
|
||||||
check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles,
|
check_browser_status, create_browser_profile_new, delete_profile, list_browser_profiles,
|
||||||
rename_profile, update_camoufox_config, update_profile_note, update_profile_proxy,
|
rename_profile, update_camoufox_config, update_profile_note, update_profile_proxy,
|
||||||
update_profile_tags,
|
update_profile_tags, update_wayfern_config,
|
||||||
};
|
};
|
||||||
|
|
||||||
use browser_version_manager::{
|
use browser_version_manager::{
|
||||||
@@ -801,6 +802,7 @@ pub fn run() {
|
|||||||
check_proxy_validity,
|
check_proxy_validity,
|
||||||
get_cached_proxy_check,
|
get_cached_proxy_check,
|
||||||
update_camoufox_config,
|
update_camoufox_config,
|
||||||
|
update_wayfern_config,
|
||||||
get_profile_groups,
|
get_profile_groups,
|
||||||
get_groups_with_profile_counts,
|
get_groups_with_profile_counts,
|
||||||
create_profile_group,
|
create_profile_group,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::camoufox_manager::CamoufoxConfig;
|
|||||||
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
|
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
|
||||||
use crate::profile::types::BrowserProfile;
|
use crate::profile::types::BrowserProfile;
|
||||||
use crate::proxy_manager::PROXY_MANAGER;
|
use crate::proxy_manager::PROXY_MANAGER;
|
||||||
|
use crate::wayfern_manager::WayfernConfig;
|
||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use std::fs::{self, create_dir_all};
|
use std::fs::{self, create_dir_all};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -13,6 +14,7 @@ use tauri::Emitter;
|
|||||||
pub struct ProfileManager {
|
pub struct ProfileManager {
|
||||||
base_dirs: BaseDirs,
|
base_dirs: BaseDirs,
|
||||||
camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager,
|
camoufox_manager: &'static crate::camoufox_manager::CamoufoxManager,
|
||||||
|
wayfern_manager: &'static crate::wayfern_manager::WayfernManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileManager {
|
impl ProfileManager {
|
||||||
@@ -20,6 +22,7 @@ impl ProfileManager {
|
|||||||
Self {
|
Self {
|
||||||
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
|
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
|
||||||
camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(),
|
camoufox_manager: crate::camoufox_manager::CamoufoxManager::instance(),
|
||||||
|
wayfern_manager: crate::wayfern_manager::WayfernManager::instance(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ impl ProfileManager {
|
|||||||
release_type: &str,
|
release_type: &str,
|
||||||
proxy_id: Option<String>,
|
proxy_id: Option<String>,
|
||||||
camoufox_config: Option<CamoufoxConfig>,
|
camoufox_config: Option<CamoufoxConfig>,
|
||||||
|
wayfern_config: Option<WayfernConfig>,
|
||||||
group_id: Option<String>,
|
group_id: Option<String>,
|
||||||
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||||
log::info!("Attempting to create profile: {name}");
|
log::info!("Attempting to create profile: {name}");
|
||||||
@@ -163,6 +167,7 @@ impl ProfileManager {
|
|||||||
last_launch: None,
|
last_launch: None,
|
||||||
release_type: release_type.to_string(),
|
release_type: release_type.to_string(),
|
||||||
camoufox_config: None,
|
camoufox_config: None,
|
||||||
|
wayfern_config: None,
|
||||||
group_id: group_id.clone(),
|
group_id: group_id.clone(),
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
@@ -198,6 +203,118 @@ impl ProfileManager {
|
|||||||
camoufox_config.clone()
|
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 {
|
let profile = BrowserProfile {
|
||||||
id: profile_id,
|
id: profile_id,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
@@ -208,6 +325,7 @@ impl ProfileManager {
|
|||||||
last_launch: None,
|
last_launch: None,
|
||||||
release_type: release_type.to_string(),
|
release_type: release_type.to_string(),
|
||||||
camoufox_config: final_camoufox_config,
|
camoufox_config: final_camoufox_config,
|
||||||
|
wayfern_config: final_wayfern_config,
|
||||||
group_id: group_id.clone(),
|
group_id: group_id.clone(),
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
note: None,
|
note: None,
|
||||||
@@ -722,6 +840,66 @@ impl ProfileManager {
|
|||||||
Ok(())
|
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(
|
pub async fn update_profile_proxy(
|
||||||
&self,
|
&self,
|
||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
@@ -825,6 +1003,11 @@ impl ProfileManager {
|
|||||||
return self.check_camoufox_status(&app_handle, profile).await;
|
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
|
// For non-camoufox browsers, use the existing PID-based logic
|
||||||
let inner_profile = profile.clone();
|
let inner_profile = profile.clone();
|
||||||
let mut system = System::new();
|
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> {
|
fn get_common_firefox_preferences(&self) -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
// Disable default browser check
|
// Disable default browser check
|
||||||
@@ -1453,6 +1718,7 @@ pub async fn create_browser_profile_with_group(
|
|||||||
release_type: String,
|
release_type: String,
|
||||||
proxy_id: Option<String>,
|
proxy_id: Option<String>,
|
||||||
camoufox_config: Option<CamoufoxConfig>,
|
camoufox_config: Option<CamoufoxConfig>,
|
||||||
|
wayfern_config: Option<WayfernConfig>,
|
||||||
group_id: Option<String>,
|
group_id: Option<String>,
|
||||||
) -> Result<BrowserProfile, String> {
|
) -> Result<BrowserProfile, String> {
|
||||||
let profile_manager = ProfileManager::instance();
|
let profile_manager = ProfileManager::instance();
|
||||||
@@ -1465,6 +1731,7 @@ pub async fn create_browser_profile_with_group(
|
|||||||
&release_type,
|
&release_type,
|
||||||
proxy_id,
|
proxy_id,
|
||||||
camoufox_config,
|
camoufox_config,
|
||||||
|
wayfern_config,
|
||||||
group_id,
|
group_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1550,6 +1817,7 @@ pub async fn create_browser_profile_new(
|
|||||||
release_type: String,
|
release_type: String,
|
||||||
proxy_id: Option<String>,
|
proxy_id: Option<String>,
|
||||||
camoufox_config: Option<CamoufoxConfig>,
|
camoufox_config: Option<CamoufoxConfig>,
|
||||||
|
wayfern_config: Option<WayfernConfig>,
|
||||||
group_id: Option<String>,
|
group_id: Option<String>,
|
||||||
) -> Result<BrowserProfile, String> {
|
) -> Result<BrowserProfile, String> {
|
||||||
let browser_type =
|
let browser_type =
|
||||||
@@ -1562,6 +1830,7 @@ pub async fn create_browser_profile_new(
|
|||||||
release_type,
|
release_type,
|
||||||
proxy_id,
|
proxy_id,
|
||||||
camoufox_config,
|
camoufox_config,
|
||||||
|
wayfern_config,
|
||||||
group_id,
|
group_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -1580,6 +1849,19 @@ pub async fn update_camoufox_config(
|
|||||||
.map_err(|e| format!("Failed to update Camoufox config: {e}"))
|
.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
|
// Global singleton instance
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> {
|
pub fn delete_profile(app_handle: tauri::AppHandle, profile_id: String) -> Result<(), String> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::camoufox_manager::CamoufoxConfig;
|
use crate::camoufox_manager::CamoufoxConfig;
|
||||||
|
use crate::wayfern_manager::WayfernConfig;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ pub struct BrowserProfile {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub camoufox_config: Option<CamoufoxConfig>, // Camoufox configuration
|
pub camoufox_config: Option<CamoufoxConfig>, // Camoufox configuration
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub wayfern_config: Option<WayfernConfig>, // Wayfern configuration
|
||||||
|
#[serde(default)]
|
||||||
pub group_id: Option<String>, // Reference to profile group
|
pub group_id: Option<String>, // Reference to profile group
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tags: Vec<String>, // Free-form tags
|
pub tags: Vec<String>, // Free-form tags
|
||||||
|
|||||||
@@ -549,6 +549,7 @@ impl ProfileImporter {
|
|||||||
last_launch: None,
|
last_launch: None,
|
||||||
release_type: "stable".to_string(),
|
release_type: "stable".to_string(),
|
||||||
camoufox_config: None,
|
camoufox_config: None,
|
||||||
|
wayfern_config: None,
|
||||||
group_id: None,
|
group_id: None,
|
||||||
tags: Vec::new(),
|
tags: Vec::new(),
|
||||||
note: None,
|
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 { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||||
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
||||||
import { showErrorToast, showSuccessToast, showToast } from "@/lib/toast-utils";
|
import { showErrorToast, showSuccessToast, showToast } from "@/lib/toast-utils";
|
||||||
import type { BrowserProfile, CamoufoxConfig } from "@/types";
|
import type { BrowserProfile, CamoufoxConfig, WayfernConfig } from "@/types";
|
||||||
|
|
||||||
type BrowserTypeString =
|
type BrowserTypeString =
|
||||||
| "firefox"
|
| "firefox"
|
||||||
@@ -37,7 +37,8 @@ type BrowserTypeString =
|
|||||||
| "chromium"
|
| "chromium"
|
||||||
| "brave"
|
| "brave"
|
||||||
| "zen"
|
| "zen"
|
||||||
| "camoufox";
|
| "camoufox"
|
||||||
|
| "wayfern";
|
||||||
|
|
||||||
interface PendingUrl {
|
interface PendingUrl {
|
||||||
id: string;
|
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(
|
const handleCreateProfile = useCallback(
|
||||||
async (profileData: {
|
async (profileData: {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -395,6 +416,7 @@ export default function Home() {
|
|||||||
releaseType: string;
|
releaseType: string;
|
||||||
proxyId?: string;
|
proxyId?: string;
|
||||||
camoufoxConfig?: CamoufoxConfig;
|
camoufoxConfig?: CamoufoxConfig;
|
||||||
|
wayfernConfig?: WayfernConfig;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
@@ -405,6 +427,7 @@ export default function Home() {
|
|||||||
releaseType: profileData.releaseType,
|
releaseType: profileData.releaseType,
|
||||||
proxyId: profileData.proxyId,
|
proxyId: profileData.proxyId,
|
||||||
camoufoxConfig: profileData.camoufoxConfig,
|
camoufoxConfig: profileData.camoufoxConfig,
|
||||||
|
wayfernConfig: profileData.wayfernConfig,
|
||||||
groupId:
|
groupId:
|
||||||
profileData.groupId ||
|
profileData.groupId ||
|
||||||
(selectedGroupId !== "default" ? selectedGroupId : undefined),
|
(selectedGroupId !== "default" ? selectedGroupId : undefined),
|
||||||
@@ -834,6 +857,7 @@ export default function Home() {
|
|||||||
}}
|
}}
|
||||||
profile={currentProfileForCamoufoxConfig}
|
profile={currentProfileForCamoufoxConfig}
|
||||||
onSave={handleSaveCamoufoxConfig}
|
onSave={handleSaveCamoufoxConfig}
|
||||||
|
onSaveWayfern={handleSaveWayfernConfig}
|
||||||
isRunning={
|
isRunning={
|
||||||
currentProfileForCamoufoxConfig
|
currentProfileForCamoufoxConfig
|
||||||
? runningProfiles.has(currentProfileForCamoufoxConfig.id)
|
? runningProfiles.has(currentProfileForCamoufoxConfig.id)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ interface CamoufoxConfigDialogProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
profile: BrowserProfile | null;
|
profile: BrowserProfile | null;
|
||||||
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
|
onSave: (profile: BrowserProfile, config: CamoufoxConfig) => Promise<void>;
|
||||||
|
onSaveWayfern?: (
|
||||||
|
profile: BrowserProfile,
|
||||||
|
config: CamoufoxConfig,
|
||||||
|
) => Promise<void>;
|
||||||
isRunning?: boolean;
|
isRunning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +40,7 @@ export function CamoufoxConfigDialog({
|
|||||||
onClose,
|
onClose,
|
||||||
profile,
|
profile,
|
||||||
onSave,
|
onSave,
|
||||||
|
onSaveWayfern,
|
||||||
isRunning = false,
|
isRunning = false,
|
||||||
}: CamoufoxConfigDialogProps) {
|
}: CamoufoxConfigDialogProps) {
|
||||||
const [config, setConfig] = useState<CamoufoxConfig>(() => ({
|
const [config, setConfig] = useState<CamoufoxConfig>(() => ({
|
||||||
@@ -44,17 +49,24 @@ export function CamoufoxConfigDialog({
|
|||||||
}));
|
}));
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const isAntiDetectBrowser =
|
||||||
|
profile?.browser === "camoufox" || profile?.browser === "wayfern";
|
||||||
|
|
||||||
// Initialize config when profile changes
|
// Initialize config when profile changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profile && profile.browser === "camoufox") {
|
if (profile && isAntiDetectBrowser) {
|
||||||
|
const profileConfig =
|
||||||
|
profile.browser === "wayfern"
|
||||||
|
? profile.wayfern_config
|
||||||
|
: profile.camoufox_config;
|
||||||
setConfig(
|
setConfig(
|
||||||
profile.camoufox_config || {
|
profileConfig || {
|
||||||
geoip: true,
|
geoip: true,
|
||||||
os: getCurrentOS(),
|
os: getCurrentOS(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [profile]);
|
}, [profile, isAntiDetectBrowser]);
|
||||||
|
|
||||||
const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => {
|
const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => {
|
||||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||||
@@ -79,10 +91,14 @@ export function CamoufoxConfigDialog({
|
|||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await onSave(profile, config);
|
if (profile.browser === "wayfern" && onSaveWayfern) {
|
||||||
|
await onSaveWayfern(profile, config);
|
||||||
|
} else {
|
||||||
|
await onSave(profile, config);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to save camoufox config:", error);
|
console.error("Failed to save config:", error);
|
||||||
const { toast } = await import("sonner");
|
const { toast } = await import("sonner");
|
||||||
toast.error("Failed to save configuration", {
|
toast.error("Failed to save configuration", {
|
||||||
description:
|
description:
|
||||||
@@ -95,9 +111,13 @@ export function CamoufoxConfigDialog({
|
|||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
// Reset config to original when closing without saving
|
// 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(
|
setConfig(
|
||||||
profile.camoufox_config || {
|
profileConfig || {
|
||||||
geoip: true,
|
geoip: true,
|
||||||
os: getCurrentOS(),
|
os: getCurrentOS(),
|
||||||
},
|
},
|
||||||
@@ -106,11 +126,11 @@ export function CamoufoxConfigDialog({
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!profile || profile.browser !== "camoufox") {
|
if (!profile || !isAntiDetectBrowser) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No OS warning needed anymore since we removed OS selection
|
const browserName = profile.browser === "wayfern" ? "Wayfern" : "Camoufox";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||||
@@ -118,7 +138,7 @@ export function CamoufoxConfigDialog({
|
|||||||
<DialogHeader className="shrink-0">
|
<DialogHeader className="shrink-0">
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{isRunning ? "View" : "Configure"} Fingerprint Settings -{" "}
|
{isRunning ? "View" : "Configure"} Fingerprint Settings -{" "}
|
||||||
{profile.name}
|
{profile.name} ({browserName})
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -129,6 +149,9 @@ export function CamoufoxConfigDialog({
|
|||||||
onConfigChange={updateConfig}
|
onConfigChange={updateConfig}
|
||||||
forceAdvanced={true}
|
forceAdvanced={true}
|
||||||
readOnly={isRunning}
|
readOnly={isRunning}
|
||||||
|
browserType={
|
||||||
|
profile.browser === "wayfern" ? "wayfern" : "camoufox"
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -24,12 +24,18 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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 { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||||
import { useProxyEvents } from "@/hooks/use-proxy-events";
|
import { useProxyEvents } from "@/hooks/use-proxy-events";
|
||||||
import { getBrowserIcon } from "@/lib/browser-utils";
|
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 => {
|
const getCurrentOS = (): CamoufoxOS => {
|
||||||
if (typeof navigator === "undefined") return "linux";
|
if (typeof navigator === "undefined") return "linux";
|
||||||
@@ -47,7 +53,8 @@ type BrowserTypeString =
|
|||||||
| "chromium"
|
| "chromium"
|
||||||
| "brave"
|
| "brave"
|
||||||
| "zen"
|
| "zen"
|
||||||
| "camoufox";
|
| "camoufox"
|
||||||
|
| "wayfern";
|
||||||
|
|
||||||
interface CreateProfileDialogProps {
|
interface CreateProfileDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -59,6 +66,7 @@ interface CreateProfileDialogProps {
|
|||||||
releaseType: string;
|
releaseType: string;
|
||||||
proxyId?: string;
|
proxyId?: string;
|
||||||
camoufoxConfig?: CamoufoxConfig;
|
camoufoxConfig?: CamoufoxConfig;
|
||||||
|
wayfernConfig?: WayfernConfig;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
selectedGroupId?: string;
|
selectedGroupId?: string;
|
||||||
@@ -115,6 +123,11 @@ export function CreateProfileDialog({
|
|||||||
os: getCurrentOS(), // Default to current OS
|
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
|
// Handle browser selection from the initial screen
|
||||||
const handleBrowserSelect = (browser: BrowserTypeString) => {
|
const handleBrowserSelect = (browser: BrowserTypeString) => {
|
||||||
setSelectedBrowser(browser);
|
setSelectedBrowser(browser);
|
||||||
@@ -197,7 +210,7 @@ export function CreateProfileDialog({
|
|||||||
// Only update state if this browser is still the one we're loading
|
// Only update state if this browser is still the one we're loading
|
||||||
if (loadingBrowserRef.current === browser) {
|
if (loadingBrowserRef.current === browser) {
|
||||||
// Filter to enforce stable-only creation, except Firefox Developer (nightly-only)
|
// Filter to enforce stable-only creation, except Firefox Developer (nightly-only)
|
||||||
if (browser === "camoufox") {
|
if (browser === "camoufox" || browser === "wayfern") {
|
||||||
const filtered: BrowserReleaseTypes = {};
|
const filtered: BrowserReleaseTypes = {};
|
||||||
if (rawReleaseTypes.stable)
|
if (rawReleaseTypes.stable)
|
||||||
filtered.stable = rawReleaseTypes.stable;
|
filtered.stable = rawReleaseTypes.stable;
|
||||||
@@ -267,8 +280,8 @@ export function CreateProfileDialog({
|
|||||||
if (selectedBrowser) {
|
if (selectedBrowser) {
|
||||||
void loadReleaseTypes(selectedBrowser);
|
void loadReleaseTypes(selectedBrowser);
|
||||||
}
|
}
|
||||||
// Check and download GeoIP database if needed for Camoufox
|
// Check and download GeoIP database if needed for Camoufox or Wayfern
|
||||||
if (selectedBrowser === "camoufox") {
|
if (selectedBrowser === "camoufox" || selectedBrowser === "wayfern") {
|
||||||
void checkAndDownloadGeoIPDatabase();
|
void checkAndDownloadGeoIPDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -333,26 +346,50 @@ export function CreateProfileDialog({
|
|||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
if (activeTab === "anti-detect") {
|
if (activeTab === "anti-detect") {
|
||||||
// Anti-detect browser - always use Camoufox with best available version
|
// Anti-detect browser - check if Wayfern or Camoufox is selected
|
||||||
const bestCamoufoxVersion = getBestAvailableVersion("camoufox");
|
if (selectedBrowser === "wayfern") {
|
||||||
if (!bestCamoufoxVersion) {
|
const bestWayfernVersion = getBestAvailableVersion("wayfern");
|
||||||
console.error("No Camoufox version available");
|
if (!bestWayfernVersion) {
|
||||||
return;
|
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 {
|
} else {
|
||||||
// Regular browser
|
// Regular browser
|
||||||
if (!selectedBrowser) {
|
if (!selectedBrowser) {
|
||||||
@@ -402,6 +439,9 @@ export function CreateProfileDialog({
|
|||||||
geoip: true, // Reset to automatic geoip
|
geoip: true, // Reset to automatic geoip
|
||||||
os: getCurrentOS(), // Reset to current OS
|
os: getCurrentOS(), // Reset to current OS
|
||||||
});
|
});
|
||||||
|
setWayfernConfig({
|
||||||
|
os: getCurrentOS() as WayfernOS, // Reset to current OS
|
||||||
|
});
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,6 +449,10 @@ export function CreateProfileDialog({
|
|||||||
setCamoufoxConfig((prev) => ({ ...prev, [key]: value }));
|
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
|
// Check if browser version is downloaded and available
|
||||||
const isBrowserVersionAvailable = useCallback(
|
const isBrowserVersionAvailable = useCallback(
|
||||||
(browserStr: string) => {
|
(browserStr: string) => {
|
||||||
@@ -461,13 +505,7 @@ export function CreateProfileDialog({
|
|||||||
onValueChange={handleTabChange}
|
onValueChange={handleTabChange}
|
||||||
className="flex flex-col flex-1 w-full min-h-0"
|
className="flex flex-col flex-1 w-full min-h-0"
|
||||||
>
|
>
|
||||||
<TabsList
|
{/* Tab list hidden - only anti-detect browsers are supported */}
|
||||||
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>
|
|
||||||
|
|
||||||
<ScrollArea className="overflow-y-auto flex-1">
|
<ScrollArea className="overflow-y-auto flex-1">
|
||||||
<div className="flex flex-col justify-center items-center w-full">
|
<div className="flex flex-col justify-center items-center w-full">
|
||||||
@@ -482,30 +520,60 @@ export function CreateProfileDialog({
|
|||||||
Anti-Detect Browser
|
Anti-Detect Browser
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Choose Firefox for anti-detection capabilities
|
Choose a browser with anti-detection capabilities
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<div className="space-y-3">
|
||||||
onClick={() => handleBrowserSelect("camoufox")}
|
{/* Wayfern (Chromium) - First */}
|
||||||
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
|
<Button
|
||||||
variant="outline"
|
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"
|
||||||
<div className="flex justify-center items-center w-8 h-8">
|
variant="outline"
|
||||||
{(() => {
|
>
|
||||||
const IconComponent = getBrowserIcon("firefox");
|
<div className="flex justify-center items-center w-8 h-8">
|
||||||
return IconComponent ? (
|
{(() => {
|
||||||
<IconComponent className="w-6 h-6" />
|
const IconComponent = getBrowserIcon("wayfern");
|
||||||
) : null;
|
return IconComponent ? (
|
||||||
})()}
|
<IconComponent className="w-6 h-6" />
|
||||||
</div>
|
) : null;
|
||||||
<div className="text-left">
|
})()}
|
||||||
<div className="font-medium">Firefox</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Anti-Detect Browser
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className="text-left">
|
||||||
</Button>
|
<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>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -570,7 +638,94 @@ export function CreateProfileDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
// Camoufox Configuration
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Camoufox Download Status */}
|
{/* Camoufox Download Status */}
|
||||||
@@ -654,6 +809,7 @@ export function CreateProfileDialog({
|
|||||||
config={camoufoxConfig}
|
config={camoufoxConfig}
|
||||||
onConfigChange={updateCamoufoxConfig}
|
onConfigChange={updateCamoufoxConfig}
|
||||||
isCreating
|
isCreating
|
||||||
|
browserType="camoufox"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1999,7 +1999,8 @@ export function ProfilesDataTable({
|
|||||||
>
|
>
|
||||||
Assign to Group
|
Assign to Group
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
{profile.browser === "camoufox" &&
|
{(profile.browser === "camoufox" ||
|
||||||
|
profile.browser === "wayfern") &&
|
||||||
meta.onConfigureCamoufox && (
|
meta.onConfigureCamoufox && (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface SharedCamoufoxConfigFormProps {
|
|||||||
isCreating?: boolean; // Flag to indicate if this is for creating a new profile
|
isCreating?: boolean; // Flag to indicate if this is for creating a new profile
|
||||||
forceAdvanced?: boolean; // Force advanced mode (for editing)
|
forceAdvanced?: boolean; // Force advanced mode (for editing)
|
||||||
readOnly?: boolean; // Flag to indicate if the form should be read-only
|
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
|
// Determine if fingerprint editing should be disabled
|
||||||
@@ -116,6 +117,7 @@ export function SharedCamoufoxConfigForm({
|
|||||||
isCreating = false,
|
isCreating = false,
|
||||||
forceAdvanced = false,
|
forceAdvanced = false,
|
||||||
readOnly = false,
|
readOnly = false,
|
||||||
|
browserType = "camoufox",
|
||||||
}: SharedCamoufoxConfigFormProps) {
|
}: SharedCamoufoxConfigFormProps) {
|
||||||
const [activeTab, setActiveTab] = useState(
|
const [activeTab, setActiveTab] = useState(
|
||||||
forceAdvanced ? "manual" : "automatic",
|
forceAdvanced ? "manual" : "automatic",
|
||||||
@@ -282,42 +284,44 @@ export function SharedCamoufoxConfigForm({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<fieldset disabled={isEditingDisabled} className="space-y-6">
|
<fieldset disabled={isEditingDisabled} className="space-y-6">
|
||||||
{/* Blocking Options */}
|
{/* Blocking Options - Only available for Camoufox */}
|
||||||
<div className="space-y-3">
|
{browserType === "camoufox" && (
|
||||||
<Label>Blocking Options</Label>
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<Label>Blocking Options</Label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="space-y-2">
|
||||||
<Checkbox
|
<div className="flex items-center space-x-2">
|
||||||
id="block-images"
|
<Checkbox
|
||||||
checked={config.block_images || false}
|
id="block-images"
|
||||||
onCheckedChange={(checked) =>
|
checked={config.block_images || false}
|
||||||
onConfigChange("block_images", checked)
|
onCheckedChange={(checked) =>
|
||||||
}
|
onConfigChange("block_images", checked)
|
||||||
/>
|
}
|
||||||
<Label htmlFor="block-images">Block Images</Label>
|
/>
|
||||||
</div>
|
<Label htmlFor="block-images">Block Images</Label>
|
||||||
<div className="flex items-center space-x-2">
|
</div>
|
||||||
<Checkbox
|
<div className="flex items-center space-x-2">
|
||||||
id="block-webrtc"
|
<Checkbox
|
||||||
checked={config.block_webrtc || false}
|
id="block-webrtc"
|
||||||
onCheckedChange={(checked) =>
|
checked={config.block_webrtc || false}
|
||||||
onConfigChange("block_webrtc", checked)
|
onCheckedChange={(checked) =>
|
||||||
}
|
onConfigChange("block_webrtc", checked)
|
||||||
/>
|
}
|
||||||
<Label htmlFor="block-webrtc">Block WebRTC</Label>
|
/>
|
||||||
</div>
|
<Label htmlFor="block-webrtc">Block WebRTC</Label>
|
||||||
<div className="flex items-center space-x-2">
|
</div>
|
||||||
<Checkbox
|
<div className="flex items-center space-x-2">
|
||||||
id="block-webgl"
|
<Checkbox
|
||||||
checked={config.block_webgl || false}
|
id="block-webgl"
|
||||||
onCheckedChange={(checked) =>
|
checked={config.block_webgl || false}
|
||||||
onConfigChange("block_webgl", checked)
|
onCheckedChange={(checked) =>
|
||||||
}
|
onConfigChange("block_webgl", checked)
|
||||||
/>
|
}
|
||||||
<Label htmlFor="block-webgl">Block WebGL</Label>
|
/>
|
||||||
|
<Label htmlFor="block-webgl">Block WebGL</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Navigator Properties */}
|
{/* Navigator Properties */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
|||||||
+10
-15
@@ -3,9 +3,7 @@
|
|||||||
* Centralized helpers for browser name mapping, icons, etc.
|
* Centralized helpers for browser name mapping, icons, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { FaChrome, FaFirefox, FaShieldAlt } from "react-icons/fa";
|
import { FaChrome, FaExclamationTriangle, FaFirefox } from "react-icons/fa";
|
||||||
import { SiBrave } from "react-icons/si";
|
|
||||||
import { ZenBrowser } from "@/components/icons/zen-browser";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map internal browser names to display names
|
* Map internal browser names to display names
|
||||||
@@ -17,7 +15,8 @@ export function getBrowserDisplayName(browserType: string): string {
|
|||||||
zen: "Zen Browser",
|
zen: "Zen Browser",
|
||||||
brave: "Brave",
|
brave: "Brave",
|
||||||
chromium: "Chromium",
|
chromium: "Chromium",
|
||||||
camoufox: "Anti-Detect",
|
camoufox: "Firefox (Camoufox)",
|
||||||
|
wayfern: "Chromium (Wayfern)",
|
||||||
};
|
};
|
||||||
|
|
||||||
return browserNames[browserType] || browserType;
|
return browserNames[browserType] || browserType;
|
||||||
@@ -25,22 +24,18 @@ export function getBrowserDisplayName(browserType: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the appropriate icon component for a browser type
|
* 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) {
|
export function getBrowserIcon(browserType: string) {
|
||||||
switch (browserType) {
|
switch (browserType) {
|
||||||
case "chromium":
|
|
||||||
return FaChrome;
|
|
||||||
case "brave":
|
|
||||||
return SiBrave;
|
|
||||||
case "firefox":
|
|
||||||
case "firefox-developer":
|
|
||||||
return FaFirefox;
|
|
||||||
case "zen":
|
|
||||||
return ZenBrowser;
|
|
||||||
case "camoufox":
|
case "camoufox":
|
||||||
return FaShieldAlt;
|
return FaFirefox; // Firefox-based anti-detect browser
|
||||||
|
case "wayfern":
|
||||||
|
return FaChrome; // Chromium-based anti-detect browser
|
||||||
default:
|
default:
|
||||||
return null;
|
// All other browsers get a warning icon
|
||||||
|
return FaExclamationTriangle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export interface BrowserProfile {
|
|||||||
last_launch?: number;
|
last_launch?: number;
|
||||||
release_type: string; // "stable" or "nightly"
|
release_type: string; // "stable" or "nightly"
|
||||||
camoufox_config?: CamoufoxConfig; // Camoufox configuration
|
camoufox_config?: CamoufoxConfig; // Camoufox configuration
|
||||||
|
wayfern_config?: WayfernConfig; // Wayfern configuration
|
||||||
group_id?: string; // Reference to profile group
|
group_id?: string; // Reference to profile group
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
note?: string; // User note
|
note?: string; // User note
|
||||||
@@ -289,6 +290,32 @@ export interface CamoufoxLaunchResult {
|
|||||||
url?: string;
|
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
|
// Traffic stats types
|
||||||
export interface BandwidthDataPoint {
|
export interface BandwidthDataPoint {
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user