1use bytes::Bytes;
2use cookie_store::CookieStore;
3use std::collections::{HashMap, HashSet};
4use std::net::{SocketAddr, ToSocketAddrs};
5use std::sync::{Arc, Mutex, RwLock};
6use tokio::net::UdpSocket;
7use tokio::sync::mpsc;
8use url::Url;
9
10use crate::client::connector::{connect, QuikConnection};
11use crate::client::proxy::Proxy;
12use crate::client::quic::QuicSession;
13use crate::client::request::{inject_chrome_headers, RequestContext};
14use crate::client::response::Response;
15use crate::error::{Error, Result};
16use crate::profile::ChromeProfile;
17
18#[derive(Clone)]
28pub struct AltSvcCache {
29 entries: Arc<RwLock<HashMap<String, String>>>,
30}
31
32impl AltSvcCache {
33 pub fn new() -> Self {
35 Self {
36 entries: Arc::new(RwLock::new(HashMap::new())),
37 }
38 }
39
40 pub fn get(&self, origin: &str) -> Option<String> {
42 let guard = self.entries.read().ok()?;
43 guard.get(origin).cloned()
44 }
45
46 pub fn insert(&self, origin: String, alt_svc: String) {
48 if let Ok(mut guard) = self.entries.write() {
49 guard.insert(origin, alt_svc);
50 }
51 }
52
53 pub fn remove(&self, origin: &str) {
60 if let Ok(mut guard) = self.entries.write() {
61 guard.remove(origin);
62 }
63 }
64}
65
66#[derive(Clone)]
74pub enum PooledConnection {
75 Http2(QuikConnection),
77 Http3(QuicSession),
79}
80
81impl PooledConnection {
82 pub async fn send(
84 &mut self,
85 request: http::Request<()>,
86 body: Option<Bytes>,
87 ) -> Result<Response> {
88 match self {
89 PooledConnection::Http2(conn) => conn.send(request, body).await,
90 PooledConnection::Http3(conn) => conn.send(request, body).await,
91 }
92 }
93}
94
95type SharedConnection = Arc<tokio::sync::Mutex<Option<PooledConnection>>>;
96type ConnectionPool = Arc<Mutex<HashMap<String, SharedConnection>>>;
97
98#[derive(Clone)]
108pub struct Client {
109 pool: ConnectionPool,
111 profile: ChromeProfile,
113 proxy: Option<Proxy>,
115 pub cookie_store: Arc<RwLock<CookieStore>>,
117 pub hint_cache: Arc<RwLock<HashSet<String>>>,
119 pub alt_svc_cache: AltSvcCache,
121}
122
123impl Default for Client {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl Client {
130 pub fn new() -> Self {
132 Self::builder().build().unwrap_or_else(|_| Client {
133 pool: Arc::new(Mutex::new(HashMap::new())),
134 profile: crate::profile::chrome_134::profile_auto(),
135 proxy: None,
136 cookie_store: Arc::new(RwLock::new(CookieStore::default())),
137 hint_cache: Arc::new(RwLock::new(HashSet::new())),
138 alt_svc_cache: AltSvcCache::new(),
139 })
140 }
141
142 pub fn builder() -> ClientBuilder {
144 ClientBuilder::default()
145 }
146
147 pub async fn get(&self, url: &str) -> Result<Response> {
149 self.execute_with_redirects("GET", url, None, RequestContext::Navigate)
150 .await
151 }
152
153 pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
155 self.execute_with_redirects("POST", url, Some(body), RequestContext::Navigate)
156 .await
157 }
158
159 async fn execute_with_redirects(
173 &self,
174 initial_method: &str,
175 initial_url: &str,
176 initial_body: Option<Bytes>,
177 context: RequestContext,
178 ) -> Result<Response> {
179 let mut current_url_str = initial_url.to_string();
180 let mut current_method = initial_method.to_string();
181 let mut current_body = initial_body;
182 let mut previous_url_str: Option<String> = None;
183
184 let mut sec_fetch_site = "none".to_string();
185 let mut is_cross_site = false;
186
187 for hop in 0..10 {
188 let parsed_url =
189 Url::parse(¤t_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
190 let authority = parsed_url
191 .host_str()
192 .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
193 let port = parsed_url.port().unwrap_or_else(|| {
194 if parsed_url.scheme() == "http" {
195 80
196 } else {
197 443
198 }
199 });
200
201 let proxy_prefix = self
203 .proxy
204 .as_ref()
205 .map(|p| match p {
206 Proxy::Http(a) => format!("http://{}@", a),
207 Proxy::Socks5(a) => format!("socks5://{}@", a),
208 })
209 .unwrap_or_default();
210
211 let origin_key = format!("{}:{}", authority, port);
213 let mut has_alt_svc = self.alt_svc_cache.get(&origin_key).is_some();
214 let transport_proto = if has_alt_svc { "h3" } else { "h2" };
215 let pool_key = format!("{}{}:{}#{}", proxy_prefix, authority, port, transport_proto);
216
217 let cookie_header = {
219 let store = self
220 .cookie_store
221 .read()
222 .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
223 let cookies: Vec<_> = store
224 .matches(&parsed_url)
225 .iter()
226 .map(|c| format!("{}={}", c.name(), c.value()))
227 .collect();
228 if cookies.is_empty() {
229 None
230 } else {
231 Some(cookies.join("; "))
232 }
233 };
234
235 let is_initial = hop == 0;
237 let accept_ch = {
238 let cache = self.hint_cache.read().unwrap();
239 cache.contains(&parsed_url.origin().ascii_serialization())
240 };
241
242 let referer_to_send = previous_url_str.as_ref().map(|prev| {
244 if is_cross_site {
245 if let Ok(prev_url) = Url::parse(prev) {
246 return prev_url.origin().ascii_serialization() + "/";
247 }
248 }
249 prev.clone()
250 });
251
252 let conn_mutex = {
255 let mut pool = self.pool.lock().map_err(|_| {
256 Error::Connect(std::io::Error::other("connection pool poisoned"))
257 })?;
258 pool.entry(pool_key.clone())
259 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
260 .clone()
261 };
262
263 let mut pooled_client = loop {
264 let conn_opt = {
265 let guard = conn_mutex.lock().await;
266 guard.as_ref().cloned()
267 };
268
269 if let Some(c) = conn_opt {
270 match c {
271 PooledConnection::Http2(mut conn) => {
272 match conn.h2.ready().await {
274 Ok(h2) => {
275 conn.h2 = h2;
276 break PooledConnection::Http2(conn);
277 }
278 Err(_) => {
279 let mut guard = conn_mutex.lock().await;
280 *guard = None;
281 }
282 }
283 }
284 PooledConnection::Http3(conn) => {
285 break PooledConnection::Http3(conn);
288 }
289 }
290 } else {
291 let mut guard = conn_mutex.lock().await;
292 if guard.is_none() {
293 match self.dial(authority, port, has_alt_svc, &self.profile).await {
295 Ok(new_conn) => {
296 *guard = Some(new_conn.clone());
297 break new_conn;
298 }
299 Err(e) => {
300 if has_alt_svc {
301 tracing::warn!("HTTP/3 dial to {} failed ({:?}); falling back to HTTP/2/TCP.", origin_key, e);
303 self.alt_svc_cache.remove(&origin_key);
304 has_alt_svc = false;
305
306 let h2_pool_key =
308 format!("{}{}:{}#h2", proxy_prefix, authority, port);
309 let h2_conn_mutex = {
310 let mut pool = self.pool.lock().map_err(|_| {
311 Error::Connect(std::io::Error::other(
312 "connection pool poisoned",
313 ))
314 })?;
315 pool.entry(h2_pool_key)
316 .or_insert_with(|| {
317 Arc::new(tokio::sync::Mutex::new(None))
318 })
319 .clone()
320 };
321
322 let mut h2_guard = h2_conn_mutex.lock().await;
323 if h2_guard.is_none() {
324 let h2_conn = self
325 .dial(authority, port, false, &self.profile)
326 .await?;
327 *h2_guard = Some(h2_conn.clone());
328 break h2_conn;
329 } else {
330 break h2_guard.as_ref().unwrap().clone();
331 }
332 } else {
333 return Err(e);
334 }
335 }
336 }
337 }
338 }
339 };
340
341 let mut request = http::Request::builder()
343 .method(current_method.as_str())
344 .uri(parsed_url.as_str())
345 .body(())
346 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
347
348 if let Some(c) = cookie_header.as_deref() {
349 if let Ok(val) = http::header::HeaderValue::from_str(c) {
350 request.headers_mut().insert("cookie", val);
351 }
352 }
353
354 if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
355 if let Ok(val) =
356 http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
357 {
358 request.headers_mut().insert("origin", val);
359 }
360 }
361
362 inject_chrome_headers(
363 request.headers_mut(),
364 &self.profile,
365 &sec_fetch_site,
366 is_initial,
367 context,
368 accept_ch,
369 referer_to_send.as_deref(),
370 );
371
372 let mut response = match pooled_client.send(request, current_body.clone()).await {
374 Ok(resp) => resp,
375 Err(e) => {
376 if let PooledConnection::Http3(_) = pooled_client {
377 tracing::warn!("HTTP/3 request transmission failed ({:?}); falling back to HTTP/2/TCP.", e);
378 self.alt_svc_cache.remove(&origin_key);
379
380 let h2_pool_key = format!("{}{}:{}#h2", proxy_prefix, authority, port);
382 let h2_conn_mutex = {
383 let mut pool = self.pool.lock().map_err(|_| {
384 Error::Connect(std::io::Error::other("connection pool poisoned"))
385 })?;
386 pool.entry(h2_pool_key)
387 .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
388 .clone()
389 };
390
391 let mut h2_guard = h2_conn_mutex.lock().await;
392 let h2_conn = if let Some(PooledConnection::Http2(mut conn)) =
393 h2_guard.as_ref().cloned()
394 {
395 match conn.h2.ready().await {
396 Ok(h2) => {
397 conn.h2 = h2;
398 *h2_guard = Some(PooledConnection::Http2(conn.clone()));
399 PooledConnection::Http2(conn)
400 }
401 Err(_) => {
402 let new_conn =
403 self.dial(authority, port, false, &self.profile).await?;
404 *h2_guard = Some(new_conn.clone());
405 new_conn
406 }
407 }
408 } else {
409 let new_conn = self.dial(authority, port, false, &self.profile).await?;
410 *h2_guard = Some(new_conn.clone());
411 new_conn
412 };
413
414 let mut fallback_request = http::Request::builder()
416 .method(current_method.as_str())
417 .uri(parsed_url.as_str())
418 .body(())
419 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
420
421 if let Some(c) = cookie_header.as_deref() {
422 if let Ok(val) = http::header::HeaderValue::from_str(c) {
423 fallback_request.headers_mut().insert("cookie", val);
424 }
425 }
426 if current_method == "POST"
427 || current_method == "PUT"
428 || current_method == "PATCH"
429 {
430 if let Ok(val) = http::header::HeaderValue::from_str(
431 &parsed_url.origin().ascii_serialization(),
432 ) {
433 fallback_request.headers_mut().insert("origin", val);
434 }
435 }
436
437 inject_chrome_headers(
438 fallback_request.headers_mut(),
439 &self.profile,
440 &sec_fetch_site,
441 is_initial,
442 context,
443 accept_ch,
444 referer_to_send.as_deref(),
445 );
446
447 let mut h2_pooled = h2_conn;
448 h2_pooled
449 .send(fallback_request, current_body.clone())
450 .await?
451 } else {
452 return Err(e);
453 }
454 }
455 };
456
457 self.store_cookies(&response, &parsed_url);
459 self.store_hints(&response, &parsed_url);
460 self.store_alt_svc(&response, &parsed_url);
461
462 let status = response.status();
463 if status.is_redirection() {
464 if let Some(location) = response.headers().get("location") {
465 let loc_str = location.to_str().unwrap_or("");
466 let next_url = parsed_url
467 .join(loc_str)
468 .map_err(|e| Error::InvalidUrl(e.to_string()))?;
469
470 if status == http::StatusCode::MOVED_PERMANENTLY
472 || status == http::StatusCode::FOUND
473 || status == http::StatusCode::SEE_OTHER
474 {
475 current_method = "GET".to_string();
476 current_body = None;
477 }
478
479 if !is_cross_site {
480 if next_url.origin() == parsed_url.origin() {
481 sec_fetch_site = "same-origin".to_string();
482 } else if next_url.domain() == parsed_url.domain() {
483 sec_fetch_site = "same-site".to_string();
484 } else {
485 sec_fetch_site = "cross-site".to_string();
486 is_cross_site = true;
487 }
488 }
489
490 previous_url_str = Some(current_url_str);
491 current_url_str = next_url.to_string();
492 continue;
493 }
494 }
495
496 response.set_url(current_url_str);
497 return Ok(response);
498 }
499
500 Err(Error::Connect(std::io::Error::other(
501 "Redirect limit exceeded (max 10)",
502 )))
503 }
504
505 async fn dial(
514 &self,
515 authority: &str,
516 port: u16,
517 dial_h3: bool,
518 profile: &ChromeProfile,
519 ) -> Result<PooledConnection> {
520 if dial_h3 {
521 let addr_str = format!("{}:{}", authority, port);
522 let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
523 std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
524 })?;
525
526 let local_addr: SocketAddr = if addr.is_ipv6() {
528 "[::]:0".parse().unwrap()
529 } else {
530 "0.0.0.0:0".parse().unwrap()
531 };
532
533 let socket = UdpSocket::bind(local_addr).await?;
534 socket.connect(addr).await?;
535
536 let mut config = crate::client::quic::configure_chrome_quic_transport()?;
537 if !profile.tls.verify_peer {
538 config.verify_peer(false);
539 }
540
541 let scid = quiche::ConnectionId::from_ref(&[]);
543 let conn = quiche::connect(Some(authority), &scid, local_addr, addr, &mut config)
544 .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
545
546 let (cmd_tx, cmd_rx) = mpsc::channel(100);
547 let socket_arc = Arc::new(socket);
548
549 tokio::spawn(crate::client::quic::run_quic_driver(
550 socket_arc, conn, addr, cmd_rx,
551 ));
552
553 Ok(PooledConnection::Http3(QuicSession {
554 tx: cmd_tx,
555 profile: profile.clone(),
556 }))
557 } else {
558 let addr_str = format!("{}:{}", authority, port);
559 let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
560 std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
561 })?;
562
563 let conn = connect(authority, port, addr, profile, self.proxy.as_ref()).await?;
564 Ok(PooledConnection::Http2(conn))
565 }
566 }
567
568 fn store_cookies(&self, resp: &Response, url: &Url) {
570 if let Ok(mut store) = self.cookie_store.write() {
571 for v in resp.headers().get_all("set-cookie").iter() {
572 if let Ok(cookie_str) = v.to_str() {
573 let _ = store.parse(cookie_str, url);
574 }
575 }
576 }
577 }
578
579 fn store_hints(&self, resp: &Response, url: &Url) {
581 if let Some(accept_ch) = resp.headers().get("accept-ch") {
582 if let Ok(ch_str) = accept_ch.to_str() {
583 if ch_str.to_lowercase().contains("sec-ch-ua-platform-version") {
584 if let Ok(mut cache) = self.hint_cache.write() {
585 cache.insert(url.origin().ascii_serialization());
586 }
587 }
588 }
589 }
590 }
591
592 fn store_alt_svc(&self, resp: &Response, url: &Url) {
594 if let Some(alt_svc) = resp.headers().get("alt-svc") {
595 if let Ok(alt_str) = alt_svc.to_str() {
596 if alt_str.contains("h3") {
597 let origin_key = format!(
598 "{}:{}",
599 url.host_str().unwrap_or(""),
600 url.port().unwrap_or(443)
601 );
602 self.alt_svc_cache.insert(origin_key, alt_str.to_string());
603 }
604 }
605 }
606 }
607}
608
609#[derive(Default)]
614pub struct ClientBuilder {
615 profile: Option<ChromeProfile>,
616 proxy: Option<Proxy>,
617 cookie_store: Option<Arc<RwLock<CookieStore>>>,
618 danger_accept_invalid_certs: bool,
619}
620
621impl ClientBuilder {
622 pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
627 self.danger_accept_invalid_certs = accept;
628 self
629 }
630
631 pub fn profile(mut self, profile: ChromeProfile) -> Self {
633 self.profile = Some(profile);
634 self
635 }
636
637 pub fn proxy(mut self, proxy: Proxy) -> Self {
639 self.proxy = Some(proxy);
640 self
641 }
642
643 pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
645 self.cookie_store = Some(store);
646 self
647 }
648
649 pub fn build(self) -> Result<Client> {
651 let mut profile = self
652 .profile
653 .unwrap_or_else(crate::profile::chrome_147::profile_auto);
654
655 if self.danger_accept_invalid_certs {
656 profile.tls.verify_peer = false;
657 }
658
659 Ok(Client {
660 pool: Arc::new(Mutex::new(HashMap::new())),
661 profile,
662 proxy: self.proxy,
663 cookie_store: self
664 .cookie_store
665 .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
666 hint_cache: Arc::new(RwLock::new(HashSet::new())),
667 alt_svc_cache: AltSvcCache::new(),
668 })
669 }
670}