use super::parse_http_user_agent_header;
use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorExt};
use rama_core::extensions::Extension;
use rama_utils::{macros::match_ignore_ascii_case_str, str::arcstr::ArcStr};
use std::{convert::Infallible, fmt, str::FromStr};
#[derive(Debug, Clone, Extension)]
#[extension(tags(ua))]
pub struct UserAgent {
pub(super) header: ArcStr,
pub(super) data: UserAgentData,
pub(super) http_agent_overwrite: Option<HttpAgent>,
pub(super) tls_agent_overwrite: Option<TlsAgent>,
}
impl fmt::Display for UserAgent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.header)
}
}
#[derive(Debug, Clone)]
pub(super) enum UserAgentData {
Standard {
info: UserAgentInfo,
platform_like: Option<PlatformLike>,
},
Platform(PlatformKind),
Device(DeviceKind),
Unknown,
}
#[derive(Debug, Clone)]
pub(super) enum PlatformLike {
Platform(PlatformKind),
Device(DeviceKind),
}
impl PlatformLike {
pub(super) fn device(&self) -> DeviceKind {
match self {
Self::Platform(platform_kind) => platform_kind.device(),
Self::Device(device_kind) => *device_kind,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserAgentInfo {
pub kind: UserAgentKind,
pub version: Option<usize>,
}
impl UserAgent {
pub fn new(header: impl Into<ArcStr>) -> Self {
parse_http_user_agent_header(header.into())
}
rama_utils::macros::generate_set_and_with! {
pub fn http_agent(mut self, http_agent: HttpAgent) -> Self {
self.http_agent_overwrite = Some(http_agent);
self
}
}
rama_utils::macros::generate_set_and_with! {
pub fn tls_agent(mut self, tls_agent: TlsAgent) -> Self {
self.tls_agent_overwrite = Some(tls_agent);
self
}
}
#[must_use]
pub fn header_str(&self) -> &str {
&self.header
}
#[must_use]
pub fn device(&self) -> Option<DeviceKind> {
match &self.data {
UserAgentData::Standard { platform_like, .. } => {
platform_like.as_ref().map(|p| p.device())
}
UserAgentData::Platform(platform) => Some(platform.device()),
UserAgentData::Device(kind) => Some(*kind),
UserAgentData::Unknown => None,
}
}
#[must_use]
pub fn info(&self) -> Option<UserAgentInfo> {
if let UserAgentData::Standard { info, .. } = &self.data {
Some(info.clone())
} else {
None
}
}
#[must_use]
pub fn ua_kind(&self) -> Option<UserAgentKind> {
match self.http_agent_overwrite {
Some(HttpAgent::Chromium) => Some(UserAgentKind::Chromium),
Some(HttpAgent::Safari) => Some(UserAgentKind::Safari),
Some(HttpAgent::Firefox) => Some(UserAgentKind::Firefox),
Some(HttpAgent::Preserve) => None,
None => match &self.data {
UserAgentData::Standard {
info: UserAgentInfo { kind, .. },
..
} => Some(*kind),
UserAgentData::Device(_) | UserAgentData::Platform(_) | UserAgentData::Unknown => {
None
}
},
}
}
#[must_use]
pub fn ua_version(&self) -> Option<usize> {
match &self.data {
UserAgentData::Standard { info, .. } => info.version,
UserAgentData::Device(_) | UserAgentData::Platform(_) | UserAgentData::Unknown => None,
}
}
#[must_use]
pub fn platform(&self) -> Option<PlatformKind> {
match &self.data {
UserAgentData::Standard { platform_like, .. } => match platform_like {
Some(PlatformLike::Platform(platform)) => Some(*platform),
None | Some(PlatformLike::Device(_)) => None,
},
UserAgentData::Platform(platform) => Some(*platform),
UserAgentData::Device(_) | UserAgentData::Unknown => None,
}
}
#[must_use]
pub fn http_agent(&self) -> Option<HttpAgent> {
match self.http_agent_overwrite {
Some(agent) => Some(agent),
None => match &self.data {
UserAgentData::Standard { info, .. } => Some(match info.kind {
UserAgentKind::Chromium => HttpAgent::Chromium,
UserAgentKind::Firefox => HttpAgent::Firefox,
UserAgentKind::Safari => HttpAgent::Safari,
}),
UserAgentData::Platform(_) | UserAgentData::Device(_) | UserAgentData::Unknown => {
None
}
},
}
}
#[must_use]
pub fn tls_agent(&self) -> Option<TlsAgent> {
match self.tls_agent_overwrite {
Some(agent) => Some(agent),
None => match &self.data {
UserAgentData::Standard { info, .. } => Some(match info.kind {
UserAgentKind::Chromium => TlsAgent::Boringssl,
UserAgentKind::Firefox => TlsAgent::Nss,
UserAgentKind::Safari => TlsAgent::Rustls,
}),
UserAgentData::Device(_) | UserAgentData::Platform(_) | UserAgentData::Unknown => {
None
}
},
}
}
}
impl FromStr for UserAgent {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::new(s))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UserAgentKind {
Chromium,
Firefox,
Safari,
}
impl UserAgentKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Chromium => "Chromium",
Self::Firefox => "Firefox",
Self::Safari => "Safari",
}
}
}
impl fmt::Display for UserAgentKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for UserAgentKind {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match_ignore_ascii_case_str! {
match (s) {
"chromium" => Ok(Self::Chromium),
"firefox" => Ok(Self::Firefox),
"safari" => Ok(Self::Safari),
_ => Err(BoxError::from_static_str("invalid user agent kind").context_str_field("str", s)),
}
}
}
}
use rama_utils::macros::serde_str::impl_serde_str;
impl_serde_str!(as_str UserAgentKind);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeviceKind {
Desktop,
Mobile,
}
impl DeviceKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Desktop => "Desktop",
Self::Mobile => "Mobile",
}
}
}
impl FromStr for DeviceKind {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match_ignore_ascii_case_str! {
match (s) {
"desktop" => Ok(Self::Desktop),
"mobile" => Ok(Self::Mobile),
_ => Err(BoxError::from_static_str("invalid device").context_str_field("str", s)),
}
}
}
}
impl_serde_str!(as_str DeviceKind);
impl fmt::Display for DeviceKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PlatformKind {
Windows,
MacOS,
Linux,
Android,
IOS,
}
impl PlatformKind {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Windows => "Windows",
Self::MacOS => "MacOS",
Self::Linux => "Linux",
Self::Android => "Android",
Self::IOS => "iOS",
}
}
#[must_use]
pub fn device(&self) -> DeviceKind {
match self {
Self::Windows | Self::MacOS | Self::Linux => DeviceKind::Desktop,
Self::Android | Self::IOS => DeviceKind::Mobile,
}
}
}
impl FromStr for PlatformKind {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match_ignore_ascii_case_str! {
match (s) {
"windows" => Ok(Self::Windows),
"macos" => Ok(Self::MacOS),
"linux" => Ok(Self::Linux),
"android" => Ok(Self::Android),
"ios" => Ok(Self::IOS),
_ => Err(BoxError::from_static_str("invalid platform").context_str_field("str", s)),
}
}
}
}
impl_serde_str!(as_str PlatformKind);
impl fmt::Display for PlatformKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HttpAgent {
Chromium,
Firefox,
Safari,
Preserve,
}
impl HttpAgent {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Chromium => "Chromium",
Self::Firefox => "Firefox",
Self::Safari => "Safari",
Self::Preserve => "Preserve",
}
}
}
impl fmt::Display for HttpAgent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl_serde_str!(as_str HttpAgent);
impl FromStr for HttpAgent {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match_ignore_ascii_case_str! {
match (s) {
"chrome" | "chromium" => Ok(Self::Chromium),
"Firefox" => Ok(Self::Firefox),
"Safari" => Ok(Self::Safari),
"preserve" => Ok(Self::Preserve),
_ => Err(BoxError::from_static_str("invalid http agent").context_str_field("str", s)),
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TlsAgent {
Rustls,
Boringssl,
Nss,
Preserve,
}
impl TlsAgent {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::Rustls => "Rustls",
Self::Boringssl => "Boringssl",
Self::Nss => "NSS",
Self::Preserve => "Preserve",
}
}
}
impl fmt::Display for TlsAgent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl_serde_str!(as_str TlsAgent);
impl FromStr for TlsAgent {
type Err = BoxError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match_ignore_ascii_case_str! {
match (s) {
"rustls" => Ok(Self::Rustls),
"boring" | "boringssl" => Ok(Self::Boringssl),
"nss" => Ok(Self::Nss),
"preserve" => Ok(Self::Preserve),
_ => Err(BoxError::from_static_str("invalid tls agent").context_str_field("str", s)),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_agent_new() {
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());
assert_eq!(
ua.header_str(),
"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"
);
assert_eq!(
ua.info(),
Some(UserAgentInfo {
kind: UserAgentKind::Chromium,
version: Some(124)
})
);
assert_eq!(ua.platform(), Some(PlatformKind::MacOS));
assert_eq!(ua.device(), Some(DeviceKind::Desktop));
assert_eq!(ua.http_agent(), Some(HttpAgent::Chromium));
assert_eq!(ua.tls_agent(), Some(TlsAgent::Boringssl));
}
#[test]
fn test_user_agent_parse() {
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();
assert_eq!(
ua.header_str(),
"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"
);
assert_eq!(
ua.info(),
Some(UserAgentInfo {
kind: UserAgentKind::Chromium,
version: Some(124)
})
);
assert_eq!(ua.platform(), Some(PlatformKind::MacOS));
assert_eq!(ua.device(), Some(DeviceKind::Desktop));
assert_eq!(ua.http_agent(), Some(HttpAgent::Chromium));
assert_eq!(ua.tls_agent(), Some(TlsAgent::Boringssl));
}
#[test]
fn test_user_agent_display() {
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();
assert_eq!(
ua.to_string(),
"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"
);
}
#[test]
fn test_tls_agent_parse() {
assert_eq!("rustls".parse::<TlsAgent>().unwrap(), TlsAgent::Rustls);
assert_eq!("rUsTlS".parse::<TlsAgent>().unwrap(), TlsAgent::Rustls);
assert_eq!("boring".parse::<TlsAgent>().unwrap(), TlsAgent::Boringssl);
assert_eq!("BoRiNg".parse::<TlsAgent>().unwrap(), TlsAgent::Boringssl);
assert_eq!("nss".parse::<TlsAgent>().unwrap(), TlsAgent::Nss);
assert_eq!("NSS".parse::<TlsAgent>().unwrap(), TlsAgent::Nss);
assert_eq!("preserve".parse::<TlsAgent>().unwrap(), TlsAgent::Preserve);
assert_eq!("Preserve".parse::<TlsAgent>().unwrap(), TlsAgent::Preserve);
"".parse::<TlsAgent>().unwrap_err();
"invalid".parse::<TlsAgent>().unwrap_err();
}
#[test]
fn test_tls_agent_deserialize() {
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""rustls""#).unwrap(),
TlsAgent::Rustls
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""RuStLs""#).unwrap(),
TlsAgent::Rustls
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""boringssl""#).unwrap(),
TlsAgent::Boringssl
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""BoringSSL""#).unwrap(),
TlsAgent::Boringssl
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""nss""#).unwrap(),
TlsAgent::Nss
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""NsS""#).unwrap(),
TlsAgent::Nss
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""preserve""#).unwrap(),
TlsAgent::Preserve
);
assert_eq!(
serde_json::from_str::<TlsAgent>(r#""PreSeRvE""#).unwrap(),
TlsAgent::Preserve
);
serde_json::from_str::<TlsAgent>(r#""invalid""#).unwrap_err();
serde_json::from_str::<TlsAgent>(r#""""#).unwrap_err();
serde_json::from_str::<TlsAgent>("1").unwrap_err();
}
#[test]
fn test_http_agent_parse() {
assert_eq!("chrome".parse::<HttpAgent>().unwrap(), HttpAgent::Chromium);
assert_eq!("ChRoMe".parse::<HttpAgent>().unwrap(), HttpAgent::Chromium);
assert_eq!("firefox".parse::<HttpAgent>().unwrap(), HttpAgent::Firefox);
assert_eq!("FiRefoX".parse::<HttpAgent>().unwrap(), HttpAgent::Firefox);
assert_eq!("safari".parse::<HttpAgent>().unwrap(), HttpAgent::Safari);
assert_eq!("SaFaRi".parse::<HttpAgent>().unwrap(), HttpAgent::Safari);
assert_eq!(
"preserve".parse::<HttpAgent>().unwrap(),
HttpAgent::Preserve
);
assert_eq!(
"Preserve".parse::<HttpAgent>().unwrap(),
HttpAgent::Preserve
);
"".parse::<HttpAgent>().unwrap_err();
"invalid".parse::<HttpAgent>().unwrap_err();
}
#[test]
fn test_http_agent_deserialize() {
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""chrome""#).unwrap(),
HttpAgent::Chromium
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""ChRoMe""#).unwrap(),
HttpAgent::Chromium
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""firefox""#).unwrap(),
HttpAgent::Firefox
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""FirEfOx""#).unwrap(),
HttpAgent::Firefox
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""safari""#).unwrap(),
HttpAgent::Safari
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""SafArI""#).unwrap(),
HttpAgent::Safari
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""preserve""#).unwrap(),
HttpAgent::Preserve
);
assert_eq!(
serde_json::from_str::<HttpAgent>(r#""PreSeRve""#).unwrap(),
HttpAgent::Preserve
);
serde_json::from_str::<HttpAgent>("1").unwrap_err();
serde_json::from_str::<HttpAgent>(r#""""#).unwrap_err();
serde_json::from_str::<HttpAgent>(r#""invalid""#).unwrap_err();
}
}