use std::collections::HashMap;
use once_cell::sync::Lazy;
use rand::prelude::IndexedRandom;
use serde::Deserialize;
const BROWSERS_JSON: &str = include_str!("../data/browsers.json");
#[derive(Debug, Deserialize, Clone)]
pub struct BrowserHeaders {
#[serde(rename = "User-Agent")]
pub user_agent: Option<String>,
#[serde(rename = "Accept")]
pub accept: String,
#[serde(rename = "Accept-Language")]
pub accept_language: String,
#[serde(rename = "Accept-Encoding")]
pub accept_encoding: String,
}
#[derive(Debug, Deserialize)]
struct BrowsersJson {
headers: HashMap<String, BrowserHeaders>,
#[serde(rename = "cipherSuite")]
cipher_suite: HashMap<String, Vec<String>>,
user_agents: UserAgentsByDevice,
}
#[derive(Debug, Deserialize)]
struct UserAgentsByDevice {
desktop: HashMap<String, HashMap<String, Vec<String>>>,
mobile: HashMap<String, HashMap<String, Vec<String>>>,
}
static BROWSERS: Lazy<BrowsersJson> =
Lazy::new(|| serde_json::from_str(BROWSERS_JSON).expect("browsers.json is malformed"));
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Browser {
Chrome,
Firefox,
}
impl Browser {
fn key(&self) -> &'static str {
match self {
Browser::Chrome => "chrome",
Browser::Firefox => "firefox",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceType {
Desktop,
Mobile,
Any,
}
#[derive(Debug, Clone)]
pub enum Platform {
Any,
Named(String),
}
#[derive(Debug, Clone)]
pub struct UserAgentOptions {
pub browser: Option<Browser>,
pub platform: Option<String>,
pub desktop: bool,
pub mobile: bool,
pub custom: Option<String>,
pub allow_brotli: bool,
}
impl Default for UserAgentOptions {
fn default() -> Self {
UserAgentOptions {
browser: None,
platform: None,
desktop: true,
mobile: true,
custom: None,
allow_brotli: false,
}
}
}
#[derive(Debug, Clone)]
pub struct UserAgent {
pub user_agent_string: String,
pub headers: BrowserHeaders,
pub cipher_suite: Vec<String>,
pub browser: Browser,
}
impl UserAgent {
pub fn new(opts: &UserAgentOptions) -> crate::error::Result<Self> {
let db = &*BROWSERS;
if let Some(custom) = &opts.custom {
let matched = db
.user_agents
.desktop
.iter()
.chain(db.user_agents.mobile.iter())
.find_map(|(_platform, browser_map)| {
browser_map.iter().find_map(|(browser_key, agents)| {
if agents.iter().any(|a| a.contains(custom.as_str())) {
Some(browser_key.clone())
} else {
None
}
})
});
let (browser, headers, cipher_suite) = if let Some(bk) = matched {
let b = if bk == "firefox" {
Browser::Firefox
} else {
Browser::Chrome
};
let h = db.headers[&bk].clone();
let c = db.cipher_suite[&bk].clone();
(b, h, c)
} else {
let h = db.headers["chrome"].clone();
let c = db.cipher_suite["chrome"].clone();
(Browser::Chrome, h, c)
};
let mut headers = headers;
headers.user_agent = Some(custom.clone());
let headers = strip_brotli_if_needed(headers, opts.allow_brotli);
return Ok(UserAgent {
user_agent_string: custom.clone(),
headers,
cipher_suite,
browser,
});
}
let mut rng = rand::rng();
let mut candidates: Vec<(String, Browser, Vec<String>)> = Vec::new();
let mut collect = |device_map: &HashMap<String, HashMap<String, Vec<String>>>| {
for (platform, browser_map) in device_map {
if let Some(pf) = &opts.platform {
if platform != pf {
continue;
}
}
for (bk, agents) in browser_map {
if agents.is_empty() {
continue;
}
let b = if bk == "firefox" {
Browser::Firefox
} else {
Browser::Chrome
};
if let Some(bf) = &opts.browser {
if &b != bf {
continue;
}
}
candidates.push((platform.clone(), b, agents.clone()));
}
}
};
if opts.desktop {
collect(&db.user_agents.desktop);
}
if opts.mobile {
collect(&db.user_agents.mobile);
}
if candidates.is_empty() {
return Err(crate::error::GhostwireError::Other(
"No matching user agents found for the given options".to_string(),
));
}
let (_, browser, agents): &(String, Browser, Vec<String>) =
candidates.choose(&mut rng).unwrap();
let ua_string = agents.choose(&mut rng).unwrap().clone();
let bk = browser.key();
let mut headers = db.headers[bk].clone();
headers.user_agent = Some(ua_string.clone());
let headers = strip_brotli_if_needed(headers, opts.allow_brotli);
let cipher_suite = db.cipher_suite[bk].clone();
Ok(UserAgent {
user_agent_string: ua_string,
headers,
cipher_suite,
browser: browser.clone(),
})
}
pub fn header_map(&self) -> reqwest::header::HeaderMap {
use reqwest::header::{ACCEPT, ACCEPT_ENCODING, ACCEPT_LANGUAGE, HeaderValue, USER_AGENT};
let mut map = reqwest::header::HeaderMap::new();
map.insert(
USER_AGENT,
HeaderValue::from_str(&self.user_agent_string).unwrap(),
);
map.insert(ACCEPT, HeaderValue::from_str(&self.headers.accept).unwrap());
map.insert(
ACCEPT_LANGUAGE,
HeaderValue::from_str(&self.headers.accept_language).unwrap(),
);
map.insert(
ACCEPT_ENCODING,
HeaderValue::from_str(&self.headers.accept_encoding).unwrap(),
);
map
}
}
fn strip_brotli_if_needed(mut headers: BrowserHeaders, allow_brotli: bool) -> BrowserHeaders {
if !allow_brotli && headers.accept_encoding.contains("br") {
headers.accept_encoding = headers
.accept_encoding
.split(',')
.map(str::trim)
.filter(|e| *e != "br")
.collect::<Vec<_>>()
.join(", ");
}
headers
}