Skip to main content

rama_ua/ua/
info.rs

1use super::parse_http_user_agent_header;
2use rama_core::error::BoxErrorExt as _;
3use rama_core::error::{BoxError, ErrorExt};
4use rama_core::extensions::Extension;
5use rama_utils::{macros::match_ignore_ascii_case_str, str::arcstr::ArcStr};
6use std::{convert::Infallible, fmt, str::FromStr};
7
8/// User Agent (UA) information.
9///
10/// See [the module level documentation](crate) for more information.
11#[derive(Debug, Clone, Extension)]
12#[extension(tags(ua))]
13pub struct UserAgent {
14    pub(super) header: ArcStr,
15    pub(super) data: UserAgentData,
16    pub(super) http_agent_overwrite: Option<HttpAgent>,
17    pub(super) tls_agent_overwrite: Option<TlsAgent>,
18}
19
20impl fmt::Display for UserAgent {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        write!(f, "{}", self.header)
23    }
24}
25
26/// internal representation of the [`UserAgent`]
27#[derive(Debug, Clone)]
28pub(super) enum UserAgentData {
29    Standard {
30        info: UserAgentInfo,
31        platform_like: Option<PlatformLike>,
32    },
33    Platform(PlatformKind),
34    Device(DeviceKind),
35    Unknown,
36}
37
38#[derive(Debug, Clone)]
39pub(super) enum PlatformLike {
40    Platform(PlatformKind),
41    Device(DeviceKind),
42}
43
44impl PlatformLike {
45    pub(super) fn device(&self) -> DeviceKind {
46        match self {
47            Self::Platform(platform_kind) => platform_kind.device(),
48            Self::Device(device_kind) => *device_kind,
49        }
50    }
51}
52
53/// Information about the [`UserAgent`]
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct UserAgentInfo {
56    /// The kind of [`UserAgent`]
57    pub kind: UserAgentKind,
58    /// The version of the [`UserAgent`]
59    pub version: Option<usize>,
60}
61
62impl UserAgent {
63    /// Create a new [`UserAgent`] from a `User-Agent` (header) value.
64    pub fn new(header: impl Into<ArcStr>) -> Self {
65        parse_http_user_agent_header(header.into())
66    }
67
68    rama_utils::macros::generate_set_and_with! {
69        /// Overwrite the [`HttpAgent`] advertised by the [`UserAgent`].
70        pub fn http_agent(mut self, http_agent: HttpAgent) -> Self {
71            self.http_agent_overwrite = Some(http_agent);
72            self
73        }
74    }
75
76    rama_utils::macros::generate_set_and_with! {
77        /// Overwrite the [`TlsAgent`] advertised by the [`UserAgent`].
78        pub fn tls_agent(mut self, tls_agent: TlsAgent) -> Self {
79            self.tls_agent_overwrite = Some(tls_agent);
80            self
81        }
82    }
83
84    /// returns the `User-Agent` (header) value used by the [`UserAgent`].
85    #[must_use]
86    pub fn header_str(&self) -> &str {
87        &self.header
88    }
89
90    /// returns the device kind of the [`UserAgent`].
91    #[must_use]
92    pub fn device(&self) -> Option<DeviceKind> {
93        match &self.data {
94            UserAgentData::Standard { platform_like, .. } => {
95                platform_like.as_ref().map(|p| p.device())
96            }
97            UserAgentData::Platform(platform) => Some(platform.device()),
98            UserAgentData::Device(kind) => Some(*kind),
99            UserAgentData::Unknown => None,
100        }
101    }
102
103    /// returns the [`UserAgent`] information, containing
104    /// the [`UserAgentKind`] and version if known.
105    #[must_use]
106    pub fn info(&self) -> Option<UserAgentInfo> {
107        if let UserAgentData::Standard { info, .. } = &self.data {
108            Some(info.clone())
109        } else {
110            None
111        }
112    }
113
114    /// returns the [`UserAgentKind`] used by the [`UserAgent`], if known.
115    #[must_use]
116    pub fn ua_kind(&self) -> Option<UserAgentKind> {
117        match self.http_agent_overwrite {
118            Some(HttpAgent::Chromium) => Some(UserAgentKind::Chromium),
119            Some(HttpAgent::Safari) => Some(UserAgentKind::Safari),
120            Some(HttpAgent::Firefox) => Some(UserAgentKind::Firefox),
121            Some(HttpAgent::Preserve) => None,
122            None => match &self.data {
123                UserAgentData::Standard {
124                    info: UserAgentInfo { kind, .. },
125                    ..
126                } => Some(*kind),
127                UserAgentData::Device(_) | UserAgentData::Platform(_) | UserAgentData::Unknown => {
128                    None
129                }
130            },
131        }
132    }
133
134    /// returns the version of the [`UserAgent`], if known.
135    #[must_use]
136    pub fn ua_version(&self) -> Option<usize> {
137        match &self.data {
138            UserAgentData::Standard { info, .. } => info.version,
139            UserAgentData::Device(_) | UserAgentData::Platform(_) | UserAgentData::Unknown => None,
140        }
141    }
142
143    /// returns the [`PlatformKind`] used by the [`UserAgent`], if known.
144    ///
145    /// This is the platform the [`UserAgent`] is running on.
146    #[must_use]
147    pub fn platform(&self) -> Option<PlatformKind> {
148        match &self.data {
149            UserAgentData::Standard { platform_like, .. } => match platform_like {
150                Some(PlatformLike::Platform(platform)) => Some(*platform),
151                None | Some(PlatformLike::Device(_)) => None,
152            },
153            UserAgentData::Platform(platform) => Some(*platform),
154            UserAgentData::Device(_) | UserAgentData::Unknown => None,
155        }
156    }
157
158    /// returns the [`HttpAgent`] used by the [`UserAgent`].
159    ///
160    /// [`UserAgent`]: super::UserAgent
161    #[must_use]
162    pub fn http_agent(&self) -> Option<HttpAgent> {
163        match self.http_agent_overwrite {
164            Some(agent) => Some(agent),
165            None => match &self.data {
166                UserAgentData::Standard { info, .. } => Some(match info.kind {
167                    UserAgentKind::Chromium => HttpAgent::Chromium,
168                    UserAgentKind::Firefox => HttpAgent::Firefox,
169                    UserAgentKind::Safari => HttpAgent::Safari,
170                }),
171                UserAgentData::Platform(_) | UserAgentData::Device(_) | UserAgentData::Unknown => {
172                    None
173                }
174            },
175        }
176    }
177
178    /// returns the [`TlsAgent`] used by the [`UserAgent`].
179    ///
180    /// [`UserAgent`]: super::UserAgent
181    #[must_use]
182    pub fn tls_agent(&self) -> Option<TlsAgent> {
183        match self.tls_agent_overwrite {
184            Some(agent) => Some(agent),
185            None => match &self.data {
186                UserAgentData::Standard { info, .. } => Some(match info.kind {
187                    UserAgentKind::Chromium => TlsAgent::Boringssl,
188                    UserAgentKind::Firefox => TlsAgent::Nss,
189                    UserAgentKind::Safari => TlsAgent::Rustls,
190                }),
191                UserAgentData::Device(_) | UserAgentData::Platform(_) | UserAgentData::Unknown => {
192                    None
193                }
194            },
195        }
196    }
197}
198
199impl FromStr for UserAgent {
200    type Err = Infallible;
201
202    fn from_str(s: &str) -> Result<Self, Self::Err> {
203        Ok(Self::new(s))
204    }
205}
206
207/// The kind of [`UserAgent`]
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
209pub enum UserAgentKind {
210    /// Chromium Browser
211    Chromium,
212    /// Firefox Browser
213    Firefox,
214    /// Safari Browser
215    Safari,
216}
217
218impl UserAgentKind {
219    #[must_use]
220    pub fn as_str(&self) -> &'static str {
221        match self {
222            Self::Chromium => "Chromium",
223            Self::Firefox => "Firefox",
224            Self::Safari => "Safari",
225        }
226    }
227}
228
229impl fmt::Display for UserAgentKind {
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "{}", self.as_str())
232    }
233}
234
235impl FromStr for UserAgentKind {
236    type Err = BoxError;
237
238    fn from_str(s: &str) -> Result<Self, Self::Err> {
239        match_ignore_ascii_case_str! {
240            match (s) {
241                "chromium" => Ok(Self::Chromium),
242                "firefox" => Ok(Self::Firefox),
243                "safari" => Ok(Self::Safari),
244                _ => Err(BoxError::from_static_str("invalid user agent kind").context_str_field("str", s)),
245            }
246        }
247    }
248}
249
250use rama_utils::macros::serde_str::impl_serde_str;
251
252impl_serde_str!(as_str UserAgentKind);
253
254/// Device on which the [`UserAgent`] operates.
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
256pub enum DeviceKind {
257    /// Personal Computers
258    Desktop,
259    /// Phones, Tablets and other mobile devices
260    Mobile,
261}
262
263impl DeviceKind {
264    #[must_use]
265    pub fn as_str(&self) -> &'static str {
266        match self {
267            Self::Desktop => "Desktop",
268            Self::Mobile => "Mobile",
269        }
270    }
271}
272
273impl FromStr for DeviceKind {
274    type Err = BoxError;
275
276    fn from_str(s: &str) -> Result<Self, Self::Err> {
277        match_ignore_ascii_case_str! {
278            match (s) {
279                "desktop" => Ok(Self::Desktop),
280                "mobile" => Ok(Self::Mobile),
281                _ => Err(BoxError::from_static_str("invalid device").context_str_field("str", s)),
282            }
283        }
284    }
285}
286
287impl_serde_str!(as_str DeviceKind);
288
289impl fmt::Display for DeviceKind {
290    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291        write!(f, "{}", self.as_str())
292    }
293}
294
295/// Platform within the [`UserAgent`] operates.
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub enum PlatformKind {
298    /// Windows Platform ([`Desktop`](DeviceKind::Desktop))
299    Windows,
300    /// MacOS Platform ([`Desktop`](DeviceKind::Desktop))
301    MacOS,
302    /// Linux Platform ([`Desktop`](DeviceKind::Desktop))
303    Linux,
304    /// Android Platform ([`Mobile`](DeviceKind::Mobile))
305    Android,
306    /// iOS Platform ([`Mobile`](DeviceKind::Mobile))
307    IOS,
308}
309
310impl PlatformKind {
311    #[must_use]
312    pub fn as_str(&self) -> &'static str {
313        match self {
314            Self::Windows => "Windows",
315            Self::MacOS => "MacOS",
316            Self::Linux => "Linux",
317            Self::Android => "Android",
318            Self::IOS => "iOS",
319        }
320    }
321
322    #[must_use]
323    pub fn device(&self) -> DeviceKind {
324        match self {
325            Self::Windows | Self::MacOS | Self::Linux => DeviceKind::Desktop,
326            Self::Android | Self::IOS => DeviceKind::Mobile,
327        }
328    }
329}
330
331impl FromStr for PlatformKind {
332    type Err = BoxError;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        match_ignore_ascii_case_str! {
336            match (s) {
337                "windows" => Ok(Self::Windows),
338                "macos" => Ok(Self::MacOS),
339                "linux" => Ok(Self::Linux),
340                "android" => Ok(Self::Android),
341                "ios" => Ok(Self::IOS),
342                _ => Err(BoxError::from_static_str("invalid platform").context_str_field("str", s)),
343            }
344        }
345    }
346}
347
348impl_serde_str!(as_str PlatformKind);
349
350impl fmt::Display for PlatformKind {
351    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352        write!(f, "{}", self.as_str())
353    }
354}
355
356/// Http implementation used by the [`UserAgent`]
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
358pub enum HttpAgent {
359    /// Chromium based browsers share the same http implementation
360    Chromium,
361    /// Firefox has its own http implementation
362    Firefox,
363    /// Safari also has its own http implementation
364    Safari,
365    /// Preserve the incoming Http Agent as much as possible.
366    ///
367    /// For emulators this means that emulators will aim to have a
368    /// hands-off approach to the incoming http request.
369    Preserve,
370}
371
372impl HttpAgent {
373    #[must_use]
374    pub fn as_str(&self) -> &'static str {
375        match self {
376            Self::Chromium => "Chromium",
377            Self::Firefox => "Firefox",
378            Self::Safari => "Safari",
379            Self::Preserve => "Preserve",
380        }
381    }
382}
383
384impl fmt::Display for HttpAgent {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        write!(f, "{}", self.as_str())
387    }
388}
389
390impl_serde_str!(as_str HttpAgent);
391
392impl FromStr for HttpAgent {
393    type Err = BoxError;
394
395    fn from_str(s: &str) -> Result<Self, Self::Err> {
396        match_ignore_ascii_case_str! {
397            match (s) {
398                "chrome" | "chromium" => Ok(Self::Chromium),
399                "Firefox" => Ok(Self::Firefox),
400                "Safari" => Ok(Self::Safari),
401                "preserve" => Ok(Self::Preserve),
402                _ => Err(BoxError::from_static_str("invalid http agent").context_str_field("str", s)),
403            }
404        }
405    }
406}
407
408/// Tls implementation used by the [`UserAgent`]
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
410pub enum TlsAgent {
411    /// Rustls is used as a fallback for all user agents,
412    /// that are not chromium based.
413    Rustls,
414    /// Boringssl is used for Chromium based user agents.
415    Boringssl,
416    /// NSS is used for Firefox
417    Nss,
418    /// Preserve the incoming TlsAgent as much as possible.
419    ///
420    /// For this Tls this means that emulators can try to
421    /// preserve details of the incoming Tls connection
422    /// such as the (Tls) Client Hello.
423    Preserve,
424}
425
426impl TlsAgent {
427    #[must_use]
428    pub fn as_str(&self) -> &'static str {
429        match self {
430            Self::Rustls => "Rustls",
431            Self::Boringssl => "Boringssl",
432            Self::Nss => "NSS",
433            Self::Preserve => "Preserve",
434        }
435    }
436}
437
438impl fmt::Display for TlsAgent {
439    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
440        write!(f, "{}", self.as_str())
441    }
442}
443
444impl_serde_str!(as_str TlsAgent);
445
446impl FromStr for TlsAgent {
447    type Err = BoxError;
448
449    fn from_str(s: &str) -> Result<Self, Self::Err> {
450        match_ignore_ascii_case_str! {
451            match (s) {
452                "rustls" => Ok(Self::Rustls),
453                "boring" | "boringssl" => Ok(Self::Boringssl),
454                "nss" => Ok(Self::Nss),
455                "preserve" => Ok(Self::Preserve),
456                _ => Err(BoxError::from_static_str("invalid tls agent").context_str_field("str", s)),
457            }
458        }
459    }
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_user_agent_new() {
468        let ua = UserAgent::new("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36".to_owned());
469        assert_eq!(
470            ua.header_str(),
471            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
472        );
473        assert_eq!(
474            ua.info(),
475            Some(UserAgentInfo {
476                kind: UserAgentKind::Chromium,
477                version: Some(124)
478            })
479        );
480        assert_eq!(ua.platform(), Some(PlatformKind::MacOS));
481        assert_eq!(ua.device(), Some(DeviceKind::Desktop));
482        assert_eq!(ua.http_agent(), Some(HttpAgent::Chromium));
483        assert_eq!(ua.tls_agent(), Some(TlsAgent::Boringssl));
484    }
485
486    #[test]
487    fn test_user_agent_parse() {
488        let ua: UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36".parse().unwrap();
489        assert_eq!(
490            ua.header_str(),
491            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
492        );
493        assert_eq!(
494            ua.info(),
495            Some(UserAgentInfo {
496                kind: UserAgentKind::Chromium,
497                version: Some(124)
498            })
499        );
500        assert_eq!(ua.platform(), Some(PlatformKind::MacOS));
501        assert_eq!(ua.device(), Some(DeviceKind::Desktop));
502        assert_eq!(ua.http_agent(), Some(HttpAgent::Chromium));
503        assert_eq!(ua.tls_agent(), Some(TlsAgent::Boringssl));
504    }
505
506    #[test]
507    fn test_user_agent_display() {
508        let ua: UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36".parse().unwrap();
509        assert_eq!(
510            ua.to_string(),
511            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
512        );
513    }
514
515    #[test]
516    fn test_tls_agent_parse() {
517        assert_eq!("rustls".parse::<TlsAgent>().unwrap(), TlsAgent::Rustls);
518        assert_eq!("rUsTlS".parse::<TlsAgent>().unwrap(), TlsAgent::Rustls);
519
520        assert_eq!("boring".parse::<TlsAgent>().unwrap(), TlsAgent::Boringssl);
521        assert_eq!("BoRiNg".parse::<TlsAgent>().unwrap(), TlsAgent::Boringssl);
522
523        assert_eq!("nss".parse::<TlsAgent>().unwrap(), TlsAgent::Nss);
524        assert_eq!("NSS".parse::<TlsAgent>().unwrap(), TlsAgent::Nss);
525
526        assert_eq!("preserve".parse::<TlsAgent>().unwrap(), TlsAgent::Preserve);
527        assert_eq!("Preserve".parse::<TlsAgent>().unwrap(), TlsAgent::Preserve);
528
529        "".parse::<TlsAgent>().unwrap_err();
530        "invalid".parse::<TlsAgent>().unwrap_err();
531    }
532
533    #[test]
534    fn test_tls_agent_deserialize() {
535        assert_eq!(
536            serde_json::from_str::<TlsAgent>(r#""rustls""#).unwrap(),
537            TlsAgent::Rustls
538        );
539        assert_eq!(
540            serde_json::from_str::<TlsAgent>(r#""RuStLs""#).unwrap(),
541            TlsAgent::Rustls
542        );
543
544        assert_eq!(
545            serde_json::from_str::<TlsAgent>(r#""boringssl""#).unwrap(),
546            TlsAgent::Boringssl
547        );
548        assert_eq!(
549            serde_json::from_str::<TlsAgent>(r#""BoringSSL""#).unwrap(),
550            TlsAgent::Boringssl
551        );
552
553        assert_eq!(
554            serde_json::from_str::<TlsAgent>(r#""nss""#).unwrap(),
555            TlsAgent::Nss
556        );
557        assert_eq!(
558            serde_json::from_str::<TlsAgent>(r#""NsS""#).unwrap(),
559            TlsAgent::Nss
560        );
561
562        assert_eq!(
563            serde_json::from_str::<TlsAgent>(r#""preserve""#).unwrap(),
564            TlsAgent::Preserve
565        );
566        assert_eq!(
567            serde_json::from_str::<TlsAgent>(r#""PreSeRvE""#).unwrap(),
568            TlsAgent::Preserve
569        );
570
571        serde_json::from_str::<TlsAgent>(r#""invalid""#).unwrap_err();
572        serde_json::from_str::<TlsAgent>(r#""""#).unwrap_err();
573        serde_json::from_str::<TlsAgent>("1").unwrap_err();
574    }
575
576    #[test]
577    fn test_http_agent_parse() {
578        assert_eq!("chrome".parse::<HttpAgent>().unwrap(), HttpAgent::Chromium);
579        assert_eq!("ChRoMe".parse::<HttpAgent>().unwrap(), HttpAgent::Chromium);
580
581        assert_eq!("firefox".parse::<HttpAgent>().unwrap(), HttpAgent::Firefox);
582        assert_eq!("FiRefoX".parse::<HttpAgent>().unwrap(), HttpAgent::Firefox);
583
584        assert_eq!("safari".parse::<HttpAgent>().unwrap(), HttpAgent::Safari);
585        assert_eq!("SaFaRi".parse::<HttpAgent>().unwrap(), HttpAgent::Safari);
586
587        assert_eq!(
588            "preserve".parse::<HttpAgent>().unwrap(),
589            HttpAgent::Preserve
590        );
591        assert_eq!(
592            "Preserve".parse::<HttpAgent>().unwrap(),
593            HttpAgent::Preserve
594        );
595
596        "".parse::<HttpAgent>().unwrap_err();
597        "invalid".parse::<HttpAgent>().unwrap_err();
598    }
599
600    #[test]
601    fn test_http_agent_deserialize() {
602        assert_eq!(
603            serde_json::from_str::<HttpAgent>(r#""chrome""#).unwrap(),
604            HttpAgent::Chromium
605        );
606        assert_eq!(
607            serde_json::from_str::<HttpAgent>(r#""ChRoMe""#).unwrap(),
608            HttpAgent::Chromium
609        );
610
611        assert_eq!(
612            serde_json::from_str::<HttpAgent>(r#""firefox""#).unwrap(),
613            HttpAgent::Firefox
614        );
615        assert_eq!(
616            serde_json::from_str::<HttpAgent>(r#""FirEfOx""#).unwrap(),
617            HttpAgent::Firefox
618        );
619
620        assert_eq!(
621            serde_json::from_str::<HttpAgent>(r#""safari""#).unwrap(),
622            HttpAgent::Safari
623        );
624        assert_eq!(
625            serde_json::from_str::<HttpAgent>(r#""SafArI""#).unwrap(),
626            HttpAgent::Safari
627        );
628
629        assert_eq!(
630            serde_json::from_str::<HttpAgent>(r#""preserve""#).unwrap(),
631            HttpAgent::Preserve
632        );
633        assert_eq!(
634            serde_json::from_str::<HttpAgent>(r#""PreSeRve""#).unwrap(),
635            HttpAgent::Preserve
636        );
637
638        serde_json::from_str::<HttpAgent>("1").unwrap_err();
639        serde_json::from_str::<HttpAgent>(r#""""#).unwrap_err();
640        serde_json::from_str::<HttpAgent>(r#""invalid""#).unwrap_err();
641    }
642}