use crate::profile::generate_config_json;
use crate::Result;
use wreq_util::{Emulation, EmulationOS, EmulationOption};
pub struct HttpClient {
inner: wreq::Client,
ua: String,
}
impl HttpClient {
pub fn new() -> Result<Self> {
Self::builder().build()
}
pub fn builder() -> HttpClientBuilder {
HttpClientBuilder::default()
}
pub fn get(&self, url: &str) -> wreq::RequestBuilder {
self.inner.get(url)
}
pub fn post(&self, url: &str) -> wreq::RequestBuilder {
self.inner.post(url)
}
pub fn put(&self, url: &str) -> wreq::RequestBuilder {
self.inner.put(url)
}
pub fn delete(&self, url: &str) -> wreq::RequestBuilder {
self.inner.delete(url)
}
pub fn patch(&self, url: &str) -> wreq::RequestBuilder {
self.inner.patch(url)
}
pub fn head(&self, url: &str) -> wreq::RequestBuilder {
self.inner.head(url)
}
pub fn request(&self, method: wreq::Method, url: &str) -> wreq::RequestBuilder {
self.inner.request(method, url)
}
pub fn user_agent(&self) -> &str {
&self.ua
}
pub fn wreq(&self) -> &wreq::Client {
&self.inner
}
}
pub struct HttpClientBuilder {
profile: Option<(usize, u64)>,
proxy: Option<String>,
timeout_secs: u64,
cookie_store: bool,
}
impl Default for HttpClientBuilder {
fn default() -> Self {
Self {
profile: None,
proxy: None,
timeout_secs: 30,
cookie_store: true,
}
}
}
impl HttpClientBuilder {
pub fn profile(mut self, index: usize, seed: u64) -> Self {
self.profile = Some((index, seed));
self
}
pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
self.proxy = Some(proxy.into());
self
}
pub fn timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
pub fn cookie_store(mut self, enable: bool) -> Self {
self.cookie_store = enable;
self
}
pub fn build(self) -> Result<HttpClient> {
let (ua, languages) = if let Some((index, seed)) = self.profile {
let json = generate_config_json(index, seed);
let config: serde_json::Value = serde_json::from_str(&json)?;
let ua = config["navigator"]["user_agent"]
.as_str()
.unwrap_or(DEFAULT_UA)
.to_string();
let langs: Vec<String> = config["navigator"]["languages"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_else(|| vec!["en-US".into(), "en".into()]);
(ua, langs.join(", "))
} else {
(DEFAULT_UA.to_string(), "en-US, en".to_string())
};
let emulation = EmulationOption::builder()
.emulation(Emulation::Chrome134)
.emulation_os(EmulationOS::Windows)
.skip_headers(true) .build();
let mut cb = wreq::Client::builder()
.emulation(emulation)
.cookie_store(self.cookie_store)
.user_agent(&ua)
.default_headers(build_chrome_headers(&ua, &languages))
.timeout(std::time::Duration::from_secs(self.timeout_secs));
if let Some(ref proxy_url) = self.proxy {
cb = cb.proxy(wreq::Proxy::all(proxy_url)?);
}
let client = cb.build()?;
Ok(HttpClient { inner: client, ua })
}
}
const DEFAULT_UA: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.6998.0 Safari/537.36";
const SEC_CH_UA: &str = r#""Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134""#;
fn build_chrome_headers(ua: &str, languages: &str) -> wreq::header::HeaderMap {
use wreq::header::{self, HeaderMap, HeaderValue};
let mut h = HeaderMap::new();
h.insert(
header::ACCEPT,
HeaderValue::from_static("text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"),
);
h.insert(
header::ACCEPT_ENCODING,
HeaderValue::from_static("gzip, deflate, br, zstd"),
);
if let Ok(v) = HeaderValue::from_str(languages) {
h.insert(header::ACCEPT_LANGUAGE, v);
}
h.insert("sec-ch-ua", HeaderValue::from_static(SEC_CH_UA));
h.insert("sec-ch-ua-mobile", HeaderValue::from_static("?0"));
h.insert("sec-ch-ua-platform", HeaderValue::from_static("\"Windows\""));
h.insert("sec-fetch-dest", HeaderValue::from_static("document"));
h.insert("sec-fetch-mode", HeaderValue::from_static("navigate"));
h.insert("sec-fetch-site", HeaderValue::from_static("none"));
h.insert("sec-fetch-user", HeaderValue::from_static("?1"));
h.insert(
header::UPGRADE_INSECURE_REQUESTS,
HeaderValue::from_static("1"),
);
if let Ok(v) = HeaderValue::from_str(ua) {
h.insert(header::USER_AGENT, v);
}
h
}