mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-27 03:31:48 +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() {
|
if wayfern_config.os.is_some() {
|
||||||
updated_wayfern_config.os = wayfern_config.os.clone();
|
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());
|
updated_profile.wayfern_config = Some(updated_wayfern_config.clone());
|
||||||
|
|
||||||
log::info!(
|
log::info!(
|
||||||
@@ -604,6 +612,62 @@ impl BrowserRunner {
|
|||||||
profile.name,
|
profile.name,
|
||||||
updated_wayfern_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0)
|
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
|
// Create ephemeral dir for ephemeral or password-protected profiles
|
||||||
|
|||||||
@@ -326,6 +326,19 @@ impl ProfileManager {
|
|||||||
log::info!("Using provided fingerprint for Wayfern profile: {name}");
|
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
|
// Clear the proxy from config after fingerprint generation
|
||||||
config.proxy = None;
|
config.proxy = None;
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ pub struct WayfernConfig {
|
|||||||
pub block_webgl: Option<bool>,
|
pub block_webgl: Option<bool>,
|
||||||
#[serde(default, skip_serializing)]
|
#[serde(default, skip_serializing)]
|
||||||
pub proxy: Option<String>,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -263,6 +269,130 @@ impl WayfernManager {
|
|||||||
Err("No response received from CDP".into())
|
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(
|
pub async fn generate_fingerprint_config(
|
||||||
&self,
|
&self,
|
||||||
_app_handle: &AppHandle,
|
_app_handle: &AppHandle,
|
||||||
@@ -424,70 +554,14 @@ impl WayfernManager {
|
|||||||
// Normalize the fingerprint: convert JSON string fields to proper types
|
// Normalize the fingerprint: convert JSON string fields to proper types
|
||||||
let mut normalized = Self::normalize_fingerprint(fp);
|
let mut normalized = Self::normalize_fingerprint(fp);
|
||||||
|
|
||||||
// Apply geolocation based on proxy IP or geoip config
|
// Apply timezone/geolocation for the proxy this fingerprint is being
|
||||||
let geoip_option = config.geoip.as_ref();
|
// generated against. Shared with the launch-time location refresh.
|
||||||
let should_geolocate = match geoip_option {
|
Self::apply_geolocation(
|
||||||
Some(serde_json::Value::Bool(false)) => false,
|
&mut normalized,
|
||||||
_ => true, // Default to auto-detect
|
config.proxy.as_deref(),
|
||||||
};
|
config.geoip.as_ref(),
|
||||||
|
)
|
||||||
if should_geolocate {
|
.await;
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
normalized
|
normalized
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,6 +424,7 @@ export interface WayfernConfig {
|
|||||||
fingerprint?: string; // JSON string of the complete fingerprint config
|
fingerprint?: string; // JSON string of the complete fingerprint config
|
||||||
randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch
|
randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch
|
||||||
os?: WayfernOS; // Operating system for fingerprint generation
|
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
|
// Wayfern fingerprint config - matches the C++ FingerprintData structure
|
||||||
|
|||||||
Reference in New Issue
Block a user