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;
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
95/// Extracts the registrable domain (eTLD+1) from a bare hostname.
96///
97/// For simple TLDs (`.com`, `.net`, `.org`), returns the second-to-last label
98/// plus the TLD. For compound public suffixes (`.co.uk`, `.com.au`, `.co.jp`),
99/// returns the label preceding the compound suffix plus the suffix itself.
100///
101/// Examples:
102///   - `sub.example.com`       → `example.com`
103///   - `a.b.example.co.uk`     → `example.co.uk`
104///   - `example.com`           → `example.com`
105///   - `localhost`             → `localhost`
106fn get_registrable_domain(domain: &str) -> &str {
107    // Compound (two-label) public suffixes commonly encountered on WAF-protected targets.
108    // This is not exhaustive but covers the vast majority of production traffic patterns.
109    const COMPOUND_TLDS: &[&str] = &[
110        "co.uk", "co.jp", "co.kr", "co.in", "co.id", "co.nz", "co.za", "co.th",
111        "com.au", "com.br", "com.cn", "com.mx", "com.tw", "com.sg", "com.hk",
112        "com.ar", "com.co", "com.my", "com.ph", "com.pk", "com.tr", "com.ua",
113        "com.vn", "com.ng", "com.eg", "com.sa",
114        "org.uk", "org.au", "net.au", "net.nz",
115        "ac.uk", "gov.uk", "gov.au",
116        "ne.jp", "or.jp",
117    ];
118
119    let parts: Vec<&str> = domain.split('.').collect();
120    if parts.len() <= 2 {
121        // Already at most a TLD + one label (e.g. "example.com" or "localhost").
122        return domain;
123    }
124
125    // Check whether the trailing two labels form a known compound suffix.
126    let last_two = format!("{}.{}", parts[parts.len() - 2], parts[parts.len() - 1]);
127    for &compound in COMPOUND_TLDS {
128        if last_two == compound {
129            if parts.len() >= 3 {
130                // Registrable domain = label before the compound suffix + compound suffix.
131                let reg_label = parts[parts.len() - 3];
132                let start = domain.len() - last_two.len() - 1 - reg_label.len();
133                return &domain[start..];
134            }
135            return domain;
136        }
137    }
138
139    // Simple TLD: registrable domain = second-to-last label + TLD.
140    let tld_len = parts[parts.len() - 1].len();
141    let sld_len = parts[parts.len() - 2].len();
142    let start = domain.len() - tld_len - sld_len - 1;
143    &domain[start..]
144}
145
146type SharedConnection = Arc<tokio::sync::Mutex<Option<PooledConnection>>>;
147type ConnectionPool = Arc<Mutex<HashMap<String, SharedConnection>>>;
148
149/// A stateful HTTP client engine enforcing deterministic Chrome identity parity.
150///
151/// The `Client` is the primary interface for managing cross-origin requests. It maintains 
152/// global state across its clones, enabling shared connection pooling and cookie persistence. 
153/// Key operational guarantees include:
154/// 
155/// - **Transport Decoupling**: Transparently routes requests over H2 or H3 based on cache states.
156/// - **Connection Pooling**: Reuses established multiplexed streams isolated by proxy and origin.
157/// - **Automated State Tracking**: Synchronizes cookies, redirects, and client-hints seamlessly.
158#[derive(Clone)]
159pub struct Client {
160    /// A synchronized hash map of active connections, strictly keyed by protocol, proxy, and origin.
161    pool: ConnectionPool,
162    /// The canonical identity profile governing TLS handshakes, H2 parameters, and HTTP metadata.
163    profile: ChromeProfile,
164    /// An optional proxy route applied uniformly to all outbound connections from this client.
165    proxy: Option<Proxy>,
166    /// A synchronized cookie jar enforcing RFC 6265 storage and cross-request persistence.
167    pub cookie_store: Arc<RwLock<CookieStore>>,
168    /// A cache tracking origins that explicitly solicited dynamic client hints (e.g. `Accept-CH`).
169    pub hint_cache: Arc<RwLock<HashSet<String>>>,
170    /// Thread-safe registry mapping origins to discovered `Alt-Svc` UDP/QUIC endpoints.
171    pub alt_svc_cache: AltSvcCache,
172    /// A synchronized cache of TLS sessions for resumption, keyed by origin/host.
173    pub tls_session_cache: Arc<Mutex<HashMap<String, boring::ssl::SslSession>>>,
174}
175
176impl Default for Client {
177    fn default() -> Self {
178        Self::new()
179    }
180}
181
182impl Client {
183    /// Creates a new `Client` with a Chrome profile auto-matched to the host OS.
184    ///
185    /// By default, this constructor initializes a ClientBuilder and compiles the default
186    /// profile to the active host platform (e.g. `chrome_148` on Linux/macOS/Windows).
187    /// If builder initialization fails, it statefully falls back to the static `chrome_148`
188    /// auto-profile to guarantee uninterrupted transport-level compliance.
189    pub fn new() -> Self {
190        Self::builder().build().unwrap_or_else(|_| Client {
191            pool: Arc::new(Mutex::new(HashMap::new())),
192            profile: crate::profile::chrome_148::profile_auto(),
193            proxy: None,
194            cookie_store: Arc::new(RwLock::new(CookieStore::default())),
195            hint_cache: Arc::new(RwLock::new(HashSet::new())),
196            alt_svc_cache: AltSvcCache::new(),
197            tls_session_cache: Arc::new(Mutex::new(HashMap::new())),
198        })
199    }
200
201    /// Returns a [`ClientBuilder`] to configure a specialized `Client` instance.
202    pub fn builder() -> ClientBuilder {
203        ClientBuilder::default()
204    }
205
206    /// Executes a GET request and follows redirects stealthily.
207    pub async fn get(&self, url: &str) -> Result<Response> {
208        self.execute_with_redirects("GET", url, None, RequestContext::Navigate)
209            .await
210    }
211
212    /// Executes a POST request and follows redirects stealthily.
213    pub async fn post(&self, url: &str, body: Bytes) -> Result<Response> {
214        self.execute_with_redirects("POST", url, Some(body), RequestContext::Navigate)
215            .await
216    }
217
218    /// Executes the primary request lifecycle, including automated redirect evaluation 
219    /// and dual-stack protocol fallback.
220    ///
221    /// ### Connection Acquisition and Fallback Topology
222    /// 1. **Routing Phase**: Evaluates `AltSvcCache` to select the target transport (H2 vs H3).
223    /// 2. **Lock Serialization**: Acquires an origin-specific async mutex to prevent connection 
224    ///    storming when multiple tasks simultaneously fault on a new origin.
225    /// 3. **Graceful Degradation**: If an active HTTP/3 dial fails or a request drops mid-flight 
226    ///    due to UDP restrictions, the engine instantly evicts the Alt-Svc mapping and 
227    ///    transparently fails over to HTTP/2 over TCP with zero user-visible latency.
228    /// 
229    /// Implements a strict limit of 10 redirects to prevent cyclical loops, applying 
230    /// RFC 7231 method rotation and Chrome-parity cross-site referer truncation on each hop.
231    async fn execute_with_redirects(
232        &self,
233        initial_method: &str,
234        initial_url: &str,
235        initial_body: Option<Bytes>,
236        context: RequestContext,
237    ) -> Result<Response> {
238        let mut current_url_str = initial_url.to_string();
239        let mut current_method = initial_method.to_string();
240        let mut current_body = initial_body;
241        let mut previous_url_str: Option<String> = None;
242
243        let mut sec_fetch_site = "none".to_string();
244        let mut is_cross_site = false;
245
246        for hop in 0..10 {
247            let parsed_url =
248                Url::parse(&current_url_str).map_err(|e| Error::InvalidUrl(e.to_string()))?;
249            let authority = parsed_url
250                .host_str()
251                .ok_or_else(|| Error::InvalidUrl("missing host".to_string()))?;
252            let port = parsed_url.port().unwrap_or_else(|| {
253                if parsed_url.scheme() == "http" {
254                    80
255                } else {
256                    443
257                }
258            });
259
260            // Isolate connection pools by proxy to prevent credential leakage or route mismatches.
261            let proxy_prefix = self
262                .proxy
263                .as_ref()
264                .map(|p| match p {
265                    Proxy::Http(a) => format!("http://{}@", a),
266                    Proxy::Socks5(a) => format!("socks5://{}@", a),
267                })
268                .unwrap_or_default();
269
270            // Differentiate H2 and H3 keys to isolate TCP and UDP multiplexers.
271            let origin_key = format!("{}:{}", authority, port);
272            let mut has_alt_svc = self.alt_svc_cache.get(&origin_key).is_some();
273            let transport_proto = if has_alt_svc { "h3" } else { "h2" };
274            let pool_key = format!("{}{}:{}#{}", proxy_prefix, authority, port, transport_proto);
275
276            // Extract cookies matched to the target domain.
277            let cookie_header = {
278                let store = self
279                    .cookie_store
280                    .read()
281                    .map_err(|_| Error::Connect(std::io::Error::other("cookie store poisoned")))?;
282                let cookies: Vec<_> = store
283                    .matches(&parsed_url)
284                    .iter()
285                    .map(|c| format!("{}={}", c.name(), c.value()))
286                    .collect();
287                if cookies.is_empty() {
288                    None
289                } else {
290                    Some(cookies.join("; "))
291                }
292            };
293
294            // Injects Chrome-identical headers.
295            let is_initial = hop == 0;
296            let accept_ch = {
297                let cache = self.hint_cache.read().unwrap();
298                cache.contains(&parsed_url.origin().ascii_serialization())
299            };
300
301            // Strict-origin-when-cross-origin referer propagation.
302            let referer_to_send = previous_url_str.as_ref().map(|prev| {
303                if is_cross_site {
304                    if let Ok(prev_url) = Url::parse(prev) {
305                        return prev_url.origin().ascii_serialization() + "/";
306                    }
307                }
308                prev.clone()
309            });
310
311            // Use an async Mutex per pool key to serialize connection establishment.
312            // This prevents connection storms when making parallel requests to a new origin.
313            let conn_mutex = {
314                let mut pool = self.pool.lock().map_err(|_| {
315                    Error::Connect(std::io::Error::other("connection pool poisoned"))
316                })?;
317                pool.entry(pool_key.clone())
318                    .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
319                    .clone()
320            };
321
322            let mut pooled_client = loop {
323                let conn_opt = {
324                    let guard = conn_mutex.lock().await;
325                    guard.as_ref().cloned()
326                };
327
328                if let Some(c) = conn_opt {
329                    match c {
330                        PooledConnection::Http2(mut conn) => {
331                            // Rebuild the TCP stream if the socket was closed or encountered a TLS error.
332                            match conn.h2.ready().await {
333                                Ok(h2) => {
334                                    conn.h2 = h2;
335                                    break PooledConnection::Http2(conn);
336                                }
337                                Err(_) => {
338                                    let mut guard = conn_mutex.lock().await;
339                                    *guard = None;
340                                }
341                            }
342                        }
343                        PooledConnection::Http3(conn) => {
344                            // HTTP/3 runs continuously via the background UDP worker task.
345                            // Handshake and channel timeouts are handled internally by the driver.
346                            break PooledConnection::Http3(conn);
347                        }
348                    }
349                } else {
350                    let mut guard = conn_mutex.lock().await;
351                    if guard.is_none() {
352                        // Dial either UDP/QUIC (H3) or TCP/TLS (H2) based on the target protocols.
353                        match self.dial(authority, port, has_alt_svc, &self.profile).await {
354                            Ok(new_conn) => {
355                                *guard = Some(new_conn.clone());
356                                break new_conn;
357                            }
358                            Err(e) => {
359                                if has_alt_svc {
360                                    // Fallback: UDP dial blocked. Evict from cache and retry over H2.
361                                    tracing::warn!("HTTP/3 dial to {} failed ({:?}); falling back to HTTP/2/TCP.", origin_key, e);
362                                    self.alt_svc_cache.remove(&origin_key);
363                                    has_alt_svc = false;
364
365                                    // Build H2 pool key and resolve.
366                                    let h2_pool_key =
367                                        format!("{}{}:{}#h2", proxy_prefix, authority, port);
368                                    let h2_conn_mutex = {
369                                        let mut pool = self.pool.lock().map_err(|_| {
370                                            Error::Connect(std::io::Error::other(
371                                                "connection pool poisoned",
372                                            ))
373                                        })?;
374                                        pool.entry(h2_pool_key)
375                                            .or_insert_with(|| {
376                                                Arc::new(tokio::sync::Mutex::new(None))
377                                            })
378                                            .clone()
379                                    };
380
381                                    let mut h2_guard = h2_conn_mutex.lock().await;
382                                    if h2_guard.is_none() {
383                                        let h2_conn = self
384                                            .dial(authority, port, false, &self.profile)
385                                            .await?;
386                                        *h2_guard = Some(h2_conn.clone());
387                                        break h2_conn;
388                                    } else {
389                                        break h2_guard.as_ref().unwrap().clone();
390                                    }
391                                } else {
392                                    return Err(e);
393                                }
394                            }
395                        }
396                    }
397                }
398            };
399
400            // Build request dynamically for outbound session sending.
401            let mut request = http::Request::builder()
402                .method(current_method.as_str())
403                .uri(parsed_url.as_str())
404                .body(())
405                .map_err(|e| Error::InvalidUrl(e.to_string()))?;
406
407            if let Some(c) = cookie_header.as_deref() {
408                if let Ok(val) = http::header::HeaderValue::from_str(c) {
409                    request.headers_mut().insert("cookie", val);
410                }
411            }
412
413            if current_method == "POST" || current_method == "PUT" || current_method == "PATCH" {
414                if let Ok(val) =
415                    http::header::HeaderValue::from_str(&parsed_url.origin().ascii_serialization())
416                {
417                    request.headers_mut().insert("origin", val);
418                }
419            }
420
421            inject_chrome_headers(
422                request.headers_mut(),
423                &self.profile,
424                &sec_fetch_site,
425                is_initial,
426                context,
427                accept_ch,
428                referer_to_send.as_deref(),
429            );
430
431            // Transmit request. If H3 fails mid-flight (e.g. silent UDP drop), evict and retry over H2.
432            let mut response = match pooled_client.send(request, current_body.clone()).await {
433                Ok(resp) => resp,
434                Err(e) => {
435                    if let PooledConnection::Http3(_) = pooled_client {
436                        tracing::warn!("HTTP/3 request transmission failed ({:?}); falling back to HTTP/2/TCP.", e);
437                        self.alt_svc_cache.remove(&origin_key);
438
439                        // Check for an existing H2 connection to preserve multiplexing and avoid handshakes.
440                        let h2_pool_key = format!("{}{}:{}#h2", proxy_prefix, authority, port);
441                        let h2_conn_mutex = {
442                            let mut pool = self.pool.lock().map_err(|_| {
443                                Error::Connect(std::io::Error::other("connection pool poisoned"))
444                            })?;
445                            pool.entry(h2_pool_key)
446                                .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(None)))
447                                .clone()
448                        };
449
450                        let mut h2_guard = h2_conn_mutex.lock().await;
451                        let h2_conn = if let Some(PooledConnection::Http2(mut conn)) =
452                            h2_guard.as_ref().cloned()
453                        {
454                            match conn.h2.ready().await {
455                                Ok(h2) => {
456                                    conn.h2 = h2;
457                                    *h2_guard = Some(PooledConnection::Http2(conn.clone()));
458                                    PooledConnection::Http2(conn)
459                                }
460                                Err(_) => {
461                                    let new_conn =
462                                        self.dial(authority, port, false, &self.profile).await?;
463                                    *h2_guard = Some(new_conn.clone());
464                                    new_conn
465                                }
466                            }
467                        } else {
468                            let new_conn = self.dial(authority, port, false, &self.profile).await?;
469                            *h2_guard = Some(new_conn.clone());
470                            new_conn
471                        };
472
473                        // Rebuild request for H2 transmission.
474                        let mut fallback_request = http::Request::builder()
475                            .method(current_method.as_str())
476                            .uri(parsed_url.as_str())
477                            .body(())
478                            .map_err(|e| Error::InvalidUrl(e.to_string()))?;
479
480                        if let Some(c) = cookie_header.as_deref() {
481                            if let Ok(val) = http::header::HeaderValue::from_str(c) {
482                                fallback_request.headers_mut().insert("cookie", val);
483                            }
484                        }
485                        if current_method == "POST"
486                            || current_method == "PUT"
487                            || current_method == "PATCH"
488                        {
489                            if let Ok(val) = http::header::HeaderValue::from_str(
490                                &parsed_url.origin().ascii_serialization(),
491                            ) {
492                                fallback_request.headers_mut().insert("origin", val);
493                            }
494                        }
495
496                        inject_chrome_headers(
497                            fallback_request.headers_mut(),
498                            &self.profile,
499                            &sec_fetch_site,
500                            is_initial,
501                            context,
502                            accept_ch,
503                            referer_to_send.as_deref(),
504                        );
505
506                        let mut h2_pooled = h2_conn;
507                        h2_pooled
508                            .send(fallback_request, current_body.clone())
509                            .await?
510                    } else {
511                        return Err(e);
512                    }
513                }
514            };
515
516            // Store cookie, hints, and Alt-Svc headers from response.
517            self.store_cookies(&response, &parsed_url);
518            self.store_hints(&response, &parsed_url);
519            self.store_alt_svc(&response, &parsed_url);
520
521            let status = response.status();
522            if status.is_redirection() {
523                if let Some(location) = response.headers().get("location") {
524                    let loc_str = location.to_str().unwrap_or("");
525                    let next_url = parsed_url
526                        .join(loc_str)
527                        .map_err(|e| Error::InvalidUrl(e.to_string()))?;
528
529                    // Rotate method to GET on 301/302/303 per RFC 7231 §6.4.
530                    if status == http::StatusCode::MOVED_PERMANENTLY
531                        || status == http::StatusCode::FOUND
532                        || status == http::StatusCode::SEE_OTHER
533                    {
534                        current_method = "GET".to_string();
535                        current_body = None;
536                    }
537
538                    if !is_cross_site {
539                        if next_url.origin() == parsed_url.origin() {
540                            sec_fetch_site = "same-origin".to_string();
541                        } else {
542                            // Compare registrable domains (eTLD+1) to correctly identify
543                            // same-site subdomain transitions (e.g. www.example.com → api.example.com)
544                            // without false cross-site classification.
545                            let is_same_site = match (next_url.domain(), parsed_url.domain()) {
546                                (Some(a), Some(b)) => {
547                                    get_registrable_domain(a) == get_registrable_domain(b)
548                                }
549                                _ => false,
550                            };
551                            if is_same_site {
552                                sec_fetch_site = "same-site".to_string();
553                            } else {
554                                sec_fetch_site = "cross-site".to_string();
555                                is_cross_site = true;
556                            }
557                        }
558                    }
559
560                    previous_url_str = Some(current_url_str);
561                    current_url_str = next_url.to_string();
562                    continue;
563                }
564            }
565
566            response.set_url(current_url_str);
567            return Ok(response);
568        }
569
570        Err(Error::Connect(std::io::Error::other(
571            "Redirect limit exceeded (max 10)",
572        )))
573    }
574
575    /// Initializes a network socket and negotiates the underlying protocol stream.
576    ///
577    /// ### Protocol Dispatch
578    /// - **HTTP/3 (`dial_h3 = true`)**: Resolves the target via DNS, binds an ephemeral 
579    ///   IPv4/IPv6 wildcard UDP socket, and delegates stream handling to a background 
580    ///   `QuicSession` worker. Emits Chrome's zero-length connection ID signature.
581    /// - **HTTP/2 (`dial_h3 = false`)**: Establishes a standard TCP connection, negotiating 
582    ///   TLS 1.3 with ALPN strictly constrained to `h2` and HTTP/1.1 fallbacks.
583    async fn dial(
584        &self,
585        authority: &str,
586        port: u16,
587        dial_h3: bool,
588        profile: &ChromeProfile,
589    ) -> Result<PooledConnection> {
590        if dial_h3 {
591            let addr_str = format!("{}:{}", authority, port);
592            // Asynchronous, non-blocking DNS resolution.
593            let addr = tokio::net::lookup_host(&addr_str).await?.next().ok_or_else(|| {
594                std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
595            })?;
596
597            // Setup dual-stack loopback or wildcard listener.
598            let local_addr: SocketAddr = if addr.is_ipv6() {
599                "[::]:0".parse().unwrap()
600            } else {
601                "0.0.0.0:0".parse().unwrap()
602            };
603
604            let socket = UdpSocket::bind(local_addr).await?;
605            socket.connect(addr).await?;
606
607            let mut config = crate::client::quic::configure_chrome_quic_transport()?;
608            if !profile.tls.verify_peer {
609                config.verify_peer(false);
610            }
611
612            // Bind zero-length CID to match Chrome wire identity.
613            let scid = quiche::ConnectionId::from_ref(&[]);
614            let conn = quiche::connect(Some(authority), &scid, local_addr, addr, &mut config)
615                .map_err(|e| Error::Connect(std::io::Error::other(e.to_string())))?;
616
617            let (cmd_tx, cmd_rx) = mpsc::channel(100);
618            let socket_arc = Arc::new(socket);
619
620            tokio::spawn(crate::client::quic::run_quic_driver(
621                socket_arc, conn, addr, cmd_rx,
622            ));
623
624            Ok(PooledConnection::Http3(QuicSession {
625                tx: cmd_tx,
626                profile: profile.clone(),
627            }))
628        } else {
629            let addr_str = format!("{}:{}", authority, port);
630            // Asynchronous, non-blocking DNS resolution.
631            let addr = tokio::net::lookup_host(&addr_str).await?.next().ok_or_else(|| {
632                std::io::Error::new(std::io::ErrorKind::NotFound, "could not resolve host")
633            })?;
634
635            // Lookup cached TLS session for resumption if available.
636            let cached_session = {
637                let cache = self.tls_session_cache.lock().map_err(|_| {
638                    Error::Connect(std::io::Error::other("TLS session cache poisoned"))
639                })?;
640                cache.get(authority).cloned()
641            };
642
643            let conn = connect(authority, port, addr, profile, self.proxy.as_ref(), cached_session).await?;
644
645            // If a new session ticket was negotiated, cache it for future resumptions.
646            if let Some(ref sess) = conn.session {
647                if let Ok(mut cache) = self.tls_session_cache.lock() {
648                    cache.insert(authority.to_string(), sess.clone());
649                }
650            }
651
652            Ok(PooledConnection::Http2(conn))
653        }
654    }
655
656    /// Persists `Set-Cookie` headers from a response into the synchronized cookie store.
657    fn store_cookies(&self, resp: &Response, url: &Url) {
658        if let Ok(mut store) = self.cookie_store.write() {
659            for v in resp.headers().get_all("set-cookie").iter() {
660                if let Ok(cookie_str) = v.to_str() {
661                    let _ = store.parse(cookie_str, url);
662                }
663            }
664        }
665    }
666
667    /// Caches `Accept-CH` headers explicitly requested by the server.
668    fn store_hints(&self, resp: &Response, url: &Url) {
669        if let Some(accept_ch) = resp.headers().get("accept-ch") {
670            if let Ok(ch_str) = accept_ch.to_str() {
671                if ch_str.to_lowercase().contains("sec-ch-ua-platform-version") {
672                    if let Ok(mut cache) = self.hint_cache.write() {
673                        cache.insert(url.origin().ascii_serialization());
674                    }
675                }
676            }
677        }
678    }
679
680    /// Caches server Alt-Svc headers.
681    fn store_alt_svc(&self, resp: &Response, url: &Url) {
682        if let Some(alt_svc) = resp.headers().get("alt-svc") {
683            if let Ok(alt_str) = alt_svc.to_str() {
684                if alt_str.contains("h3") {
685                    let origin_key = format!(
686                        "{}:{}",
687                        url.host_str().unwrap_or(""),
688                        url.port().unwrap_or(443)
689                    );
690                    self.alt_svc_cache.insert(origin_key, alt_str.to_string());
691                }
692            }
693        }
694    }
695}
696
697/// A builder pattern for instantiating a custom [`Client`] with specific overrides.
698///
699/// Provides a declarative interface to override the default Chrome profile, configure 
700/// outbound proxy routes, or inject a pre-populated synchronized cookie store.
701#[derive(Default)]
702pub struct ClientBuilder {
703    profile: Option<ChromeProfile>,
704    proxy: Option<Proxy>,
705    cookie_store: Option<Arc<RwLock<CookieStore>>>,
706    danger_accept_invalid_certs: bool,
707}
708
709impl ClientBuilder {
710    /// Bypasses TLS certificate validation.
711    ///
712    /// Disables peer verification in BoringSSL. This is strictly intended for debugging 
713    /// environments or corporate proxies and undermines transport layer security.
714    pub fn danger_accept_invalid_certs(mut self, accept: bool) -> Self {
715        self.danger_accept_invalid_certs = accept;
716        self
717    }
718
719    /// Sets the Chrome identity profile.
720    pub fn profile(mut self, profile: ChromeProfile) -> Self {
721        self.profile = Some(profile);
722        self
723    }
724
725    /// Configures an outbound proxy.
726    pub fn proxy(mut self, proxy: Proxy) -> Self {
727        self.proxy = Some(proxy);
728        self
729    }
730
731    /// Provides a pre-existing synchronized cookie store.
732    pub fn cookie_store(mut self, store: Arc<RwLock<CookieStore>>) -> Self {
733        self.cookie_store = Some(store);
734        self
735    }
736
737    /// Finalizes the configuration and constructs a `Client`.
738    ///
739    /// If no explicit `ChromeProfile` was supplied, this method defaults to the
740    /// modern, high-fidelity `chrome_148` profile matched statefully to the host operating system
741    /// to preserve p0f network characteristics.
742    pub fn build(self) -> Result<Client> {
743        let mut profile = self
744            .profile
745            .unwrap_or_else(crate::profile::chrome_148::profile_auto);
746
747        if self.danger_accept_invalid_certs {
748            profile.tls.verify_peer = false;
749        }
750
751        Ok(Client {
752            pool: Arc::new(Mutex::new(HashMap::new())),
753            profile,
754            proxy: self.proxy,
755            cookie_store: self
756                .cookie_store
757                .unwrap_or_else(|| Arc::new(RwLock::new(CookieStore::default()))),
758            hint_cache: Arc::new(RwLock::new(HashSet::new())),
759            alt_svc_cache: AltSvcCache::new(),
760            tls_session_cache: Arc::new(Mutex::new(HashMap::new())),
761        })
762    }
763}