diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 22921fa..90f7829 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -984,6 +984,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf 0.12.1", +] + [[package]] name = "cipher" version = "0.4.4" @@ -1600,6 +1610,7 @@ dependencies = [ "boringtun", "bzip2", "chrono", + "chrono-tz", "clap", "core-foundation 0.10.1", "crossbeam-channel", @@ -4354,6 +4365,15 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared 0.12.1", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -4458,6 +4478,15 @@ dependencies = [ "siphasher 1.0.2", ] +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher 1.0.2", +] + [[package]] name = "pico-args" version = "0.5.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e0829e4..9da7543 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -72,6 +72,7 @@ mime_guess = "2" once_cell = "1" urlencoding = "2.1" chrono = { version = "0.4", features = ["serde"] } +chrono-tz = "0.10" axum = { version = "0.8.8", features = ["ws"] } tower = "0.5" tower-http = { version = "0.6", features = ["cors"] } diff --git a/src-tauri/src/camoufox/config.rs b/src-tauri/src/camoufox/config.rs index 5e7aaf4..0ab1eca 100644 --- a/src-tauri/src/camoufox/config.rs +++ b/src-tauri/src/camoufox/config.rs @@ -425,8 +425,28 @@ impl CamoufoxConfigBuilder { /// Build the complete Camoufox launch configuration with async geolocation support. /// This method should be used when geoip option is set to Auto. pub async fn build_async(self) -> Result { - // Get proxy URL for IP detection if set - let proxy_url = self.proxy.as_ref().map(|p| p.server.clone()); + // Get full proxy URL (with credentials) for IP detection + let proxy_url = self.proxy.as_ref().map(|p| { + if let (Some(user), Some(pass)) = (&p.username, &p.password) { + // Reconstruct URL with credentials: scheme://user:pass@host:port + if let Ok(mut parsed) = url::Url::parse(&p.server) { + let _ = parsed.set_username(user); + let _ = parsed.set_password(Some(pass)); + parsed.to_string() + } else { + p.server.clone() + } + } else if let Some(user) = &p.username { + if let Ok(mut parsed) = url::Url::parse(&p.server) { + let _ = parsed.set_username(user); + parsed.to_string() + } else { + p.server.clone() + } + } else { + p.server.clone() + } + }); let geoip_option = self.geoip.clone(); let block_webrtc = self.block_webrtc; diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 79831f7..61f72ad 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -342,18 +342,69 @@ impl WayfernManager { // Normalize the fingerprint: convert JSON string fields to proper types let mut normalized = Self::normalize_fingerprint(fp); - // Add default timezone/geolocation if not present - // Wayfern's Bayesian network generator doesn't include these fields, - // so we need to add sensible defaults - if let Some(obj) = normalized.as_object_mut() { - if !obj.contains_key("timezone") { - obj.insert("timezone".to_string(), json!("America/New_York")); + // Apply geolocation based on proxy IP or geoip config + let geoip_option = config.geoip.as_ref(); + let should_geolocate = match geoip_option { + Some(serde_json::Value::Bool(false)) => false, + _ => true, // Default to auto-detect + }; + + if should_geolocate { + let geo_result = async { + let ip = match geoip_option { + Some(serde_json::Value::String(ip_str)) => ip_str.clone(), + _ => { + // Auto-detect IP, optionally through proxy + crate::ip_utils::fetch_public_ip(config.proxy.as_deref()) + .await + .map_err(|e| format!("Failed to fetch public IP: {e}"))? + } + }; + + crate::camoufox::geolocation::get_geolocation(&ip) + .map_err(|e| format!("Failed to get geolocation for IP {ip}: {e}")) } - if !obj.contains_key("timezoneOffset") { - obj.insert("timezoneOffset".to_string(), json!(300)); // EST = UTC-5 = 300 minutes + .await; + + match geo_result { + Ok(geo) => { + if let Some(obj) = normalized.as_object_mut() { + obj.insert("timezone".to_string(), json!(geo.timezone)); + // Calculate timezone offset from IANA timezone name + if let Ok(tz) = geo.timezone.parse::() { + use chrono::Offset; + let now = chrono::Utc::now().with_timezone(&tz); + let offset_seconds = now.offset().fix().local_minus_utc(); + let offset_minutes = -(offset_seconds / 60); + obj.insert("timezoneOffset".to_string(), json!(offset_minutes)); + } + obj.insert("latitude".to_string(), json!(geo.latitude)); + obj.insert("longitude".to_string(), json!(geo.longitude)); + let locale_str = geo.locale.as_string(); + obj.insert("language".to_string(), json!(&locale_str)); + obj.insert( + "languages".to_string(), + json!([&locale_str, &geo.locale.language]), + ); + } + log::info!( + "Applied geolocation to Wayfern fingerprint: {} ({})", + geo.locale.as_string(), + geo.timezone + ); + } + Err(e) => { + log::warn!("Geolocation failed, using defaults: {e}"); + if let Some(obj) = normalized.as_object_mut() { + if !obj.contains_key("timezone") { + obj.insert("timezone".to_string(), json!("America/New_York")); + } + if !obj.contains_key("timezoneOffset") { + obj.insert("timezoneOffset".to_string(), json!(300)); + } + } + } } - // Note: latitude/longitude are intentionally not set by default - // as they reveal precise location. Users should set these manually if needed. } normalized @@ -567,6 +618,38 @@ impl WayfernManager { log::warn!("No fingerprint found in config, browser will use default fingerprint"); } + // Set geolocation override via CDP so navigator.geolocation.getCurrentPosition() matches + if let Some(fingerprint_json) = &config.fingerprint { + if let Ok(fp) = serde_json::from_str::(fingerprint_json) { + let fp_obj = if fp.get("fingerprint").is_some() { + fp.get("fingerprint").unwrap() + } else { + &fp + }; + if let (Some(lat), Some(lng)) = ( + fp_obj.get("latitude").and_then(|v| v.as_f64()), + fp_obj.get("longitude").and_then(|v| v.as_f64()), + ) { + let accuracy = fp_obj + .get("accuracy") + .and_then(|v| v.as_f64()) + .unwrap_or(100.0); + if let Some(target) = page_targets.first() { + if let Some(ws_url) = &target.websocket_debugger_url { + let _ = self + .send_cdp_command( + ws_url, + "Emulation.setGeolocationOverride", + json!({ "latitude": lat, "longitude": lng, "accuracy": accuracy }), + ) + .await; + log::info!("Set geolocation override: lat={lat}, lng={lng}"); + } + } + } + } + } + // Navigate to URL via CDP - fingerprint will be applied at navigation commit time if let Some(url) = url { log::info!("Navigating to URL via CDP: {}", url); @@ -583,6 +666,25 @@ impl WayfernManager { } } + // Close the debugging port to prevent localhost port-scan detection. + // Reopen on a random high port after 5s so we can still manage the browser. + let reopen_port = port; // Reopen on same port for find_wayfern_by_profile recovery + if let Some(target) = page_targets.first() { + if let Some(ws_url) = &target.websocket_debugger_url { + match self + .send_cdp_command( + ws_url, + "Wayfern.closeDebuggingPort", + json!({ "reopenPort": reopen_port, "reopenDelayMs": 30000 }), + ) + .await + { + Ok(_) => log::info!("Closed debugging port, will reopen on {reopen_port} after 30s"), + Err(e) => log::warn!("Failed to close debugging port: {e}"), + } + } + } + let id = uuid::Uuid::new_v4().to_string(); let instance = WayfernInstance { id: id.clone(),