mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-09-08 18:58:56 +02:00
refactor: cleanup
This commit is contained in:
@@ -474,8 +474,21 @@ impl BrowserRunner {
|
||||
// tested immediately below - which is a deliberate per-profile setting
|
||||
// and not a consequence of the version.
|
||||
let mut updated_profile = profile.clone();
|
||||
// ONE-TIME MIGRATION: a profile that still stores a whole device beside
|
||||
// its identity moves to identity-only storage here, before the launch
|
||||
// reads it, and the migrated shape is what gets persisted below.
|
||||
if crate::wayfern_manager::WayfernManager::migrate_identity_config(&mut wayfern_config) {
|
||||
let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default();
|
||||
crate::wayfern_manager::WayfernManager::migrate_identity_config(&mut cfg);
|
||||
updated_profile.wayfern_config = Some(cfg);
|
||||
log::info!(
|
||||
"Migrated Wayfern profile {} to identity-only storage",
|
||||
profile.name
|
||||
);
|
||||
}
|
||||
let randomize_requested = wayfern_config.randomize_fingerprint_on_launch == Some(true);
|
||||
let needs_device = wayfern_config.fingerprint.is_none();
|
||||
let needs_device =
|
||||
wayfern_config.fingerprint.is_none() && wayfern_config.identity_id.is_none();
|
||||
if randomize_requested || needs_device {
|
||||
if needs_device && !randomize_requested {
|
||||
log::info!(
|
||||
@@ -530,16 +543,29 @@ impl BrowserRunner {
|
||||
generated.identity_id
|
||||
);
|
||||
|
||||
// Update the config with the new fingerprint for launching
|
||||
wayfern_config.fingerprint = Some(generated.fingerprint.clone());
|
||||
// Update the config with the new device for launching. An identity
|
||||
// stores the id and the location only; a legacy browser stores the
|
||||
// whole payload.
|
||||
let is_identity = generated.identity_id.is_some();
|
||||
wayfern_config.identity_id = generated.identity_id.clone();
|
||||
wayfern_config.identity_baseline = generated.identity_baseline.clone();
|
||||
wayfern_config.location = generated.location.clone();
|
||||
wayfern_config.identity_baseline = None;
|
||||
wayfern_config.fingerprint = if is_identity {
|
||||
None
|
||||
} else {
|
||||
Some(generated.fingerprint.clone())
|
||||
};
|
||||
|
||||
// Save the updated fingerprint to the profile so it persists.
|
||||
// Save the updated device to the profile so it persists.
|
||||
let mut updated_wayfern_config = updated_profile.wayfern_config.clone().unwrap_or_default();
|
||||
updated_wayfern_config.fingerprint = Some(generated.fingerprint);
|
||||
updated_wayfern_config.identity_id = generated.identity_id;
|
||||
updated_wayfern_config.identity_baseline = generated.identity_baseline;
|
||||
updated_wayfern_config.location = generated.location;
|
||||
updated_wayfern_config.identity_baseline = None;
|
||||
updated_wayfern_config.fingerprint = if is_identity {
|
||||
None
|
||||
} else {
|
||||
Some(generated.fingerprint)
|
||||
};
|
||||
// Preserve the randomize flag so it persists across launches
|
||||
updated_wayfern_config.randomize_fingerprint_on_launch =
|
||||
wayfern_config.randomize_fingerprint_on_launch;
|
||||
@@ -714,23 +740,19 @@ impl BrowserRunner {
|
||||
// which may differ from the stored one. Persist it so the next launch
|
||||
// starts from that value — saved below via
|
||||
// save_process_info(&updated_profile).
|
||||
// LEGACY profiles only: an identity-backed profile never persists the
|
||||
// device (the manager returns no echo for it), so this block is reached
|
||||
// only by a whole-payload profile applied with setFingerprint.
|
||||
if let Some(used_fp) = wayfern_result.used_fingerprint.clone() {
|
||||
let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default();
|
||||
let baseline_changed = wayfern_result.used_identity_baseline.is_some()
|
||||
&& cfg.identity_baseline != wayfern_result.used_identity_baseline;
|
||||
if cfg.fingerprint.as_deref() != Some(used_fp.as_str()) || baseline_changed {
|
||||
if cfg.identity_id.is_none() && cfg.fingerprint.as_deref() != Some(used_fp.as_str()) {
|
||||
log::info!(
|
||||
"Persisting applied fingerprint echoed by Wayfern for profile: {} (len {})",
|
||||
profile.name,
|
||||
used_fp.len()
|
||||
);
|
||||
cfg.fingerprint = Some(used_fp);
|
||||
// The baseline must move with the fingerprint it was computed
|
||||
// against, or the next launch diffs the two apart and invents
|
||||
// overrides the user never asked for.
|
||||
if let Some(baseline) = wayfern_result.used_identity_baseline.clone() {
|
||||
cfg.identity_baseline = Some(baseline);
|
||||
}
|
||||
cfg.identity_baseline = None;
|
||||
updated_profile.wayfern_config = Some(cfg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,12 +146,13 @@ fn language_matches_country(cc: &str, language: &str) -> Option<bool> {
|
||||
crate::geolocation::locale_selector()?.region_speaks(cc, language)
|
||||
}
|
||||
|
||||
/// Extract (timezone, language) from a profile's stored fingerprint JSON.
|
||||
/// Extract (timezone, language) from a profile's stored location, or from its
|
||||
/// legacy fingerprint payload when it still stores one.
|
||||
fn fingerprint_locale(profile: &BrowserProfile) -> (Option<String>, Option<String>) {
|
||||
let Some(config) = &profile.wayfern_config else {
|
||||
return (None, None);
|
||||
};
|
||||
let Some(fp_str) = &config.fingerprint else {
|
||||
let Some(fp_str) = config.location.as_ref().or(config.fingerprint.as_ref()) else {
|
||||
return (None, None);
|
||||
};
|
||||
let Ok(fp) = serde_json::from_str::<serde_json::Value>(fp_str) else {
|
||||
@@ -363,20 +364,34 @@ pub async fn match_profile_fingerprint_to_exit(
|
||||
let mut config = profile
|
||||
.wayfern_config
|
||||
.clone()
|
||||
.filter(|c| c.fingerprint.is_some())
|
||||
.filter(|c| c.fingerprint.is_some() || c.identity_id.is_some())
|
||||
.ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?;
|
||||
let fingerprint = config.fingerprint.clone().unwrap();
|
||||
|
||||
let geoip_override = serde_json::Value::String(exit_ip);
|
||||
let refreshed = crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation(
|
||||
&fingerprint,
|
||||
None,
|
||||
Some(&geoip_override),
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?;
|
||||
|
||||
config.fingerprint = Some(refreshed);
|
||||
if let Some(fingerprint) = config.fingerprint.clone() {
|
||||
// Legacy payload: the location lives inside the stored device.
|
||||
let refreshed = crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation(
|
||||
&fingerprint,
|
||||
None,
|
||||
Some(&geoip_override),
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?;
|
||||
config.fingerprint = Some(refreshed);
|
||||
} else {
|
||||
// Identity-backed: only the location object moves; the device stays
|
||||
// whatever the identity derives.
|
||||
let location = config.location.clone().unwrap_or_else(|| "{}".to_string());
|
||||
let refreshed = crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation(
|
||||
&location,
|
||||
None,
|
||||
Some(&geoip_override),
|
||||
)
|
||||
.await
|
||||
.ok_or_else(|| serde_json::json!({ "code": "FINGERPRINT_MATCH_FAILED" }).to_string())?;
|
||||
config.location = crate::wayfern_manager::WayfernManager::fingerprint_object(&refreshed)
|
||||
.and_then(|object| crate::wayfern_manager::WayfernManager::location_of(&object));
|
||||
}
|
||||
profile.wayfern_config = Some(config);
|
||||
manager.save_profile(&profile).map_err(|e| {
|
||||
serde_json::json!({ "code": "INTERNAL_ERROR", "params": { "detail": e.to_string() } })
|
||||
|
||||
@@ -1304,7 +1304,7 @@ async fn list_active_vpn_connections() -> Result<Vec<vpn::VpnStatus>, String> {
|
||||
struct SampleFingerprint {
|
||||
fingerprint: String,
|
||||
identity_id: Option<String>,
|
||||
identity_baseline: Option<String>,
|
||||
location: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1356,7 +1356,7 @@ async fn generate_sample_fingerprint(
|
||||
.map(|generated| SampleFingerprint {
|
||||
fingerprint: generated.fingerprint,
|
||||
identity_id: generated.identity_id,
|
||||
identity_baseline: generated.identity_baseline,
|
||||
location: generated.location,
|
||||
})
|
||||
.map_err(|e| format!("Failed to generate fingerprint: {e}"))
|
||||
} else {
|
||||
|
||||
@@ -4224,6 +4224,9 @@ impl McpServer {
|
||||
serde_json::json!({
|
||||
"browser": "wayfern",
|
||||
"fingerprint": config.fingerprint,
|
||||
"identity_id": config.identity_id,
|
||||
"identity_overrides": config.identity_overrides,
|
||||
"location": config.location,
|
||||
"os": config.os,
|
||||
"randomize_fingerprint_on_launch": config.randomize_fingerprint_on_launch,
|
||||
"screen_max_width": config.screen_max_width,
|
||||
|
||||
@@ -181,8 +181,9 @@ impl ProfileManager {
|
||||
// behavior; for generated ones this comes from the geolocation lookup.
|
||||
let mut geolocation_applied = true;
|
||||
|
||||
// Generate fingerprint if not already provided
|
||||
if config.fingerprint.is_none() {
|
||||
// Generate a device if the profile has neither a legacy payload nor an
|
||||
// identity.
|
||||
if config.fingerprint.is_none() && config.identity_id.is_none() {
|
||||
log::info!("Generating fingerprint for Wayfern profile: {name}");
|
||||
|
||||
// Create a temporary profile for fingerprint generation
|
||||
@@ -224,12 +225,16 @@ impl ProfileManager {
|
||||
.await
|
||||
{
|
||||
Ok(generated) => {
|
||||
config.fingerprint = Some(generated.fingerprint);
|
||||
// Set together with the fingerprint they describe. A profile that
|
||||
// stored one without the other would either lose reproducibility or
|
||||
// diff its whole device into overrides on the next launch.
|
||||
// An identity-backed profile stores the id and the location and
|
||||
// never the device; a legacy browser stores the whole payload.
|
||||
config.identity_id = generated.identity_id;
|
||||
config.identity_baseline = generated.identity_baseline;
|
||||
config.location = generated.location;
|
||||
config.identity_baseline = None;
|
||||
config.fingerprint = if config.identity_id.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(generated.fingerprint)
|
||||
};
|
||||
geolocation_applied = generated.geolocation_applied;
|
||||
log::info!("Successfully generated fingerprint for Wayfern profile: {name}");
|
||||
}
|
||||
@@ -1193,12 +1198,42 @@ impl ProfileManager {
|
||||
// re-mint the device on the next launch and throw the edit away with it,
|
||||
// which is the opposite of what an override is for. Carry it forward unless
|
||||
// the caller either supplied its own or cleared the fingerprint outright.
|
||||
if config.identity_id.is_none() && config.fingerprint.is_some() {
|
||||
if let Some(stored) = profile.wayfern_config.as_ref() {
|
||||
if let Some(stored) = profile.wayfern_config.as_ref() {
|
||||
if config.identity_id.is_none()
|
||||
&& (config.fingerprint.is_some() || config.identity_overrides.is_some())
|
||||
{
|
||||
config.identity_id = stored.identity_id.clone();
|
||||
config.identity_baseline = stored.identity_baseline.clone();
|
||||
}
|
||||
if config.identity_id.is_some() {
|
||||
if config.location.is_none() {
|
||||
config.location = stored.location.clone();
|
||||
}
|
||||
if config.identity_overrides.is_none() {
|
||||
config.identity_overrides = stored.identity_overrides.clone();
|
||||
}
|
||||
// A WHOLE fingerprint sent for an identity-backed profile (an older UI
|
||||
// or an API/MCP caller) is an explicit set of fields: it becomes the
|
||||
// override map and is never stored as a device.
|
||||
if let Some(fingerprint) = config.fingerprint.take() {
|
||||
if let Some(object) =
|
||||
crate::wayfern_manager::WayfernManager::fingerprint_object(&fingerprint)
|
||||
{
|
||||
let overrides =
|
||||
crate::wayfern_manager::WayfernManager::overrides_from_explicit_fingerprint(&object);
|
||||
config.identity_overrides = if overrides.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::to_string(&overrides).ok()
|
||||
};
|
||||
if config.location.is_none() {
|
||||
config.location = crate::wayfern_manager::WayfernManager::location_of(&object);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The baseline is a legacy field; nothing writes it any more.
|
||||
config.identity_baseline = None;
|
||||
|
||||
// Update the Wayfern configuration
|
||||
profile.wayfern_config = Some(config);
|
||||
@@ -2036,7 +2071,7 @@ pub async fn update_wayfern_config(
|
||||
profile_id: String,
|
||||
config: WayfernConfig,
|
||||
) -> Result<(), String> {
|
||||
if config.fingerprint.is_some()
|
||||
if (config.fingerprint.is_some() || config.identity_overrides.is_some())
|
||||
&& !crate::cloud_auth::CLOUD_AUTH
|
||||
.can_use_cross_os_fingerprints()
|
||||
.await
|
||||
|
||||
@@ -948,7 +948,7 @@ impl ProfileImporter {
|
||||
}
|
||||
}
|
||||
|
||||
if config.fingerprint.is_none() {
|
||||
if config.fingerprint.is_none() && config.identity_id.is_none() {
|
||||
let temp_profile = BrowserProfile {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: new_profile_name.to_string(),
|
||||
@@ -989,9 +989,14 @@ impl ProfileImporter {
|
||||
// geo_proxy_signature is intentionally left unset here: the first
|
||||
// launch's signature-mismatch refresh verifies the location either way.
|
||||
Ok(generated) => {
|
||||
config.fingerprint = Some(generated.fingerprint);
|
||||
config.identity_id = generated.identity_id;
|
||||
config.identity_baseline = generated.identity_baseline;
|
||||
config.location = generated.location;
|
||||
config.identity_baseline = None;
|
||||
config.fingerprint = if config.identity_id.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(generated.fingerprint)
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = fs::remove_dir_all(&new_profile_uuid_dir);
|
||||
|
||||
+202
-112
@@ -45,15 +45,30 @@ pub struct WayfernConfig {
|
||||
/// location can be refreshed instead of showing stale data.
|
||||
#[serde(default)]
|
||||
pub geo_proxy_signature: Option<String>,
|
||||
/// Identity handle for this profile, when it has one. `None` means the
|
||||
/// profile stores a whole fingerprint payload instead.
|
||||
/// Identity handle for this profile, when it has one. An identity-backed
|
||||
/// profile stores the id, its `location` and its `identity_overrides` and
|
||||
/// NOTHING else: the device is rebuilt from the id by the browser on every
|
||||
/// launch, so no fingerprint payload ever sits on disk to be copied.
|
||||
/// `None` means a legacy profile that still stores a whole payload in
|
||||
/// `fingerprint` and is applied with `Wayfern.setFingerprint`.
|
||||
#[serde(default)]
|
||||
pub identity_id: Option<String>,
|
||||
/// The fingerprint as first received for `identity_id`, before geolocation
|
||||
/// and before any user edit. Diffed against `fingerprint` on launch to
|
||||
/// recover the user's own edits.
|
||||
/// LEGACY, read only by `migrate_identity_config`: the derived device an
|
||||
/// older build snapshotted so the user's edits could be diffed out of the
|
||||
/// stored payload. Cleared by the migration; never written again.
|
||||
#[serde(default)]
|
||||
pub identity_baseline: Option<String>,
|
||||
/// The user's own edits to an identity-backed device, as a JSON object of
|
||||
/// fingerprint fields. Sent verbatim as `setIdentity` overrides; everything
|
||||
/// not listed here comes from the identity. `None` means no edits.
|
||||
#[serde(default)]
|
||||
pub identity_overrides: Option<String>,
|
||||
/// The location the profile's exit resolves to (timezone, timezoneOffset,
|
||||
/// language, languages, latitude, longitude, accuracy) as a JSON object.
|
||||
/// It depends on the proxy, not on the identity, which is why it is the one
|
||||
/// piece of device state an identity-backed profile persists.
|
||||
#[serde(default)]
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
/// First Wayfern version that ships `createIdentity`/`setIdentity`/
|
||||
@@ -119,12 +134,15 @@ const LOCALE_CARRY_OVER_KEYS: [&str; 7] = [
|
||||
/// A freshly generated device, plus its identity handle when the browser
|
||||
/// supports identities.
|
||||
pub struct GeneratedFingerprint {
|
||||
/// The fingerprint JSON to store in `WayfernConfig::fingerprint`. Both paths
|
||||
/// produce a flat camelCase object, so everything that reads the stored
|
||||
/// fingerprint keeps working either way.
|
||||
/// The device the browser produced, as a flat camelCase JSON object. For a
|
||||
/// LEGACY browser this is what `WayfernConfig::fingerprint` stores. For an
|
||||
/// identity-backed profile it is a VIEW for the caller to show once and
|
||||
/// discard: only `identity_id` and `location` are persisted.
|
||||
pub fingerprint: String,
|
||||
pub identity_id: Option<String>,
|
||||
pub identity_baseline: Option<String>,
|
||||
/// `WayfernConfig::location` for the exit this device was generated
|
||||
/// against, or `None` when no location field was resolved.
|
||||
pub location: Option<String>,
|
||||
/// Whether fresh geolocation was resolved and applied. Callers must only
|
||||
/// stamp `geo_proxy_signature` when this is true.
|
||||
pub geolocation_applied: bool,
|
||||
@@ -277,7 +295,7 @@ impl WayfernManager {
|
||||
|
||||
/// Parse a stored fingerprint JSON into its object, tolerating the legacy
|
||||
/// `{ "fingerprint": {...} }` wrapper some old profiles carry.
|
||||
fn fingerprint_object(
|
||||
pub fn fingerprint_object(
|
||||
fingerprint_json: &str,
|
||||
) -> Option<serde_json::Map<String, serde_json::Value>> {
|
||||
let parsed: serde_json::Value = serde_json::from_str(fingerprint_json).ok()?;
|
||||
@@ -285,6 +303,94 @@ impl WayfernManager {
|
||||
fp.as_object().cloned()
|
||||
}
|
||||
|
||||
/// A stored JSON object field (`identity_overrides`, `location`), or an
|
||||
/// empty map when absent or unparsable.
|
||||
pub fn stored_object(json: Option<&str>) -> serde_json::Map<String, serde_json::Value> {
|
||||
json.and_then(Self::fingerprint_object).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The exit-derived location fields a device object carries, in the shape
|
||||
/// `WayfernConfig::location` stores; `None` when it carries none.
|
||||
pub fn location_of(
|
||||
device: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> Option<String> {
|
||||
let mut location = serde_json::Map::new();
|
||||
for key in LOCALE_CARRY_OVER_KEYS {
|
||||
if let Some(value) = device.get(key) {
|
||||
if !value.is_null() {
|
||||
location.insert(key.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if location.is_empty() {
|
||||
None
|
||||
} else {
|
||||
serde_json::to_string(&location).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides from a WHOLE fingerprint an API or MCP caller supplied for an
|
||||
/// identity-backed profile: every field it names is taken as an explicit
|
||||
/// edit, except the provenance keys the browser refuses and the location
|
||||
/// keys, which travel through `location`.
|
||||
pub fn overrides_from_explicit_fingerprint(
|
||||
fingerprint: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut overrides = serde_json::Map::new();
|
||||
for (key, value) in fingerprint {
|
||||
if DERIVED_PROVENANCE_KEYS.contains(&key.as_str())
|
||||
|| GEO_PARAM_KEYS.contains(&key.as_str())
|
||||
|| LOCALE_CARRY_OVER_KEYS.contains(&key.as_str())
|
||||
|| value.is_null()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
overrides.insert(key.clone(), value.clone());
|
||||
}
|
||||
overrides
|
||||
}
|
||||
|
||||
/// ONE-TIME MIGRATION to identity-only storage. A profile created by an
|
||||
/// earlier build stored the whole device in `fingerprint` beside its
|
||||
/// `identity_id`, with `identity_baseline` recording the derived view so the
|
||||
/// user's edits could be diffed out. This moves those edits into
|
||||
/// `identity_overrides`, the exit-derived fields into `location`, and drops
|
||||
/// the payload and the baseline. Returns whether anything changed.
|
||||
///
|
||||
/// Without a baseline nothing can separate an edit from a derived value, so
|
||||
/// no override is recovered: pinning the whole device would defeat the
|
||||
/// identity, and the browser rebuilds every field from the id anyway.
|
||||
pub fn migrate_identity_config(config: &mut WayfernConfig) -> bool {
|
||||
if config.identity_id.is_none() {
|
||||
return false;
|
||||
}
|
||||
let Some(stored_json) = config.fingerprint.clone() else {
|
||||
if config.identity_baseline.is_some() {
|
||||
config.identity_baseline = None;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
let stored = Self::fingerprint_object(&stored_json).unwrap_or_default();
|
||||
let overrides = match config
|
||||
.identity_baseline
|
||||
.as_deref()
|
||||
.and_then(Self::fingerprint_object)
|
||||
{
|
||||
Some(baseline) => Self::identity_overrides(&stored, &baseline),
|
||||
None => serde_json::Map::new(),
|
||||
};
|
||||
if config.identity_overrides.is_none() && !overrides.is_empty() {
|
||||
config.identity_overrides = serde_json::to_string(&overrides).ok();
|
||||
}
|
||||
if config.location.is_none() {
|
||||
config.location = Self::location_of(&stored);
|
||||
}
|
||||
config.fingerprint = None;
|
||||
config.identity_baseline = None;
|
||||
true
|
||||
}
|
||||
|
||||
/// The user's edits, recovered as the difference between the fingerprint the
|
||||
/// profile stores and the view the browser derived from the identity.
|
||||
///
|
||||
@@ -379,35 +485,6 @@ impl WayfernManager {
|
||||
locale.split('-').next().unwrap_or(locale)
|
||||
}
|
||||
|
||||
/// The baseline to persist after a successful `setIdentity`.
|
||||
///
|
||||
/// For every key the user did NOT override, adopt whatever the browser just
|
||||
/// derived, so a value that changes on a newer browser flows through instead
|
||||
/// of reading as a user edit forever. For overridden keys the applied view
|
||||
/// holds the override rather than the derived value, so the previous baseline
|
||||
/// is kept as the diff reference.
|
||||
fn refreshed_identity_baseline(
|
||||
applied: &serde_json::Map<String, serde_json::Value>,
|
||||
previous_baseline: &serde_json::Map<String, serde_json::Value>,
|
||||
overrides: &serde_json::Map<String, serde_json::Value>,
|
||||
) -> serde_json::Map<String, serde_json::Value> {
|
||||
let mut baseline = applied.clone();
|
||||
for key in overrides.keys() {
|
||||
match previous_baseline.get(key) {
|
||||
Some(previous) => {
|
||||
baseline.insert(key.clone(), previous.clone());
|
||||
}
|
||||
// The user added a field the baseline never carried, so there is
|
||||
// nothing to fall back to and the key must stay absent from the
|
||||
// baseline or the edit would diff away on the next launch.
|
||||
None => {
|
||||
baseline.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
baseline
|
||||
}
|
||||
|
||||
/// The `setIdentity` geolocation parameters carried by a stored fingerprint.
|
||||
fn geo_params(
|
||||
fingerprint: &serde_json::Map<String, serde_json::Value>,
|
||||
@@ -942,7 +1019,7 @@ impl WayfernManager {
|
||||
}
|
||||
};
|
||||
|
||||
let (fingerprint, identity_id, identity_baseline, geolocation_applied) = match generate_result {
|
||||
let (fingerprint, identity_id, geolocation_applied) = match generate_result {
|
||||
Ok(result) => {
|
||||
// createIdentity returns { identityId, identity }; getFingerprint
|
||||
// returns { fingerprint: {...} }. A bare object is tolerated so a
|
||||
@@ -961,15 +1038,6 @@ impl WayfernManager {
|
||||
// Normalize the fingerprint: convert JSON string fields to proper types
|
||||
let mut normalized = Self::normalize_fingerprint(fp);
|
||||
|
||||
// Snapshot the derived view BEFORE geolocation is applied, so the
|
||||
// location fields donut writes below are not mistaken for user edits
|
||||
// when the overrides are recovered on launch.
|
||||
let identity_baseline = if use_identity_api {
|
||||
serde_json::to_string(&normalized).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// reqwest's SOCKS connector (hyper-util) corrupts its parse buffer
|
||||
// when a proxy splits a handshake reply across TCP segments, so a
|
||||
// socks upstream here can fail even though the proxy is healthy.
|
||||
@@ -1020,12 +1088,7 @@ impl WayfernManager {
|
||||
let _ = crate::proxy_runner::stop_proxy_process(&worker_id).await;
|
||||
}
|
||||
|
||||
(
|
||||
normalized,
|
||||
identity_id,
|
||||
identity_baseline,
|
||||
geolocation_applied,
|
||||
)
|
||||
(normalized, identity_id, geolocation_applied)
|
||||
}
|
||||
Err(e) => {
|
||||
cleanup().await;
|
||||
@@ -1075,9 +1138,9 @@ impl WayfernManager {
|
||||
}
|
||||
|
||||
Ok(GeneratedFingerprint {
|
||||
location: fingerprint.as_object().and_then(Self::location_of),
|
||||
fingerprint: fingerprint_json,
|
||||
identity_id,
|
||||
identity_baseline,
|
||||
geolocation_applied,
|
||||
})
|
||||
}
|
||||
@@ -1407,8 +1470,88 @@ impl WayfernManager {
|
||||
|
||||
// Apply fingerprint if configured
|
||||
let mut used_fingerprint: Option<String> = None;
|
||||
let mut used_identity_baseline: Option<String> = None;
|
||||
if let Some(fingerprint_json) = &config.fingerprint {
|
||||
// Always None: nothing writes a baseline any more. The field stays on the
|
||||
// result for the one launch that still migrates a legacy identity profile.
|
||||
let used_identity_baseline: Option<String> = None;
|
||||
// An identity-backed profile: the id, the user's overrides and the exit's
|
||||
// location are all the browser needs, and all the profile stores. The
|
||||
// device comes back in the response and is deliberately NOT persisted.
|
||||
let identity_only = supports_identity_api(&profile.version)
|
||||
&& config.identity_id.is_some()
|
||||
&& config.fingerprint.is_none();
|
||||
if identity_only {
|
||||
let identity_id = config.identity_id.clone().unwrap_or_default();
|
||||
let overrides = Self::stored_object(config.identity_overrides.as_deref());
|
||||
let location = Self::stored_object(config.location.as_deref());
|
||||
let wayfern_token = crate::cloud_auth::CLOUD_AUTH.get_wayfern_token().await;
|
||||
|
||||
let mut params = serde_json::Map::new();
|
||||
params.insert("identityId".to_string(), json!(identity_id));
|
||||
// The claimed OS travels explicitly as well as inside the id. A Wayfern
|
||||
// 152 id carries an epoch and a 16-bit check that a 151 browser's decoder
|
||||
// does not know; without this parameter 151 would read such an id as
|
||||
// untagged and rebuild the HOST OS. Both releases let the explicit
|
||||
// parameter win, so this keeps one stored profile portable across them.
|
||||
if let Some(os) = config.os.as_deref().filter(|os| !os.is_empty()) {
|
||||
params.insert("operatingSystem".to_string(), json!(os));
|
||||
}
|
||||
if !overrides.is_empty() {
|
||||
params.insert(
|
||||
"overrides".to_string(),
|
||||
serde_json::Value::Object(overrides.clone()),
|
||||
);
|
||||
}
|
||||
// Location is a property of the exit, not of the identity, so it travels
|
||||
// in setIdentity's own parameters rather than as an override.
|
||||
params.extend(Self::geo_params(&location));
|
||||
if let Some(ref token) = wayfern_token {
|
||||
params.insert("wayfernToken".to_string(), json!(token));
|
||||
}
|
||||
log::info!(
|
||||
"Applying Wayfern identity {} with {} override(s): {:?}",
|
||||
identity_id,
|
||||
overrides.len(),
|
||||
overrides.keys().collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
let mut applied_ok = false;
|
||||
let mut last_apply_error: Option<String> = None;
|
||||
for target in &page_targets {
|
||||
if let Some(ws_url) = &target.websocket_debugger_url {
|
||||
match self
|
||||
.send_cdp_command(
|
||||
ws_url,
|
||||
"Wayfern.setIdentity",
|
||||
serde_json::Value::Object(params.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
applied_ok = true;
|
||||
log::info!("Successfully applied identity to page target");
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to apply identity to target: {e}");
|
||||
last_apply_error = Some(e.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !applied_ok {
|
||||
let detail = last_apply_error
|
||||
.unwrap_or_else(|| "the browser exposed no page target to apply it to".to_string());
|
||||
log::error!(
|
||||
"Killing Wayfern (pid {process_id:?}) for profile {}: the identity was never applied: {detail}",
|
||||
profile.name
|
||||
);
|
||||
if let Some(pid) = process_id {
|
||||
kill_browser_process(pid);
|
||||
}
|
||||
return Err(
|
||||
Self::apply_failure_error(&detail, Self::claimed_operating_system(config, None)).into(),
|
||||
);
|
||||
}
|
||||
} else if let Some(fingerprint_json) = &config.fingerprint {
|
||||
log::info!(
|
||||
"Applying fingerprint to Wayfern browser, fingerprint length: {} chars",
|
||||
fingerprint_json.len()
|
||||
@@ -1490,7 +1633,7 @@ impl WayfernManager {
|
||||
|
||||
// On the identity path only the user's own edits are sent; everything
|
||||
// else comes from the identity itself.
|
||||
let (apply_method, apply_params, previous_baseline, overrides) =
|
||||
let (apply_method, apply_params, _previous_baseline, overrides) =
|
||||
match config.identity_id.as_deref().filter(|_| apply_by_identity) {
|
||||
Some(identity_id) => {
|
||||
let previous_baseline = config
|
||||
@@ -1577,23 +1720,6 @@ impl WayfernManager {
|
||||
.cloned()
|
||||
.unwrap_or(result);
|
||||
if let Some(applied_obj) = applied.as_object() {
|
||||
if apply_by_identity {
|
||||
// The baseline is "what the browser derived", so it is
|
||||
// computed from the untouched response. Move it forward for
|
||||
// everything the user did not override: leaving it behind
|
||||
// would make the next launch read a re-derived value as a
|
||||
// user edit and pin it.
|
||||
let baseline = Self::refreshed_identity_baseline(
|
||||
applied_obj,
|
||||
&previous_baseline,
|
||||
&overrides,
|
||||
);
|
||||
match serde_json::to_string(&baseline) {
|
||||
Ok(s) => used_identity_baseline = Some(s),
|
||||
Err(e) => log::warn!("Failed to serialize identity baseline: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
let mut persisted = applied;
|
||||
if apply_by_identity {
|
||||
// The location travelled as setIdentity parameters rather
|
||||
@@ -2156,42 +2282,6 @@ mod tests {
|
||||
assert!(geo.get("platform").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baseline_adopts_rederived_values_but_keeps_overridden_ones() {
|
||||
// The same identity on a newer browser derives 12 cores where it used to
|
||||
// derive 8, while the user has pinned deviceMemory to 32.
|
||||
let previous = obj(r#"{"hardwareConcurrency": 8, "deviceMemory": 8}"#);
|
||||
let overrides = obj(r#"{"deviceMemory": 32}"#);
|
||||
let applied = obj(r#"{"hardwareConcurrency": 12, "deviceMemory": 32}"#);
|
||||
|
||||
let refreshed = WayfernManager::refreshed_identity_baseline(&applied, &previous, &overrides);
|
||||
|
||||
// Re-derived: adopted, so the next launch does not mistake it for an edit.
|
||||
assert_eq!(refreshed.get("hardwareConcurrency"), Some(&json!(12)));
|
||||
// Overridden: the applied view holds the override, so the derived value is
|
||||
// kept as the diff reference and the override survives.
|
||||
assert_eq!(refreshed.get("deviceMemory"), Some(&json!(8)));
|
||||
|
||||
let next_overrides = WayfernManager::identity_overrides(&applied, &refreshed);
|
||||
assert_eq!(next_overrides.len(), 1);
|
||||
assert_eq!(next_overrides.get("deviceMemory"), Some(&json!(32)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn baseline_keeps_a_user_added_key_out_so_the_override_survives() {
|
||||
// The user set a field the derivation never produces. There is no derived
|
||||
// value to fall back to, so the key must stay absent from the baseline.
|
||||
let previous = obj(r#"{"platform": "Win32"}"#);
|
||||
let overrides = obj(r#"{"doNotTrack": "1"}"#);
|
||||
let applied = obj(r#"{"platform": "Win32", "doNotTrack": "1"}"#);
|
||||
|
||||
let refreshed = WayfernManager::refreshed_identity_baseline(&applied, &previous, &overrides);
|
||||
assert!(refreshed.get("doNotTrack").is_none());
|
||||
|
||||
let next_overrides = WayfernManager::identity_overrides(&applied, &refreshed);
|
||||
assert_eq!(next_overrides.get("doNotTrack"), Some(&json!("1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_launch_echo_only_fills_location_the_browser_left_out() {
|
||||
// setIdentity carries the location in its own parameters, so the applied
|
||||
|
||||
@@ -388,7 +388,10 @@ export function enableProfileSync(profileId: string): Promise<void> {
|
||||
* the operator happens to be sitting. Falls back to this machine's zone.
|
||||
*/
|
||||
export function profileTimezone(profile: BrowserProfile): string {
|
||||
const raw = profile.wayfern_config?.fingerprint;
|
||||
// Identity-backed profiles keep the exit's location in `location`; legacy
|
||||
// ones carry it inside the stored payload.
|
||||
const raw =
|
||||
profile.wayfern_config?.location ?? profile.wayfern_config?.fingerprint;
|
||||
if (raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as WayfernFingerprintConfig;
|
||||
|
||||
@@ -31,7 +31,11 @@ import { RippleButton } from "./ui/ripple";
|
||||
function getScreenSize(
|
||||
profile: BrowserProfile,
|
||||
): { w: number; h: number } | null {
|
||||
const fp = profile.wayfern_config?.fingerprint;
|
||||
// An identity-backed profile stores no device, only the user's edits, so a
|
||||
// screen size is available only when the user pinned one.
|
||||
const fp =
|
||||
profile.wayfern_config?.fingerprint ??
|
||||
profile.wayfern_config?.identity_overrides;
|
||||
if (!fp) return null;
|
||||
try {
|
||||
const parsed: WayfernFingerprintConfig = JSON.parse(fp);
|
||||
|
||||
@@ -65,9 +65,10 @@ export function WayfernConfigDialog({
|
||||
const handleSave = async () => {
|
||||
if (!profile) return;
|
||||
|
||||
if (config.fingerprint) {
|
||||
const storedJson = config.identity_overrides ?? config.fingerprint;
|
||||
if (storedJson) {
|
||||
try {
|
||||
JSON.parse(config.fingerprint);
|
||||
JSON.parse(storedJson);
|
||||
} catch (_error) {
|
||||
const { toast } = await import("sonner");
|
||||
toast.error(t("wayfernConfigDialog.invalidFingerprint"), {
|
||||
|
||||
@@ -55,7 +55,7 @@ const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => {
|
||||
interface GeneratedFingerprint {
|
||||
fingerprint: string;
|
||||
identity_id: string | null;
|
||||
identity_baseline: string | null;
|
||||
location: string | null;
|
||||
}
|
||||
|
||||
const getCurrentOS = (): WayfernOS => {
|
||||
@@ -109,15 +109,18 @@ export function WayfernConfigForm({
|
||||
configJson,
|
||||
},
|
||||
);
|
||||
onConfigChange("fingerprint", result.fingerprint);
|
||||
// The identity travels with the fingerprint it produced. Storing one
|
||||
// without the other leaves a device the launch path cannot reproduce, so
|
||||
// it would be discarded and re-minted on the next launch.
|
||||
// An identity-backed profile stores the id, its location and the user's
|
||||
// edits, never the device: the browser rebuilds the device from the id
|
||||
// on every launch, so nothing worth copying is ever written to disk. A
|
||||
// legacy browser without the identity API still stores the payload.
|
||||
onConfigChange("identity_id", result.identity_id ?? undefined);
|
||||
onConfigChange("location", result.location ?? undefined);
|
||||
onConfigChange(
|
||||
"identity_baseline",
|
||||
result.identity_baseline ?? undefined,
|
||||
"fingerprint",
|
||||
result.identity_id ? undefined : result.fingerprint,
|
||||
);
|
||||
onConfigChange("identity_overrides", undefined);
|
||||
onConfigChange("identity_baseline", undefined);
|
||||
} catch (error) {
|
||||
console.error("Failed to generate fingerprint:", error);
|
||||
} finally {
|
||||
@@ -164,12 +167,16 @@ export function WayfernConfigForm({
|
||||
onConfigChange,
|
||||
]);
|
||||
|
||||
// What the form edits: the override map for an identity-backed profile
|
||||
// (only the user's own edits exist on disk), the whole payload for a legacy
|
||||
// one.
|
||||
const editedJson = config.identity_id
|
||||
? config.identity_overrides
|
||||
: config.fingerprint;
|
||||
useEffect(() => {
|
||||
if (config.fingerprint) {
|
||||
if (editedJson) {
|
||||
try {
|
||||
const parsed = JSON.parse(
|
||||
config.fingerprint,
|
||||
) as WayfernFingerprintConfig;
|
||||
const parsed = JSON.parse(editedJson) as WayfernFingerprintConfig;
|
||||
setFingerprintConfig(parsed);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse fingerprint config:", error);
|
||||
@@ -178,7 +185,7 @@ export function WayfernConfigForm({
|
||||
} else {
|
||||
setFingerprintConfig({});
|
||||
}
|
||||
}, [config.fingerprint]);
|
||||
}, [editedJson]);
|
||||
|
||||
const updateFingerprintConfig = (
|
||||
key: keyof WayfernFingerprintConfig,
|
||||
@@ -200,7 +207,12 @@ export function WayfernConfigForm({
|
||||
|
||||
try {
|
||||
const jsonString = JSON.stringify(newConfig);
|
||||
onConfigChange("fingerprint", jsonString);
|
||||
onConfigChange(
|
||||
config.identity_id ? "identity_overrides" : "fingerprint",
|
||||
Object.keys(newConfig).length === 0 && config.identity_id
|
||||
? undefined
|
||||
: jsonString,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to serialize fingerprint config:", error);
|
||||
}
|
||||
|
||||
+3
-1
@@ -414,7 +414,9 @@ export interface WayfernConfig {
|
||||
os?: WayfernOS; // Operating system for fingerprint generation
|
||||
geo_proxy_signature?: string; // Internal: routing the fingerprint's location was computed for
|
||||
identity_id?: string; // Internal: UUID the device is derived from on browsers with the identity API
|
||||
identity_baseline?: string; // Internal: derived fingerprint before edits, diffed to recover overrides
|
||||
identity_baseline?: string; // Legacy: read once by the migration to identity-only storage, never written
|
||||
identity_overrides?: string; // JSON object of the user's own edits to an identity-backed device
|
||||
location?: string; // JSON object of the exit-derived location fields (timezone, language, coordinates)
|
||||
}
|
||||
|
||||
// Wayfern fingerprint config - matches the C++ FingerprintData structure
|
||||
|
||||
Reference in New Issue
Block a user