rustenium_identity/
cdp.rs1use 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::target_manager::InitScript;
10use rustenium_cdp_definitions::browser_protocol::emulation::commands::{
11 SetDeviceMetricsOverride, SetEmitTouchEventsForMouse, SetEmitTouchEventsForMouseConfiguration,
12 SetHardwareConcurrencyOverride, SetLocaleOverride, SetTimezoneOverride,
13 SetTouchEmulationEnabled, SetUserAgentOverride,
14};
15use rustenium_cdp_definitions::browser_protocol::emulation::types::{
16 UserAgentBrandVersion, UserAgentMetadata,
17};
18
19fn grease(major: u16) -> (String, &'static str) {
32 const CHARS: [&str; 11] = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
33 const VERSIONS: [&str; 3] = ["8", "99", "24"];
34 let seed = major as usize;
35 (
36 format!(
37 "Not{}A{}Brand",
38 CHARS[seed % CHARS.len()],
39 CHARS[(seed + 1) % CHARS.len()]
40 ),
41 VERSIONS[seed % VERSIONS.len()],
42 )
43}
44
45fn build_client_hints(identity: &Identity) -> UserAgentMetadata {
47 let major_number = identity.browser_version.first().copied().unwrap_or(0);
48 let major = major_number.to_string();
49 let full_version = identity
50 .browser_version
51 .iter()
52 .map(|v| v.to_string())
53 .collect::<Vec<_>>()
54 .join(".");
55 let is_mobile = identity.device_model.is_some();
56
57 let (brand_name, brand_name_full) = match identity.browser {
58 Browser::Edge => ("Microsoft Edge", "Microsoft Edge"),
59 _ => ("Google Chrome", "Google Chrome"),
60 };
61
62 let (grease_brand, grease_version) = grease(major_number);
63
64 let brands = vec![
69 UserAgentBrandVersion::new(&grease_brand, grease_version),
70 UserAgentBrandVersion::new(brand_name, &major),
71 UserAgentBrandVersion::new("Chromium", &major),
72 ];
73
74 let full_version_list = vec![
75 UserAgentBrandVersion::new(&grease_brand, &format!("{grease_version}.0.0.0")),
76 UserAgentBrandVersion::new(brand_name_full, &full_version),
77 UserAgentBrandVersion::new("Chromium", &full_version),
78 ];
79
80 let platform = match identity.os {
81 Os::Windows => "Windows",
82 Os::Macintosh => "macOS",
83 Os::Linux => "Linux",
84 Os::Android => "Android",
85 Os::Ios => "iOS",
86 };
87
88 let (architecture, bitness) = if is_mobile {
90 (String::new(), String::new())
91 } else {
92 (
93 identity.platform.architecture.clone().unwrap_or_else(|| "x86".into()),
94 identity.platform.bitness.clone().unwrap_or_else(|| "64".into()),
95 )
96 };
97 let model = identity.device_model.clone().unwrap_or_default();
98
99 UserAgentMetadata::builder()
100 .brands(brands)
101 .full_version_lists(full_version_list)
102 .platform(platform)
103 .platform_version(&identity.platform.version)
104 .architecture(&architecture)
105 .model(&model)
106 .mobile(is_mobile)
107 .bitness(bitness)
108 .build()
109 .unwrap()
110}
111
112pub async fn apply_identity(
114 browser: &mut ChromeBrowser,
115 identity: &Identity,
116 timezone: &str,
117) -> Result<(), IdentityError> {
118 let user_agent = ua::build_user_agent(identity)?;
119 let is_mobile = identity.device_model.is_some();
120
121 let lang0 = identity.language.first().map(String::as_str).unwrap_or_else(|| {
124 tracing::warn!("identity has no language; falling back to en-US");
125 "en-US"
126 });
127
128 let client_hints = if matches!(identity.browser, Browser::Chrome | Browser::Edge)
130 && !matches!(identity.os, Os::Ios)
131 {
132 Some(build_client_hints(identity))
133 } else {
134 None
135 };
136
137 let mut ua_builder = SetUserAgentOverride::builder()
148 .user_agent(&user_agent)
149 .accept_language(identity.language.join(","))
150 .platform(identity.platform.navigator_platform.as_str());
151 if let Some(ref metadata) = client_hints {
152 ua_builder = ua_builder.user_agent_metadata(metadata.clone());
153 }
154 let ua_cmd = ua_builder.build().unwrap();
155
156 let full_version = identity
165 .browser_version
166 .iter()
167 .map(|v| v.to_string())
168 .collect::<Vec<_>>()
169 .join(".");
170 let ua_extensions = serde_json::json!({
171 "userAgentMetadata": { "fullVersion": full_version }
172 });
173 browser
174 .send_command_extended(ua_cmd.into(), ua_extensions)
175 .await
176 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
177
178 if is_mobile {
180 let screen_width = (identity.screen.logical_width as f64 * identity.screen.density_pixel_ratio as f64) as i64;
181 let screen_height = (identity.screen.logical_height as f64 * identity.screen.density_pixel_ratio as f64) as i64;
182 let device_cmd = SetDeviceMetricsOverride::builder()
183 .width(identity.screen.logical_width as i64)
184 .height(identity.screen.logical_height as i64)
185 .device_scale_factor(identity.screen.density_pixel_ratio as f64)
186 .mobile(true)
187 .screen_width(screen_width)
188 .screen_height(screen_height)
189 .build()
190 .unwrap();
191 browser
192 .send_command(device_cmd.into())
193 .await
194 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
195 }
196
197 if identity.has_touch {
199 let touch_cmd = SetTouchEmulationEnabled::builder()
200 .enabled(true)
201 .max_touch_points(5i64)
202 .build()
203 .unwrap();
204 browser
205 .send_command(touch_cmd.into())
206 .await
207 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
208
209 let configuration = if is_mobile {
213 SetEmitTouchEventsForMouseConfiguration::Mobile
214 } else {
215 SetEmitTouchEventsForMouseConfiguration::Desktop
216 };
217 let emit_touch_cmd = SetEmitTouchEventsForMouse::builder()
218 .enabled(true)
219 .configuration(configuration)
220 .build()
221 .unwrap();
222 browser
223 .send_command(emit_touch_cmd.into())
224 .await
225 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
226 }
227
228 let locale_cmd = SetLocaleOverride::builder().locale(lang0).build();
245 browser
246 .send_command(locale_cmd.into())
247 .await
248 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
249
250 let tz_cmd = SetTimezoneOverride::builder()
252 .timezone_id(timezone)
253 .build()
254 .unwrap();
255 browser
256 .send_command(tz_cmd.into())
257 .await
258 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
259
260 let hc_cmd = SetHardwareConcurrencyOverride::builder()
262 .hardware_concurrency(identity.hardware_concurrency as i64)
263 .build()
264 .unwrap();
265 browser
266 .send_command(hc_cmd.into())
267 .await
268 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
269
270 browser
283 .add_init_script(InitScript {
284 page: Some(script::build_stealth_script(identity)),
285 worker: Some(script::build_worker_script(identity)),
286 })
287 .await;
288 Ok(())
289}
290
291#[cfg(test)]
292mod tests {
293 use super::grease;
294
295 #[test]
299 fn grease_matches_the_browsers_it_was_measured_against() {
300 assert_eq!(grease(151), ("Not=A?Brand".to_string(), "99"));
301 assert_eq!(grease(152), ("Not?A_Brand".to_string(), "24"));
302 }
303
304 #[test]
307 fn the_old_hardcoded_pair_was_never_a_real_release() {
308 let (brand, version) = grease(150);
309 assert_eq!(brand, "Not;A=Brand");
310 assert_ne!(version, "24");
311 }
312}