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};
18use rustenium_cdp_definitions::browser_protocol::network::commands::SetUserAgentOverride as NetworkSetUserAgentOverride;
19
20fn grease(major: u16) -> (String, &'static str) {
33 const CHARS: [&str; 11] = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
34 const VERSIONS: [&str; 3] = ["8", "99", "24"];
35 let seed = major as usize;
36 (
37 format!(
38 "Not{}A{}Brand",
39 CHARS[seed % CHARS.len()],
40 CHARS[(seed + 1) % CHARS.len()]
41 ),
42 VERSIONS[seed % VERSIONS.len()],
43 )
44}
45
46fn build_client_hints(identity: &Identity) -> UserAgentMetadata {
48 let major_number = identity.browser_version.first().copied().unwrap_or(0);
49 let major = major_number.to_string();
50 let full_version = identity
51 .browser_version
52 .iter()
53 .map(|v| v.to_string())
54 .collect::<Vec<_>>()
55 .join(".");
56 let is_mobile = identity.device_model.is_some();
57
58 let (brand_name, brand_name_full) = match identity.browser {
59 Browser::Edge => ("Microsoft Edge", "Microsoft Edge"),
60 _ => ("Google Chrome", "Google Chrome"),
61 };
62
63 let (grease_brand, grease_version) = grease(major_number);
64
65 let brands = vec![
70 UserAgentBrandVersion::new(&grease_brand, grease_version),
71 UserAgentBrandVersion::new(brand_name, &major),
72 UserAgentBrandVersion::new("Chromium", &major),
73 ];
74
75 let full_version_list = vec![
76 UserAgentBrandVersion::new(&grease_brand, &format!("{grease_version}.0.0.0")),
77 UserAgentBrandVersion::new(brand_name_full, &full_version),
78 UserAgentBrandVersion::new("Chromium", &full_version),
79 ];
80
81 let platform = match identity.os {
82 Os::Windows => "Windows",
83 Os::Macintosh => "macOS",
84 Os::Linux => "Linux",
85 Os::Android => "Android",
86 Os::Ios => "iOS",
87 };
88
89 let (architecture, bitness) = if is_mobile {
91 (String::new(), String::new())
92 } else {
93 (
94 identity.platform.architecture.clone().unwrap_or_else(|| "x86".into()),
95 identity.platform.bitness.clone().unwrap_or_else(|| "64".into()),
96 )
97 };
98 let model = identity.device_model.clone().unwrap_or_default();
99
100 UserAgentMetadata::builder()
101 .brands(brands)
102 .full_version_lists(full_version_list)
103 .platform(platform)
104 .platform_version(&identity.platform.version)
105 .architecture(&architecture)
106 .model(&model)
107 .mobile(is_mobile)
108 .bitness(bitness)
109 .build()
110 .unwrap()
111}
112
113pub async fn apply_identity(
115 browser: &mut ChromeBrowser,
116 identity: &Identity,
117 timezone: &str,
118) -> Result<(), IdentityError> {
119 let user_agent = ua::build_user_agent(identity)?;
120 let lang0 = identity
121 .language
122 .first()
123 .cloned()
124 .unwrap_or_else(|| "en-US".into());
125 let is_mobile = identity.device_model.is_some();
126
127 let client_hints = if matches!(identity.browser, Browser::Chrome | Browser::Edge)
129 && !matches!(identity.os, Os::Ios)
130 {
131 Some(build_client_hints(identity))
132 } else {
133 None
134 };
135
136 let mut ua_builder = SetUserAgentOverride::builder()
142 .user_agent(&user_agent)
143 .accept_language(&lang0)
144 .platform(identity.platform.navigator_platform.as_str());
145 if let Some(ref metadata) = client_hints {
146 ua_builder = ua_builder.user_agent_metadata(metadata.clone());
147 }
148 let ua_cmd = ua_builder.build().unwrap();
149
150 let full_version = identity
159 .browser_version
160 .iter()
161 .map(|v| v.to_string())
162 .collect::<Vec<_>>()
163 .join(".");
164 let ua_extensions = serde_json::json!({
165 "userAgentMetadata": { "fullVersion": full_version }
166 });
167 browser
168 .send_command_extended(ua_cmd.into(), ua_extensions.clone())
169 .await
170 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
171
172 let mut net_builder = NetworkSetUserAgentOverride::builder()
174 .user_agent(&user_agent)
175 .accept_language(&lang0);
176 if let Some(ref metadata) = client_hints {
177 net_builder = net_builder.user_agent_metadata(metadata.clone());
178 }
179 let net_ua_cmd = net_builder.build().unwrap();
180 browser
183 .send_command_extended(net_ua_cmd.into(), ua_extensions.clone())
184 .await
185 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
186
187 if is_mobile {
189 let screen_width = (identity.screen.logical_width as f64 * identity.screen.density_pixel_ratio as f64) as i64;
190 let screen_height = (identity.screen.logical_height as f64 * identity.screen.density_pixel_ratio as f64) as i64;
191 let device_cmd = SetDeviceMetricsOverride::builder()
192 .width(identity.screen.logical_width as i64)
193 .height(identity.screen.logical_height as i64)
194 .device_scale_factor(identity.screen.density_pixel_ratio as f64)
195 .mobile(true)
196 .screen_width(screen_width)
197 .screen_height(screen_height)
198 .build()
199 .unwrap();
200 browser
201 .send_command(device_cmd.into())
202 .await
203 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
204 }
205
206 if identity.has_touch {
208 let touch_cmd = SetTouchEmulationEnabled::builder()
209 .enabled(true)
210 .max_touch_points(5i64)
211 .build()
212 .unwrap();
213 browser
214 .send_command(touch_cmd.into())
215 .await
216 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
217
218 let configuration = if is_mobile {
222 SetEmitTouchEventsForMouseConfiguration::Mobile
223 } else {
224 SetEmitTouchEventsForMouseConfiguration::Desktop
225 };
226 let emit_touch_cmd = SetEmitTouchEventsForMouse::builder()
227 .enabled(true)
228 .configuration(configuration)
229 .build()
230 .unwrap();
231 browser
232 .send_command(emit_touch_cmd.into())
233 .await
234 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
235 }
236
237 let locale_cmd = SetLocaleOverride::builder()
239 .locale(&lang0)
240 .build();
241 browser
242 .send_command(locale_cmd.into())
243 .await
244 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
245
246 let tz_cmd = SetTimezoneOverride::builder()
248 .timezone_id(timezone)
249 .build()
250 .unwrap();
251 browser
252 .send_command(tz_cmd.into())
253 .await
254 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
255
256 let hc_cmd = SetHardwareConcurrencyOverride::builder()
258 .hardware_concurrency(identity.hardware_concurrency as i64)
259 .build()
260 .unwrap();
261 browser
262 .send_command(hc_cmd.into())
263 .await
264 .map_err(|e| IdentityError::CdpError(e.to_string()))?;
265
266 browser
279 .add_init_script(InitScript {
280 page: Some(script::build_stealth_script(identity)),
281 worker: Some(script::build_worker_script(identity)),
282 })
283 .await;
284 Ok(())
285}
286
287#[cfg(test)]
288mod tests {
289 use super::grease;
290
291 #[test]
295 fn grease_matches_the_browsers_it_was_measured_against() {
296 assert_eq!(grease(151), ("Not=A?Brand".to_string(), "99"));
297 assert_eq!(grease(152), ("Not?A_Brand".to_string(), "24"));
298 }
299
300 #[test]
303 fn the_old_hardcoded_pair_was_never_a_real_release() {
304 let (brand, version) = grease(150);
305 assert_eq!(brand, "Not;A=Brand");
306 assert_ne!(version, "24");
307 }
308}