Skip to main content

http_quik/client/
connector.rs

1use bytes::Bytes;
2use foreign_types::ForeignTypeRef;
3use http2::client::SendRequest;
4use std::net::SocketAddr;
5use tokio::net::TcpStream;
6
7use crate::client::proxy::{dial_proxy, Proxy};
8use crate::client::response::Response;
9use crate::error::Result;
10use crate::http2::configure_builder;
11use crate::profile::ChromeProfile;
12use crate::tls::build_connector;
13
14/// Represents an established HTTP/2 connection with a fixed Chrome identity.
15///
16/// This structure holds the active H2 request handle and the profile used
17/// to establish the connection. Reusing this connection ensures that all
18/// subsequent requests adhere to the same behavioral constraints (e.g.,
19/// same SETTINGS, same window increments).
20pub struct QuikConnection {
21    /// The handle used to initiate new H2 streams.
22    pub h2: SendRequest<Bytes>,
23    /// The profile used for TLS and H2 handshake parity.
24    pub profile: ChromeProfile,
25}
26
27/// Establishes a new network connection following the Chrome 134 transport pipeline.
28///
29/// This function orchestrates a multi-stage handshake to ensure the resulting
30/// connection is indistinguishable from a real browser:
31///
32/// 1. **Proxy/TCP**: Dials the target host (optionally via a SOCKS5/HTTP tunnel).
33/// 2. **TLS Handshake**: Performs a BoringSSL handshake with ClientHello permutation,
34///    GREASE, and extension shuffling.
35/// 3. **ALPS/ECH**: Injects per-connection application settings (ALPS) and ECH GREASE
36///    via raw BoringSSL FFI calls.
37/// 4. **H2 Handshake**: Negotiates the HTTP/2 session using a specialized builder that
38///    replicates Chromium's SETTINGS frame order and connection window increments.
39pub async fn connect(
40    host: &str,
41    port: u16,
42    addr: SocketAddr,
43    profile: &ChromeProfile,
44    proxy: Option<&Proxy>,
45) -> Result<QuikConnection> {
46    // Stage 1: Establish raw TCP transport.
47    let tcp = if let Some(p) = proxy {
48        dial_proxy(p, host, port).await?
49    } else {
50        TcpStream::connect(addr).await?
51    };
52
53    // Stage 2: Configure the TLS connector.
54    let connector = build_connector(&profile.tls)?;
55    let mut config = connector.configure()?;
56
57    // Request OCSP stapling to match Chrome's certificate verification behavior.
58    config.set_status_type(boring::ssl::StatusType::OCSP)?;
59
60    // Stage 3: Per-connection FFI for advanced Chrome features.
61    let ssl_ptr = config.as_ptr();
62
63    // Helper to dynamically build the ALPS payload from H2 settings.
64    fn build_alps_payload(settings: &crate::profile::SettingsFrame) -> [u8; 24] {
65        let mut payload = [0u8; 24];
66        payload[0..2].copy_from_slice(&1u16.to_be_bytes());
67        payload[2..6].copy_from_slice(&settings.header_table_size.to_be_bytes());
68        payload[6..8].copy_from_slice(&2u16.to_be_bytes());
69        payload[8..12].copy_from_slice(&(settings.enable_push as u32).to_be_bytes());
70        payload[12..14].copy_from_slice(&4u16.to_be_bytes());
71        payload[14..18].copy_from_slice(&settings.initial_window_size.to_be_bytes());
72        payload[18..20].copy_from_slice(&6u16.to_be_bytes());
73        payload[20..24].copy_from_slice(&settings.max_header_list_size.to_be_bytes());
74        payload
75    }
76
77    // SAFETY: The `ssl_ptr` is valid for the duration of the configuration phase.
78    // We pass valid pointers for the ALPN protocol "h2" and the static ALPS buffer.
79    // These calls are required because high-level Rust wrappers often do not yet
80    // expose the latest Chromium-specific BoringSSL features.
81    unsafe {
82        if profile.tls.enable_ech_grease {
83            boring_sys::SSL_set_enable_ech_grease(ssl_ptr, 1);
84        }
85        if profile.tls.alps_enabled {
86            let alps_data = build_alps_payload(&profile.h2.settings);
87
88            boring_sys::SSL_add_application_settings(
89                ssl_ptr,
90                b"h2".as_ptr(),
91                2,
92                alps_data.as_ptr(),
93                alps_data.len(),
94            );
95        }
96    }
97
98    // Stage 4: TLS handshake.
99    let tls_stream = tokio_boring::connect(config, host, tcp)
100        .await
101        .map_err(|e| {
102            tracing::error!("TLS handshake failed: {:?}", e);
103            e
104        })?;
105
106    // Stage 5: HTTP/2 handshake.
107    let mut h2_builder = http2::client::Builder::new();
108    configure_builder(&mut h2_builder, &profile.h2);
109
110    let (h2, connection) = h2_builder.handshake(tls_stream).await?;
111
112    // Drive the connection in the background. If this task terminates,
113    // the H2 session is considered dead.
114    tokio::spawn(async move {
115        if let Err(e) = connection.await {
116            tracing::error!("HTTP/2 connection driver failed: {:?}", e);
117        }
118    });
119
120    Ok(QuikConnection {
121        h2,
122        profile: profile.clone(),
123    })
124}
125
126impl QuikConnection {
127    /// Dispatches an HTTP request over the established H2 session.
128    pub async fn send(
129        &mut self,
130        request: http::Request<()>,
131        body: Option<Bytes>,
132    ) -> Result<Response> {
133        let url_str = request.uri().to_string();
134        if let Some(data) = body {
135            let (response_future, mut send_stream) = self.h2.send_request(request, false)?;
136            send_stream.send_data(data, true)?;
137            let response = response_future.await?;
138            Ok(Response::new(response, url_str))
139        } else {
140            let (response_future, _) = self.h2.send_request(request, true)?;
141            let response = response_future.await?;
142            Ok(Response::new(response, url_str))
143        }
144    }
145}