diff --git a/.vscode/settings.json b/.vscode/settings.json index deed89f..1da025e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "autoconfig", "autologin", "biomejs", + "CAMOU", "camoufox", "cdylib", "CFURL", @@ -50,7 +51,9 @@ "librsvg", "libwebkit", "libxdo", + "localipv", "localtime", + "memorysaver", "mmdb", "mountpoint", "msiexec", @@ -66,6 +69,7 @@ "objc", "orhun", "osascript", + "oscpu", "peerconnection", "pixbuf", "plasmohq", @@ -78,6 +82,7 @@ "SARIF", "serde", "shadcn", + "showcursor", "signon", "sonner", "splitn", @@ -85,6 +90,7 @@ "staticlib", "stefanzweifel", "subdirs", + "Subframes", "subkey", "SUPPRESSMSGBOXES", "swatinem", diff --git a/nodecar/src/camoufox-launcher.ts b/nodecar/src/camoufox-launcher.ts index f287698..acd3b03 100644 --- a/nodecar/src/camoufox-launcher.ts +++ b/nodecar/src/camoufox-launcher.ts @@ -228,19 +228,249 @@ function buildCamoufoxArgs( } /** - * Create user.js file with Camoufox preferences + * Create Camoufox configuration object from launch options + * This follows the complete Camoufox schema for CAMOU_CONFIG_* environment variables */ -function createUserJs( +function createCamoufoxConfig(options: CamoufoxLaunchOptions): any { + const config: any = {}; + + // Debug flag + if (options.debug !== undefined) { + config.debug = options.debug; + } + + // Locale settings - parse locale string into language and region + if (options.locale) { + const localeValue = Array.isArray(options.locale) + ? options.locale[0] + : options.locale; + + // Parse locale like "en-US" into language and region + const localeParts = localeValue.split("-"); + if (localeParts.length >= 2) { + config["locale:language$__LOCALE"] = localeParts[0]; + config["locale:region$__LOCALE"] = localeParts[1]; + } else { + config["locale:language$__LOCALE"] = localeParts[0]; + // Default region if not specified + config["locale:region$__LOCALE"] = "US"; + } + + // Set navigator language properties + config["navigator.language"] = localeValue; + config["navigator.languages"] = Array.isArray(options.locale) + ? options.locale + : [localeValue]; + config["headers.Accept-Language"] = localeValue; + config["locale:all"] = localeValue; + } + + // Screen dimensions from screen options + if (options.screen) { + if (options.screen.maxWidth) { + config["screen.width$__SC"] = options.screen.maxWidth; + config["screen.availWidth$__SC"] = options.screen.maxWidth; + } + if (options.screen.maxHeight) { + config["screen.height$__SC"] = options.screen.maxHeight; + config["screen.availHeight$__SC"] = options.screen.maxHeight; + } + + // Set default screen properties if not specified + if (!options.screen.maxWidth) { + config["screen.width$__SC"] = 1920; + config["screen.availWidth$__SC"] = 1920; + } + if (!options.screen.maxHeight) { + config["screen.height$__SC"] = 1080; + config["screen.availHeight$__SC"] = 1080; + } + + // Default screen position and color depth + config["screen.availTop"] = 0; + config["screen.availLeft"] = 0; + config["screen.colorDepth"] = 24; + config["screen.pixelDepth"] = 24; + } else { + // Default screen settings if not specified + config["screen.width$__SC"] = 1920; + config["screen.height$__SC"] = 1080; + config["screen.availWidth$__SC"] = 1920; + config["screen.availHeight$__SC"] = 1080; + config["screen.availTop"] = 0; + config["screen.availLeft"] = 0; + config["screen.colorDepth"] = 24; + config["screen.pixelDepth"] = 24; + } + + // Window dimensions + if (options.window) { + config["window.outerWidth$__W_OUTER"] = options.window[0]; + config["window.outerHeight$__W_OUTER"] = options.window[1]; + config["window.innerWidth$__W_INNER"] = options.window[0] - 16; // Account for scrollbars + config["window.innerHeight$__W_INNER"] = options.window[1] - 100; // Account for browser chrome + } else { + // Default window dimensions + config["window.outerWidth$__W_OUTER"] = 1280; + config["window.outerHeight$__W_OUTER"] = 720; + config["window.innerWidth$__W_INNER"] = 1264; + config["window.innerHeight$__W_INNER"] = 620; + } + + // Window position and properties + config["window.screenX"] = 0; + config["window.screenY"] = 0; + config["window.devicePixelRatio"] = 1.0; + config["window.scrollMinX"] = 0; + config["window.scrollMinY"] = 0; + config["window.scrollMaxX"] = 0; + config["window.scrollMaxY"] = 0; + config["screen.pageXOffset"] = 0.0; + config["screen.pageYOffset"] = 0.0; + + // Document body dimensions + config["document.body.clientWidth$__DOC_BODY"] = + config["window.innerWidth$__W_INNER"]; + config["document.body.clientHeight$__DOC_BODY"] = + config["window.innerHeight$__W_INNER"]; + config["document.body.clientTop"] = 0; + config["document.body.clientLeft"] = 0; + + // Geolocation + if (options.geolocation) { + config["geolocation:latitude$__GEO"] = options.geolocation.latitude; + config["geolocation:longitude$__GEO"] = options.geolocation.longitude; + if (options.geolocation.accuracy) { + config["geolocation:accuracy"] = options.geolocation.accuracy; + } + } + + // Timezone + if (options.timezone) { + config.timezone = options.timezone; + } + + // User Agent based on OS option + const osOption = Array.isArray(options.os) ? options.os[0] : options.os; + let userAgent: string; + let platform: string; + let oscpu: string; + let appVersion: string; + + switch (osOption) { + case "macos": + userAgent = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0"; + platform = "MacIntel"; + oscpu = "Intel Mac OS X 10.15"; + appVersion = + "5.0 (Macintosh; Intel Mac OS X 10.15; rv:135.0) Gecko/20100101 Firefox/135.0"; + break; + case "linux": + userAgent = + "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"; + platform = "Linux x86_64"; + oscpu = "Linux x86_64"; + appVersion = + "5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"; + break; + case "windows": + default: + userAgent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0"; + platform = "Win32"; + oscpu = "Windows NT 10.0; Win64; x64"; + appVersion = + "5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0"; + break; + } + + config["navigator.userAgent"] = userAgent; + config["navigator.appVersion"] = appVersion; + config["navigator.platform"] = platform; + config["navigator.oscpu"] = oscpu; + config["headers.User-Agent"] = userAgent; + + // Headers + config["headers.Accept-Encoding"] = "gzip, deflate, br"; + + // Fonts + if (options.fonts && options.fonts.length > 0) { + config.fonts = options.fonts; + } + config["fonts:spacing_seed"] = 0; + + // WebGL configuration + if ( + options.webgl_config && + Array.isArray(options.webgl_config) && + options.webgl_config.length === 2 + ) { + config["webGl:vendor$__WEBGL"] = options.webgl_config[0]; + config["webGl:renderer$__WEBGL"] = options.webgl_config[1]; + } + + // WebRTC IP spoofing from geoip + if ( + options.geoip && + typeof options.geoip === "string" && + options.geoip !== "auto" + ) { + if (options.geoip.includes(":")) { + // IPv6 + config["webrtc:ipv6"] = options.geoip; + config["webrtc:localipv6"] = options.geoip; + } else { + // IPv4 + config["webrtc:ipv4"] = options.geoip; + config["webrtc:localipv4"] = options.geoip; + } + } + + // Addons + if (options.addons && options.addons.length > 0) { + config.addons = options.addons; + } + + // Humanization + if (options.humanize !== undefined) { + config.humanize = !!options.humanize; + if (typeof options.humanize === "number") { + config["humanize:maxTime"] = options.humanize; + config["humanize:minTime"] = 0.0; + } else { + config["humanize:maxTime"] = 5.0; + config["humanize:minTime"] = 0.5; + } + } + + // Cursor visibility + config.showcursor = false; + + // Advanced browser settings + if (options.main_world_eval) { + config.allowMainWorld = options.main_world_eval; + } + + config.forceScopeAccess = false; + config.enableRemoteSubframes = false; + config.disableTheming = false; + config.memorysaver = false; + + return config; +} + +/** + * Create minimal user.js for Firefox-specific settings that are not part of Camoufox fingerprint config + */ +function createMinimalUserJs( profilePath: string, options: CamoufoxLaunchOptions, ): void { const preferences: string[] = []; - // Anti-detect preferences - preferences.push('user_pref("privacy.resistFingerprinting", true);'); - preferences.push( - 'user_pref("privacy.resistFingerprinting.letterboxing", true);', - ); + // Basic privacy settings + preferences.push('user_pref("privacy.resistFingerprinting", false);'); // Let Camoufox handle fingerprinting preferences.push('user_pref("privacy.trackingprotection.enabled", true);'); // Disable telemetry and data collection @@ -275,35 +505,6 @@ function createUserJs( ); } - // Locale settings - if (options.locale) { - const localeStr = Array.isArray(options.locale) - ? options.locale[0] - : options.locale; - preferences.push(`user_pref("intl.locale.requested", "${localeStr}");`); - preferences.push(`user_pref("general.useragent.locale", "${localeStr}");`); - } - - // Timezone - if (options.timezone) { - preferences.push( - `user_pref("privacy.resistFingerprinting.timezone", "${options.timezone}");`, - ); - } - - // Custom Firefox preferences - if (options.firefox_user_prefs) { - for (const [key, value] of Object.entries(options.firefox_user_prefs)) { - if (typeof value === "string") { - preferences.push(`user_pref("${key}", "${value}");`); - } else if (typeof value === "boolean") { - preferences.push(`user_pref("${key}", ${value});`); - } else if (typeof value === "number") { - preferences.push(`user_pref("${key}", ${value});`); - } - } - } - // Proxy settings if (options.proxy) { if (typeof options.proxy === "string") { @@ -349,19 +550,56 @@ function createUserJs( } } - // Geolocation - if (options.geolocation) { - preferences.push('user_pref("geo.enabled", true);'); - preferences.push( - `user_pref("geo.wifi.uri", "data:application/json,{\\"location\\": {\\"lat\\": ${options.geolocation.latitude}, \\"lng\\": ${options.geolocation.longitude}}, \\"accuracy\\": ${options.geolocation.accuracy || 100}}");`, - ); - } else { - preferences.push('user_pref("geo.enabled", false);'); + // Custom Firefox preferences + if (options.firefox_user_prefs) { + for (const [key, value] of Object.entries(options.firefox_user_prefs)) { + if (typeof value === "string") { + preferences.push(`user_pref("${key}", "${value}");`); + } else if (typeof value === "boolean") { + preferences.push(`user_pref("${key}", ${value});`); + } else if (typeof value === "number") { + preferences.push(`user_pref("${key}", ${value});`); + } + } } - // Write user.js file - const userJsPath = path.join(profilePath, "user.js"); - fs.writeFileSync(userJsPath, preferences.join("\n")); + // Cache settings + if (options.enable_cache === false) { + preferences.push('user_pref("browser.cache.disk.enable", false);'); + preferences.push('user_pref("browser.cache.memory.enable", false);'); + } + + // Write user.js file only if we have preferences to set + if (preferences.length > 0) { + const userJsPath = path.join(profilePath, "user.js"); + fs.writeFileSync(userJsPath, preferences.join("\n")); + } +} + +/** + * Set Camoufox configuration via environment variables + */ +function setCamoufoxConfigEnv(config: any, env: NodeJS.ProcessEnv): void { + const configJson = JSON.stringify(config); + const chunkSize = os.platform() === "win32" ? 2047 : 32767; + + // Clear any existing CAMOU_CONFIG_* variables + for (const key in env) { + if (key.startsWith("CAMOU_CONFIG_")) { + delete env[key]; + } + } + + // Split config into chunks + const chunks: string[] = []; + for (let i = 0; i < configJson.length; i += chunkSize) { + chunks.push(configJson.slice(i, i + chunkSize)); + } + + // Set environment variables (start from index 1 as expected by Camoufox) + for (let i = 0; i < chunks.length; i++) { + env[`CAMOU_CONFIG_${i + 1}`] = chunks[i]; + } } /** @@ -380,18 +618,43 @@ export async function launchCamoufox( fs.mkdirSync(profilePath, { recursive: true }); } - // Create user.js with preferences - createUserJs(profilePath, options); + // Create Camoufox configuration + const camoufoxConfig = createCamoufoxConfig(options); + + // Create minimal user.js for Firefox-specific settings (proxy, blocking, etc.) + createMinimalUserJs(profilePath, options); // Build command line arguments const args = buildCamoufoxArgs(options, profilePath, url); // Prepare environment variables - const env = { + const env: NodeJS.ProcessEnv = { ...process.env, - ...options.env, }; + // Add custom environment variables from options, converting values to strings + if (options.env) { + for (const [key, value] of Object.entries(options.env)) { + if (value !== undefined) { + env[key] = String(value); + } + } + } + + // Set Camoufox configuration via environment variables + setCamoufoxConfigEnv(camoufoxConfig, env); + + if (options.debug) { + console.log( + "Camoufox configuration:", + JSON.stringify(camoufoxConfig, null, 2), + ); + console.log( + "Environment variables set:", + Object.keys(env).filter((key) => key.startsWith("CAMOU_CONFIG_")), + ); + } + // Handle virtual display if (options.virtual_display) { env.DISPLAY = options.virtual_display; @@ -481,14 +744,19 @@ export function listCamoufoxProcesses(): any[] { for (const [id, config] of activeCamoufoxProcesses) { if (config.pid && isProcessRunning(config.pid)) { + // Ensure we have the required fields, fall back to empty strings if missing + const executablePath = config.executablePath || ""; + const profilePath = config.profilePath || ""; + // Return in snake_case format for Rust compatibility + // Always include executable_path and profile_path, even if empty activeConfigs.push({ id: config.id, pid: config.pid, - executable_path: config.executablePath, - profile_path: config.profilePath, - url: config.url, - options: config.options, + executable_path: executablePath, + profile_path: profilePath, + url: config.url || null, + options: config.options || {}, }); } else { // Clean up dead processes diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index 34f46ac..9a4eb30 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -277,6 +277,8 @@ program camoufoxOptions.geoip = options.geoip === "auto" ? true : options.geoip; } + + // Combine latitude/longitude into geolocation object if both provided if (options.latitude && options.longitude) { camoufoxOptions.geolocation = { latitude: options.latitude, @@ -284,6 +286,8 @@ program accuracy: 100, }; } + + // Set timezone and country only if explicitly provided if (options.country) camoufoxOptions.country = options.country; if (options.timezone) camoufoxOptions.timezone = options.timezone; @@ -295,7 +299,7 @@ program if (options.locale) { camoufoxOptions.locale = options.locale.includes(",") ? options.locale.split(",") - : options.locale; + : [options.locale]; } // Extensions and fonts @@ -305,14 +309,21 @@ program 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; + // Screen dimensions - combine into screen object if any are provided + const screenOptions: any = {}; + if (options.screenMinWidth) + screenOptions.minWidth = options.screenMinWidth; + if (options.screenMaxWidth) + screenOptions.maxWidth = options.screenMaxWidth; + if (options.screenMinHeight) + screenOptions.minHeight = options.screenMinHeight; + if (options.screenMaxHeight) + screenOptions.maxHeight = options.screenMaxHeight; + if (Object.keys(screenOptions).length > 0) { + camoufoxOptions.screen = screenOptions; + } + // Window dimensions - combine into window tuple if both provided if (options.windowWidth && options.windowHeight) { camoufoxOptions.window = [options.windowWidth, options.windowHeight]; } @@ -320,6 +331,8 @@ program // Advanced options if (options.ffVersion) camoufoxOptions.ff_version = options.ffVersion; if (options.mainWorldEval) camoufoxOptions.main_world_eval = true; + + // WebGL - combine vendor and renderer into webgl_config tuple if both provided if (options.webglVendor && options.webglRenderer) { camoufoxOptions.webgl_config = [ options.webglVendor, @@ -390,15 +403,8 @@ program process.exit(0); } else if (action === "list") { const processes = listCamoufoxProcesses(); - // Convert camelCase to snake_case for Rust compatibility - const rustCompatibleProcesses = processes.map((process) => ({ - id: process.id, - pid: process.pid, - executable_path: process.executablePath, - profile_path: process.profilePath, - url: process.url, - })); - console.log(JSON.stringify(rustCompatibleProcesses)); + // The processes already have snake_case properties, no conversion needed + console.log(JSON.stringify(processes)); process.exit(0); } else if (action === "open-url") { if (!options.id || !options.url) { diff --git a/src-tauri/src/camoufox.rs b/src-tauri/src/camoufox.rs index b36471d..bc26814 100644 --- a/src-tauri/src/camoufox.rs +++ b/src-tauri/src/camoufox.rs @@ -457,9 +457,7 @@ impl CamoufoxLauncher { .and_then(|v| v.as_str()) .or_else(|| process.get("profilePath").and_then(|v| v.as_str())); - if let (Some(id), Some(executable_path), Some(profile_path)) = - (id, executable_path, profile_path) - { + if let Some(id) = id { let pid = process .get("pid") .and_then(|v| v.as_u64()) @@ -470,6 +468,10 @@ impl CamoufoxLauncher { .and_then(|v| v.as_str()) .map(|s| s.to_string()); + // Use empty strings if executable_path or profile_path are missing + let executable_path = executable_path.unwrap_or(""); + let profile_path = profile_path.unwrap_or(""); + results.push(CamoufoxLaunchResult { id: id.to_string(), pid,