From b088ae675b6adb34c8626c5d876026360cd84ff8 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 3 Aug 2025 14:38:44 +0400 Subject: [PATCH] feat: finalize camoufox integration --- .vscode/settings.json | 1 + nodecar/src/camoufox-launcher.ts | 46 +- nodecar/src/camoufox-worker.ts | 178 ++++++-- nodecar/src/index.ts | 15 +- src-tauri/src/browser_runner.rs | 33 +- src-tauri/src/camoufox.rs | 40 +- src-tauri/tests/nodecar_integration.rs | 9 - src/components/camoufox-config-dialog.tsx | 3 + src/components/create-profile-dialog.tsx | 7 - .../shared-camoufox-config-form.tsx | 404 ++++++++---------- 10 files changed, 385 insertions(+), 351 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index cb0be67..3d47b4e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -63,6 +63,7 @@ "kdeglobals", "keras", "KHTML", + "Kolkata", "kreadconfig", "launchservices", "letterboxing", diff --git a/nodecar/src/camoufox-launcher.ts b/nodecar/src/camoufox-launcher.ts index 8aea5cc..c40f1ac 100644 --- a/nodecar/src/camoufox-launcher.ts +++ b/nodecar/src/camoufox-launcher.ts @@ -176,29 +176,48 @@ export async function stopCamoufoxProcess(id: string): Promise { } try { - const killByPattern = spawn("pkill", ["-f", `camoufox-worker.*${id}`], { - stdio: "ignore", - }); + console.log(`Stopping Camoufox process ${id} (PID: ${config.processId})`); - // Method 2: If we have a process ID, kill by PID + // Method 1: If we have a process ID, kill by PID with proper signal sequence if (config.processId) { try { + // First try SIGTERM for graceful shutdown process.kill(config.processId, "SIGTERM"); + console.log(`Sent SIGTERM to Camoufox process ${config.processId}`); - // Give it a moment to terminate gracefully - await new Promise((resolve) => setTimeout(resolve, 2000)); + // Give it more time to terminate gracefully (increased from 2s to 5s) + await new Promise((resolve) => setTimeout(resolve, 5000)); - // Force kill if still running + // Check if process is still running try { + process.kill(config.processId, 0); // Signal 0 checks if process exists + // Process still exists, force kill + console.log( + `Camoufox process ${config.processId} still running, sending SIGKILL`, + ); process.kill(config.processId, "SIGKILL"); } catch { // Process already terminated + console.log( + `Camoufox process ${config.processId} terminated gracefully`, + ); } - } catch (error) { - // Process not found or already terminated + } catch { + console.log( + `Camoufox process ${config.processId} not found or already terminated`, + ); } } + // Method 2: Pattern-based kill as fallback + const killByPattern = spawn( + "pkill", + ["-TERM", "-f", `camoufox-worker.*${id}`], + { + stdio: "ignore", + }, + ); + // Wait for pattern-based kill command to complete await new Promise((resolve) => { killByPattern.on("exit", () => resolve()); @@ -206,10 +225,17 @@ export async function stopCamoufoxProcess(id: string): Promise { setTimeout(() => resolve(), 3000); }); + // Final cleanup with SIGKILL if needed + setTimeout(() => { + spawn("pkill", ["-KILL", "-f", `camoufox-worker.*${id}`], { + stdio: "ignore", + }); + }, 1000); + // Delete the configuration deleteCamoufoxConfig(id); return true; - } catch (error) { + } catch { // Delete the configuration even if stopping failed deleteCamoufoxConfig(id); return false; diff --git a/nodecar/src/camoufox-worker.ts b/nodecar/src/camoufox-worker.ts index c662824..881659f 100644 --- a/nodecar/src/camoufox-worker.ts +++ b/nodecar/src/camoufox-worker.ts @@ -1,5 +1,5 @@ -import { Camoufox } from "camoufox-js"; -import type { Page } from "playwright-core"; +import { launchServer } from "camoufox-js"; +import { type Browser, type BrowserServer, firefox } from "playwright-core"; import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js"; /** @@ -20,34 +20,56 @@ export async function runCamoufoxWorker(id: string): Promise { process.exit(1); } - // Return success immediately - before any async operations - const processId = process.pid; + config.processId = process.pid; + saveCamoufoxConfig(config); console.log( JSON.stringify({ success: true, id: id, - processId, + processId: process.pid, profilePath: config.profilePath, message: "Camoufox worker started successfully", }), ); - // Update config with process details - config.processId = processId; - saveCamoufoxConfig(config); - - // Handle process termination gracefully - const gracefulShutdown = async () => { - process.exit(0); - }; - - process.on("SIGTERM", () => void gracefulShutdown()); - process.on("SIGINT", () => void gracefulShutdown()); - // Launch browser in background - this can take time and may fail setImmediate(async () => { - let page: Page | null = null; + let browser: Browser | null = null; + let server: BrowserServer | null = null; + let windowCheckInterval: NodeJS.Timeout | null = null; + + // Graceful shutdown handler with access to browser and server + const gracefulShutdown = async () => { + try { + // Clear any intervals first + if (windowCheckInterval) { + clearInterval(windowCheckInterval); + } + + // Close browser context and server if they exist + if (browser?.isConnected()) { + await browser.close(); + } + if (server) { + server.process().kill(); + await server.close(); + } + } catch { + // Ignore cleanup errors during shutdown + } + process.exit(0); + }; + + // Handle various quit signals for proper macOS Command+Q support + process.on("SIGTERM", () => void gracefulShutdown()); + process.on("SIGINT", () => void gracefulShutdown()); + process.on("SIGHUP", () => void gracefulShutdown()); + process.on("SIGQUIT", () => void gracefulShutdown()); + + // Handle uncaught exceptions and unhandled rejections + process.on("uncaughtException", () => void gracefulShutdown()); + process.on("unhandledRejection", () => void gracefulShutdown()); try { // Prepare options for Camoufox @@ -58,7 +80,7 @@ export async function runCamoufoxWorker(id: string): Promise { camoufoxOptions.user_data_dir = config.profilePath; } - // Remove custom properties before passing to Camoufox + // Theming camoufoxOptions.disableTheming = true; camoufoxOptions.showcursor = false; @@ -72,24 +94,108 @@ export async function runCamoufoxWorker(id: string): Promise { camoufoxOptions.headless = false; } - const browser = await Camoufox(camoufoxOptions); + // Launch the server with proper options + server = await launchServer({ + ws_path: `/ws_${config.id}`, + os: camoufoxOptions.os, + block_images: camoufoxOptions.block_images, + block_webrtc: camoufoxOptions.block_webrtc, + block_webgl: camoufoxOptions.block_webgl, + disable_coop: camoufoxOptions.disable_coop, + geoip: camoufoxOptions.geoip, + humanize: camoufoxOptions.humanize, + locale: camoufoxOptions.locale, + addons: camoufoxOptions.addons, + fonts: camoufoxOptions.fonts, + custom_fonts_only: camoufoxOptions.custom_fonts_only, + exclude_addons: camoufoxOptions.exclude_addons, + screen: camoufoxOptions.screen, + window: camoufoxOptions.window, + fingerprint: camoufoxOptions.fingerprint, + ff_version: camoufoxOptions.ff_version, + headless: camoufoxOptions.headless, + main_world_eval: camoufoxOptions.main_world_eval, + executable_path: camoufoxOptions.executable_path, + firefox_user_prefs: camoufoxOptions.firefox_user_prefs, + proxy: camoufoxOptions.proxy, + enable_cache: camoufoxOptions.enable_cache, + args: camoufoxOptions.args, + env: camoufoxOptions.env, + debug: camoufoxOptions.debug, + virtual_display: camoufoxOptions.virtual_display, + webgl_config: camoufoxOptions.webgl_config, + config: { + disableTheming: true, + showcursor: false, + timezone: camoufoxOptions.timezone, + }, + }); + + // Connect to the server + browser = await firefox.connect(server.wsEndpoint()); const context = await browser.newContext(); + // Handle browser disconnection for proper cleanup + browser.on("disconnected", () => void gracefulShutdown()); + saveCamoufoxConfig(config); - // Handle URL opening if provided - if (config.url && context) { - try { - if (!page) { - page = await context.newPage(); + // Monitor for window closure to handle Command+Q properly + + const startWindowMonitoring = () => { + windowCheckInterval = setInterval(async () => { + try { + if (browser?.isConnected()) { + const contexts = browser.contexts(); + let totalPages = 0; + + for (const ctx of contexts) { + const pages = ctx.pages(); + totalPages += pages.length; + } + + // If no pages are open, terminate the server + if (totalPages === 0) { + if (windowCheckInterval) { + clearInterval(windowCheckInterval); + } + await gracefulShutdown(); + } + } + } catch { + // If we can't check windows, assume browser is closing + if (windowCheckInterval) { + clearInterval(windowCheckInterval); + } + await gracefulShutdown(); } + }, 1000); // Check every second + }; + + // Handle URL opening if provided + if (config.url) { + try { + const page = await context.newPage(); await page.goto(config.url, { waitUntil: "domcontentloaded", timeout: 30000, }); - } catch { + + // Start monitoring after page is created + startWindowMonitoring(); + } catch (urlError) { + console.error({ + message: "Failed to open URL", + error: urlError, + }); // URL opening failure doesn't affect startup success + // Still start monitoring + startWindowMonitoring(); } + } else { + await context.newPage(); + // Start monitoring after page is created + startWindowMonitoring(); } // Monitor browser connection @@ -97,16 +203,24 @@ export async function runCamoufoxWorker(id: string): Promise { try { if (!browser || !browser.isConnected()) { clearInterval(keepAlive); - process.exit(0); + await gracefulShutdown(); } - } catch { + } catch (error) { + console.error({ + message: "Error in keepAlive check", + error, + }); clearInterval(keepAlive); - process.exit(0); + await gracefulShutdown(); } }, 2000); - } catch { - // Browser launch failed, but worker is still "successful" - // Process will stay alive due to the main setInterval above + } catch (error) { + console.error({ + message: "Failed to launch Camoufox", + error, + }); + // Browser launch failed, attempt cleanup + await gracefulShutdown(); } }); diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index 1c48d12..7647254 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -233,8 +233,7 @@ program // Firefox preferences .option("--firefox-prefs ", "Firefox user preferences (JSON string)") - .option("--disable-theming", "disable Firefox theming") - .option("--no-showcursor", "disable cursor display") + // Note: theming and cursor options are hardcoded and not user-configurable .description("manage Camoufox browser instances") .action( @@ -262,11 +261,12 @@ program // Security options if (options.disableCoop) camoufoxOptions.disable_coop = true; - // Geolocation + // Geolocation - always enable geoip for proper spoofing if (options.geoip) { camoufoxOptions.geoip = options.geoip === "auto" ? true : (options.geoip as string); } + if (options.latitude && options.longitude) { camoufoxOptions.geolocation = { latitude: options.latitude as number, @@ -279,9 +279,8 @@ program if (options.timezone) camoufoxOptions.timezone = options.timezone as string; - // UI and behavior if (options.humanize) - camoufoxOptions.humanize = options.humanize as boolean | number; + camoufoxOptions.humanize = options.humanize as boolean; if (options.headless) camoufoxOptions.headless = true; // Localization @@ -388,11 +387,6 @@ program } } - // Theming and cursor - these are custom properties for camoufox-js - if (options.disableTheming) camoufoxOptions.disableTheming = true; - if (options.showcursor === false) camoufoxOptions.showcursor = false; - - // Use the launcher to start Camoufox properly const config = await startCamoufoxProcess( camoufoxOptions, typeof options.profilePath === "string" @@ -401,7 +395,6 @@ program typeof options.url === "string" ? options.url : undefined, ); - // Output the configuration as JSON for the Rust side to parse console.log( JSON.stringify({ id: config.id, diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 3cb02e7..3661cdb 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -185,29 +185,17 @@ impl BrowserRunner { // Set proxy in camoufox config camoufox_config.proxy = Some(proxy_url); + + // Ensure geoip is always enabled for proper geolocation spoofing + if camoufox_config.geoip.is_none() { + camoufox_config.geoip = Some(serde_json::Value::Bool(true)); + } println!( - "Configured local proxy for Camoufox: {:?}", - camoufox_config.proxy + "Configured local proxy for Camoufox: {:?}, geoip: {:?}", + camoufox_config.proxy, camoufox_config.geoip ); - // Use the existing config or create a test config if none exists - let final_config = if camoufox_config.timezone.is_some() - || camoufox_config.screen_min_width.is_some() - || camoufox_config.window_width.is_some() - { - camoufox_config.clone() - } else { - // No meaningful config provided, use test config to ensure anti-fingerprinting works - println!("No Camoufox configuration provided, using test configuration"); - let mut test_config = crate::camoufox::CamoufoxNodecarLauncher::create_test_config(); - // Preserve any proxy settings from the original config - test_config.proxy = camoufox_config.proxy.clone(); - test_config.headless = camoufox_config.headless; - test_config.debug = Some(true); // Enable debug for troubleshooting - test_config - }; - // Use the nodecar camoufox launcher println!( "Launching Camoufox via nodecar for profile: {}", @@ -215,7 +203,12 @@ impl BrowserRunner { ); let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); let camoufox_result = camoufox_launcher - .launch_camoufox_profile_nodecar(app_handle.clone(), profile.clone(), final_config, url) + .launch_camoufox_profile_nodecar( + app_handle.clone(), + profile.clone(), + camoufox_config, + url, + ) .await .map_err(|e| -> Box { format!("Failed to launch camoufox via nodecar: {e}").into() diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs index ef584de..2784f43 100644 --- a/src-tauri/src/camoufox.rs +++ b/src-tauri/src/camoufox.rs @@ -56,7 +56,7 @@ impl Default for CamoufoxConfig { block_webrtc: None, block_webgl: None, disable_coop: None, - geoip: None, + geoip: Some(serde_json::Value::Bool(true)), country: None, timezone: None, latitude: None, @@ -80,7 +80,7 @@ impl Default for CamoufoxConfig { webgl_vendor: None, webgl_renderer: None, proxy: None, - enable_cache: Some(true), // Cache enabled by default + enable_cache: Some(true), virtual_display: None, debug: None, additional_args: None, @@ -133,44 +133,20 @@ impl CamoufoxNodecarLauncher { &CAMOUFOX_NODECAR_LAUNCHER } - /// Create a test configuration to verify anti-fingerprinting is working + /// Create a test configuration + #[allow(dead_code)] pub fn create_test_config() -> CamoufoxConfig { CamoufoxConfig { // Core anti-fingerprinting settings - timezone: Some("Europe/London".to_string()), screen_min_width: Some(1440), screen_min_height: Some(900), - window_width: Some(1200), - window_height: Some(800), - - // Locale settings - locale: Some(vec!["en-GB".to_string(), "en-US".to_string()]), // WebGL spoofing webgl_vendor: Some("Intel Inc.".to_string()), webgl_renderer: Some("Intel Iris Pro OpenGL Engine".to_string()), - // Geolocation spoofing (London coordinates) - latitude: Some(51.5074), - longitude: Some(-0.1278), - - // Font settings - fonts: Some(vec![ - "Arial".to_string(), - "Times New Roman".to_string(), - "Helvetica".to_string(), - "Georgia".to_string(), - ]), - custom_fonts_only: Some(true), - // Humanization humanize: Some(true), - humanize_duration: Some(2.0), - - // Blocking features - block_images: Some(false), // Don't block images for testing - block_webrtc: Some(true), - block_webgl: Some(false), // Don't block WebGL so we can test spoofing // Other settings debug: Some(true), @@ -646,22 +622,19 @@ mod tests { let test_config = CamoufoxNodecarLauncher::create_test_config(); // Verify test config has expected values - assert_eq!(test_config.timezone, Some("Europe/London".to_string())); assert_eq!(test_config.screen_min_width, Some(1440)); assert_eq!(test_config.screen_min_height, Some(900)); - assert_eq!(test_config.window_width, Some(1200)); - assert_eq!(test_config.window_height, Some(800)); assert_eq!(test_config.webgl_vendor, Some("Intel Inc.".to_string())); assert_eq!( test_config.webgl_renderer, Some("Intel Iris Pro OpenGL Engine".to_string()) ); - assert_eq!(test_config.latitude, Some(51.5074)); - assert_eq!(test_config.longitude, Some(-0.1278)); assert_eq!(test_config.humanize, Some(true)); assert_eq!(test_config.debug, Some(true)); assert_eq!(test_config.enable_cache, Some(true)); assert_eq!(test_config.headless, Some(false)); + // Verify that geoip is enabled by default (from Default implementation) + assert_eq!(test_config.geoip, Some(serde_json::Value::Bool(true))); } #[test] @@ -670,6 +643,7 @@ mod tests { // Verify defaults assert_eq!(default_config.enable_cache, Some(true)); + assert_eq!(default_config.geoip, Some(serde_json::Value::Bool(true))); assert_eq!(default_config.timezone, None); assert_eq!(default_config.debug, None); assert_eq!(default_config.headless, None); diff --git a/src-tauri/tests/nodecar_integration.rs b/src-tauri/tests/nodecar_integration.rs index ce04afb..6562b35 100644 --- a/src-tauri/tests/nodecar_integration.rs +++ b/src-tauri/tests/nodecar_integration.rs @@ -591,15 +591,6 @@ async fn test_nodecar_command_validation() -> Result<(), Box({ enable_cache: true, os: [getCurrentOS()], + geoip: true, }); const [isSaving, setIsSaving] = useState(false); @@ -40,6 +41,7 @@ export function CamoufoxConfigDialog({ profile.camoufox_config || { enable_cache: true, os: [getCurrentOS()], + geoip: true, }, ); } @@ -70,6 +72,7 @@ export function CamoufoxConfigDialog({ profile.camoufox_config || { enable_cache: true, os: [getCurrentOS()], + geoip: true, }, ); } diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 6d6ba11..8cb38b4 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -453,13 +453,6 @@ export function CreateProfileDialog({ - {/* Anti-Detect Description */} -
-

- Powered by Camoufox -

-
-
{/* Camoufox Download Status */} {!isBrowserVersionAvailable("camoufox") && diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx index 206eed8..99e70be 100644 --- a/src/components/shared-camoufox-config-form.tsx +++ b/src/components/shared-camoufox-config-form.tsx @@ -14,11 +14,11 @@ import { } from "@/components/ui/select"; import type { CamoufoxConfig } from "@/types"; -const osOptions = [ - { value: "windows", label: "Windows" }, - { value: "macos", label: "macOS" }, - { value: "linux", label: "Linux" }, -]; +// const osOptions = [ +// { value: "windows", label: "Windows" }, +// { value: "macos", label: "macOS" }, +// { value: "linux", label: "Linux" }, +// ]; const timezoneOptions = [ { value: "America/New_York", label: "America/New_York" }, @@ -143,15 +143,15 @@ const localeOptions = [ { value: "mt-MT", label: "Maltese (Malta)" }, ]; -const getCurrentOS = () => { - if (typeof window !== "undefined") { - const userAgent = window.navigator.userAgent; - if (userAgent.includes("Win")) return "windows"; - if (userAgent.includes("Mac")) return "macos"; - if (userAgent.includes("Linux")) return "linux"; - } - return "unknown"; -}; +// const getCurrentOS = () => { +// if (typeof window !== "undefined") { +// const userAgent = window.navigator.userAgent; +// if (userAgent.includes("Win")) return "windows"; +// if (userAgent.includes("Mac")) return "macos"; +// if (userAgent.includes("Linux")) return "linux"; +// } +// return "unknown"; +// }; interface SystemLocale { locale: string; @@ -211,16 +211,36 @@ export function SharedCamoufoxConfigForm({ loadSystemDefaults(); }, []); + // Determine if automatic location configuration is enabled + // Default to true if geoip is not explicitly set to false + const isAutoLocationEnabled = config.geoip !== false; + + // Handle automatic location configuration toggle + const handleAutoLocationToggle = (enabled: boolean) => { + if (enabled) { + // Enable automatic configuration - set geoip to true and clear manual fields + onConfigChange("geoip", true); + onConfigChange("country", undefined); + onConfigChange("timezone", undefined); + onConfigChange("latitude", undefined); + onConfigChange("longitude", undefined); + onConfigChange("locale", undefined); + } else { + // Disable automatic configuration - set geoip to false + onConfigChange("geoip", false); + } + }; + // Get the selected OS for warning - const selectedOS = config.os?.[0]; - const currentOS = getCurrentOS(); - const showOSWarning = - selectedOS && selectedOS !== currentOS && currentOS !== "unknown"; + // const selectedOS = config.os?.[0]; + // const currentOS = getCurrentOS(); + // const showOSWarning = + // selectedOS && selectedOS !== currentOS && currentOS !== "unknown"; return (
{/* OS Selection */} -
+ {/*
- onConfigChange("country", e.target.value || undefined) - } - placeholder={ - systemLocale - ? `e.g., ${systemLocale.country}` - : "e.g., US, GB, DE" - } - /> + {!isAutoLocationEnabled && ( +
+ +
+

+ ⚠️ Warning: Configuring variables yourself may not always work due + to underlying technology. It's recommended to use automatic + location configuration. +

-
- - + onConfigChange("country", e.target.value || undefined) + } + placeholder={ + systemLocale + ? `e.g., ${systemLocale.country}` + : "e.g., US, GB, DE" + } + /> +
+
+ + + {timezoneOptions.map((tz) => ( + + {tz.label} + + ))} + + +
+
+
+
+ + + onConfigChange( + "latitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., 40.7128" + /> +
+
+ + + onConfigChange( + "longitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., -74.0060" + /> +
-
-
- - - onConfigChange( - "latitude", - e.target.value ? parseFloat(e.target.value) : undefined, - ) - } - placeholder="e.g., 40.7128" - /> -
-
- - - onConfigChange( - "longitude", - e.target.value ? parseFloat(e.target.value) : undefined, - ) - } - placeholder="e.g., -74.0060" - /> -
-
-
+ )} {/* Localization */} -
- - -
+ {!isAutoLocationEnabled && ( +
+ + +
+ )} {/* Screen Resolution */}
@@ -469,72 +481,6 @@ export function SharedCamoufoxConfigForm({
- {/* Window Size */} -
- -
-
- - - onConfigChange( - "window_width", - e.target.value ? parseInt(e.target.value) : undefined, - ) - } - placeholder="e.g., 1366" - /> -
-
- - - onConfigChange( - "window_height", - e.target.value ? parseInt(e.target.value) : undefined, - ) - } - placeholder="e.g., 768" - /> -
-
-
- - {/* Advanced Options */} -
- -
-
- - onConfigChange("enable_cache", checked) - } - /> - -
-
- - onConfigChange("main_world_eval", checked) - } - /> - -
-
-
- {/* WebGL Settings */}