use crate::http::UNKNOWN_SOFTWARE;
use crate::matcher_api::{HttpMatcher, HttpRequestMatch};
use std::fmt;
use std::net::IpAddr;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Serialize))]
pub struct ObservedOs {
pub name: String,
}
pub trait ObservedOsSource: Send + Sync {
fn observed_os(&self, client: IpAddr, client_port: u16) -> Option<ObservedOs>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObservedOsInput<'a> {
NoSource,
Missing,
Present(&'a ObservedOs),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Serialize))]
pub enum NotCheckedReason {
NoMatch,
NotUserlandApp,
NoUserAgent,
Dishonest,
UaNotInTable,
NoSource,
NoObservedOs,
}
impl fmt::Display for NotCheckedReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NoMatch => "no match",
Self::NotUserlandApp => "not userland app",
Self::NoUserAgent => "no user-agent",
Self::Dishonest => "dishonest",
Self::UaNotInTable => "ua not in table",
Self::NoSource => "no source",
Self::NoObservedOs => "no observed os",
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Serialize))]
pub enum UaOsAgreement {
NotChecked(NotCheckedReason),
Consistent { os: String },
Divergent { ua_os: String, network_os: String },
}
impl fmt::Display for UaOsAgreement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotChecked(reason) => write!(f, "not checked ({reason})"),
Self::Consistent { os } => write!(f, "consistent ({os})"),
Self::Divergent { ua_os, network_os } => {
write!(f, "divergent (ua={ua_os}, net={network_os})")
}
}
}
}
fn usable_user_agent(ua: Option<&str>) -> Option<&str> {
let ua = ua
.map(str::trim)
.filter(|s| !s.is_empty() && *s != UNKNOWN_SOFTWARE)?;
Some(ua)
}
pub fn check_ua_os_agreement(
ua: Option<&str>,
matched: Option<&HttpRequestMatch>,
matcher: Option<&dyn HttpMatcher>,
observed: ObservedOsInput<'_>,
) -> UaOsAgreement {
let Some(matched) = matched else {
return UaOsAgreement::NotChecked(NotCheckedReason::NoMatch);
};
if matched.browser.family.is_some() {
return UaOsAgreement::NotChecked(NotCheckedReason::NotUserlandApp);
}
let Some(ua) = usable_user_agent(ua) else {
return UaOsAgreement::NotChecked(NotCheckedReason::NoUserAgent);
};
if matched.dishonest {
return UaOsAgreement::NotChecked(NotCheckedReason::Dishonest);
}
let Some(ua_os) = matcher.and_then(|m| m.match_user_agent(ua)) else {
return UaOsAgreement::NotChecked(NotCheckedReason::UaNotInTable);
};
let observed = match observed {
ObservedOsInput::NoSource => {
return UaOsAgreement::NotChecked(NotCheckedReason::NoSource);
}
ObservedOsInput::Missing => {
return UaOsAgreement::NotChecked(NotCheckedReason::NoObservedOs);
}
ObservedOsInput::Present(os) => os,
};
if ua_os.family == observed.name {
UaOsAgreement::Consistent { os: ua_os.family }
} else {
UaOsAgreement::Divergent { ua_os: ua_os.family, network_os: observed.name.clone() }
}
}