refactor: popular camoufox data via env variable

This commit is contained in:
zhom
2025-07-08 19:11:30 +04:00
parent a1403c88f9
commit add4f6d3f8
4 changed files with 357 additions and 75 deletions
+6
View File
@@ -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",
+323 -55
View File
@@ -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
+23 -17
View File
@@ -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) {
+5 -3
View File
@@ -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,