1use std::collections::HashMap;
26
27use crate::network::{BayesianNetwork, header_network, input_network};
28use crate::{Error, OrderedMap, Restrictions, Result, Rng, Values};
29
30pub const SUPPORTED_BROWSERS: [&str; 4] = ["chrome", "firefox", "safari", "edge"];
32pub const SUPPORTED_OPERATING_SYSTEMS: [&str; 5] = ["windows", "macos", "linux", "android", "ios"];
34pub const SUPPORTED_DEVICES: [&str; 2] = ["desktop", "mobile"];
36pub const MISSING_VALUE: &str = "*MISSING_VALUE*";
38pub const MAX_LOCALES: usize = 10;
44
45pub const SEC_FETCH: [(&str, &str); 4] = [
53 ("sec-fetch-site", "same-site"),
54 ("sec-fetch-mode", "navigate"),
55 ("sec-fetch-user", "?1"),
56 ("sec-fetch-dest", "document"),
57];
58
59const UA_INPUT_DRAWS: usize = 16;
67
68const BROWSER_HELPER_JSON: &str = include_str!("../data/browser-helper-file.json");
69const HEADERS_ORDER_JSON: &str = include_str!("../data/headers-order.json");
70
71pub type Headers = OrderedMap<String>;
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
76pub enum HttpVersion {
77 One,
79 #[default]
82 Two,
83}
84
85impl HttpVersion {
86 #[must_use]
88 pub const fn as_str(self) -> &'static str {
89 match self {
90 Self::One => "1",
91 Self::Two => "2",
92 }
93 }
94
95 fn parse(value: &str) -> Option<Self> {
96 match value {
97 "1" => Some(Self::One),
98 "2" => Some(Self::Two),
99 _ => None,
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Browser {
108 pub name: String,
110 pub min_version: Option<u32>,
112 pub max_version: Option<u32>,
114 pub http_version: HttpVersion,
116}
117
118impl Browser {
119 #[must_use]
121 pub fn new(name: impl Into<String>) -> Self {
122 Self {
123 name: name.into(),
124 min_version: None,
125 max_version: None,
126 http_version: HttpVersion::Two,
127 }
128 }
129
130 fn matches(&self, candidate: &HttpBrowser) -> bool {
131 self.name == candidate.name
132 && self.min_version.is_none_or(|min| min <= candidate.major)
133 && self.max_version.is_none_or(|max| max >= candidate.major)
134 && self.http_version == candidate.http_version
135 }
136}
137
138fn default_browsers(http_version: HttpVersion) -> Vec<Browser> {
139 SUPPORTED_BROWSERS
140 .iter()
141 .map(|&name| Browser {
142 http_version,
143 ..Browser::new(name)
144 })
145 .collect()
146}
147
148fn owned(values: &[&str]) -> Vec<String> {
149 values.iter().map(|&v| v.to_owned()).collect()
150}
151
152#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct HeaderOptions {
157 pub browsers: Vec<Browser>,
159 pub os: Vec<String>,
161 pub devices: Vec<String>,
163 pub locales: Vec<String>,
166 pub http_version: HttpVersion,
169 pub strict: bool,
171}
172
173impl Default for HeaderOptions {
174 fn default() -> Self {
175 Self {
176 browsers: default_browsers(HttpVersion::Two),
177 os: owned(&SUPPORTED_OPERATING_SYSTEMS),
178 devices: owned(&SUPPORTED_DEVICES),
179 locales: vec!["en-US".to_owned()],
180 http_version: HttpVersion::Two,
181 strict: false,
182 }
183 }
184}
185
186impl HeaderOptions {
187 #[must_use]
191 pub fn with_http_version(mut self, http_version: HttpVersion) -> Self {
192 self.http_version = http_version;
193 for browser in &mut self.browsers {
194 browser.http_version = http_version;
195 }
196 self
197 }
198
199 fn relax(&mut self) -> bool {
206 let defaults = Self::default().with_http_version(self.http_version);
207 if self.locales != defaults.locales {
208 self.locales = defaults.locales;
209 } else if self.devices != defaults.devices {
210 self.devices = defaults.devices;
211 } else if self.os != defaults.os {
212 self.os = defaults.os;
213 } else if self.browsers != defaults.browsers {
214 self.browsers = defaults.browsers;
215 } else {
216 return false;
217 }
218 true
219 }
220}
221
222#[derive(Debug, Clone, Default, PartialEq, Eq)]
224pub struct HeaderOverrides {
225 pub user_agent: Vec<String>,
227 pub request_dependent_headers: Headers,
234}
235
236#[derive(Debug, Clone, PartialEq, Eq)]
238struct HttpBrowser {
239 name: String,
240 major: u32,
241 complete: String,
242 http_version: HttpVersion,
243}
244
245impl HttpBrowser {
246 fn parse(value: &str) -> Result<Self> {
247 let malformed = || Error::Data(format!("malformed browser entry {value:?}"));
248 let (browser, http) = value.split_once('|').ok_or_else(malformed)?;
249 let (name, version) = browser.split_once('/').ok_or_else(malformed)?;
250 let mut parts = version.split('.');
251 let major = parts
252 .next()
253 .and_then(|part| part.parse().ok())
254 .ok_or_else(malformed)?;
255 if parts.any(|part| part.parse::<u32>().is_err()) {
256 return Err(malformed());
257 }
258 Ok(Self {
259 name: name.to_owned(),
260 major,
261 complete: value.to_owned(),
262 http_version: HttpVersion::parse(http).ok_or_else(malformed)?,
263 })
264 }
265
266 fn sends_sec_fetch(&self) -> bool {
268 match self.name.as_str() {
269 "chrome" => self.major >= 76,
270 "firefox" => self.major >= 90,
271 "edge" => self.major >= 79,
272 _ => false,
273 }
274 }
275}
276
277struct UaClosures {
280 http1: Restrictions,
281 http2: Restrictions,
282}
283
284impl UaClosures {
285 fn contains(closure: &Restrictions, key: &str, value: &str) -> bool {
286 closure
287 .get(key)
288 .is_some_and(|values| values.iter().any(|v| v == value))
289 }
290
291 fn allows_browser_http(&self, value: &str) -> bool {
293 let (browser, http) = value.split_once('|').unwrap_or((value, ""));
294 let closure = if http == "1" {
295 &self.http1
296 } else {
297 &self.http2
298 };
299 Self::contains(closure, "*BROWSER", browser)
300 }
301
302 fn allows(&self, key: &str, value: &str) -> bool {
304 Self::contains(&self.http1, key, value) || Self::contains(&self.http2, key, value)
305 }
306}
307
308#[derive(Debug, Clone)]
310pub struct HeaderGenerator {
311 options: HeaderOptions,
312 unique_browsers: Vec<HttpBrowser>,
313 headers_order: HashMap<String, Vec<String>>,
314}
315
316impl HeaderGenerator {
317 pub fn new(options: HeaderOptions) -> Result<Self> {
326 for browser in &options.browsers {
327 if let (Some(min), Some(max)) = (browser.min_version, browser.max_version)
328 && min > max
329 {
330 return Err(Error::InvalidOptions(format!(
331 "Browser min version constraint ({min}) cannot exceed max version ({max})"
332 )));
333 }
334 }
335 if options.locales.len() > MAX_LOCALES {
336 return Err(Error::InvalidOptions(format!(
337 "at most {MAX_LOCALES} locales are allowed, got {}",
338 options.locales.len()
339 )));
340 }
341 let data_error = |e: serde_json::Error| Error::Data(e.to_string());
342 let helper: Vec<String> = serde_json::from_str(BROWSER_HELPER_JSON).map_err(data_error)?;
343 let unique_browsers = helper
344 .iter()
345 .filter(|entry| *entry != MISSING_VALUE)
346 .map(|entry| HttpBrowser::parse(entry))
347 .collect::<Result<_>>()?;
348 let headers_order = serde_json::from_str(HEADERS_ORDER_JSON).map_err(data_error)?;
349 Ok(Self {
350 options,
351 unique_browsers,
352 headers_order,
353 })
354 }
355
356 #[must_use]
358 pub fn options(&self) -> &HeaderOptions {
359 &self.options
360 }
361
362 pub fn generate(&self, rng: &mut Rng, overrides: &HeaderOverrides) -> Result<Headers> {
381 let input_net = input_network()?;
382 let header_net = header_network()?;
383 let closures = if overrides.user_agent.is_empty() {
384 None
385 } else {
386 let restrict = |node: &str| -> Result<Restrictions> {
387 let mut restriction = Restrictions::new();
388 restriction.insert(node, overrides.user_agent.clone());
389 header_net.possible_values(&restriction)
390 };
391 Some(UaClosures {
392 http1: restrict("User-Agent")?,
393 http2: restrict("user-agent")?,
394 })
395 };
396 let draws = if closures.is_some() {
397 UA_INPUT_DRAWS
398 } else {
399 1
400 };
401
402 let mut state = self.options.clone();
403 loop {
404 let constraints = self.constraints(&state, closures.as_ref());
405 for _ in 0..draws {
406 let Some(input) = input_net.consistent_sample(rng, &constraints) else {
407 break;
408 };
409 if let Some(mut headers) =
410 self.headers_for(rng, header_net, &input, &state.locales, overrides)?
411 {
412 add_request_dependent(&mut headers, &overrides.request_dependent_headers);
413 return Ok(headers);
414 }
415 }
416 if state.http_version == HttpVersion::One {
417 state = state.with_http_version(HttpVersion::Two);
418 } else if state.strict || !state.relax() {
419 return Err(Error::NoHeaders);
420 }
421 }
422 }
423
424 fn constraints(&self, state: &HeaderOptions, closures: Option<&UaClosures>) -> Restrictions {
427 let mut browser_http: Vec<String> = state
428 .browsers
429 .iter()
430 .flat_map(|browser| {
431 self.unique_browsers
432 .iter()
433 .filter(|candidate| browser.matches(candidate))
434 .map(|candidate| candidate.complete.clone())
435 })
436 .collect();
437 let mut os = state.os.clone();
438 let mut devices = state.devices.clone();
439 if let Some(closures) = closures {
440 browser_http.retain(|value| closures.allows_browser_http(value));
441 os.retain(|value| closures.allows("*OPERATING_SYSTEM", value));
442 devices.retain(|value| closures.allows("*DEVICE", value));
443 }
444 let mut constraints = Restrictions::new();
445 constraints.insert("*BROWSER_HTTP", browser_http);
446 constraints.insert("*OPERATING_SYSTEM", os);
447 constraints.insert("*DEVICE", devices);
448 constraints
449 }
450
451 fn headers_for(
454 &self,
455 rng: &mut Rng,
456 header_net: &BayesianNetwork,
457 input: &Values,
458 locales: &[String],
459 overrides: &HeaderOverrides,
460 ) -> Result<Option<Headers>> {
461 let browser_http = input
462 .get("*BROWSER_HTTP")
463 .ok_or_else(|| Error::Data("input sample without *BROWSER_HTTP".to_owned()))?;
464 let browser = HttpBrowser::parse(browser_http)?;
465 let http2 = browser.http_version == HttpVersion::Two;
466
467 let mut restrictions = Restrictions::new();
470 if !overrides.user_agent.is_empty() {
471 let node = if http2 { "user-agent" } else { "User-Agent" };
472 restrictions.insert(node, overrides.user_agent.clone());
473 }
474 let Some(mut sample) = header_net.restricted_sample(rng, input, &restrictions) else {
475 return Ok(None);
476 };
477
478 let accept_language = if http2 {
479 "accept-language"
480 } else {
481 "Accept-Language"
482 };
483 sample.insert(accept_language, accept_language_header(locales));
484 if browser.sends_sec_fetch() {
485 for (name, value) in SEC_FETCH {
486 let name = if http2 {
487 name.to_owned()
488 } else {
489 pascalize(name)
490 };
491 sample.insert(name, value.to_owned());
492 }
493 }
494 let kept: Headers = sample
495 .into_iter()
496 .filter(|(name, value)| {
497 !(name.starts_with('*')
498 || value == MISSING_VALUE
499 || (name.eq_ignore_ascii_case("connection") && value == "close"))
500 })
501 .collect();
502 let ordered = self.order(kept)?;
503 Ok(Some(if http2 {
504 ordered
505 .into_iter()
506 .map(|(name, value)| (pascalize(&name), value))
507 .collect()
508 } else {
509 ordered
510 }))
511 }
512
513 fn order(&self, headers: Headers) -> Result<Headers> {
517 let user_agent = get_user_agent(&headers).ok_or(Error::MissingUserAgent)?;
518 let browser =
519 browser_of(user_agent).ok_or_else(|| Error::UnknownBrowser(user_agent.to_owned()))?;
520 let Some(order) = self.headers_order.get(browser) else {
521 return Ok(headers);
522 };
523 Ok(order
524 .iter()
525 .filter_map(|name| {
526 headers
527 .get(name)
528 .map(|value| (name.as_str(), value.clone()))
529 })
530 .collect())
531 }
532}
533
534fn accept_language_header(locales: &[String]) -> String {
536 locales
537 .iter()
538 .enumerate()
539 .map(|(index, locale)| {
540 let tenths = 10_usize.saturating_sub(index).max(1);
542 format!("{locale};q={}.{}", tenths / 10, tenths % 10)
543 })
544 .collect::<Vec<_>>()
545 .join(", ")
546}
547
548fn add_request_dependent(headers: &mut Headers, extra: &Headers) {
550 for (name, value) in extra {
551 let existing = headers
552 .keys()
553 .find(|key| key.eq_ignore_ascii_case(name))
554 .map(str::to_owned);
555 headers.insert(existing.unwrap_or_else(|| name.to_owned()), value.clone());
556 }
557}
558
559#[must_use]
562pub fn get_user_agent(headers: &Headers) -> Option<&str> {
563 ["User-Agent", "user-agent"]
564 .into_iter()
565 .filter_map(|name| headers.get(name))
566 .map(String::as_str)
567 .find(|value| !value.is_empty())
568}
569
570#[must_use]
577pub fn browser_of(user_agent: &str) -> Option<&'static str> {
578 let has = |aliases: &[&str]| aliases.iter().any(|alias| user_agent.contains(alias));
579 if has(&["Firefox", "FxiOS"]) {
580 Some("firefox")
581 } else if has(&["Edge", "EdgA", "EdgiOS", "Edg"]) {
582 Some("edge")
583 } else if has(&["Chrome", "CriOS"]) {
584 Some("chrome")
585 } else if has(&["Safari"]) {
586 Some("safari")
587 } else {
588 None
589 }
590}
591
592#[must_use]
597pub fn pascalize(name: &str) -> String {
598 if name.starts_with(':') || name.starts_with("sec-ch-ua") {
599 return name.to_owned();
600 }
601 if matches!(name, "dnt" | "rtt" | "ect") {
602 return name.to_ascii_uppercase();
603 }
604 let mut pascal = String::with_capacity(name.len());
605 let mut after_letter = false;
606 for c in name.chars() {
607 if after_letter {
608 pascal.extend(c.to_lowercase());
609 } else {
610 pascal.extend(c.to_uppercase());
611 }
612 after_letter = c.is_alphabetic();
613 }
614 pascal
615}