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
138    let screen_width = (identity.screen.logical_width as f64 * identity.screen.density_pixel_ratio as f64) as i64;
139    let screen_height = (identity.screen.logical_height as f64 * identity.screen.density_pixel_ratio as f64) as i64;
140    let device_cmd = SetDeviceMetricsOverride::builder()
141        .width(identity.screen.logical_width as i64)
142        .height(identity.screen.logical_height as i64)
143        .device_scale_factor(identity.screen.density_pixel_ratio as f64)
144        .mobile(is_mobile)
145        .screen_width(screen_width)
146        .screen_height(screen_height)
147        .build()
148        .unwrap();
149    browser
150        .send_command(device_cmd.into())
151        .await
152        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
153
154    // Emulation.setTouchEmulationEnabled
155    if identity.has_touch {
156        let touch_cmd = SetTouchEmulationEnabled::builder()
157            .enabled(true)
158            .max_touch_points(5i64)
159            .build()
160            .unwrap();
161        browser
162            .send_command(touch_cmd.into())
163            .await
164            .map_err(|e| IdentityError::CdpError(e.to_string()))?;
165    }
166
167    // Emulation.setLocaleOverride
168    let locale_cmd = SetLocaleOverride::builder()
169        .locale(&lang0)
170        .build();
171    browser
172        .send_command(locale_cmd.into())
173        .await
174        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
175
176    // Emulation.setTimezoneOverride
177    let tz_cmd = SetTimezoneOverride::builder()
178        .timezone_id(timezone)
179        .build()
180        .unwrap();
181    browser
182        .send_command(tz_cmd.into())
183        .await
184        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
185
186    // Emulation.setHardwareConcurrencyOverride
187    let hc_cmd = SetHardwareConcurrencyOverride::builder()
188        .hardware_concurrency(identity.hardware_concurrency as i64)
189        .build()
190        .unwrap();
191    browser
192        .send_command(hc_cmd.into())
193        .await
194        .map_err(|e| IdentityError::CdpError(e.to_string()))?;
195
196    // Page.addScriptToEvaluateOnNewDocument — stealth bootstrap
197    let stealth_script = script::build_stealth_script(identity);
198    let cmd = AddScriptToEvaluateOnNewDocument {
199        method: AddScriptToEvaluateOnNewDocumentMethod::AddScriptToEvaluateOnNewDocument,
200        params: AddScriptToEvaluateOnNewDocumentParams::new(stealth_script),
201    };
202    browser
203        .send_command(cmd.into())
204        .await
205        .map_err(|e| IdentityError::InjectionError(e.to_string()))?;
206
207    Ok(())
208}