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(¤t_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}