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