From 75eb2c72a97cc8659aabca7f7fb7952c284ad7cc Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 11 Jan 2026 02:25:12 +0400 Subject: [PATCH] refactor: separate form for wayfern --- src-tauri/src/wayfern_manager.rs | 131 +- src/components/camoufox-config-dialog.tsx | 45 +- src/components/create-profile-dialog.tsx | 4 +- .../shared-camoufox-config-form.tsx | 24 +- src/components/wayfern-config-form.tsx | 1091 +++++++++++++++++ src/types.ts | 112 +- 6 files changed, 1369 insertions(+), 38 deletions(-) create mode 100644 src/components/wayfern-config-form.tsx diff --git a/src-tauri/src/wayfern_manager.rs b/src-tauri/src/wayfern_manager.rs index 370c1ec..f647d1d 100644 --- a/src-tauri/src/wayfern_manager.rs +++ b/src-tauri/src/wayfern_manager.rs @@ -131,6 +131,26 @@ impl WayfernManager { Ok(port) } + /// Normalize fingerprint data from Wayfern CDP format to our storage format. + /// Wayfern returns fields like fonts, webglParameters as JSON strings which we keep as-is. + fn normalize_fingerprint(fingerprint: serde_json::Value) -> serde_json::Value { + // Our storage format matches what Wayfern returns: + // - fonts, plugins, mimeTypes, voices are JSON strings + // - webglParameters, webgl2Parameters, etc. are JSON strings + // The form displays them as JSON text areas, so no conversion needed. + fingerprint + } + + /// Denormalize fingerprint data from our storage format to Wayfern CDP format. + /// Wayfern expects certain fields as JSON strings. + fn denormalize_fingerprint(fingerprint: serde_json::Value) -> serde_json::Value { + // Our storage format matches what Wayfern expects: + // - fonts, plugins, mimeTypes, voices are JSON strings + // - webglParameters, webgl2Parameters, etc. are JSON strings + // So no conversion is needed + fingerprint + } + async fn wait_for_cdp_ready( &self, port: u16, @@ -316,7 +336,25 @@ impl WayfernManager { Ok(result) => { // Wayfern.getFingerprint returns { fingerprint: {...} } // We need to extract just the fingerprint object - result.get("fingerprint").cloned().unwrap_or(result) + let fp = result.get("fingerprint").cloned().unwrap_or(result); + // Normalize the fingerprint: convert JSON string fields to proper types + let mut normalized = Self::normalize_fingerprint(fp); + + // Add default timezone/geolocation if not present + // Wayfern's Bayesian network generator doesn't include these fields, + // so we need to add sensible defaults + 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)); // EST = UTC-5 = 300 minutes + } + // Note: latitude/longitude are intentionally not set by default + // as they reveal precise location. Users should set these manually if needed. + } + + normalized } Err(e) => { cleanup().await; @@ -336,6 +374,19 @@ impl WayfernManager { .as_object() .map(|o| o.keys().collect::>()) ); + + // Log timezone/geolocation fields specifically for debugging + if let Some(obj) = fingerprint.as_object() { + log::info!( + "Generated fingerprint - timezone: {:?}, timezoneOffset: {:?}, latitude: {:?}, longitude: {:?}, language: {:?}", + obj.get("timezone"), + obj.get("timezoneOffset"), + obj.get("latitude"), + obj.get("longitude"), + obj.get("language") + ); + } + Ok(fingerprint_json) } @@ -383,9 +434,8 @@ impl WayfernManager { args.push(format!("--proxy-server={proxy}")); } - if let Some(url) = url { - args.push(url.to_string()); - } + // Don't add URL to args - we'll navigate via CDP after setting fingerprint + // This ensures fingerprint is applied at navigation commit time let mut cmd = TokioCommand::new(&executable_path); cmd.args(&args); @@ -397,6 +447,14 @@ impl WayfernManager { self.wait_for_cdp_ready(port).await?; + // Get CDP targets first - needed for both fingerprint and navigation + let targets = self.get_cdp_targets(port).await?; + log::info!("Found {} CDP targets", targets.len()); + + let page_targets: Vec<_> = targets.iter().filter(|t| t.target_type == "page").collect(); + log::info!("Found {} page targets", page_targets.len()); + + // Apply fingerprint if configured if let Some(fingerprint_json) = &config.fingerprint { log::info!( "Applying fingerprint to Wayfern browser, fingerprint length: {} chars", @@ -408,7 +466,7 @@ impl WayfernManager { // The stored fingerprint should be the fingerprint object directly (after our fix in generate_fingerprint_config) // But for backwards compatibility, also handle the wrapped format - let fingerprint = if stored_value.get("fingerprint").is_some() { + let mut fingerprint = if stored_value.get("fingerprint").is_some() { // Old format: {"fingerprint": {...}} - extract the inner fingerprint stored_value.get("fingerprint").cloned().unwrap() } else { @@ -416,28 +474,51 @@ impl WayfernManager { stored_value.clone() }; + // Add default timezone if not present (for profiles created before timezone was added) + if let Some(obj) = fingerprint.as_object_mut() { + if !obj.contains_key("timezone") { + obj.insert("timezone".to_string(), json!("America/New_York")); + log::info!("Added default timezone to fingerprint"); + } + if !obj.contains_key("timezoneOffset") { + obj.insert("timezoneOffset".to_string(), json!(300)); + log::info!("Added default timezoneOffset to fingerprint"); + } + } + + // Denormalize fingerprint for Wayfern CDP (convert arrays/objects to JSON strings) + let fingerprint_for_cdp = Self::denormalize_fingerprint(fingerprint); + log::info!( "Fingerprint prepared for CDP command, fields: {:?}", - fingerprint + fingerprint_for_cdp .as_object() - .map(|o| o.keys().take(5).collect::>()) + .map(|o| o.keys().collect::>()) ); - let targets = self.get_cdp_targets(port).await?; - log::info!("Found {} CDP targets", targets.len()); + // Log timezone and geolocation fields specifically for debugging + if let Some(obj) = fingerprint_for_cdp.as_object() { + log::info!( + "Timezone/Geolocation fields - timezone: {:?}, timezoneOffset: {:?}, latitude: {:?}, longitude: {:?}, language: {:?}, languages: {:?}", + obj.get("timezone"), + obj.get("timezoneOffset"), + obj.get("latitude"), + obj.get("longitude"), + obj.get("language"), + obj.get("languages") + ); + } - let page_targets: Vec<_> = targets.iter().filter(|t| t.target_type == "page").collect(); - log::info!( - "Found {} page targets for fingerprint application", - page_targets.len() - ); - - for target in page_targets { + for target in &page_targets { if let Some(ws_url) = &target.websocket_debugger_url { log::info!("Applying fingerprint to target via WebSocket: {}", ws_url); // Wayfern.setFingerprint expects the fingerprint object directly, NOT wrapped match self - .send_cdp_command(ws_url, "Wayfern.setFingerprint", fingerprint.clone()) + .send_cdp_command( + ws_url, + "Wayfern.setFingerprint", + fingerprint_for_cdp.clone(), + ) .await { Ok(result) => log::info!( @@ -452,6 +533,22 @@ impl WayfernManager { log::warn!("No fingerprint found in config, browser will use default fingerprint"); } + // Navigate to URL via CDP - fingerprint will be applied at navigation commit time + if let Some(url) = url { + log::info!("Navigating to URL via CDP: {}", url); + if let Some(target) = page_targets.first() { + if let Some(ws_url) = &target.websocket_debugger_url { + match self + .send_cdp_command(ws_url, "Page.navigate", json!({ "url": url })) + .await + { + Ok(_) => log::info!("Successfully navigated to URL: {}", url), + Err(e) => log::error!("Failed to navigate to URL: {e}"), + } + } + } + } + let id = uuid::Uuid::new_v4().to_string(); let instance = WayfernInstance { id: id.clone(), diff --git a/src/components/camoufox-config-dialog.tsx b/src/components/camoufox-config-dialog.tsx index 899722e..b16c032 100644 --- a/src/components/camoufox-config-dialog.tsx +++ b/src/components/camoufox-config-dialog.tsx @@ -10,7 +10,13 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; -import type { BrowserProfile, CamoufoxConfig, CamoufoxOS } from "@/types"; +import { WayfernConfigForm } from "@/components/wayfern-config-form"; +import type { + BrowserProfile, + CamoufoxConfig, + CamoufoxOS, + WayfernConfig, +} from "@/types"; const getCurrentOS = (): CamoufoxOS => { if (typeof navigator === "undefined") return "linux"; @@ -43,7 +49,8 @@ export function CamoufoxConfigDialog({ onSaveWayfern, isRunning = false, }: CamoufoxConfigDialogProps) { - const [config, setConfig] = useState(() => ({ + // Use union type to support both Camoufox and Wayfern configs + const [config, setConfig] = useState(() => ({ geoip: true, os: getCurrentOS(), })); @@ -68,7 +75,10 @@ export function CamoufoxConfigDialog({ } }, [profile, isAntiDetectBrowser]); - const updateConfig = (key: keyof CamoufoxConfig, value: unknown) => { + const updateConfig = ( + key: keyof CamoufoxConfig | keyof WayfernConfig, + value: unknown, + ) => { setConfig((prev) => ({ ...prev, [key]: value })); }; @@ -92,9 +102,9 @@ export function CamoufoxConfigDialog({ setIsSaving(true); try { if (profile.browser === "wayfern" && onSaveWayfern) { - await onSaveWayfern(profile, config); + await onSaveWayfern(profile, config as CamoufoxConfig); } else { - await onSave(profile, config); + await onSave(profile, config as CamoufoxConfig); } onClose(); } catch (error) { @@ -144,15 +154,22 @@ export function CamoufoxConfigDialog({
- + {profile.browser === "wayfern" ? ( + + ) : ( + + )}
diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index 7cf1382..985ee78 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -25,6 +25,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Tabs, TabsContent } from "@/components/ui/tabs"; +import { WayfernConfigForm } from "@/components/wayfern-config-form"; import { useBrowserDownload } from "@/hooks/use-browser-download"; import { useProxyEvents } from "@/hooks/use-proxy-events"; @@ -727,11 +728,10 @@ export function CreateProfileDialog({ )} - ) : selectedBrowser === "camoufox" ? ( diff --git a/src/components/shared-camoufox-config-form.tsx b/src/components/shared-camoufox-config-form.tsx index c4180a0..8c18118 100644 --- a/src/components/shared-camoufox-config-form.tsx +++ b/src/components/shared-camoufox-config-form.tsx @@ -855,12 +855,28 @@ export function SharedCamoufoxConfigForm({
({ + value={(() => { + // Handle fonts being either an array or a JSON string (Wayfern format) + let fontsArray: string[] = []; + if (fingerprintConfig.fonts) { + if (Array.isArray(fingerprintConfig.fonts)) { + fontsArray = fingerprintConfig.fonts; + } else if (typeof fingerprintConfig.fonts === "string") { + try { + const parsed = JSON.parse(fingerprintConfig.fonts); + if (Array.isArray(parsed)) { + fontsArray = parsed; + } + } catch { + // Invalid JSON, ignore + } + } + } + return fontsArray.map((font) => ({ label: font, value: font, - })) || [] - } + })); + })()} onChange={(selected: Option[]) => updateFingerprintConfig( "fonts", diff --git a/src/components/wayfern-config-form.tsx b/src/components/wayfern-config-form.tsx new file mode 100644 index 0000000..80cd255 --- /dev/null +++ b/src/components/wayfern-config-form.tsx @@ -0,0 +1,1091 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import type { + WayfernConfig, + WayfernFingerprintConfig, + WayfernOS, +} from "@/types"; + +interface WayfernConfigFormProps { + config: WayfernConfig; + onConfigChange: (key: keyof WayfernConfig, value: unknown) => void; + className?: string; + isCreating?: boolean; + forceAdvanced?: boolean; + readOnly?: boolean; +} + +const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => { + return config.randomize_fingerprint_on_launch === true; +}; + +const getCurrentOS = (): WayfernOS => { + if (typeof navigator === "undefined") return "linux"; + const platform = navigator.platform.toLowerCase(); + if (platform.includes("win")) return "windows"; + if (platform.includes("mac")) return "macos"; + return "linux"; +}; + +const osLabels: Record = { + windows: "Windows", + macos: "macOS", + linux: "Linux", + android: "Android", + ios: "iOS", +}; + +export function WayfernConfigForm({ + config, + onConfigChange, + className = "", + isCreating = false, + forceAdvanced = false, + readOnly = false, +}: WayfernConfigFormProps) { + const [activeTab, setActiveTab] = useState( + forceAdvanced ? "manual" : "automatic", + ); + const [fingerprintConfig, setFingerprintConfig] = + useState({}); + const [currentOS] = useState(getCurrentOS); + + const selectedOS = config.os || currentOS; + const isOSDifferent = selectedOS !== currentOS; + const isMobileOS = selectedOS === "android" || selectedOS === "ios"; + + useEffect(() => { + if (isCreating && typeof window !== "undefined") { + const screenWidth = window.screen.width; + const screenHeight = window.screen.height; + + if (!config.screen_max_width) { + onConfigChange("screen_max_width", screenWidth); + } + if (!config.screen_max_height) { + onConfigChange("screen_max_height", screenHeight); + } + } + }, [ + isCreating, + config.screen_max_width, + config.screen_max_height, + onConfigChange, + ]); + + useEffect(() => { + if (config.fingerprint) { + try { + const parsed = JSON.parse( + config.fingerprint, + ) as WayfernFingerprintConfig; + setFingerprintConfig(parsed); + } catch (error) { + console.error("Failed to parse fingerprint config:", error); + setFingerprintConfig({}); + } + } else { + setFingerprintConfig({}); + } + }, [config.fingerprint]); + + const updateFingerprintConfig = ( + key: keyof WayfernFingerprintConfig, + value: unknown, + ) => { + const newConfig = { ...fingerprintConfig }; + + if ( + value === undefined || + value === "" || + (Array.isArray(value) && value.length === 0) + ) { + delete newConfig[key]; + } else { + (newConfig as Record)[key] = value; + } + + setFingerprintConfig(newConfig); + + try { + const jsonString = JSON.stringify(newConfig); + onConfigChange("fingerprint", jsonString); + } catch (error) { + console.error("Failed to serialize fingerprint config:", error); + } + }; + + const isAutoLocationEnabled = config.geoip !== false; + + const handleAutoLocationToggle = (enabled: boolean) => { + if (enabled) { + onConfigChange("geoip", true); + } else { + onConfigChange("geoip", false); + } + }; + + const isEditingDisabled = isFingerprintEditingDisabled(config) || readOnly; + + const renderAdvancedForm = () => ( +
+ {/* Operating System Selection */} +
+ + + {isOSDifferent && !isMobileOS && ( + + + Warning: Selecting an OS different from your current system ( + {osLabels[currentOS]}) increases the risk of detection. + + + )} + {isMobileOS && ( + + + Warning: Mobile OS spoofing on desktop has high detection risk. + Websites can detect the mismatch between fingerprint and behavior. + + + )} +
+ + {/* Randomize Fingerprint Option */} +
+
+ + onConfigChange("randomize_fingerprint_on_launch", checked) + } + disabled={readOnly} + /> + +
+

+ When enabled, a new fingerprint will be generated each time the + browser is launched. +

+
+ + {isEditingDisabled ? ( + + + {readOnly + ? "Fingerprint editing is disabled because the profile is currently running." + : "Fingerprint editing is disabled because random fingerprint generation is enabled."} + + + ) : ( + + + Warning: Only edit these parameters if you know what you're doing. + + + )} + +
+ {/* User Agent and Platform */} +
+ +
+
+ + + updateFingerprintConfig( + "userAgent", + e.target.value || undefined, + ) + } + placeholder="Mozilla/5.0..." + /> +
+
+ + + updateFingerprintConfig( + "platform", + e.target.value || undefined, + ) + } + placeholder="e.g., Win32, MacIntel, Linux x86_64" + /> +
+
+ + + updateFingerprintConfig( + "platformVersion", + e.target.value || undefined, + ) + } + placeholder="e.g., 10.0.0" + /> +
+
+ + + updateFingerprintConfig("brand", e.target.value || undefined) + } + placeholder="e.g., Google Chrome" + /> +
+
+ + + updateFingerprintConfig( + "brandVersion", + e.target.value || undefined, + ) + } + placeholder="e.g., 143" + /> +
+
+
+ + {/* Hardware Properties */} +
+ +
+
+ + + updateFingerprintConfig( + "hardwareConcurrency", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 8" + /> +
+
+ + + updateFingerprintConfig( + "maxTouchPoints", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 0" + /> +
+
+ + + updateFingerprintConfig( + "deviceMemory", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 8" + /> +
+
+
+ + {/* Screen Properties */} +
+ +
+
+ + + updateFingerprintConfig( + "screenWidth", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1920" + /> +
+
+ + + updateFingerprintConfig( + "screenHeight", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1080" + /> +
+
+ + + updateFingerprintConfig( + "devicePixelRatio", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., 1.0" + /> +
+
+ + + updateFingerprintConfig( + "screenAvailWidth", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1920" + /> +
+
+ + + updateFingerprintConfig( + "screenAvailHeight", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1040" + /> +
+
+ + + updateFingerprintConfig( + "screenColorDepth", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 24" + /> +
+
+
+ + {/* Window Properties */} +
+ +
+
+ + + updateFingerprintConfig( + "windowOuterWidth", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1920" + /> +
+
+ + + updateFingerprintConfig( + "windowOuterHeight", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1040" + /> +
+
+ + + updateFingerprintConfig( + "windowInnerWidth", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 1920" + /> +
+
+ + + updateFingerprintConfig( + "windowInnerHeight", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 940" + /> +
+
+ + + updateFingerprintConfig( + "screenX", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 0" + /> +
+
+ + + updateFingerprintConfig( + "screenY", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 0" + /> +
+
+
+ + {/* Language & Locale */} +
+ +
+
+ + + updateFingerprintConfig( + "language", + e.target.value || undefined, + ) + } + placeholder="e.g., en-US" + /> +
+
+ + { + if (!e.target.value) { + updateFingerprintConfig("languages", undefined); + return; + } + try { + const parsed = JSON.parse(e.target.value); + if (Array.isArray(parsed)) { + updateFingerprintConfig("languages", parsed); + } + } catch { + // Invalid JSON, keep current value + } + }} + placeholder='["en-US", "en"]' + /> +
+
+ + +
+
+
+ + {/* Timezone and Geolocation */} +
+ +

+ These values override the browser's timezone and geolocation APIs. +

+
+
+ + + updateFingerprintConfig( + "timezone", + e.target.value || undefined, + ) + } + placeholder="e.g., America/New_York" + /> +
+
+ + + updateFingerprintConfig( + "timezoneOffset", + e.target.value ? parseInt(e.target.value, 10) : undefined, + ) + } + placeholder="e.g., 300 for EST (UTC-5)" + /> +
+
+ + + updateFingerprintConfig( + "latitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., 40.7128" + /> +
+
+ + + updateFingerprintConfig( + "longitude", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., -74.0060" + /> +
+
+ + + updateFingerprintConfig( + "accuracy", + e.target.value ? parseFloat(e.target.value) : undefined, + ) + } + placeholder="e.g., 100" + /> +
+
+
+ + {/* WebGL Properties */} +
+ +
+
+ + + updateFingerprintConfig( + "webglVendor", + e.target.value || undefined, + ) + } + placeholder="e.g., Intel" + /> +
+
+ + + updateFingerprintConfig( + "webglRenderer", + e.target.value || undefined, + ) + } + placeholder="e.g., Intel(R) HD Graphics" + /> +
+
+
+ + {/* WebGL Parameters (JSON) */} +
+ +