From 9236ad38c8f0a5c6ab7461d3185695ef18b0cdc4 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 25 May 2026 02:19:20 +0400 Subject: [PATCH] refactor: cleanup --- src-tauri/src/api_server.rs | 90 +++++++++++++++++++++++++ src-tauri/src/camoufox/config.rs | 11 ++-- src-tauri/src/camoufox_manager.rs | 82 ++++++++++++++--------- src-tauri/src/mcp_server.rs | 91 ++++++++++++++++++++++++++ src/components/profile-data-table.tsx | 18 +++-- src/components/profile-info-dialog.tsx | 42 ++++++++---- 6 files changed, 276 insertions(+), 58 deletions(-) diff --git a/src-tauri/src/api_server.rs b/src-tauri/src/api_server.rs index fc941ec..8e908d4 100644 --- a/src-tauri/src/api_server.rs +++ b/src-tauri/src/api_server.rs @@ -217,6 +217,20 @@ struct OpenUrlRequest { url: String, } +#[derive(Debug, Deserialize, ToSchema)] +struct ImportCookiesRequest { + /// Raw cookie file content. Format is auto-detected: a JSON array + /// (Puppeteer / EditThisCookie style) or a Netscape `cookies.txt`. + content: String, +} + +#[derive(Debug, Serialize, ToSchema)] +struct ImportCookiesResponse { + cookies_imported: usize, + cookies_replaced: usize, + errors: Vec, +} + #[derive(OpenApi)] #[openapi( paths( @@ -228,6 +242,7 @@ struct OpenUrlRequest { run_profile, open_url_in_profile, kill_profile, + import_profile_cookies, get_groups, get_group, create_group, @@ -270,6 +285,8 @@ struct OpenUrlRequest { RunProfileResponse, RunProfileRequest, OpenUrlRequest, + ImportCookiesRequest, + ImportCookiesResponse, ProxySettings, )), tags( @@ -279,6 +296,7 @@ struct OpenUrlRequest { (name = "proxies", description = "Proxy management endpoints"), (name = "vpns", description = "VPN management endpoints"), (name = "browsers", description = "Browser management endpoints"), + (name = "cookies", description = "Cookie management endpoints"), ), modifiers(&SecurityAddon), )] @@ -365,6 +383,7 @@ impl ApiServer { .routes(routes!(run_profile)) .routes(routes!(open_url_in_profile)) .routes(routes!(kill_profile)) + .routes(routes!(import_profile_cookies)) .routes(routes!(get_groups, create_group)) .routes(routes!(get_group, update_group, delete_group)) .routes(routes!(get_tags)) @@ -1834,6 +1853,77 @@ async fn kill_profile( Ok(StatusCode::NO_CONTENT) } +#[utoipa::path( + post, + path = "/v1/profiles/{id}/cookies/import", + params( + ("id" = String, Path, description = "Profile ID") + ), + request_body = ImportCookiesRequest, + responses( + (status = 200, description = "Cookies imported successfully", body = ImportCookiesResponse), + (status = 400, description = "Invalid cookie file or unsupported browser"), + (status = 401, description = "Unauthorized"), + (status = 404, description = "Profile not found"), + (status = 409, description = "Browser is currently running"), + (status = 500, description = "Internal server error") + ), + security( + ("bearer_auth" = []) + ), + tag = "cookies" +)] +async fn import_profile_cookies( + Path(id): Path, + State(state): State, + Json(request): Json, +) -> Result, StatusCode> { + let profile_manager = ProfileManager::instance(); + let profiles = profile_manager + .list_profiles() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + if !profiles.iter().any(|p| p.id.to_string() == id) { + return Err(StatusCode::NOT_FOUND); + } + + match crate::cookie_manager::CookieManager::import_cookies( + &state.app_handle, + &id, + &request.content, + ) + .await + { + Ok(result) => { + if let Some(scheduler) = crate::sync::get_global_scheduler() { + if let Some(profile) = profiles.iter().find(|p| p.id.to_string() == id) { + if profile.is_sync_enabled() { + let pid = id.clone(); + tauri::async_runtime::spawn(async move { + scheduler.queue_profile_sync(pid).await; + }); + } + } + } + Ok(Json(ImportCookiesResponse { + cookies_imported: result.cookies_imported, + cookies_replaced: result.cookies_replaced, + errors: result.errors, + })) + } + Err(e) => { + let msg = e.to_lowercase(); + if msg.contains("running") { + Err(StatusCode::CONFLICT) + } else if msg.contains("no valid cookies") || msg.contains("unsupported browser") { + Err(StatusCode::BAD_REQUEST) + } else { + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + } +} + // API Handler - Download Browser #[utoipa::path( post, diff --git a/src-tauri/src/camoufox/config.rs b/src-tauri/src/camoufox/config.rs index b8773a9..1942d16 100644 --- a/src-tauri/src/camoufox/config.rs +++ b/src-tauri/src/camoufox/config.rs @@ -376,11 +376,12 @@ impl CamoufoxConfigBuilder { (config, target_os) }; - // Add random window history length - config.insert( - "window.history.length".to_string(), - serde_json::json!(rng.random_range(1..=5)), - ); + // Note: we used to spoof `window.history.length` to a random value in + // [1, 5] here. Newer Camoufox builds clamp the docShell session history + // to this value, which disables the toolbar back/forward buttons when + // the spoof rolls a small number. The fingerprint value drifts on every + // user navigation anyway, so a constant spoof is detectable and not + // worth the broken navigation UX. // Add fonts if !self.custom_fonts_only { diff --git a/src-tauri/src/camoufox_manager.rs b/src-tauri/src/camoufox_manager.rs index 68252ad..aeda09f 100644 --- a/src-tauri/src/camoufox_manager.rs +++ b/src-tauri/src/camoufox_manager.rs @@ -222,10 +222,16 @@ impl CamoufoxManager { .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; // Parse the fingerprint config JSON - let fingerprint_config: HashMap = + let mut fingerprint_config: HashMap = serde_json::from_str(&custom_config) .map_err(|e| format!("Failed to parse fingerprint config: {e}"))?; + // Strip `window.history.length` even when present in a previously-saved + // fingerprint. Newer Camoufox clamps the docShell session history to the + // spoofed value, which disables the toolbar back/forward buttons. See + // the matching note in camoufox/config.rs. + fingerprint_config.remove("window.history.length"); + // Convert to environment variables using CAMOU_CONFIG chunking let env_vars = crate::camoufox::env_vars::config_to_env_vars(&fingerprint_config) .map_err(|e| format!("Failed to convert config to env vars: {e}"))?; @@ -690,10 +696,11 @@ impl CamoufoxManager { } } - // Write explicit proxy + extension prefs to user.js so Camoufox always - // uses the local donut-proxy and picks up sideloaded extensions. user.js - // values override prefs.js on every launch, so this is always canonical. - if let Some(proxy_str) = &config.proxy { + // Patch user.js with Camoufox-specific overrides on every launch. This + // always runs (not gated on the proxy being set) because Camoufox's + // bundled camoufox.cfg ships defaults that break basic browser features + // and we need to override them per-profile. + { let user_js_path = profile_path.join("user.js"); let mut prefs = String::new(); @@ -703,6 +710,8 @@ impl CamoufoxManager { "network.proxy.", "xpinstall.signatures.required", "extensions.startupScanScopes", + "browser.sessionhistory.max_entries", + "browser.sessionhistory.max_total_viewers", ]; if let Ok(existing) = std::fs::read_to_string(&user_js_path) { for line in existing.lines() { @@ -713,6 +722,15 @@ impl CamoufoxManager { } } + // Camoufox's bundled camoufox.cfg sets these to 0, which makes + // docShell remember zero prior pages and leaves the toolbar + // back/forward buttons permanently disabled no matter how much + // the user navigates. Restore Firefox defaults. + prefs.push_str( + "user_pref(\"browser.sessionhistory.max_entries\", 50);\n\ + user_pref(\"browser.sessionhistory.max_total_viewers\", -1);\n", + ); + // Required for sideloaded extensions: // - signatures.required=false accepts unsigned .xpi (Camoufox is built // without MOZ_REQUIRE_SIGNING so this is honored). @@ -723,35 +741,37 @@ impl CamoufoxManager { user_pref(\"extensions.startupScanScopes\", 1);\n", ); - if let Ok(parsed) = url::Url::parse(proxy_str) { - let host = parsed.host_str().unwrap_or("127.0.0.1"); - let port = parsed.port().unwrap_or(8080); - let scheme = parsed.scheme(); + if let Some(proxy_str) = &config.proxy { + if let Ok(parsed) = url::Url::parse(proxy_str) { + let host = parsed.host_str().unwrap_or("127.0.0.1"); + let port = parsed.port().unwrap_or(8080); + let scheme = parsed.scheme(); - if scheme == "socks5" || scheme == "socks4" { - prefs.push_str(&format!( - "user_pref(\"network.proxy.type\", 1);\n\ - user_pref(\"network.proxy.socks\", \"{host}\");\n\ - user_pref(\"network.proxy.socks_port\", {port});\n\ - user_pref(\"network.proxy.socks_version\", {});\n\ - user_pref(\"network.proxy.socks_remote_dns\", true);\n", - if scheme == "socks5" { 5 } else { 4 } - )); - } else { - // HTTP/HTTPS proxy - prefs.push_str(&format!( - "user_pref(\"network.proxy.type\", 1);\n\ - user_pref(\"network.proxy.http\", \"{host}\");\n\ - user_pref(\"network.proxy.http_port\", {port});\n\ - user_pref(\"network.proxy.ssl\", \"{host}\");\n\ - user_pref(\"network.proxy.ssl_port\", {port});\n\ - user_pref(\"network.proxy.no_proxies_on\", \"\");\n" - )); + if scheme == "socks5" || scheme == "socks4" { + prefs.push_str(&format!( + "user_pref(\"network.proxy.type\", 1);\n\ + user_pref(\"network.proxy.socks\", \"{host}\");\n\ + user_pref(\"network.proxy.socks_port\", {port});\n\ + user_pref(\"network.proxy.socks_version\", {});\n\ + user_pref(\"network.proxy.socks_remote_dns\", true);\n", + if scheme == "socks5" { 5 } else { 4 } + )); + } else { + // HTTP/HTTPS proxy + prefs.push_str(&format!( + "user_pref(\"network.proxy.type\", 1);\n\ + user_pref(\"network.proxy.http\", \"{host}\");\n\ + user_pref(\"network.proxy.http_port\", {port});\n\ + user_pref(\"network.proxy.ssl\", \"{host}\");\n\ + user_pref(\"network.proxy.ssl_port\", {port});\n\ + user_pref(\"network.proxy.no_proxies_on\", \"\");\n" + )); + } } + } - if let Err(e) = std::fs::write(&user_js_path, prefs) { - log::warn!("Failed to write user.js: {e}"); - } + if let Err(e) = std::fs::write(&user_js_path, prefs) { + log::warn!("Failed to write user.js: {e}"); } } diff --git a/src-tauri/src/mcp_server.rs b/src-tauri/src/mcp_server.rs index 7d37ce1..69caa14 100644 --- a/src-tauri/src/mcp_server.rs +++ b/src-tauri/src/mcp_server.rs @@ -1145,6 +1145,25 @@ impl McpServer { "required": ["profile_id"] }), }, + // Cookie management tools + McpTool { + name: "import_profile_cookies".to_string(), + description: "Import cookies into a Wayfern or Camoufox profile from a JSON array (Puppeteer / EditThisCookie format) or a Netscape cookies.txt. Format is auto-detected. The browser must not be running.".to_string(), + input_schema: serde_json::json!({ + "type": "object", + "properties": { + "profile_id": { + "type": "string", + "description": "The UUID of the target profile" + }, + "content": { + "type": "string", + "description": "Raw cookie file content (JSON array or Netscape cookies.txt)" + } + }, + "required": ["profile_id", "content"] + }), + }, // Team lock tools McpTool { name: "get_team_locks".to_string(), @@ -1674,6 +1693,8 @@ impl McpServer { .handle_assign_extension_group_to_profile(arguments) .await } + // Cookie management + "import_profile_cookies" => self.handle_import_profile_cookies(arguments).await, // Team lock tools "get_team_locks" => self.handle_get_team_locks().await, "get_team_lock_status" => self.handle_get_team_lock_status(arguments).await, @@ -2855,6 +2876,74 @@ impl McpServer { })) } + // Cookie management handlers + async fn handle_import_profile_cookies( + &self, + arguments: &serde_json::Value, + ) -> Result { + let profile_id = arguments + .get("profile_id") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing profile_id".to_string(), + })?; + + let content = arguments + .get("content") + .and_then(|v| v.as_str()) + .ok_or_else(|| McpError { + code: -32602, + message: "Missing content".to_string(), + })?; + + let app_handle = { + let inner = self.inner.lock().await; + inner + .app_handle + .as_ref() + .ok_or_else(|| McpError { + code: -32000, + message: "MCP server not properly initialized".to_string(), + })? + .clone() + }; + + let result = + crate::cookie_manager::CookieManager::import_cookies(&app_handle, profile_id, content) + .await + .map_err(|e| McpError { + code: -32000, + message: format!("Failed to import cookies: {e}"), + })?; + + if let Some(scheduler) = crate::sync::get_global_scheduler() { + let profile_manager = crate::profile::manager::ProfileManager::instance(); + if let Ok(profiles) = profile_manager.list_profiles() { + if let Some(profile) = profiles.iter().find(|p| p.id.to_string() == profile_id) { + if profile.is_sync_enabled() { + let pid = profile_id.to_string(); + tauri::async_runtime::spawn(async move { + scheduler.queue_profile_sync(pid).await; + }); + } + } + } + } + + Ok(serde_json::json!({ + "content": [{ + "type": "text", + "text": format!( + "Import complete: {} imported, {} replaced, {} parse error(s)", + result.cookies_imported, + result.cookies_replaced, + result.errors.len() + ) + }] + })) + } + // VPN management handlers async fn handle_import_vpn( &self, @@ -4968,6 +5057,8 @@ mod tests { assert!(tool_names.contains(&"delete_extension")); assert!(tool_names.contains(&"delete_extension_group")); assert!(tool_names.contains(&"assign_extension_group_to_profile")); + // Cookie tools + assert!(tool_names.contains(&"import_profile_cookies")); // Team lock tools assert!(tool_names.contains(&"get_team_locks")); assert!(tool_names.contains(&"get_team_lock_status")); diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 6ffa870..fb8bc1a 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -691,7 +691,7 @@ const TagsCell = React.memo<{ ); return ( -
+
{ButtonContent} {hiddenCount > 0 && ( @@ -717,7 +717,7 @@ const TagsCell = React.memo<{ return (
@@ -925,19 +925,17 @@ const NoteCell = React.memo<{ }, [openNoteEditorFor, profile.id]); const displayNote = effectiveNote ?? ""; - const trimmedNote = - displayNote.length > 12 ? `${displayNote.slice(0, 12)}...` : displayNote; - const showTooltip = displayNote.length > 12 || displayNote.length > 0; + const showTooltip = displayNote.length > 0; if (openNoteEditorFor !== profile.id) { return ( -
+
@@ -974,7 +972,7 @@ const NoteCell = React.memo<{ return (
diff --git a/src/components/profile-info-dialog.tsx b/src/components/profile-info-dialog.tsx index 9262d4a..3fd3568 100644 --- a/src/components/profile-info-dialog.tsx +++ b/src/components/profile-info-dialog.tsx @@ -24,6 +24,7 @@ import { LuShield, LuShieldCheck, LuTrash2, + LuUpload, LuUsers, LuX, } from "react-icons/lu"; @@ -907,6 +908,7 @@ function ProfileInfoLayout({ isRunning={isRunning} isDisabled={isDisabled} onCopyCookies={cookiesCopyAction?.onClick} + onImportCookies={cookiesManageAction?.onClick} t={t} /> )} @@ -1439,12 +1441,14 @@ function CookiesSectionInline({ isRunning, isDisabled, onCopyCookies, + onImportCookies, t, }: { profile: BrowserProfile; isRunning: boolean; isDisabled: boolean; onCopyCookies?: () => void; + onImportCookies?: () => void; t: (key: string, options?: Record) => string; }) { type CookieStats = { @@ -1493,18 +1497,32 @@ function CookiesSectionInline({ {t("profileInfo.sections.cookies")}
- {onCopyCookies && ( - - )} +
+ {onImportCookies && ( + + )} + {onCopyCookies && ( + + )} +

{t("profileInfo.sectionDesc.cookies")}