#![cfg(feature = "impersonate")]
use std::time::Instant;
use anyhow::{anyhow, Context, Result};
use reqwest::blocking::Response as ReqwestResponse;
use crate::cli::Args;
use crate::metrics::RequestMetrics;
pub fn is_active(args: &Args) -> bool {
args.impersonate.is_some()
|| args.ja3.is_some()
|| args.ja4.is_some()
|| args.http2_fingerprint.is_some()
}
pub fn validate_combination(args: &Args) -> Result<()> {
if args.ciphers.is_some() || args.tls13_ciphers.is_some() {
return Err(anyhow!(
"--ciphers / --tls13-ciphers cannot be combined with browser \
fingerprint impersonation: the profile owns the cipher list."
));
}
if args.tlsv12 || args.tlsv13 {
return Err(anyhow!(
"--tlsv1.2 / --tlsv1.3 cannot be combined with browser fingerprint \
impersonation: the profile owns the protocol version."
));
}
if args.client_cert.is_some() || args.client_key.is_some() {
return Err(anyhow!(
"--client-cert / --client-key (mutual auth) is not supported with \
browser fingerprint impersonation in v1 (deferred — see OUT-OF-SCOPE.md)."
));
}
if args.cacert.is_some() || args.capath.is_some() {
return Err(anyhow!(
"--cacert / --capath is not supported with browser fingerprint \
impersonation in v1 (system roots only — see OUT-OF-SCOPE.md)."
));
}
if args.ja3.is_some() && args.ja4.is_some() {
eprintln!(
"warning: both --ja3 and --ja4 set; JA4 will take precedence \
(they describe overlapping but different views of the ClientHello)."
);
}
Ok(())
}
fn parse_emulation(name: &str) -> Result<wreq_util::Emulation> {
let normalized = name.trim().replace('-', "_").to_ascii_lowercase();
let value = serde_json::Value::String(normalized.clone());
serde_json::from_value::<wreq_util::Emulation>(value).map_err(|e| {
anyhow!(
"unknown impersonate profile '{name}' (normalized to '{normalized}'). \
Valid examples: chrome_131, firefox_128, safari_17.5, edge_131, \
chrome_android_131, safari_ios_17.4.1, okhttp_5. \
See `recon --help impersonate` for the full list. ({e})"
)
})
}
async fn convert_response(resp: wreq::Response) -> Result<ReqwestResponse> {
let status = resp.status();
let headers = resp.headers().clone();
let version = resp.version();
let bytes = resp.bytes().await.map_err(|e| anyhow!("read body: {e}"))?;
let mut builder = http::Response::builder()
.status(status)
.version(version);
for (k, v) in headers.iter() {
builder = builder.header(k, v);
}
let http_resp = builder
.body(bytes.to_vec())
.map_err(|e| anyhow!("build http::Response: {e}"))?;
Ok(ReqwestResponse::from(http_resp))
}
pub fn execute(args: &Args) -> Result<(ReqwestResponse, RequestMetrics)> {
validate_combination(args)?;
if args.ja3.is_some() || args.ja4.is_some() || args.http2_fingerprint.is_some() {
return Err(anyhow!(
"--ja3 / --ja4 / --http2-fingerprint are not implemented yet. \
Use --impersonate <profile> for a named-profile fingerprint \
instead. See OUT-OF-SCOPE.md for the upstream-blockers rationale."
));
}
let profile_name = args.impersonate.as_deref().ok_or_else(|| {
anyhow!("impersonate::execute called without --impersonate (this is a bug)")
})?;
let emulation = parse_emulation(profile_name)?;
let mut metrics = RequestMetrics::default();
metrics.request_start = Some(Instant::now());
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.context("build tokio runtime for impersonate path")?;
let url = args.target_url().to_string();
let method_str = args.effective_method();
let timeout_secs = args.timeout;
let insecure = args.insecure;
let converted = runtime.block_on(async move {
let client = wreq::Client::builder()
.emulation(emulation)
.cert_verification(!insecure)
.connect_timeout(std::time::Duration::from_secs(timeout_secs))
.build()
.map_err(|e| anyhow!("wreq client build: {e}"))?;
let method = wreq::Method::from_bytes(method_str.as_bytes())
.map_err(|e| anyhow!("invalid HTTP method '{method_str}': {e}"))?;
let resp = client
.request(method, url)
.send()
.await
.map_err(|e| anyhow!("impersonate request: {e}"))?;
convert_response(resp).await
})?;
crate::client::snapshot_response_for_impersonate(&mut metrics, args, &converted);
metrics.url_effective = Some(args.target_url().to_string());
Ok((converted, metrics))
}