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    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    /// Returns a [`ClientBuilder`] to configure a specialized `Client` instance.
143    pub fn builder() -> ClientBuilder {
144        ClientBuilder::default()
145    }
146
147    /// Executes a GET request and follows redirects stealthily.
148    pub async fn get(&self, url: &str) -> Result<Response> {
149        self.execute_with_redirects("GET", url, None, RequestContext::Navigate)
150            .await
151    }
152
153    /// Executes a POST request and follows redirects stealthily.
154    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    /// Executes the primary request lifecycle, including automated redirect evaluation 
160    /// and dual-stack protocol fallback.
161    ///
162    /// ### Connection Acquisition and Fallback Topology
163    /// 1. **Routing Phase**: Evaluates `AltSvcCache` to select the target transport (H2 vs H3).
164    /// 2. **Lock Serialization**: Acquires an origin-specific async mutex to prevent connection 
165    ///    storming when multiple tasks simultaneously fault on a new origin.
166    /// 3. **Graceful Degradation**: If an active HTTP/3 dial fails or a request drops mid-flight 
167    ///    due to UDP restrictions, the engine instantly evicts the Alt-Svc mapping and 
168    ///    transparently fails over to HTTP/2 over TCP with zero user-visible latency.
169    /// 
170    /// Implements a strict limit of 10 redirects to prevent cyclical loops, applying 
171    /// RFC 7231 method rotation and Chrome-parity cross-site referer truncation on each hop.
172    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(&current_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            // Isolate connection pools by proxy to prevent credential leakage or route mismatches.
202            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            // Differentiate H2 and H3 keys to isolate TCP and UDP multiplexers.
212            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            // Extract cookies matched to the target domain.
218            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            // Injects Chrome-identical headers.
236            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            // Strict-origin-when-cross-origin referer propagation.
243            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            // Use an async Mutex per pool key to serialize connection establishment.
253            // This prevents connection storms when making parallel requests to a new origin.
254            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                            // Rebuild the TCP stream if the socket was closed or encountered a TLS error.
273                            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                            // HTTP/3 runs continuously via the background UDP worker task.
286                            // Handshake and channel timeouts are handled internally by the driver.
287                            break PooledConnection::Http3(conn);
288                        }
289                    }
290                } else {
291                    let mut guard = conn_mutex.lock().await;
292                    if guard.is_none() {
293                        // Dial either UDP/QUIC (H3) or TCP/TLS (H2) based on the target protocols.
294                        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                                    // Fallback: UDP dial blocked. Evict from cache and retry over H2.
302                                    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                                    // Build H2 pool key and resolve.
307                                    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            // Build request dynamically for outbound session sending.
342            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            // Transmit request. If H3 fails mid-flight (e.g. silent UDP drop), evict and retry over H2.
373            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                        // Check for an existing H2 connection to preserve multiplexing and avoid handshakes.
381                        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                        // Rebuild request for H2 transmission.
415                        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            // Store cookie, hints, and Alt-Svc headers from response.
458            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                    // Rotate method to GET on 301/302/303 per RFC 7231 ยง6.4.
471                    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    /// Initializes a network socket and negotiates the underlying protocol stream.
506    ///
507    /// ### Protocol Dispatch
508    /// - **HTTP/3 (`dial_h3 = true`)**: Resolves the target via DNS, binds an ephemeral 
509    ///   IPv4/IPv6 wildcard UDP socket, and delegates stream handling to a background 
510    ///   `QuicSession` worker. Emits Chrome's zero-length connection ID signature.
511    /// - **HTTP/2 (`dial_h3 = false`)**: Establishes a standard TCP connection, negotiating 
512    ///   TLS 1.3 with ALPN strictly constrained to `h2` and HTTP/1.1 fallbacks.
513    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            // Setup dual-stack loopback or wildcard listener.
527            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            // Bind zero-length CID to match Chrome wire identity.
542            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    /// Persists `Set-Cookie` headers from a response into the synchronized cookie store.
569    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    /// Caches `Accept-CH` headers explicitly requested by the server.
580    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    /// Caches server Alt-Svc headers.
593    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/// A builder pattern for instantiating a custom [`Client`] with specific overrides.
610///
611/// Provides a declarative interface to override the default Chrome profile, configure 
612/// outbound proxy routes, or inject a pre-populated synchronized cookie store.
613#[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    /// Bypasses TLS certificate validation.
623    ///
624    /// Disables peer verification in BoringSSL. This is strictly intended for debugging 
625    /// environments or corporate proxies and undermines transport layer security.
626    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
627        self.danger_accept_invalid_certs = accept;
628        self
629    }
630
631    /// Sets the Chrome identity profile.
632    pub fn profile(mut self, profile: ChromeProfile) -> Self {
633        self.profile = Some(profile);
634        self
635    }
636
637    /// Configures an outbound proxy.
638    pub fn proxy(mut self, proxy: Proxy) -> Self {
639        self.proxy = Some(proxy);
640        self
641    }
642
643    /// Provides a pre-existing synchronized cookie store.
644    pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
645        self.cookie_store = Some(store);
646        self
647    }
648
649    /// Finalizes the configuration and constructs a `Client`.
650    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}