refactor: cleanup

This commit is contained in:
zhom
2026-05-25 02:19:20 +04:00
parent 6850f2c573
commit 9236ad38c8
6 changed files with 276 additions and 58 deletions
+90
View File
@@ -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<String>,
}
#[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<String>,
State(state): State<ApiServerState>,
Json(request): Json<ImportCookiesRequest>,
) -> Result<Json<ImportCookiesResponse>, 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,
+6 -5
View File
@@ -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 {
+51 -31
View File
@@ -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<String, serde_json::Value> =
let mut fingerprint_config: HashMap<String, serde_json::Value> =
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}");
}
}
+91
View File
@@ -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<serde_json::Value, McpError> {
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"));
+8 -10
View File
@@ -691,7 +691,7 @@ const TagsCell = React.memo<{
);
return (
<div className="w-40 h-6 cursor-pointer">
<div className="w-full h-6 cursor-pointer">
<Tooltip>
<TooltipTrigger asChild>{ButtonContent}</TooltipTrigger>
{hiddenCount > 0 && (
@@ -717,7 +717,7 @@ const TagsCell = React.memo<{
return (
<div
className={cn(
"w-40 h-6 relative",
"w-full h-6 relative",
isDisabled && "opacity-60 pointer-events-none",
)}
>
@@ -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 (
<div className="w-24 min-h-6">
<div className="w-full min-h-6">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className={cn(
"flex items-start px-2 py-1 min-h-6 w-full bg-transparent rounded border-none text-left",
"flex items-center px-2 py-1 min-h-6 w-full min-w-0 bg-transparent rounded border-none text-left",
isDisabled
? "opacity-60 cursor-not-allowed"
: "cursor-pointer hover:bg-accent/50",
@@ -951,11 +949,11 @@ const NoteCell = React.memo<{
>
<span
className={cn(
"text-sm wrap-break-word",
"text-sm truncate block w-full",
!effectiveNote && "text-muted-foreground",
)}
>
{effectiveNote ? trimmedNote : t("profiles.note.empty")}
{effectiveNote ? displayNote : t("profiles.note.empty")}
</span>
</button>
</TooltipTrigger>
@@ -974,7 +972,7 @@ const NoteCell = React.memo<{
return (
<div
className={cn(
"w-24 relative",
"w-full relative",
isDisabled && "opacity-60 pointer-events-none",
)}
>
+30 -12
View File
@@ -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, unknown>) => string;
}) {
type CookieStats = {
@@ -1493,18 +1497,32 @@ function CookiesSectionInline({
<LuCookie className="size-4" />
{t("profileInfo.sections.cookies")}
</div>
{onCopyCookies && (
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5"
disabled={isDisabled}
onClick={onCopyCookies}
>
<LuCopy className="size-3.5" />
{t("profiles.actions.copyCookies")}
</Button>
)}
<div className="flex items-center gap-2">
{onImportCookies && (
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5"
disabled={isDisabled || isRunning}
onClick={onImportCookies}
>
<LuUpload className="size-3.5" />
{t("cookies.import.title")}
</Button>
)}
{onCopyCookies && (
<Button
variant="outline"
size="sm"
className="h-7 gap-1.5"
disabled={isDisabled}
onClick={onCopyCookies}
>
<LuCopy className="size-3.5" />
{t("profiles.actions.copyCookies")}
</Button>
)}
</div>
</div>
<p className="text-xs text-muted-foreground">
{t("profileInfo.sectionDesc.cookies")}