use base64::{engine::general_purpose, Engine as _}; use serde::{Deserialize, Serialize}; use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ProxySettings { pub enabled: bool, pub proxy_type: String, // "http", "https", "socks4", or "socks5" pub host: String, pub port: u16, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum BrowserType { MullvadBrowser, Chromium, Firefox, FirefoxDeveloper, Brave, Zen, TorBrowser, } impl BrowserType { pub fn as_str(&self) -> &'static str { match self { BrowserType::MullvadBrowser => "mullvad-browser", BrowserType::Chromium => "chromium", BrowserType::Firefox => "firefox", BrowserType::FirefoxDeveloper => "firefox-developer", BrowserType::Brave => "brave", BrowserType::Zen => "zen", BrowserType::TorBrowser => "tor-browser", } } pub fn from_str(s: &str) -> Result { match s { "mullvad-browser" => Ok(BrowserType::MullvadBrowser), "chromium" => Ok(BrowserType::Chromium), "firefox" => Ok(BrowserType::Firefox), "firefox-developer" => Ok(BrowserType::FirefoxDeveloper), "brave" => Ok(BrowserType::Brave), "zen" => Ok(BrowserType::Zen), "tor-browser" => Ok(BrowserType::TorBrowser), _ => Err(format!("Unknown browser type: {s}")), } } } pub trait Browser: Send + Sync { fn browser_type(&self) -> BrowserType; fn get_executable_path(&self, install_dir: &Path) -> Result>; fn create_launch_args( &self, profile_path: &str, _proxy_settings: Option<&ProxySettings>, url: Option, ) -> Result, Box>; fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool; } pub struct FirefoxBrowser { browser_type: BrowserType, } impl FirefoxBrowser { pub fn new(browser_type: BrowserType) -> Self { Self { browser_type } } } impl Browser for FirefoxBrowser { fn browser_type(&self) -> BrowserType { self.browser_type.clone() } fn get_executable_path(&self, install_dir: &Path) -> Result> { // 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("Browser 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 first executable in the MacOS directory 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.starts_with("firefox") || name.starts_with("mullvad") || name.starts_with("zen") || name.starts_with("tor") || name.contains("Browser") }) .map(|entry| entry.path()) .ok_or("No executable found in MacOS directory")?; Ok(executable_path) } fn create_launch_args( &self, profile_path: &str, _proxy_settings: Option<&ProxySettings>, url: Option, ) -> Result, Box> { let mut args = vec!["-profile".to_string(), profile_path.to_string()]; // Only use -no-remote for browsers that require it for security (Mullvad, Tor) // Regular Firefox browsers can use remote commands for better URL handling match self.browser_type { BrowserType::MullvadBrowser | BrowserType::TorBrowser => { args.push("-no-remote".to_string()); } BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen => { // Don't use -no-remote so we can communicate with existing instances } _ => {} } // Firefox-based browsers use profile directory and user.js for proxy configuration if let Some(url) = url { args.push(url); } Ok(args) } fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { let browser_dir = binaries_dir .join(self.browser_type().as_str()) .join(version); println!("Firefox browser checking version {version} in directory: {browser_dir:?}"); // Only check if directory exists and contains a .app file if browser_dir.exists() { println!("Directory exists, checking for .app files..."); if let Ok(entries) = std::fs::read_dir(&browser_dir) { for entry in entries.flatten() { println!(" Found entry: {:?}", entry.path()); if entry.path().extension().is_some_and(|ext| ext == "app") { println!(" Found .app file: {:?}", entry.path()); return true; } } } println!("No .app files found in directory"); } else { println!("Directory does not exist: {browser_dir:?}"); } false } } // Chromium-based browsers (Chromium, Brave) pub struct ChromiumBrowser { browser_type: BrowserType, } impl ChromiumBrowser { pub fn new(browser_type: BrowserType) -> Self { Self { browser_type } } } impl Browser for ChromiumBrowser { fn browser_type(&self) -> BrowserType { self.browser_type.clone() } fn get_executable_path(&self, install_dir: &Path) -> Result> { // 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("Browser 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 first executable in the MacOS directory 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.contains("Brave") || name.contains("Google Chrome") }) .map(|entry| entry.path()) .ok_or("No executable found in MacOS directory")?; Ok(executable_path) } fn create_launch_args( &self, profile_path: &str, proxy_settings: Option<&ProxySettings>, url: Option, ) -> Result, Box> { 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(), ]; // Add proxy configuration if provided if let Some(proxy) = proxy_settings { if proxy.enabled { let pac_path = Path::new(profile_path).join("proxy.pac"); if pac_path.exists() { let pac_content = fs::read(&pac_path)?; let pac_base64 = general_purpose::STANDARD.encode(&pac_content); args.push(format!( "--proxy-pac-url=data:application/x-javascript-config;base64,{pac_base64}" )); } } } if let Some(url) = url { args.push(url); } Ok(args) } fn is_version_downloaded(&self, version: &str, binaries_dir: &Path) -> bool { let browser_dir = binaries_dir .join(self.browser_type().as_str()) .join(version); println!("Chromium browser checking version {version} in directory: {browser_dir:?}"); // Check if directory exists and contains at least one .app file if browser_dir.exists() { println!("Directory exists, checking for .app files..."); if let Ok(entries) = std::fs::read_dir(&browser_dir) { for entry in entries.flatten() { println!(" Found entry: {:?}", entry.path()); if entry.path().extension().is_some_and(|ext| ext == "app") { println!(" Found .app file: {:?}", entry.path()); // Try to get the executable path as a final verification if self.get_executable_path(&browser_dir).is_ok() { println!(" Executable path verification successful"); return true; } else { println!(" Executable path verification failed"); } } } } println!("No valid .app files found in directory"); } else { println!("Directory does not exist: {browser_dir:?}"); } false } } // Factory function to create browser instances pub fn create_browser(browser_type: BrowserType) -> Box { match browser_type { BrowserType::MullvadBrowser | BrowserType::Firefox | BrowserType::FirefoxDeveloper | BrowserType::Zen | BrowserType::TorBrowser => Box::new(FirefoxBrowser::new(browser_type)), BrowserType::Chromium | BrowserType::Brave => Box::new(ChromiumBrowser::new(browser_type)), } } // Add GithubRelease and GithubAsset structs to browser.rs if they don't already exist #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GithubRelease { pub tag_name: String, #[serde(default)] pub name: String, pub assets: Vec, #[serde(default)] pub published_at: String, #[serde(default)] pub is_alpha: bool, #[serde(default)] pub prerelease: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct GithubAsset { pub name: String, pub browser_download_url: String, #[serde(default)] pub size: u64, } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; #[test] fn test_browser_type_conversions() { // Test as_str assert_eq!(BrowserType::MullvadBrowser.as_str(), "mullvad-browser"); assert_eq!(BrowserType::Firefox.as_str(), "firefox"); assert_eq!(BrowserType::FirefoxDeveloper.as_str(), "firefox-developer"); assert_eq!(BrowserType::Chromium.as_str(), "chromium"); assert_eq!(BrowserType::Brave.as_str(), "brave"); assert_eq!(BrowserType::Zen.as_str(), "zen"); assert_eq!(BrowserType::TorBrowser.as_str(), "tor-browser"); // Test from_str assert_eq!( BrowserType::from_str("mullvad-browser").unwrap(), BrowserType::MullvadBrowser ); assert_eq!( BrowserType::from_str("firefox").unwrap(), BrowserType::Firefox ); assert_eq!( BrowserType::from_str("firefox-developer").unwrap(), BrowserType::FirefoxDeveloper ); assert_eq!( BrowserType::from_str("chromium").unwrap(), BrowserType::Chromium ); assert_eq!(BrowserType::from_str("brave").unwrap(), BrowserType::Brave); assert_eq!(BrowserType::from_str("zen").unwrap(), BrowserType::Zen); assert_eq!( BrowserType::from_str("tor-browser").unwrap(), BrowserType::TorBrowser ); // Test invalid browser type assert!(BrowserType::from_str("invalid").is_err()); assert!(BrowserType::from_str("").is_err()); assert!(BrowserType::from_str("Firefox").is_err()); // Case sensitive } #[test] fn test_firefox_browser_creation() { let browser = FirefoxBrowser::new(BrowserType::Firefox); assert_eq!(browser.browser_type(), BrowserType::Firefox); let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser); assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser); let browser = FirefoxBrowser::new(BrowserType::TorBrowser); assert_eq!(browser.browser_type(), BrowserType::TorBrowser); let browser = FirefoxBrowser::new(BrowserType::Zen); assert_eq!(browser.browser_type(), BrowserType::Zen); } #[test] fn test_chromium_browser_creation() { let browser = ChromiumBrowser::new(BrowserType::Chromium); assert_eq!(browser.browser_type(), BrowserType::Chromium); let browser = ChromiumBrowser::new(BrowserType::Brave); assert_eq!(browser.browser_type(), BrowserType::Brave); } #[test] fn test_browser_factory() { // Test Firefox-based browsers let browser = create_browser(BrowserType::Firefox); assert_eq!(browser.browser_type(), BrowserType::Firefox); let browser = create_browser(BrowserType::MullvadBrowser); assert_eq!(browser.browser_type(), BrowserType::MullvadBrowser); let browser = create_browser(BrowserType::Zen); assert_eq!(browser.browser_type(), BrowserType::Zen); let browser = create_browser(BrowserType::TorBrowser); assert_eq!(browser.browser_type(), BrowserType::TorBrowser); let browser = create_browser(BrowserType::FirefoxDeveloper); assert_eq!(browser.browser_type(), BrowserType::FirefoxDeveloper); // Test Chromium-based browsers let browser = create_browser(BrowserType::Chromium); assert_eq!(browser.browser_type(), BrowserType::Chromium); let browser = create_browser(BrowserType::Brave); assert_eq!(browser.browser_type(), BrowserType::Brave); } #[test] fn test_firefox_launch_args() { // Test regular Firefox (should not use -no-remote) let browser = FirefoxBrowser::new(BrowserType::Firefox); let args = browser .create_launch_args("/path/to/profile", None, None) .unwrap(); assert_eq!(args, vec!["-profile", "/path/to/profile"]); assert!(!args.contains(&"-no-remote".to_string())); let args = browser .create_launch_args( "/path/to/profile", None, Some("https://example.com".to_string()), ) .unwrap(); assert_eq!( args, vec!["-profile", "/path/to/profile", "https://example.com"] ); // Test Mullvad Browser (should use -no-remote) let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser); let args = browser .create_launch_args("/path/to/profile", None, None) .unwrap(); assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); // Test Tor Browser (should use -no-remote) let browser = FirefoxBrowser::new(BrowserType::TorBrowser); let args = browser .create_launch_args("/path/to/profile", None, None) .unwrap(); assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); // Test Zen Browser (should not use -no-remote) let browser = FirefoxBrowser::new(BrowserType::Zen); let args = browser .create_launch_args("/path/to/profile", None, None) .unwrap(); assert_eq!(args, vec!["-profile", "/path/to/profile"]); assert!(!args.contains(&"-no-remote".to_string())); } #[test] fn test_chromium_launch_args() { let browser = ChromiumBrowser::new(BrowserType::Chromium); let args = browser .create_launch_args("/path/to/profile", None, None) .unwrap(); // Test that basic required arguments are present assert!(args.contains(&"--user-data-dir=/path/to/profile".to_string())); assert!(args.contains(&"--no-default-browser-check".to_string())); // Test that automatic update disabling arguments are present assert!(args.contains(&"--disable-background-mode".to_string())); assert!(args.contains(&"--disable-component-update".to_string())); let args_with_url = browser .create_launch_args( "/path/to/profile", None, Some("https://example.com".to_string()), ) .unwrap(); assert!(args_with_url.contains(&"https://example.com".to_string())); // Verify URL is at the end assert_eq!(args_with_url.last().unwrap(), "https://example.com"); } #[test] fn test_proxy_settings_creation() { let proxy = ProxySettings { enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, }; assert!(proxy.enabled); assert_eq!(proxy.proxy_type, "http"); assert_eq!(proxy.host, "127.0.0.1"); assert_eq!(proxy.port, 8080); // Test different proxy types let socks_proxy = ProxySettings { enabled: true, proxy_type: "socks5".to_string(), host: "proxy.example.com".to_string(), port: 1080, }; assert_eq!(socks_proxy.proxy_type, "socks5"); assert_eq!(socks_proxy.host, "proxy.example.com"); assert_eq!(socks_proxy.port, 1080); } #[test] fn test_version_downloaded_check() { let temp_dir = TempDir::new().unwrap(); let binaries_dir = temp_dir.path(); // Create a mock Firefox browser installation let browser_dir = binaries_dir.join("firefox").join("139.0"); fs::create_dir_all(&browser_dir).unwrap(); // Create a mock .app directory let app_dir = browser_dir.join("Firefox.app"); fs::create_dir_all(&app_dir).unwrap(); let browser = FirefoxBrowser::new(BrowserType::Firefox); assert!(browser.is_version_downloaded("139.0", binaries_dir)); assert!(!browser.is_version_downloaded("140.0", binaries_dir)); // Test with Chromium browser let chromium_dir = binaries_dir.join("chromium").join("1465660"); fs::create_dir_all(&chromium_dir).unwrap(); let chromium_app_dir = chromium_dir.join("Chromium.app"); fs::create_dir_all(chromium_app_dir.join("Contents").join("MacOS")).unwrap(); // Create a mock executable let executable_path = chromium_app_dir .join("Contents") .join("MacOS") .join("Chromium"); fs::write(&executable_path, "mock executable").unwrap(); let chromium_browser = ChromiumBrowser::new(BrowserType::Chromium); assert!(chromium_browser.is_version_downloaded("1465660", binaries_dir)); assert!(!chromium_browser.is_version_downloaded("1465661", binaries_dir)); } #[test] fn test_version_downloaded_no_app_directory() { let temp_dir = TempDir::new().unwrap(); let binaries_dir = temp_dir.path(); // Create browser directory but no .app directory let browser_dir = binaries_dir.join("firefox").join("139.0"); fs::create_dir_all(&browser_dir).unwrap(); // Create some other files but no .app fs::write(browser_dir.join("readme.txt"), "Some content").unwrap(); let browser = FirefoxBrowser::new(BrowserType::Firefox); assert!(!browser.is_version_downloaded("139.0", binaries_dir)); } #[test] fn test_browser_type_clone_and_debug() { let browser_type = BrowserType::Firefox; let cloned = browser_type.clone(); assert_eq!(browser_type, cloned); // Test Debug trait let debug_str = format!("{browser_type:?}"); assert!(debug_str.contains("Firefox")); } #[test] fn test_proxy_settings_serialization() { let proxy = ProxySettings { enabled: true, proxy_type: "http".to_string(), host: "127.0.0.1".to_string(), port: 8080, }; // Test that it can be serialized (implements Serialize) let json = serde_json::to_string(&proxy).unwrap(); assert!(json.contains("127.0.0.1")); assert!(json.contains("8080")); assert!(json.contains("http")); // Test that it can be deserialized (implements Deserialize) let deserialized: ProxySettings = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.enabled, proxy.enabled); assert_eq!(deserialized.proxy_type, proxy.proxy_type); assert_eq!(deserialized.host, proxy.host); assert_eq!(deserialized.port, proxy.port); } }