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#[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#[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#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct UserAgentInfo {
56 pub kind: UserAgentKind,
58 pub version: Option<usize>,
60}
61
62impl UserAgent {
63 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 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 pub fn tls_agent(mut self, tls_agent: TlsAgent) -> Self {
79 self.tls_agent_overwrite = Some(tls_agent);
80 self
81 }
82 }
83
84 #[must_use]
86 pub fn header_str(&self) -> &str {
87 &self.header
88 }
89
90 #[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 #[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 #[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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
209pub enum UserAgentKind {
210 Chromium,
212 Firefox,
214 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
256pub enum DeviceKind {
257 Desktop,
259 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
297pub enum PlatformKind {
298 Windows,
300 MacOS,
302 Linux,
304 Android,
306 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
358pub enum HttpAgent {
359 Chromium,
361 Firefox,
363 Safari,
365 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
410pub enum TlsAgent {
411 Rustls,
414 Boringssl,
416 Nss,
418 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}