Skip to main content

rustenium_identity/
ua.rs

1use crate::identity::{Browser, Identity, Os};
2use crate::error::IdentityError;
3
4/// Build the full version string from the version parts, e.g. [124, 0, 6367, 78] → "124.0.6367.78"
5fn full_version(parts: &[u16]) -> String {
6    parts.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(".")
7}
8
9/// Build the User-Agent string for a given identity.
10pub fn build_user_agent(identity: &Identity) -> Result<String, IdentityError> {
11    let chrome_full = full_version(&identity.browser_version);
12
13    match (&identity.os, &identity.browser) {
14        // §4.1 Chrome on Windows
15        (Os::Windows, Browser::Chrome) => {
16            let nt_version = windows_nt_version(&identity.os_version);
17            Ok(format!(
18                "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
19                nt_version, chrome_full
20            ))
21        }
22
23        // §4.2 Chrome on macOS
24        (Os::Macintosh, Browser::Chrome) => {
25            let osx_underscored = identity.os_version.replace('.', "_");
26            Ok(format!(
27                "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
28                osx_underscored, chrome_full
29            ))
30        }
31
32        // §4.3 Chrome on Android
33        (Os::Android, Browser::Chrome) => {
34            let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
35            Ok(format!(
36                "Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile Safari/537.36",
37                identity.os_version, device, chrome_full
38            ))
39        }
40
41        // §4.3 Edge on Android
42        (Os::Android, Browser::Edge) => {
43            let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
44            Ok(format!(
45                "Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile EdgA/{} Safari/537.36",
46                identity.os_version, device, chrome_full, chrome_full
47            ))
48        }
49
50        // Safari on iOS
51        (Os::Ios, Browser::Safari) => {
52            let os_underscored = identity.os_version.replace('.', "_");
53            let nav_platform = identity.platform.navigator_platform.as_str();
54            Ok(format!(
55                "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Mobile/15E148 Safari/604.1",
56                nav_platform, os_underscored, full_version(&identity.browser_version)
57            ))
58        }
59
60        // Chrome on iOS (WebKit-based, uses CriOS)
61        (Os::Ios, Browser::Chrome) => {
62            let os_underscored = identity.os_version.replace('.', "_");
63            let nav_platform = identity.platform.navigator_platform.as_str();
64            Ok(format!(
65                "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/{} Mobile/15E148 Safari/604.1",
66                nav_platform, os_underscored, chrome_full
67            ))
68        }
69
70        // Edge on iOS (WebKit-based, uses EdgiOS)
71        (Os::Ios, Browser::Edge) => {
72            let os_underscored = identity.os_version.replace('.', "_");
73            let nav_platform = identity.platform.navigator_platform.as_str();
74            Ok(format!(
75                "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} EdgiOS/{} Mobile/15E148 Safari/604.1",
76                nav_platform, os_underscored, full_version(&identity.browser_version), chrome_full
77            ))
78        }
79
80        // Safari on macOS
81        (Os::Macintosh, Browser::Safari) => {
82            let osx_underscored = identity.os_version.replace('.', "_");
83            Ok(format!(
84                "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Safari/605.1.15",
85                osx_underscored, full_version(&identity.browser_version)
86            ))
87        }
88
89        // Chrome on Linux
90        (Os::Linux, Browser::Chrome) => {
91            Ok(format!(
92                "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
93                chrome_full
94            ))
95        }
96
97        // Edge on Windows
98        (Os::Windows, Browser::Edge) => {
99            let nt_version = windows_nt_version(&identity.os_version);
100            Ok(format!(
101                "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
102                nt_version, chrome_full, chrome_full
103            ))
104        }
105
106        // Edge on macOS
107        (Os::Macintosh, Browser::Edge) => {
108            let osx_underscored = identity.os_version.replace('.', "_");
109            Ok(format!(
110                "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
111                osx_underscored, chrome_full, chrome_full
112            ))
113        }
114
115        (os, browser) => Err(IdentityError::UaError(format!(
116            "unsupported OS/browser combination: {:?}/{:?}",
117            os, browser
118        ))),
119    }
120}
121
122/// Map common Windows version strings to NT version numbers.
123fn windows_nt_version(os_version: &str) -> &str {
124    match os_version {
125        "11" => "10.0",
126        "10" => "10.0",
127        "8.1" => "6.3",
128        "8" => "6.2",
129        "7" => "6.1",
130        _ => "10.0",
131    }
132}