Skip to main content

cf_mach/nq_core/connection/
http.rs

1// Copyright (c) 2023-2024 Cloudflare, Inc.
2// Licensed under the BSD-3-Clause license found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
3
4use std::fmt::Debug;
5use std::future::Future;
6use std::net::SocketAddr;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, Ordering};
9
10use anyhow::bail;
11use boring::ssl::{SslConnector, SslMethod, SslVerifyMode, SslVersion};
12use boring::x509::X509;
13use boring::x509::store::X509StoreBuilder;
14use http::header::HOST;
15use http::{HeaderValue, Request, Response};
16use hyper::body::Incoming;
17use hyper::client::conn::{http1, http2};
18use hyper_util::rt::TokioIo;
19use tokio::select;
20use tokio_util::sync::CancellationToken;
21use tracing::{Instrument, debug, error, info, warn};
22
23use crate::nq_core::body::NqBody;
24use crate::nq_core::util::ByteStream;
25use crate::nq_core::{ConnectionTiming, ConnectionType, ResponseFuture, Time};
26
27pub type TlsStream = tokio_boring::SslStream<Box<dyn ByteStream>>;
28
29/// Process-wide switch to skip TLS certificate verification. Off by default.
30///
31/// This exists only to allow testing against servers presenting self-signed
32/// certificates (e.g. a local `wrangler dev` speed-test server). It must never
33/// be enabled against production endpoints.
34static INSECURE_TLS: AtomicBool = AtomicBool::new(false);
35
36/// Enable or disable skipping TLS certificate verification globally.
37pub fn set_insecure_tls(insecure: bool) {
38    INSECURE_TLS.store(insecure, Ordering::Relaxed);
39}
40
41/// Whether TLS certificate verification is currently being skipped.
42pub fn insecure_tls() -> bool {
43    INSECURE_TLS.load(Ordering::Relaxed)
44}
45
46/// An [`EstablishedConnection`] contains the connection's timing and a handle
47/// to send HTTP requests with.
48#[derive(Debug)]
49pub struct EstablishedConnection {
50    timing: ConnectionTiming,
51    send_request: Option<SendRequest>,
52}
53
54/// Represents an established connection with timing information and a send request handler.
55impl EstablishedConnection {
56    /// Creates a new `EstablishedConnection`.
57    pub fn new(timing: ConnectionTiming, send_request: SendRequest) -> Self {
58        Self {
59            timing,
60            send_request: Some(send_request),
61        }
62    }
63
64    /// Sends a request using the connection.
65    pub fn send_request(&mut self, req: Request<NqBody>) -> Option<ResponseFuture> {
66        self.send_request.as_mut().map(|s| s.send_request(req))
67    }
68
69    /// Returns the timing information of the connection.
70    pub fn timing(&self) -> ConnectionTiming {
71        self.timing
72    }
73
74    /// Drops the send request handler.
75    pub fn drop_send_request(&mut self) {
76        self.send_request = None;
77    }
78}
79
80#[tracing::instrument(skip(io, time))]
81pub async fn tls_connection(
82    conn_type: ConnectionType,
83    domain: &str,
84    timing: &mut ConnectionTiming,
85    io: impl ByteStream,
86    time: &dyn Time,
87) -> anyhow::Result<TlsStream> {
88    let mut builder = SslConnector::builder(SslMethod::tls())?;
89
90    // Use platform CA certs
91    let mut store_builder = X509StoreBuilder::new()?;
92    if let Ok(ca_certs) = rustls_native_certs::load_native_certs() {
93        for root in ca_certs {
94            let _ = store_builder.add_cert(X509::from_der(&root)?);
95        }
96    }
97    builder.set_verify_cert_store(store_builder.build())?;
98    if insecure_tls() {
99        debug!("TLS certificate verification disabled (insecure mode)");
100        builder.set_verify(SslVerifyMode::NONE);
101    } else {
102        builder.set_verify(SslVerifyMode::PEER);
103    }
104
105    let alpn: &[u8] = match conn_type {
106        ConnectionType::H1 { use_tls: false } => {
107            bail!("cannot create tls connection if `use_tls: false`")
108        }
109        ConnectionType::H1 { use_tls: true } => b"\x08http/1.1",
110        ConnectionType::H2 => b"\x02h2",
111        ConnectionType::H3 => b"\x02h3",
112    };
113
114    builder.set_alpn_protos(alpn)?;
115    let config = builder.build().configure()?;
116
117    let ssl_stream = tokio_boring::connect(config, domain, Box::new(io) as Box<dyn ByteStream>)
118        .await
119        .map_err(|e| anyhow::anyhow!("unable to create tls stream: {e}"))?;
120
121    timing.set_secure(time.now());
122
123    // Normalize the TLS handshake time to the number of round-trips the
124    // negotiated version takes (draft-ietf-ippm-responsiveness-09 ยง5.3).
125    // TLS 1.3 completes in 1 round-trip, TLS 1.2 in 2. Default to 1.
126    let tls_round_trips = match ssl_stream.ssl().version2() {
127        Some(SslVersion::TLS1_3) => 1,
128        Some(SslVersion::TLS1_2) => 2,
129        _ => 1,
130    };
131    timing.set_tls_round_trips(tls_round_trips);
132
133    debug!(tls_round_trips, "created tls connection");
134
135    Ok(ssl_stream)
136}
137
138#[tracing::instrument(skip(io, time, shutdown))]
139pub async fn start_h1_conn(
140    domain: String,
141    mut timing: ConnectionTiming,
142    io: impl ByteStream,
143    time: &dyn Time,
144    shutdown: CancellationToken,
145) -> anyhow::Result<EstablishedConnection> {
146    let (send_request, connection) = http1::handshake(TokioIo::new(io)).await?;
147    timing.set_application(time.now());
148
149    tokio::spawn(
150        async move {
151            select! {
152                Err(e) = connection => {
153                    debug!(error=%e, "error running h1 connection");
154                }
155                _ = shutdown.cancelled() => {
156                    debug!("shutting down h1 connection");
157                }
158            }
159
160            info!("connection finished");
161        }
162        .in_current_span(),
163    );
164
165    let established_connection = EstablishedConnection::new(
166        timing,
167        SendRequest::H1 {
168            dispatch: send_request,
169        },
170    );
171
172    Ok(established_connection)
173}
174
175#[tracing::instrument(skip(timing, io, time, shutdown))]
176pub async fn start_h2_conn(
177    addr: SocketAddr,
178    domain: String,
179    mut timing: ConnectionTiming,
180    io: impl ByteStream,
181    time: &dyn Time,
182    shutdown: CancellationToken,
183) -> anyhow::Result<EstablishedConnection> {
184    let (dispatch, connection) = http2::handshake(TokioExecutor, TokioIo::new(io)).await?;
185    timing.set_application(time.now());
186
187    debug!("finished h2 handshake");
188
189    tokio::spawn(
190        async move {
191            select! {
192                Err(e) = connection => {
193                    error!(error=%e, "error running h2 connection");
194                }
195                _ = shutdown.cancelled() => {
196                    debug!("shutting down h2 connection");
197                }
198            }
199
200            info!("connection finished");
201        }
202        .in_current_span(),
203    );
204
205    info!(?timing, "established connection");
206    let established_connection = EstablishedConnection::new(timing, SendRequest::H2 { dispatch });
207
208    Ok(established_connection)
209}
210
211#[derive(Debug)]
212pub enum SendRequest {
213    #[allow(unused)]
214    H1 {
215        dispatch: http1::SendRequest<NqBody>,
216    },
217    H2 {
218        dispatch: http2::SendRequest<NqBody>,
219    },
220}
221
222impl SendRequest {
223    fn send_request(
224        &mut self,
225        mut req: Request<NqBody>,
226    ) -> Pin<Box<dyn Future<Output = hyper::Result<Response<Incoming>>> + Send>> {
227        match self {
228            SendRequest::H1 {
229                dispatch: send_request,
230            } => {
231                // HTTP/1.1 to an origin server requires origin-form request
232                // targets (`GET /path`) plus a `Host` header carrying the
233                // authority. Building the request from an absolute URI leaves
234                // it in proxy/absolute-form (`GET http://host/path`), which
235                // origin servers (e.g. workerd) reject. Normalize here.
236                Self::normalize_h1_request(&mut req);
237                Box::pin(send_request.send_request(req))
238            }
239            SendRequest::H2 {
240                dispatch: send_request,
241            } => {
242                // HTTP/2 uses the :authority pseudo-header derived from the
243                // absolute URI, so leave the request untouched.
244                Box::pin(send_request.send_request(req))
245            }
246        }
247    }
248
249    /// Rewrite an HTTP/1.1 request into origin-form with a proper `Host` header.
250    fn normalize_h1_request(req: &mut Request<NqBody>) {
251        // Set `Host` from the full authority (host and, if present, port).
252        if !req.headers().contains_key(HOST)
253            && let Some(authority) = req.uri().authority().cloned()
254        {
255            if let Ok(host) = HeaderValue::from_str(authority.as_str()) {
256                req.headers_mut().insert(HOST, host);
257            } else {
258                // HTTP/1.1 requires a Host header; without one the origin
259                // answers 400. An authority is already restricted to
260                // characters a header value accepts, so this is not
261                // expected to be reachable -- but a silent drop is
262                // near-impossible to diagnose from the far end.
263                warn!(
264                    %authority,
265                    "could not build a Host header from the URI authority; \
266                     sending the request without one"
267                );
268            }
269        }
270
271        // Collapse the request target to origin-form (path + query only).
272        let path_and_query = req
273            .uri()
274            .path_and_query()
275            .map(|pq| pq.as_str().to_owned())
276            .unwrap_or_else(|| "/".to_owned());
277
278        match path_and_query.parse::<http::Uri>() {
279            Ok(uri) => *req.uri_mut() = uri,
280            // Leaves the request in absolute-form, which origin servers
281            // reject. The string comes from an already-validated
282            // PathAndQuery so this is not expected to be reachable, but the
283            // failure would otherwise be invisible.
284            Err(error) => warn!(
285                path_and_query,
286                %error,
287                "failed to parse origin-form URI; sending absolute-form"
288            ),
289        }
290    }
291}
292
293#[derive(Clone)]
294struct TokioExecutor;
295
296impl<F> hyper::rt::Executor<F> for TokioExecutor
297where
298    F: Future + Send + 'static,
299    F::Output: Send + 'static,
300{
301    fn execute(&self, future: F) {
302        tokio::spawn(future);
303    }
304}