From 9061e4db8fc54d44492a9a0f2f775d4e1a96da21 Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Wed, 24 Jun 2026 05:18:50 +0400 Subject: [PATCH] refactor: improve location info generation for fresh profiles --- src-tauri/src/browser_runner.rs | 64 ++++++++++ src-tauri/src/profile/manager.rs | 13 ++ src-tauri/src/wayfern_manager.rs | 202 +++++++++++++++++++++---------- src/types.ts | 1 + 4 files changed, 216 insertions(+), 64 deletions(-) diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index aec04f8..cbe0996 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -597,6 +597,14 @@ impl BrowserRunner { if wayfern_config.os.is_some() { updated_wayfern_config.os = wayfern_config.os.clone(); } + // The fresh fingerprint's location matches the current routing; record + // its signature so launches keep it in sync with the non-randomize path. + updated_wayfern_config.geo_proxy_signature = + Some(crate::wayfern_manager::WayfernManager::geo_signature( + upstream_proxy.as_ref(), + profile.vpn_id.as_deref(), + wayfern_config.geoip.as_ref(), + )); updated_profile.wayfern_config = Some(updated_wayfern_config.clone()); log::info!( @@ -604,6 +612,62 @@ impl BrowserRunner { profile.name, updated_wayfern_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0) ); + } else { + // Safety net: the stored fingerprint's timezone and geolocation were + // computed for whatever proxy was set when the fingerprint was + // generated. If the profile's proxy or VPN has changed since (the + // common case being a user who forgot to set a proxy at creation and + // added one afterwards), that location data is stale and the user would + // see the wrong timezone on first launch. When the routing signature no + // longer matches, refresh just the location fields of the stored + // fingerprint through the current proxy. Wayfern only; the randomize + // path above already regenerates the whole fingerprint each launch. + let current_geo_sig = crate::wayfern_manager::WayfernManager::geo_signature( + upstream_proxy.as_ref(), + profile.vpn_id.as_deref(), + wayfern_config.geoip.as_ref(), + ); + let geo_enabled = !matches!( + wayfern_config.geoip.as_ref(), + Some(serde_json::Value::Bool(false)) + ); + if geo_enabled + && wayfern_config.geo_proxy_signature.as_deref() != Some(current_geo_sig.as_str()) + { + if let Some(stored_fp) = wayfern_config.fingerprint.clone() { + log::info!( + "Routing changed for Wayfern profile {} since its fingerprint was generated (was {:?}, now {}); refreshing timezone and geolocation", + profile.name, + wayfern_config.geo_proxy_signature, + current_geo_sig + ); + match crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation( + &stored_fp, + wayfern_config.proxy.as_deref(), + wayfern_config.geoip.as_ref(), + ) + .await + { + Some(refreshed) => { + // Use the refreshed fingerprint for this launch... + wayfern_config.fingerprint = Some(refreshed.clone()); + wayfern_config.geo_proxy_signature = Some(current_geo_sig.clone()); + // ...and persist it so the corrected location sticks and we do + // not refresh again on the next launch with the same proxy. + let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default(); + cfg.fingerprint = Some(refreshed); + cfg.geo_proxy_signature = Some(current_geo_sig); + updated_profile.wayfern_config = Some(cfg); + } + None => { + log::warn!( + "Could not refresh geolocation for Wayfern profile {} (proxy unreachable?); launching with existing location and will retry next launch", + profile.name + ); + } + } + } + } } // Create ephemeral dir for ephemeral or password-protected profiles diff --git a/src-tauri/src/profile/manager.rs b/src-tauri/src/profile/manager.rs index e9522bc..1021ae3 100644 --- a/src-tauri/src/profile/manager.rs +++ b/src-tauri/src/profile/manager.rs @@ -326,6 +326,19 @@ impl ProfileManager { log::info!("Using provided fingerprint for Wayfern profile: {name}"); } + // Record which proxy/geoip the fingerprint's location data was computed + // for. On launch this is compared against the profile's current routing + // so a proxy that was changed after creation triggers a location refresh + // instead of showing a stale timezone. + config.geo_proxy_signature = Some(crate::wayfern_manager::WayfernManager::geo_signature( + proxy_id + .as_ref() + .and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id)) + .as_ref(), + None, + config.geoip.as_ref(), + )); + // Clear the proxy from config after fingerprint generation config.proxy = None; diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 8afec72..d0b78ee 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -39,6 +39,12 @@ pub struct WayfernConfig { pub block_webgl: Option, #[serde(default, skip_serializing)] pub proxy: Option, + /// Stable signature of the proxy/VPN/geoip the fingerprint's location data + /// (timezone, latitude/longitude, language) was last computed for. Compared + /// on launch to detect that the routing changed since creation, so the + /// location can be refreshed instead of showing stale data. + #[serde(default)] + pub geo_proxy_signature: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -263,6 +269,130 @@ impl WayfernManager { Err("No response received from CDP".into()) } + /// Stable signature describing what determines this profile's geolocation + /// (timezone, latitude/longitude, language): the geoip mode first, then the + /// VPN, the proxy, or a direct connection. Compared across creation and + /// launch to detect a change. The VPN case keys off `vpn_id` rather than the + /// per-launch local port, and the proxy case off type/host/port/username so + /// that editing the proxy is also caught. + pub fn geo_signature( + proxy: Option<&crate::browser::ProxySettings>, + vpn_id: Option<&str>, + geoip: Option<&serde_json::Value>, + ) -> String { + match geoip { + Some(serde_json::Value::Bool(false)) => "off".to_string(), + Some(serde_json::Value::String(ip)) if !ip.is_empty() => format!("ip:{ip}"), + _ => { + if let Some(id) = vpn_id { + format!("vpn:{id}") + } else if let Some(p) = proxy { + format!( + "proxy:{}://{}@{}:{}", + p.proxy_type.to_lowercase(), + p.username.as_deref().unwrap_or(""), + p.host, + p.port + ) + } else { + "direct".to_string() + } + } + } + } + + /// Apply timezone/geolocation fields to a fingerprint object from the proxy's + /// exit IP (or a fixed geoip IP). Mutates `fingerprint` in place. Returns true + /// if fresh geolocation was fetched and applied, false if geolocation is + /// disabled or could not be resolved (in which case only safe defaults are + /// filled in). Shared by fingerprint generation and the launch-time refresh + /// so both produce identical location data. + async fn apply_geolocation( + fingerprint: &mut serde_json::Value, + proxy: Option<&str>, + geoip: Option<&serde_json::Value>, + ) -> bool { + // Default to auto-detect; only an explicit `false` disables geolocation. + let should_geolocate = !matches!(geoip, Some(serde_json::Value::Bool(false))); + if !should_geolocate { + return false; + } + + let geo_result = async { + let ip = match geoip { + Some(serde_json::Value::String(ip_str)) => ip_str.clone(), + _ => crate::ip_utils::fetch_public_ip(proxy) + .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}")) + } + .await; + + match geo_result { + Ok(geo) => { + if let Some(obj) = fingerprint.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 + ); + true + } + Err(e) => { + log::warn!("Geolocation failed, using defaults: {e}"); + if let Some(obj) = fingerprint.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)); + } + } + false + } + } + } + + /// Refresh ONLY the location fields (timezone, offset, latitude/longitude, + /// language) of an already-generated fingerprint to match the current proxy, + /// leaving every other fingerprint field untouched. `proxy` is the local + /// proxy URL the browser will use. Returns the updated fingerprint JSON on + /// success, or None if geolocation is disabled or could not be resolved, in + /// which case the caller keeps the existing fingerprint and retries on the + /// next launch. + pub async fn refresh_fingerprint_geolocation( + fingerprint_json: &str, + proxy: Option<&str>, + geoip: Option<&serde_json::Value>, + ) -> Option { + let mut fp: serde_json::Value = serde_json::from_str(fingerprint_json).ok()?; + if Self::apply_geolocation(&mut fp, proxy, geoip).await { + serde_json::to_string(&fp).ok() + } else { + None + } + } + pub async fn generate_fingerprint_config( &self, _app_handle: &AppHandle, @@ -424,70 +554,14 @@ impl WayfernManager { // Normalize the fingerprint: convert JSON string fields to proper types let mut normalized = Self::normalize_fingerprint(fp); - // 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}")) - } - .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)); - } - } - } - } - } + // Apply timezone/geolocation for the proxy this fingerprint is being + // generated against. Shared with the launch-time location refresh. + Self::apply_geolocation( + &mut normalized, + config.proxy.as_deref(), + config.geoip.as_ref(), + ) + .await; normalized } diff --git a/src/types.ts b/src/types.ts index 6c9309d..7faba79 100644 --- a/src/types.ts +++ b/src/types.ts @@ -424,6 +424,7 @@ export interface WayfernConfig { fingerprint?: string; // JSON string of the complete fingerprint config randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch os?: WayfernOS; // Operating system for fingerprint generation + geo_proxy_signature?: string; // Internal: routing the fingerprint's location was computed for } // Wayfern fingerprint config - matches the C++ FingerprintData structure