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