refactor: improve location info generation for fresh profiles

This commit is contained in:
zhom
2026-06-24 05:18:50 +04:00
parent 0da8529e07
commit 9061e4db8f
4 changed files with 216 additions and 64 deletions
+64
View File
@@ -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
+13
View File
@@ -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;
+138 -64
View File
@@ -39,6 +39,12 @@ pub struct WayfernConfig {
pub block_webgl: Option<bool>,
#[serde(default, skip_serializing)]
pub proxy: Option<String>,
/// 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<String>,
}
#[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::<chrono_tz::Tz>() {
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<String> {
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::<chrono_tz::Tz>() {
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
}
+1
View File
@@ -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