use std::collections::HashMap;
use crate::network::{BayesianNetwork, header_network, input_network};
use crate::{Error, OrderedMap, Restrictions, Result, Rng, Values};
pub const SUPPORTED_BROWSERS: [&str; 4] = ["chrome", "firefox", "safari", "edge"];
pub const SUPPORTED_OPERATING_SYSTEMS: [&str; 5] = ["windows", "macos", "linux", "android", "ios"];
pub const SUPPORTED_DEVICES: [&str; 2] = ["desktop", "mobile"];
pub const MISSING_VALUE: &str = "*MISSING_VALUE*";
pub const MAX_LOCALES: usize = 10;
pub const SEC_FETCH: [(&str, &str); 4] = [
("sec-fetch-site", "same-site"),
("sec-fetch-mode", "navigate"),
("sec-fetch-user", "?1"),
("sec-fetch-dest", "document"),
];
const UA_INPUT_DRAWS: usize = 16;
const BROWSER_HELPER_JSON: &str = include_str!("../data/browser-helper-file.json");
const HEADERS_ORDER_JSON: &str = include_str!("../data/headers-order.json");
pub type Headers = OrderedMap<String>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum HttpVersion {
One,
#[default]
Two,
}
impl HttpVersion {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::One => "1",
Self::Two => "2",
}
}
fn parse(value: &str) -> Option<Self> {
match value {
"1" => Some(Self::One),
"2" => Some(Self::Two),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Browser {
pub name: String,
pub min_version: Option<u32>,
pub max_version: Option<u32>,
pub http_version: HttpVersion,
}
impl Browser {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
min_version: None,
max_version: None,
http_version: HttpVersion::Two,
}
}
fn matches(&self, candidate: &HttpBrowser) -> bool {
self.name == candidate.name
&& self.min_version.is_none_or(|min| min <= candidate.major)
&& self.max_version.is_none_or(|max| max >= candidate.major)
&& self.http_version == candidate.http_version
}
}
fn default_browsers(http_version: HttpVersion) -> Vec<Browser> {
SUPPORTED_BROWSERS
.iter()
.map(|&name| Browser {
http_version,
..Browser::new(name)
})
.collect()
}
fn owned(values: &[&str]) -> Vec<String> {
values.iter().map(|&v| v.to_owned()).collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HeaderOptions {
pub browsers: Vec<Browser>,
pub os: Vec<String>,
pub devices: Vec<String>,
pub locales: Vec<String>,
pub http_version: HttpVersion,
pub strict: bool,
}
impl Default for HeaderOptions {
fn default() -> Self {
Self {
browsers: default_browsers(HttpVersion::Two),
os: owned(&SUPPORTED_OPERATING_SYSTEMS),
devices: owned(&SUPPORTED_DEVICES),
locales: vec!["en-US".to_owned()],
http_version: HttpVersion::Two,
strict: false,
}
}
}
impl HeaderOptions {
#[must_use]
pub fn with_http_version(mut self, http_version: HttpVersion) -> Self {
self.http_version = http_version;
for browser in &mut self.browsers {
browser.http_version = http_version;
}
self
}
fn relax(&mut self) -> bool {
let defaults = Self::default().with_http_version(self.http_version);
if self.locales != defaults.locales {
self.locales = defaults.locales;
} else if self.devices != defaults.devices {
self.devices = defaults.devices;
} else if self.os != defaults.os {
self.os = defaults.os;
} else if self.browsers != defaults.browsers {
self.browsers = defaults.browsers;
} else {
return false;
}
true
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HeaderOverrides {
pub user_agent: Vec<String>,
pub request_dependent_headers: Headers,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct HttpBrowser {
name: String,
major: u32,
complete: String,
http_version: HttpVersion,
}
impl HttpBrowser {
fn parse(value: &str) -> Result<Self> {
let malformed = || Error::Data(format!("malformed browser entry {value:?}"));
let (browser, http) = value.split_once('|').ok_or_else(malformed)?;
let (name, version) = browser.split_once('/').ok_or_else(malformed)?;
let mut parts = version.split('.');
let major = parts
.next()
.and_then(|part| part.parse().ok())
.ok_or_else(malformed)?;
if parts.any(|part| part.parse::<u32>().is_err()) {
return Err(malformed());
}
Ok(Self {
name: name.to_owned(),
major,
complete: value.to_owned(),
http_version: HttpVersion::parse(http).ok_or_else(malformed)?,
})
}
fn sends_sec_fetch(&self) -> bool {
match self.name.as_str() {
"chrome" => self.major >= 76,
"firefox" => self.major >= 90,
"edge" => self.major >= 79,
_ => false,
}
}
}
struct UaClosures {
http1: Restrictions,
http2: Restrictions,
}
impl UaClosures {
fn contains(closure: &Restrictions, key: &str, value: &str) -> bool {
closure
.get(key)
.is_some_and(|values| values.iter().any(|v| v == value))
}
fn allows_browser_http(&self, value: &str) -> bool {
let (browser, http) = value.split_once('|').unwrap_or((value, ""));
let closure = if http == "1" {
&self.http1
} else {
&self.http2
};
Self::contains(closure, "*BROWSER", browser)
}
fn allows(&self, key: &str, value: &str) -> bool {
Self::contains(&self.http1, key, value) || Self::contains(&self.http2, key, value)
}
}
#[derive(Debug, Clone)]
pub struct HeaderGenerator {
options: HeaderOptions,
unique_browsers: Vec<HttpBrowser>,
headers_order: HashMap<String, Vec<String>>,
}
impl HeaderGenerator {
pub fn new(options: HeaderOptions) -> Result<Self> {
for browser in &options.browsers {
if let (Some(min), Some(max)) = (browser.min_version, browser.max_version)
&& min > max
{
return Err(Error::InvalidOptions(format!(
"Browser min version constraint ({min}) cannot exceed max version ({max})"
)));
}
}
if options.locales.len() > MAX_LOCALES {
return Err(Error::InvalidOptions(format!(
"at most {MAX_LOCALES} locales are allowed, got {}",
options.locales.len()
)));
}
let data_error = |e: serde_json::Error| Error::Data(e.to_string());
let helper: Vec<String> = serde_json::from_str(BROWSER_HELPER_JSON).map_err(data_error)?;
let unique_browsers = helper
.iter()
.filter(|entry| *entry != MISSING_VALUE)
.map(|entry| HttpBrowser::parse(entry))
.collect::<Result<_>>()?;
let headers_order = serde_json::from_str(HEADERS_ORDER_JSON).map_err(data_error)?;
Ok(Self {
options,
unique_browsers,
headers_order,
})
}
#[must_use]
pub fn options(&self) -> &HeaderOptions {
&self.options
}
pub fn generate(&self, rng: &mut Rng, overrides: &HeaderOverrides) -> Result<Headers> {
let input_net = input_network()?;
let header_net = header_network()?;
let closures = if overrides.user_agent.is_empty() {
None
} else {
let restrict = |node: &str| -> Result<Restrictions> {
let mut restriction = Restrictions::new();
restriction.insert(node, overrides.user_agent.clone());
header_net.possible_values(&restriction)
};
Some(UaClosures {
http1: restrict("User-Agent")?,
http2: restrict("user-agent")?,
})
};
let draws = if closures.is_some() {
UA_INPUT_DRAWS
} else {
1
};
let mut state = self.options.clone();
loop {
let constraints = self.constraints(&state, closures.as_ref());
for _ in 0..draws {
let Some(input) = input_net.consistent_sample(rng, &constraints) else {
break;
};
if let Some(mut headers) =
self.headers_for(rng, header_net, &input, &state.locales, overrides)?
{
add_request_dependent(&mut headers, &overrides.request_dependent_headers);
return Ok(headers);
}
}
if state.http_version == HttpVersion::One {
state = state.with_http_version(HttpVersion::Two);
} else if state.strict || !state.relax() {
return Err(Error::NoHeaders);
}
}
}
fn constraints(&self, state: &HeaderOptions, closures: Option<&UaClosures>) -> Restrictions {
let mut browser_http: Vec<String> = state
.browsers
.iter()
.flat_map(|browser| {
self.unique_browsers
.iter()
.filter(|candidate| browser.matches(candidate))
.map(|candidate| candidate.complete.clone())
})
.collect();
let mut os = state.os.clone();
let mut devices = state.devices.clone();
if let Some(closures) = closures {
browser_http.retain(|value| closures.allows_browser_http(value));
os.retain(|value| closures.allows("*OPERATING_SYSTEM", value));
devices.retain(|value| closures.allows("*DEVICE", value));
}
let mut constraints = Restrictions::new();
constraints.insert("*BROWSER_HTTP", browser_http);
constraints.insert("*OPERATING_SYSTEM", os);
constraints.insert("*DEVICE", devices);
constraints
}
fn headers_for(
&self,
rng: &mut Rng,
header_net: &BayesianNetwork,
input: &Values,
locales: &[String],
overrides: &HeaderOverrides,
) -> Result<Option<Headers>> {
let browser_http = input
.get("*BROWSER_HTTP")
.ok_or_else(|| Error::Data("input sample without *BROWSER_HTTP".to_owned()))?;
let browser = HttpBrowser::parse(browser_http)?;
let http2 = browser.http_version == HttpVersion::Two;
let mut restrictions = Restrictions::new();
if !overrides.user_agent.is_empty() {
let node = if http2 { "user-agent" } else { "User-Agent" };
restrictions.insert(node, overrides.user_agent.clone());
}
let Some(mut sample) = header_net.restricted_sample(rng, input, &restrictions) else {
return Ok(None);
};
let accept_language = if http2 {
"accept-language"
} else {
"Accept-Language"
};
sample.insert(accept_language, accept_language_header(locales));
if browser.sends_sec_fetch() {
for (name, value) in SEC_FETCH {
let name = if http2 {
name.to_owned()
} else {
pascalize(name)
};
sample.insert(name, value.to_owned());
}
}
let kept: Headers = sample
.into_iter()
.filter(|(name, value)| {
!(name.starts_with('*')
|| value == MISSING_VALUE
|| (name.eq_ignore_ascii_case("connection") && value == "close"))
})
.collect();
let ordered = self.order(kept)?;
Ok(Some(if http2 {
ordered
.into_iter()
.map(|(name, value)| (pascalize(&name), value))
.collect()
} else {
ordered
}))
}
fn order(&self, headers: Headers) -> Result<Headers> {
let user_agent = get_user_agent(&headers).ok_or(Error::MissingUserAgent)?;
let browser =
browser_of(user_agent).ok_or_else(|| Error::UnknownBrowser(user_agent.to_owned()))?;
let Some(order) = self.headers_order.get(browser) else {
return Ok(headers);
};
Ok(order
.iter()
.filter_map(|name| {
headers
.get(name)
.map(|value| (name.as_str(), value.clone()))
})
.collect())
}
}
fn accept_language_header(locales: &[String]) -> String {
locales
.iter()
.enumerate()
.map(|(index, locale)| {
let tenths = 10_usize.saturating_sub(index).max(1);
format!("{locale};q={}.{}", tenths / 10, tenths % 10)
})
.collect::<Vec<_>>()
.join(", ")
}
fn add_request_dependent(headers: &mut Headers, extra: &Headers) {
for (name, value) in extra {
let existing = headers
.keys()
.find(|key| key.eq_ignore_ascii_case(name))
.map(str::to_owned);
headers.insert(existing.unwrap_or_else(|| name.to_owned()), value.clone());
}
}
#[must_use]
pub fn get_user_agent(headers: &Headers) -> Option<&str> {
["User-Agent", "user-agent"]
.into_iter()
.filter_map(|name| headers.get(name))
.map(String::as_str)
.find(|value| !value.is_empty())
}
#[must_use]
pub fn browser_of(user_agent: &str) -> Option<&'static str> {
let has = |aliases: &[&str]| aliases.iter().any(|alias| user_agent.contains(alias));
if has(&["Firefox", "FxiOS"]) {
Some("firefox")
} else if has(&["Edge", "EdgA", "EdgiOS", "Edg"]) {
Some("edge")
} else if has(&["Chrome", "CriOS"]) {
Some("chrome")
} else if has(&["Safari"]) {
Some("safari")
} else {
None
}
}
#[must_use]
pub fn pascalize(name: &str) -> String {
if name.starts_with(':') || name.starts_with("sec-ch-ua") {
return name.to_owned();
}
if matches!(name, "dnt" | "rtt" | "ect") {
return name.to_ascii_uppercase();
}
let mut pascal = String::with_capacity(name.len());
let mut after_letter = false;
for c in name.chars() {
if after_letter {
pascal.extend(c.to_lowercase());
} else {
pascal.extend(c.to_uppercase());
}
after_letter = c.is_alphabetic();
}
pascal
}