use super::SessionOptions;
use crate::Result;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BrowserProfile {
#[default]
None,
Chrome,
Firefox,
Safari,
Edge,
}
pub(crate) struct RawResponse {
pub status: u16,
pub headers: Vec<(String, String)>,
pub body: String,
}
pub(crate) enum HttpBackend {
Plain(reqwest::Client),
#[cfg(feature = "impersonate")]
Impersonate(Box<wreq::Client>),
}
impl HttpBackend {
pub(crate) fn build(opts: &SessionOptions) -> Result<Self> {
#[cfg(feature = "impersonate")]
if opts.profile != BrowserProfile::None {
return Ok(HttpBackend::Impersonate(Box::new(build_impersonate(opts)?)));
}
#[cfg(not(feature = "impersonate"))]
if opts.profile != BrowserProfile::None {
tracing::warn!(
"SessionOptions.profile 已设但未启用 `impersonate` feature → 回退纯 reqwest(无 TLS 指纹);\
如需生效请加 `--features impersonate`。"
);
}
Ok(HttpBackend::Plain(build_plain(opts)?))
}
pub(crate) async fn send_once(
&self,
method: &str,
url: &str,
headers: &[(String, String)],
body: Option<&str>,
) -> Result<RawResponse> {
match self {
HttpBackend::Plain(c) => {
let m = reqwest::Method::from_bytes(method.as_bytes())
.unwrap_or(reqwest::Method::GET);
let mut req = c.request(m, url);
for (k, v) in headers {
req = req.header(k.as_str(), v.as_str());
}
if let Some(b) = body {
req = req.body(b.to_string());
}
let resp = req.send().await?;
let status = resp.status().as_u16();
let headers = collect_headers_reqwest(resp.headers());
let body = resp.text().await.unwrap_or_default();
Ok(RawResponse {
status,
headers,
body,
})
}
#[cfg(feature = "impersonate")]
HttpBackend::Impersonate(c) => {
let m = wreq::Method::from_bytes(method.as_bytes()).unwrap_or(wreq::Method::GET);
let mut req = c.request(m, url);
for (k, v) in headers {
req = req.header(k.as_str(), v.as_str());
}
if let Some(b) = body {
req = req.body(b.to_string());
}
let resp = req.send().await?;
let status = resp.status().as_u16();
let headers = collect_headers_wreq(resp.headers());
let body = resp.text().await.unwrap_or_default();
Ok(RawResponse {
status,
headers,
body,
})
}
}
}
}
fn build_plain(opts: &SessionOptions) -> Result<reqwest::Client> {
let mut b = reqwest::Client::builder()
.user_agent(opts.user_agent.clone())
.timeout(opts.timeout)
.redirect(reqwest::redirect::Policy::none());
if opts.ignore_https_errors {
b = b.danger_accept_invalid_certs(true);
}
if let Some(p) = &opts.proxy {
let mut pr = reqwest::Proxy::all(&p.server)?;
if let (Some(u), Some(pw)) = (&p.username, &p.password) {
pr = pr.basic_auth(u, pw);
}
b = b.proxy(pr);
}
Ok(b.build()?)
}
fn collect_headers_reqwest(h: &reqwest::header::HeaderMap) -> Vec<(String, String)> {
h.iter()
.filter_map(|(k, v)| v.to_str().ok().map(|s| (k.as_str().to_string(), s.to_string())))
.collect()
}
#[cfg(feature = "impersonate")]
fn build_impersonate(opts: &SessionOptions) -> Result<wreq::Client> {
let mut b = wreq::Client::builder()
.emulation(profile_to_emulation(opts.profile))
.timeout(opts.timeout)
.redirect(wreq::redirect::Policy::none());
if opts.ignore_https_errors {
b = b.cert_verification(false);
}
if let Some(p) = &opts.proxy {
let mut pr = wreq::Proxy::all(&p.server)?;
if let (Some(u), Some(pw)) = (&p.username, &p.password) {
pr = pr.basic_auth(u, pw);
}
b = b.proxy(pr);
}
Ok(b.build()?)
}
#[cfg(feature = "impersonate")]
fn profile_to_emulation(p: BrowserProfile) -> wreq_util::Emulation {
use wreq_util::Emulation;
match p {
BrowserProfile::None | BrowserProfile::Chrome => Emulation::Chrome137,
BrowserProfile::Firefox => Emulation::Firefox139,
BrowserProfile::Safari => Emulation::Safari18_5,
BrowserProfile::Edge => Emulation::Edge134,
}
}
#[cfg(feature = "impersonate")]
fn collect_headers_wreq(h: &wreq::header::HeaderMap) -> Vec<(String, String)> {
h.iter()
.filter_map(|(k, v)| v.to_str().ok().map(|s| (k.as_str().to_string(), s.to_string())))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profile_default_is_none() {
assert_eq!(BrowserProfile::default(), BrowserProfile::None);
}
#[test]
fn profile_variants_distinct() {
let all = [
BrowserProfile::None,
BrowserProfile::Chrome,
BrowserProfile::Firefox,
BrowserProfile::Safari,
BrowserProfile::Edge,
];
for (i, a) in all.iter().enumerate() {
for b in &all[i + 1..] {
assert_ne!(a, b);
}
}
}
#[cfg(feature = "impersonate")]
#[test]
fn each_profile_maps_to_distinct_emulation() {
let chrome = profile_to_emulation(BrowserProfile::Chrome);
let firefox = profile_to_emulation(BrowserProfile::Firefox);
let safari = profile_to_emulation(BrowserProfile::Safari);
let edge = profile_to_emulation(BrowserProfile::Edge);
assert_ne!(chrome, firefox);
assert_ne!(chrome, safari);
assert_ne!(chrome, edge);
assert_ne!(firefox, safari);
}
}