use crate::error::IdentityError;
use crate::identity::{Browser, Identity, Os};
use crate::script;
use crate::ua;
use rustenium::browsers::chrome::browser::ChromeBrowser;
use rustenium::browsers::cdp_browser::CdpBrowser;
use rustenium::cdp::target_manager::InitScript;
use rustenium_cdp_definitions::browser_protocol::emulation::commands::{
SetDeviceMetricsOverride, SetEmitTouchEventsForMouse, SetEmitTouchEventsForMouseConfiguration,
SetHardwareConcurrencyOverride, SetLocaleOverride, SetTimezoneOverride,
SetTouchEmulationEnabled, SetUserAgentOverride,
};
use rustenium_cdp_definitions::browser_protocol::emulation::types::{
UserAgentBrandVersion, UserAgentMetadata,
};
fn grease(major: u16) -> (String, &'static str) {
const CHARS: [&str; 11] = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
const VERSIONS: [&str; 3] = ["8", "99", "24"];
let seed = major as usize;
(
format!(
"Not{}A{}Brand",
CHARS[seed % CHARS.len()],
CHARS[(seed + 1) % CHARS.len()]
),
VERSIONS[seed % VERSIONS.len()],
)
}
fn build_client_hints(identity: &Identity) -> UserAgentMetadata {
let major_number = identity.browser_version.first().copied().unwrap_or(0);
let major = major_number.to_string();
let full_version = identity
.browser_version
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(".");
let is_mobile = identity.device_model.is_some();
let (brand_name, brand_name_full) = match identity.browser {
Browser::Edge => ("Microsoft Edge", "Microsoft Edge"),
_ => ("Google Chrome", "Google Chrome"),
};
let (grease_brand, grease_version) = grease(major_number);
let brands = vec![
UserAgentBrandVersion::new(&grease_brand, grease_version),
UserAgentBrandVersion::new(brand_name, &major),
UserAgentBrandVersion::new("Chromium", &major),
];
let full_version_list = vec![
UserAgentBrandVersion::new(&grease_brand, &format!("{grease_version}.0.0.0")),
UserAgentBrandVersion::new(brand_name_full, &full_version),
UserAgentBrandVersion::new("Chromium", &full_version),
];
let platform = match identity.os {
Os::Windows => "Windows",
Os::Macintosh => "macOS",
Os::Linux => "Linux",
Os::Android => "Android",
Os::Ios => "iOS",
};
let (architecture, bitness) = if is_mobile {
(String::new(), String::new())
} else {
(
identity.platform.architecture.clone().unwrap_or_else(|| "x86".into()),
identity.platform.bitness.clone().unwrap_or_else(|| "64".into()),
)
};
let model = identity.device_model.clone().unwrap_or_default();
UserAgentMetadata::builder()
.brands(brands)
.full_version_lists(full_version_list)
.platform(platform)
.platform_version(&identity.platform.version)
.architecture(&architecture)
.model(&model)
.mobile(is_mobile)
.bitness(bitness)
.build()
.unwrap()
}
pub async fn apply_identity(
browser: &mut ChromeBrowser,
identity: &Identity,
timezone: &str,
) -> Result<(), IdentityError> {
let user_agent = ua::build_user_agent(identity)?;
let is_mobile = identity.device_model.is_some();
let lang0 = identity.language.first().map(String::as_str).unwrap_or_else(|| {
tracing::warn!("identity has no language; falling back to en-US");
"en-US"
});
let client_hints = if matches!(identity.browser, Browser::Chrome | Browser::Edge)
&& !matches!(identity.os, Os::Ios)
{
Some(build_client_hints(identity))
} else {
None
};
let mut ua_builder = SetUserAgentOverride::builder()
.user_agent(&user_agent)
.accept_language(identity.language.join(","))
.platform(identity.platform.navigator_platform.as_str());
if let Some(ref metadata) = client_hints {
ua_builder = ua_builder.user_agent_metadata(metadata.clone());
}
let ua_cmd = ua_builder.build().unwrap();
let full_version = identity
.browser_version
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(".");
let ua_extensions = serde_json::json!({
"userAgentMetadata": { "fullVersion": full_version }
});
browser
.send_command_extended(ua_cmd.into(), ua_extensions)
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
if is_mobile {
let screen_width = (identity.screen.logical_width as f64 * identity.screen.density_pixel_ratio as f64) as i64;
let screen_height = (identity.screen.logical_height as f64 * identity.screen.density_pixel_ratio as f64) as i64;
let device_cmd = SetDeviceMetricsOverride::builder()
.width(identity.screen.logical_width as i64)
.height(identity.screen.logical_height as i64)
.device_scale_factor(identity.screen.density_pixel_ratio as f64)
.mobile(true)
.screen_width(screen_width)
.screen_height(screen_height)
.build()
.unwrap();
browser
.send_command(device_cmd.into())
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
}
if identity.has_touch {
let touch_cmd = SetTouchEmulationEnabled::builder()
.enabled(true)
.max_touch_points(5i64)
.build()
.unwrap();
browser
.send_command(touch_cmd.into())
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
let configuration = if is_mobile {
SetEmitTouchEventsForMouseConfiguration::Mobile
} else {
SetEmitTouchEventsForMouseConfiguration::Desktop
};
let emit_touch_cmd = SetEmitTouchEventsForMouse::builder()
.enabled(true)
.configuration(configuration)
.build()
.unwrap();
browser
.send_command(emit_touch_cmd.into())
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
}
let locale_cmd = SetLocaleOverride::builder().locale(lang0).build();
browser
.send_command(locale_cmd.into())
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
let tz_cmd = SetTimezoneOverride::builder()
.timezone_id(timezone)
.build()
.unwrap();
browser
.send_command(tz_cmd.into())
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
let hc_cmd = SetHardwareConcurrencyOverride::builder()
.hardware_concurrency(identity.hardware_concurrency as i64)
.build()
.unwrap();
browser
.send_command(hc_cmd.into())
.await
.map_err(|e| IdentityError::CdpError(e.to_string()))?;
browser
.add_init_script(InitScript {
page: Some(script::build_stealth_script(identity)),
worker: Some(script::build_worker_script(identity)),
})
.await;
Ok(())
}
#[cfg(test)]
mod tests {
use super::grease;
#[test]
fn grease_matches_the_browsers_it_was_measured_against() {
assert_eq!(grease(151), ("Not=A?Brand".to_string(), "99"));
assert_eq!(grease(152), ("Not?A_Brand".to_string(), "24"));
}
#[test]
fn the_old_hardcoded_pair_was_never_a_real_release() {
let (brand, version) = grease(150);
assert_eq!(brand, "Not;A=Brand");
assert_ne!(version, "24");
}
}