Skip to main content

http_quik/client/
pool.rs

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/// Tracks dynamic origin advertisements of HTTP/3 protocol support.
19///
20/// Under RFC 9114, servers advertise HTTP/3 availability via the `Alt-Svc` header
21/// (e.g. `alt-svc: h3=":443"; ma=86400`). This structure implements a thread-safe
22/// dynamic cache to record these mappings. Subsequent requests to identical origins
23/// intercept this cache and bypass standard TCP/TLS handshakes, attempting UDP/QUIC directly.
24///
25/// ### Thread-Safety Design:
26/// We wrap the mapping in an `Arc<RwLock<HashMap<...>>>`. This allows multiple parallel threads
27/// to query cache hits concurrently with zero-latency lock contention, while reserving exclusive write locks
28/// only when discovering new advertisements or degrading failed endpoints.
29#[derive(Clone)]
30pub struct AltSvcCache {
31    entries: Arc<RwLock<HashMap<String, String>>>,
32}
33
34impl AltSvcCache {
35    /// Instantiates a new thread-safe in-memory cache.
36    pub fn new() -> Self {
37        Self {
38            entries: Arc::new(RwLock::new(HashMap::new())),
39        }
40    }
41
42    /// Retrieves cached H3 signals for a target origin string.
43    pub fn get(&self, origin: &str) -> Option<String> {
44        let guard = self.entries.read().ok()?;
45        guard.get(origin).cloned()
46    }
47
48    /// Stores/updates an Alt-Svc advertisement.
49    pub fn insert(&self, origin: String, alt_svc: String) {
50        if let Ok(mut guard) = self.entries.write() {
51            guard.insert(origin, alt_svc);
52        }
53    }
54
55    /// Degrades/removes an origin entry on UDP dial failures.
56    ///
57    /// When a network path drops UDP packets or WAF rules block QUIC handshakes,
58    /// this function evicts the origin entry. The pool then routes subsequent requests
59    /// over H2/TCP, restoring standard connection fallback.
60    pub fn remove(&self, origin: &str) {
61        if let Ok(mut guard) = self.entries.write() {
62            guard.remove(origin);
63        }
64    }
65}
66
67/// Polymorphic representation of an active pooled session.
68///
69/// Enforces complete transport decoupling at the connection interface. The request runner
70/// interacts solely with this polymorphic interface, routing standard `http::Request` blocks
71/// warning-free without needing to know if the frame is translated to TCP byte streams (HTTP/2)
72/// or UDP datagram packets (HTTP/3).
73#[derive(Clone)]
74pub enum PooledConnection {
75    /// Persistent HTTP/2 multiplexed TCP/TLS transport.
76    Http2(QuikConnection),
77    /// Stealth HTTP/3 multiplexed UDP/QUIC transport.
78    Http3(QuicSession),
79}
80
81impl PooledConnection {
82    /// Dispatches an HTTP request over the active session.
83    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/// A stateful, pooling HTTP client that enforces Chrome transport identity.
99///
100/// The `Client` is the primary entry point for the `http-quik` library. It manages:
101/// 1. **Connection Pooling**: Reuses established H2 or H3 sessions to maintain persistent fingerprints.
102/// 2. **Cookie Persistence**: A synchronized cookie jar shared across all requests.
103/// 3. **Stealth Redirects**: Automatically follows redirects while mutating headers and methods
104///    to match Chromium's behavioral markers.
105/// 4. **OS Auto-Detection**: Defaults to a Chrome profile matched to the host OS.
106/// 5. **Dual-Stack H3 Routing**: Seamlessly resolves Alt-Svc advertisements and executes
107///    stealth HTTP/3 fetches, falling back automatically to H2 on UDP blockages.
108#[derive(Clone)]
109pub struct Client {
110    /// A synchronized pool of active H2/H3 connections keyed by their origin and proxy.
111    pool: ConnectionPool,
112    /// The canonical identity profile used for all transport-layer operations.
113    profile: ChromeProfile,
114    /// An optional proxy used for all outbound connections.
115    proxy: Option<Proxy>,
116    /// A synchronized cookie jar shared across all requests.
117    pub cookie_store: Arc<RwLock<CookieStore>>,
118    /// A synchronized cache for Client Hints explicitly solicited by servers.
119    pub hint_cache: Arc<RwLock<HashSet<String>>>,
120    /// Thread-safe registry tracking servers Solicit Alt-Svc targets.
121    pub alt_svc_cache: AltSvcCache,
122}
123
124impl Default for Client {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl Client {
131    /// Creates a new `Client` with a Chrome profile auto-matched to the host OS.
132    pub fn new() -> Self {
133        Self::builder().build().unwrap_or_else(|_| Client {
134            pool: Arc::new(Mutex::new(HashMap::new())),
135            profile: crate::profile::chrome_134::profile_auto(),
136            proxy: None,
137            cookie_store: Arc::new(RwLock::new(CookieStore::default())),
138            hint_cache: Arc::new(RwLock::new(HashSet::new())),
139            alt_svc_cache: AltSvcCache::new(),
140        })
141    }
142
143    /// Returns a [`ClientBuilder`] to configure a specialized `Client` instance.
144    pub fn builder() -> ClientBuilder {
145        ClientBuilder::default()
146    }
147
148    /// Executes a GET request and follows redirects stealthily.
149    pub async fn get(&self, url: &str) -> Result<Response> {
150        self.execute_with_redirects("GET", url, None, RequestContext::Navigate)
151            .await
152    }
153
154    /// Executes a POST request and follows redirects stealthily.
155    pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
156        self.execute_with_redirects("POST", url, Some(body), RequestContext::Navigate)
157            .await
158    }
159
160    /// Core request execution engine with automated, stateful redirect handling.
161    ///
162    /// This method integrates our dual-stack transport fallback state machine:
163    ///
164    /// 1. **Alt-Svc Lookup**: Before building any connection, checks the `AltSvcCache` for the target origin.
165    /// 2. **Stateful Connection Keying**: Pools are split using target protocols (`#h2` vs `#h3`)
166    ///    to isolate transport streams.
167    /// 3. **Acquisition / Dials**:
168    ///    - Attempts to reuse an existing pooled H3 connection.
169    ///    - If none exists, executes a concurrent dial using the dynamic QUIC background driver.
170    ///    - If the dial fails immediately, the cache is degraded and we instantly switch to H2.
171    /// 4. **Resilient Transmission Fallback**: If connection establishment succeeds but request transmission
172    ///    subsequently fails (e.g., due to middlebox UDP drops during early frames), the loop intercepts
173    ///    the error, evicts the host from `AltSvcCache`, searches the pool for active TCP/H2 connections
174    ///    to preserve multiplexing, and falls back to TCP/TLS with zero user-visible latency.
175    async fn execute_with_redirects(
176        &self,
177        initial_method: &str,
178        initial_url: &str,
179        initial_body: Option<Bytes>,
180        context: RequestContext,
181    ) -> Result<Response> {
182        let mut current_url_str = initial_url.to_string();
183        let mut current_method = initial_method.to_string();
184        let mut current_body = initial_body;
185        let mut previous_url_str: Option<String> = None;
186
187        let mut sec_fetch_site = "none".to_string();
188        let mut is_cross_site = false;
189
190        for hop in 0..10 {
191            let parsed_url =
192                Url::parse(&current_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
193            let authority = parsed_url
194                .host_str()
195                .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
196            let port = parsed_url.port().unwrap_or_else(|| {
197                if parsed_url.scheme() == "http" {
198                    80
199                } else {
200                    443
201                }
202            });
203
204            // Build a unique pool key considering the proxy and target origin.
205            // This is required to isolate connection states when different proxies are used,
206            // avoiding leakage of target credentials or mismatching destination routes.
207            let proxy_prefix = self
208                .proxy
209                .as_ref()
210                .map(|p| match p {
211                    Proxy::Http(a) => format!("http://{}@", a),
212                    Proxy::Socks5(a) => format!("socks5://{}@", a),
213                })
214                .unwrap_or_default();
215
216            // We differentiate H2 and H3 keys within the pool to avoid sharing TCP/UDP socket handles.
217            // Using a distinct suffix ("#h2" vs "#h3") ensures that protocol-specific multiplexers
218            // are kept isolated while keeping pooling fast and deterministic.
219            let origin_key = format!("{}:{}", authority, port);
220            let mut has_alt_svc = self.alt_svc_cache.get(&origin_key).is_some();
221            let transport_proto = if has_alt_svc { "h3" } else { "h2" };
222            let pool_key = format!("{}{}:{}#{}", proxy_prefix, authority, port, transport_proto);
223
224            // Extract relevant cookies for the current target URL.
225            // A read lock is acquired on the cookie store to safely retrieve cookies matched
226            // to the destination domain, maintaining the synchronized cookie jar.
227            let cookie_header = {
228                let store = self
229                    .cookie_store
230                    .read()
231                    .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
232                let cookies: Vec<_> = store
233                    .matches(&parsed_url)
234                    .iter()
235                    .map(|c| format!("{}={}", c.name(), c.value()))
236                    .collect();
237                if cookies.is_empty() {
238                    None
239                } else {
240                    Some(cookies.join("; "))
241                }
242            };
243
244            // Injects Chrome-identical headers.
245            let is_initial = hop == 0;
246            let accept_ch = {
247                let cache = self.hint_cache.read().unwrap();
248                cache.contains(&parsed_url.origin().ascii_serialization())
249            };
250
251            // Referer propagation
252            // Follows strict-origin-when-cross-origin policy, matching Chrome's behavior.
253            let referer_to_send = previous_url_str.as_ref().map(|prev| {
254                if is_cross_site {
255                    if let Ok(prev_url) = Url::parse(prev) {
256                        return prev_url.origin().ascii_serialization() + "/";
257                    }
258                }
259                prev.clone()
260            });
261
262            // Connection acquisition logic: use an async Mutex per origin to avoid race conditions.
263            // Using a single-lock model ensures that parallel concurrent calls to the same endpoint
264            // serialize on connection establishment, avoiding connection storming signatures
265            // which easily trigger bot mitigation blocks.
266            let conn_mutex = {
267                let mut pool = self.pool.lock().map_err(|_| {
268                    Error::Connect(std::io::Error::other("connection pool poisoned"))
269                })?;
270                pool.entry(pool_key.clone())
271                    .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
272                    .clone()
273            };
274
275            let mut pooled_client = loop {
276                let conn_opt = {
277                    let guard = conn_mutex.lock().await;
278                    guard.as_ref().cloned()
279                };
280
281                if let Some(c) = conn_opt {
282                    match c {
283                        PooledConnection::Http2(mut conn) => {
284                            // Check if the underlying HTTP/2 multiplexed TCP stream is still alive.
285                            // If a stream drops or encounters a TLS socket error, we discard it
286                            // and allow the next loop tick to rebuild it.
287                            match conn.h2.ready().await {
288                                Ok(h2) => {
289                                    conn.h2 = h2;
290                                    break PooledConnection::Http2(conn);
291                                }
292                                Err(_) => {
293                                    let mut guard = conn_mutex.lock().await;
294                                    *guard = None;
295                                }
296                            }
297                        }
298                        PooledConnection::Http3(conn) => {
299                            // HTTP/3 runs continuously via the background UDP worker task.
300                            // Handshake and channel timeouts are handled internally by the driver.
301                            break PooledConnection::Http3(conn);
302                        }
303                    }
304                } else {
305                    let mut guard = conn_mutex.lock().await;
306                    if guard.is_none() {
307                        // Dial either UDP/QUIC (H3) or TCP/TLS (H2) based on the target protocols.
308                        match self.dial(authority, port, has_alt_svc, &self.profile).await {
309                            Ok(new_conn) => {
310                                *guard = Some(new_conn.clone());
311                                break new_conn;
312                            }
313                            Err(e) => {
314                                if has_alt_svc {
315                                    // HTTP/3 UDP dialing encountered a block (e.g. port closed).
316                                    // We statefully degrade the cache entry and fall back immediately to H2.
317                                    tracing::warn!("HTTP/3 dial to {} failed ({:?}); falling back to HTTP/2/TCP.", origin_key, e);
318                                    self.alt_svc_cache.remove(&origin_key);
319                                    has_alt_svc = false;
320
321                                    // Build H2 pool key and resolve.
322                                    let h2_pool_key =
323                                        format!("{}{}:{}#h2", proxy_prefix, authority, port);
324                                    let h2_conn_mutex = {
325                                        let mut pool = self.pool.lock().map_err(|_| {
326                                            Error::Connect(std::io::Error::other(
327                                                "connection pool poisoned",
328                                            ))
329                                        })?;
330                                        pool.entry(h2_pool_key)
331                                            .or_insert_with(|| {
332                                                Arc::new(tokio::sync::Mutex::new(None))
333                                            })
334                                            .clone()
335                                    };
336
337                                    let mut h2_guard = h2_conn_mutex.lock().await;
338                                    if h2_guard.is_none() {
339                                        let h2_conn = self
340                                            .dial(authority, port, false, &self.profile)
341                                            .await?;
342                                        *h2_guard = Some(h2_conn.clone());
343                                        break h2_conn;
344                                    } else {
345                                        break h2_guard.as_ref().unwrap().clone();
346                                    }
347                                } else {
348                                    return Err(e);
349                                }
350                            }
351                        }
352                    }
353                }
354            };
355
356            // Build request dynamically for outbound session sending.
357            let mut request = http::Request::builder()
358                .method(current_method.as_str())
359                .uri(parsed_url.as_str())
360                .body(())
361                .map_err(|e| Error::InvalidUrl(e.to_string()))?;
362
363            if let Some(c) = cookie_header.as_deref() {
364                if let Ok(val) = http::header::HeaderValue::from_str(c) {
365                    request.headers_mut().insert("cookie", val);
366                }
367            }
368
369            if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
370                if let Ok(val) =
371                    http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
372                {
373                    request.headers_mut().insert("origin", val);
374                }
375            }
376
377            inject_chrome_headers(
378                request.headers_mut(),
379                &self.profile,
380                &sec_fetch_site,
381                is_initial,
382                context,
383                accept_ch,
384                referer_to_send.as_deref(),
385            );
386
387            // Execute request transmission. If H3 fails, fallback instantly and transparently to H2.
388            // This isolates path UDP/QUIC blockage risks, protecting user interactions from
389            // failing when networks block UDP/443 traffic silently.
390            let mut response = match pooled_client.send(request, current_body.clone()).await {
391                Ok(resp) => resp,
392                Err(e) => {
393                    if let PooledConnection::Http3(_) = pooled_client {
394                        tracing::warn!("HTTP/3 request transmission failed ({:?}); falling back to HTTP/2/TCP.", e);
395                        self.alt_svc_cache.remove(&origin_key);
396
397                        // Check if an H2 connection already exists in the pool to preserve multiplexing.
398                        // Reusing an active TCP connection avoids building a second handshake, ensuring speed.
399                        let h2_pool_key = format!("{}{}:{}#h2", proxy_prefix, authority, port);
400                        let h2_conn_mutex = {
401                            let mut pool = self.pool.lock().map_err(|_| {
402                                Error::Connect(std::io::Error::other("connection pool poisoned"))
403                            })?;
404                            pool.entry(h2_pool_key)
405                                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
406                                .clone()
407                        };
408
409                        let mut h2_guard = h2_conn_mutex.lock().await;
410                        let h2_conn = if let Some(PooledConnection::Http2(mut conn)) =
411                            h2_guard.as_ref().cloned()
412                        {
413                            match conn.h2.ready().await {
414                                Ok(h2) => {
415                                    conn.h2 = h2;
416                                    *h2_guard = Some(PooledConnection::Http2(conn.clone()));
417                                    PooledConnection::Http2(conn)
418                                }
419                                Err(_) => {
420                                    let new_conn =
421                                        self.dial(authority, port, false, &self.profile).await?;
422                                    *h2_guard = Some(new_conn.clone());
423                                    new_conn
424                                }
425                            }
426                        } else {
427                            let new_conn = self.dial(authority, port, false, &self.profile).await?;
428                            *h2_guard = Some(new_conn.clone());
429                            new_conn
430                        };
431
432                        // Rebuild request for H2 transmission.
433                        let mut fallback_request = http::Request::builder()
434                            .method(current_method.as_str())
435                            .uri(parsed_url.as_str())
436                            .body(())
437                            .map_err(|e| Error::InvalidUrl(e.to_string()))?;
438
439                        if let Some(c) = cookie_header.as_deref() {
440                            if let Ok(val) = http::header::HeaderValue::from_str(c) {
441                                fallback_request.headers_mut().insert("cookie", val);
442                            }
443                        }
444                        if current_method == "POST"
445                            || current_method == "PUT"
446                            || current_method == "PATCH"
447                        {
448                            if let Ok(val) = http::header::HeaderValue::from_str(
449                                &parsed_url.origin().ascii_serialization(),
450                            ) {
451                                fallback_request.headers_mut().insert("origin", val);
452                            }
453                        }
454
455                        inject_chrome_headers(
456                            fallback_request.headers_mut(),
457                            &self.profile,
458                            &sec_fetch_site,
459                            is_initial,
460                            context,
461                            accept_ch,
462                            referer_to_send.as_deref(),
463                        );
464
465                        let mut h2_pooled = h2_conn;
466                        h2_pooled
467                            .send(fallback_request, current_body.clone())
468                            .await?
469                    } else {
470                        return Err(e);
471                    }
472                }
473            };
474
475            // Store cookie, hints, and Alt-Svc headers from response.
476            self.store_cookies(&response, &parsed_url);
477            self.store_hints(&response, &parsed_url);
478            self.store_alt_svc(&response, &parsed_url);
479
480            let status = response.status();
481            if status.is_redirection() {
482                if let Some(location) = response.headers().get("location") {
483                    let loc_str = location.to_str().unwrap_or("");
484                    let next_url = parsed_url
485                        .join(loc_str)
486                        .map_err(|e| Error::InvalidUrl(e.to_string()))?;
487
488                    // Redirect Mutation: Rotate POST to GET on standard redirects, matching browser specifications.
489                    if status == http::StatusCode::MOVED_PERMANENTLY
490                        || status == http::StatusCode::FOUND
491                        || status == http::StatusCode::SEE_OTHER
492                    {
493                        current_method = "GET".to_string();
494                        current_body = None;
495                    }
496
497                    if !is_cross_site {
498                        if next_url.origin() == parsed_url.origin() {
499                            sec_fetch_site = "same-origin".to_string();
500                        } else if next_url.domain() == parsed_url.domain() {
501                            sec_fetch_site = "same-site".to_string();
502                        } else {
503                            sec_fetch_site = "cross-site".to_string();
504                            is_cross_site = true;
505                        }
506                    }
507
508                    previous_url_str = Some(current_url_str);
509                    current_url_str = next_url.to_string();
510                    continue;
511                }
512            }
513
514            response.set_url(current_url_str);
515            return Ok(response);
516        }
517
518        Err(Error::Connect(std::io::Error::other(
519            "Redirect limit exceeded (max 10)",
520        )))
521    }
522
523    /// Dials either an H2 or H3 connection based on origin flags.
524    ///
525    /// ### Dial Mechanics:
526    /// - **HTTP/3 (dial_h3 = true)**:
527    ///   - Resolves target host address.
528    ///   - Binds wildcard UDP Socket aligned to IPv4/IPv6 address families.
529    ///   - Spawns background asynchronous loop worker task (`run_quic_driver`) to handle frame polls.
530    /// - **HTTP/2 (dial_h3 = false)**:
531    ///   - Opens standard TCP connection, negotiating TLS ALPN "h2".
532    async fn dial(
533        &self,
534        authority: &str,
535        port: u16,
536        dial_h3: bool,
537        profile: &ChromeProfile,
538    ) -> Result<PooledConnection> {
539        if dial_h3 {
540            let addr_str = format!("{}:{}", authority, port);
541            let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
542                std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
543            })?;
544
545            // Setup dual-stack loopback or wildcard listener.
546            let local_addr: SocketAddr = if addr.is_ipv6() {
547                "[::]:0".parse().unwrap()
548            } else {
549                "0.0.0.0:0".parse().unwrap()
550            };
551
552            let socket = UdpSocket::bind(local_addr).await?;
553            socket.connect(addr).await?;
554
555            let mut config = crate::client::quic::configure_chrome_quic_transport()?;
556            if !profile.tls.verify_peer {
557                config.verify_peer(false);
558            }
559
560            // Bind zero-length CID to match Chrome wire identity.
561            let scid = quiche::ConnectionId::from_ref(&[]);
562            let conn = quiche::connect(Some(authority), &scid, local_addr, addr, &mut config)
563                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
564
565            let (cmd_tx, cmd_rx) = mpsc::channel(100);
566            let socket_arc = Arc::new(socket);
567
568            tokio::spawn(crate::client::quic::run_quic_driver(
569                socket_arc, conn, addr, cmd_rx,
570            ));
571
572            Ok(PooledConnection::Http3(QuicSession {
573                tx: cmd_tx,
574                profile: profile.clone(),
575            }))
576        } else {
577            let addr_str = format!("{}:{}", authority, port);
578            let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
579                std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
580            })?;
581
582            let conn = connect(authority, port, addr, profile, self.proxy.as_ref()).await?;
583            Ok(PooledConnection::Http2(conn))
584        }
585    }
586
587    /// Persists `Set-Cookie` headers from a response into the synchronized cookie store.
588    fn store_cookies(&self, resp: &Response, url: &Url) {
589        if let Ok(mut store) = self.cookie_store.write() {
590            for v in resp.headers().get_all("set-cookie").iter() {
591                if let Ok(cookie_str) = v.to_str() {
592                    let _ = store.parse(cookie_str, url);
593                }
594            }
595        }
596    }
597
598    /// Caches `Accept-CH` headers explicitly requested by the server.
599    fn store_hints(&self, resp: &Response, url: &Url) {
600        if let Some(accept_ch) = resp.headers().get("accept-ch") {
601            if let Ok(ch_str) = accept_ch.to_str() {
602                if ch_str.to_lowercase().contains("sec-ch-ua-platform-version") {
603                    if let Ok(mut cache) = self.hint_cache.write() {
604                        cache.insert(url.origin().ascii_serialization());
605                    }
606                }
607            }
608        }
609    }
610
611    /// Caches server Alt-Svc headers.
612    fn store_alt_svc(&self, resp: &Response, url: &Url) {
613        if let Some(alt_svc) = resp.headers().get("alt-svc") {
614            if let Ok(alt_str) = alt_svc.to_str() {
615                if alt_str.contains("h3") {
616                    let origin_key = format!(
617                        "{}:{}",
618                        url.host_str().unwrap_or(""),
619                        url.port().unwrap_or(443)
620                    );
621                    self.alt_svc_cache.insert(origin_key, alt_str.to_string());
622                }
623            }
624        }
625    }
626}
627
628/// A builder for constructing a `Client` with specific identity and transport settings.
629#[derive(Default)]
630pub struct ClientBuilder {
631    profile: Option<ChromeProfile>,
632    proxy: Option<Proxy>,
633    cookie_store: Option<Arc<RwLock<CookieStore>>>,
634    danger_accept_invalid_certs: bool,
635}
636
637impl ClientBuilder {
638    /// Disables certificate verification.
639    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
640        self.danger_accept_invalid_certs = accept;
641        self
642    }
643
644    /// Sets the Chrome identity profile.
645    pub fn profile(mut self, profile: ChromeProfile) -> Self {
646        self.profile = Some(profile);
647        self
648    }
649
650    /// Configures an outbound proxy.
651    pub fn proxy(mut self, proxy: Proxy) -> Self {
652        self.proxy = Some(proxy);
653        self
654    }
655
656    /// Provides a pre-existing synchronized cookie store.
657    pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
658        self.cookie_store = Some(store);
659        self
660    }
661
662    /// Finalizes the configuration and constructs a `Client`.
663    pub fn build(self) -> Result<Client> {
664        let mut profile = self
665            .profile
666            .unwrap_or_else(crate::profile::chrome_134::profile_auto);
667
668        if self.danger_accept_invalid_certs {
669            profile.tls.verify_peer = false;
670        }
671
672        Ok(Client {
673            pool: Arc::new(Mutex::new(HashMap::new())),
674            profile,
675            proxy: self.proxy,
676            cookie_store: self
677                .cookie_store
678                .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
679            hint_cache: Arc::new(RwLock::new(HashSet::new())),
680            alt_svc_cache: AltSvcCache::new(),
681        })
682    }
683}