stealth: route scripted fetch()/XHR through the wreq client
op_fetch_url backed JS fetch() and XHR with a plain reqwest/rustls client, so in stealth mode scripted requests carried a non-browser TLS fingerprint and none of the Chrome client hints the main navigation sends. Bot managers that score subresource requests (the AWS WAF challenge verify call, Akamai sensors) read that as a script and reject it. Add StealthHttpClient::send_single and a stealth_fetch_all path that op_fetch_url uses when a stealth client is installed, preserving the redirect, SSRF, and CORS handling so JS-level requests share the navigation's Chrome TLS fingerprint and sec-ch-ua client hints. The reqwest path is unchanged for non-stealth. Adds a stealth feature to obscura-js gated on obscura-net/stealth.
This commit is contained in:
1 parent
4309935abd
commit
2e0981ef3f
6 files changed
+249
-1
No files matched your search
@@ -154,6 +154,85 @@ impl StealthHttpClient {
|
||||
Err(ObscuraNetError::TooManyRedirects(url.to_string()))
|
||||
}
|
||||
|
||||
/// One request with no redirect following, for scripted fetch()/XHR. Reads
|
||||
/// the cookie jar for the Cookie header and stores Set-Cookie back into it,
|
||||
/// so the caller only owns redirect hops and SSRF re-validation. Used in
|
||||
/// stealth mode so JS-level requests carry the same Chrome TLS fingerprint
|
||||
/// and client hints as the main navigation instead of the rustls ClientHello
|
||||
/// that op_fetch_url would otherwise send (which bot managers read as a
|
||||
/// non-browser script and reject, e.g. the AWS WAF challenge verify call).
|
||||
pub async fn send_single(
|
||||
&self,
|
||||
method: &str,
|
||||
url: &Url,
|
||||
headers: &HashMap<String, String>,
|
||||
body: &str,
|
||||
) -> Result<Response, ObscuraNetError> {
|
||||
if let Some(host) = url.host_str() {
|
||||
if crate::blocklist::is_blocked(host) {
|
||||
tracing::debug!("Blocked tracker: {}", url);
|
||||
return Ok(Response {
|
||||
status: 0,
|
||||
url: url.clone(),
|
||||
headers: HashMap::new(),
|
||||
body: Vec::new(),
|
||||
redirected_from: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let req_method = method
|
||||
.parse::<wreq::Method>()
|
||||
.map_err(|e| ObscuraNetError::Network(format!("invalid method '{}': {}", method, e)))?;
|
||||
let mut req = self.client.request(req_method, url.as_str());
|
||||
|
||||
let cookie_header = self.cookie_jar.get_cookie_header(url);
|
||||
if !cookie_header.is_empty() {
|
||||
req = req.header("cookie", &cookie_header);
|
||||
}
|
||||
for (k, v) in self.extra_headers.read().await.iter() {
|
||||
req = req.header(k.as_str(), v.as_str());
|
||||
}
|
||||
for (k, v) in headers.iter() {
|
||||
req = req.header(k.as_str(), v.as_str());
|
||||
}
|
||||
if !body.is_empty() {
|
||||
req = req.body(body.to_string());
|
||||
}
|
||||
|
||||
self.in_flight.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let resp = req.send().await.map_err(|e| {
|
||||
self.in_flight.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
|
||||
ObscuraNetError::Network(format!("{}: {}", url, e))
|
||||
})?;
|
||||
self.in_flight.fetch_sub(1, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
let status = resp.status();
|
||||
for val in resp.headers().get_all("set-cookie") {
|
||||
if let Ok(s) = val.to_str() {
|
||||
self.cookie_jar.set_cookie(s, url);
|
||||
}
|
||||
}
|
||||
let response_headers: HashMap<String, String> = resp
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str().to_lowercase(), v.to_str().unwrap_or("").to_string()))
|
||||
.collect();
|
||||
let resp_body = resp
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| ObscuraNetError::Network(format!("Failed to read body: {}", e)))?
|
||||
.to_vec();
|
||||
|
||||
Ok(Response {
|
||||
url: url.clone(),
|
||||
status: status.as_u16(),
|
||||
headers: response_headers,
|
||||
body: resp_body,
|
||||
redirected_from: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn set_extra_headers(&self, headers: HashMap<String, String>) {
|
||||
*self.extra_headers.write().await = headers;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user