1use std::collections::HashMap;
2use std::net::ToSocketAddrs;
3use std::sync::{Arc, Mutex};
4use url::Url;
5
6use crate::client::connector::{connect, QuikConnection};
7use crate::client::proxy::Proxy;
8use crate::client::request::{inject_chrome_headers, RequestContext};
9use crate::client::response::Response;
10use crate::error::{Error, Result};
11use crate::profile::ChromeProfile;
12
13use bytes::Bytes;
14use cookie_store::CookieStore;
15use std::sync::RwLock;
16
17type SharedConnection = Arc<tokio::sync::Mutex<Option<QuikConnection>>>;
34type ConnectionPool = Arc<Mutex<HashMap<String, SharedConnection>>>;
35
36#[derive(Clone)]
37pub struct Client {
38 pool: ConnectionPool,
40 profile: ChromeProfile,
42 proxy: Option<Proxy>,
44 pub cookie_store: Arc<RwLock<CookieStore>>,
49 pub hint_cache: Arc<RwLock<std::collections::HashSet<String>>>,
51}
52
53impl Default for Client {
54 fn default() -> Self {
55 Self::new()
56 }
57}
58
59impl Client {
60 pub fn new() -> Self {
66 Self::builder().build().unwrap_or_else(|_| Client {
67 pool: Arc::new(Mutex::new(HashMap::new())),
68 profile: crate::profile::chrome_134::profile_auto(),
69 proxy: None,
70 cookie_store: Arc::new(RwLock::new(CookieStore::default())),
71 hint_cache: Arc::new(RwLock::new(std::collections::HashSet::new())),
72 })
73 }
74
75 pub fn builder() -> ClientBuilder {
77 ClientBuilder::default()
78 }
79
80 pub async fn get(&self, url: &str) -> Result<Response> {
82 self.execute_with_redirects("GET", url, None, RequestContext::Navigate).await
83 }
84
85 pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
87 self.execute_with_redirects("POST", url, Some(body), RequestContext::Navigate).await
88 }
89
90 async fn execute_with_redirects(
103 &self,
104 initial_method: &str,
105 initial_url: &str,
106 initial_body: Option<Bytes>,
107 context: RequestContext,
108 ) -> Result<Response> {
109 let mut current_url_str = initial_url.to_string();
110 let mut current_method = initial_method.to_string();
111 let mut current_body = initial_body;
112 let mut previous_url_str: Option<String> = None;
113
114 let mut sec_fetch_site = "none".to_string();
115 let mut is_cross_site = false;
116
117 for hop in 0..10 {
118 let parsed_url =
119 Url::parse(¤t_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
120 let authority = parsed_url
121 .host_str()
122 .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
123 let port = parsed_url.port().unwrap_or_else(|| {
124 if parsed_url.scheme() == "http" {
125 80
126 } else {
127 443
128 }
129 });
130
131 let proxy_prefix = self
133 .proxy
134 .as_ref()
135 .map(|p| match p {
136 Proxy::Http(a) => format!("http://{}@", a),
137 Proxy::Socks5(a) => format!("socks5://{}@", a),
138 })
139 .unwrap_or_default();
140
141 let key = format!("{}{}:{}", proxy_prefix, authority, port);
142
143 let cookie_header = {
145 let store = self
146 .cookie_store
147 .read()
148 .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
149 let cookies: Vec<_> = store
150 .matches(&parsed_url)
151 .iter()
152 .map(|c| format!("{}={}", c.name(), c.value()))
153 .collect();
154 if cookies.is_empty() {
155 None
156 } else {
157 Some(cookies.join("; "))
158 }
159 };
160
161 let mut request = http::Request::builder()
162 .method(current_method.as_str())
163 .uri(parsed_url.as_str())
164 .body(())
165 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
166
167 if let Some(c) = cookie_header.as_deref() {
168 if let Ok(val) = http::header::HeaderValue::from_str(c) {
169 request.headers_mut().insert("cookie", val);
170 }
171 }
172
173 if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
176 if let Ok(val) =
177 http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
178 {
179 request.headers_mut().insert("origin", val);
180 }
181 }
182
183 let is_initial = hop == 0;
185 let accept_ch = {
186 let cache = self.hint_cache.read().unwrap();
187 cache.contains(&parsed_url.origin().ascii_serialization())
188 };
189
190 let referer_to_send = previous_url_str.as_ref().map(|prev| {
192 if is_cross_site {
193 if let Ok(prev_url) = Url::parse(prev) {
195 return prev_url.origin().ascii_serialization() + "/";
196 }
197 }
198 prev.clone()
199 });
200
201 inject_chrome_headers(
202 request.headers_mut(),
203 &self.profile,
204 &sec_fetch_site,
205 is_initial,
206 context,
207 accept_ch,
208 referer_to_send.as_deref(),
209 );
210
211 let conn_mutex = {
214 let mut pool = self.pool.lock().map_err(|_| {
215 Error::Connect(std::io::Error::other("connection pool poisoned"))
216 })?;
217 pool.entry(key.clone())
218 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
219 .clone()
220 };
221
222 let mut h2_client = loop {
223 let conn_opt = {
224 let guard = conn_mutex.lock().await;
225 guard.as_ref().cloned()
226 };
227
228 if let Some(mut c) = conn_opt {
229 match c.h2.ready().await {
230 Ok(h2) => {
231 c.h2 = h2;
232 break c;
233 }
234 Err(_) => {
235 let mut guard = conn_mutex.lock().await;
236 *guard = None;
237 }
238 }
239 } else {
240 let mut guard = conn_mutex.lock().await;
241 if guard.is_none() {
242 let new_conn = self.dial(authority, port, &self.profile).await?;
243 *guard = Some(new_conn);
244 }
245 }
246 };
247
248 let mut response = h2_client.send(request, current_body.clone()).await?;
249
250 self.store_cookies(&response, &parsed_url);
251 self.store_hints(&response, &parsed_url);
252
253 let status = response.status();
254 if status.is_redirection() {
255 if let Some(location) = response.headers().get("location") {
256 let loc_str = location.to_str().unwrap_or("");
257 let next_url = parsed_url
258 .join(loc_str)
259 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
260
261 if status == http::StatusCode::MOVED_PERMANENTLY
263 || status == http::StatusCode::FOUND
264 || status == http::StatusCode::SEE_OTHER
265 {
266 current_method = "GET".to_string();
267 current_body = None;
268 }
269
270 if !is_cross_site {
272 if next_url.origin() == parsed_url.origin() {
273 sec_fetch_site = "same-origin".to_string();
274 } else if next_url.domain() == parsed_url.domain() {
275 sec_fetch_site = "same-site".to_string();
276 } else {
277 sec_fetch_site = "cross-site".to_string();
278 is_cross_site = true;
279 }
280 }
281
282 previous_url_str = Some(current_url_str);
283 current_url_str = next_url.to_string();
284 continue;
285 }
286 }
287
288 response.set_url(current_url_str);
289 return Ok(response);
290 }
291
292 Err(Error::Connect(std::io::Error::other(
293 "Redirect limit exceeded (max 10)",
294 )))
295 }
296
297 async fn dial(
299 &self,
300 authority: &str,
301 port: u16,
302 profile: &ChromeProfile,
303 ) -> Result<QuikConnection> {
304 let addr_str = format!("{}:{}", authority, port);
305 let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
306 std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
307 })?;
308
309 connect(authority, port, addr, profile, self.proxy.as_ref()).await
310 }
311
312 fn store_cookies(&self, resp: &Response, url: &Url) {
314 if let Ok(mut store) = self.cookie_store.write() {
315 for v in resp.headers().get_all("set-cookie").iter() {
316 if let Ok(cookie_str) = v.to_str() {
317 let _ = store.parse(cookie_str, url);
318 }
319 }
320 }
321 }
322
323 fn store_hints(&self, resp: &Response, url: &Url) {
325 if let Some(accept_ch) = resp.headers().get("accept-ch") {
326 if let Ok(ch_str) = accept_ch.to_str() {
327 if ch_str.to_lowercase().contains("sec-ch-ua-platform-version") {
328 if let Ok(mut cache) = self.hint_cache.write() {
329 cache.insert(url.origin().ascii_serialization());
330 }
331 }
332 }
333 }
334 }
335}
336
337#[derive(Default)]
339pub struct ClientBuilder {
340 profile: Option<ChromeProfile>,
341 proxy: Option<Proxy>,
342 cookie_store: Option<Arc<RwLock<CookieStore>>>,
343 danger_accept_invalid_certs: bool,
344}
345
346impl ClientBuilder {
347 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
353 self.danger_accept_invalid_certs = accept;
354 self
355 }
356
357 pub fn profile(mut self, profile: ChromeProfile) -> Self {
359 self.profile = Some(profile);
360 self
361 }
362
363 pub fn proxy(mut self, proxy: Proxy) -> Self {
365 self.proxy = Some(proxy);
366 self
367 }
368
369 pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
371 self.cookie_store = Some(store);
372 self
373 }
374
375 pub fn build(self) -> Result<Client> {
377 let mut profile = self
378 .profile
379 .unwrap_or_else(crate::profile::chrome_134::profile_auto);
380
381 if self.danger_accept_invalid_certs {
382 profile.tls.verify_peer = false;
383 }
384
385 Ok(Client {
386 pool: Arc::new(Mutex::new(HashMap::new())),
387 profile,
388 proxy: self.proxy,
389 cookie_store: self
390 .cookie_store
391 .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
392 hint_cache: Arc::new(RwLock::new(std::collections::HashSet::new())),
393 })
394 }
395}