Skip to main content

rustenium_identity/
cdp.rs

1use crate::error::IdentityError;
2use crate::identity::{Browser, Identity, Os};
3
4use crate::script;
5use crate::ua;
6
7use rustenium::browsers::chrome::browser::ChromeBrowser;
8use rustenium::browsers::cdp_browser::CdpBrowser;
9use rustenium_cdp_definitions::browser_protocol::emulation::commands::{
10    SetDeviceMetricsOverride, SetHardwareConcurrencyOverride, SetLocaleOverride,
11    SetTimezoneOverride, SetTouchEmulationEnabled, SetUserAgentOverride,
12};
13use rustenium_cdp_definitions::browser_protocol::emulation::types::{
14    UserAgentBrandVersion, UserAgentMetadata,
15};
16use rustenium_cdp_definitions::browser_protocol::network::commands::SetUserAgentOverride as NetworkSetUserAgentOverride;
17use rustenium_cdp_definitions::browser_protocol::page::commands::{
18    AddScriptToEvaluateOnNewDocument, AddScriptToEvaluateOnNewDocumentMethod,
19    AddScriptToEvaluateOnNewDocumentParams,
20};
21
22/// Build UserAgentMetadata (Client Hints) for Chrome/Edge identities.
23fn build_client_hints(identity: &Identity) -> UserAgentMetadata {
24    let major = identity.browser_version.first().copied().unwrap_or(0).to_string();
25    let full_version = identity
26        .browser_version
27        .iter()
28        .map(|v| v.to_string())
29        .collect::<Vec<_>>()
30        .join(".");
31    let is_mobile = identity.device_model.is_some();
32
33    let (brand_name, brand_name_full) = match identity.browser {
34        Browser::Edge => ("Microsoft Edge", "Microsoft Edge"),
35        _ => ("Google Chrome", "Google Chrome"),
36    };
37
38    let grease_brand = match identity.os {
39        Os::Android => "Not-A.Brand",
40        _ => "Not;A=Brand",
41    };
42
43    let brands = vec![
44        UserAgentBrandVersion::new("Chromium", &major),
45        UserAgentBrandVersion::new(grease_brand, "24"),
46        UserAgentBrandVersion::new(brand_name, &major),
47    ];
48
49    let full_version_list = vec![
50        UserAgentBrandVersion::new("Chromium", &full_version),
51        UserAgentBrandVersion::new(grease_brand, "24.0.0.0"),
52        UserAgentBrandVersion::new(brand_name_full, &full_version),
53    ];
54
55    let platform = match identity.os {
56        Os::Windows => "Windows",
57        Os::Macintosh => "macOS",
58        Os::Linux => "Linux",
59        Os::Android => "Android",
60        Os::Ios => "iOS",
61    };
62
63    // On mobile, architecture and bitness should be empty
64    let (architecture, bitness) = if is_mobile {
65        (String::new(), String::new())
66    } else {
67        (
68            identity.platform.architecture.clone().unwrap_or_else(|| "x86".into()),
69            identity.platform.bitness.clone().unwrap_or_else(|| "64".into()),
70        )
71    };
72    let model = identity.device_model.clone().unwrap_or_default();
73
74    UserAgentMetadata::builder()
75        .brands(brands)
76        .full_version_lists(full_version_list)
77        .platform(platform)
78        .platform_version(&identity.platform.version)
79        .architecture(&architecture)
80        .model(&model)
81        .mobile(is_mobile)
82        .bitness(bitness)
83        .build()
84        .unwrap()
85}
86
87/// Apply all CDP emulation commands and register the stealth script.
88pub async fn apply_identity(
89    browser: &mut ChromeBrowser,
90    identity: &Identity,
91    timezone: &str,
92) -> Result<(), IdentityError> {
93    let user_agent = ua::build_user_agent(identity)?;
94    let lang0 = identity
95        .language
96        .first()
97        .cloned()
98        .unwrap_or_else(|| "en-US".into());
99    let is_mobile = identity.device_model.is_some();
100
101    // Build client hints for Chrome/Edge (skip iOS — all iOS browsers are WebKit-based)
102    let client_hints = if matches!(identity.browser, Browser::Chrome | Browser::Edge)
103        && !matches!(identity.os, Os::Ios)
104    {
105        Some(build_client_hints(identity))
106    } else {
107        None
108    };
109
110    // Emulation.setUserAgentOverride
111    let mut ua_builder = SetUserAgentOverride::builder()
112        .user_agent(&user_agent)
113        .accept_language(&lang0)
114        .platform(identity.platform.navigator_platform.as_str());
115    if let Some(ref metadata) = client_hints {
116        ua_builder = ua_builder.user_agent_metadata(metadata.clone());
117    }
118    let ua_cmd = ua_builder.build().unwrap();
119    browser
120        .send_command(ua_cmd.into())
121        .await
122        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
123
124    // Network.setUserAgentOverride (belt-and-suspenders, also with client hints)
125    let mut net_builder = NetworkSetUserAgentOverride::builder()
126        .user_agent(&user_agent)
127        .accept_language(&lang0);
128    if let Some(ref metadata) = client_hints {
129        net_builder = net_builder.user_agent_metadata(metadata.clone());
130    }
131    let net_ua_cmd = net_builder.build().unwrap();
132    browser
133        .send_command(net_ua_cmd.into())
134        .await
135        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
136
137    // Emulation.setDeviceMetricsOverride — only spoof display dimensions for mobile
138    if is_mobile {
139        let screen_width = (identity.screen.logical_width as f64 * identity.screen.density_pixel_ratio as f64) as i64;
140        let screen_height = (identity.screen.logical_height as f64 * identity.screen.density_pixel_ratio as f64) as i64;
141        let device_cmd = SetDeviceMetricsOverride::builder()
142            .width(identity.screen.logical_width as i64)
143            .height(identity.screen.logical_height as i64)
144            .device_scale_factor(identity.screen.density_pixel_ratio as f64)
145            .mobile(true)
146            .screen_width(screen_width)
147            .screen_height(screen_height)
148            .build()
149            .unwrap();
150        browser
151            .send_command(device_cmd.into())
152            .await
153            .map_err(|e| IdentityError::CdpError(e.to_string()))?;
154    }
155
156    // Emulation.setTouchEmulationEnabled
157    if identity.has_touch {
158        let touch_cmd = SetTouchEmulationEnabled::builder()
159            .enabled(true)
160            .max_touch_points(5i64)
161            .build()
162            .unwrap();
163        browser
164            .send_command(touch_cmd.into())
165            .await
166            .map_err(|e| IdentityError::CdpError(e.to_string()))?;
167    }
168
169    // Emulation.setLocaleOverride
170    let locale_cmd = SetLocaleOverride::builder()
171        .locale(&lang0)
172        .build();
173    browser
174        .send_command(locale_cmd.into())
175        .await
176        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
177
178    // Emulation.setTimezoneOverride
179    let tz_cmd = SetTimezoneOverride::builder()
180        .timezone_id(timezone)
181        .build()
182        .unwrap();
183    browser
184        .send_command(tz_cmd.into())
185        .await
186        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
187
188    // Emulation.setHardwareConcurrencyOverride
189    let hc_cmd = SetHardwareConcurrencyOverride::builder()
190        .hardware_concurrency(identity.hardware_concurrency as i64)
191        .build()
192        .unwrap();
193    browser
194        .send_command(hc_cmd.into())
195        .await
196        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
197
198    // Page.addScriptToEvaluateOnNewDocument — stealth bootstrap
199    let stealth_script = script::build_stealth_script(identity);
200    let cmd = AddScriptToEvaluateOnNewDocument {
201        method: AddScriptToEvaluateOnNewDocumentMethod::AddScriptToEvaluateOnNewDocument,
202        params: AddScriptToEvaluateOnNewDocumentParams::new(stealth_script),
203    };
204    browser
205        .send_command(cmd.into())
206        .await
207        .map_err(|e| IdentityError::InjectionError(e.to_string()))?;
208
209    Ok(())
210}