mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-30 23:58:11 +02:00
refactor: partially migrate from launching camoufox directly to launching via playwright
This commit is contained in:
@@ -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<string, any>;
|
||||
firefox_user_prefs?: Record<string, unknown>;
|
||||
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<any> {
|
||||
profilePath?: string,
|
||||
url?: string,
|
||||
): Promise<CamoufoxConfig> {
|
||||
// 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<CamoufoxConfig>((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<boolean> {
|
||||
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<void> {
|
||||
const configs = listCamoufoxConfigs();
|
||||
|
||||
const stopPromises = configs.map((config) => stopCamoufoxProcess(config.id));
|
||||
await Promise.all(stopPromises);
|
||||
}
|
||||
|
||||
@@ -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<boolean> {
|
||||
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)}`;
|
||||
}
|
||||
@@ -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<void> {
|
||||
// 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<ReturnType<typeof launchServer>> | null = null;
|
||||
let browser: Awaited<ReturnType<typeof firefox.connect>> | 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);
|
||||
}
|
||||
}
|
||||
+276
-118
@@ -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("<action>", "generate Camoufox configuration")
|
||||
.command("camoufox")
|
||||
.argument("<action>", "start, stop, or list 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(
|
||||
@@ -231,130 +239,280 @@ program
|
||||
// Firefox preferences
|
||||
.option("--firefox-prefs <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<string, string | number | boolean | undefined>,
|
||||
) => {
|
||||
if (action === "start") {
|
||||
try {
|
||||
// Build Camoufox options in the format expected by camoufox-js
|
||||
const camoufoxOptions: Record<string, unknown> = {};
|
||||
|
||||
// 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<string, unknown> = {};
|
||||
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("<action>", "start a Camoufox worker")
|
||||
.requiredOption("--id <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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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<String>,
|
||||
local_proxy_settings: Option<&ProxySettings>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<dyn std::error::Error + Send + Sync> {
|
||||
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<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<dyn std::error::Error>> {
|
||||
// Find the profile by name
|
||||
let profiles = self.list_profiles()?;
|
||||
@@ -2758,15 +2758,14 @@ impl BrowserRunner {
|
||||
app_handle: tauri::AppHandle,
|
||||
profile: &BrowserProfile,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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"
|
||||
|
||||
@@ -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<Vec<String>>,
|
||||
pub block_images: Option<bool>,
|
||||
pub block_webrtc: Option<bool>,
|
||||
pub block_webgl: Option<bool>,
|
||||
pub disable_coop: Option<bool>,
|
||||
pub geoip: Option<serde_json::Value>, // Can be String or bool
|
||||
pub country: Option<String>,
|
||||
pub timezone: Option<String>,
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
pub humanize: Option<bool>,
|
||||
pub humanize_duration: Option<f64>,
|
||||
pub headless: Option<bool>,
|
||||
pub locale: Option<Vec<String>>,
|
||||
pub addons: Option<Vec<String>>,
|
||||
pub fonts: Option<Vec<String>>,
|
||||
pub custom_fonts_only: Option<bool>,
|
||||
pub exclude_addons: Option<Vec<String>>,
|
||||
pub screen_min_width: Option<u32>,
|
||||
pub screen_max_width: Option<u32>,
|
||||
pub screen_min_height: Option<u32>,
|
||||
pub screen_max_height: Option<u32>,
|
||||
pub window_width: Option<u32>,
|
||||
pub window_height: Option<u32>,
|
||||
pub ff_version: Option<u32>,
|
||||
pub main_world_eval: Option<bool>,
|
||||
pub webgl_vendor: Option<String>,
|
||||
pub webgl_renderer: Option<String>,
|
||||
pub proxy: Option<String>,
|
||||
pub enable_cache: Option<bool>,
|
||||
pub virtual_display: Option<String>,
|
||||
pub debug: Option<bool>,
|
||||
pub additional_args: Option<Vec<String>>,
|
||||
pub env_vars: Option<HashMap<String, String>>,
|
||||
pub firefox_prefs: Option<HashMap<String, serde_json::Value>>,
|
||||
pub disable_theming: Option<bool>,
|
||||
pub showcursor: Option<bool>,
|
||||
}
|
||||
|
||||
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<u32>,
|
||||
#[serde(alias = "ws_endpoint")]
|
||||
pub wsEndpoint: Option<String>,
|
||||
#[serde(alias = "profile_path")]
|
||||
pub profilePath: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CamoufoxInstance {
|
||||
#[allow(dead_code)]
|
||||
id: String,
|
||||
port: Option<u32>,
|
||||
ws_endpoint: Option<String>,
|
||||
profile_path: Option<String>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
struct CamoufoxNodecarLauncherInner {
|
||||
instances: HashMap<String, CamoufoxInstance>,
|
||||
}
|
||||
|
||||
pub struct CamoufoxNodecarLauncher {
|
||||
inner: Arc<AsyncMutex<CamoufoxNodecarLauncherInner>>,
|
||||
}
|
||||
|
||||
// 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<tauri_plugin_shell::process::Command, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<bool, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<Option<CamoufoxLaunchResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// 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<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
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<String>,
|
||||
) -> Result<CamoufoxLaunchResult, String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<bool>,
|
||||
) -> Result<String, String> {
|
||||
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())
|
||||
}
|
||||
|
||||
+25
-7
@@ -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,
|
||||
|
||||
+38
-16
@@ -172,19 +172,9 @@ export default function Home() {
|
||||
setProcessingUrls((prev) => new Set(prev).add(url));
|
||||
|
||||
try {
|
||||
// Use smart profile selection
|
||||
const result = await invoke<string>("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<string>("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}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+6
-3
@@ -106,12 +106,15 @@ export interface CamoufoxConfig {
|
||||
additional_args?: string[];
|
||||
env_vars?: Record<string, string>;
|
||||
firefox_prefs?: Record<string, unknown>;
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user