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;
9use crate::client::response::Response;
10use crate::error::{Error, Result};
11use crate::profile::{ChromeProfile, Platform};
12
13use bytes::Bytes;
14use cookie_store::CookieStore;
15use std::sync::RwLock;
16
17#[derive(Clone)]
32pub struct Client {
33 pool: Arc<Mutex<HashMap<String, QuikConnection>>>,
35 profile: ChromeProfile,
37 proxy: Option<Proxy>,
39 pub cookie_store: Arc<RwLock<CookieStore>>,
44}
45
46impl Default for Client {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl Client {
53 pub fn new() -> Self {
58 Self::builder().build().unwrap_or_else(|_| Client {
59 pool: Arc::new(Mutex::new(HashMap::new())),
60 profile: crate::profile::chrome_134::profile(Platform::MacOsArm),
61 proxy: None,
62 cookie_store: Arc::new(RwLock::new(CookieStore::default())),
63 })
64 }
65
66 pub fn builder() -> ClientBuilder {
68 ClientBuilder::default()
69 }
70
71 pub async fn get(&self, url: &str) -> Result<Response> {
73 self.execute_with_redirects("GET", url, None).await
74 }
75
76 pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
78 self.execute_with_redirects("POST", url, Some(body)).await
79 }
80
81 async fn execute_with_redirects(
94 &self,
95 initial_method: &str,
96 initial_url: &str,
97 initial_body: Option<Bytes>,
98 ) -> Result<Response> {
99 let mut current_url_str = initial_url.to_string();
100 let mut current_method = initial_method.to_string();
101 let mut current_body = initial_body;
102
103 let mut sec_fetch_site = "none".to_string();
104 let mut is_cross_site = false;
105
106 for hop in 0..10 {
107 let parsed_url =
108 Url::parse(¤t_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
109 let authority = parsed_url
110 .host_str()
111 .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
112 let port = parsed_url.port().unwrap_or(443);
113
114 let proxy_prefix = self
116 .proxy
117 .as_ref()
118 .map(|p| match p {
119 Proxy::Http(a) => format!("http://{}@", a),
120 Proxy::Socks5(a) => format!("socks5://{}@", a),
121 })
122 .unwrap_or_default();
123
124 let key = format!("{}{}:{}", proxy_prefix, authority, port);
125
126 let cookie_header = {
128 let store = self
129 .cookie_store
130 .read()
131 .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
132 let cookies: Vec<_> = store
133 .matches(&parsed_url)
134 .iter()
135 .map(|c| format!("{}={}", c.name(), c.value()))
136 .collect();
137 if cookies.is_empty() {
138 None
139 } else {
140 Some(cookies.join("; "))
141 }
142 };
143
144 let mut request = http::Request::builder()
145 .method(current_method.as_str())
146 .uri(parsed_url.as_str())
147 .body(())
148 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
149
150 if let Some(c) = cookie_header.as_deref() {
151 if let Ok(val) = http::header::HeaderValue::from_str(c) {
152 request.headers_mut().insert("cookie", val);
153 }
154 }
155
156 if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
159 if let Ok(val) =
160 http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
161 {
162 request.headers_mut().insert("origin", val);
163 }
164 }
165
166 let is_initial = hop == 0;
168 inject_chrome_headers(
169 request.headers_mut(),
170 &self.profile,
171 &sec_fetch_site,
172 is_initial,
173 );
174
175 let conn = {
177 let mut pool = self.pool.lock().map_err(|_| {
178 Error::Connect(std::io::Error::other("connection pool poisoned"))
179 })?;
180 pool.remove(&key)
181 };
182
183 let mut h2_client = if let Some(mut c) = conn {
184 match c.h2.ready().await {
186 Ok(h2) => {
187 c.h2 = h2;
188 c
189 }
190 Err(_) => self.dial(authority, port, &self.profile).await?,
191 }
192 } else {
193 self.dial(authority, port, &self.profile).await?
194 };
195
196 let mut response = h2_client.send(request, current_body.clone()).await?;
197
198 if let Ok(mut pool) = self.pool.lock() {
200 pool.insert(key, h2_client);
201 }
202
203 self.store_cookies(&response, &parsed_url);
204
205 let status = response.status();
206 if status.is_redirection() {
207 if let Some(location) = response.headers().get("location") {
208 let loc_str = location.to_str().unwrap_or("");
209 let next_url = parsed_url
210 .join(loc_str)
211 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
212
213 if status == http::StatusCode::MOVED_PERMANENTLY
215 || status == http::StatusCode::FOUND
216 || status == http::StatusCode::SEE_OTHER
217 {
218 current_method = "GET".to_string();
219 current_body = None;
220 }
221
222 if !is_cross_site {
224 if next_url.origin() == parsed_url.origin() {
225 sec_fetch_site = "same-origin".to_string();
226 } else if next_url.domain() == parsed_url.domain() {
227 sec_fetch_site = "same-site".to_string();
228 } else {
229 sec_fetch_site = "cross-site".to_string();
230 is_cross_site = true;
231 }
232 }
233
234 current_url_str = next_url.to_string();
235 continue;
236 }
237 }
238
239 response.set_url(current_url_str);
240 return Ok(response);
241 }
242
243 Err(Error::Connect(std::io::Error::other(
244 "Redirect limit exceeded (max 10)",
245 )))
246 }
247
248 async fn dial(
250 &self,
251 authority: &str,
252 port: u16,
253 profile: &ChromeProfile,
254 ) -> Result<QuikConnection> {
255 let addr_str = format!("{}:{}", authority, port);
256 let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
257 std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
258 })?;
259
260 connect(authority, port, addr, profile, self.proxy.as_ref()).await
261 }
262
263 fn store_cookies(&self, resp: &Response, url: &Url) {
265 if let Ok(mut store) = self.cookie_store.write() {
266 for v in resp.headers().get_all("set-cookie").iter() {
267 if let Ok(cookie_str) = v.to_str() {
268 let _ = store.parse(cookie_str, url);
269 }
270 }
271 }
272 }
273}
274
275#[derive(Default)]
277pub struct ClientBuilder {
278 profile: Option<ChromeProfile>,
279 proxy: Option<Proxy>,
280 cookie_store: Option<Arc<RwLock<CookieStore>>>,
281}
282
283impl ClientBuilder {
284 pub fn profile(mut self, profile: ChromeProfile) -> Self {
286 self.profile = Some(profile);
287 self
288 }
289
290 pub fn proxy(mut self, proxy: Proxy) -> Self {
292 self.proxy = Some(proxy);
293 self
294 }
295
296 pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
298 self.cookie_store = Some(store);
299 self
300 }
301
302 pub fn build(self) -> Result<Client> {
304 let profile = self
305 .profile
306 .unwrap_or_else(|| crate::profile::chrome_134::profile(Platform::MacOsArm));
307
308 Ok(Client {
309 pool: Arc::new(Mutex::new(HashMap::new())),
310 profile,
311 proxy: self.proxy,
312 cookie_store: self
313 .cookie_store
314 .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
315 })
316 }
317}