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/// Establishes a new network connection following the Chrome transport pipeline.
15///
16/// This function orchestrates the full TLS + HTTP/2 handshake sequence,
17/// injecting platform-specific ALPS data and ECH GREASE via raw BoringSSL
18/// FFI calls. The resulting [`QuikConnection`] maintains the identity
19/// established during the handshake for the lifetime of the session.
20/// subsequent requests adhere to the same behavioral constraints (e.g.,
21/// same SETTINGS, same window increments).
22#[derive(Clone)]
23pub struct QuikConnection {
24    /// The handle used to initiate new H2 streams.
25    pub h2: SendRequest<Bytes>,
26    /// The profile used for TLS and H2 handshake parity.
27    pub profile: ChromeProfile,
28}
29
30/// Establishes a new network connection following the Chrome 134 transport pipeline.
31///
32/// This function orchestrates a multi-stage handshake to ensure the resulting
33/// connection is indistinguishable from a real browser:
34///
35/// 1. **Proxy/TCP**: Dials the target host (optionally via a SOCKS5/HTTP tunnel).
36/// 2. **TLS Handshake**: Performs a BoringSSL handshake with ClientHello permutation,
37///    GREASE, and extension shuffling.
38/// 3. **ALPS/ECH**: Injects per-connection application settings (ALPS) and ECH GREASE
39///    via raw BoringSSL FFI calls.
40/// 4. **H2 Handshake**: Negotiates the HTTP/2 session using a specialized builder that
41///    replicates Chromium's SETTINGS frame order and connection window increments.
42pub async fn connect(
43    host: &str,
44    port: u16,
45    addr: SocketAddr,
46    profile: &ChromeProfile,
47    proxy: Option<&Proxy>,
48) -> Result<QuikConnection> {
49    // Stage 1: Establish raw TCP transport.
50    let tcp = if let Some(p) = proxy {
51        dial_proxy(p, host, port).await?
52    } else {
53        TcpStream::connect(addr).await?
54    };
55
56    // Stage 2: Configure the TLS connector.
57    let connector = build_connector(&profile.tls)?;
58    let mut config = connector.configure()?;
59
60    // Request OCSP stapling to match Chrome's certificate verification behavior.
61    config.set_status_type(boring::ssl::StatusType::OCSP)?;
62
63    // Stage 3: Per-connection FFI for advanced Chrome features.
64    let ssl_ptr = config.as_ptr();
65
66    // Serializes the H2 SETTINGS into a raw ALPS payload.
67    //
68    // The base payload is 24 bytes (4 settings x 6 bytes each). On Windows
69    // and Linux, `extra` adds one entry (setting 0x7A9A), extending the
70    // payload to 30 bytes. macOS passes an empty slice, keeping it at 24.
71    fn build_alps_payload(
72        settings: &crate::profile::SettingsFrame,
73        extra: &[(u16, u32)],
74    ) -> Vec<u8> {
75        let entry_count = 4 + extra.len();
76        let mut payload = Vec::with_capacity(entry_count * 6);
77        // Standard Chrome settings (IDs 1, 2, 4, 6).
78        payload.extend_from_slice(&1u16.to_be_bytes());
79        payload.extend_from_slice(&settings.header_table_size.to_be_bytes());
80        payload.extend_from_slice(&2u16.to_be_bytes());
81        payload.extend_from_slice(&(settings.enable_push as u32).to_be_bytes());
82        payload.extend_from_slice(&4u16.to_be_bytes());
83        payload.extend_from_slice(&settings.initial_window_size.to_be_bytes());
84        payload.extend_from_slice(&6u16.to_be_bytes());
85        payload.extend_from_slice(&settings.max_header_list_size.to_be_bytes());
86        // OS-specific extra settings (e.g., 0x7A9A on Windows/Linux).
87        for &(id, value) in extra {
88            payload.extend_from_slice(&id.to_be_bytes());
89            payload.extend_from_slice(&value.to_be_bytes());
90        }
91        payload
92    }
93
94    // SAFETY: The `ssl_ptr` is valid for the duration of the configuration phase.
95    // We pass valid pointers for the ALPN protocol "h2" and the dynamically
96    // built ALPS buffer. These calls are required because high-level Rust
97    // wrappers do not yet expose the latest Chromium-specific BoringSSL features.
98    unsafe {
99        if profile.tls.enable_ech_grease {
100            boring_sys::SSL_set_enable_ech_grease(ssl_ptr, 1);
101        }
102        if profile.tls.alps_enabled {
103            let alps_data =
104                build_alps_payload(&profile.h2.settings, profile.tls.alps_extra_settings);
105
106            let alps_res = boring_sys::SSL_add_application_settings(
107                ssl_ptr,
108                b"h2".as_ptr(),
109                2,
110                alps_data.as_ptr(),
111                alps_data.len(),
112            );
113            if alps_res != 1 {
114                return Err(crate::error::Error::Connect(std::io::Error::other(
115                    "failed to inject ALPS settings",
116                )));
117            }
118        }
119    }
120
121    // Stage 4: TLS handshake.
122    let tls_stream = tokio_boring::connect(config, host, tcp)
123        .await
124        .map_err(|e| {
125            tracing::error!("TLS handshake failed: {:?}", e);
126            e
127        })?;
128
129    // Stage 5: HTTP/2 handshake.
130    let mut h2_builder = http2::client::Builder::new();
131    configure_builder(&mut h2_builder, &profile.h2);
132
133    let (h2, connection) = h2_builder.handshake(tls_stream).await?;
134
135    // Drive the connection in the background. If this task terminates,
136    // the H2 session is considered dead.
137    tokio::spawn(async move {
138        if let Err(e) = connection.await {
139            tracing::error!("HTTP/2 connection driver failed: {:?}", e);
140        }
141    });
142
143    Ok(QuikConnection {
144        h2,
145        profile: profile.clone(),
146    })
147}
148
149impl QuikConnection {
150    /// Dispatches an HTTP request over the established H2 session.
151    pub async fn send(
152        &mut self,
153        request: http::Request<()>,
154        body: Option<Bytes>,
155    ) -> Result<Response> {
156        let url_str = request.uri().to_string();
157        if let Some(data) = body {
158            let (response_future, mut send_stream) = self.h2.send_request(request, false)?;
159            send_stream.send_data(data, true)?;
160            let response = response_future.await?;
161            Ok(Response::new(response, url_str))
162        } else {
163            let (response_future, _) = self.h2.send_request(request, true)?;
164            let response = response_future.await?;
165            Ok(Response::new(response, url_str))
166        }
167    }
168}