mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-24 07:29:56 +02:00
refactor: improve location info generation for fresh profiles
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user