From c43f1419077767f28bbdfe070948b7f5a0754262 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Mon, 1 Dec 2025 01:46:28 +0400 Subject: [PATCH] refactor: better error handling --- .vscode/settings.json | 2 + src-tauri/Cargo.lock | 53 +++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/bin/proxy_server.rs | 6 ++ src-tauri/src/browser.rs | 35 ++---- src-tauri/src/profile/manager.rs | 174 +++++++++++++----------------- src-tauri/src/proxy_runner.rs | 18 ++-- src-tauri/src/proxy_server.rs | 47 +++++--- 8 files changed, 183 insertions(+), 153 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 42b4f74..e29077a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -112,6 +112,7 @@ "nobrowse", "noconfirm", "nodecar", + "NODELAY", "nodemon", "norestart", "NSIS", @@ -199,6 +200,7 @@ "unrs", "urlencoding", "urllib", + "utoipa", "venv", "vercel", "VERYSILENT", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9fc84c7..255a6aa 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1306,6 +1306,7 @@ dependencies = [ "clap", "core-foundation 0.10.1", "directories", + "env_logger", "flate2", "futures-util", "http-body-util", @@ -1458,6 +1459,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2524,6 +2538,30 @@ dependencies = [ "system-deps", ] +[[package]] +name = "jiff" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "jni" version = "0.21.1" @@ -3728,6 +3766,21 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.4" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7705ff0..0e64c12 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -39,6 +39,7 @@ tauri-plugin-dialog = "2" tauri-plugin-macos-permissions = "2" tauri-plugin-log = "2" log = "0.4" +env_logger = "0.11" directories = "6" reqwest = { version = "0.12", features = ["json", "stream", "socks"] } diff --git a/src-tauri/src/bin/proxy_server.rs b/src-tauri/src/bin/proxy_server.rs index af853aa..3980ebb 100644 --- a/src-tauri/src/bin/proxy_server.rs +++ b/src-tauri/src/bin/proxy_server.rs @@ -82,6 +82,12 @@ fn build_proxy_url( #[tokio::main(flavor = "multi_thread")] async fn main() { + // Initialize logger to write to stderr (which will be redirected to file) + env_logger::Builder::from_default_env() + .filter_level(log::LevelFilter::Debug) + .format_timestamp_millis() + .init(); + // Set up panic handler to log panics before process exits std::panic::set_hook(Box::new(|panic_info| { log::error!("PANIC in proxy worker: {:?}", panic_info); diff --git a/src-tauri/src/browser.rs b/src-tauri/src/browser.rs index 3958fcd..df7e669 100644 --- a/src-tauri/src/browser.rs +++ b/src-tauri/src/browser.rs @@ -624,22 +624,9 @@ impl Browser for FirefoxBrowser { args.push("--headless".to_string()); } - // Use -no-remote for browsers that require it for security (Mullvad, Tor) or when remote debugging - match self.browser_type { - BrowserType::MullvadBrowser | BrowserType::TorBrowser => { - args.push("-no-remote".to_string()); - } - BrowserType::Firefox - | BrowserType::FirefoxDeveloper - | BrowserType::Zen - | BrowserType::Camoufox => { - // Use -no-remote when remote debugging to avoid conflicts - if remote_debugging_port.is_some() { - args.push("-no-remote".to_string()); - } - // Don't use -no-remote for normal launches so we can communicate with existing instances - } - _ => {} + // Use -no-remote when remote debugging to avoid conflicts with existing instances + if remote_debugging_port.is_some() { + args.push("-no-remote".to_string()); } // Firefox-based browsers use profile directory and user.js for proxy configuration @@ -741,6 +728,8 @@ impl Browser for ChromiumBrowser { "--disable-session-crashed-bubble".to_string(), "--hide-crash-restore-bubble".to_string(), "--disable-infobars".to_string(), + // Disable QUIC/HTTP3 to ensure traffic goes through HTTP proxy + "--disable-quic".to_string(), ]; // Add remote debugging if requested @@ -1100,30 +1089,26 @@ mod tests { "Firefox should include debugging port" ); - // Test Mullvad Browser (should always use -no-remote) + // Test Mullvad Browser (no special flags without remote debugging) let browser = FirefoxBrowser::new(BrowserType::MullvadBrowser); let args = browser .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Mullvad Browser"); - assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); + assert_eq!(args, vec!["-profile", "/path/to/profile"]); - // Test Tor Browser (should always use -no-remote) + // Test Tor Browser (no special flags without remote debugging) let browser = FirefoxBrowser::new(BrowserType::TorBrowser); let args = browser .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Tor Browser"); - assert_eq!(args, vec!["-profile", "/path/to/profile", "-no-remote"]); + assert_eq!(args, vec!["-profile", "/path/to/profile"]); - // Test Zen Browser (should not use -no-remote for normal launch) + // Test Zen Browser (no special flags without remote debugging) let browser = FirefoxBrowser::new(BrowserType::Zen); let args = browser .create_launch_args("/path/to/profile", None, None, None, false) .expect("Failed to create launch args for Zen Browser"); assert_eq!(args, vec!["-profile", "/path/to/profile"]); - assert!( - !args.contains(&"-no-remote".to_string()), - "Zen Browser should not use -no-remote for normal launch" - ); // Test headless mode let args = browser diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index 2fb513c..ea51965 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -1159,114 +1159,76 @@ impl ProfileManager { let mut preferences = Vec::new(); - // Get the UUID directory (parent of profile data directory) - let uuid_dir = profile_data_path - .parent() - .ok_or("Invalid profile path - cannot find UUID directory")?; - // Add common Firefox preferences (like disabling default browser check) preferences.extend(self.get_common_firefox_preferences()); - // Use embedded PAC template instead of reading from file - const PAC_TEMPLATE: &str = r#"function FindProxyForURL(url, host) { - return "{{proxy_url}}"; -}"#; + // Determine which proxy settings to use + let effective_proxy = internal_proxy.unwrap_or(proxy); + let proxy_host = &effective_proxy.host; + let proxy_port = effective_proxy.port; - // Format proxy URL based on type and whether we have an internal proxy - let proxy_url = if let Some(internal) = internal_proxy { - // Use internal proxy (local proxy) as the primary proxy - // This is the local proxy that forwards to the upstream proxy - log::info!( - "Applying local proxy settings to Firefox profile: {}:{}", - internal.host, - internal.port - ); - format!("HTTP {}:{}", internal.host, internal.port) - } else { - // Use user-configured proxy directly (upstream proxy) - log::info!( - "Applying upstream proxy settings to Firefox profile: {}:{} ({})", - proxy.host, - proxy.port, - proxy.proxy_type - ); - match proxy.proxy_type.as_str() { - "http" => format!("HTTP {}:{}", proxy.host, proxy.port), - "https" => format!("HTTPS {}:{}", proxy.host, proxy.port), - "socks4" => format!("SOCKS4 {}:{}", proxy.host, proxy.port), - "socks5" => format!("SOCKS5 {}:{}", proxy.host, proxy.port), - _ => return Err(format!("Unsupported proxy type: {}", proxy.proxy_type).into()), - } - }; + // Check if this is a SOCKS proxy (only possible when using upstream directly) + let is_socks = + internal_proxy.is_none() && (proxy.proxy_type == "socks4" || proxy.proxy_type == "socks5"); - // Replace placeholders in PAC file - let pac_content = PAC_TEMPLATE - .replace("{{proxy_url}}", &proxy_url) - .replace("{{proxy_credentials}}", ""); // Credentials are now handled by the PAC file - - // Save PAC file in UUID directory - let pac_path = uuid_dir.join("proxy.pac"); log::info!( - "Creating PAC file at: {} with proxy: {}", - pac_path.display(), - proxy_url - ); - fs::write(&pac_path, &pac_content)?; - log::info!( - "Created PAC file at: {} with content: {}", - pac_path.display(), - pac_content + "Applying manual proxy settings to Firefox profile: {}:{} (is_internal: {}, is_socks: {})", + proxy_host, + proxy_port, + internal_proxy.is_some(), + is_socks ); - // Configure Firefox to use the PAC file - // Convert path to absolute and properly format for file:// URL - let pac_path_absolute = pac_path.canonicalize().unwrap_or_else(|_| pac_path.clone()); - let pac_url = if cfg!(windows) { - // Windows: file:///C:/path/to/file.pac - format!( - "file:///{}", - pac_path_absolute.to_string_lossy().replace('\\', "/") - ) + // Use MANUAL proxy configuration (type 1) instead of PAC file (type 2) + // PAC files with file:// URLs are blocked by privacy-focused browsers like Zen and Mullvad + // Manual proxy configuration works reliably across all Firefox variants + preferences.push("user_pref(\"network.proxy.type\", 1);".to_string()); + + if is_socks { + // SOCKS proxy configuration + preferences.extend([ + format!("user_pref(\"network.proxy.socks\", \"{}\");", proxy_host), + format!("user_pref(\"network.proxy.socks_port\", {});", proxy_port), + format!( + "user_pref(\"network.proxy.socks_version\", {});", + if proxy.proxy_type == "socks5" { 5 } else { 4 } + ), + "user_pref(\"network.proxy.http\", \"\");".to_string(), + "user_pref(\"network.proxy.http_port\", 0);".to_string(), + "user_pref(\"network.proxy.ssl\", \"\");".to_string(), + "user_pref(\"network.proxy.ssl_port\", 0);".to_string(), + ]); } else { - // Unix/macOS: file:///absolute/path/to/file.pac (three slashes for absolute path) - format!("file://{}", pac_path_absolute.to_string_lossy()) - }; - - log::info!("PAC file path (absolute): {}", pac_path_absolute.display()); - log::info!("PAC file URL for Firefox: {}", pac_url); + // HTTP/HTTPS proxy configuration (including our internal local proxy) + preferences.extend([ + format!("user_pref(\"network.proxy.http\", \"{}\");", proxy_host), + format!("user_pref(\"network.proxy.http_port\", {});", proxy_port), + format!("user_pref(\"network.proxy.ssl\", \"{}\");", proxy_host), + format!("user_pref(\"network.proxy.ssl_port\", {});", proxy_port), + format!("user_pref(\"network.proxy.ftp\", \"{}\");", proxy_host), + format!("user_pref(\"network.proxy.ftp_port\", {});", proxy_port), + "user_pref(\"network.proxy.socks\", \"\");".to_string(), + "user_pref(\"network.proxy.socks_port\", 0);".to_string(), + ]); + } + // Common proxy settings - keep it simple like proxy-chain expected preferences.extend([ - "user_pref(\"network.proxy.type\", 2);".to_string(), - format!( - "user_pref(\"network.proxy.autoconfig_url\", \"{}\");", - pac_url - ), - "user_pref(\"network.proxy.failover_direct\", false);".to_string(), - "user_pref(\"network.proxy.socks_remote_dns\", true);".to_string(), "user_pref(\"network.proxy.no_proxies_on\", \"\");".to_string(), - "user_pref(\"signon.autologin.proxy\", true);".to_string(), - "user_pref(\"network.proxy.share_proxy_settings\", false);".to_string(), - "user_pref(\"network.automatic-ntlm-auth.allow-proxies\", false);".to_string(), - "user_pref(\"network.auth-use-sspi\", false);".to_string(), + "user_pref(\"network.proxy.autoconfig_url\", \"\");".to_string(), + // Disable QUIC/HTTP3 - it bypasses HTTP proxy + "user_pref(\"network.http.http3.enable\", false);".to_string(), + "user_pref(\"network.http.http3.enabled\", false);".to_string(), ]); // Write settings to user.js file let user_js_content = preferences.join("\n"); fs::write(user_js_path, &user_js_content)?; - log::info!("Updated user.js with proxy settings. PAC URL: {}", pac_url); - if let Some(internal) = internal_proxy { - log::info!( - "Firefox will use LOCAL proxy: {}:{} (which forwards to upstream)", - internal.host, - internal.port - ); - } else { - log::info!( - "Firefox will use UPSTREAM proxy directly: {}:{}", - proxy.host, - proxy.port - ); - } + log::info!( + "Updated user.js with manual proxy settings: {}:{}", + proxy_host, + proxy_port + ); Ok(()) } @@ -1428,20 +1390,28 @@ mod tests { assert!(user_js_path.exists(), "user.js should be created"); let content = fs::read_to_string(&user_js_path).expect("Should read user.js"); + + // Check for manual proxy configuration (type 1) instead of PAC (type 2) + // Manual proxy is used because PAC file:// URLs are blocked by privacy browsers like Zen assert!( - content.contains("network.proxy.type"), - "Should contain proxy type setting" + content.contains("network.proxy.type\", 1"), + "Should set proxy type to 1 (manual)" ); - assert!(content.contains("2"), "Should set proxy type to 2 (PAC)"); - - // Check that PAC file was created - let pac_path = uuid_dir.join("proxy.pac"); - assert!(pac_path.exists(), "proxy.pac should be created"); - - let pac_content = fs::read_to_string(&pac_path).expect("Should read proxy.pac"); assert!( - pac_content.contains("FindProxyForURL"), - "PAC file should contain FindProxyForURL function" + content.contains("network.proxy.http\", \"proxy.example.com\""), + "Should set HTTP proxy host" + ); + assert!( + content.contains("network.proxy.http_port\", 8080"), + "Should set HTTP proxy port" + ); + assert!( + content.contains("network.proxy.ssl\", \"proxy.example.com\""), + "Should set SSL proxy host" + ); + assert!( + content.contains("network.proxy.ssl_port\", 8080"), + "Should set SSL proxy port" ); } } diff --git a/src-tauri/src/proxy_runner.rs b/src-tauri/src/proxy_runner.rs index 253822e..328f90d 100644 --- a/src-tauri/src/proxy_runner.rs +++ b/src-tauri/src/proxy_runner.rs @@ -59,18 +59,12 @@ pub async fn start_proxy_process_with_profile( cmd.stdin(Stdio::null()); cmd.stdout(Stdio::null()); - #[cfg(debug_assertions)] - { - let log_path = std::path::PathBuf::from("/tmp").join(format!("donut-proxy-{}.log", id)); - if let Ok(file) = std::fs::File::create(&log_path) { - log::error!("Proxy worker stderr will be logged to: {:?}", log_path); - cmd.stderr(Stdio::from(file)); - } else { - cmd.stderr(Stdio::null()); - } - } - #[cfg(not(debug_assertions))] - { + // Always log to file for diagnostics (both debug and release builds) + let log_path = std::path::PathBuf::from("/tmp").join(format!("donut-proxy-{}.log", id)); + if let Ok(file) = std::fs::File::create(&log_path) { + log::info!("Proxy worker stderr will be logged to: {:?}", log_path); + cmd.stderr(Stdio::from(file)); + } else { cmd.stderr(Stdio::null()); } diff --git a/src-tauri/src/proxy_server.rs b/src-tauri/src/proxy_server.rs index 54c1f49..af5e7e2 100644 --- a/src-tauri/src/proxy_server.rs +++ b/src-tauri/src/proxy_server.rs @@ -367,6 +367,13 @@ async fn handle_http( // This is faster and more reliable than trying to use hyper-proxy with version conflicts use reqwest::Client; + log::error!( + "DEBUG: Handling HTTP request: {} {} (host: {:?})", + req.method(), + req.uri(), + req.uri().host() + ); + // Extract domain for traffic tracking let domain = req .uri() @@ -609,7 +616,12 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box { + Ok((mut stream, peer_addr)) => { + // Enable TCP_NODELAY to ensure small packets are sent immediately + // This is critical for CONNECT responses to be sent before tunneling begins + let _ = stream.set_nodelay(true); + log::error!("DEBUG: Accepted connection from {:?}", peer_addr); + let upstream = upstream_url.clone(); tokio::task::spawn(async move { @@ -617,9 +629,13 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box= 7 => { + Ok(0) => { + log::error!("DEBUG: Connection closed immediately (0 bytes read)"); + } + Ok(n) => { let request_start = String::from_utf8_lossy(&peek_buffer[..n.min(7)]); - if request_start.starts_with("CONNECT") { + log::error!("DEBUG: Read {} bytes, starts with: {:?}", n, request_start); + if n >= 7 && request_start.starts_with("CONNECT") { // Handle CONNECT request manually for tunneling let mut full_request = Vec::with_capacity(4096); full_request.extend_from_slice(&peek_buffer[..n]); @@ -651,7 +667,14 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box Result<(), Box {} - } - - // For non-CONNECT requests, use hyper's HTTP handling - let io = TokioIo::new(stream); - let service = service_fn(move |req| handle_request(req, upstream.clone())); - - if let Err(err) = http1::Builder::new().serve_connection(io, service).await { - log::error!("Error serving connection: {:?}", err); + Err(e) => { + log::error!("Error reading from connection: {:?}", e); + } } }); } @@ -807,6 +823,9 @@ async fn handle_connect_from_buffer( } }; + // Enable TCP_NODELAY on target stream for immediate data transfer + let _ = target_stream.set_nodelay(true); + // Send 200 Connection Established response to client // CRITICAL: Must flush after writing to ensure response is sent before tunneling client_stream