mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-25 21:36:23 +02:00
592 lines
19 KiB
Rust
592 lines
19 KiB
Rust
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<Self, String> {
|
|
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<PathBuf, Box<dyn std::error::Error>>;
|
|
fn create_launch_args(
|
|
&self,
|
|
profile_path: &str,
|
|
_proxy_settings: Option<&ProxySettings>,
|
|
url: Option<String>,
|
|
) -> Result<Vec<String>, Box<dyn std::error::Error>>;
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
// 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<String>,
|
|
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
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<PathBuf, Box<dyn std::error::Error>> {
|
|
// 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<String>,
|
|
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
|
|
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<dyn Browser> {
|
|
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<GithubAsset>,
|
|
#[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);
|
|
}
|
|
}
|