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/// Thread-safe registry for dynamically discovered HTTP/3 Alt-Svc advertisements.
19///
20/// Under RFC 9114, origins advertise QUIC support via the `Alt-Svc` HTTP header. 
21/// This cache records valid `h3` mappings to preemptively bypass TCP and TLS handshakes 
22/// on subsequent requests to the same origin, establishing QUIC connections directly.
23///
24/// The cache employs a reader-writer lock design to eliminate contention on the hot path.
25/// Reads (origin lookups) are wait-free under shared locks, while exclusive locks are 
26/// strictly reserved for initial discovery or deterministic eviction during network drops.
27#[derive(Clone)]
28pub struct AltSvcCache {
29    entries: Arc<RwLock<HashMap<String, String>>>,
30}
31
32impl AltSvcCache {
33    /// Instantiates a new thread-safe in-memory cache.
34    pub fn new() -> Self {
35        Self {
36            entries: Arc::new(RwLock::new(HashMap::new())),
37        }
38    }
39
40    /// Retrieves cached H3 signals for a target origin string.
41    pub fn get(&self, origin: &str) -> Option<String> {
42        let guard = self.entries.read().ok()?;
43        guard.get(origin).cloned()
44    }
45
46    /// Stores/updates an Alt-Svc advertisement.
47    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    /// Evicts an origin from the cache to trigger protocol fallback.
54    ///
55    /// Used defensively when a QUIC handshake fails or a middlebox silently drops
56    /// UDP packets. Eviction ensures that subsequent requests to the affected origin 
57    /// are deterministically routed over HTTP/2 and TCP, maintaining connectivity 
58    /// on restrictive networks.
59    pub fn remove(&self, origin: &str) {
60        if let Ok(mut guard) = self.entries.write() {
61            guard.remove(origin);
62        }
63    }
64}
65
66/// An abstract representation of an active, multiplexed connection session.
67///
68/// This enum enforces strict transport decoupling. The request runner interacts 
69/// exclusively with the polymorphic `send` interface, completely abstracting whether 
70/// the underlying byte stream is multiplexed over HTTP/2 (TCP/TLS) or HTTP/3 (UDP/QUIC).
71/// It ensures connection lifecycles and multiplexing limits are handled seamlessly 
72/// behind a unified boundary.
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 HTTP client engine enforcing deterministic Chrome identity parity.
99///
100/// The `Client` is the primary interface for managing cross-origin requests. It maintains 
101/// global state across its clones, enabling shared connection pooling and cookie persistence. 
102/// Key operational guarantees include:
103/// 
104/// - **Transport Decoupling**: Transparently routes requests over H2 or H3 based on cache states.
105/// - **Connection Pooling**: Reuses established multiplexed streams isolated by proxy and origin.
106/// - **Automated State Tracking**: Synchronizes cookies, redirects, and client-hints seamlessly.
107#[derive(Clone)]
108pub struct Client {
109    /// A synchronized hash map of active connections, strictly keyed by protocol, proxy, and origin.
110    pool: ConnectionPool,
111    /// The canonical identity profile governing TLS handshakes, H2 parameters, and HTTP metadata.
112    profile: ChromeProfile,
113    /// An optional proxy route applied uniformly to all outbound connections from this client.
114    proxy: Option<Proxy>,
115    /// A synchronized cookie jar enforcing RFC 6265 storage and cross-request persistence.
116    pub cookie_store: Arc<RwLock<CookieStore>>,
117    /// A cache tracking origins that explicitly solicited dynamic client hints (e.g. `Accept-CH`).
118    pub hint_cache: Arc<RwLock<HashSet<String>>>,
119    /// Thread-safe registry mapping origins to discovered `Alt-Svc` UDP/QUIC endpoints.
120    pub alt_svc_cache: AltSvcCache,
121}
122
123impl Default for Client {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl Client {
130    /// Creates a new `Client` with a Chrome profile auto-matched to the host OS.
131    ///
132    /// By default, this constructor initializes a ClientBuilder and compiles the default
133    /// profile to the active host platform (e.g. `chrome_148` on Linux/macOS/Windows).
134    /// If builder initialization fails, it statefully falls back to the static `chrome_148`
135    /// auto-profile to guarantee uninterrupted transport-level compliance.
136    pub fn new() -> Self {
137        Self::builder().build().unwrap_or_else(|_| Client {
138            pool: Arc::new(Mutex::new(HashMap::new())),
139            profile: crate::profile::chrome_148::profile_auto(),
140            proxy: None,
141            cookie_store: Arc::new(RwLock::new(CookieStore::default())),
142            hint_cache: Arc::new(RwLock::new(HashSet::new())),
143            alt_svc_cache: AltSvcCache::new(),
144        })
145    }
146
147    /// Returns a [`ClientBuilder`] to configure a specialized `Client` instance.
148    pub fn builder() -> ClientBuilder {
149        ClientBuilder::default()
150    }
151
152    /// Executes a GET request and follows redirects stealthily.
153    pub async fn get(&self, url: &str) -> Result<Response> {
154        self.execute_with_redirects("GET", url, None, RequestContext::Navigate)
155            .await
156    }
157
158    /// Executes a POST request and follows redirects stealthily.
159    pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
160        self.execute_with_redirects("POST", url, Some(body), RequestContext::Navigate)
161            .await
162    }
163
164    /// Executes the primary request lifecycle, including automated redirect evaluation 
165    /// and dual-stack protocol fallback.
166    ///
167    /// ### Connection Acquisition and Fallback Topology
168    /// 1. **Routing Phase**: Evaluates `AltSvcCache` to select the target transport (H2 vs H3).
169    /// 2. **Lock Serialization**: Acquires an origin-specific async mutex to prevent connection 
170    ///    storming when multiple tasks simultaneously fault on a new origin.
171    /// 3. **Graceful Degradation**: If an active HTTP/3 dial fails or a request drops mid-flight 
172    ///    due to UDP restrictions, the engine instantly evicts the Alt-Svc mapping and 
173    ///    transparently fails over to HTTP/2 over TCP with zero user-visible latency.
174    /// 
175    /// Implements a strict limit of 10 redirects to prevent cyclical loops, applying 
176    /// RFC 7231 method rotation and Chrome-parity cross-site referer truncation on each hop.
177    async fn execute_with_redirects(
178        &self,
179        initial_method: &str,
180        initial_url: &str,
181        initial_body: Option<Bytes>,
182        context: RequestContext,
183    ) -> Result<Response> {
184        let mut current_url_str = initial_url.to_string();
185        let mut current_method = initial_method.to_string();
186        let mut current_body = initial_body;
187        let mut previous_url_str: Option<String> = None;
188
189        let mut sec_fetch_site = "none".to_string();
190        let mut is_cross_site = false;
191
192        for hop in 0..10 {
193            let parsed_url =
194                Url::parse(&current_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
195            let authority = parsed_url
196                .host_str()
197                .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
198            let port = parsed_url.port().unwrap_or_else(|| {
199                if parsed_url.scheme() == "http" {
200                    80
201                } else {
202                    443
203                }
204            });
205
206            // Isolate connection pools by proxy to prevent credential leakage or route mismatches.
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            // Differentiate H2 and H3 keys to isolate TCP and UDP multiplexers.
217            let origin_key = format!("{}:{}", authority, port);
218            let mut has_alt_svc = self.alt_svc_cache.get(&origin_key).is_some();
219            let transport_proto = if has_alt_svc { "h3" } else { "h2" };
220            let pool_key = format!("{}{}:{}#{}", proxy_prefix, authority, port, transport_proto);
221
222            // Extract cookies matched to the target domain.
223            let cookie_header = {
224                let store = self
225                    .cookie_store
226                    .read()
227                    .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
228                let cookies: Vec<_> = store
229                    .matches(&parsed_url)
230                    .iter()
231                    .map(|c| format!("{}={}", c.name(), c.value()))
232                    .collect();
233                if cookies.is_empty() {
234                    None
235                } else {
236                    Some(cookies.join("; "))
237                }
238            };
239
240            // Injects Chrome-identical headers.
241            let is_initial = hop == 0;
242            let accept_ch = {
243                let cache = self.hint_cache.read().unwrap();
244                cache.contains(&parsed_url.origin().ascii_serialization())
245            };
246
247            // Strict-origin-when-cross-origin referer propagation.
248            let referer_to_send = previous_url_str.as_ref().map(|prev| {
249                if is_cross_site {
250                    if let Ok(prev_url) = Url::parse(prev) {
251                        return prev_url.origin().ascii_serialization() + "/";
252                    }
253                }
254                prev.clone()
255            });
256
257            // Use an async Mutex per pool key to serialize connection establishment.
258            // This prevents connection storms when making parallel requests to a new origin.
259            let conn_mutex = {
260                let mut pool = self.pool.lock().map_err(|_| {
261                    Error::Connect(std::io::Error::other("connection pool poisoned"))
262                })?;
263                pool.entry(pool_key.clone())
264                    .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
265                    .clone()
266            };
267
268            let mut pooled_client = loop {
269                let conn_opt = {
270                    let guard = conn_mutex.lock().await;
271                    guard.as_ref().cloned()
272                };
273
274                if let Some(c) = conn_opt {
275                    match c {
276                        PooledConnection::Http2(mut conn) => {
277                            // Rebuild the TCP stream if the socket was closed or encountered a TLS error.
278                            match conn.h2.ready().await {
279                                Ok(h2) => {
280                                    conn.h2 = h2;
281                                    break PooledConnection::Http2(conn);
282                                }
283                                Err(_) => {
284                                    let mut guard = conn_mutex.lock().await;
285                                    *guard = None;
286                                }
287                            }
288                        }
289                        PooledConnection::Http3(conn) => {
290                            // HTTP/3 runs continuously via the background UDP worker task.
291                            // Handshake and channel timeouts are handled internally by the driver.
292                            break PooledConnection::Http3(conn);
293                        }
294                    }
295                } else {
296                    let mut guard = conn_mutex.lock().await;
297                    if guard.is_none() {
298                        // Dial either UDP/QUIC (H3) or TCP/TLS (H2) based on the target protocols.
299                        match self.dial(authority, port, has_alt_svc, &self.profile).await {
300                            Ok(new_conn) => {
301                                *guard = Some(new_conn.clone());
302                                break new_conn;
303                            }
304                            Err(e) => {
305                                if has_alt_svc {
306                                    // Fallback: UDP dial blocked. Evict from cache and retry over H2.
307                                    tracing::warn!("HTTP/3 dial to {} failed ({:?}); falling back to HTTP/2/TCP.", origin_key, e);
308                                    self.alt_svc_cache.remove(&origin_key);
309                                    has_alt_svc = false;
310
311                                    // Build H2 pool key and resolve.
312                                    let h2_pool_key =
313                                        format!("{}{}:{}#h2", proxy_prefix, authority, port);
314                                    let h2_conn_mutex = {
315                                        let mut pool = self.pool.lock().map_err(|_| {
316                                            Error::Connect(std::io::Error::other(
317                                                "connection pool poisoned",
318                                            ))
319                                        })?;
320                                        pool.entry(h2_pool_key)
321                                            .or_insert_with(|| {
322                                                Arc::new(tokio::sync::Mutex::new(None))
323                                            })
324                                            .clone()
325                                    };
326
327                                    let mut h2_guard = h2_conn_mutex.lock().await;
328                                    if h2_guard.is_none() {
329                                        let h2_conn = self
330                                            .dial(authority, port, false, &self.profile)
331                                            .await?;
332                                        *h2_guard = Some(h2_conn.clone());
333                                        break h2_conn;
334                                    } else {
335                                        break h2_guard.as_ref().unwrap().clone();
336                                    }
337                                } else {
338                                    return Err(e);
339                                }
340                            }
341                        }
342                    }
343                }
344            };
345
346            // Build request dynamically for outbound session sending.
347            let mut request = http::Request::builder()
348                .method(current_method.as_str())
349                .uri(parsed_url.as_str())
350                .body(())
351                .map_err(|e| Error::InvalidUrl(e.to_string()))?;
352
353            if let Some(c) = cookie_header.as_deref() {
354                if let Ok(val) = http::header::HeaderValue::from_str(c) {
355                    request.headers_mut().insert("cookie", val);
356                }
357            }
358
359            if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
360                if let Ok(val) =
361                    http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
362                {
363                    request.headers_mut().insert("origin", val);
364                }
365            }
366
367            inject_chrome_headers(
368                request.headers_mut(),
369                &self.profile,
370                &sec_fetch_site,
371                is_initial,
372                context,
373                accept_ch,
374                referer_to_send.as_deref(),
375            );
376
377            // Transmit request. If H3 fails mid-flight (e.g. silent UDP drop), evict and retry over H2.
378            let mut response = match pooled_client.send(request, current_body.clone()).await {
379                Ok(resp) => resp,
380                Err(e) => {
381                    if let PooledConnection::Http3(_) = pooled_client {
382                        tracing::warn!("HTTP/3 request transmission failed ({:?}); falling back to HTTP/2/TCP.", e);
383                        self.alt_svc_cache.remove(&origin_key);
384
385                        // Check for an existing H2 connection to preserve multiplexing and avoid handshakes.
386                        let h2_pool_key = format!("{}{}:{}#h2", proxy_prefix, authority, port);
387                        let h2_conn_mutex = {
388                            let mut pool = self.pool.lock().map_err(|_| {
389                                Error::Connect(std::io::Error::other("connection pool poisoned"))
390                            })?;
391                            pool.entry(h2_pool_key)
392                                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
393                                .clone()
394                        };
395
396                        let mut h2_guard = h2_conn_mutex.lock().await;
397                        let h2_conn = if let Some(PooledConnection::Http2(mut conn)) =
398                            h2_guard.as_ref().cloned()
399                        {
400                            match conn.h2.ready().await {
401                                Ok(h2) => {
402                                    conn.h2 = h2;
403                                    *h2_guard = Some(PooledConnection::Http2(conn.clone()));
404                                    PooledConnection::Http2(conn)
405                                }
406                                Err(_) => {
407                                    let new_conn =
408                                        self.dial(authority, port, false, &self.profile).await?;
409                                    *h2_guard = Some(new_conn.clone());
410                                    new_conn
411                                }
412                            }
413                        } else {
414                            let new_conn = self.dial(authority, port, false, &self.profile).await?;
415                            *h2_guard = Some(new_conn.clone());
416                            new_conn
417                        };
418
419                        // Rebuild request for H2 transmission.
420                        let mut fallback_request = http::Request::builder()
421                            .method(current_method.as_str())
422                            .uri(parsed_url.as_str())
423                            .body(())
424                            .map_err(|e| Error::InvalidUrl(e.to_string()))?;
425
426                        if let Some(c) = cookie_header.as_deref() {
427                            if let Ok(val) = http::header::HeaderValue::from_str(c) {
428                                fallback_request.headers_mut().insert("cookie", val);
429                            }
430                        }
431                        if current_method == "POST"
432                            || current_method == "PUT"
433                            || current_method == "PATCH"
434                        {
435                            if let Ok(val) = http::header::HeaderValue::from_str(
436                                &parsed_url.origin().ascii_serialization(),
437                            ) {
438                                fallback_request.headers_mut().insert("origin", val);
439                            }
440                        }
441
442                        inject_chrome_headers(
443                            fallback_request.headers_mut(),
444                            &self.profile,
445                            &sec_fetch_site,
446                            is_initial,
447                            context,
448                            accept_ch,
449                            referer_to_send.as_deref(),
450                        );
451
452                        let mut h2_pooled = h2_conn;
453                        h2_pooled
454                            .send(fallback_request, current_body.clone())
455                            .await?
456                    } else {
457                        return Err(e);
458                    }
459                }
460            };
461
462            // Store cookie, hints, and Alt-Svc headers from response.
463            self.store_cookies(&response, &parsed_url);
464            self.store_hints(&response, &parsed_url);
465            self.store_alt_svc(&response, &parsed_url);
466
467            let status = response.status();
468            if status.is_redirection() {
469                if let Some(location) = response.headers().get("location") {
470                    let loc_str = location.to_str().unwrap_or("");
471                    let next_url = parsed_url
472                        .join(loc_str)
473                        .map_err(|e| Error::InvalidUrl(e.to_string()))?;
474
475                    // Rotate method to GET on 301/302/303 per RFC 7231 ยง6.4.
476                    if status == http::StatusCode::MOVED_PERMANENTLY
477                        || status == http::StatusCode::FOUND
478                        || status == http::StatusCode::SEE_OTHER
479                    {
480                        current_method = "GET".to_string();
481                        current_body = None;
482                    }
483
484                    if !is_cross_site {
485                        if next_url.origin() == parsed_url.origin() {
486                            sec_fetch_site = "same-origin".to_string();
487                        } else if next_url.domain() == parsed_url.domain() {
488                            sec_fetch_site = "same-site".to_string();
489                        } else {
490                            sec_fetch_site = "cross-site".to_string();
491                            is_cross_site = true;
492                        }
493                    }
494
495                    previous_url_str = Some(current_url_str);
496                    current_url_str = next_url.to_string();
497                    continue;
498                }
499            }
500
501            response.set_url(current_url_str);
502            return Ok(response);
503        }
504
505        Err(Error::Connect(std::io::Error::other(
506            "Redirect limit exceeded (max 10)",
507        )))
508    }
509
510    /// Initializes a network socket and negotiates the underlying protocol stream.
511    ///
512    /// ### Protocol Dispatch
513    /// - **HTTP/3 (`dial_h3 = true`)**: Resolves the target via DNS, binds an ephemeral 
514    ///   IPv4/IPv6 wildcard UDP socket, and delegates stream handling to a background 
515    ///   `QuicSession` worker. Emits Chrome's zero-length connection ID signature.
516    /// - **HTTP/2 (`dial_h3 = false`)**: Establishes a standard TCP connection, negotiating 
517    ///   TLS 1.3 with ALPN strictly constrained to `h2` and HTTP/1.1 fallbacks.
518    async fn dial(
519        &self,
520        authority: &str,
521        port: u16,
522        dial_h3: bool,
523        profile: &ChromeProfile,
524    ) -> Result<PooledConnection> {
525        if dial_h3 {
526            let addr_str = format!("{}:{}", authority, port);
527            let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
528                std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
529            })?;
530
531            // Setup dual-stack loopback or wildcard listener.
532            let local_addr: SocketAddr = if addr.is_ipv6() {
533                "[::]:0".parse().unwrap()
534            } else {
535                "0.0.0.0:0".parse().unwrap()
536            };
537
538            let socket = UdpSocket::bind(local_addr).await?;
539            socket.connect(addr).await?;
540
541            let mut config = crate::client::quic::configure_chrome_quic_transport()?;
542            if !profile.tls.verify_peer {
543                config.verify_peer(false);
544            }
545
546            // Bind zero-length CID to match Chrome wire identity.
547            let scid = quiche::ConnectionId::from_ref(&[]);
548            let conn = quiche::connect(Some(authority), &scid, local_addr, addr, &mut config)
549                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
550
551            let (cmd_tx, cmd_rx) = mpsc::channel(100);
552            let socket_arc = Arc::new(socket);
553
554            tokio::spawn(crate::client::quic::run_quic_driver(
555                socket_arc, conn, addr, cmd_rx,
556            ));
557
558            Ok(PooledConnection::Http3(QuicSession {
559                tx: cmd_tx,
560                profile: profile.clone(),
561            }))
562        } else {
563            let addr_str = format!("{}:{}", authority, port);
564            let addr = addr_str.to_socket_addrs()?.next().ok_or_else(|| {
565                std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
566            })?;
567
568            let conn = connect(authority, port, addr, profile, self.proxy.as_ref()).await?;
569            Ok(PooledConnection::Http2(conn))
570        }
571    }
572
573    /// Persists `Set-Cookie` headers from a response into the synchronized cookie store.
574    fn store_cookies(&self, resp: &Response, url: &Url) {
575        if let Ok(mut store) = self.cookie_store.write() {
576            for v in resp.headers().get_all("set-cookie").iter() {
577                if let Ok(cookie_str) = v.to_str() {
578                    let _ = store.parse(cookie_str, url);
579                }
580            }
581        }
582    }
583
584    /// Caches `Accept-CH` headers explicitly requested by the server.
585    fn store_hints(&self, resp: &Response, url: &Url) {
586        if let Some(accept_ch) = resp.headers().get("accept-ch") {
587            if let Ok(ch_str) = accept_ch.to_str() {
588                if ch_str.to_lowercase().contains("sec-ch-ua-platform-version") {
589                    if let Ok(mut cache) = self.hint_cache.write() {
590                        cache.insert(url.origin().ascii_serialization());
591                    }
592                }
593            }
594        }
595    }
596
597    /// Caches server Alt-Svc headers.
598    fn store_alt_svc(&self, resp: &Response, url: &Url) {
599        if let Some(alt_svc) = resp.headers().get("alt-svc") {
600            if let Ok(alt_str) = alt_svc.to_str() {
601                if alt_str.contains("h3") {
602                    let origin_key = format!(
603                        "{}:{}",
604                        url.host_str().unwrap_or(""),
605                        url.port().unwrap_or(443)
606                    );
607                    self.alt_svc_cache.insert(origin_key, alt_str.to_string());
608                }
609            }
610        }
611    }
612}
613
614/// A builder pattern for instantiating a custom [`Client`] with specific overrides.
615///
616/// Provides a declarative interface to override the default Chrome profile, configure 
617/// outbound proxy routes, or inject a pre-populated synchronized cookie store.
618#[derive(Default)]
619pub struct ClientBuilder {
620    profile: Option<ChromeProfile>,
621    proxy: Option<Proxy>,
622    cookie_store: Option<Arc<RwLock<CookieStore>>>,
623    danger_accept_invalid_certs: bool,
624}
625
626impl ClientBuilder {
627    /// Bypasses TLS certificate validation.
628    ///
629    /// Disables peer verification in BoringSSL. This is strictly intended for debugging 
630    /// environments or corporate proxies and undermines transport layer security.
631    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
632        self.danger_accept_invalid_certs = accept;
633        self
634    }
635
636    /// Sets the Chrome identity profile.
637    pub fn profile(mut self, profile: ChromeProfile) -> Self {
638        self.profile = Some(profile);
639        self
640    }
641
642    /// Configures an outbound proxy.
643    pub fn proxy(mut self, proxy: Proxy) -> Self {
644        self.proxy = Some(proxy);
645        self
646    }
647
648    /// Provides a pre-existing synchronized cookie store.
649    pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
650        self.cookie_store = Some(store);
651        self
652    }
653
654    /// Finalizes the configuration and constructs a `Client`.
655    ///
656    /// If no explicit `ChromeProfile` was supplied, this method defaults to the
657    /// modern, high-fidelity `chrome_148` profile matched statefully to the host operating system
658    /// to preserve p0f network characteristics.
659    pub fn build(self) -> Result<Client> {
660        let mut profile = self
661            .profile
662            .unwrap_or_else(crate::profile::chrome_148::profile_auto);
663
664        if self.danger_accept_invalid_certs {
665            profile.tls.verify_peer = false;
666        }
667
668        Ok(Client {
669            pool: Arc::new(Mutex::new(HashMap::new())),
670            profile,
671            proxy: self.proxy,
672            cookie_store: self
673                .cookie_store
674                .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
675            hint_cache: Arc::new(RwLock::new(HashSet::new())),
676            alt_svc_cache: AltSvcCache::new(),
677        })
678    }
679}