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