use super::error::{Error, Result};
use super::stats::{HttpStat, ALPN_HTTP1, ALPN_HTTP2};
use bytes::Bytes;
use http::request::Builder;
use http::HeaderValue;
use http::Request;
use http::Uri;
use http::{HeaderMap, Method};
use http_body_util::Full;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
use std::time::Instant;
const VERSION: &str = env!("CARGO_PKG_VERSION");
pub(crate) fn finish_with_error(
mut stat: HttpStat,
error: impl ToString,
start: Instant,
) -> HttpStat {
stat.error = Some(error.to_string());
stat.total = Some(start.elapsed());
stat
}
#[derive(Debug, Clone)]
pub struct ConnectTo {
src_host: String,
src_port: Option<u16>,
pub dst_host: String,
pub dst_port: Option<u16>,
}
fn parse_host_segment(s: &str) -> (String, &str) {
if let Some(rest) = s.strip_prefix('[') {
if let Some(end) = rest.find(']') {
return (rest[..end].to_string(), &rest[end + 1..]);
}
}
let colon = s.find(':').unwrap_or(s.len());
(s[..colon].to_string(), &s[colon..])
}
impl ConnectTo {
pub fn parse(s: &str) -> Option<Self> {
let (src_host, rest) = parse_host_segment(s);
let rest = rest.strip_prefix(':')?;
let colon = rest.find(':')?;
let src_port = if rest[..colon].is_empty() {
None
} else {
Some(rest[..colon].parse().ok()?)
};
let rest = &rest[colon + 1..];
let (dst_host, rest) = parse_host_segment(rest);
let port2_str = rest.strip_prefix(':').unwrap_or(rest);
let dst_port = if port2_str.is_empty() {
None
} else {
Some(port2_str.parse().ok()?)
};
Some(ConnectTo {
src_host,
src_port,
dst_host,
dst_port,
})
}
pub fn matches(&self, host: &str, port: u16) -> bool {
let host_ok = self.src_host.is_empty() || self.src_host.eq_ignore_ascii_case(host);
let port_ok = self.src_port.is_none() || self.src_port == Some(port);
host_ok && port_ok
}
}
#[derive(Default, Debug, Clone)]
pub struct HttpRequest {
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>, }
impl HttpRequest {
pub fn get_port(&self) -> u16 {
let schema = if let Some(scheme) = self.uri.scheme() {
scheme.to_string()
} else {
"".to_string()
};
let default_port = if ["https", "grpcs"].contains(&schema.as_str()) {
443
} else {
80
};
self.uri.port_u16().unwrap_or(default_port)
}
pub fn builder(&self, is_http1: bool) -> Builder {
let uri = &self.uri;
let method = if let Some(method) = &self.method {
Method::from_str(method).unwrap_or(Method::GET)
} else {
Method::GET
};
let mut builder = if is_http1 && !self.use_absolute_uri {
if let Some(value) = uri.path_and_query() {
Request::builder().uri(value.to_string())
} else {
Request::builder().uri(uri)
}
} else {
Request::builder().uri(uri)
};
builder = builder.method(method);
let mut set_host = false;
let mut set_user_agent = false;
if let Some(headers) = &self.headers {
for (key, value) in headers.iter() {
builder = builder.header(key, value);
match key.to_string().to_lowercase().as_str() {
"host" => set_host = true,
"user-agent" => set_user_agent = true,
_ => {}
}
}
}
if !set_host {
if let Some(host) = uri.host() {
let port = self.get_port();
if port != 80 && port != 443 {
builder = builder.header("Host", format!("{host}:{port}"));
} else {
builder = builder.header("Host", host);
}
}
}
if !set_user_agent {
builder = builder.header("User-Agent", format!("httpstat.rs/{VERSION}"));
}
builder
}
}
impl TryFrom<&str> for HttpRequest {
type Error = Error;
fn try_from(url: &str) -> Result<Self> {
let prefixes = ["http://", "https://", "grpc://", "grpcs://"];
let value = if prefixes.iter().any(|prefix| url.starts_with(prefix)) {
url.to_string()
} else {
format!("http://{url}")
};
let uri = value.parse::<Uri>().map_err(|e| Error::Uri { source: e })?;
Ok(Self {
uri,
alpn_protocols: vec![ALPN_HTTP2.to_string(), ALPN_HTTP1.to_string()],
..Default::default()
})
}
}
impl TryFrom<&HttpRequest> for Request<Full<Bytes>> {
type Error = Error;
fn try_from(req: &HttpRequest) -> Result<Self> {
req.builder(true)
.body(Full::new(req.body.clone().unwrap_or_default()))
.map_err(|e| Error::Http { source: e })
}
}
pub(crate) fn build_http_request(
req: &HttpRequest,
is_http1: bool,
) -> Result<Request<Full<Bytes>>> {
req.builder(is_http1)
.body(Full::new(req.body.clone().unwrap_or_default()))
.map_err(|e| Error::Http { source: e })
}