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}
50
51impl Default for Client {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl Client {
58 pub fn new() -> Self {
64 Self::builder().build().unwrap_or_else(|_| Client {
65 pool: Arc::new(Mutex::new(HashMap::new())),
66 profile: crate::profile::chrome_134::profile_auto(),
67 proxy: None,
68 cookie_store: Arc::new(RwLock::new(CookieStore::default())),
69 })
70 }
71
72 pub fn builder() -> ClientBuilder {
74 ClientBuilder::default()
75 }
76
77 pub async fn get(&self, url: &str) -> Result<Response> {
79 self.execute_with_redirects("GET", url, None, RequestContext::Navigate).await
80 }
81
82 pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
84 self.execute_with_redirects("POST", url, Some(body), RequestContext::Navigate).await
85 }
86
87 async fn execute_with_redirects(
100 &self,
101 initial_method: &str,
102 initial_url: &str,
103 initial_body: Option<Bytes>,
104 context: RequestContext,
105 ) -> Result<Response> {
106 let mut current_url_str = initial_url.to_string();
107 let mut current_method = initial_method.to_string();
108 let mut current_body = initial_body;
109 let mut previous_url_str: Option<String> = None;
110
111 let mut sec_fetch_site = "none".to_string();
112 let mut is_cross_site = false;
113
114 for hop in 0..10 {
115 let parsed_url =
116 Url::parse(¤t_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
117 let authority = parsed_url
118 .host_str()
119 .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
120 let port = parsed_url.port().unwrap_or_else(|| {
121 if parsed_url.scheme() == "http" {
122 80
123 } else {
124 443
125 }
126 });
127
128 let proxy_prefix = self
130 .proxy
131 .as_ref()
132 .map(|p| match p {
133 Proxy::Http(a) => format!("http://{}@", a),
134 Proxy::Socks5(a) => format!("socks5://{}@", a),
135 })
136 .unwrap_or_default();
137
138 let key = format!("{}{}:{}", proxy_prefix, authority, port);
139
140 let cookie_header = {
142 let store = self
143 .cookie_store
144 .read()
145 .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
146 let cookies: Vec<_> = store
147 .matches(&parsed_url)
148 .iter()
149 .map(|c| format!("{}={}", c.name(), c.value()))
150 .collect();
151 if cookies.is_empty() {
152 None
153 } else {
154 Some(cookies.join("; "))
155 }
156 };
157
158 let mut request = http::Request::builder()
159 .method(current_method.as_str())
160 .uri(parsed_url.as_str())
161 .body(())
162 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
163
164 if let Some(c) = cookie_header.as_deref() {
165 if let Ok(val) = http::header::HeaderValue::from_str(c) {
166 request.headers_mut().insert("cookie", val);
167 }
168 }
169
170 if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
173 if let Ok(val) =
174 http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
175 {
176 request.headers_mut().insert("origin", val);
177 }
178 }
179
180 let is_initial = hop == 0;
182 let accept_ch = false;
184
185 inject_chrome_headers(
186 request.headers_mut(),
187 &self.profile,
188 &sec_fetch_site,
189 is_initial,
190 context,
191 accept_ch,
192 previous_url_str.as_deref(),
193 );
194
195 let conn_mutex = {
198 let mut pool = self.pool.lock().map_err(|_| {
199 Error::Connect(std::io::Error::other("connection pool poisoned"))
200 })?;
201 pool.entry(key.clone())
202 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
203 .clone()
204 };
205
206 let mut h2_client = {
207 let mut guard = conn_mutex.lock().await;
208
209 let need_dial = match &mut *guard {
210 Some(c) => {
211 match c.h2.clone().ready().await {
213 Ok(h2) => {
214 c.h2 = h2;
215 false
216 }
217 Err(_) => true,
218 }
219 }
220 None => true,
221 };
222
223 if need_dial {
224 let new_conn = self.dial(authority, port, &self.profile).await?;
225 *guard = Some(new_conn);
226 }
227
228 guard.as_ref().unwrap().clone()
229 };
230
231 let mut response = h2_client.send(request, current_body.clone()).await?;
232
233 self.store_cookies(&response, &parsed_url);
234
235 let status = response.status();
236 if status.is_redirection() {
237 if let Some(location) = response.headers().get("location") {
238 let loc_str = location.to_str().unwrap_or("");
239 let next_url = parsed_url
240 .join(loc_str)
241 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
242
243 if status == http::StatusCode::MOVED_PERMANENTLY
245 || status == http::StatusCode::FOUND
246 || status == http::StatusCode::SEE_OTHER
247 {
248 current_method = "GET".to_string();
249 current_body = None;
250 }
251
252 if !is_cross_site {
254 if next_url.origin() == parsed_url.origin() {
255 sec_fetch_site = "same-origin".to_string();
256 } else if next_url.domain() == parsed_url.domain() {
257 sec_fetch_site = "same-site".to_string();
258 } else {
259 sec_fetch_site = "cross-site".to_string();
260 is_cross_site = true;
261 }
262 }
263
264 previous_url_str = Some(current_url_str);
265 current_url_str = next_url.to_string();
266 continue;
267 }
268 }
269
270 response.set_url(current_url_str);
271 return Ok(response);
272 }
273
274 Err(Error::Connect(std::io::Error::other(
275 "Redirect limit exceeded (max 10)",
276 )))
277 }
278
279 async fn dial(
281 &self,
282 authority: &str,
283 port: u16,
284 profile: &ChromeProfile,
285 ) -> Result<QuikConnection> {
286 let addr_str = format!("{}:{}", authority, port);
287 let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
288 std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
289 })?;
290
291 connect(authority, port, addr, profile, self.proxy.as_ref()).await
292 }
293
294 fn store_cookies(&self, resp: &Response, url: &Url) {
296 if let Ok(mut store) = self.cookie_store.write() {
297 for v in resp.headers().get_all("set-cookie").iter() {
298 if let Ok(cookie_str) = v.to_str() {
299 let _ = store.parse(cookie_str, url);
300 }
301 }
302 }
303 }
304}
305
306#[derive(Default)]
308pub struct ClientBuilder {
309 profile: Option<ChromeProfile>,
310 proxy: Option<Proxy>,
311 cookie_store: Option<Arc<RwLock<CookieStore>>>,
312 danger_accept_invalid_certs: bool,
313}
314
315impl ClientBuilder {
316 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
322 self.danger_accept_invalid_certs = accept;
323 self
324 }
325
326 pub fn profile(mut self, profile: ChromeProfile) -> Self {
328 self.profile = Some(profile);
329 self
330 }
331
332 pub fn proxy(mut self, proxy: Proxy) -> Self {
334 self.proxy = Some(proxy);
335 self
336 }
337
338 pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
340 self.cookie_store = Some(store);
341 self
342 }
343
344 pub fn build(self) -> Result<Client> {
346 let mut profile = self
347 .profile
348 .unwrap_or_else(crate::profile::chrome_134::profile_auto);
349
350 if self.danger_accept_invalid_certs {
351 profile.tls.verify_peer = false;
352 }
353
354 Ok(Client {
355 pool: Arc::new(Mutex::new(HashMap::new())),
356 profile,
357 proxy: self.proxy,
358 cookie_store: self
359 .cookie_store
360 .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
361 })
362 }
363}