use crate::stealth::gpu::GpuProfile;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DeviceClass {
#[default]
Desktop,
MobileAndroid,
MobileIOS,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaDeviceInfo {
pub device_id: String,
pub kind: String,
pub label: String,
pub group_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StealthProfile {
pub user_agent: String,
pub browser_name: String,
pub browser_version: String,
pub os_name: String,
pub os_version: String,
pub platform: String,
pub vendor: String,
pub vendor_sub: String,
pub product_sub: String,
pub app_version: String,
pub screen_width: u32,
pub screen_height: u32,
pub screen_avail_width: u32,
pub screen_avail_height: u32,
pub screen_avail_top: u32,
pub screen_color_depth: u32,
pub device_pixel_ratio: f64,
pub cpu_cores: u8,
pub device_memory: u8,
pub max_touch_points: u8,
pub webgl_vendor: String,
pub webgl_renderer: String,
#[serde(default = "default_gpu_profile")]
pub gpu_profile: GpuProfile,
pub language: String,
pub languages: Vec<String>,
pub timezone: String,
#[serde(default = "default_cpu_architecture")]
pub cpu_architecture: String, #[serde(default = "default_cpu_bitness")]
pub cpu_bitness: String, #[serde(default)]
pub platform_version: String,
#[serde(default)]
pub ua_model: String,
#[serde(default)]
pub ua_wow64: bool,
#[serde(default)]
pub device_class: DeviceClass,
pub tls_impersonate: String,
pub connection_effective_type: String,
pub connection_rtt: u32,
pub connection_downlink: f64,
pub pdf_viewer_enabled: bool,
pub plugins_count: u32,
pub mime_types_count: u32,
pub canvas_seed: u64,
pub audio_seed: u64,
#[serde(default = "default_audio_sample_rate")]
pub audio_sample_rate: u32,
#[serde(default)]
pub has_platform_authenticator: bool,
#[serde(default = "default_true")]
pub conditional_mediation: bool,
#[serde(default)]
pub allow_http3: bool,
pub prefers_color_scheme: String,
pub pointer_type: String,
pub hover_capability: String,
#[serde(default = "default_color_gamut")]
pub color_gamut: String,
pub inner_width: u32,
pub inner_height: u32,
pub outer_width: u32,
pub outer_height: u32,
#[serde(default)]
pub proxy: Option<String>,
#[serde(default)]
pub media_devices: Vec<MediaDeviceInfo>,
#[serde(default = "default_true")]
pub enforce_csp: bool,
}
fn default_color_gamut() -> String {
"srgb".to_string()
}
fn default_true() -> bool {
true
}
fn default_gpu_profile() -> GpuProfile {
crate::stealth::gpu::nvidia_rtx_3060_windows()
}
fn default_cpu_architecture() -> String {
"x86".into()
}
fn default_cpu_bitness() -> String {
"64".into()
}
fn default_audio_sample_rate() -> u32 {
44100
}
impl StealthProfile {
pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = Vec::new();
let ua_major: String = self
.browser_version
.split('.')
.next()
.unwrap_or("")
.to_string();
let chrome_form = format!("{}.0.0.0", ua_major);
let firefox_form = format!("{}.0", ua_major);
let ua_ok =
self.user_agent.contains(&chrome_form) || self.user_agent.contains(&firefox_form);
if !ua_ok {
errors.push(format!(
"UA '{}' doesn't contain reduced major version '{}' or '{}'",
self.user_agent, chrome_form, firefox_form
));
}
match self.os_name.as_str() {
"Windows" if self.platform != "Win32" => {
errors.push(format!("Windows OS but platform is '{}'", self.platform));
}
"macOS" if self.platform != "MacIntel" => {
errors.push(format!("macOS but platform is '{}'", self.platform));
}
"Linux" if !self.platform.starts_with("Linux") => {
errors.push(format!("Linux OS but platform is '{}'", self.platform));
}
_ => {}
}
if self.max_touch_points > 0 && self.screen_width > 1024 && self.pointer_type == "fine" {
errors.push("Touch points > 0 but desktop pointer type".into());
}
if self.webgl_renderer.contains("NVIDIA") && !self.webgl_vendor.contains("NVIDIA") {
errors.push("WebGL renderer is NVIDIA but vendor doesn't match".into());
}
if self.webgl_renderer.contains("Intel") && !self.webgl_vendor.contains("Intel") {
errors.push("WebGL renderer is Intel but vendor doesn't match".into());
}
if self.webgl_renderer.contains("Apple") && !self.webgl_vendor.contains("Apple") {
errors.push("WebGL renderer is Apple but vendor doesn't match".into());
}
if self.webgl_renderer.contains("Apple") && self.os_name != "macOS" {
errors.push("Apple GPU on non-macOS".into());
}
if self.screen_width == 0 || self.screen_height == 0 {
errors.push("Screen dimensions cannot be zero".into());
}
if self.inner_width > self.screen_width {
errors.push("inner_width > screen_width".into());
}
if self.outer_width < self.inner_width {
errors.push("outer_width < inner_width".into());
}
if self.cpu_cores == 0 || self.cpu_cores > 128 {
errors.push(format!("Unrealistic cpu_cores: {}", self.cpu_cores));
}
if self.device_memory == 0 || self.device_memory > 64 {
errors.push(format!("Unrealistic device_memory: {}", self.device_memory));
}
if !self.languages.contains(&self.language) {
errors.push(format!(
"language '{}' not in languages {:?}",
self.language, self.languages
));
}
if !matches!(self.cpu_architecture.as_str(), "x86" | "arm") {
errors.push(format!(
"cpu_architecture must be 'x86' or 'arm' (got '{}')",
self.cpu_architecture
));
}
if !matches!(self.cpu_bitness.as_str(), "64" | "32") {
errors.push(format!(
"cpu_bitness must be '64' or '32' (got '{}')",
self.cpu_bitness
));
}
if self.ua_wow64 && (self.os_name != "Windows" || self.cpu_bitness != "32") {
errors.push(format!(
"ua_wow64=true requires os_name=Windows and cpu_bitness=32 (got {} / {})",
self.os_name, self.cpu_bitness
));
}
if self.os_name == "Linux" && !self.platform_version.is_empty() {
errors.push(format!(
"Chrome on Linux must report empty platform_version (got '{}')",
self.platform_version
));
}
if self.cpu_architecture == "arm"
&& !matches!(self.os_name.as_str(), "macOS" | "Android" | "ChromeOS")
{
errors.push(format!(
"cpu_architecture=arm only on macOS/Android/ChromeOS (got '{}')",
self.os_name
));
}
if !self.ua_model.is_empty() && self.max_touch_points == 0 {
errors.push(format!(
"ua_model='{}' on a desktop (max_touch_points=0) profile",
self.ua_model
));
}
if !matches!(self.audio_sample_rate, 44100 | 48000 | 96000 | 192000) {
errors.push(format!(
"audio_sample_rate must be one of {{44100, 48000, 96000, 192000}} (got {})",
self.audio_sample_rate
));
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}