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 std::net::IpAddr;
28use std::str::FromStr;
29use std::time::Duration;
30use std::time::Instant;
31
32// Version information from Cargo.toml
33const VERSION: &str = env!("CARGO_PKG_VERSION");
34
35// Handle request error and update statistics
36pub(crate) fn finish_with_error(
37    mut stat: HttpStat,
38    error: impl ToString,
39    start: Instant,
40) -> HttpStat {
41    stat.error = Some(error.to_string());
42    stat.total = Some(start.elapsed());
43    stat
44}
45
46// HttpRequest struct to hold request configuration
47#[derive(Default, Debug, Clone)]
48pub struct HttpRequest {
49    pub uri: Uri,                                // Target URI
50    pub method: Option<String>,                  // HTTP method (GET, POST, etc.)
51    pub alpn_protocols: Vec<String>,             // Supported ALPN protocols
52    pub resolve: Option<IpAddr>,                 // Custom DNS resolution
53    pub headers: Option<HeaderMap<HeaderValue>>, // Custom HTTP headers
54    pub ip_version: Option<i32>,                 // IP version (4 for IPv4, 6 for IPv6)
55    pub skip_verify: bool,                       // Skip TLS certificate verification
56    pub body: Option<Bytes>,                     // Request body
57    pub dns_servers: Option<Vec<String>>,        // DNS servers
58    pub dns_timeout: Option<Duration>,           // DNS resolution timeout
59    pub tcp_timeout: Option<Duration>,           // TCP connection timeout
60    pub tls_timeout: Option<Duration>,           // TLS handshake timeout
61    pub request_timeout: Option<Duration>,       // HTTP request timeout
62    pub quic_timeout: Option<Duration>,          // QUIC connection timeout
63}
64
65impl HttpRequest {
66    pub fn get_port(&self) -> u16 {
67        let schema = if let Some(scheme) = self.uri.scheme() {
68            scheme.to_string()
69        } else {
70            "".to_string()
71        };
72
73        let default_port = if ["https", "grpcs"].contains(&schema.as_str()) {
74            443
75        } else {
76            80
77        };
78        self.uri.port_u16().unwrap_or(default_port)
79    }
80    // Build HTTP request with proper headers
81    pub fn builder(&self, is_http1: bool) -> Builder {
82        let uri = &self.uri;
83        let method = if let Some(method) = &self.method {
84            Method::from_str(method).unwrap_or(Method::GET)
85        } else {
86            Method::GET
87        };
88        let mut builder = if is_http1 {
89            if let Some(value) = uri.path_and_query() {
90                Request::builder().uri(value.to_string())
91            } else {
92                Request::builder().uri(uri)
93            }
94        } else {
95            Request::builder().uri(uri)
96        };
97        builder = builder.method(method);
98        let mut set_host = false;
99        let mut set_user_agent = false;
100
101        // Add custom headers if provided
102        if let Some(headers) = &self.headers {
103            for (key, value) in headers.iter() {
104                builder = builder.header(key, value);
105                match key.to_string().to_lowercase().as_str() {
106                    "host" => set_host = true,
107                    "user-agent" => set_user_agent = true,
108                    _ => {}
109                }
110            }
111        }
112
113        // Set default Host header if not provided
114        if !set_host {
115            if let Some(host) = uri.host() {
116                builder = builder.header("Host", host);
117            }
118        }
119
120        // Set default User-Agent if not provided
121        if !set_user_agent {
122            builder = builder.header("User-Agent", format!("httpstat.rs/{VERSION}"));
123        }
124        builder
125    }
126}
127
128// Convert string URL to HttpRequest
129impl TryFrom<&str> for HttpRequest {
130    type Error = Error;
131
132    fn try_from(url: &str) -> Result<Self> {
133        let prefixes = ["http://", "https://", "grpc://", "grpcs://"];
134
135        let value = if prefixes.iter().any(|prefix| url.starts_with(prefix)) {
136            url.to_string()
137        } else {
138            format!("http://{url}")
139        };
140        let uri = value.parse::<Uri>().map_err(|e| Error::Uri { source: e })?;
141        Ok(Self {
142            uri,
143            alpn_protocols: vec![ALPN_HTTP2.to_string(), ALPN_HTTP1.to_string()],
144            ..Default::default()
145        })
146    }
147}
148
149// Convert HttpRequest to hyper Request
150impl TryFrom<&HttpRequest> for Request<Full<Bytes>> {
151    type Error = Error;
152    fn try_from(req: &HttpRequest) -> Result<Self> {
153        req.builder(true)
154            .body(Full::new(req.body.clone().unwrap_or_default()))
155            .map_err(|e| Error::Http { source: e })
156    }
157}
158
159pub(crate) fn build_http_request(
160    req: &HttpRequest,
161    is_http1: bool,
162) -> Result<Request<Full<Bytes>>> {
163    req.builder(is_http1)
164        .body(Full::new(req.body.clone().unwrap_or_default()))
165        .map_err(|e| Error::Http { source: e })
166}