1use 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
34const VERSION: &str = env!("CARGO_PKG_VERSION");
36
37pub(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#[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 if let Some(end) = rest.find(']') {
68 return (rest[..end].to_string(), &rest[end + 1..]);
69 }
70 }
71 let colon = s.find(':').unwrap_or(s.len());
73 (s[..colon].to_string(), &s[colon..])
74}
75
76impl ConnectTo {
77 pub fn parse(s: &str) -> Option<Self> {
79 let (src_host, rest) = parse_host_segment(s);
80 let rest = rest.strip_prefix(':')?; 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 let (dst_host, rest) = parse_host_segment(rest);
93
94 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 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#[derive(Default, Debug, Clone)]
120pub struct HttpRequest {
121 pub uri: Uri, pub method: Option<String>, pub alpn_protocols: Vec<String>, pub resolve: Option<IpAddr>, pub headers: Option<HeaderMap<HeaderValue>>, pub ip_version: Option<i32>, pub skip_verify: bool, pub body: Option<Bytes>, pub dns_servers: Option<Vec<String>>, pub dns_timeout: Option<Duration>, pub tcp_timeout: Option<Duration>, pub tls_timeout: Option<Duration>, pub request_timeout: Option<Duration>, pub quic_timeout: Option<Duration>, pub client_cert: Option<Vec<u8>>, pub client_key: Option<Vec<u8>>, pub proxy: Option<String>, pub use_absolute_uri: bool, pub connect_to: Vec<String>, pub bind_addr: Option<IpAddr>, pub max_body_size: Option<usize>,
146 pub tls_session_store: Option<Arc<dyn ClientSessionStore>>,
152}
153
154impl HttpRequest {
155 pub fn get_port(&self) -> u16 {
156 let scheme = self.uri.scheme_str().unwrap_or("");
157 let default_port = if matches!(scheme, "https" | "grpcs") {
158 443
159 } else {
160 80
161 };
162 self.uri.port_u16().unwrap_or(default_port)
163 }
164 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 if let Some(headers) = &self.headers {
187 for (key, value) in headers.iter() {
188 builder = builder.header(key, value);
189 match key.as_str() {
192 "host" => set_host = true,
193 "user-agent" => set_user_agent = true,
194 _ => {}
195 }
196 }
197 }
198
199 if !set_host {
201 if let Some(host) = uri.host() {
202 let port = self.get_port();
203 if port != 80 && port != 443 {
204 builder = builder.header("Host", format!("{host}:{port}"));
205 } else {
206 builder = builder.header("Host", host);
207 }
208 }
209 }
210
211 if !set_user_agent {
213 builder = builder.header("User-Agent", format!("httpstat.rs/{VERSION}"));
214 }
215 builder
216 }
217}
218
219pub fn new_tls_session_store(capacity: usize) -> Arc<dyn ClientSessionStore> {
224 Arc::new(ClientSessionMemoryCache::new(capacity))
225}
226
227impl TryFrom<&str> for HttpRequest {
229 type Error = Error;
230
231 fn try_from(url: &str) -> Result<Self> {
232 let prefixes = ["http://", "https://", "grpc://", "grpcs://"];
233
234 let value = if prefixes.iter().any(|prefix| url.starts_with(prefix)) {
235 url.to_string()
236 } else {
237 format!("http://{url}")
238 };
239 let uri = value.parse::<Uri>().map_err(|e| Error::Uri { source: e })?;
240 Ok(Self {
241 uri,
242 alpn_protocols: vec![ALPN_HTTP2.to_string(), ALPN_HTTP1.to_string()],
243 ..Default::default()
244 })
245 }
246}
247
248impl TryFrom<&HttpRequest> for Request<Full<Bytes>> {
250 type Error = Error;
251 fn try_from(req: &HttpRequest) -> Result<Self> {
252 req.builder(true)
253 .body(Full::new(req.body.clone().unwrap_or_default()))
254 .map_err(|e| Error::Http { source: e })
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
264 fn parse_host_segment_plain_and_bracketed() {
265 assert_eq!(parse_host_segment("host:443"), ("host".to_string(), ":443"));
266 assert_eq!(parse_host_segment("[::1]:443"), ("::1".to_string(), ":443"));
267 assert_eq!(parse_host_segment("host"), ("host".to_string(), ""));
268 }
269
270 #[test]
272 fn connect_to_full_entry() {
273 let c = ConnectTo::parse("example.com:443:1.2.3.4:8443").unwrap();
274 assert_eq!(c.dst_host, "1.2.3.4");
275 assert_eq!(c.dst_port, Some(8443));
276 assert!(c.matches("example.com", 443));
277 assert!(c.matches("EXAMPLE.COM", 443)); assert!(!c.matches("other.com", 443));
279 assert!(!c.matches("example.com", 80));
280 }
281
282 #[test]
283 fn connect_to_wildcards() {
284 let any_host = ConnectTo::parse(":443:1.2.3.4:8443").unwrap();
286 assert!(any_host.matches("whatever.com", 443));
287 assert!(!any_host.matches("whatever.com", 80));
288
289 let any_port = ConnectTo::parse("example.com::1.2.3.4:8443").unwrap();
291 assert!(any_port.matches("example.com", 1));
292 assert!(any_port.matches("example.com", 65535));
293 assert!(!any_port.matches("other.com", 443));
294 }
295
296 #[test]
297 fn connect_to_ipv6_and_optional_dst_port() {
298 let dst6 = ConnectTo::parse("example.com:443:[2001:db8::1]:8443").unwrap();
299 assert_eq!(dst6.dst_host, "2001:db8::1");
300 assert_eq!(dst6.dst_port, Some(8443));
301
302 let src6 = ConnectTo::parse("[2001:db8::1]:443:1.2.3.4:8443").unwrap();
303 assert!(src6.matches("2001:db8::1", 443));
304
305 let no_dst_port = ConnectTo::parse("example.com:443:1.2.3.4").unwrap();
307 assert_eq!(no_dst_port.dst_host, "1.2.3.4");
308 assert_eq!(no_dst_port.dst_port, None);
309 }
310
311 #[test]
312 fn connect_to_rejects_malformed() {
313 assert!(ConnectTo::parse("example.com").is_none()); assert!(ConnectTo::parse("example.com:443").is_none()); assert!(ConnectTo::parse("example.com:notaport:1.2.3.4:8443").is_none());
316 }
318
319 #[test]
321 fn try_from_adds_scheme_and_default_alpn() {
322 let req = HttpRequest::try_from("example.com").unwrap();
323 assert_eq!(req.uri.scheme_str(), Some("http"));
324 assert_eq!(
325 req.alpn_protocols,
326 vec![ALPN_HTTP2.to_string(), ALPN_HTTP1.to_string()]
327 );
328
329 assert_eq!(
330 HttpRequest::try_from("https://example.com/path")
331 .unwrap()
332 .uri
333 .scheme_str(),
334 Some("https")
335 );
336 assert_eq!(
337 HttpRequest::try_from("grpc://svc:50051")
338 .unwrap()
339 .uri
340 .scheme_str(),
341 Some("grpc")
342 );
343 }
344
345 #[test]
347 fn get_port_defaults_by_scheme() {
348 assert_eq!(HttpRequest::try_from("http://x").unwrap().get_port(), 80);
349 assert_eq!(HttpRequest::try_from("https://x").unwrap().get_port(), 443);
350 assert_eq!(HttpRequest::try_from("grpcs://x").unwrap().get_port(), 443);
351 assert_eq!(
352 HttpRequest::try_from("http://x:8080").unwrap().get_port(),
353 8080
354 );
355 }
356
357 #[test]
359 fn builder_sets_default_host_and_user_agent() {
360 let req = HttpRequest::try_from("http://example.com/path").unwrap();
361 let r = req.builder(true).body(()).unwrap();
362 assert_eq!(r.headers().get("host").unwrap(), "example.com");
363 assert!(r
364 .headers()
365 .get("user-agent")
366 .unwrap()
367 .to_str()
368 .unwrap()
369 .starts_with("httpstat.rs/"));
370 }
371
372 #[test]
373 fn builder_includes_nondefault_port_in_host() {
374 let req = HttpRequest::try_from("http://example.com:8080/").unwrap();
375 let r = req.builder(true).body(()).unwrap();
376 assert_eq!(r.headers().get("host").unwrap(), "example.com:8080");
377 }
378
379 #[test]
380 fn builder_respects_custom_host_header() {
381 let mut req = HttpRequest::try_from("http://example.com/").unwrap();
382 let mut hm = HeaderMap::new();
383 hm.insert(http::header::HOST, HeaderValue::from_static("custom.test"));
384 req.headers = Some(hm);
385 let r = req.builder(true).body(()).unwrap();
386 assert_eq!(r.headers().get("host").unwrap(), "custom.test");
387 }
388}