mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-25 22:00:52 +02:00
feat: fully implement happy flow for persistant fingerprint generation
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { launchOptions } from "camoufox-js";
|
||||
import type { LaunchOptions } from "camoufox-js/dist/utils.js";
|
||||
import {
|
||||
type CamoufoxConfig,
|
||||
@@ -10,6 +11,194 @@ import {
|
||||
saveCamoufoxConfig,
|
||||
} from "./camoufox-storage.js";
|
||||
|
||||
/**
|
||||
* Convert fingerprint-generator format to camoufox fingerprint format (reverse of convertCamoufoxToFingerprintGenerator)
|
||||
* @param fingerprintObj The fingerprint-generator object
|
||||
* @returns camoufox fingerprint object
|
||||
*/
|
||||
export function convertFingerprintGeneratorToCamoufox(
|
||||
fingerprintObj: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
const camoufoxData: Record<string, any> = {};
|
||||
|
||||
// Reverse mappings from fingerprint-generator structure to camoufox keys
|
||||
const reverseMappings: Record<string, string> = {
|
||||
// Navigator properties
|
||||
"navigator.userAgent": "navigator.userAgent",
|
||||
"navigator.platform": "navigator.platform",
|
||||
"navigator.hardwareConcurrency": "navigator.hardwareConcurrency",
|
||||
"navigator.maxTouchPoints": "navigator.maxTouchPoints",
|
||||
"navigator.doNotTrack": "navigator.doNotTrack",
|
||||
"navigator.appCodeName": "navigator.appCodeName",
|
||||
"navigator.appName": "navigator.appName",
|
||||
"navigator.appVersion": "navigator.appVersion",
|
||||
"navigator.oscpu": "navigator.oscpu",
|
||||
"navigator.product": "navigator.product",
|
||||
"navigator.language": "navigator.language",
|
||||
"navigator.languages": "navigator.languages",
|
||||
"navigator.globalPrivacyControl": "navigator.globalPrivacyControl",
|
||||
|
||||
// Screen properties
|
||||
"screen.width": "screen.width",
|
||||
"screen.height": "screen.height",
|
||||
"screen.availWidth": "screen.availWidth",
|
||||
"screen.availHeight": "screen.availHeight",
|
||||
"screen.availTop": "screen.availTop",
|
||||
"screen.availLeft": "screen.availLeft",
|
||||
"screen.colorDepth": "screen.colorDepth",
|
||||
"screen.pixelDepth": "screen.pixelDepth",
|
||||
"screen.outerWidth": "window.outerWidth",
|
||||
"screen.outerHeight": "window.outerHeight",
|
||||
"screen.innerWidth": "window.innerWidth",
|
||||
"screen.innerHeight": "window.innerHeight",
|
||||
"screen.screenX": "window.screenX",
|
||||
"screen.screenY": "window.screenY",
|
||||
"screen.pageXOffset": "screen.pageXOffset",
|
||||
"screen.pageYOffset": "screen.pageYOffset",
|
||||
"screen.devicePixelRatio": "window.devicePixelRatio",
|
||||
"screen.clientWidth": "document.body.clientWidth",
|
||||
"screen.clientHeight": "document.body.clientHeight",
|
||||
|
||||
// WebGL properties
|
||||
"videoCard.vendor": "webGl:vendor",
|
||||
"videoCard.renderer": "webGl:renderer",
|
||||
|
||||
// Headers
|
||||
"headers.Accept-Encoding": "headers.Accept-Encoding",
|
||||
|
||||
// Battery
|
||||
"battery.charging": "battery:charging",
|
||||
"battery.chargingTime": "battery:chargingTime",
|
||||
"battery.dischargingTime": "battery:dischargingTime",
|
||||
};
|
||||
|
||||
// Apply reverse mappings
|
||||
for (const [fingerprintPath, camoufoxKey] of Object.entries(
|
||||
reverseMappings,
|
||||
)) {
|
||||
const pathParts = fingerprintPath.split(".");
|
||||
let current = fingerprintObj;
|
||||
|
||||
// Navigate to the nested property
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
const part = pathParts[i];
|
||||
if (!current[part]) {
|
||||
break;
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
// Get the final value
|
||||
const finalKey = pathParts[pathParts.length - 1];
|
||||
if (current && current[finalKey] !== undefined) {
|
||||
camoufoxData[camoufoxKey] = current[finalKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle fonts separately
|
||||
if (fingerprintObj.fonts && Array.isArray(fingerprintObj.fonts)) {
|
||||
camoufoxData.fonts = fingerprintObj.fonts;
|
||||
}
|
||||
|
||||
return camoufoxData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert camoufox fingerprint format to fingerprint-generator format
|
||||
* @param camoufoxFingerprint The camoufox fingerprint object
|
||||
* @returns fingerprint-generator object
|
||||
*/
|
||||
function convertCamoufoxToFingerprintGenerator(
|
||||
camoufoxFingerprint: Record<string, any>,
|
||||
): any {
|
||||
const fingerprintObj: Record<string, any> = {
|
||||
navigator: {},
|
||||
screen: {},
|
||||
videoCard: {},
|
||||
headers: {},
|
||||
battery: {},
|
||||
};
|
||||
|
||||
// Mapping from camoufox keys to fingerprint-generator structure based on the YAML
|
||||
const mappings: Record<string, string> = {
|
||||
// Navigator properties
|
||||
"navigator.userAgent": "navigator.userAgent",
|
||||
"navigator.platform": "navigator.platform",
|
||||
"navigator.hardwareConcurrency": "navigator.hardwareConcurrency",
|
||||
"navigator.maxTouchPoints": "navigator.maxTouchPoints",
|
||||
"navigator.doNotTrack": "navigator.doNotTrack",
|
||||
"navigator.appCodeName": "navigator.appCodeName",
|
||||
"navigator.appName": "navigator.appName",
|
||||
"navigator.appVersion": "navigator.appVersion",
|
||||
"navigator.oscpu": "navigator.oscpu",
|
||||
"navigator.product": "navigator.product",
|
||||
"navigator.language": "navigator.language",
|
||||
"navigator.languages": "navigator.languages",
|
||||
"navigator.globalPrivacyControl": "navigator.globalPrivacyControl",
|
||||
|
||||
// Screen properties
|
||||
"screen.width": "screen.width",
|
||||
"screen.height": "screen.height",
|
||||
"screen.availWidth": "screen.availWidth",
|
||||
"screen.availHeight": "screen.availHeight",
|
||||
"screen.availTop": "screen.availTop",
|
||||
"screen.availLeft": "screen.availLeft",
|
||||
"screen.colorDepth": "screen.colorDepth",
|
||||
"screen.pixelDepth": "screen.pixelDepth",
|
||||
"window.outerWidth": "screen.outerWidth",
|
||||
"window.outerHeight": "screen.outerHeight",
|
||||
"window.innerWidth": "screen.innerWidth",
|
||||
"window.innerHeight": "screen.innerHeight",
|
||||
"window.screenX": "screen.screenX",
|
||||
"window.screenY": "screen.screenY",
|
||||
"screen.pageXOffset": "screen.pageXOffset",
|
||||
"screen.pageYOffset": "screen.pageYOffset",
|
||||
"window.devicePixelRatio": "screen.devicePixelRatio",
|
||||
"document.body.clientWidth": "screen.clientWidth",
|
||||
"document.body.clientHeight": "screen.clientHeight",
|
||||
|
||||
// WebGL properties
|
||||
"webGl:vendor": "videoCard.vendor",
|
||||
"webGl:renderer": "videoCard.renderer",
|
||||
|
||||
// Headers
|
||||
"headers.Accept-Encoding": "headers.Accept-Encoding",
|
||||
|
||||
// Battery
|
||||
"battery:charging": "battery.charging",
|
||||
"battery:chargingTime": "battery.chargingTime",
|
||||
"battery:dischargingTime": "battery.dischargingTime",
|
||||
};
|
||||
|
||||
// Apply mappings
|
||||
for (const [camoufoxKey, fingerprintPath] of Object.entries(mappings)) {
|
||||
if (camoufoxFingerprint[camoufoxKey] !== undefined) {
|
||||
const pathParts = fingerprintPath.split(".");
|
||||
let current = fingerprintObj;
|
||||
|
||||
// Navigate to the nested property, creating objects as needed
|
||||
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||
const part = pathParts[i];
|
||||
if (!current[part]) {
|
||||
current[part] = {};
|
||||
}
|
||||
current = current[part];
|
||||
}
|
||||
|
||||
// Set the final value
|
||||
const finalKey = pathParts[pathParts.length - 1];
|
||||
current[finalKey] = camoufoxFingerprint[camoufoxKey];
|
||||
}
|
||||
}
|
||||
|
||||
// Handle fonts separately
|
||||
if (camoufoxFingerprint.fonts && Array.isArray(camoufoxFingerprint.fonts)) {
|
||||
fingerprintObj.fonts = camoufoxFingerprint.fonts;
|
||||
}
|
||||
|
||||
return fingerprintObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a Camoufox instance in a separate process
|
||||
* @param options Camoufox launch options
|
||||
@@ -21,6 +210,7 @@ export async function startCamoufoxProcess(
|
||||
options: LaunchOptions = {},
|
||||
profilePath?: string,
|
||||
url?: string,
|
||||
customConfig?: string,
|
||||
): Promise<CamoufoxConfig> {
|
||||
// Generate a unique ID for this instance
|
||||
const id = generateCamoufoxId();
|
||||
@@ -31,6 +221,7 @@ export async function startCamoufoxProcess(
|
||||
options,
|
||||
profilePath,
|
||||
url,
|
||||
customConfig,
|
||||
};
|
||||
|
||||
// Save the configuration before starting the process
|
||||
@@ -252,3 +443,105 @@ export async function stopAllCamoufoxProcesses(): Promise<void> {
|
||||
const stopPromises = configs.map((config) => stopCamoufoxProcess(config.id));
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
|
||||
interface GenerateConfigOptions {
|
||||
proxy?: string;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
geoip?: string | boolean;
|
||||
blockImages?: boolean;
|
||||
blockWebrtc?: boolean;
|
||||
blockWebgl?: boolean;
|
||||
executablePath?: string;
|
||||
fingerprint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Camoufox configuration using launchOptions
|
||||
* @param options Configuration options
|
||||
* @returns Promise resolving to the generated config JSON string
|
||||
*/
|
||||
export async function generateCamoufoxConfig(
|
||||
options: GenerateConfigOptions,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Build launch options
|
||||
const launchOpts: LaunchOptions = {
|
||||
// Always set these defaults
|
||||
headless: false,
|
||||
i_know_what_im_doing: true,
|
||||
config: {
|
||||
disableTheming: true,
|
||||
showcursor: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Always set geoip and blocking options
|
||||
launchOpts.geoip = options.geoip !== undefined ? options.geoip : true;
|
||||
|
||||
if (options.blockImages) {
|
||||
launchOpts.block_images = true;
|
||||
}
|
||||
if (options.blockWebrtc) {
|
||||
launchOpts.block_webrtc = true;
|
||||
}
|
||||
if (options.blockWebgl) {
|
||||
launchOpts.block_webgl = true;
|
||||
}
|
||||
|
||||
if (options.executablePath) {
|
||||
launchOpts.executable_path = options.executablePath;
|
||||
}
|
||||
|
||||
// If fingerprint is provided, use it and ignore other options except executable_path and block_*
|
||||
if (options.fingerprint) {
|
||||
try {
|
||||
const camoufoxFingerprint = JSON.parse(options.fingerprint);
|
||||
|
||||
// Convert camoufox fingerprint format to fingerprint-generator format
|
||||
const fingerprintObj =
|
||||
convertCamoufoxToFingerprintGenerator(camoufoxFingerprint);
|
||||
launchOpts.fingerprint = fingerprintObj;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid fingerprint JSON: ${error}`);
|
||||
}
|
||||
} else {
|
||||
// Use individual options to build configuration
|
||||
if (options.proxy) {
|
||||
launchOpts.proxy = options.proxy;
|
||||
}
|
||||
|
||||
if (options.maxWidth && options.maxHeight) {
|
||||
launchOpts.screen = {
|
||||
maxWidth: options.maxWidth,
|
||||
maxHeight: options.maxHeight,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the configuration using launchOptions
|
||||
const generatedOptions = await launchOptions(launchOpts);
|
||||
|
||||
// Extract the environment variables that contain the config
|
||||
const envVars = generatedOptions.env || {};
|
||||
|
||||
// Reconstruct the config from environment variables using getEnvVars utility
|
||||
let configStr = "";
|
||||
let chunkIndex = 1;
|
||||
|
||||
while (envVars[`CAMOU_CONFIG_${chunkIndex}`]) {
|
||||
configStr += envVars[`CAMOU_CONFIG_${chunkIndex}`];
|
||||
chunkIndex++;
|
||||
}
|
||||
|
||||
if (!configStr) {
|
||||
throw new Error("No configuration generated");
|
||||
}
|
||||
|
||||
// Parse and return the config as JSON string
|
||||
const config = JSON.parse(configStr);
|
||||
return JSON.stringify(config);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to generate Camoufox config: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface CamoufoxConfig {
|
||||
profilePath?: string;
|
||||
url?: string;
|
||||
processId?: number;
|
||||
customConfig?: string; // JSON string of the fingerprint config
|
||||
}
|
||||
|
||||
const STORAGE_DIR = path.join(tmp.tmpdir, "donutbrowser", "camoufox");
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { launchServer } from "camoufox-js";
|
||||
import { launchOptions } from "camoufox-js";
|
||||
import type { LaunchOptions } from "camoufox-js/dist/utils.js";
|
||||
import { type Browser, type BrowserServer, firefox } from "playwright-core";
|
||||
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
|
||||
import { getEnvVars } from "./utils.js";
|
||||
|
||||
/**
|
||||
* Run a Camoufox browser server as a worker process
|
||||
@@ -73,62 +75,67 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
|
||||
|
||||
try {
|
||||
// Prepare options for Camoufox
|
||||
const camoufoxOptions = { ...config.options };
|
||||
const camoufoxOptions: LaunchOptions = { ...config.options };
|
||||
|
||||
// Add profile path if provided
|
||||
if (config.profilePath) {
|
||||
camoufoxOptions.user_data_dir = config.profilePath;
|
||||
}
|
||||
|
||||
// Theming
|
||||
camoufoxOptions.disableTheming = true;
|
||||
camoufoxOptions.showcursor = false;
|
||||
|
||||
// Set Firefox preferences for theming
|
||||
if (!camoufoxOptions.firefox_user_prefs) {
|
||||
camoufoxOptions.firefox_user_prefs = {};
|
||||
if (camoufoxOptions.block_images) {
|
||||
camoufoxOptions.block_images = true;
|
||||
}
|
||||
|
||||
// Default to non-headless for visibility
|
||||
if (camoufoxOptions.headless === undefined) {
|
||||
camoufoxOptions.headless = false;
|
||||
if (camoufoxOptions.block_webgl) {
|
||||
camoufoxOptions.block_webgl = true;
|
||||
}
|
||||
|
||||
// Launch the server with proper options
|
||||
server = await launchServer({
|
||||
ws_path: `/ws_${config.id}`,
|
||||
os: camoufoxOptions.os,
|
||||
block_images: camoufoxOptions.block_images,
|
||||
block_webrtc: camoufoxOptions.block_webrtc,
|
||||
block_webgl: camoufoxOptions.block_webgl,
|
||||
disable_coop: camoufoxOptions.disable_coop,
|
||||
geoip: camoufoxOptions.geoip,
|
||||
humanize: camoufoxOptions.humanize,
|
||||
locale: camoufoxOptions.locale,
|
||||
addons: camoufoxOptions.addons,
|
||||
fonts: camoufoxOptions.fonts,
|
||||
custom_fonts_only: camoufoxOptions.custom_fonts_only,
|
||||
exclude_addons: camoufoxOptions.exclude_addons,
|
||||
screen: camoufoxOptions.screen,
|
||||
window: camoufoxOptions.window,
|
||||
fingerprint: camoufoxOptions.fingerprint,
|
||||
ff_version: camoufoxOptions.ff_version,
|
||||
headless: camoufoxOptions.headless,
|
||||
main_world_eval: camoufoxOptions.main_world_eval,
|
||||
executable_path: camoufoxOptions.executable_path,
|
||||
firefox_user_prefs: camoufoxOptions.firefox_user_prefs,
|
||||
proxy: camoufoxOptions.proxy,
|
||||
enable_cache: camoufoxOptions.enable_cache,
|
||||
args: camoufoxOptions.args,
|
||||
env: camoufoxOptions.env,
|
||||
debug: camoufoxOptions.debug,
|
||||
virtual_display: camoufoxOptions.virtual_display,
|
||||
webgl_config: camoufoxOptions.webgl_config,
|
||||
config: {
|
||||
disableTheming: true,
|
||||
showcursor: false,
|
||||
timezone: camoufoxOptions.timezone,
|
||||
},
|
||||
if (camoufoxOptions.block_webrtc) {
|
||||
camoufoxOptions.block_webrtc = true;
|
||||
}
|
||||
|
||||
// Check for headless mode from config (no environment variable check)
|
||||
if (camoufoxOptions.headless) {
|
||||
camoufoxOptions.headless = true;
|
||||
}
|
||||
|
||||
// Always set these defaults
|
||||
camoufoxOptions.i_know_what_im_doing = true;
|
||||
camoufoxOptions.config = {
|
||||
disableTheming: true,
|
||||
showcursor: false,
|
||||
...(camoufoxOptions.config || {}),
|
||||
};
|
||||
|
||||
// Generate the configuration using launchOptions
|
||||
const generatedOptions = await launchOptions(camoufoxOptions);
|
||||
|
||||
// If we have a custom config from Rust, use it directly as environment variables
|
||||
let finalEnv = generatedOptions.env || {};
|
||||
|
||||
if (config.customConfig) {
|
||||
try {
|
||||
// Parse the custom config JSON string
|
||||
const customConfigObj = JSON.parse(config.customConfig);
|
||||
|
||||
// Convert custom config to environment variables using getEnvVars
|
||||
const customEnvVars = getEnvVars(customConfigObj);
|
||||
|
||||
// Merge custom config with generated config (custom takes precedence)
|
||||
finalEnv = { ...finalEnv, ...customEnvVars };
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Failed to parse custom config, using generated config:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Launch the server with the final configuration
|
||||
server = await firefox.launchServer({
|
||||
...generatedOptions,
|
||||
wsPath: `/ws_${config.id}`,
|
||||
env: finalEnv,
|
||||
});
|
||||
|
||||
// Connect to the server
|
||||
@@ -140,8 +147,7 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
|
||||
|
||||
saveCamoufoxConfig(config);
|
||||
|
||||
// Monitor for window closure to handle Command+Q properly
|
||||
|
||||
// Monitor for window closure
|
||||
const startWindowMonitoring = () => {
|
||||
windowCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
|
||||
+87
-77
@@ -1,6 +1,7 @@
|
||||
import type { LaunchOptions } from "camoufox-js/dist/utils.js";
|
||||
import { program } from "commander";
|
||||
import {
|
||||
generateCamoufoxConfig,
|
||||
startCamoufoxProcess,
|
||||
stopAllCamoufoxProcesses,
|
||||
stopCamoufoxProcess,
|
||||
@@ -152,88 +153,26 @@ program
|
||||
// Command for Camoufox management
|
||||
program
|
||||
.command("camoufox")
|
||||
.argument("<action>", "start, stop, or list Camoufox instances")
|
||||
.argument(
|
||||
"<action>",
|
||||
"start, stop, list, or generate-config Camoufox instances",
|
||||
)
|
||||
.option("--id <id>", "Camoufox ID for stop command")
|
||||
.option("--profile-path <path>", "profile directory path")
|
||||
.option("--url <url>", "URL to open")
|
||||
|
||||
// Operating system fingerprinting
|
||||
.option(
|
||||
"--os <os>",
|
||||
"OS to emulate (windows, macos, linux, or comma-separated list)",
|
||||
)
|
||||
|
||||
// Blocking options
|
||||
.option("--block-images", "block all images")
|
||||
.option("--block-webrtc", "block WebRTC entirely")
|
||||
// Config generation options
|
||||
.option("--proxy <proxy>", "proxy URL for config generation")
|
||||
.option("--max-width <width>", "maximum screen width", parseInt)
|
||||
.option("--max-height <height>", "maximum screen height", parseInt)
|
||||
.option("--geoip [ip]", "enable geoip or specify IP")
|
||||
.option("--block-images", "block images")
|
||||
.option("--block-webrtc", "block WebRTC")
|
||||
.option("--block-webgl", "block WebGL")
|
||||
|
||||
// Security options
|
||||
.option("--disable-coop", "disable Cross-Origin-Opener-Policy")
|
||||
|
||||
// Geolocation and IP
|
||||
.option(
|
||||
"--geoip <ip>",
|
||||
"IP address for geolocation spoofing (or 'auto' for automatic)",
|
||||
)
|
||||
.option("--country <country>", "country code for geolocation")
|
||||
.option("--timezone <timezone>", "timezone to spoof")
|
||||
.option("--latitude <lat>", "latitude for geolocation", parseFloat)
|
||||
.option("--longitude <lng>", "longitude for geolocation", parseFloat)
|
||||
|
||||
// UI and behavior
|
||||
.option(
|
||||
"--humanize [duration]",
|
||||
"humanize cursor movement (optional max duration in seconds)",
|
||||
(val) => (val ? parseFloat(val) : true),
|
||||
)
|
||||
.option("--executable-path <path>", "executable path")
|
||||
.option("--fingerprint <json>", "fingerprint JSON string")
|
||||
.option("--headless", "run in headless mode")
|
||||
|
||||
// Localization
|
||||
.option("--locale <locale>", "locale(s) to use (comma-separated)")
|
||||
|
||||
// Extensions and fonts
|
||||
.option("--addons <addons>", "Firefox addons to load (comma-separated paths)")
|
||||
.option("--fonts <fonts>", "additional fonts to load (comma-separated)")
|
||||
.option("--custom-fonts-only", "use only custom fonts, exclude OS fonts")
|
||||
.option(
|
||||
"--exclude-addons <addons>",
|
||||
"default addons to exclude (comma-separated)",
|
||||
)
|
||||
|
||||
// Screen and window
|
||||
.option("--screen-min-width <width>", "minimum screen width", parseInt)
|
||||
.option("--screen-max-width <width>", "maximum screen width", parseInt)
|
||||
.option("--screen-min-height <height>", "minimum screen height", parseInt)
|
||||
.option("--screen-max-height <height>", "maximum screen height", parseInt)
|
||||
.option("--window-width <width>", "fixed window width", parseInt)
|
||||
.option("--window-height <height>", "fixed window height", parseInt)
|
||||
|
||||
// Advanced options
|
||||
.option("--ff-version <version>", "Firefox version to emulate", parseInt)
|
||||
.option("--main-world-eval", "enable main world script evaluation")
|
||||
.option("--webgl-vendor <vendor>", "WebGL vendor string")
|
||||
.option("--webgl-renderer <renderer>", "WebGL renderer string")
|
||||
|
||||
// Proxy
|
||||
.option(
|
||||
"--proxy <proxy>",
|
||||
"proxy URL (protocol://[username:password@]host:port)",
|
||||
)
|
||||
|
||||
// Cache and performance
|
||||
.option("--disable-cache", "disable browser cache (cache enabled by default)")
|
||||
|
||||
// Environment and debugging
|
||||
.option("--virtual-display <display>", "virtual display number (e.g., :99)")
|
||||
.option("--debug", "enable debug output")
|
||||
.option("--args <args>", "additional browser arguments (comma-separated)")
|
||||
.option("--env <env>", "environment variables (JSON string)")
|
||||
|
||||
// Firefox preferences
|
||||
.option("--firefox-prefs <prefs>", "Firefox user preferences (JSON string)")
|
||||
|
||||
// Note: theming and cursor options are hardcoded and not user-configurable
|
||||
.option("--custom-config <json>", "custom config JSON string")
|
||||
|
||||
.description("manage Camoufox browser instances")
|
||||
.action(
|
||||
@@ -349,6 +288,11 @@ program
|
||||
if (options.virtualDisplay)
|
||||
camoufoxOptions.virtual_display = options.virtualDisplay as string;
|
||||
if (options.debug) camoufoxOptions.debug = true;
|
||||
|
||||
// Handle headless mode via flag instead of environment variable
|
||||
if (options.headless) {
|
||||
camoufoxOptions.headless = true;
|
||||
}
|
||||
if (options.args && typeof options.args === "string")
|
||||
camoufoxOptions.args = options.args.split(",");
|
||||
if (options.env && typeof options.env === "string") {
|
||||
@@ -393,6 +337,9 @@ program
|
||||
? options.profilePath
|
||||
: undefined,
|
||||
typeof options.url === "string" ? options.url : undefined,
|
||||
typeof options.customConfig === "string"
|
||||
? options.customConfig
|
||||
: undefined,
|
||||
);
|
||||
|
||||
console.log(
|
||||
@@ -427,8 +374,71 @@ program
|
||||
const configs = listCamoufoxConfigs();
|
||||
console.log(JSON.stringify(configs));
|
||||
process.exit(0);
|
||||
} else if (action === "generate-config") {
|
||||
try {
|
||||
// Handle geoip option properly
|
||||
let geoipValue: string | boolean = true; // Default to true
|
||||
if (options.geoip !== undefined) {
|
||||
if (typeof options.geoip === "boolean") {
|
||||
geoipValue = options.geoip;
|
||||
} else if (typeof options.geoip === "string") {
|
||||
if (options.geoip === "true") {
|
||||
geoipValue = true;
|
||||
} else if (options.geoip === "false") {
|
||||
geoipValue = false;
|
||||
} else {
|
||||
geoipValue = options.geoip; // IP address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const config = await generateCamoufoxConfig({
|
||||
proxy:
|
||||
typeof options.proxy === "string" ? options.proxy : undefined,
|
||||
maxWidth:
|
||||
typeof options.maxWidth === "number"
|
||||
? options.maxWidth
|
||||
: undefined,
|
||||
maxHeight:
|
||||
typeof options.maxHeight === "number"
|
||||
? options.maxHeight
|
||||
: undefined,
|
||||
geoip: geoipValue,
|
||||
blockImages:
|
||||
typeof options.blockImages === "boolean"
|
||||
? options.blockImages
|
||||
: undefined,
|
||||
blockWebrtc:
|
||||
typeof options.blockWebrtc === "boolean"
|
||||
? options.blockWebrtc
|
||||
: undefined,
|
||||
blockWebgl:
|
||||
typeof options.blockWebgl === "boolean"
|
||||
? options.blockWebgl
|
||||
: undefined,
|
||||
executablePath:
|
||||
typeof options.executablePath === "string"
|
||||
? options.executablePath
|
||||
: undefined,
|
||||
fingerprint:
|
||||
typeof options.fingerprint === "string"
|
||||
? options.fingerprint
|
||||
: undefined,
|
||||
});
|
||||
console.log(config);
|
||||
process.exit(0);
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
`Failed to generate config: ${
|
||||
error instanceof Error ? error.message : JSON.stringify(error)
|
||||
}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.error("Invalid action. Use 'start', 'stop', or 'list'");
|
||||
console.error(
|
||||
"Invalid action. Use 'start', 'stop', 'list', or 'generate-config'",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const OS_MAP: { [key: string]: "mac" | "win" | "lin" } = {
|
||||
darwin: "mac",
|
||||
linux: "lin",
|
||||
win32: "win",
|
||||
};
|
||||
|
||||
const OS_NAME: "mac" | "win" | "lin" = OS_MAP[process.platform];
|
||||
|
||||
export function getEnvVars(configMap: Record<string, string>) {
|
||||
const envVars: {
|
||||
[key: string]: string | number | boolean;
|
||||
} = {};
|
||||
let updatedConfigData: Uint8Array;
|
||||
|
||||
try {
|
||||
updatedConfigData = new TextEncoder().encode(JSON.stringify(configMap));
|
||||
} catch (e) {
|
||||
console.error(`Error updating config: ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const chunkSize = OS_NAME === "win" ? 2047 : 32767;
|
||||
const configStr = new TextDecoder().decode(updatedConfigData);
|
||||
|
||||
for (let i = 0; i < configStr.length; i += chunkSize) {
|
||||
const chunk = configStr.slice(i, i + chunkSize);
|
||||
const envName = `CAMOU_CONFIG_${Math.floor(i / chunkSize) + 1}`;
|
||||
try {
|
||||
envVars[envName] = chunk;
|
||||
} catch (e) {
|
||||
console.error(`Error setting ${envName}: ${e}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
}
|
||||
Reference in New Issue
Block a user