From 77a50c60d12209265cffea4b5aad3620cfa6c77b Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 3 Aug 2025 01:08:59 +0400 Subject: [PATCH] refactor: launch all browsers via proxy --- nodecar/src/index.ts | 12 +- nodecar/src/proxy-runner.ts | 8 +- nodecar/src/proxy-storage.ts | 2 +- nodecar/src/proxy-worker.ts | 4 + src-tauri/src/browser_runner.rs | 165 ++++++++++++++----------- src-tauri/src/profile/manager.rs | 7 +- src-tauri/src/proxy_manager.rs | 62 ++++++---- src-tauri/tests/nodecar_integration.rs | 71 +++++++++++ 8 files changed, 217 insertions(+), 114 deletions(-) diff --git a/nodecar/src/index.ts b/nodecar/src/index.ts index dbd4d48..1c48d12 100644 --- a/nodecar/src/index.ts +++ b/nodecar/src/index.ts @@ -52,7 +52,7 @@ program }, ) => { if (action === "start") { - let upstreamUrl: string; + let upstreamUrl: string | undefined; // Build upstream URL from individual components if provided if (options.host && options.proxyPort && options.type) { @@ -69,16 +69,8 @@ program upstreamUrl = `${protocol}://${auth}${options.host}:${options.proxyPort}`; } else if (options.upstream) { upstreamUrl = options.upstream; - } else { - console.error( - "Error: Either --upstream URL or --host, --proxy-port, and --type are required", - ); - console.log( - "Example: proxy start --host proxy.example.com --proxy-port 9000 --type http --username user --password pass", - ); - process.exit(1); - return; } + // If no upstream is provided, create a direct proxy try { const config = await startProxyProcess(upstreamUrl, { diff --git a/nodecar/src/proxy-runner.ts b/nodecar/src/proxy-runner.ts index 8b34172..7022874 100644 --- a/nodecar/src/proxy-runner.ts +++ b/nodecar/src/proxy-runner.ts @@ -2,23 +2,23 @@ import { spawn } from "node:child_process"; import path from "node:path"; import getPort from "get-port"; import { - type ProxyConfig, deleteProxyConfig, generateProxyId, getProxyConfig, isProcessRunning, listProxyConfigs, + type ProxyConfig, saveProxyConfig, } from "./proxy-storage"; /** * Start a proxy in a separate process - * @param upstreamUrl The upstream proxy URL + * @param upstreamUrl The upstream proxy URL (optional for direct proxy) * @param options Optional configuration * @returns Promise resolving to the proxy configuration */ export async function startProxyProcess( - upstreamUrl: string, + upstreamUrl?: string, options: { port?: number; ignoreProxyCertificate?: boolean } = {}, ): Promise { // Generate a unique ID for this proxy @@ -30,7 +30,7 @@ export async function startProxyProcess( // Create the proxy configuration const config: ProxyConfig = { id, - upstreamUrl, + upstreamUrl: upstreamUrl || "DIRECT", localPort: port, ignoreProxyCertificate: options.ignoreProxyCertificate ?? false, }; diff --git a/nodecar/src/proxy-storage.ts b/nodecar/src/proxy-storage.ts index 94a9ef5..9a25ee2 100644 --- a/nodecar/src/proxy-storage.ts +++ b/nodecar/src/proxy-storage.ts @@ -4,7 +4,7 @@ import tmp from "tmp"; export interface ProxyConfig { id: string; - upstreamUrl: string; + upstreamUrl: string; // Can be "DIRECT" for direct proxy localPort?: number; ignoreProxyCertificate?: boolean; localUrl?: string; diff --git a/nodecar/src/proxy-worker.ts b/nodecar/src/proxy-worker.ts index 4ccc295..3b0e435 100644 --- a/nodecar/src/proxy-worker.ts +++ b/nodecar/src/proxy-worker.ts @@ -19,6 +19,10 @@ export async function runProxyWorker(id: string): Promise { port: config.localPort, host: "127.0.0.1", prepareRequestFunction: () => { + // If upstreamUrl is "DIRECT", don't use upstream proxy + if (config.upstreamUrl === "DIRECT") { + return {}; + } return { upstreamProxyUrl: config.upstreamUrl, ignoreUpstreamProxyCertificate: config.ignoreProxyCertificate ?? false, diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 14ee3d3..3cb02e7 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -137,7 +137,7 @@ impl BrowserRunner { app_handle: tauri::AppHandle, profile: &BrowserProfile, url: Option, - local_proxy_settings: Option<&ProxySettings>, + _local_proxy_settings: Option<&ProxySettings>, ) -> Result> { // Check if browser is disabled due to ongoing update let auto_updater = crate::auto_updater::AutoUpdater::instance(); @@ -154,60 +154,42 @@ impl BrowserRunner { // 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 - if let Some(proxy_id) = &profile.proxy_id { - if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { - println!("Starting proxy for Camoufox profile: {}", profile.name); + // Always start a local proxy for Camoufox (for traffic monitoring and geoip support) + let upstream_proxy = profile + .proxy_id + .as_ref() + .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); - // Start the proxy and get local proxy settings - let local_proxy = PROXY_MANAGER - .start_proxy( - app_handle.clone(), - &stored_proxy, - 0, // Use 0 as temporary PID, will be updated later - Some(&profile.name), - ) - .await - .map_err(|e| format!("Failed to start proxy for Camoufox: {e}"))?; + println!( + "Starting local proxy for Camoufox profile: {} (upstream: {})", + profile.name, + upstream_proxy + .as_ref() + .map(|p| format!("{}:{}", p.host, p.port)) + .unwrap_or_else(|| "DIRECT".to_string()) + ); - // Format proxy URL for camoufox - let proxy_url = format!( - "{}://{}:{}", - if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" { - &stored_proxy.proxy_type - } else { - "http" - }, - local_proxy.host, - local_proxy.port - ); + // Start the proxy and get local proxy settings + let local_proxy = PROXY_MANAGER + .start_proxy( + app_handle.clone(), + upstream_proxy.as_ref(), + 0, // Use 0 as temporary PID, will be updated later + Some(&profile.name), + ) + .await + .map_err(|e| format!("Failed to start local proxy for Camoufox: {e}"))?; - // Add username and password if available - let proxy_url = if let (Some(username), Some(password)) = - (&stored_proxy.username, &stored_proxy.password) - { - format!( - "{}://{}:{}@{}:{}", - if stored_proxy.proxy_type == "socks5" || stored_proxy.proxy_type == "socks4" { - &stored_proxy.proxy_type - } else { - "http" - }, - username, - password, - local_proxy.host, - local_proxy.port - ) - } else { - proxy_url - }; + // Format proxy URL for camoufox - always use HTTP for the local proxy + let proxy_url = format!("http://{}:{}", local_proxy.host, local_proxy.port); - // Set proxy in camoufox config - camoufox_config.proxy = Some(proxy_url); + // Set proxy in camoufox config + camoufox_config.proxy = Some(proxy_url); - println!("Configured proxy for Camoufox: {:?}", camoufox_config.proxy); - } - } + println!( + "Configured local proxy for Camoufox: {:?}", + camoufox_config.proxy + ); // Use the existing config or create a test config if none exists let final_config = if camoufox_config.timezone.is_some() @@ -289,18 +271,14 @@ impl BrowserRunner { // Continue anyway, the error might not be critical } - // For Chromium browsers, use local proxy settings if available - // For Firefox browsers, proxy settings are handled via PAC files - let stored_proxy_settings = profile + // Get stored proxy settings for later use (removed as we handle this in proxy startup) + let _stored_proxy_settings = profile .proxy_id .as_ref() .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); - let proxy_for_launch_args = match browser_type { - BrowserType::Chromium | BrowserType::Brave => { - local_proxy_settings.or(stored_proxy_settings.as_ref()) - } - _ => None, // Firefox browsers use PAC files, not launch args - }; + + // For now, don't use proxy in launch args - we'll set it up after launch + let proxy_for_launch_args: Option<&ProxySettings> = None; // Get profile data path and launch arguments let profiles_dir = self.get_profiles_dir(); @@ -399,24 +377,56 @@ impl BrowserRunner { // which is already handled in the profile creation process } - // Start proxy if configured and needed (for Chromium-based browsers) - if let Some(proxy_id) = &profile.proxy_id { - if let Some(stored_proxy) = PROXY_MANAGER.get_proxy_settings_by_id(proxy_id) { - println!("Starting proxy for profile: {}", profile.name); + // Always start a local proxy for traffic monitoring and potential upstream routing + let upstream_proxy = profile + .proxy_id + .as_ref() + .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)); - match PROXY_MANAGER - .start_proxy( - app_handle.clone(), - &stored_proxy, - actual_pid, - Some(&profile.name), - ) - .await - { - Ok(_) => println!("Proxy started successfully for profile: {}", profile.name), - Err(e) => println!("Warning: Failed to start proxy: {e}"), + println!( + "Starting local proxy for profile: {} (upstream: {})", + profile.name, + upstream_proxy + .as_ref() + .map(|p| format!("{}:{}", p.host, p.port)) + .unwrap_or_else(|| "DIRECT".to_string()) + ); + + match PROXY_MANAGER + .start_proxy( + app_handle.clone(), + upstream_proxy.as_ref(), + actual_pid, + Some(&profile.name), + ) + .await + { + Ok(local_proxy) => { + println!( + "Local proxy started successfully for profile: {} on port: {}", + profile.name, local_proxy.port + ); + + // For Firefox-based browsers, update the PAC file with the local proxy + if matches!( + browser_type, + BrowserType::Firefox + | BrowserType::FirefoxDeveloper + | BrowserType::Zen + | BrowserType::TorBrowser + | BrowserType::MullvadBrowser + ) { + let profiles_dir = self.get_profiles_dir(); + let profile_path = profiles_dir + .join(updated_profile.id.to_string()) + .join("profile"); + + if let Err(e) = self.apply_proxy_settings_to_profile(&profile_path, &local_proxy, None) { + println!("Warning: Failed to update Firefox proxy settings: {e}"); + } } } + Err(e) => println!("Warning: Failed to start local proxy: {e}"), } // Emit profile update event to frontend @@ -1351,7 +1361,12 @@ pub async fn launch_browser_profile( // Start the proxy first match PROXY_MANAGER - .start_proxy(app_handle.clone(), &proxy, temp_pid, Some(&profile.name)) + .start_proxy( + app_handle.clone(), + Some(&proxy), + temp_pid, + Some(&profile.name), + ) .await { Ok(internal_proxy) => { diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 11214f1..e991b8e 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -434,7 +434,12 @@ impl ProfileManager { // Browser is running and proxy is enabled, start new proxy if let Some(pid) = profile.process_id { match PROXY_MANAGER - .start_proxy(app_handle.clone(), &proxy_settings, pid, Some(profile_name)) + .start_proxy( + app_handle.clone(), + Some(&proxy_settings), + pid, + Some(profile_name), + ) .await { Ok(internal_proxy_settings) => { diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index 438490b..6224fb7 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -249,10 +249,11 @@ impl ProxyManager { } // Start a proxy for given proxy settings and associate it with a browser process ID + // If proxy_settings is None, starts a direct proxy for traffic monitoring pub async fn start_proxy( &self, app_handle: tauri::AppHandle, - proxy_settings: &ProxySettings, + proxy_settings: Option<&ProxySettings>, browser_pid: u32, profile_name: Option<&str>, ) -> Result { @@ -273,15 +274,19 @@ impl ProxyManager { // Check if we have a preferred port for this profile let preferred_port = if let Some(name) = profile_name { let profile_proxies = self.profile_proxies.lock().unwrap(); - profile_proxies.get(name).and_then(|settings| { + profile_proxies.get(name).and_then(|_settings| { // Find existing proxy with same settings to reuse port let active_proxies = self.active_proxies.lock().unwrap(); active_proxies .values() .find(|p| { - p.upstream_host == settings.host - && p.upstream_port == settings.port - && p.upstream_type == settings.proxy_type + if let Some(proxy_settings) = proxy_settings { + p.upstream_host == proxy_settings.host + && p.upstream_port == proxy_settings.port + && p.upstream_type == proxy_settings.proxy_type + } else { + p.upstream_type == "DIRECT" + } }) .map(|p| p.local_port) }) @@ -295,20 +300,25 @@ impl ProxyManager { .sidecar("nodecar") .map_err(|e| format!("Failed to create sidecar: {e}"))? .arg("proxy") - .arg("start") - .arg("--host") - .arg(&proxy_settings.host) - .arg("--proxy-port") - .arg(proxy_settings.port.to_string()) - .arg("--type") - .arg(&proxy_settings.proxy_type); + .arg("start"); - // Add credentials if provided - if let Some(username) = &proxy_settings.username { - nodecar = nodecar.arg("--username").arg(username); - } - if let Some(password) = &proxy_settings.password { - nodecar = nodecar.arg("--password").arg(password); + // Add upstream proxy settings if provided, otherwise create direct proxy + if let Some(proxy_settings) = proxy_settings { + nodecar = nodecar + .arg("--host") + .arg(&proxy_settings.host) + .arg("--proxy-port") + .arg(proxy_settings.port.to_string()) + .arg("--type") + .arg(&proxy_settings.proxy_type); + + // Add credentials if provided + if let Some(username) = &proxy_settings.username { + nodecar = nodecar.arg("--username").arg(username); + } + if let Some(password) = &proxy_settings.password { + nodecar = nodecar.arg("--password").arg(password); + } } // If we have a preferred port, use it @@ -349,9 +359,13 @@ impl ProxyManager { let proxy_info = ProxyInfo { id: id.to_string(), local_url, - upstream_host: proxy_settings.host.clone(), - upstream_port: proxy_settings.port, - upstream_type: proxy_settings.proxy_type.clone(), + upstream_host: proxy_settings + .map(|p| p.host.clone()) + .unwrap_or_else(|| "DIRECT".to_string()), + upstream_port: proxy_settings.map(|p| p.port).unwrap_or(0), + upstream_type: proxy_settings + .map(|p| p.proxy_type.clone()) + .unwrap_or_else(|| "DIRECT".to_string()), local_port, }; @@ -363,8 +377,10 @@ impl ProxyManager { // Store the profile proxy info for persistence if let Some(name) = profile_name { - let mut profile_proxies = self.profile_proxies.lock().unwrap(); - profile_proxies.insert(name.to_string(), proxy_settings.clone()); + if let Some(proxy_settings) = proxy_settings { + let mut profile_proxies = self.profile_proxies.lock().unwrap(); + profile_proxies.insert(name.to_string(), proxy_settings.clone()); + } } // Return proxy settings for the browser diff --git a/src-tauri/tests/nodecar_integration.rs b/src-tauri/tests/nodecar_integration.rs index 40159f9..ce04afb 100644 --- a/src-tauri/tests/nodecar_integration.rs +++ b/src-tauri/tests/nodecar_integration.rs @@ -700,6 +700,77 @@ async fn test_nodecar_proxy_types() -> Result<(), Box Result<(), Box> { + let nodecar_path = setup_test().await?; + let mut tracker = TestResourceTracker::new(nodecar_path.clone()); + + // Test starting a direct proxy (no upstream) + let args = ["proxy", "start"]; + + println!("Starting direct proxy with nodecar..."); + let output = TestUtils::execute_nodecar_command(&nodecar_path, &args, 30).await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + tracker.cleanup_all().await; + return Err(format!("Direct proxy start failed - stdout: {stdout}, stderr: {stderr}").into()); + } + + let stdout = String::from_utf8(output.stdout)?; + let config: Value = serde_json::from_str(&stdout)?; + + // Verify proxy configuration structure + assert!(config["id"].is_string(), "Proxy ID should be a string"); + assert!( + config["localPort"].is_number(), + "Local port should be a number" + ); + assert!( + config["localUrl"].is_string(), + "Local URL should be a string" + ); + assert_eq!( + config["upstreamUrl"].as_str().unwrap(), + "DIRECT", + "Upstream URL should be DIRECT" + ); + + let proxy_id = config["id"].as_str().unwrap().to_string(); + let local_port = config["localPort"].as_u64().unwrap() as u16; + tracker.track_proxy(proxy_id.clone()); + + println!("Direct proxy started with ID: {proxy_id} on port: {local_port}"); + + // Wait for the proxy to start listening + let is_listening = TestUtils::wait_for_port_state(local_port, true, 10).await; + assert!( + is_listening, + "Direct proxy should be listening on the assigned port" + ); + + // Test stopping the proxy + let stop_args = ["proxy", "stop", "--id", &proxy_id]; + let stop_output = TestUtils::execute_nodecar_command(&nodecar_path, &stop_args, 10).await?; + + assert!( + stop_output.status.success(), + "Direct proxy stop should succeed" + ); + + let port_available = TestUtils::wait_for_port_state(local_port, false, 5).await; + assert!( + port_available, + "Port should be available after stopping direct proxy" + ); + + println!("Direct proxy test completed successfully"); + tracker.cleanup_all().await; + Ok(()) +} + /// Test SOCKS5 proxy chaining - create two proxies where the second uses the first as upstream #[tokio::test] async fn test_nodecar_socks5_proxy_chaining() -> Result<(), Box>