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 let is_initial = hop == 0;
158 inject_chrome_headers(
159 request.headers_mut(),
160 &self.profile,
161 &sec_fetch_site,
162 is_initial,
163 );
164
165 let conn = {
167 let mut pool = self.pool.lock().map_err(|_| {
168 Error::Connect(std::io::Error::other("connection pool poisoned"))
169 })?;
170 pool.remove(&key)
171 };
172
173 let mut h2_client = if let Some(mut c) = conn {
174 match c.h2.ready().await {
176 Ok(h2) => {
177 c.h2 = h2;
178 c
179 }
180 Err(_) => self.dial(authority, port, &self.profile).await?,
181 }
182 } else {
183 self.dial(authority, port, &self.profile).await?
184 };
185
186 let mut response = h2_client.send(request, current_body.clone()).await?;
187
188 if let Ok(mut pool) = self.pool.lock() {
190 pool.insert(key, h2_client);
191 }
192
193 self.store_cookies(&response, &parsed_url);
194
195 let status = response.status();
196 if status.is_redirection() {
197 if let Some(location) = response.headers().get("location") {
198 let loc_str = location.to_str().unwrap_or("");
199 let next_url = parsed_url
200 .join(loc_str)
201 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
202
203 if status == http::StatusCode::MOVED_PERMANENTLY
205 || status == http::StatusCode::FOUND
206 || status == http::StatusCode::SEE_OTHER
207 {
208 current_method = "GET".to_string();
209 current_body = None;
210 }
211
212 if !is_cross_site {
214 if next_url.origin() == parsed_url.origin() {
215 sec_fetch_site = "same-origin".to_string();
216 } else if next_url.domain() == parsed_url.domain() {
217 sec_fetch_site = "same-site".to_string();
218 } else {
219 sec_fetch_site = "cross-site".to_string();
220 is_cross_site = true;
221 }
222 }
223
224 current_url_str = next_url.to_string();
225 continue;
226 }
227 }
228
229 response.set_url(current_url_str);
230 return Ok(response);
231 }
232
233 Err(Error::Connect(std::io::Error::other(
234 "Redirect limit exceeded (max 10)",
235 )))
236 }
237
238 async fn dial(
240 &self,
241 authority: &str,
242 port: u16,
243 profile: &ChromeProfile,
244 ) -> Result<QuikConnection> {
245 let addr_str = format!("{}:{}", authority, port);
246 let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
247 std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
248 })?;
249
250 connect(authority, port, addr, profile, self.proxy.as_ref()).await
251 }
252
253 fn store_cookies(&self, resp: &Response, url: &Url) {
255 if let Ok(mut store) = self.cookie_store.write() {
256 for v in resp.headers().get_all("set-cookie").iter() {
257 if let Ok(cookie_str) = v.to_str() {
258 let _ = store.parse(cookie_str, url);
259 }
260 }
261 }
262 }
263}
264
265#[derive(Default)]
267pub struct ClientBuilder {
268 profile: Option<ChromeProfile>,
269 proxy: Option<Proxy>,
270 cookie_store: Option<Arc<RwLock<CookieStore>>>,
271}
272
273impl ClientBuilder {
274 pub fn profile(mut self, profile: ChromeProfile) -> Self {
276 self.profile = Some(profile);
277 self
278 }
279
280 pub fn proxy(mut self, proxy: Proxy) -> Self {
282 self.proxy = Some(proxy);
283 self
284 }
285
286 pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
288 self.cookie_store = Some(store);
289 self
290 }
291
292 pub fn build(self) -> Result<Client> {
294 let profile = self
295 .profile
296 .unwrap_or_else(|| crate::profile::chrome_134::profile(Platform::MacOsArm));
297
298 Ok(Client {
299 pool: Arc::new(Mutex::new(HashMap::new())),
300 profile,
301 proxy: self.proxy,
302 cookie_store: self
303 .cookie_store
304 .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
305 })
306 }
307}