refactor: better error handling

This commit is contained in:
zhom
2025-12-01 01:46:28 +04:00
parent cd33accb1a
commit c43f141907
8 changed files with 183 additions and 153 deletions
+2
View File
@@ -112,6 +112,7 @@
"nobrowse",
"noconfirm",
"nodecar",
"NODELAY",
"nodemon",
"norestart",
"NSIS",
@@ -199,6 +200,7 @@
"unrs",
"urlencoding",
"urllib",
"utoipa",
"venv",
"vercel",
"VERYSILENT",
+53
View File
@@ -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"
+1
View File
@@ -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"] }
+6
View File
@@ -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);
+10 -25
View File
@@ -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
+72 -102
View File
@@ -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"
);
}
}
+6 -12
View File
@@ -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());
}
+33 -14
View File
@@ -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<dyn std::er
// This ensures the process doesn't exit even if there are no active connections
loop {
match listener.accept().await {
Ok((mut stream, _)) => {
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<dyn std::er
// CONNECT requests need special handling for tunneling
let mut peek_buffer = [0u8; 8];
match stream.read(&mut peek_buffer).await {
Ok(n) if n >= 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<dyn std::er
}
return;
}
// Not CONNECT - reconstruct stream with consumed bytes prepended
// Not CONNECT (or partial read) - reconstruct stream with consumed bytes prepended
// This is critical: we MUST prepend any bytes we consumed, even if < 7 bytes
log::error!(
"DEBUG: Non-CONNECT request, first {} bytes: {:?}",
n,
String::from_utf8_lossy(&peek_buffer[..n])
);
let prepended_bytes = peek_buffer[..n].to_vec();
let prepended_reader = PrependReader {
prepended: prepended_bytes,
@@ -664,17 +687,10 @@ pub async fn run_proxy_server(config: ProxyConfig) -> Result<(), Box<dyn std::er
if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
log::error!("Error serving connection: {:?}", err);
}
return;
}
_ => {}
}
// 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