Files
donutbrowser/src-tauri/src/cookie_manager.rs
T
2026-02-21 15:50:23 +04:00

607 lines
19 KiB
Rust

use crate::profile::manager::ProfileManager;
use crate::profile::BrowserProfile;
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tauri::AppHandle;
/// Unified cookie representation that works across both browser types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnifiedCookie {
pub name: String,
pub value: String,
pub domain: String,
pub path: String,
pub expires: i64,
pub is_secure: bool,
pub is_http_only: bool,
pub same_site: i32,
pub creation_time: i64,
pub last_accessed: i64,
}
/// Cookies grouped by domain for UI display
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainCookies {
pub domain: String,
pub cookies: Vec<UnifiedCookie>,
pub cookie_count: usize,
}
/// Result of reading cookies from a profile
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CookieReadResult {
pub profile_id: String,
pub browser_type: String,
pub domains: Vec<DomainCookies>,
pub total_count: usize,
}
/// Request to copy specific cookies
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CookieCopyRequest {
pub source_profile_id: String,
pub target_profile_ids: Vec<String>,
pub selected_cookies: Vec<SelectedCookie>,
}
/// Identifies a specific cookie to copy
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SelectedCookie {
pub domain: String,
pub name: String,
}
/// Result of a copy operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CookieCopyResult {
pub target_profile_id: String,
pub cookies_copied: usize,
pub cookies_replaced: usize,
pub errors: Vec<String>,
}
/// Result of a cookie import operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CookieImportResult {
pub cookies_imported: usize,
pub cookies_replaced: usize,
pub errors: Vec<String>,
}
pub struct CookieManager;
impl CookieManager {
/// Windows epoch offset: seconds between 1601-01-01 and 1970-01-01
const WINDOWS_EPOCH_DIFF: i64 = 11644473600;
/// Get the cookie database path for a profile
fn get_cookie_db_path(profile: &BrowserProfile, profiles_dir: &Path) -> Result<PathBuf, String> {
let profile_data_path = profile.get_profile_data_path(profiles_dir);
match profile.browser.as_str() {
"wayfern" => {
let path = profile_data_path.join("Default").join("Cookies");
if path.exists() {
Ok(path)
} else {
Err(format!("Cookie database not found at: {}", path.display()))
}
}
"camoufox" => {
let path = profile_data_path.join("cookies.sqlite");
if path.exists() {
Ok(path)
} else {
Err(format!("Cookie database not found at: {}", path.display()))
}
}
_ => Err(format!(
"Unsupported browser type for cookie operations: {}",
profile.browser
)),
}
}
/// Convert Chrome timestamp (Windows epoch, microseconds) to Unix timestamp (seconds)
fn chrome_time_to_unix(chrome_time: i64) -> i64 {
if chrome_time == 0 {
return 0;
}
(chrome_time / 1_000_000) - Self::WINDOWS_EPOCH_DIFF
}
/// Convert Unix timestamp (seconds) to Chrome timestamp (Windows epoch, microseconds)
fn unix_to_chrome_time(unix_time: i64) -> i64 {
if unix_time == 0 {
return 0;
}
(unix_time + Self::WINDOWS_EPOCH_DIFF) * 1_000_000
}
/// Read cookies from a Firefox/Camoufox profile
fn read_firefox_cookies(db_path: &Path) -> Result<Vec<UnifiedCookie>, String> {
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
let mut stmt = conn
.prepare(
"SELECT name, value, host, path, expiry, isSecure, isHttpOnly,
sameSite, creationTime, lastAccessed
FROM moz_cookies",
)
.map_err(|e| format!("Failed to prepare statement: {e}"))?;
let cookies = stmt
.query_map([], |row| {
Ok(UnifiedCookie {
name: row.get(0)?,
value: row.get(1)?,
domain: row.get(2)?,
path: row.get(3)?,
expires: row.get(4)?,
is_secure: row.get::<_, i32>(5)? != 0,
is_http_only: row.get::<_, i32>(6)? != 0,
same_site: row.get(7)?,
creation_time: row.get::<_, i64>(8)? / 1_000_000,
last_accessed: row.get::<_, i64>(9)? / 1_000_000,
})
})
.map_err(|e| format!("Failed to query cookies: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Failed to collect cookies: {e}"))?;
Ok(cookies)
}
/// Read cookies from a Chrome/Wayfern profile
fn read_chrome_cookies(db_path: &Path) -> Result<Vec<UnifiedCookie>, String> {
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
let mut stmt = conn
.prepare(
"SELECT name, value, host_key, path, expires_utc, is_secure,
is_httponly, samesite, creation_utc, last_access_utc
FROM cookies",
)
.map_err(|e| format!("Failed to prepare statement: {e}"))?;
let cookies = stmt
.query_map([], |row| {
Ok(UnifiedCookie {
name: row.get(0)?,
value: row.get(1)?,
domain: row.get(2)?,
path: row.get(3)?,
expires: Self::chrome_time_to_unix(row.get(4)?),
is_secure: row.get::<_, i32>(5)? != 0,
is_http_only: row.get::<_, i32>(6)? != 0,
same_site: row.get(7)?,
creation_time: Self::chrome_time_to_unix(row.get(8)?),
last_accessed: Self::chrome_time_to_unix(row.get(9)?),
})
})
.map_err(|e| format!("Failed to query cookies: {e}"))?
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("Failed to collect cookies: {e}"))?;
Ok(cookies)
}
/// Write cookies to a Firefox/Camoufox profile
fn write_firefox_cookies(
db_path: &Path,
cookies: &[UnifiedCookie],
) -> Result<(usize, usize), String> {
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
let mut copied = 0;
let mut replaced = 0;
for cookie in cookies {
let existing: Option<i64> = conn
.query_row(
"SELECT id FROM moz_cookies WHERE host = ?1 AND name = ?2 AND path = ?3",
params![&cookie.domain, &cookie.name, &cookie.path],
|row| row.get(0),
)
.ok();
if existing.is_some() {
conn
.execute(
"UPDATE moz_cookies SET value = ?1, expiry = ?2, isSecure = ?3,
isHttpOnly = ?4, sameSite = ?5, lastAccessed = ?6
WHERE host = ?7 AND name = ?8 AND path = ?9",
params![
&cookie.value,
cookie.expires,
cookie.is_secure as i32,
cookie.is_http_only as i32,
cookie.same_site,
cookie.last_accessed * 1_000_000,
&cookie.domain,
&cookie.name,
&cookie.path,
],
)
.map_err(|e| format!("Failed to update cookie: {e}"))?;
replaced += 1;
} else {
conn
.execute(
"INSERT INTO moz_cookies
(originAttributes, name, value, host, path, expiry, lastAccessed,
creationTime, isSecure, isHttpOnly, sameSite, rawSameSite, schemeMap)
VALUES ('', ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, 2)",
params![
&cookie.name,
&cookie.value,
&cookie.domain,
&cookie.path,
cookie.expires,
cookie.last_accessed * 1_000_000,
cookie.creation_time * 1_000_000,
cookie.is_secure as i32,
cookie.is_http_only as i32,
cookie.same_site,
],
)
.map_err(|e| format!("Failed to insert cookie: {e}"))?;
copied += 1;
}
}
Ok((copied, replaced))
}
/// Write cookies to a Chrome/Wayfern profile
fn write_chrome_cookies(
db_path: &Path,
cookies: &[UnifiedCookie],
) -> Result<(usize, usize), String> {
let conn = Connection::open(db_path).map_err(|e| format!("Failed to open database: {e}"))?;
let mut copied = 0;
let mut replaced = 0;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
for cookie in cookies {
let existing: Option<i64> = conn
.query_row(
"SELECT rowid FROM cookies WHERE host_key = ?1 AND name = ?2 AND path = ?3",
params![&cookie.domain, &cookie.name, &cookie.path],
|row| row.get(0),
)
.ok();
if existing.is_some() {
conn
.execute(
"UPDATE cookies SET value = ?1, expires_utc = ?2, is_secure = ?3,
is_httponly = ?4, samesite = ?5, last_access_utc = ?6, last_update_utc = ?7
WHERE host_key = ?8 AND name = ?9 AND path = ?10",
params![
&cookie.value,
Self::unix_to_chrome_time(cookie.expires),
cookie.is_secure as i32,
cookie.is_http_only as i32,
cookie.same_site,
Self::unix_to_chrome_time(cookie.last_accessed),
Self::unix_to_chrome_time(now),
&cookie.domain,
&cookie.name,
&cookie.path,
],
)
.map_err(|e| format!("Failed to update cookie: {e}"))?;
replaced += 1;
} else {
conn.execute(
"INSERT INTO cookies
(creation_utc, host_key, top_frame_site_key, name, value, encrypted_value,
path, expires_utc, is_secure, is_httponly, last_access_utc, has_expires,
is_persistent, priority, samesite, source_scheme, source_port, source_type,
has_cross_site_ancestor, last_update_utc)
VALUES (?1, ?2, '', ?3, ?4, X'', ?5, ?6, ?7, ?8, ?9, 1, 1, 1, ?10, 2, -1, 0, 0, ?11)",
params![
Self::unix_to_chrome_time(cookie.creation_time),
&cookie.domain,
&cookie.name,
&cookie.value,
&cookie.path,
Self::unix_to_chrome_time(cookie.expires),
cookie.is_secure as i32,
cookie.is_http_only as i32,
Self::unix_to_chrome_time(cookie.last_accessed),
cookie.same_site,
Self::unix_to_chrome_time(now),
],
)
.map_err(|e| format!("Failed to insert cookie: {e}"))?;
copied += 1;
}
}
Ok((copied, replaced))
}
/// Public API: Read cookies from a profile
pub fn read_cookies(profile_id: &str) -> Result<CookieReadResult, String> {
let profile_manager = ProfileManager::instance();
let profiles_dir = profile_manager.get_profiles_dir();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let profile = profiles
.iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| format!("Profile not found: {profile_id}"))?;
let db_path = Self::get_cookie_db_path(profile, &profiles_dir)?;
let cookies = match profile.browser.as_str() {
"camoufox" => Self::read_firefox_cookies(&db_path)?,
"wayfern" => Self::read_chrome_cookies(&db_path)?,
_ => return Err(format!("Unsupported browser type: {}", profile.browser)),
};
let mut domain_map: HashMap<String, Vec<UnifiedCookie>> = HashMap::new();
for cookie in cookies {
domain_map
.entry(cookie.domain.clone())
.or_default()
.push(cookie);
}
let mut domains: Vec<DomainCookies> = domain_map
.into_iter()
.map(|(domain, cookies)| DomainCookies {
domain,
cookie_count: cookies.len(),
cookies,
})
.collect();
domains.sort_by(|a, b| a.domain.cmp(&b.domain));
let total_count = domains.iter().map(|d| d.cookie_count).sum();
Ok(CookieReadResult {
profile_id: profile_id.to_string(),
browser_type: profile.browser.clone(),
domains,
total_count,
})
}
/// Public API: Copy cookies between profiles
pub async fn copy_cookies(
app_handle: &AppHandle,
request: CookieCopyRequest,
) -> Result<Vec<CookieCopyResult>, String> {
let profile_manager = ProfileManager::instance();
let profiles_dir = profile_manager.get_profiles_dir();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let source = profiles
.iter()
.find(|p| p.id.to_string() == request.source_profile_id)
.ok_or_else(|| format!("Source profile not found: {}", request.source_profile_id))?;
let source_db_path = Self::get_cookie_db_path(source, &profiles_dir)?;
let all_cookies = match source.browser.as_str() {
"camoufox" => Self::read_firefox_cookies(&source_db_path)?,
"wayfern" => Self::read_chrome_cookies(&source_db_path)?,
_ => return Err(format!("Unsupported browser type: {}", source.browser)),
};
let cookies_to_copy: Vec<UnifiedCookie> = if request.selected_cookies.is_empty() {
all_cookies
} else {
all_cookies
.into_iter()
.filter(|c| {
request.selected_cookies.iter().any(|s| {
if s.name.is_empty() {
c.domain == s.domain
} else {
c.domain == s.domain && c.name == s.name
}
})
})
.collect()
};
let mut results = Vec::new();
for target_id in &request.target_profile_ids {
let target = match profiles.iter().find(|p| p.id.to_string() == *target_id) {
Some(p) => p,
None => {
results.push(CookieCopyResult {
target_profile_id: target_id.clone(),
cookies_copied: 0,
cookies_replaced: 0,
errors: vec![format!("Profile not found: {target_id}")],
});
continue;
}
};
let is_running = profile_manager
.check_browser_status(app_handle.clone(), target)
.await
.unwrap_or(false);
if is_running {
results.push(CookieCopyResult {
target_profile_id: target_id.clone(),
cookies_copied: 0,
cookies_replaced: 0,
errors: vec![format!("Browser is running for profile: {}", target.name)],
});
continue;
}
let target_db_path = match Self::get_cookie_db_path(target, &profiles_dir) {
Ok(p) => p,
Err(e) => {
results.push(CookieCopyResult {
target_profile_id: target_id.clone(),
cookies_copied: 0,
cookies_replaced: 0,
errors: vec![e],
});
continue;
}
};
let write_result = match target.browser.as_str() {
"camoufox" => Self::write_firefox_cookies(&target_db_path, &cookies_to_copy),
"wayfern" => Self::write_chrome_cookies(&target_db_path, &cookies_to_copy),
_ => {
results.push(CookieCopyResult {
target_profile_id: target_id.clone(),
cookies_copied: 0,
cookies_replaced: 0,
errors: vec![format!("Unsupported browser: {}", target.browser)],
});
continue;
}
};
match write_result {
Ok((copied, replaced)) => {
results.push(CookieCopyResult {
target_profile_id: target_id.clone(),
cookies_copied: copied,
cookies_replaced: replaced,
errors: vec![],
});
}
Err(e) => {
results.push(CookieCopyResult {
target_profile_id: target_id.clone(),
cookies_copied: 0,
cookies_replaced: 0,
errors: vec![e],
});
}
}
}
Ok(results)
}
/// Parse Netscape format cookies from text content
fn parse_netscape_cookies(content: &str) -> (Vec<UnifiedCookie>, Vec<String>) {
let mut cookies = Vec::new();
let mut errors = Vec::new();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
for (i, line) in content.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() < 7 {
errors.push(format!(
"Line {}: expected 7 tab-separated fields, got {}",
i + 1,
fields.len()
));
continue;
}
let domain = fields[0].to_string();
let path = fields[2].to_string();
let is_secure = fields[3].eq_ignore_ascii_case("TRUE");
let expires = fields[4].parse::<i64>().unwrap_or(0);
let name = fields[5].to_string();
let value = fields[6].to_string();
cookies.push(UnifiedCookie {
name,
value,
domain,
path,
expires,
is_secure,
is_http_only: false,
same_site: 0,
creation_time: now,
last_accessed: now,
});
}
(cookies, errors)
}
/// Public API: Import cookies from Netscape format content
pub async fn import_netscape_cookies(
app_handle: &AppHandle,
profile_id: &str,
content: &str,
) -> Result<CookieImportResult, String> {
let profile_manager = ProfileManager::instance();
let profiles_dir = profile_manager.get_profiles_dir();
let profiles = profile_manager
.list_profiles()
.map_err(|e| format!("Failed to list profiles: {e}"))?;
let profile = profiles
.iter()
.find(|p| p.id.to_string() == profile_id)
.ok_or_else(|| format!("Profile not found: {profile_id}"))?;
let is_running = profile_manager
.check_browser_status(app_handle.clone(), profile)
.await
.unwrap_or(false);
if is_running {
return Err(format!(
"Cannot import cookies while browser is running for profile: {}",
profile.name
));
}
let (cookies, parse_errors) = Self::parse_netscape_cookies(content);
if cookies.is_empty() {
return Err("No valid cookies found in the file".to_string());
}
let db_path = Self::get_cookie_db_path(profile, &profiles_dir)?;
let write_result = match profile.browser.as_str() {
"camoufox" => Self::write_firefox_cookies(&db_path, &cookies),
"wayfern" => Self::write_chrome_cookies(&db_path, &cookies),
_ => return Err(format!("Unsupported browser type: {}", profile.browser)),
};
match write_result {
Ok((imported, replaced)) => Ok(CookieImportResult {
cookies_imported: imported,
cookies_replaced: replaced,
errors: parse_errors,
}),
Err(e) => Err(format!("Failed to write cookies: {e}")),
}
}
}