Skip to main content

clawser_browser/
client.rs

1//! Lightweight HTTP client that impersonates Chrome 134 at the TLS/HTTP2 level.
2//!
3//! Uses wreq (BoringSSL) to produce identical JA3/JA4 and HTTP/2 fingerprints
4//! as the real Chromium binary. ~50-100x faster than launching a browser.
5//!
6//! ```rust,no_run
7//! use clawser_browser::HttpClient;
8//!
9//! #[tokio::main]
10//! async fn main() -> clawser_browser::Result<()> {
11//!     let client = HttpClient::new()?;
12//!     let resp = client.get("https://example.com").send().await?;
13//!     println!("{}", resp.text().await?);
14//!     Ok(())
15//! }
16//! ```
17
18use crate::profile::generate_config_json;
19use crate::Result;
20use wreq_util::{Emulation, EmulationOS, EmulationOption};
21
22/// HTTP client impersonating Chrome 134 at TLS + HTTP/2 level.
23///
24/// Produces identical JA3/JA4, HTTP/2 SETTINGS, pseudo-header ordering,
25/// and default headers as a real Chrome 134 browser on Windows.
26pub struct HttpClient {
27    inner: wreq::Client,
28    ua: String,
29}
30
31impl HttpClient {
32    /// Create a client impersonating Chrome 134 on Windows with default settings.
33    pub fn new() -> Result<Self> {
34        Self::builder().build()
35    }
36
37    /// Start building a custom client.
38    pub fn builder() -> HttpClientBuilder {
39        HttpClientBuilder::default()
40    }
41
42    // ── Request methods ────────────────────────────────────────
43
44    /// Start a GET request.
45    pub fn get(&self, url: &str) -> wreq::RequestBuilder {
46        self.inner.get(url)
47    }
48
49    /// Start a POST request.
50    pub fn post(&self, url: &str) -> wreq::RequestBuilder {
51        self.inner.post(url)
52    }
53
54    /// Start a PUT request.
55    pub fn put(&self, url: &str) -> wreq::RequestBuilder {
56        self.inner.put(url)
57    }
58
59    /// Start a DELETE request.
60    pub fn delete(&self, url: &str) -> wreq::RequestBuilder {
61        self.inner.delete(url)
62    }
63
64    /// Start a PATCH request.
65    pub fn patch(&self, url: &str) -> wreq::RequestBuilder {
66        self.inner.patch(url)
67    }
68
69    /// Start a HEAD request.
70    pub fn head(&self, url: &str) -> wreq::RequestBuilder {
71        self.inner.head(url)
72    }
73
74    /// Build a request with any HTTP method.
75    pub fn request(&self, method: wreq::Method, url: &str) -> wreq::RequestBuilder {
76        self.inner.request(method, url)
77    }
78
79    // ── Accessors ──────────────────────────────────────────────
80
81    /// User-Agent string this client sends.
82    pub fn user_agent(&self) -> &str {
83        &self.ua
84    }
85
86    /// Access the underlying wreq::Client for advanced usage.
87    pub fn wreq(&self) -> &wreq::Client {
88        &self.inner
89    }
90}
91
92// ── Builder ────────────────────────────────────────────────────
93
94/// Builder for [`HttpClient`].
95pub struct HttpClientBuilder {
96    profile: Option<(usize, u64)>,
97    proxy: Option<String>,
98    timeout_secs: u64,
99    cookie_store: bool,
100}
101
102impl Default for HttpClientBuilder {
103    fn default() -> Self {
104        Self {
105            profile: None,
106            proxy: None,
107            timeout_secs: 30,
108            cookie_store: true,
109        }
110    }
111}
112
113impl HttpClientBuilder {
114    /// Use a specific hardware profile (same index/seed as Browser::builder().profile()).
115    /// Extracts matching User-Agent and Accept-Language from the profile config.
116    pub fn profile(mut self, index: usize, seed: u64) -> Self {
117        self.profile = Some((index, seed));
118        self
119    }
120
121    /// Set an HTTP/HTTPS/SOCKS5 proxy.
122    /// Examples: `"http://proxy:8080"`, `"socks5://user:pass@proxy:1080"`
123    pub fn proxy(mut self, proxy: impl Into<String>) -> Self {
124        self.proxy = Some(proxy.into());
125        self
126    }
127
128    /// Request timeout in seconds (default: 30).
129    pub fn timeout(mut self, secs: u64) -> Self {
130        self.timeout_secs = secs;
131        self
132    }
133
134    /// Enable/disable cookie jar (default: true).
135    pub fn cookie_store(mut self, enable: bool) -> Self {
136        self.cookie_store = enable;
137        self
138    }
139
140    /// Build the client.
141    pub fn build(self) -> Result<HttpClient> {
142        // Extract UA and languages from profile config if provided
143        let (ua, languages) = if let Some((index, seed)) = self.profile {
144            let json = generate_config_json(index, seed);
145            let config: serde_json::Value = serde_json::from_str(&json)?;
146            let ua = config["navigator"]["user_agent"]
147                .as_str()
148                .unwrap_or(DEFAULT_UA)
149                .to_string();
150            let langs: Vec<String> = config["navigator"]["languages"]
151                .as_array()
152                .map(|arr| {
153                    arr.iter()
154                        .filter_map(|v| v.as_str().map(String::from))
155                        .collect()
156                })
157                .unwrap_or_else(|| vec!["en-US".into(), "en".into()]);
158            (ua, langs.join(", "))
159        } else {
160            (DEFAULT_UA.to_string(), "en-US, en".to_string())
161        };
162
163        // Chrome 134 Windows emulation — matches our Chromium binary's TLS/HTTP2
164        let emulation = EmulationOption::builder()
165            .emulation(Emulation::Chrome134)
166            .emulation_os(EmulationOS::Windows)
167            .skip_headers(true) // we set our own headers
168            .build();
169
170        let mut cb = wreq::Client::builder()
171            .emulation(emulation)
172            .cookie_store(self.cookie_store)
173            .user_agent(&ua)
174            .default_headers(build_chrome_headers(&ua, &languages))
175            .timeout(std::time::Duration::from_secs(self.timeout_secs));
176
177        if let Some(ref proxy_url) = self.proxy {
178            cb = cb.proxy(wreq::Proxy::all(proxy_url)?);
179        }
180
181        let client = cb.build()?;
182
183        Ok(HttpClient { inner: client, ua })
184    }
185}
186
187// ── Internals ──────────────────────────────────────────────────
188
189const 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";
190
191const SEC_CH_UA: &str = r#""Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134""#;
192
193/// Build default headers matching a real Chrome 134 request.
194fn build_chrome_headers(ua: &str, languages: &str) -> wreq::header::HeaderMap {
195    use wreq::header::{self, HeaderMap, HeaderValue};
196
197    let mut h = HeaderMap::new();
198
199    // Order matters — Chrome sends these in a specific sequence
200    h.insert(
201        header::ACCEPT,
202        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"),
203    );
204    h.insert(
205        header::ACCEPT_ENCODING,
206        HeaderValue::from_static("gzip, deflate, br, zstd"),
207    );
208    if let Ok(v) = HeaderValue::from_str(languages) {
209        h.insert(header::ACCEPT_LANGUAGE, v);
210    }
211    h.insert("sec-ch-ua", HeaderValue::from_static(SEC_CH_UA));
212    h.insert("sec-ch-ua-mobile", HeaderValue::from_static("?0"));
213    h.insert("sec-ch-ua-platform", HeaderValue::from_static("\"Windows\""));
214    h.insert("sec-fetch-dest", HeaderValue::from_static("document"));
215    h.insert("sec-fetch-mode", HeaderValue::from_static("navigate"));
216    h.insert("sec-fetch-site", HeaderValue::from_static("none"));
217    h.insert("sec-fetch-user", HeaderValue::from_static("?1"));
218    h.insert(
219        header::UPGRADE_INSECURE_REQUESTS,
220        HeaderValue::from_static("1"),
221    );
222    if let Ok(v) = HeaderValue::from_str(ua) {
223        h.insert(header::USER_AGENT, v);
224    }
225
226    h
227}