diff --git a/nodecar/src/camoufox-launcher.ts b/nodecar/src/camoufox-launcher.ts index 8d54f33..af67bb7 100644 --- a/nodecar/src/camoufox-launcher.ts +++ b/nodecar/src/camoufox-launcher.ts @@ -1,8 +1,17 @@ -import { launchOptions } from "camoufox-js"; +import { spawn } from "node:child_process"; +import path from "node:path"; +import { + type CamoufoxConfig, + deleteCamoufoxConfig, + generateCamoufoxId, + getCamoufoxConfig, + listCamoufoxConfigs, + saveCamoufoxConfig, +} from "./camoufox-storage.js"; export interface CamoufoxLaunchOptions { // Operating system to use for fingerprint generation - os?: "windows" | "macos" | "linux" | string[]; + os?: "windows" | "macos" | "linux"[]; // Blocking options block_images?: boolean; @@ -25,7 +34,7 @@ export interface CamoufoxLaunchOptions { addons?: string[]; fonts?: string[]; custom_fonts_only?: boolean; - exclude_addons?: string[]; + exclude_addons?: "UBO"[]; // Screen and window screen?: { @@ -36,8 +45,9 @@ export interface CamoufoxLaunchOptions { }; window?: [number, number]; - // Fingerprint fingerprint?: any; + disableTheming?: boolean; + showcursor?: boolean; // Version and mode ff_version?: number; @@ -48,7 +58,8 @@ export interface CamoufoxLaunchOptions { executable_path?: string; // Firefox preferences - firefox_user_prefs?: Record; + firefox_user_prefs?: Record; + user_data_dir?: string; // Proxy settings proxy?: @@ -81,83 +92,202 @@ export interface CamoufoxLaunchOptions { } /** - * Generate Camoufox configuration using camoufox-js-lsd + * Start a Camoufox instance in a separate process + * @param options Camoufox launch options + * @param profilePath Profile directory path + * @param url Optional URL to open + * @returns Promise resolving to the Camoufox configuration */ -export async function generateCamoufoxConfig( +export async function startCamoufoxProcess( options: CamoufoxLaunchOptions = {}, -): Promise { + profilePath?: string, + url?: string, +): Promise { + // Generate a unique ID for this instance + const id = generateCamoufoxId(); + + // Create the Camoufox configuration + const config: CamoufoxConfig = { + id, + options, + profilePath, + url, + }; + + // Save the configuration before starting the process + saveCamoufoxConfig(config); + + // Build the command arguments + const args = [ + path.join(__dirname, "index.js"), + "camoufox-worker", + "start", + "--id", + id, + ]; + + // Spawn the process with proper detachment + const child = spawn(process.execPath, args, { + detached: true, + stdio: ["ignore", "pipe", "pipe"], // Capture stdout and stderr for debugging + cwd: process.cwd(), + env: { ...process.env, NODE_ENV: "production" }, // Ensure consistent environment + }); + + saveCamoufoxConfig(config); + + // Wait for the worker to start successfully or fail + return new Promise((resolve, reject) => { + let resolved = false; + let stdoutBuffer = ""; + let stderrBuffer = ""; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + reject( + new Error(`Camoufox worker ${id} startup timeout after 30 seconds`), + ); + } + }, 30000); + + // Handle stdout - look for success JSON + if (child.stdout) { + child.stdout.on("data", (data) => { + const output = data.toString(); + stdoutBuffer += output; + + // Look for success JSON message + const lines = stdoutBuffer.split("\n"); + for (const line of lines) { + if (line.trim()) { + try { + const parsed = JSON.parse(line.trim()); + if ( + parsed.success && + parsed.id === id && + parsed.port && + parsed.wsEndpoint + ) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + // Update config with server details + config.port = parsed.port; + config.wsEndpoint = parsed.wsEndpoint; + saveCamoufoxConfig(config); + child.unref(); // Allow parent to exit independently + resolve(config); + return; + } + } + } catch { + // Not JSON, continue + } + } + } + }); + } + + // Handle stderr - look for error JSON + if (child.stderr) { + child.stderr.on("data", (data) => { + const output = data.toString(); + stderrBuffer += output; + + // Look for error JSON message + const lines = stderrBuffer.split("\n"); + for (const line of lines) { + if (line.trim()) { + try { + const parsed = JSON.parse(line.trim()); + if (parsed.error && parsed.id === id) { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + reject( + new Error( + `Camoufox worker failed: ${parsed.message || parsed.error}`, + ), + ); + return; + } + } + } catch { + // Not JSON, continue + } + } + } + }); + } + + child.on("exit", (code, signal) => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + if (code !== 0) { + reject( + new Error( + `Camoufox worker ${id} exited with code ${code} and signal ${signal}. Stderr: ${stderrBuffer}`, + ), + ); + } else { + // Process exited successfully but we didn't get success message + reject( + new Error( + `Camoufox worker ${id} exited without success confirmation`, + ), + ); + } + } + }); + }); +} + +/** + * Stop a Camoufox process + * @param id The Camoufox ID to stop + * @returns Promise resolving to true if stopped, false if not found + */ +export async function stopCamoufoxProcess(id: string): Promise { + const config = getCamoufoxConfig(id); + + if (!config) { + return false; + } + try { - // Convert our options to camoufox-js-lsd format - const camoufoxOptions: any = {}; - - // Map our options to camoufox-js-lsd format - if (options.os) camoufoxOptions.os = options.os; - if (options.block_images !== undefined) - camoufoxOptions.block_images = options.block_images; - if (options.block_webrtc !== undefined) - camoufoxOptions.block_webrtc = options.block_webrtc; - if (options.block_webgl !== undefined) - camoufoxOptions.block_webgl = options.block_webgl; - if (options.disable_coop !== undefined) - camoufoxOptions.disable_coop = options.disable_coop; - if (options.geoip !== undefined) camoufoxOptions.geoip = options.geoip; - if (options.humanize !== undefined) - camoufoxOptions.humanize = options.humanize; - if (options.locale) camoufoxOptions.locale = options.locale; - if (options.addons) camoufoxOptions.addons = options.addons; - if (options.fonts) camoufoxOptions.fonts = options.fonts; - if (options.custom_fonts_only !== undefined) - camoufoxOptions.custom_fonts_only = options.custom_fonts_only; - if (options.exclude_addons) - camoufoxOptions.exclude_addons = options.exclude_addons; - if (options.screen) camoufoxOptions.screen = options.screen; - if (options.window) camoufoxOptions.window = options.window; - if (options.fingerprint) camoufoxOptions.fingerprint = options.fingerprint; - if (options.ff_version !== undefined) - camoufoxOptions.ff_version = options.ff_version; - if (options.headless !== undefined) - camoufoxOptions.headless = options.headless; - if (options.main_world_eval !== undefined) - camoufoxOptions.main_world_eval = options.main_world_eval; - if (options.executable_path) - camoufoxOptions.executable_path = options.executable_path; - if (options.firefox_user_prefs) - camoufoxOptions.firefox_user_prefs = options.firefox_user_prefs; - if (options.proxy) camoufoxOptions.proxy = options.proxy; - if (options.enable_cache !== undefined) - camoufoxOptions.enable_cache = options.enable_cache; - if (options.args) camoufoxOptions.args = options.args; - if (options.env) camoufoxOptions.env = options.env; - if (options.debug !== undefined) camoufoxOptions.debug = options.debug; - if (options.virtual_display) - camoufoxOptions.virtual_display = options.virtual_display; - if (options.webgl_config) - camoufoxOptions.webgl_config = options.webgl_config; - - // Handle custom options that might need mapping - if (options.timezone) { - // If timezone is provided directly, we can set it in the generated config - // This will be handled after generation - } - if (options.country) { - // Similar for country - } - if (options.geolocation) { - // Handle geolocation coordinates + // If we have a port, try to gracefully shutdown the server + if (config.port) { + try { + await fetch(`http://localhost:${config.port}/shutdown`, { + method: "POST", + signal: AbortSignal.timeout(5000), + }); + // Wait a bit for graceful shutdown + await new Promise((resolve) => setTimeout(resolve, 1000)); + } catch { + // Graceful shutdown failed, continue with force stop + } } - // Generate the configuration using camoufox-js-lsd - const generatedConfig = await launchOptions(camoufoxOptions); - - // Apply any custom overrides - if (options.timezone) { - generatedConfig.env = generatedConfig.env || {}; - // The timezone will be handled in the CAMOU_CONFIG environment variable - } - - return generatedConfig; + // Delete the configuration + deleteCamoufoxConfig(id); + return true; } catch (error) { - console.error(`Failed to generate Camoufox config: ${error}`); - throw error; + // Delete the configuration even if stopping failed + deleteCamoufoxConfig(id); + return false; } } + +/** + * Stop all Camoufox processes + * @returns Promise resolving when all instances are stopped + */ +export async function stopAllCamoufoxProcesses(): Promise { + const configs = listCamoufoxConfigs(); + + const stopPromises = configs.map((config) => stopCamoufoxProcess(config.id)); + await Promise.all(stopPromises); +} diff --git a/nodecar/src/camoufox-storage.ts b/nodecar/src/camoufox-storage.ts new file mode 100644 index 0000000..a46e74c --- /dev/null +++ b/nodecar/src/camoufox-storage.ts @@ -0,0 +1,152 @@ +import fs from "node:fs"; +import path from "node:path"; +import tmp from "tmp"; +import type { CamoufoxLaunchOptions } from "./camoufox-launcher.js"; + +export interface CamoufoxConfig { + id: string; + options: CamoufoxLaunchOptions; + profilePath?: string; + url?: string; + port?: number; + wsEndpoint?: string; +} + +const STORAGE_DIR = path.join(tmp.tmpdir, "donutbrowser", "camoufox"); + +if (!fs.existsSync(STORAGE_DIR)) { + fs.mkdirSync(STORAGE_DIR, { recursive: true }); +} + +/** + * Save a Camoufox configuration to disk + * @param config The Camoufox configuration to save + */ +export function saveCamoufoxConfig(config: CamoufoxConfig): void { + const filePath = path.join(STORAGE_DIR, `${config.id}.json`); + fs.writeFileSync(filePath, JSON.stringify(config, null, 2)); +} + +/** + * Get a Camoufox configuration by ID + * @param id The Camoufox ID + * @returns The Camoufox configuration or null if not found + */ +export function getCamoufoxConfig(id: string): CamoufoxConfig | null { + const filePath = path.join(STORAGE_DIR, `${id}.json`); + + if (!fs.existsSync(filePath)) { + return null; + } + + try { + const content = fs.readFileSync(filePath, "utf-8"); + return JSON.parse(content) as CamoufoxConfig; + } catch (error) { + console.error(`Error reading Camoufox config ${id}:`, error); + return null; + } +} + +/** + * Delete a Camoufox configuration + * @param id The Camoufox ID to delete + * @returns True if deleted, false if not found + */ +export function deleteCamoufoxConfig(id: string): boolean { + const filePath = path.join(STORAGE_DIR, `${id}.json`); + + if (!fs.existsSync(filePath)) { + return false; + } + + try { + fs.unlinkSync(filePath); + return true; + } catch (error) { + console.error(`Error deleting Camoufox config ${id}:`, error); + return false; + } +} + +/** + * List all saved Camoufox configurations + * @returns Array of Camoufox configurations + */ +export function listCamoufoxConfigs(): CamoufoxConfig[] { + if (!fs.existsSync(STORAGE_DIR)) { + return []; + } + + try { + return fs + .readdirSync(STORAGE_DIR) + .filter((file) => file.endsWith(".json")) + .map((file) => { + try { + const content = fs.readFileSync( + path.join(STORAGE_DIR, file), + "utf-8", + ); + return JSON.parse(content) as CamoufoxConfig; + } catch (error) { + console.error(`Error reading Camoufox config ${file}:`, error); + return null; + } + }) + .filter((config): config is CamoufoxConfig => config !== null); + } catch (error) { + console.error("Error listing Camoufox configs:", error); + return []; + } +} + +/** + * Update a Camoufox configuration + * @param config The Camoufox configuration to update + * @returns True if updated, false if not found + */ +export function updateCamoufoxConfig(config: CamoufoxConfig): boolean { + const filePath = path.join(STORAGE_DIR, `${config.id}.json`); + + try { + fs.readFileSync(filePath, "utf-8"); + fs.writeFileSync(filePath, JSON.stringify(config, null, 2)); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") { + console.error( + `Config ${config.id} was deleted while the app was running`, + ); + return false; + } + + console.error(`Error updating Camoufox config ${config.id}:`, error); + return false; + } +} + +/** + * Check if a Camoufox server is running + * @param port The port to check + * @returns True if running, false otherwise + */ +export async function isServerRunning(port: number): Promise { + try { + const response = await fetch(`http://localhost:${port}/json/version`, { + method: "GET", + signal: AbortSignal.timeout(1000), + }); + return response.ok; + } catch { + return false; + } +} + +/** + * Generate a unique ID for a Camoufox instance + * @returns A unique ID string + */ +export function generateCamoufoxId(): string { + return `camoufox_${Date.now()}_${Math.floor(Math.random() * 10000)}`; +} diff --git a/nodecar/src/camoufox-worker.ts b/nodecar/src/camoufox-worker.ts new file mode 100644 index 0000000..7ec8118 --- /dev/null +++ b/nodecar/src/camoufox-worker.ts @@ -0,0 +1,231 @@ +import { launchServer } from "camoufox-js"; +import getPort from "get-port"; +import type { Page } from "playwright-core"; +import { firefox } from "playwright-core"; +import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js"; + +/** + * Run a Camoufox browser server as a worker process + * @param id The Camoufox configuration ID + */ +export async function runCamoufoxWorker(id: string): Promise { + // Get the Camoufox configuration + const config = getCamoufoxConfig(id); + + if (!config) { + console.error( + JSON.stringify({ + error: "Configuration not found", + id: id, + }), + ); + process.exit(1); + } + + let server: Awaited> | null = null; + let browser: Awaited> | null = null; + + // Handle process termination gracefully + const gracefulShutdown = async () => { + try { + if (browser) { + await browser.close(); + } + if (server) { + await server.close(); + } + } catch { + // Ignore errors during shutdown + } + process.exit(0); + }; + + process.on("SIGTERM", () => void gracefulShutdown()); + process.on("SIGINT", () => void gracefulShutdown()); + + // Handle uncaught exceptions + process.on("uncaughtException", (error) => { + console.error( + JSON.stringify({ + error: "Uncaught exception", + message: error.message, + stack: error.stack, + id: id, + }), + ); + process.exit(1); + }); + + process.on("unhandledRejection", (reason) => { + console.error( + JSON.stringify({ + error: "Unhandled rejection", + reason: String(reason), + id: id, + }), + ); + process.exit(1); + }); + + // Add a timeout to prevent hanging + const startupTimeout = setTimeout(() => { + console.error( + JSON.stringify({ + error: "Startup timeout", + message: "Worker startup timeout after 30 seconds", + id: id, + }), + ); + process.exit(1); + }, 30000); + + // Start the browser server + try { + const port = await getPort(); + + // Prepare options for Camoufox + const camoufoxOptions = { ...config.options }; + + // Add profile path if provided + if (config.profilePath) { + camoufoxOptions.user_data_dir = config.profilePath; + } + + camoufoxOptions.disableTheming = true; + camoufoxOptions.showcursor = false; + + // Don't force headless mode - let the user configuration decide + if (camoufoxOptions.headless === undefined) { + camoufoxOptions.headless = false; // Default to visible for debugging + } + + try { + // Launch Camoufox server + server = await launchServer({ + ...camoufoxOptions, + port: port, + ws_path: "/camoufox", + }); + } catch (error) { + console.error( + JSON.stringify({ + error: "Failed to launch Camoufox server", + message: error instanceof Error ? error.message : String(error), + id: id, + }), + ); + process.exit(1); + } + + if (!server) { + console.error( + JSON.stringify({ + error: "Failed to launch Camoufox server", + message: + "Camoufox is not installed. Please install Camoufox first by running: npx camoufox-js fetch", + id: id, + }), + ); + process.exit(1); + } + + // Connect to the server + try { + browser = await firefox.connect(server.wsEndpoint()); + } catch (error) { + console.error( + JSON.stringify({ + error: "Failed to connect to Camoufox server", + message: error instanceof Error ? error.message : String(error), + id: id, + }), + ); + process.exit(1); + } + + // Update config with server details + config.port = port; + config.wsEndpoint = server.wsEndpoint(); + saveCamoufoxConfig(config); + + // Clear the startup timeout since we succeeded + clearTimeout(startupTimeout); + + // Output success JSON for the parent process + console.log( + JSON.stringify({ + success: true, + id: id, + port: port, + wsEndpoint: server.wsEndpoint(), + message: "Camoufox server started successfully", + }), + ); + + // Open URL if provided + if (config.url) { + try { + const page: Page = await browser.newPage(); + await page.goto(config.url); + } catch (error) { + // Don't exit here, just log the error as JSON + console.error( + JSON.stringify({ + error: "Failed to open URL", + url: config.url, + message: error instanceof Error ? error.message : String(error), + id: id, + }), + ); + } + } else { + // If no URL is provided, create a blank page to keep the browser alive + try { + await browser.newPage(); + } catch (error) { + console.error( + JSON.stringify({ + error: "Failed to create blank page", + message: error instanceof Error ? error.message : String(error), + id: id, + }), + ); + } + } + + // Keep the process alive by waiting for the browser to disconnect + browser.on("disconnected", () => { + process.exit(0); + }); + + // Keep the process alive with a simple check + const keepAlive = setInterval(async () => { + try { + // Check if browser is still connected + if (!browser || !browser.isConnected()) { + clearInterval(keepAlive); + process.exit(0); + } + } catch (error) { + // If we can't check the connection, assume it's dead + clearInterval(keepAlive); + process.exit(0); + } + }, 5000); + + // Handle process staying alive + process.stdin.resume(); + } catch (error) { + clearTimeout(startupTimeout); + console.error( + JSON.stringify({ + error: "Failed to start Camoufox worker", + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + config: config, + id: id, + }), + ); + process.exit(1); + } +} diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index 636cd9b..6dfd5dc 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -1,5 +1,10 @@ import { program } from "commander"; -import { generateCamoufoxConfig } from "./camoufox-launcher.js"; +import { + stopAllCamoufoxProcesses, + stopCamoufoxProcess, +} from "./camoufox-launcher.js"; +import { listCamoufoxConfigs } from "./camoufox-storage.js"; +import { runCamoufoxWorker } from "./camoufox-worker.js"; import { startProxyProcess, stopAllProxyProcesses, @@ -150,10 +155,13 @@ program } }); -// Command for generating Camoufox configuration +// Command for Camoufox management program - .command("camoufox-config") - .argument("", "generate Camoufox configuration") + .command("camoufox") + .argument("", "start, stop, or list Camoufox instances") + .option("--id ", "Camoufox ID for stop command") + .option("--profile-path ", "profile directory path") + .option("--url ", "URL to open") // Operating system fingerprinting .option( @@ -231,130 +239,280 @@ program // Firefox preferences .option("--firefox-prefs ", "Firefox user preferences (JSON string)") - .description("generate Camoufox configuration using camoufox-js") - .action(async (action: string, options: any) => { - try { - if (action === "generate") { - // Build Camoufox options - const camoufoxOptions: any = { - enable_cache: !options.disableCache, // Cache enabled by default - }; + // Anti-detect options + .option( + "--disable-theming", + "disable Firefox theming (required for anti-detect)", + ) + .option( + "--no-showcursor", + "disable cursor display (required for anti-detect)", + ) - // OS fingerprinting - if (options.os) { - camoufoxOptions.os = options.os.includes(",") - ? options.os.split(",") - : options.os; - } + .description("manage Camoufox browser instances") + .action( + async ( + action: string, + options: Record, + ) => { + if (action === "start") { + try { + // Build Camoufox options in the format expected by camoufox-js + const camoufoxOptions: Record = {}; - // Blocking options - if (options.blockImages) camoufoxOptions.block_images = true; - if (options.blockWebrtc) camoufoxOptions.block_webrtc = true; - if (options.blockWebgl) camoufoxOptions.block_webgl = true; - - // Security options - if (options.disableCoop) camoufoxOptions.disable_coop = true; - - // Geolocation - if (options.geoip) { - camoufoxOptions.geoip = - options.geoip === "auto" ? true : options.geoip; - } - if (options.latitude && options.longitude) { - camoufoxOptions.geolocation = { - latitude: options.latitude, - longitude: options.longitude, - accuracy: 100, - }; - } - if (options.country) camoufoxOptions.country = options.country; - if (options.timezone) camoufoxOptions.timezone = options.timezone; - - // UI and behavior - if (options.humanize) camoufoxOptions.humanize = options.humanize; - if (options.headless) camoufoxOptions.headless = true; - - // Localization - if (options.locale) { - camoufoxOptions.locale = options.locale.includes(",") - ? options.locale.split(",") - : options.locale; - } - - // Extensions and fonts - if (options.addons) camoufoxOptions.addons = options.addons.split(","); - if (options.fonts) camoufoxOptions.fonts = options.fonts.split(","); - if (options.customFontsOnly) camoufoxOptions.custom_fonts_only = true; - if (options.excludeAddons) - camoufoxOptions.exclude_addons = options.excludeAddons.split(","); - - // Screen and window - const screen: any = {}; - if (options.screenMinWidth) screen.minWidth = options.screenMinWidth; - if (options.screenMaxWidth) screen.maxWidth = options.screenMaxWidth; - if (options.screenMinHeight) screen.minHeight = options.screenMinHeight; - if (options.screenMaxHeight) screen.maxHeight = options.screenMaxHeight; - if (Object.keys(screen).length > 0) camoufoxOptions.screen = screen; - - if (options.windowWidth && options.windowHeight) { - camoufoxOptions.window = [options.windowWidth, options.windowHeight]; - } - - // Advanced options - if (options.ffVersion) camoufoxOptions.ff_version = options.ffVersion; - if (options.mainWorldEval) camoufoxOptions.main_world_eval = true; - if (options.webglVendor && options.webglRenderer) { - camoufoxOptions.webgl_config = [ - options.webglVendor, - options.webglRenderer, - ]; - } - - // Proxy - if (options.proxy) camoufoxOptions.proxy = options.proxy; - - // Environment and debugging - if (options.virtualDisplay) - camoufoxOptions.virtual_display = options.virtualDisplay; - if (options.debug) camoufoxOptions.debug = true; - if (options.args) camoufoxOptions.args = options.args.split(","); - if (options.env) { - try { - camoufoxOptions.env = JSON.parse(options.env); - } catch (e) { - console.error("Invalid JSON for --env option"); - process.exit(1); - return; + // OS fingerprinting + if (options.os && typeof options.os === "string") { + camoufoxOptions.os = options.os.includes(",") + ? options.os.split(",") + : options.os; } - } - // Firefox preferences - if (options.firefoxPrefs) { - try { - camoufoxOptions.firefox_user_prefs = JSON.parse( - options.firefoxPrefs, - ); - } catch (e) { - console.error("Invalid JSON for --firefox-prefs option"); - process.exit(1); - return; + // Blocking options + if (options.blockImages) camoufoxOptions.block_images = true; + if (options.blockWebrtc) camoufoxOptions.block_webrtc = true; + if (options.blockWebgl) camoufoxOptions.block_webgl = true; + + // Security options + if (options.disableCoop) camoufoxOptions.disable_coop = true; + + // Geolocation + if (options.geoip) { + camoufoxOptions.geoip = + options.geoip === "auto" ? true : options.geoip; } + if (options.latitude && options.longitude) { + camoufoxOptions.geolocation = { + latitude: options.latitude, + longitude: options.longitude, + accuracy: 100, + }; + } + if (options.country) camoufoxOptions.country = options.country; + if (options.timezone) camoufoxOptions.timezone = options.timezone; + + // UI and behavior + if (options.humanize) camoufoxOptions.humanize = options.humanize; + if (options.headless) camoufoxOptions.headless = true; + + // Localization + if (options.locale && typeof options.locale === "string") { + camoufoxOptions.locale = options.locale.includes(",") + ? options.locale.split(",") + : options.locale; + } + + // Extensions and fonts + if (options.addons && typeof options.addons === "string") + camoufoxOptions.addons = options.addons.split(","); + if (options.fonts && typeof options.fonts === "string") + camoufoxOptions.fonts = options.fonts.split(","); + if (options.customFontsOnly) camoufoxOptions.custom_fonts_only = true; + if ( + options.excludeAddons && + typeof options.excludeAddons === "string" + ) + camoufoxOptions.exclude_addons = options.excludeAddons.split(","); + + // Screen and window + const screen: Record = {}; + if (options.screenMinWidth) screen.minWidth = options.screenMinWidth; + if (options.screenMaxWidth) screen.maxWidth = options.screenMaxWidth; + if (options.screenMinHeight) + screen.minHeight = options.screenMinHeight; + if (options.screenMaxHeight) + screen.maxHeight = options.screenMaxHeight; + if (Object.keys(screen).length > 0) camoufoxOptions.screen = screen; + + if (options.windowWidth && options.windowHeight) { + camoufoxOptions.window = [ + options.windowWidth, + options.windowHeight, + ]; + } + + // Advanced options + if (options.ffVersion) camoufoxOptions.ff_version = options.ffVersion; + if (options.mainWorldEval) camoufoxOptions.main_world_eval = true; + if (options.webglVendor && options.webglRenderer) { + camoufoxOptions.webgl_config = [ + options.webglVendor, + options.webglRenderer, + ]; + } + + // Proxy + if (options.proxy) camoufoxOptions.proxy = options.proxy; + + // Cache and performance - default to enabled + camoufoxOptions.enable_cache = !options.disableCache; + + // Environment and debugging + if (options.virtualDisplay) + camoufoxOptions.virtual_display = options.virtualDisplay; + if (options.debug) camoufoxOptions.debug = true; + if (options.args && typeof options.args === "string") + camoufoxOptions.args = options.args.split(","); + if (options.env && typeof options.env === "string") { + try { + camoufoxOptions.env = JSON.parse(options.env); + } catch (e) { + console.error( + JSON.stringify({ + error: "Invalid JSON for --env option", + message: String(e), + }), + ); + process.exit(1); + return; + } + } + + // Firefox preferences + if ( + options.firefoxPrefs && + typeof options.firefoxPrefs === "string" + ) { + try { + camoufoxOptions.firefox_user_prefs = JSON.parse( + options.firefoxPrefs, + ); + } catch (e) { + console.error( + JSON.stringify({ + error: "Invalid JSON for --firefox-prefs option", + message: String(e), + }), + ); + process.exit(1); + return; + } + } + + // Generate a unique ID for this instance + const id = `camoufox_${Date.now()}_${Math.floor(Math.random() * 10000)}`; + + // Add profile path if provided + if (typeof options.profilePath === "string") { + camoufoxOptions.user_data_dir = options.profilePath; + } + + camoufoxOptions.disableTheming = true; + camoufoxOptions.showcursor = false; + + // Don't force headless mode - let the user configuration decide + if (camoufoxOptions.headless === undefined) { + camoufoxOptions.headless = false; // Default to visible + } + + // Use the server-based approach via launchServer + const { launchServer } = await import("camoufox-js"); + const { firefox } = await import("playwright-core"); + const getPort = (await import("get-port")).default; + + // Get an available port + const port = await getPort(); + + // Launch Camoufox server + const server = await launchServer({ + ...camoufoxOptions, + port: port, + ws_path: "/camoufox", + }); + + // Connect to the server + const browser = await firefox.connect(server.wsEndpoint()); + + // Open URL if provided + if (typeof options.url === "string") { + try { + const page = await browser.newPage(); + await page.goto(options.url); + } catch { + // Don't fail if URL opening fails + } + } else { + // Create a blank page to keep the browser alive + try { + await browser.newPage(); + } catch { + // Ignore if we can't create a page + } + } + + // Output the configuration as JSON for the Rust side to parse + console.log( + JSON.stringify({ + id: id, + port: port, + wsEndpoint: server.wsEndpoint(), + profilePath: + typeof options.profilePath === "string" + ? options.profilePath + : undefined, + url: typeof options.url === "string" ? options.url : undefined, + }), + ); + + // Keep the process alive by waiting for the browser to disconnect + browser.on("disconnected", () => { + process.exit(0); + }); + + // Keep the process alive with a simple interval + const keepAlive = setInterval(() => { + try { + if (!browser.isConnected()) { + clearInterval(keepAlive); + process.exit(0); + } + } catch { + clearInterval(keepAlive); + process.exit(0); + } + }, 5000); + + // Handle process staying alive + process.stdin.resume(); + } catch (error: unknown) { + console.error( + JSON.stringify({ + error: "Failed to start Camoufox", + message: error instanceof Error ? error.message : String(error), + }), + ); + process.exit(1); } - - // Generate configuration - const config = await generateCamoufoxConfig(camoufoxOptions); - - // Output the configuration as JSON - console.log(JSON.stringify(config, null, 2)); + } else if (action === "stop") { + if (options.id && typeof options.id === "string") { + const stopped = await stopCamoufoxProcess(options.id); + console.log(JSON.stringify({ success: stopped })); + } else { + await stopAllCamoufoxProcesses(); + console.log(JSON.stringify({ success: true })); + } + process.exit(0); + } else if (action === "list") { + const configs = listCamoufoxConfigs(); + console.log(JSON.stringify(configs)); process.exit(0); } else { - console.error("Invalid action. Use 'generate'"); + console.error("Invalid action. Use 'start', 'stop', or 'list'"); process.exit(1); } - } catch (error: unknown) { - console.error( - `Camoufox config generation failed: ${error instanceof Error ? error.message : JSON.stringify(error)}`, - ); + }, + ); + +// Command for Camoufox worker (internal use) +program + .command("camoufox-worker") + .argument("", "start a Camoufox worker") + .requiredOption("--id ", "Camoufox configuration ID") + .description("run a Camoufox worker process") + .action(async (action: string, options: { id: string }) => { + if (action === "start") { + await runCamoufoxWorker(options.id); + } else { + console.error("Invalid action for camoufox-worker. Use 'start'"); process.exit(1); } }); diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 58e281a..af8e8e6 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -13,7 +13,7 @@ use crate::browser::{create_browser, BrowserType, ProxySettings}; use crate::browser_version_service::{ BrowserVersionInfo, BrowserVersionService, BrowserVersionsResult, }; -use crate::camoufox_direct::CamoufoxConfig; +use crate::camoufox::CamoufoxConfig; use crate::download::{DownloadProgress, Downloader}; use crate::downloaded_browsers::DownloadedBrowsersRegistry; use crate::extraction::Extractor; @@ -1884,7 +1884,7 @@ impl BrowserRunner { url: Option, local_proxy_settings: Option<&ProxySettings>, ) -> Result> { - // Handle camoufox profiles specially using only the direct launcher + // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { if let Some(mut camoufox_config) = profile.camoufox_config.clone() { // Handle proxy settings for camoufox @@ -1951,8 +1951,7 @@ impl BrowserRunner { } 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_direct::CamoufoxDirectLauncher::create_test_config(); + 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; @@ -1960,8 +1959,8 @@ impl BrowserRunner { test_config }; - // Use the direct camoufox launcher - let camoufox_result = crate::camoufox_direct::launch_camoufox_profile_direct( + // Use the nodecar camoufox launcher + let camoufox_result = crate::camoufox::launch_camoufox_profile_nodecar( app_handle.clone(), profile.clone(), final_config, @@ -1969,21 +1968,16 @@ impl BrowserRunner { ) .await .map_err(|e| -> Box { - format!("Failed to launch camoufox: {e}").into() + format!("Failed to launch camoufox via nodecar: {e}").into() })?; - // Update proxy with actual PID if proxy was started - if let Some(pid) = camoufox_result.pid { - if profile.proxy_id.is_some() { - if let Err(e) = PROXY_MANAGER.update_proxy_pid(0, pid) { - println!("Warning: Failed to update proxy PID: {e}"); - } - } - } + // For server-based Camoufox, we don't have a PID but we have a port + // We'll use the port as a unique identifier for the running instance + let process_id = camoufox_result.port; // Update profile with the process info from camoufox result let mut updated_profile = profile.clone(); - updated_profile.process_id = camoufox_result.pid; + updated_profile.process_id = process_id; updated_profile.last_launch = Some(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()); // Save the updated profile @@ -2166,10 +2160,9 @@ impl BrowserRunner { url: &str, _internal_proxy_settings: Option<&ProxySettings>, ) -> Result<(), Box> { - // Handle camoufox profiles specially using only the direct launcher + // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { - let camoufox_launcher = - crate::camoufox_direct::CamoufoxDirectLauncher::new(app_handle.clone()); + let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone()); // Get the profile path based on the UUID let profiles_dir = self.get_profiles_dir(); @@ -2588,12 +2581,15 @@ impl BrowserRunner { let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers (including camoufox), check for exact profile path match - if profile.browser == "tor-browser" + if profile.browser == "camoufox" { + // Camoufox uses user_data_dir like Chromium browsers + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } else if profile.browser == "tor-browser" || profile.browser == "firefox" || profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" || profile.browser == "zen" - || profile.browser == "camoufox" { arg == profile_data_path_str || arg == format!("-profile={profile_data_path_str}") @@ -2665,7 +2661,11 @@ impl BrowserRunner { let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "tor-browser" + if profile.browser == "camoufox" { + // Camoufox uses user_data_dir like Chromium browsers + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } else if profile.browser == "tor-browser" || profile.browser == "firefox" || profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" @@ -2726,7 +2726,7 @@ impl BrowserRunner { pub fn update_camoufox_config( &self, profile_name: &str, - config: crate::camoufox_direct::CamoufoxConfig, + config: crate::camoufox::CamoufoxConfig, ) -> Result<(), Box> { // Find the profile by name let profiles = self.list_profiles()?; @@ -2758,15 +2758,14 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, ) -> Result<(), Box> { - // Handle camoufox profiles specially using only the direct launcher + // Handle camoufox profiles using nodecar launcher if profile.browser == "camoufox" { - let camoufox_launcher = - crate::camoufox_direct::CamoufoxDirectLauncher::new(app_handle.clone()); + let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle.clone()); // Try to stop by PID first (faster) if let Some(stored_pid) = profile.process_id { match camoufox_launcher - .stop_camoufox(&stored_pid.to_string()) + .stop_camoufox(&app_handle, &stored_pid.to_string()) .await { Ok(stopped) => { @@ -2791,7 +2790,10 @@ impl BrowserRunner { .await { Ok(Some(camoufox_process)) => { - match camoufox_launcher.stop_camoufox(&camoufox_process.id).await { + match camoufox_launcher + .stop_camoufox(&app_handle, &camoufox_process.id) + .await + { Ok(stopped) => { if stopped { println!( @@ -2889,7 +2891,11 @@ impl BrowserRunner { let profile_path_match = cmd.iter().any(|s| { let arg = s.to_str().unwrap_or(""); // For Firefox-based browsers, check for exact profile path match - if profile.browser == "tor-browser" + if profile.browser == "camoufox" { + // Camoufox uses user_data_dir like Chromium browsers + arg.contains(&format!("--user-data-dir={profile_data_path_str}")) + || arg == profile_data_path_str + } else if profile.browser == "tor-browser" || profile.browser == "firefox" || profile.browser == "firefox-developer" || profile.browser == "mullvad-browser" diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs new file mode 100644 index 0000000..0b65f0b --- /dev/null +++ b/src-tauri/src/camoufox.rs @@ -0,0 +1,671 @@ +use crate::browser_runner::BrowserProfile; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; + +use tauri::AppHandle; +use tauri_plugin_shell::ShellExt; +use tokio::sync::Mutex as AsyncMutex; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CamoufoxConfig { + pub os: Option>, + pub block_images: Option, + pub block_webrtc: Option, + pub block_webgl: Option, + pub disable_coop: Option, + pub geoip: Option, // Can be String or bool + pub country: Option, + pub timezone: Option, + pub latitude: Option, + pub longitude: Option, + pub humanize: Option, + pub humanize_duration: Option, + pub headless: Option, + pub locale: Option>, + pub addons: Option>, + pub fonts: Option>, + pub custom_fonts_only: Option, + pub exclude_addons: Option>, + pub screen_min_width: Option, + pub screen_max_width: Option, + pub screen_min_height: Option, + pub screen_max_height: Option, + pub window_width: Option, + pub window_height: Option, + pub ff_version: Option, + pub main_world_eval: Option, + pub webgl_vendor: Option, + pub webgl_renderer: Option, + pub proxy: Option, + pub enable_cache: Option, + pub virtual_display: Option, + pub debug: Option, + pub additional_args: Option>, + pub env_vars: Option>, + pub firefox_prefs: Option>, + pub disable_theming: Option, + pub showcursor: Option, +} + +impl Default for CamoufoxConfig { + fn default() -> Self { + Self { + os: None, + block_images: None, + block_webrtc: None, + block_webgl: None, + disable_coop: None, + geoip: None, + country: None, + timezone: None, + latitude: None, + longitude: None, + humanize: None, + humanize_duration: None, + headless: None, + locale: None, + addons: None, + fonts: None, + custom_fonts_only: None, + exclude_addons: None, + screen_min_width: None, + screen_max_width: None, + screen_min_height: None, + screen_max_height: None, + window_width: None, + window_height: None, + ff_version: None, + main_world_eval: None, + webgl_vendor: None, + webgl_renderer: None, + proxy: None, + enable_cache: Some(true), // Cache enabled by default + virtual_display: None, + debug: None, + additional_args: None, + env_vars: None, + firefox_prefs: None, + disable_theming: Some(true), // Required for anti-detect + showcursor: Some(false), // Required for anti-detect + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[allow(non_snake_case)] +pub struct CamoufoxLaunchResult { + pub id: String, + pub port: Option, + #[serde(alias = "ws_endpoint")] + pub wsEndpoint: Option, + #[serde(alias = "profile_path")] + pub profilePath: Option, + pub url: Option, +} + +#[derive(Debug)] +struct CamoufoxInstance { + #[allow(dead_code)] + id: String, + port: Option, + ws_endpoint: Option, + profile_path: Option, + url: Option, +} + +struct CamoufoxNodecarLauncherInner { + instances: HashMap, +} + +pub struct CamoufoxNodecarLauncher { + inner: Arc>, +} + +// Global singleton instance +lazy_static::lazy_static! { + static ref GLOBAL_NODECAR_LAUNCHER: CamoufoxNodecarLauncher = CamoufoxNodecarLauncher::new_singleton(); +} + +impl CamoufoxNodecarLauncher { + pub fn new(_app_handle: AppHandle) -> Self { + // Return a reference to the global singleton + GLOBAL_NODECAR_LAUNCHER.clone() + } + + pub fn new_singleton() -> Self { + Self { + inner: Arc::new(AsyncMutex::new(CamoufoxNodecarLauncherInner { + instances: HashMap::new(), + })), + } + } + + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } + + /// Create a test configuration to verify anti-fingerprinting is working + 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), + enable_cache: Some(true), + headless: Some(false), // Not headless for testing + + ..Default::default() + } + } + + /// Get the nodecar sidecar command + fn get_nodecar_sidecar( + &self, + app_handle: &AppHandle, + ) -> Result> { + let shell = app_handle.shell(); + let sidecar_command = shell + .sidecar("nodecar") + .map_err(|e| format!("Failed to create nodecar sidecar: {e}"))?; + Ok(sidecar_command) + } + + /// Launch Camoufox browser using nodecar sidecar + pub async fn launch_camoufox( + &self, + app_handle: &AppHandle, + profile_path: &str, + config: &CamoufoxConfig, + url: Option<&str>, + ) -> Result> { + // Build nodecar command arguments + let mut args = vec!["camoufox".to_string(), "start".to_string()]; + + // Add profile path + args.extend(["--profile-path".to_string(), profile_path.to_string()]); + + // Add URL if provided + if let Some(url) = url { + args.extend(["--url".to_string(), url.to_string()]); + } + + // Add configuration options + if let Some(os_list) = &config.os { + let os_str = os_list.join(","); + args.extend(["--os".to_string(), os_str]); + } + + if let Some(block_images) = config.block_images { + if block_images { + args.push("--block-images".to_string()); + } + } + + if let Some(block_webrtc) = config.block_webrtc { + if block_webrtc { + args.push("--block-webrtc".to_string()); + } + } + + if let Some(block_webgl) = config.block_webgl { + if block_webgl { + args.push("--block-webgl".to_string()); + } + } + + if let Some(disable_coop) = config.disable_coop { + if disable_coop { + args.push("--disable-coop".to_string()); + } + } + + if let Some(geoip) = &config.geoip { + match geoip { + serde_json::Value::Bool(true) => { + args.extend(["--geoip".to_string(), "auto".to_string()]); + } + serde_json::Value::String(ip) => { + args.extend(["--geoip".to_string(), ip.clone()]); + } + _ => {} + } + } + + if let Some(country) = &config.country { + args.extend(["--country".to_string(), country.clone()]); + } + + if let Some(timezone) = &config.timezone { + args.extend(["--timezone".to_string(), timezone.clone()]); + } + + if let Some(latitude) = config.latitude { + args.extend(["--latitude".to_string(), latitude.to_string()]); + } + + if let Some(longitude) = config.longitude { + args.extend(["--longitude".to_string(), longitude.to_string()]); + } + + if let Some(humanize) = config.humanize { + if humanize { + if let Some(duration) = config.humanize_duration { + args.extend(["--humanize".to_string(), duration.to_string()]); + } else { + args.push("--humanize".to_string()); + } + } + } + + if let Some(headless) = config.headless { + if headless { + args.push("--headless".to_string()); + } + } + + if let Some(locale_list) = &config.locale { + let locale_str = locale_list.join(","); + args.extend(["--locale".to_string(), locale_str]); + } + + if let Some(addons) = &config.addons { + let addons_str = addons.join(","); + args.extend(["--addons".to_string(), addons_str]); + } + + if let Some(fonts) = &config.fonts { + let fonts_str = fonts.join(","); + args.extend(["--fonts".to_string(), fonts_str]); + } + + if let Some(custom_fonts_only) = config.custom_fonts_only { + if custom_fonts_only { + args.push("--custom-fonts-only".to_string()); + } + } + + if let Some(exclude_addons) = &config.exclude_addons { + let exclude_str = exclude_addons.join(","); + args.extend(["--exclude-addons".to_string(), exclude_str]); + } + + if let Some(screen_min_width) = config.screen_min_width { + args.extend([ + "--screen-min-width".to_string(), + screen_min_width.to_string(), + ]); + } + + if let Some(screen_max_width) = config.screen_max_width { + args.extend([ + "--screen-max-width".to_string(), + screen_max_width.to_string(), + ]); + } + + if let Some(screen_min_height) = config.screen_min_height { + args.extend([ + "--screen-min-height".to_string(), + screen_min_height.to_string(), + ]); + } + + if let Some(screen_max_height) = config.screen_max_height { + args.extend([ + "--screen-max-height".to_string(), + screen_max_height.to_string(), + ]); + } + + if let Some(window_width) = config.window_width { + args.extend(["--window-width".to_string(), window_width.to_string()]); + } + + if let Some(window_height) = config.window_height { + args.extend(["--window-height".to_string(), window_height.to_string()]); + } + + if let Some(ff_version) = config.ff_version { + args.extend(["--ff-version".to_string(), ff_version.to_string()]); + } + + if let Some(main_world_eval) = config.main_world_eval { + if main_world_eval { + args.push("--main-world-eval".to_string()); + } + } + + if let Some(webgl_vendor) = &config.webgl_vendor { + args.extend(["--webgl-vendor".to_string(), webgl_vendor.clone()]); + } + + if let Some(webgl_renderer) = &config.webgl_renderer { + args.extend(["--webgl-renderer".to_string(), webgl_renderer.clone()]); + } + + if let Some(proxy) = &config.proxy { + args.extend(["--proxy".to_string(), proxy.clone()]); + } + + if let Some(enable_cache) = config.enable_cache { + if !enable_cache { + args.push("--disable-cache".to_string()); + } + } + + if let Some(virtual_display) = &config.virtual_display { + args.extend(["--virtual-display".to_string(), virtual_display.clone()]); + } + + if let Some(debug) = config.debug { + if debug { + args.push("--debug".to_string()); + } + } + + if let Some(additional_args) = &config.additional_args { + let args_str = additional_args.join(","); + args.extend(["--args".to_string(), args_str]); + } + + if let Some(env_vars) = &config.env_vars { + let env_json = serde_json::to_string(env_vars)?; + args.extend(["--env".to_string(), env_json]); + } + + if let Some(firefox_prefs) = &config.firefox_prefs { + let prefs_json = serde_json::to_string(firefox_prefs)?; + args.extend(["--firefox-prefs".to_string(), prefs_json]); + } + + // Required anti-detect options + if let Some(disable_theming) = config.disable_theming { + if disable_theming { + args.push("--disable-theming".to_string()); + } + } + + if let Some(showcursor) = config.showcursor { + if !showcursor { + args.push("--no-showcursor".to_string()); + } + } + + // Get the nodecar sidecar command + let mut sidecar_command = self.get_nodecar_sidecar(app_handle)?; + + // Add all arguments to the sidecar command + for arg in &args { + sidecar_command = sidecar_command.arg(arg); + } + + // Execute nodecar sidecar command + let output = sidecar_command.output().await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("nodecar camoufox failed: {stderr}").into()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + + // Parse the JSON output + let launch_result: CamoufoxLaunchResult = serde_json::from_str(&stdout) + .map_err(|e| format!("Failed to parse nodecar output as JSON: {e}"))?; + + // Store the instance + let instance = CamoufoxInstance { + id: launch_result.id.clone(), + port: launch_result.port, + ws_endpoint: launch_result.wsEndpoint.clone(), + profile_path: launch_result.profilePath.clone(), + url: launch_result.url.clone(), + }; + + { + let mut inner = self.inner.lock().await; + inner.instances.insert(launch_result.id.clone(), instance); + } + + Ok(launch_result) + } + + /// Stop a Camoufox process by ID + pub async fn stop_camoufox( + &self, + app_handle: &AppHandle, + id: &str, + ) -> Result> { + // Get the nodecar sidecar command + let sidecar_command = self + .get_nodecar_sidecar(app_handle)? + .arg("camoufox") + .arg("stop") + .arg("--id") + .arg(id); + + // Execute nodecar stop command + let output = sidecar_command.output().await?; + + if !output.status.success() { + let _stderr = String::from_utf8_lossy(&output.stderr); + return Ok(false); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let result: serde_json::Value = serde_json::from_str(&stdout) + .map_err(|e| format!("Failed to parse nodecar stop output: {e}"))?; + + let success = result + .get("success") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + if success { + // Remove from our tracking + let mut inner = self.inner.lock().await; + inner.instances.remove(id); + } + + Ok(success) + } + + /// Find Camoufox server by profile path (for integration with browser_runner) + pub async fn find_camoufox_by_profile( + &self, + profile_path: &str, + ) -> Result, Box> { + // First clean up any dead instances + self.cleanup_dead_instances().await?; + + let inner = self.inner.lock().await; + + // Convert paths to canonical form for comparison + let target_path = std::path::Path::new(profile_path) + .canonicalize() + .unwrap_or_else(|_| std::path::Path::new(profile_path).to_path_buf()); + + for (id, instance) in inner.instances.iter() { + if let Some(instance_profile_path) = &instance.profile_path { + let instance_path = std::path::Path::new(instance_profile_path) + .canonicalize() + .unwrap_or_else(|_| std::path::Path::new(instance_profile_path).to_path_buf()); + + if instance_path == target_path { + // Verify the server is actually running by checking the port + if let Some(port) = instance.port { + if self.is_server_running(port).await { + return Ok(Some(CamoufoxLaunchResult { + id: id.clone(), + port: instance.port, + wsEndpoint: instance.ws_endpoint.clone(), + profilePath: instance.profile_path.clone(), + url: instance.url.clone(), + })); + } + } + } + } + } + + Ok(None) + } + + /// Check if servers are still alive and clean up dead instances + pub async fn cleanup_dead_instances( + &self, + ) -> Result, Box> { + let mut dead_instances = Vec::new(); + let mut instances_to_remove = Vec::new(); + + { + let inner = self.inner.lock().await; + + for (id, instance) in inner.instances.iter() { + if let Some(port) = instance.port { + // Check if the server is still alive + if !self.is_server_running(port).await { + // Server is dead + dead_instances.push(id.clone()); + instances_to_remove.push(id.clone()); + } + } else { + // No port means it's likely a dead instance + dead_instances.push(id.clone()); + instances_to_remove.push(id.clone()); + } + } + } + + // Remove dead instances + if !instances_to_remove.is_empty() { + let mut inner = self.inner.lock().await; + for id in &instances_to_remove { + inner.instances.remove(id); + } + } + + Ok(dead_instances) + } + + /// Check if a Camoufox server is running on the given port + async fn is_server_running(&self, port: u32) -> bool { + let client = reqwest::Client::new(); + let url = format!("http://localhost:{port}/json/version"); + + match client + .get(&url) + .timeout(std::time::Duration::from_secs(1)) + .send() + .await + { + Ok(response) => response.status().is_success(), + Err(_) => false, + } + } +} + +pub async fn launch_camoufox_profile_nodecar( + app_handle: AppHandle, + profile: BrowserProfile, + config: CamoufoxConfig, + url: Option, +) -> Result { + let launcher = CamoufoxNodecarLauncher::new(app_handle.clone()); + + // Get profile path + let browser_runner = crate::browser_runner::BrowserRunner::new(); + let profiles_dir = browser_runner.get_profiles_dir(); + let profile_path = profile.get_profile_data_path(&profiles_dir); + let profile_path_str = profile_path.to_string_lossy(); + + // Check if there's already a running instance for this profile + if let Ok(Some(existing)) = launcher.find_camoufox_by_profile(&profile_path_str).await { + // If there's an existing instance, stop it first to avoid conflicts + let _ = launcher.stop_camoufox(&app_handle, &existing.id).await; + } + + // Clean up any dead instances before launching + let _ = launcher.cleanup_dead_instances().await; + + launcher + .launch_camoufox(&app_handle, &profile_path_str, &config, url.as_deref()) + .await + .map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_camoufox_config_creation() { + 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)); + } + + #[test] + fn test_default_config() { + let default_config = CamoufoxConfig::default(); + + // Verify defaults + assert_eq!(default_config.enable_cache, Some(true)); + assert_eq!(default_config.timezone, None); + assert_eq!(default_config.debug, None); + assert_eq!(default_config.headless, None); + } +} diff --git a/src-tauri/src/camoufox_direct.rs b/src-tauri/src/camoufox_direct.rs deleted file mode 100644 index 088850c..0000000 --- a/src-tauri/src/camoufox_direct.rs +++ /dev/null @@ -1,1325 +0,0 @@ -use crate::browser_runner::BrowserProfile; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::Path; -use std::process::{Child, Command, Stdio}; -use std::sync::Arc; -use sysinfo::{Pid, System}; -use tauri::AppHandle; -use tokio::sync::Mutex as AsyncMutex; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CamoufoxConfig { - pub os: Option>, - pub block_images: Option, - pub block_webrtc: Option, - pub block_webgl: Option, - pub disable_coop: Option, - pub geoip: Option, // Can be String or bool - pub country: Option, - pub timezone: Option, - pub latitude: Option, - pub longitude: Option, - pub humanize: Option, - pub humanize_duration: Option, - pub headless: Option, - pub locale: Option>, - pub addons: Option>, - pub fonts: Option>, - pub custom_fonts_only: Option, - pub exclude_addons: Option>, - pub screen_min_width: Option, - pub screen_max_width: Option, - pub screen_min_height: Option, - pub screen_max_height: Option, - pub window_width: Option, - pub window_height: Option, - pub ff_version: Option, - pub main_world_eval: Option, - pub webgl_vendor: Option, - pub webgl_renderer: Option, - pub proxy: Option, - pub enable_cache: Option, - pub virtual_display: Option, - pub debug: Option, - pub additional_args: Option>, - pub env_vars: Option>, - pub firefox_prefs: Option>, -} - -impl Default for CamoufoxConfig { - fn default() -> Self { - Self { - os: None, - block_images: None, - block_webrtc: None, - block_webgl: None, - disable_coop: None, - geoip: None, - country: None, - timezone: None, - latitude: None, - longitude: None, - humanize: None, - humanize_duration: None, - headless: None, - locale: None, - addons: None, - fonts: None, - custom_fonts_only: None, - exclude_addons: None, - screen_min_width: None, - screen_max_width: None, - screen_min_height: None, - screen_max_height: None, - window_width: None, - window_height: None, - ff_version: None, - main_world_eval: None, - webgl_vendor: None, - webgl_renderer: None, - proxy: None, - enable_cache: Some(true), // Cache enabled by default - virtual_display: None, - debug: None, - additional_args: None, - env_vars: None, - firefox_prefs: None, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[allow(non_snake_case)] -pub struct CamoufoxLaunchResult { - pub id: String, - pub pid: Option, - #[serde(alias = "executable_path")] - pub executablePath: String, - #[serde(alias = "profile_path")] - pub profilePath: String, - pub url: Option, -} - -#[derive(Debug)] -struct CamoufoxInstance { - pid: u32, - executable_path: String, - profile_path: String, - url: Option, - _child: Option, // Keep handle to prevent zombie processes -} - -struct CamoufoxDirectLauncherInner { - instances: HashMap, -} - -pub struct CamoufoxDirectLauncher { - inner: Arc>, -} - -// Global singleton instance -lazy_static::lazy_static! { - static ref GLOBAL_DIRECT_LAUNCHER: CamoufoxDirectLauncher = CamoufoxDirectLauncher::new_singleton(); -} - -impl CamoufoxDirectLauncher { - pub fn new(_app_handle: AppHandle) -> Self { - // Return a reference to the global singleton - GLOBAL_DIRECT_LAUNCHER.clone() - } - - pub fn new_singleton() -> Self { - Self { - inner: Arc::new(AsyncMutex::new(CamoufoxDirectLauncherInner { - instances: HashMap::new(), - })), - } - } - - fn clone(&self) -> Self { - Self { - inner: Arc::clone(&self.inner), - } - } - - /// Create a test configuration to verify anti-fingerprinting is working - 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), - enable_cache: Some(true), - headless: Some(false), // Not headless for testing - - ..Default::default() - } - } - - /// Generate Camoufox configuration using nodecar with camoufox-js-lsd - async fn generate_camoufox_config_with_nodecar( - &self, - config: &CamoufoxConfig, - ) -> Result> { - println!("Generating Camoufox configuration using nodecar with camoufox-js-lsd..."); - - // Build nodecar command arguments - let mut args = vec!["camoufox-config".to_string(), "generate".to_string()]; - - // Add configuration options - if let Some(os_list) = &config.os { - let os_str = os_list.join(","); - args.extend(["--os".to_string(), os_str]); - } - - if let Some(block_images) = config.block_images { - if block_images { - args.push("--block-images".to_string()); - } - } - - if let Some(block_webrtc) = config.block_webrtc { - if block_webrtc { - args.push("--block-webrtc".to_string()); - } - } - - if let Some(block_webgl) = config.block_webgl { - if block_webgl { - args.push("--block-webgl".to_string()); - } - } - - if let Some(disable_coop) = config.disable_coop { - if disable_coop { - args.push("--disable-coop".to_string()); - } - } - - if let Some(geoip) = &config.geoip { - match geoip { - serde_json::Value::Bool(true) => { - args.extend(["--geoip".to_string(), "auto".to_string()]); - } - serde_json::Value::String(ip) => { - args.extend(["--geoip".to_string(), ip.clone()]); - } - _ => {} - } - } - - if let Some(country) = &config.country { - args.extend(["--country".to_string(), country.clone()]); - } - - if let Some(timezone) = &config.timezone { - args.extend(["--timezone".to_string(), timezone.clone()]); - } - - if let Some(latitude) = config.latitude { - args.extend(["--latitude".to_string(), latitude.to_string()]); - } - - if let Some(longitude) = config.longitude { - args.extend(["--longitude".to_string(), longitude.to_string()]); - } - - if let Some(humanize) = config.humanize { - if humanize { - if let Some(duration) = config.humanize_duration { - args.extend(["--humanize".to_string(), duration.to_string()]); - } else { - args.push("--humanize".to_string()); - } - } - } - - if let Some(headless) = config.headless { - if headless { - args.push("--headless".to_string()); - } - } - - if let Some(locale_list) = &config.locale { - let locale_str = locale_list.join(","); - args.extend(["--locale".to_string(), locale_str]); - } - - if let Some(addons) = &config.addons { - let addons_str = addons.join(","); - args.extend(["--addons".to_string(), addons_str]); - } - - if let Some(fonts) = &config.fonts { - let fonts_str = fonts.join(","); - args.extend(["--fonts".to_string(), fonts_str]); - } - - if let Some(custom_fonts_only) = config.custom_fonts_only { - if custom_fonts_only { - args.push("--custom-fonts-only".to_string()); - } - } - - if let Some(exclude_addons) = &config.exclude_addons { - let exclude_str = exclude_addons.join(","); - args.extend(["--exclude-addons".to_string(), exclude_str]); - } - - if let Some(screen_min_width) = config.screen_min_width { - args.extend([ - "--screen-min-width".to_string(), - screen_min_width.to_string(), - ]); - } - - if let Some(screen_max_width) = config.screen_max_width { - args.extend([ - "--screen-max-width".to_string(), - screen_max_width.to_string(), - ]); - } - - if let Some(screen_min_height) = config.screen_min_height { - args.extend([ - "--screen-min-height".to_string(), - screen_min_height.to_string(), - ]); - } - - if let Some(screen_max_height) = config.screen_max_height { - args.extend([ - "--screen-max-height".to_string(), - screen_max_height.to_string(), - ]); - } - - if let Some(window_width) = config.window_width { - args.extend(["--window-width".to_string(), window_width.to_string()]); - } - - if let Some(window_height) = config.window_height { - args.extend(["--window-height".to_string(), window_height.to_string()]); - } - - if let Some(ff_version) = config.ff_version { - args.extend(["--ff-version".to_string(), ff_version.to_string()]); - } - - if let Some(main_world_eval) = config.main_world_eval { - if main_world_eval { - args.push("--main-world-eval".to_string()); - } - } - - if let Some(webgl_vendor) = &config.webgl_vendor { - args.extend(["--webgl-vendor".to_string(), webgl_vendor.clone()]); - } - - if let Some(webgl_renderer) = &config.webgl_renderer { - args.extend(["--webgl-renderer".to_string(), webgl_renderer.clone()]); - } - - if let Some(proxy) = &config.proxy { - args.extend(["--proxy".to_string(), proxy.clone()]); - } - - if let Some(enable_cache) = config.enable_cache { - if !enable_cache { - args.push("--disable-cache".to_string()); - } - } - - if let Some(virtual_display) = &config.virtual_display { - args.extend(["--virtual-display".to_string(), virtual_display.clone()]); - } - - if let Some(debug) = config.debug { - if debug { - args.push("--debug".to_string()); - } - } - - if let Some(additional_args) = &config.additional_args { - let args_str = additional_args.join(","); - args.extend(["--args".to_string(), args_str]); - } - - if let Some(env_vars) = &config.env_vars { - let env_json = serde_json::to_string(env_vars)?; - args.extend(["--env".to_string(), env_json]); - } - - if let Some(firefox_prefs) = &config.firefox_prefs { - let prefs_json = serde_json::to_string(firefox_prefs)?; - args.extend(["--firefox-prefs".to_string(), prefs_json]); - } - - // Get the nodecar binary path - let nodecar_path = self.get_nodecar_binary_path()?; - - println!("Executing nodecar command: {nodecar_path:?} with args: {args:?}"); - - // Execute nodecar command - let output = tokio::process::Command::new(nodecar_path) - .args(&args) - .output() - .await?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(format!("nodecar camoufox-config failed: {stderr}").into()); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - println!("nodecar output: {stdout}"); - - // Parse the JSON output - let config_json: serde_json::Value = serde_json::from_str(&stdout) - .map_err(|e| format!("Failed to parse nodecar output as JSON: {e}"))?; - - Ok(config_json) - } - - /// Get the path to the nodecar binary - fn get_nodecar_binary_path( - &self, - ) -> Result> { - // Try to find nodecar binary in the same directory as the current executable - let current_exe = std::env::current_exe()?; - let exe_dir = current_exe - .parent() - .ok_or("Failed to get executable directory")?; - - // Check for nodecar in the same directory - let nodecar_path = exe_dir.join("nodecar"); - if nodecar_path.exists() { - return Ok(nodecar_path); - } - - // Check for nodecar with .exe extension on Windows - #[cfg(target_os = "windows")] - { - let nodecar_exe_path = exe_dir.join("nodecar.exe"); - if nodecar_exe_path.exists() { - return Ok(nodecar_exe_path); - } - } - - // Fallback to system PATH - Ok(std::path::PathBuf::from("nodecar")) - } - - /// Build the CAMOU_CONFIG JSON from CamoufoxConfig (fallback method) - fn build_camou_config(&self, config: &CamoufoxConfig) -> serde_json::Value { - let mut camou_config = serde_json::Map::new(); - - // Always set some basic anti-fingerprinting defaults to ensure the system works - camou_config.insert("debug".to_string(), serde_json::Value::Bool(true)); // Enable debug for troubleshooting - - // Set some default values that should always work to test the system - if config.timezone.is_none() { - camou_config.insert( - "timezone".to_string(), - serde_json::Value::String("America/New_York".to_string()), - ); - } - - // Set default screen size if not specified - if config.screen_min_width.is_none() { - camou_config.insert( - "screen.width".to_string(), - serde_json::Value::Number(1920.into()), - ); - camou_config.insert( - "screen.availWidth".to_string(), - serde_json::Value::Number(1920.into()), - ); - } - if config.screen_min_height.is_none() { - camou_config.insert( - "screen.height".to_string(), - serde_json::Value::Number(1080.into()), - ); - camou_config.insert( - "screen.availHeight".to_string(), - serde_json::Value::Number(1080.into()), - ); - } - - // Set default window size if not specified - if config.window_width.is_none() { - camou_config.insert( - "window.outerWidth".to_string(), - serde_json::Value::Number(1366.into()), - ); - camou_config.insert( - "window.innerWidth".to_string(), - serde_json::Value::Number(1350.into()), - ); - } - if config.window_height.is_none() { - camou_config.insert( - "window.outerHeight".to_string(), - serde_json::Value::Number(768.into()), - ); - camou_config.insert( - "window.innerHeight".to_string(), - serde_json::Value::Number(668.into()), - ); - } - - // Screen dimensions - use proper camoufox format - if let Some(width) = config.screen_min_width { - camou_config.insert( - "screen.width".to_string(), - serde_json::Value::Number(width.into()), - ); - camou_config.insert( - "screen.availWidth".to_string(), - serde_json::Value::Number(width.into()), - ); - } - if let Some(height) = config.screen_min_height { - camou_config.insert( - "screen.height".to_string(), - serde_json::Value::Number(height.into()), - ); - camou_config.insert( - "screen.availHeight".to_string(), - serde_json::Value::Number(height.into()), - ); - } - - // Window dimensions - use proper camoufox format - if let Some(width) = config.window_width { - camou_config.insert( - "window.outerWidth".to_string(), - serde_json::Value::Number(width.into()), - ); - camou_config.insert( - "window.innerWidth".to_string(), - serde_json::Value::Number((width.saturating_sub(16)).into()), // Account for scrollbar - ); - } - if let Some(height) = config.window_height { - camou_config.insert( - "window.outerHeight".to_string(), - serde_json::Value::Number(height.into()), - ); - camou_config.insert( - "window.innerHeight".to_string(), - serde_json::Value::Number((height.saturating_sub(100)).into()), // Account for browser chrome - ); - } - - // Geolocation - use proper camoufox format (colon notation) - if let Some(latitude) = config.latitude { - camou_config.insert( - "geolocation:latitude".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(latitude).unwrap_or(serde_json::Number::from(0)), - ), - ); - } - if let Some(longitude) = config.longitude { - camou_config.insert( - "geolocation:longitude".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(longitude).unwrap_or(serde_json::Number::from(0)), - ), - ); - } - - // Timezone - use proper camoufox format - if let Some(timezone) = &config.timezone { - camou_config.insert( - "timezone".to_string(), - serde_json::Value::String(timezone.clone()), - ); - } - - // Locale - use proper camoufox format (colon notation) - if let Some(locale_list) = &config.locale { - if let Some(first_locale) = locale_list.first() { - // Parse locale (e.g., "en-US" -> language: "en", region: "US") - let parts: Vec<&str> = first_locale.split('-').collect(); - if parts.len() >= 2 { - camou_config.insert( - "locale:language".to_string(), - serde_json::Value::String(parts[0].to_string()), - ); - camou_config.insert( - "locale:region".to_string(), - serde_json::Value::String(parts[1].to_string()), - ); - } - - // Set the full locale - camou_config.insert( - "locale:all".to_string(), - serde_json::Value::String(first_locale.clone()), - ); - - // Set navigator language properties - camou_config.insert( - "navigator.language".to_string(), - serde_json::Value::String(first_locale.clone()), - ); - - // Set Accept-Language header - camou_config.insert( - "headers.Accept-Language".to_string(), - serde_json::Value::String(first_locale.clone()), - ); - - // Convert to languages array for navigator.languages - let languages: Vec = locale_list - .iter() - .map(|l| serde_json::Value::String(l.clone())) - .collect(); - camou_config.insert( - "navigator.languages".to_string(), - serde_json::Value::Array(languages), - ); - } - } - - // WebGL - use proper camoufox format (colon notation) - if let Some(vendor) = &config.webgl_vendor { - camou_config.insert( - "webGl:vendor".to_string(), - serde_json::Value::String(vendor.clone()), - ); - } - if let Some(renderer) = &config.webgl_renderer { - camou_config.insert( - "webGl:renderer".to_string(), - serde_json::Value::String(renderer.clone()), - ); - } - - // Fonts - use proper camoufox format - if let Some(fonts) = &config.fonts { - let font_values: Vec = fonts - .iter() - .map(|f| serde_json::Value::String(f.clone())) - .collect(); - camou_config.insert("fonts".to_string(), serde_json::Value::Array(font_values)); - } - - // Custom fonts only - if let Some(custom_fonts_only) = config.custom_fonts_only { - camou_config.insert( - "customFontsOnly".to_string(), - serde_json::Value::Bool(custom_fonts_only), - ); - } - - // Humanization - use proper camoufox format (colon notation) - if let Some(humanize) = config.humanize { - camou_config.insert("humanize".to_string(), serde_json::Value::Bool(humanize)); - if let Some(duration) = config.humanize_duration { - camou_config.insert( - "humanize:maxTime".to_string(), - serde_json::Value::Number( - serde_json::Number::from_f64(duration * 1000.0).unwrap_or(serde_json::Number::from(0)), // Convert to milliseconds - ), - ); - } - } - - // Debug mode - if let Some(debug) = config.debug { - camou_config.insert("debug".to_string(), serde_json::Value::Bool(debug)); - } - - // Main world evaluation - if let Some(main_world_eval) = config.main_world_eval { - camou_config.insert( - "allowMainWorld".to_string(), - serde_json::Value::Bool(main_world_eval), - ); - } - - // Addons - if let Some(addons) = &config.addons { - let addon_values: Vec = addons - .iter() - .map(|a| serde_json::Value::String(a.clone())) - .collect(); - camou_config.insert("addons".to_string(), serde_json::Value::Array(addon_values)); - } - - // Exclude addons - if let Some(exclude_addons) = &config.exclude_addons { - let exclude_addon_values: Vec = exclude_addons - .iter() - .map(|a| serde_json::Value::String(a.clone())) - .collect(); - camou_config.insert( - "excludeAddons".to_string(), - serde_json::Value::Array(exclude_addon_values), - ); - } - - // Block features - if let Some(block_images) = config.block_images { - camou_config.insert( - "blockImages".to_string(), - serde_json::Value::Bool(block_images), - ); - } - if let Some(block_webrtc) = config.block_webrtc { - camou_config.insert( - "blockWebRTC".to_string(), - serde_json::Value::Bool(block_webrtc), - ); - } - if let Some(block_webgl) = config.block_webgl { - camou_config.insert( - "blockWebGL".to_string(), - serde_json::Value::Bool(block_webgl), - ); - } - - // COOP disable - if let Some(disable_coop) = config.disable_coop { - camou_config.insert( - "disableCOOP".to_string(), - serde_json::Value::Bool(disable_coop), - ); - } - - // GeoIP - if let Some(geoip) = &config.geoip { - camou_config.insert("geoip".to_string(), geoip.clone()); - } - - // Country - if let Some(country) = &config.country { - camou_config.insert( - "country".to_string(), - serde_json::Value::String(country.clone()), - ); - } - - // Firefox version - if let Some(ff_version) = config.ff_version { - camou_config.insert( - "ffVersion".to_string(), - serde_json::Value::Number(ff_version.into()), - ); - } - - // Enable cache - if let Some(enable_cache) = config.enable_cache { - camou_config.insert( - "enableCache".to_string(), - serde_json::Value::Bool(enable_cache), - ); - } - - // Proxy configuration - if let Some(proxy) = &config.proxy { - camou_config.insert( - "proxy".to_string(), - serde_json::Value::String(proxy.clone()), - ); - } - - // Firefox preferences - if let Some(firefox_prefs) = &config.firefox_prefs { - let mut prefs_obj = serde_json::Map::new(); - for (key, value) in firefox_prefs { - prefs_obj.insert(key.clone(), value.clone()); - } - camou_config.insert( - "firefoxPrefs".to_string(), - serde_json::Value::Object(prefs_obj), - ); - } - - camou_config.insert("showcursor".to_string(), serde_json::Value::Bool(false)); - - camou_config.insert("disableTheming".to_string(), serde_json::Value::Bool(true)); - - let final_config = serde_json::Value::Object(camou_config); - println!( - "Built CAMOU_CONFIG: {}", - serde_json::to_string_pretty(&final_config).unwrap_or_default() - ); - - // Validate that we have some basic anti-fingerprinting settings - let config_obj = final_config.as_object().unwrap(); - let has_timezone = config_obj.contains_key("timezone"); - let has_screen = - config_obj.contains_key("screen.width") || config_obj.contains_key("screen.height"); - let has_window = - config_obj.contains_key("window.outerWidth") || config_obj.contains_key("window.outerHeight"); - - println!("Anti-fingerprinting validation:"); - println!(" - Has timezone: {has_timezone}"); - println!(" - Has screen dimensions: {has_screen}"); - println!(" - Has window dimensions: {has_window}"); - - if !has_timezone && !has_screen && !has_window { - println!( - "WARNING: No anti-fingerprinting settings detected! Camoufox may not work as expected." - ); - } - - final_config - } - - /// Launch Camoufox browser with the specified configuration using direct process management - pub async fn launch_camoufox( - &self, - executable_path: &str, - profile_path: &str, - config: &CamoufoxConfig, - url: Option<&str>, - ) -> Result> { - println!("Launching Camoufox directly with executable: {executable_path}"); - println!("Profile path: {profile_path}"); - println!("URL: {url:?}"); - - // Generate unique ID for this instance - let instance_id = uuid::Uuid::new_v4().to_string(); - - // Try to generate configuration using nodecar first, fallback to manual build - let camou_config_json = match self.generate_camoufox_config_with_nodecar(config).await { - Ok(config) => { - println!("✅ Successfully generated Camoufox config using nodecar with camoufox-js-lsd"); - config - } - Err(e) => { - println!("⚠️ Failed to generate config with nodecar, falling back to manual build: {e}"); - self.build_camou_config(config) - } - }; - - // Build command arguments - let mut args = vec!["-profile".to_string(), profile_path.to_string()]; - - // Add URL if provided - if let Some(url) = url { - args.push(url.to_string()); - } - - // Add headless mode if specified - // if config.headless.unwrap_or(false) { - // args.push("-headless".to_string()); - // } - - // Add additional arguments - if let Some(additional_args) = &config.additional_args { - args.extend(additional_args.clone()); - } - - // Extract the env object from the generated config if it exists - let mut final_env_vars = std::collections::HashMap::new(); - if let Some(env_obj) = camou_config_json.get("env") { - if let Some(env_map) = env_obj.as_object() { - for (key, value) in env_map { - let value_str = match value { - serde_json::Value::String(s) => s.clone(), - serde_json::Value::Number(n) => n.to_string(), - serde_json::Value::Bool(b) => b.to_string(), - _ => value.to_string(), - }; - final_env_vars.insert(key.clone(), value_str); - } - } - } - - // Add user-specified environment variables (they override generated ones) - if let Some(user_env_vars) = &config.env_vars { - for (key, value) in user_env_vars { - final_env_vars.insert(key.clone(), value.clone()); - } - } - - // Remove the env key from the config JSON since we'll set it as actual env vars - let mut config_for_env = camou_config_json.clone(); - if let Some(config_obj) = config_for_env.as_object_mut() { - config_obj.remove("env"); - } - let camou_config_str = config_for_env.to_string(); - - // Set CAMOU_CONFIG environment variable - this is crucial for anti-fingerprinting - println!("Setting CAMOU_CONFIG environment variable: {camou_config_str}"); - - // Build environment variables - let mut cmd = Command::new(executable_path); - cmd.args(&args); - - // Don't suppress stderr in debug mode so we can see Camoufox error messages - if config.debug.unwrap_or(false) { - println!("Debug mode enabled - keeping stderr output for troubleshooting"); - } else { - cmd.stdout(Stdio::null()); - cmd.stderr(Stdio::null()); - } - - // CRITICAL: Add cache-busting environment variables to force Camoufox config refresh - // This works around the std::call_once limitation in MaskConfig.hpp - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos(); - - let cache_buster = format!("{}_{}", std::process::id(), timestamp); - - // Multiple cache-busting strategies to ensure config refresh - cmd.env("CAMOU_CACHE_INVALIDATE", &cache_buster); - cmd.env("CAMOU_CONFIG_REFRESH", timestamp.to_string()); - cmd.env("CAMOU_PROCESS_ISOLATION", &cache_buster); - - // Force Camoufox to treat this as a completely new process context - cmd.env("CAMOU_FORCE_CONFIG_RELOAD", "1"); - cmd.env("CAMOU_DISABLE_CONFIG_CACHE", "1"); - - println!("Setting cache-busting environment variables with timestamp: {timestamp}"); - - // Check if the config string is too large for a single environment variable - const MAX_ENV_SIZE: usize = 2000; - - if camou_config_str.len() > MAX_ENV_SIZE { - // Split into multiple environment variables - let chunks: Vec<&str> = camou_config_str - .as_bytes() - .chunks(MAX_ENV_SIZE) - .map(|chunk| std::str::from_utf8(chunk).unwrap_or("")) - .collect(); - - for (i, chunk) in chunks.iter().enumerate() { - let env_name = format!("CAMOU_CONFIG_{}", i + 1); - println!( - "Setting {} (chunk {} of {}): {} bytes", - env_name, - i + 1, - chunks.len(), - chunk.len() - ); - cmd.env(&env_name, chunk); - } - } else { - // Use single environment variable - cmd.env("CAMOU_CONFIG", &camou_config_str); - } - - // Set working directory to the executable's directory for better compatibility - if let Some(parent_dir) = std::path::Path::new(executable_path).parent() { - cmd.current_dir(parent_dir); - println!("Set working directory to: {parent_dir:?}"); - } - - // Set all environment variables from the generated config - for (key, value) in &final_env_vars { - println!("Setting generated environment variable: {key}={value}"); - cmd.env(key, value); - } - - // Add user-specified environment variables (they override generated ones) - if let Some(user_env_vars) = &config.env_vars { - for (key, value) in user_env_vars { - println!("Setting user environment variable: {key}={value}"); - cmd.env(key, value); - } - } - - // Set virtual display if specified - if let Some(virtual_display) = &config.virtual_display { - println!("Setting DISPLAY environment variable: {virtual_display}"); - cmd.env("DISPLAY", virtual_display); - } - - // Debug: Print launch information - println!("=== Camoufox Launch Debug Info ==="); - println!("Executable: {executable_path}"); - println!("Arguments: {args:?}"); - println!("CAMOU_CONFIG length: {} bytes", camou_config_str.len()); - - // Verify the JSON is valid - match serde_json::from_str::(&camou_config_str) { - Ok(parsed) => { - println!("✅ CAMOU_CONFIG JSON is valid"); - if let Some(obj) = parsed.as_object() { - println!("📊 Config contains {} keys:", obj.len()); - for key in obj.keys() { - println!(" - {key}"); - } - } - } - Err(e) => { - println!("❌ CAMOU_CONFIG JSON is invalid: {e}"); - } - } - - // Launch the process - let child = cmd - .spawn() - .map_err(|e| format!("Failed to launch Camoufox process: {e}"))?; - - let pid = child.id(); - println!("Launched Camoufox with PID: {pid}"); - - // Store the instance - let instance = CamoufoxInstance { - pid, - executable_path: executable_path.to_string(), - profile_path: profile_path.to_string(), - url: url.map(|u| u.to_string()), - _child: Some(child), - }; - - { - let mut inner = self.inner.lock().await; - inner.instances.insert(instance_id.clone(), instance); - } - - // Return launch result - Ok(CamoufoxLaunchResult { - id: instance_id, - pid: Some(pid), - executablePath: executable_path.to_string(), - profilePath: profile_path.to_string(), - url: url.map(|u| u.to_string()), - }) - } - - /// Stop a Camoufox process by ID - pub async fn stop_camoufox( - &self, - id: &str, - ) -> Result> { - println!("Stopping Camoufox process with ID: {id}"); - - let instance = { - let mut inner = self.inner.lock().await; - inner.instances.remove(id) - }; - - if let Some(mut instance) = instance { - // Try to kill the process gracefully first - let system = System::new_all(); - if let Some(process) = system.process(Pid::from(instance.pid as usize)) { - if process.kill() { - println!( - "Successfully killed Camoufox process: {id} (PID: {})", - instance.pid - ); - } else { - println!( - "Failed to kill Camoufox process: {id} (PID: {})", - instance.pid - ); - } - } - - // Also try to kill the child process if we still have a handle - if let Some(ref mut child) = instance._child { - let _ = child.kill(); - } - - Ok(true) - } else { - println!("Camoufox process with ID {id} not found"); - Ok(false) - } - } - - /// Find Camoufox process by profile path (for integration with browser_runner) - pub async fn find_camoufox_by_profile( - &self, - profile_path: &str, - ) -> Result, Box> { - println!("Looking for Camoufox process with profile path: {profile_path}"); - - let inner = self.inner.lock().await; - - // Convert paths to canonical form for comparison - let target_path = Path::new(profile_path) - .canonicalize() - .unwrap_or_else(|_| Path::new(profile_path).to_path_buf()); - - for (id, instance) in inner.instances.iter() { - let instance_path = Path::new(&instance.profile_path) - .canonicalize() - .unwrap_or_else(|_| Path::new(&instance.profile_path).to_path_buf()); - - if instance_path == target_path { - println!("Found match using canonical path comparison"); - return Ok(Some(CamoufoxLaunchResult { - id: id.clone(), - pid: Some(instance.pid), - executablePath: instance.executable_path.clone(), - profilePath: instance.profile_path.clone(), - url: instance.url.clone(), - })); - } - } - - println!("No matching Camoufox process found for profile path: {profile_path}"); - Ok(None) - } - - /// Check if processes are still alive and clean up dead instances - pub async fn cleanup_dead_instances( - &self, - ) -> Result, Box> { - let mut dead_instances = Vec::new(); - let mut instances_to_remove = Vec::new(); - - { - let inner = self.inner.lock().await; - let system = System::new_all(); - - for (id, instance) in inner.instances.iter() { - // Check if the process is still alive - if let Some(_process) = system.process(Pid::from(instance.pid as usize)) { - // Process is still alive - continue; - } else { - // Process is dead - println!( - "Detected dead Camoufox instance: {} (PID: {})", - id, instance.pid - ); - dead_instances.push(id.clone()); - instances_to_remove.push(id.clone()); - } - } - } - - // Remove dead instances - if !instances_to_remove.is_empty() { - let mut inner = self.inner.lock().await; - for id in &instances_to_remove { - inner.instances.remove(id); - } - println!( - "Cleaned up {} dead Camoufox instances", - instances_to_remove.len() - ); - } - - Ok(dead_instances) - } -} - -pub async fn launch_camoufox_profile_direct( - app_handle: AppHandle, - profile: BrowserProfile, - config: CamoufoxConfig, - url: Option, -) -> Result { - let launcher = CamoufoxDirectLauncher::new(app_handle); - - // Get the executable path for Camoufox - let browser_runner = crate::browser_runner::BrowserRunner::new(); - let binaries_dir = browser_runner.get_binaries_dir(); - let browser_dir = binaries_dir.join("camoufox").join(&profile.version); - - // Get executable path - let browser = crate::browser::create_browser(crate::browser::BrowserType::Camoufox); - let executable_path = browser - .get_executable_path(&browser_dir) - .map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?; - - // Get profile path - let profiles_dir = browser_runner.get_profiles_dir(); - let profile_path = profile.get_profile_data_path(&profiles_dir); - - launcher - .launch_camoufox( - &executable_path.to_string_lossy(), - &profile_path.to_string_lossy(), - &config, - url.as_deref(), - ) - .await - .map_err(|e| format!("Failed to launch Camoufox: {e}")) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_nodecar_config_generation() { - let launcher = CamoufoxDirectLauncher::new_singleton(); - - // Test with empty config (should generate random config) - let empty_config = CamoufoxConfig::default(); - let empty_result = launcher - .generate_camoufox_config_with_nodecar(&empty_config) - .await; - - match empty_result { - Ok(config) => { - println!("✅ Empty config test passed"); - - // Check if it has essential properties - if let Some(obj) = config.as_object() { - let has_navigator_ua = obj.contains_key("navigator.userAgent"); - let has_screen_width = obj.contains_key("screen.width"); - let has_timezone = obj.contains_key("timezone"); - - // At least one of these should be present in a valid config - assert!( - has_navigator_ua || has_screen_width || has_timezone, - "Generated config should have at least one fingerprinting property" - ); - } - } - Err(e) => { - // This is expected if nodecar is not available in test environment - println!("⚠️ Nodecar not available in test environment: {e}"); - } - } - - // Test with configured values - let test_config = CamoufoxDirectLauncher::create_test_config(); - let test_result = launcher - .generate_camoufox_config_with_nodecar(&test_config) - .await; - - match test_result { - Ok(config) => { - println!("✅ Test config generation passed"); - - // Verify the config is valid JSON - assert!( - config.is_object(), - "Generated config should be a JSON object" - ); - - // Check if user settings might be respected (this depends on nodecar being available) - if let Some(obj) = config.as_object() { - // At least verify we got a valid config structure - assert!(!obj.is_empty(), "Generated config should not be empty"); - } - } - Err(e) => { - println!("⚠️ Nodecar not available for test config: {e}"); - } - } - } - - #[test] - fn test_camoufox_config_creation() { - let test_config = CamoufoxDirectLauncher::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)); - } - - #[test] - fn test_fallback_config_generation() { - let launcher = CamoufoxDirectLauncher::new_singleton(); - - let test_config = CamoufoxDirectLauncher::create_test_config(); - let fallback_config = launcher.build_camou_config(&test_config); - - // Verify fallback config structure - assert!( - fallback_config.is_object(), - "Fallback config should be a JSON object" - ); - - let config_obj = fallback_config.as_object().unwrap(); - - // Check essential anti-fingerprinting properties - assert!(config_obj.contains_key("timezone"), "Should have timezone"); - assert!( - config_obj.contains_key("screen.width"), - "Should have screen width" - ); - assert!( - config_obj.contains_key("window.outerWidth"), - "Should have window width" - ); - assert!(config_obj.contains_key("debug"), "Should have debug flag"); - - // Verify specific values - assert_eq!( - config_obj.get("timezone").unwrap().as_str().unwrap(), - "Europe/London" - ); - assert_eq!( - config_obj.get("screen.width").unwrap().as_u64().unwrap(), - 1440 - ); - assert_eq!( - config_obj - .get("window.outerWidth") - .unwrap() - .as_u64() - .unwrap(), - 1200 - ); - } - - #[test] - fn test_default_config() { - let default_config = CamoufoxConfig::default(); - - // Verify defaults - assert_eq!(default_config.enable_cache, Some(true)); - assert_eq!(default_config.timezone, None); - assert_eq!(default_config.debug, None); - assert_eq!(default_config.headless, None); - } -} diff --git a/src-tauri/src/default_browser.rs b/src-tauri/src/default_browser.rs index ef24bbd..9ded19d 100644 --- a/src-tauri/src/default_browser.rs +++ b/src-tauri/src/default_browser.rs @@ -545,112 +545,3 @@ pub async fn open_url_with_profile( println!("Successfully opened URL '{url}' with profile '{profile_name}'"); Ok(()) } - -#[tauri::command] -pub async fn smart_open_url( - app_handle: tauri::AppHandle, - url: String, - _is_startup: Option, -) -> Result { - use crate::browser_runner::BrowserRunner; - - let runner = BrowserRunner::new(); - - // Get all profiles - let profiles = runner - .list_profiles() - .map_err(|e| format!("Failed to list profiles: {e}"))?; - - if profiles.is_empty() { - return Err("no_profiles".to_string()); - } - - println!( - "URL opening - Total profiles: {}, checking for running profiles", - profiles.len() - ); - - // Check for running profiles and find the first one that can handle URLs - for profile in &profiles { - // Check if this profile is running - let is_running = runner - .check_browser_status(app_handle.clone(), profile) - .await - .unwrap_or(false); - - if is_running { - println!( - "Found running profile '{}', attempting to open URL", - profile.name - ); - - // For TOR browser: Check if any other TOR browser is running - if profile.browser == "tor-browser" { - let mut other_tor_running = false; - for p in &profiles { - if p.browser == "tor-browser" - && p.name != profile.name - && runner - .check_browser_status(app_handle.clone(), p) - .await - .unwrap_or(false) - { - other_tor_running = true; - break; - } - } - - if other_tor_running { - continue; // Skip this one, can't have multiple TOR instances - } - } - - // For Mullvad browser: Check if any other Mullvad browser is running - if profile.browser == "mullvad-browser" { - let mut other_mullvad_running = false; - for p in &profiles { - if p.browser == "mullvad-browser" - && p.name != profile.name - && runner - .check_browser_status(app_handle.clone(), p) - .await - .unwrap_or(false) - { - other_mullvad_running = true; - break; - } - } - - if other_mullvad_running { - continue; // Skip this one, can't have multiple Mullvad instances - } - } - - // Try to open the URL with this running profile - match runner - .launch_or_open_url(app_handle.clone(), profile, Some(url.clone()), None) - .await - { - Ok(_) => { - println!( - "Successfully opened URL '{}' with running profile '{}'", - url, profile.name - ); - return Ok(format!("opened_with_profile:{}", profile.name)); - } - Err(e) => { - println!( - "Failed to open URL with running profile '{}': {}", - profile.name, e - ); - // Continue to try other profiles or show selector - } - } - } - } - - println!("No suitable running profiles found, showing profile selector"); - - // No suitable running profile found, show the profile selector - Err("show_selector".to_string()) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8ca48fc..7255975 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,7 +13,7 @@ mod auto_updater; mod browser; mod browser_runner; mod browser_version_service; -mod camoufox_direct; +mod camoufox; mod default_browser; mod download; mod downloaded_browsers; @@ -44,9 +44,7 @@ use settings_manager::{ save_app_settings, save_table_sorting_settings, should_show_settings_on_startup, }; -use default_browser::{ - is_default_browser, open_url_with_profile, set_as_default_browser, smart_open_url, -}; +use default_browser::{is_default_browser, open_url_with_profile, set_as_default_browser}; use version_updater::{ get_version_update_status, get_version_updater, trigger_manual_version_update, @@ -179,7 +177,7 @@ async fn delete_stored_proxy(proxy_id: String) -> Result<(), String> { #[tauri::command] async fn update_camoufox_config( profile_name: String, - config: crate::camoufox_direct::CamoufoxConfig, + config: crate::camoufox::CamoufoxConfig, ) -> Result<(), String> { let browser_runner = browser_runner::BrowserRunner::new(); browser_runner @@ -337,6 +335,27 @@ pub fn run() { auto_updater::check_for_updates_with_progress(app_handle_auto_updater).await; }); + // Handle any pending URLs that were received before the window was ready + let handle_pending = handle.clone(); + tauri::async_runtime::spawn(async move { + // Wait a bit for the window to be fully ready + tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await; + + let pending_urls = { + let mut pending = PENDING_URLS.lock().unwrap(); + let urls = pending.clone(); + pending.clear(); + urls + }; + + for url in pending_urls { + println!("Processing pending URL: {url}"); + if let Err(e) = handle_url_open(handle_pending.clone(), url).await { + eprintln!("Failed to handle pending URL: {e}"); + } + } + }); + // Start periodic cleanup task for unused binaries tauri::async_runtime::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(300)); // Every 5 minutes @@ -382,7 +401,7 @@ pub fn run() { // Start Camoufox cleanup task let app_handle_cleanup = app.handle().clone(); tauri::async_runtime::spawn(async move { - let launcher = crate::camoufox_direct::CamoufoxDirectLauncher::new(app_handle_cleanup); + let launcher = crate::camoufox::CamoufoxNodecarLauncher::new(app_handle_cleanup); let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5)); loop { @@ -469,7 +488,6 @@ pub fn run() { is_default_browser, open_url_with_profile, set_as_default_browser, - smart_open_url, trigger_manual_version_update, get_version_update_status, check_for_browser_updates, diff --git a/src/app/page.tsx b/src/app/page.tsx index a4488fc..3efe4f9 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -172,19 +172,9 @@ export default function Home() { setProcessingUrls((prev) => new Set(prev).add(url)); try { - // Use smart profile selection - const result = await invoke("smart_open_url", { - url, - }); - console.log("Smart URL opening succeeded:", result); - // URL was handled successfully, no need to show selector - } catch (error: unknown) { - console.log( - "Smart URL opening failed or requires profile selection:", - error, - ); + console.log("URL received for opening:", url); - // Show profile selector for manual selection + // Always show profile selector for manual selection - never auto-open // Replace any existing pending URL with the new one setPendingUrls([{ id: Date.now().toString(), url }]); } finally { @@ -238,11 +228,12 @@ export default function Home() { useAppUpdateNotifications(); - // For some reason, app.deep_link().get_current() is not working properly + // Check for startup URLs but only process them once const checkCurrentUrl = useCallback(async () => { try { const currentUrl = await getCurrent(); if (currentUrl && currentUrl.length > 0) { + console.log("Startup URL detected:", currentUrl[0]); void handleUrlOpen(currentUrl[0]); } } catch (error) { @@ -315,7 +306,7 @@ export default function Home() { // Listen for show profile selector events await listen("show-profile-selector", (event) => { console.log("Received show profile selector request:", event.payload); - setPendingUrls([{ id: Date.now().toString(), url: event.payload }]); + void handleUrlOpen(event.payload); }); // Listen for show create profile dialog events @@ -329,6 +320,25 @@ export default function Home() { ); setCreateProfileDialogOpen(true); }); + + // Listen for custom logo click events + const handleLogoUrlEvent = (event: CustomEvent) => { + console.log("Received logo URL event:", event.detail); + void handleUrlOpen(event.detail); + }; + + window.addEventListener( + "url-open-request", + handleLogoUrlEvent as EventListener, + ); + + // Return cleanup function + return () => { + window.removeEventListener( + "url-open-request", + handleLogoUrlEvent as EventListener, + ); + }; } catch (error) { console.error("Failed to setup URL listener:", error); } @@ -643,8 +653,16 @@ export default function Home() { // Check for startup default browser prompt void checkStartupPrompt(); - // Listen for URL open events - void listenForUrlEvents(); + // Listen for URL open events and get cleanup function + const setupListeners = async () => { + const cleanup = await listenForUrlEvents(); + return cleanup; + }; + + let cleanup: (() => void) | undefined; + setupListeners().then((cleanupFn) => { + cleanup = cleanupFn; + }); // Check for startup URLs (when app was launched as default browser) void checkCurrentUrl(); @@ -659,6 +677,9 @@ export default function Home() { return () => { clearInterval(updateInterval); + if (cleanup) { + cleanup(); + } }; }, [ loadProfilesWithUpdateCheck, @@ -850,6 +871,7 @@ export default function Home() { description={`This action cannot be undone. This will permanently delete ${selectedProfiles.length} profile${selectedProfiles.length !== 1 ? "s" : ""} and all associated data.`} confirmButtonText={`Delete ${selectedProfiles.length} Profile${selectedProfiles.length !== 1 ? "s" : ""}`} isLoading={isBulkDeleting} + profileNames={selectedProfiles} /> ); diff --git a/src/types.ts b/src/types.ts index b11c1d4..d0975e9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,12 +106,15 @@ export interface CamoufoxConfig { additional_args?: string[]; env_vars?: Record; firefox_prefs?: Record; + // Required options for anti-detect features + disableTheming?: boolean; + showcursor?: boolean; } export interface CamoufoxLaunchResult { id: string; - pid?: number; - executable_path: string; - profile_path: string; + port?: number; + wsEndpoint?: string; + profilePath?: string; url?: string; }