Add multiple features

This commit is contained in:
DaZuo0122
2026-01-16 23:16:58 +08:00
parent c367ca29e4
commit cb022127c0
18 changed files with 1883 additions and 4 deletions

View File

@@ -0,0 +1,11 @@
[package]
name = "wtfnet-http"
version = "0.1.0"
edition = "2024"
[dependencies]
reqwest = { version = "0.11", features = ["rustls-tls"] }
serde = { version = "1", features = ["derive"] }
thiserror = "2"
tokio = { version = "1", features = ["net", "time"] }
url = "2"

View File

@@ -0,0 +1,182 @@
use reqwest::{Client, Method, StatusCode};
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
use std::time::{Duration, Instant};
use tokio::net::lookup_host;
use thiserror::Error;
use url::Url;
#[derive(Debug, Error)]
pub enum HttpError {
#[error("invalid url: {0}")]
Url(String),
#[error("request error: {0}")]
Request(String),
#[error("response error: {0}")]
Response(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpTiming {
pub total_ms: u128,
pub dns_ms: Option<u128>,
pub connect_ms: Option<u128>,
pub tls_ms: Option<u128>,
pub ttfb_ms: Option<u128>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpReport {
pub url: String,
pub final_url: Option<String>,
pub method: String,
pub status: Option<u16>,
pub http_version: Option<String>,
pub resolved_ips: Vec<String>,
pub headers: Vec<(String, String)>,
pub body: Option<String>,
pub timing: HttpTiming,
}
#[derive(Debug, Clone, Copy)]
pub enum HttpMethod {
Head,
Get,
}
impl HttpMethod {
fn to_reqwest(self) -> Method {
match self {
HttpMethod::Head => Method::HEAD,
HttpMethod::Get => Method::GET,
}
}
}
#[derive(Debug, Clone)]
pub struct HttpRequestOptions {
pub method: HttpMethod,
pub timeout_ms: u64,
pub follow_redirects: Option<u32>,
pub max_body_bytes: usize,
pub show_headers: bool,
pub show_body: bool,
pub http1_only: bool,
pub http2_only: bool,
}
pub async fn request(url: &str, opts: HttpRequestOptions) -> Result<HttpReport, HttpError> {
let parsed = Url::parse(url).map_err(|err| HttpError::Url(err.to_string()))?;
let host = parsed
.host_str()
.ok_or_else(|| HttpError::Url("missing host".to_string()))?;
let port = parsed
.port_or_known_default()
.ok_or_else(|| HttpError::Url("missing port".to_string()))?;
let mut resolved_ips = Vec::new();
let dns_start = Instant::now();
if let Ok(ip) = host.parse::<IpAddr>() {
resolved_ips.push(ip.to_string());
} else {
let addrs = lookup_host((host, port))
.await
.map_err(|err| HttpError::Request(err.to_string()))?;
for addr in addrs {
resolved_ips.push(addr.ip().to_string());
}
resolved_ips.sort();
resolved_ips.dedup();
if resolved_ips.is_empty() {
return Err(HttpError::Request("no addresses resolved".to_string()));
}
}
let dns_ms = dns_start.elapsed().as_millis();
let mut builder = Client::builder().timeout(Duration::from_millis(opts.timeout_ms));
builder = if let Some(max) = opts.follow_redirects {
builder.redirect(reqwest::redirect::Policy::limited(max as usize))
} else {
builder.redirect(reqwest::redirect::Policy::none())
};
if opts.http1_only {
builder = builder.http1_only();
}
if opts.http2_only {
builder = builder.http2_prior_knowledge();
}
if let Some(first) = resolved_ips.first() {
if let Ok(ip) = first.parse::<IpAddr>() {
let addr = SocketAddr::new(ip, port);
builder = builder.resolve(host, addr);
}
}
let client = builder.build().map_err(|err| HttpError::Request(err.to_string()))?;
let start = Instant::now();
let response = client
.request(opts.method.to_reqwest(), parsed.clone())
.send()
.await
.map_err(|err| HttpError::Request(err.to_string()))?;
let ttfb_ms = start.elapsed().as_millis();
let status = response.status();
let final_url = response.url().to_string();
let version = response.version();
let headers = if opts.show_headers {
response
.headers()
.iter()
.map(|(name, value)| {
let value = value.to_str().unwrap_or("-").to_string();
(name.to_string(), value)
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let body = if opts.show_body {
let bytes = response
.bytes()
.await
.map_err(|err| HttpError::Response(err.to_string()))?;
let sliced = if bytes.len() > opts.max_body_bytes {
&bytes[..opts.max_body_bytes]
} else {
&bytes
};
Some(String::from_utf8_lossy(sliced).to_string())
} else {
None
};
let total_ms = start.elapsed().as_millis();
Ok(HttpReport {
url: url.to_string(),
final_url: Some(final_url),
method: match opts.method {
HttpMethod::Head => "HEAD".to_string(),
HttpMethod::Get => "GET".to_string(),
},
status: status_code(status),
http_version: Some(format!("{version:?}")),
resolved_ips,
headers,
body,
timing: HttpTiming {
total_ms,
dns_ms: Some(dns_ms),
connect_ms: None,
tls_ms: None,
ttfb_ms: Some(ttfb_ms),
},
})
}
fn status_code(status: StatusCode) -> Option<u16> {
Some(status.as_u16())
}