Skip to main content

http_stat/
http_request.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// This file implements HTTP request functionality with support for HTTP/1.1, HTTP/2, and HTTP/3
16// It includes features like DNS resolution, TLS handshake, and request/response handling
17
18use super::error::{Error, Result};
19use super::stats::{HttpStat, ALPN_HTTP1, ALPN_HTTP2};
20use bytes::Bytes;
21use http::request::Builder;
22use http::HeaderValue;
23use http::Request;
24use http::Uri;
25use http::{HeaderMap, Method};
26use http_body_util::Full;
27use rustls::client::{ClientSessionMemoryCache, ClientSessionStore};
28use std::net::IpAddr;
29use std::str::FromStr;
30use std::sync::Arc;
31use std::time::Duration;
32use std::time::Instant;
33
34// Version information from Cargo.toml
35const VERSION: &str = env!("CARGO_PKG_VERSION");
36
37// Handle request error and update statistics
38pub(crate) fn finish_with_error(
39    mut stat: HttpStat,
40    error: impl ToString,
41    start: Instant,
42) -> HttpStat {
43    stat.error = Some(error.to_string());
44    stat.total = Some(start.elapsed());
45    stat
46}
47
48/// A single `--connect-to HOST1:PORT1:HOST2:PORT2` entry.
49///
50/// When the request target matches `(src_host, src_port)`, the TCP connection is
51/// established to `(dst_host, dst_port)`. TLS SNI and the HTTP `Host` header still
52/// use the original hostname — only the actual TCP destination changes.
53///
54/// Empty `src_host` / `src_port` act as wildcards. Empty `dst_host` / absent `dst_port`
55/// keep the original value.
56#[derive(Debug, Clone)]
57pub struct ConnectTo {
58    src_host: String,
59    src_port: Option<u16>,
60    pub dst_host: String,
61    pub dst_port: Option<u16>,
62}
63
64fn parse_host_segment(s: &str) -> (String, &str) {
65    if let Some(rest) = s.strip_prefix('[') {
66        // IPv6 bracketed: [addr]...
67        if let Some(end) = rest.find(']') {
68            return (rest[..end].to_string(), &rest[end + 1..]);
69        }
70    }
71    // Plain host: take up to first ':'
72    let colon = s.find(':').unwrap_or(s.len());
73    (s[..colon].to_string(), &s[colon..])
74}
75
76impl ConnectTo {
77    /// Parse `HOST1:PORT1:HOST2:PORT2`. Any field may be empty; IPv6 uses `[addr]`.
78    pub fn parse(s: &str) -> Option<Self> {
79        let (src_host, rest) = parse_host_segment(s);
80        let rest = rest.strip_prefix(':')?; // require separator after HOST1
81
82        // PORT1 up to next ':'
83        let colon = rest.find(':')?;
84        let src_port = if rest[..colon].is_empty() {
85            None
86        } else {
87            Some(rest[..colon].parse().ok()?)
88        };
89        let rest = &rest[colon + 1..];
90
91        // HOST2
92        let (dst_host, rest) = parse_host_segment(rest);
93
94        // Optional ':PORT2'
95        let port2_str = rest.strip_prefix(':').unwrap_or(rest);
96        let dst_port = if port2_str.is_empty() {
97            None
98        } else {
99            Some(port2_str.parse().ok()?)
100        };
101
102        Some(ConnectTo {
103            src_host,
104            src_port,
105            dst_host,
106            dst_port,
107        })
108    }
109
110    /// Returns `true` if this entry applies to the given `(host, port)`.
111    pub fn matches(&self, host: &str, port: u16) -> bool {
112        let host_ok = self.src_host.is_empty() || self.src_host.eq_ignore_ascii_case(host);
113        let port_ok = self.src_port.is_none() || self.src_port == Some(port);
114        host_ok && port_ok
115    }
116}
117
118// HttpRequest struct to hold request configuration
119#[derive(Default, Debug, Clone)]
120pub struct HttpRequest {
121    pub uri: Uri,                                // Target URI
122    pub method: Option<String>,                  // HTTP method (GET, POST, etc.)
123    pub alpn_protocols: Vec<String>,             // Supported ALPN protocols
124    pub resolve: Option<IpAddr>,                 // Custom DNS resolution
125    pub headers: Option<HeaderMap<HeaderValue>>, // Custom HTTP headers
126    pub ip_version: Option<i32>,                 // IP version (4 for IPv4, 6 for IPv6)
127    pub skip_verify: bool,                       // Skip TLS certificate verification
128    pub body: Option<Bytes>,                     // Request body
129    pub dns_servers: Option<Vec<String>>,        // DNS servers
130    pub dns_timeout: Option<Duration>,           // DNS resolution timeout
131    pub tcp_timeout: Option<Duration>,           // TCP connection timeout
132    pub tls_timeout: Option<Duration>,           // TLS handshake timeout
133    pub request_timeout: Option<Duration>,       // HTTP request timeout
134    pub quic_timeout: Option<Duration>,          // QUIC connection timeout
135    pub client_cert: Option<Vec<u8>>,            // PEM-encoded client certificate (mTLS)
136    pub client_key: Option<Vec<u8>>,             // PEM-encoded client private key (mTLS)
137    pub proxy: Option<String>,                   // Proxy URL (http://, https://, socks5://)
138    pub use_absolute_uri: bool,                  // Send absolute URI (HTTP forward proxy)
139    pub connect_to: Vec<String>,                 // --connect-to HOST1:PORT1:HOST2:PORT2 overrides
140    pub bind_addr: Option<IpAddr>,               // Local source IP to bind before connecting
141    /// Optional shared TLS session store. When set, the rustls `ClientConfig`
142    /// is wired with `Resumption::store(...)` and `enable_early_data = true`,
143    /// so subsequent requests sharing this store can perform a resumed
144    /// handshake and attempt 0-RTT. Intended for the `-n` benchmark loop:
145    /// install one cache before the loop and clone it onto every request.
146    pub tls_session_store: Option<Arc<dyn ClientSessionStore>>,
147}
148
149impl HttpRequest {
150    pub fn get_port(&self) -> u16 {
151        let schema = if let Some(scheme) = self.uri.scheme() {
152            scheme.to_string()
153        } else {
154            "".to_string()
155        };
156
157        let default_port = if ["https", "grpcs"].contains(&schema.as_str()) {
158            443
159        } else {
160            80
161        };
162        self.uri.port_u16().unwrap_or(default_port)
163    }
164    // Build HTTP request with proper headers
165    pub fn builder(&self, is_http1: bool) -> Builder {
166        let uri = &self.uri;
167        let method = if let Some(method) = &self.method {
168            Method::from_str(method).unwrap_or(Method::GET)
169        } else {
170            Method::GET
171        };
172        let mut builder = if is_http1 && !self.use_absolute_uri {
173            if let Some(value) = uri.path_and_query() {
174                Request::builder().uri(value.to_string())
175            } else {
176                Request::builder().uri(uri)
177            }
178        } else {
179            Request::builder().uri(uri)
180        };
181        builder = builder.method(method);
182        let mut set_host = false;
183        let mut set_user_agent = false;
184
185        // Add custom headers if provided
186        if let Some(headers) = &self.headers {
187            for (key, value) in headers.iter() {
188                builder = builder.header(key, value);
189                match key.to_string().to_lowercase().as_str() {
190                    "host" => set_host = true,
191                    "user-agent" => set_user_agent = true,
192                    _ => {}
193                }
194            }
195        }
196
197        // Set default Host header if not provided
198        if !set_host {
199            if let Some(host) = uri.host() {
200                let port = self.get_port();
201                if port != 80 && port != 443 {
202                    builder = builder.header("Host", format!("{host}:{port}"));
203                } else {
204                    builder = builder.header("Host", host);
205                }
206            }
207        }
208
209        // Set default User-Agent if not provided
210        if !set_user_agent {
211            builder = builder.header("User-Agent", format!("httpstat.rs/{VERSION}"));
212        }
213        builder
214    }
215}
216
217/// Create an in-memory TLS session store suitable for sharing across multiple
218/// requests (e.g. across a benchmark `-n` loop). Pass the returned `Arc` into
219/// `HttpRequest::tls_session_store` on every request that should be able to
220/// resume a previous TLS session.
221pub fn new_tls_session_store(capacity: usize) -> Arc<dyn ClientSessionStore> {
222    Arc::new(ClientSessionMemoryCache::new(capacity))
223}
224
225// Convert string URL to HttpRequest
226impl TryFrom<&str> for HttpRequest {
227    type Error = Error;
228
229    fn try_from(url: &str) -> Result<Self> {
230        let prefixes = ["http://", "https://", "grpc://", "grpcs://"];
231
232        let value = if prefixes.iter().any(|prefix| url.starts_with(prefix)) {
233            url.to_string()
234        } else {
235            format!("http://{url}")
236        };
237        let uri = value.parse::<Uri>().map_err(|e| Error::Uri { source: e })?;
238        Ok(Self {
239            uri,
240            alpn_protocols: vec![ALPN_HTTP2.to_string(), ALPN_HTTP1.to_string()],
241            ..Default::default()
242        })
243    }
244}
245
246// Convert HttpRequest to hyper Request
247impl TryFrom<&HttpRequest> for Request<Full<Bytes>> {
248    type Error = Error;
249    fn try_from(req: &HttpRequest) -> Result<Self> {
250        req.builder(true)
251            .body(Full::new(req.body.clone().unwrap_or_default()))
252            .map_err(|e| Error::Http { source: e })
253    }
254}