refactor: separate form for wayfern

This commit is contained in:
zhom
2026-01-11 02:25:12 +04:00
parent cddc4544b0
commit 75eb2c72a9
6 changed files with 1369 additions and 38 deletions
+114 -17
View File
@@ -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::<Vec<_>>())
);
// 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::<Vec<_>>())
.map(|o| o.keys().collect::<Vec<_>>())
);
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(),
+31 -14
View File
@@ -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<CamoufoxConfig>(() => ({
// Use union type to support both Camoufox and Wayfern configs
const [config, setConfig] = useState<CamoufoxConfig | WayfernConfig>(() => ({
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({
<ScrollArea className="flex-1 h-[300px]">
<div className="py-4">
<SharedCamoufoxConfigForm
config={config}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
browserType={
profile.browser === "wayfern" ? "wayfern" : "camoufox"
}
/>
{profile.browser === "wayfern" ? (
<WayfernConfigForm
config={config as WayfernConfig}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
/>
) : (
<SharedCamoufoxConfigForm
config={config as CamoufoxConfig}
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
browserType="camoufox"
/>
)}
</div>
</ScrollArea>
+2 -2
View File
@@ -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({
</div>
)}
<SharedCamoufoxConfigForm
<WayfernConfigForm
config={wayfernConfig}
onConfigChange={updateWayfernConfig}
isCreating
browserType="wayfern"
/>
</div>
) : selectedBrowser === "camoufox" ? (
+20 -4
View File
@@ -855,12 +855,28 @@ export function SharedCamoufoxConfigForm({
<div className="space-y-3">
<Label>Fonts</Label>
<MultipleSelector
value={
fingerprintConfig.fonts?.map((font) => ({
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",
File diff suppressed because it is too large Load Diff
+111 -1
View File
@@ -290,7 +290,7 @@ export interface CamoufoxLaunchResult {
url?: string;
}
export type WayfernOS = "windows" | "macos" | "linux";
export type WayfernOS = "windows" | "macos" | "linux" | "android" | "ios";
export interface WayfernConfig {
proxy?: string;
@@ -308,6 +308,116 @@ export interface WayfernConfig {
os?: WayfernOS; // Operating system for fingerprint generation
}
// Wayfern fingerprint config - matches the C++ FingerprintData structure
export interface WayfernFingerprintConfig {
// User agent and platform
userAgent?: string;
platform?: string;
platformVersion?: string;
brand?: string;
brandVersion?: string;
// Hardware
hardwareConcurrency?: number;
maxTouchPoints?: number;
deviceMemory?: number;
// Screen
screenWidth?: number;
screenHeight?: number;
screenAvailWidth?: number;
screenAvailHeight?: number;
screenColorDepth?: number;
screenPixelDepth?: number;
devicePixelRatio?: number;
// Window
windowOuterWidth?: number;
windowOuterHeight?: number;
windowInnerWidth?: number;
windowInnerHeight?: number;
screenX?: number;
screenY?: number;
// Language
language?: string;
languages?: string[];
// Browser features
doNotTrack?: string;
cookieEnabled?: boolean;
webdriver?: boolean;
pdfViewerEnabled?: boolean;
// WebGL
webglVendor?: string;
webglRenderer?: string;
webglVersion?: string;
webglShadingLanguageVersion?: string;
webglParameters?: string; // JSON string
webgl2Parameters?: string; // JSON string
webglShaderPrecisionFormats?: string; // JSON string
webgl2ShaderPrecisionFormats?: string; // JSON string
// Timezone and geolocation
timezone?: string;
timezoneOffset?: number;
latitude?: number;
longitude?: number;
accuracy?: number;
// Media queries / preferences
prefersReducedMotion?: boolean;
prefersDarkMode?: boolean;
prefersContrast?: string;
prefersReducedData?: boolean;
// Color/HDR
colorGamutSrgb?: boolean;
colorGamutP3?: boolean;
colorGamutRec2020?: boolean;
hdrSupport?: boolean;
// Audio
audioSampleRate?: number;
audioMaxChannelCount?: number;
// Storage
localStorage?: boolean;
sessionStorage?: boolean;
indexedDb?: boolean;
// Canvas
canvasNoiseSeed?: string;
// Fonts, plugins, mime types (JSON strings)
fonts?: string; // JSON array string
plugins?: string; // JSON array string
mimeTypes?: string; // JSON array string
// Battery (optional)
batteryCharging?: boolean;
batteryChargingTime?: number;
batteryDischargingTime?: number;
batteryLevel?: number;
// Voices
voices?: string; // JSON array string
// Vendor info
vendor?: string;
vendorSub?: string;
productSub?: string;
// Network (optional)
connectionEffectiveType?: string;
connectionDownlink?: number;
connectionRtt?: number;
// Performance
performanceMemory?: number;
}
export interface WayfernLaunchResult {
id: string;
processId?: number;