checkpoint

This commit is contained in:
zhom
2025-08-06 20:43:14 +04:00
parent 5159f943df
commit a461fd4798
13 changed files with 301 additions and 204 deletions
+1
View File
@@ -99,6 +99,7 @@
"orhun", "orhun",
"orjson", "orjson",
"osascript", "osascript",
"oscpu",
"outpath", "outpath",
"pathex", "pathex",
"pathlib", "pathlib",
+1 -1
View File
@@ -23,8 +23,8 @@
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"@types/node": "^24.1.0", "@types/node": "^24.1.0",
"camoufox-js": "^0.6.2",
"commander": "^14.0.0", "commander": "^14.0.0",
"donutbrowser-camoufox-js": "^0.6.2",
"dotenv": "^17.2.1", "dotenv": "^17.2.1",
"fingerprint-generator": "^2.1.69", "fingerprint-generator": "^2.1.69",
"get-port": "^7.1.0", "get-port": "^7.1.0",
+14 -9
View File
@@ -1,7 +1,7 @@
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import path from "node:path"; import path from "node:path";
import { launchOptions } from "camoufox-js"; import { launchOptions } from "donutbrowser-camoufox-js";
import type { LaunchOptions } from "camoufox-js/dist/utils.js"; import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import { import {
type CamoufoxConfig, type CamoufoxConfig,
deleteCamoufoxConfig, deleteCamoufoxConfig,
@@ -196,7 +196,7 @@ function convertCamoufoxToFingerprintGenerator(
fingerprintObj.fonts = camoufoxFingerprint.fonts; fingerprintObj.fonts = camoufoxFingerprint.fonts;
} }
return fingerprintObj; return { ...camoufoxFingerprint, ...fingerprintObj };
} }
/** /**
@@ -465,7 +465,7 @@ export async function generateCamoufoxConfig(
options: GenerateConfigOptions, options: GenerateConfigOptions,
): Promise<string> { ): Promise<string> {
try { try {
const launchOpts: LaunchOptions = { const launchOpts: any = {
headless: false, headless: false,
i_know_what_im_doing: true, i_know_what_im_doing: true,
config: { config: {
@@ -474,8 +474,8 @@ export async function generateCamoufoxConfig(
}, },
}; };
if (options.geoip !== undefined) { if (options.geoip) {
launchOpts.geoip = options.geoip; launchOpts.geoip = true;
} }
if (options.blockImages) { if (options.blockImages) {
@@ -492,11 +492,19 @@ export async function generateCamoufoxConfig(
launchOpts.executable_path = options.executablePath; launchOpts.executable_path = options.executablePath;
} }
if (options.proxy) {
launchOpts.proxy = options.proxy;
}
// If fingerprint is provided, use it and ignore other options except executable_path and block_* // If fingerprint is provided, use it and ignore other options except executable_path and block_*
if (options.fingerprint) { if (options.fingerprint) {
try { try {
const camoufoxFingerprint = JSON.parse(options.fingerprint); const camoufoxFingerprint = JSON.parse(options.fingerprint);
if (camoufoxFingerprint.timezone) {
launchOpts.config.timezone = camoufoxFingerprint.timezone;
}
// Convert camoufox fingerprint format to fingerprint-generator format // Convert camoufox fingerprint format to fingerprint-generator format
const fingerprintObj = const fingerprintObj =
convertCamoufoxToFingerprintGenerator(camoufoxFingerprint); convertCamoufoxToFingerprintGenerator(camoufoxFingerprint);
@@ -506,9 +514,6 @@ export async function generateCamoufoxConfig(
} }
} else { } else {
// Use individual options to build configuration // Use individual options to build configuration
if (options.proxy) {
launchOpts.proxy = options.proxy;
}
if (options.maxWidth && options.maxHeight) { if (options.maxWidth && options.maxHeight) {
launchOpts.screen = { launchOpts.screen = {
+1 -1
View File
@@ -1,6 +1,6 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import type { LaunchOptions } from "camoufox-js/dist/utils.js"; import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import tmp from "tmp"; import tmp from "tmp";
export interface CamoufoxConfig { export interface CamoufoxConfig {
+21 -5
View File
@@ -1,8 +1,8 @@
import { launchOptions } from "camoufox-js"; import { launchOptions } from "donutbrowser-camoufox-js";
import type { LaunchOptions } from "camoufox-js/dist/utils.js"; import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import { type Browser, type BrowserServer, firefox } from "playwright-core"; import { type Browser, type BrowserServer, firefox } from "playwright-core";
import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js"; import { getCamoufoxConfig, saveCamoufoxConfig } from "./camoufox-storage.js";
import { getEnvVars } from "./utils.js"; import { getEnvVars, parseProxyString } from "./utils.js";
/** /**
* Run a Camoufox browser server as a worker process * Run a Camoufox browser server as a worker process
@@ -128,15 +128,31 @@ export async function runCamoufoxWorker(id: string): Promise<void> {
"Failed to parse custom config, using generated config:", "Failed to parse custom config, using generated config:",
error, error,
); );
return;
} }
} }
// Launch the server with the final configuration // Launch the server with the final configuration
server = await firefox.launchServer({ const finalOptions: any = {
...generatedOptions, ...generatedOptions,
wsPath: `/ws_${config.id}`, wsPath: `/ws_${config.id}`,
env: finalEnv, env: finalEnv,
}); };
// Only add proxy if it exists and is valid
if (camoufoxOptions.proxy) {
try {
finalOptions.proxy = parseProxyString(camoufoxOptions.proxy);
} catch (error) {
console.error({
message: "Failed to parse proxy, launching without proxy",
error,
});
return;
}
}
server = await firefox.launchServer(finalOptions);
// Connect to the server // Connect to the server
browser = await firefox.connect(server.wsEndpoint()); browser = await firefox.connect(server.wsEndpoint());
+16 -29
View File
@@ -1,5 +1,5 @@
import type { LaunchOptions } from "camoufox-js/dist/utils.js";
import { program } from "commander"; import { program } from "commander";
import type { LaunchOptions } from "donutbrowser-camoufox-js/dist/utils.js";
import { import {
generateCamoufoxConfig, generateCamoufoxConfig,
startCamoufoxProcess, startCamoufoxProcess,
@@ -165,7 +165,7 @@ program
.option("--proxy <proxy>", "proxy URL for config generation") .option("--proxy <proxy>", "proxy URL for config generation")
.option("--max-width <width>", "maximum screen width", parseInt) .option("--max-width <width>", "maximum screen width", parseInt)
.option("--max-height <height>", "maximum screen height", parseInt) .option("--max-height <height>", "maximum screen height", parseInt)
.option("--geoip [ip]", "enable geoip or specify IP") .option("--geoip", "enable geoip")
.option("--block-images", "block images") .option("--block-images", "block images")
.option("--block-webrtc", "block WebRTC") .option("--block-webrtc", "block WebRTC")
.option("--block-webgl", "block WebGL") .option("--block-webgl", "block WebGL")
@@ -325,7 +325,6 @@ program
}), }),
); );
process.exit(1); process.exit(1);
return;
} }
} }
@@ -374,22 +373,6 @@ program
process.exit(0); process.exit(0);
} else if (action === "generate-config") { } else if (action === "generate-config") {
try { 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({ const config = await generateCamoufoxConfig({
proxy: proxy:
typeof options.proxy === "string" ? options.proxy : undefined, typeof options.proxy === "string" ? options.proxy : undefined,
@@ -401,7 +384,7 @@ program
typeof options.maxHeight === "number" typeof options.maxHeight === "number"
? options.maxHeight ? options.maxHeight
: undefined, : undefined,
geoip: geoipValue, geoip: Boolean(options.geoip),
blockImages: blockImages:
typeof options.blockImages === "boolean" typeof options.blockImages === "boolean"
? options.blockImages ? options.blockImages
@@ -426,17 +409,18 @@ program
console.log(config); console.log(config);
process.exit(0); process.exit(0);
} catch (error: unknown) { } catch (error: unknown) {
console.error( console.error({
`Failed to generate config: ${ error: "Failed to generate config",
error instanceof Error ? error.message : JSON.stringify(error) message:
}`, error instanceof Error ? error.message : JSON.stringify(error),
); });
process.exit(1); process.exit(1);
} }
} else { } else {
console.error( console.error({
"Invalid action. Use 'start', 'stop', 'list', or 'generate-config'", error: "Invalid action",
); message: "Use 'start', 'stop', 'list', or 'generate-config'",
});
process.exit(1); process.exit(1);
} }
}, },
@@ -452,7 +436,10 @@ program
if (action === "start") { if (action === "start") {
await runCamoufoxWorker(options.id); await runCamoufoxWorker(options.id);
} else { } else {
console.error("Invalid action for camoufox-worker. Use 'start'"); console.error({
error: "Invalid action for camoufox-worker",
message: "Use 'start'",
});
process.exit(1); process.exit(1);
} }
}); });
+80
View File
@@ -1,3 +1,5 @@
import type { LaunchOptions } from "playwright-core";
const OS_MAP: { [key: string]: "mac" | "win" | "lin" } = { const OS_MAP: { [key: string]: "mac" | "win" | "lin" } = {
darwin: "mac", darwin: "mac",
linux: "lin", linux: "lin",
@@ -35,3 +37,81 @@ export function getEnvVars(configMap: Record<string, string>) {
return envVars; return envVars;
} }
export function parseProxyString(proxyString: LaunchOptions["proxy"] | string) {
if (typeof proxyString === "object") {
return proxyString;
}
if (!proxyString || typeof proxyString !== "string") {
throw new Error("Invalid proxy string provided");
}
// Remove any leading/trailing whitespace
const trimmed = proxyString.trim();
// Handle different proxy string formats:
// 1. http://username:password@host:port
// 2. host:port
// 3. protocol://host:port
// 4. username:password@host:port
let server = "";
let username: string | undefined;
let password: string | undefined;
try {
// Try parsing as URL first (handles protocol://username:password@host:port)
if (trimmed.includes("://")) {
const url = new URL(trimmed);
server = `${url.hostname}:${url.port}`;
if (url.username) {
username = decodeURIComponent(url.username);
}
if (url.password) {
password = decodeURIComponent(url.password);
}
} else {
// Handle formats without protocol
let workingString = trimmed;
// Check for username:password@ prefix
const authMatch = workingString.match(/^([^:@]+):([^@]+)@(.+)$/);
if (authMatch) {
username = authMatch[1];
password = authMatch[2];
workingString = authMatch[3];
}
// The remaining part should be host:port
server = workingString;
}
// Validate that we have a server
if (!server) {
throw new Error("Could not extract server information");
}
// Basic validation for host:port format
if (!server.includes(":") || server.split(":").length !== 2) {
throw new Error("Server must be in host:port format");
}
const result: LaunchOptions["proxy"] = { server };
if (username !== undefined) {
result.username = username;
}
if (password !== undefined) {
result.password = password;
}
return result;
} catch (error) {
throw new Error(
`Failed to parse proxy string: ${error instanceof Error ? error.message : "Unknown error"}`,
);
}
}
+35 -35
View File
@@ -144,12 +144,12 @@ importers:
'@types/node': '@types/node':
specifier: ^24.1.0 specifier: ^24.1.0
version: 24.1.0 version: 24.1.0
camoufox-js:
specifier: ^0.6.2
version: 0.6.2(encoding@0.1.13)(playwright-core@1.54.2)
commander: commander:
specifier: ^14.0.0 specifier: ^14.0.0
version: 14.0.0 version: 14.0.0
donutbrowser-camoufox-js:
specifier: ^0.6.2
version: 0.6.2(encoding@0.1.13)(playwright-core@1.54.2)
dotenv: dotenv:
specifier: ^17.2.1 specifier: ^17.2.1
version: 17.2.1 version: 17.2.1
@@ -1614,12 +1614,6 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
camoufox-js@0.6.2:
resolution: {integrity: sha512-oeQigZ8+TsV34kJguKnwqALiL+go9IXA/3Opyw2P+VQSDWoFDTfc/pKEmkhrpX7xBPtcbarcnU7PnPmQyebNIg==}
hasBin: true
peerDependencies:
playwright-core: '*'
caniuse-lite@1.0.30001731: caniuse-lite@1.0.30001731:
resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==}
@@ -1763,6 +1757,12 @@ packages:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'} engines: {node: '>=0.3.1'}
donutbrowser-camoufox-js@0.6.2:
resolution: {integrity: sha512-nIgofMkGnPJgLSqmclOS6wYtcpkfsjnkCALeF4gNjuB9KWZBBHoEyxr9Lb5flG1Fwrx8X3C0gmyvDaQ/s/Vgjw==}
hasBin: true
peerDependencies:
playwright-core: '*'
dot-prop@6.0.1: dot-prop@6.0.1:
resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2256,8 +2256,8 @@ packages:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
maxmind@4.3.28: maxmind@4.3.29:
resolution: {integrity: sha512-K3PcTVjhrSU6xzY7niQf/CHPmx/qzkMIpMumd3x0JXMkIDJMerL4LY9RsEhArHb4T3IK0NBxQcUlwjsxRm9rIw==} resolution: {integrity: sha512-Vxx0rh7omBekjZnsDxpw35SrGGM3Uy7NoIKWzZlvh3UcjihGaySR8n+YSQ8YBseCvhEn+yehA98rZkTDW+uhPw==}
engines: {node: '>=12', npm: '>=6'} engines: {node: '>=12', npm: '>=6'}
micromatch@4.0.8: micromatch@4.0.8:
@@ -2774,8 +2774,8 @@ packages:
tauri-plugin-macos-permissions-api@2.3.0: tauri-plugin-macos-permissions-api@2.3.0:
resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==} resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==}
tiny-lru@11.3.3: tiny-lru@11.3.4:
resolution: {integrity: sha512-/ShxBZOgHXDdZi7FxajcsH0MfcBqwP+t7i4T3PGjI//NUA5aCpC7cB9bbdAYrAeQLBUTJfg2rk191fzZGeo7DA==} resolution: {integrity: sha512-UxWEfRKpFCabAf6fkTNdlfSw/RDUJ/4C6i1aLZaDnGF82PERHyYhz5CMCVYXtLt34LbqgfpJ2bjmgGKgxuF/6A==}
engines: {node: '>=12'} engines: {node: '>=12'}
tinyglobby@0.2.14: tinyglobby@0.2.14:
@@ -4258,25 +4258,6 @@ snapshots:
callsites@3.1.0: {} callsites@3.1.0: {}
camoufox-js@0.6.2(encoding@0.1.13)(playwright-core@1.54.2):
dependencies:
adm-zip: 0.5.16
commander: 13.1.0
fingerprint-generator: 2.1.69
impit: 0.5.3
js-yaml: 4.1.0
language-tags: 2.1.0
maxmind: 4.3.28
playwright-core: 1.54.2
progress: 2.0.3
sqlite3: 5.1.7
ua-parser-js: 2.0.4(encoding@0.1.13)
xml2js: 0.6.2
transitivePeerDependencies:
- bluebird
- encoding
- supports-color
caniuse-lite@1.0.30001731: {} caniuse-lite@1.0.30001731: {}
chalk@4.1.2: chalk@4.1.2:
@@ -4405,6 +4386,25 @@ snapshots:
diff@4.0.2: {} diff@4.0.2: {}
donutbrowser-camoufox-js@0.6.2(encoding@0.1.13)(playwright-core@1.54.2):
dependencies:
adm-zip: 0.5.16
commander: 13.1.0
fingerprint-generator: 2.1.69
impit: 0.5.3
js-yaml: 4.1.0
language-tags: 2.1.0
maxmind: 4.3.29
playwright-core: 1.54.2
progress: 2.0.3
sqlite3: 5.1.7
ua-parser-js: 2.0.4(encoding@0.1.13)
xml2js: 0.6.2
transitivePeerDependencies:
- bluebird
- encoding
- supports-color
dot-prop@6.0.1: dot-prop@6.0.1:
dependencies: dependencies:
is-obj: 2.0.0 is-obj: 2.0.0
@@ -4901,10 +4901,10 @@ snapshots:
math-intrinsics@1.1.0: {} math-intrinsics@1.1.0: {}
maxmind@4.3.28: maxmind@4.3.29:
dependencies: dependencies:
mmdb-lib: 2.2.1 mmdb-lib: 2.2.1
tiny-lru: 11.3.3 tiny-lru: 11.3.4
micromatch@4.0.8: micromatch@4.0.8:
dependencies: dependencies:
@@ -5488,7 +5488,7 @@ snapshots:
dependencies: dependencies:
'@tauri-apps/api': 2.7.0 '@tauri-apps/api': 2.7.0
tiny-lru@11.3.3: {} tiny-lru@11.3.4: {}
tinyglobby@0.2.14: tinyglobby@0.2.14:
dependencies: dependencies:
+27 -8
View File
@@ -77,6 +77,28 @@ impl BrowserRunner {
profile_manager.get_profiles_dir() profile_manager.get_profiles_dir()
} }
/// Get the executable path for a browser profile
/// This is a common helper to eliminate code duplication across the codebase
pub fn get_browser_executable_path(
&self,
profile: &BrowserProfile,
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
// Create browser instance to get executable path
let browser_type = crate::browser::BrowserType::from_str(&profile.browser)
.map_err(|e| format!("Invalid browser type: {e}"))?;
let browser = crate::browser::create_browser(browser_type);
// Construct browser directory path: binaries/<browser>/<version>/
let mut browser_dir = self.get_binaries_dir();
browser_dir.push(&profile.browser);
browser_dir.push(&profile.version);
// Get platform-specific executable path
browser
.get_executable_path(&browser_dir)
.map_err(|e| format!("Failed to get executable path for {}: {e}", profile.browser).into())
}
/// Internal method to cleanup unused binaries (used by auto-cleanup) /// Internal method to cleanup unused binaries (used by auto-cleanup)
pub fn cleanup_unused_binaries_internal( pub fn cleanup_unused_binaries_internal(
&self, &self,
@@ -242,16 +264,13 @@ impl BrowserRunner {
.map_err(|_| format!("Invalid browser type: {}", profile.browser))?; .map_err(|_| format!("Invalid browser type: {}", profile.browser))?;
let browser = create_browser(browser_type.clone()); let browser = create_browser(browser_type.clone());
// Get executable path - path structure: binaries/<browser>/<version>/ // Get executable path using common helper
let mut browser_dir = self.get_binaries_dir(); let executable_path = self
browser_dir.push(&profile.browser); .get_browser_executable_path(profile)
browser_dir.push(&profile.version);
println!("Browser directory: {browser_dir:?}");
let executable_path = browser
.get_executable_path(&browser_dir)
.expect("Failed to get executable path"); .expect("Failed to get executable path");
println!("Executable path: {executable_path:?}");
// Prepare the executable (set permissions, etc.) // Prepare the executable (set permissions, etc.)
if let Err(e) = browser.prepare_executable(&executable_path) { if let Err(e) = browser.prepare_executable(&executable_path) {
println!("Warning: Failed to prepare executable: {e}"); println!("Warning: Failed to prepare executable: {e}");
+52 -101
View File
@@ -92,30 +92,32 @@ impl CamoufoxNodecarLauncher {
pub async fn generate_fingerprint_config( pub async fn generate_fingerprint_config(
&self, &self,
app_handle: &AppHandle, app_handle: &AppHandle,
profile: &crate::profile::BrowserProfile,
config: &CamoufoxConfig, config: &CamoufoxConfig,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()]; let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()];
// For fingerprint generation during profile creation, we can pass proxy directly // Always ensure executable_path is set to the user's binary location
// but we set geoip to false during tests to avoid network requests let executable_path = if let Some(path) = &config.executable_path {
if std::env::var("CAMOUFOX_TEST").is_ok() { path.clone()
config_args.extend(["--geoip".to_string(), "false".to_string()]);
} else if let Some(geoip) = &config.geoip {
match geoip {
serde_json::Value::Bool(true) => {
config_args.extend(["--geoip".to_string(), "true".to_string()]);
}
serde_json::Value::Bool(false) => {
config_args.extend(["--geoip".to_string(), "false".to_string()]);
}
serde_json::Value::String(ip) => {
config_args.extend(["--geoip".to_string(), ip.clone()]);
}
_ => {}
}
} else { } else {
// Default to true for fingerprint generation // Use the browser runner helper with the real profile
config_args.extend(["--geoip".to_string(), "true".to_string()]); let browser_runner = crate::browser_runner::BrowserRunner::instance();
browser_runner
.get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
.to_string_lossy()
.to_string()
};
config_args.extend(["--executable-path".to_string(), executable_path]);
// Pass existing fingerprint if provided (for advanced form partial fingerprints)
if let Some(fingerprint) = &config.fingerprint {
config_args.extend(["--fingerprint".to_string(), fingerprint.clone()]);
}
if let Some(serde_json::Value::Bool(true)) = &config.geoip {
config_args.push("--geoip".to_string());
} }
// Add proxy if provided (can be passed directly during fingerprint generation) // Add proxy if provided (can be passed directly during fingerprint generation)
@@ -132,7 +134,7 @@ impl CamoufoxNodecarLauncher {
config_args.extend(["--max-height".to_string(), max_height.to_string()]); config_args.extend(["--max-height".to_string(), max_height.to_string()]);
} }
// Add block_* and executable_path options // Add block_* options
if let Some(block_images) = config.block_images { if let Some(block_images) = config.block_images {
if block_images { if block_images {
config_args.push("--block-images".to_string()); config_args.push("--block-images".to_string());
@@ -151,10 +153,6 @@ impl CamoufoxNodecarLauncher {
} }
} }
if let Some(executable_path) = &config.executable_path {
config_args.extend(["--executable-path".to_string(), executable_path.clone()]);
}
// Execute config generation command // Execute config generation command
let mut config_sidecar = self.get_nodecar_sidecar(app_handle)?; let mut config_sidecar = self.get_nodecar_sidecar(app_handle)?;
for arg in &config_args { for arg in &config_args {
@@ -186,84 +184,29 @@ impl CamoufoxNodecarLauncher {
pub async fn launch_camoufox( pub async fn launch_camoufox(
&self, &self,
app_handle: &AppHandle, app_handle: &AppHandle,
profile: &crate::profile::BrowserProfile,
profile_path: &str, profile_path: &str,
config: &CamoufoxConfig, config: &CamoufoxConfig,
url: Option<&str>, url: Option<&str>,
) -> Result<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> { ) -> Result<CamoufoxLaunchResult, Box<dyn std::error::Error + Send + Sync>> {
// Generate or use existing configuration
let custom_config = if let Some(existing_fingerprint) = &config.fingerprint { let custom_config = if let Some(existing_fingerprint) = &config.fingerprint {
// Use existing fingerprint from profile metadata
println!("Using existing fingerprint from profile metadata"); println!("Using existing fingerprint from profile metadata");
existing_fingerprint.clone() existing_fingerprint.clone()
} else { } else {
// Generate new configuration using nodecar generate-config command return Err("No fingerprint provided".into());
println!("Generating new fingerprint configuration"); };
let mut config_args = vec!["camoufox".to_string(), "generate-config".to_string()];
// Use individual options to build configuration // Always ensure executable_path is set to the user's binary location
if let Some(proxy) = &config.proxy { let executable_path = if let Some(path) = &config.executable_path {
config_args.extend(["--proxy".to_string(), proxy.clone()]); path.clone()
} } else {
// Use the browser runner helper with the real profile
if let Some(max_width) = config.screen_max_width { let browser_runner = crate::browser_runner::BrowserRunner::instance();
config_args.extend(["--max-width".to_string(), max_width.to_string()]); browser_runner
} .get_browser_executable_path(profile)
.map_err(|e| format!("Failed to get Camoufox executable path: {e}"))?
if let Some(max_height) = config.screen_max_height { .to_string_lossy()
config_args.extend(["--max-height".to_string(), max_height.to_string()]); .to_string()
}
if let Some(geoip) = &config.geoip {
match geoip {
serde_json::Value::Bool(true) => {
config_args.extend(["--geoip".to_string(), "true".to_string()]);
}
serde_json::Value::Bool(false) => {
config_args.extend(["--geoip".to_string(), "false".to_string()]);
}
serde_json::Value::String(ip) => {
config_args.extend(["--geoip".to_string(), ip.clone()]);
}
_ => {}
}
}
// Always add block_* and executable_path options
if let Some(block_images) = config.block_images {
if block_images {
config_args.push("--block-images".to_string());
}
}
if let Some(block_webrtc) = config.block_webrtc {
if block_webrtc {
config_args.push("--block-webrtc".to_string());
}
}
if let Some(block_webgl) = config.block_webgl {
if block_webgl {
config_args.push("--block-webgl".to_string());
}
}
if let Some(executable_path) = &config.executable_path {
config_args.extend(["--executable-path".to_string(), executable_path.clone()]);
}
// Execute config generation command
let mut config_sidecar = self.get_nodecar_sidecar(app_handle)?;
for arg in &config_args {
config_sidecar = config_sidecar.arg(arg);
}
let config_output = config_sidecar.output().await?;
if !config_output.status.success() {
let stderr = String::from_utf8_lossy(&config_output.stderr);
return Err(format!("Failed to generate camoufox config: {stderr}").into());
}
String::from_utf8_lossy(&config_output.stdout).to_string()
}; };
// Build nodecar command arguments // Build nodecar command arguments
@@ -277,6 +220,9 @@ impl CamoufoxNodecarLauncher {
args.extend(["--url".to_string(), url.to_string()]); args.extend(["--url".to_string(), url.to_string()]);
} }
// Always add the executable path
args.extend(["--executable-path".to_string(), executable_path]);
// Always add the generated custom config // Always add the generated custom config
args.extend(["--custom-config".to_string(), custom_config]); args.extend(["--custom-config".to_string(), custom_config]);
@@ -392,7 +338,7 @@ impl CamoufoxNodecarLauncher {
// Verify the server is actually running by checking the process // Verify the server is actually running by checking the process
if let Some(process_id) = instance.process_id { if let Some(process_id) = instance.process_id {
if self.is_server_running(process_id).await { if self.is_server_running(process_id).await {
println!("Found running Camoufox instance for profile: {profile_path}"); // Found running Camoufox instance
return Ok(Some(CamoufoxLaunchResult { return Ok(Some(CamoufoxLaunchResult {
id: id.clone(), id: id.clone(),
processId: instance.process_id, processId: instance.process_id,
@@ -400,14 +346,13 @@ impl CamoufoxNodecarLauncher {
url: instance.url.clone(), url: instance.url.clone(),
})); }));
} else { } else {
println!("Camoufox instance found but process is not running: {id}"); // Camoufox instance found but process is not running
} }
} }
} }
} }
} }
println!("No running Camoufox instance found for profile: {profile_path}");
Ok(None) Ok(None)
} }
@@ -426,13 +371,13 @@ impl CamoufoxNodecarLauncher {
// Check if the process is still alive // Check if the process is still alive
if !self.is_server_running(process_id).await { if !self.is_server_running(process_id).await {
// Process is dead // Process is dead
println!("Camoufox instance {id} (PID: {process_id}) is no longer running"); // Camoufox instance is no longer running
dead_instances.push(id.clone()); dead_instances.push(id.clone());
instances_to_remove.push(id.clone()); instances_to_remove.push(id.clone());
} }
} else { } else {
// No process_id means it's likely a dead instance // No process_id means it's likely a dead instance
println!("Camoufox instance {id} has no PID, marking as dead"); // Camoufox instance has no PID, marking as dead
dead_instances.push(id.clone()); dead_instances.push(id.clone());
instances_to_remove.push(id.clone()); instances_to_remove.push(id.clone());
} }
@@ -444,7 +389,7 @@ impl CamoufoxNodecarLauncher {
let mut inner = self.inner.lock().await; let mut inner = self.inner.lock().await;
for id in &instances_to_remove { for id in &instances_to_remove {
inner.instances.remove(id); inner.instances.remove(id);
println!("Removed dead Camoufox instance: {id}"); // Removed dead Camoufox instance
} }
} }
@@ -466,7 +411,7 @@ impl CamoufoxNodecarLauncher {
}); });
if is_camoufox { if is_camoufox {
println!("Found running Camoufox process with PID: {process_id}"); // Found running Camoufox process
return true; return true;
} }
} }
@@ -499,7 +444,13 @@ impl CamoufoxNodecarLauncher {
let _ = self.cleanup_dead_instances().await; let _ = self.cleanup_dead_instances().await;
self self
.launch_camoufox(&app_handle, &profile_path_str, &config, url.as_deref()) .launch_camoufox(
&app_handle,
&profile,
&profile_path_str,
&config,
url.as_deref(),
)
.await .await
.map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}")) .map_err(|e| format!("Failed to launch Camoufox via nodecar: {e}"))
} }
+2 -7
View File
@@ -376,13 +376,8 @@ pub fn run() {
interval.tick().await; interval.tick().await;
match launcher.cleanup_dead_instances().await { match launcher.cleanup_dead_instances().await {
Ok(dead_instances) => { Ok(_dead_instances) => {
if !dead_instances.is_empty() { // Cleanup completed silently
println!(
"Cleaned up {} dead Camoufox instances",
dead_instances.len()
);
}
} }
Err(e) => { Err(e) => {
eprintln!("Error during Camoufox cleanup: {e}"); eprintln!("Error during Camoufox cleanup: {e}");
+47 -7
View File
@@ -100,11 +100,37 @@ impl ProfileManager {
crate::camoufox::CamoufoxConfig::default() crate::camoufox::CamoufoxConfig::default()
}); });
// Always ensure executable_path is set to the user's binary location
if config.executable_path.is_none() {
let browser_runner = crate::browser_runner::BrowserRunner::instance();
let mut browser_dir = browser_runner.get_binaries_dir();
browser_dir.push(browser);
browser_dir.push(version);
#[cfg(target_os = "macos")]
let binary_path = browser_dir
.join("Camoufox.app")
.join("Contents")
.join("MacOS")
.join("camoufox");
#[cfg(target_os = "windows")]
let binary_path = browser_dir.join("camoufox.exe");
#[cfg(target_os = "linux")]
let binary_path = browser_dir.join("camoufox");
config.executable_path = Some(binary_path.to_string_lossy().to_string());
println!("Set Camoufox executable path: {:?}", config.executable_path);
}
// Pass upstream proxy information to config for fingerprint generation // Pass upstream proxy information to config for fingerprint generation
if let Some(proxy_id_ref) = &proxy_id { if let Some(proxy_id_ref) = &proxy_id {
if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) { if let Some(proxy_settings) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id_ref) {
// For fingerprint generation, pass upstream proxy directly with credentials if present // For fingerprint generation, pass upstream proxy directly with credentials if present
let proxy_url = if let (Some(username), Some(password)) = (&proxy_settings.username, &proxy_settings.password) { let proxy_url = if let (Some(username), Some(password)) =
(&proxy_settings.username, &proxy_settings.password)
{
format!( format!(
"{}://{}:{}@{}:{}", "{}://{}:{}@{}:{}",
proxy_settings.proxy_type.to_lowercase(), proxy_settings.proxy_type.to_lowercase(),
@@ -137,8 +163,23 @@ impl ProfileManager {
// Use the camoufox launcher to generate the config // Use the camoufox launcher to generate the config
let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance(); let camoufox_launcher = crate::camoufox::CamoufoxNodecarLauncher::instance();
// Create a temporary profile for fingerprint generation
let temp_profile = BrowserProfile {
id: uuid::Uuid::new_v4(),
name: name.to_string(),
browser: browser.to_string(),
version: version.to_string(),
proxy_id: proxy_id.clone(),
process_id: None,
last_launch: None,
release_type: release_type.to_string(),
camoufox_config: None,
group_id: group_id.clone(),
};
match camoufox_launcher match camoufox_launcher
.generate_fingerprint_config(app_handle, &config) .generate_fingerprint_config(app_handle, &temp_profile, &config)
.await .await
{ {
Ok(generated_fingerprint) => { Ok(generated_fingerprint) => {
@@ -146,7 +187,9 @@ impl ProfileManager {
println!("Successfully generated fingerprint for profile: {name}"); println!("Successfully generated fingerprint for profile: {name}");
} }
Err(e) => { Err(e) => {
return Err(format!("Failed to generate fingerprint for Camoufox profile '{name}': {e}").into()); return Err(
format!("Failed to generate fingerprint for Camoufox profile '{name}': {e}").into(),
);
} }
} }
} else { } else {
@@ -639,10 +682,7 @@ impl ProfileManager {
if profile_path_match { if profile_path_match {
is_running = true; is_running = true;
found_pid = Some(pid); found_pid = Some(pid);
println!( // Found existing browser process
"Found existing browser process with PID: {} for profile: {}",
pid, profile.name
);
} else { } else {
println!("PID {pid} exists but doesn't match our profile path exactly, searching for correct process..."); println!("PID {pid} exists but doesn't match our profile path exactly, searching for correct process...");
} }
+4 -1
View File
@@ -67,9 +67,12 @@ export function ProxySettingsDialog({
loadStoredProxies(); loadStoredProxies();
if (isProxyDisabled) { if (isProxyDisabled) {
setSelectedProxyId(null); setSelectedProxyId(null);
} else {
// Reset to initial proxy ID when dialog opens
setSelectedProxyId(initialProxyId || null);
} }
} }
}, [isOpen, isProxyDisabled, loadStoredProxies]); }, [isOpen, isProxyDisabled, loadStoredProxies, initialProxyId]);
const handleCreateProxy = useCallback(() => { const handleCreateProxy = useCallback(() => {
setShowProxyForm(true); setShowProxyForm(true);