use crate::auth::AuthMethod;
use crate::network::{Http2Config, TlsConfig};
use crate::stats::{RequestResult, StatsCollector};
use crate::user_agent::UserAgentManager;
use anyhow::Result;
use chrono::Utc;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use reqwest::{Client, Method};
use std::sync::Arc;
use std::time::{Duration, Instant};
pub struct HttpClient {
client: Client,
url: String,
method: Method,
headers: HeaderMap,
payload: Option<String>,
user_agent_manager: Arc<UserAgentManager>,
stats_collector: Arc<StatsCollector>,
auth_method: Arc<AuthMethod>,
http2_config: Arc<Http2Config>,
tls_config: Arc<TlsConfig>,
}
impl HttpClient {
#[allow(clippy::too_many_arguments)]
pub fn new(
url: String,
method: Method,
headers: Vec<String>,
payload: Option<String>,
user_agent_manager: Arc<UserAgentManager>,
stats_collector: Arc<StatsCollector>,
timeout: Option<Duration>,
auth_method: Arc<AuthMethod>,
http2_config: Arc<Http2Config>,
tls_config: Arc<TlsConfig>,
) -> Result<Self> {
let mut client_builder = Client::builder();
if let Some(timeout) = timeout {
client_builder = client_builder.timeout(timeout);
}
client_builder = http2_config.apply_to_client_builder(client_builder);
client_builder = tls_config.apply_to_client_builder(client_builder)?;
let client = client_builder.build()?;
let mut header_map = HeaderMap::new();
for header in headers {
if let Some((key, value)) = header.split_once(':') {
let key = key.trim();
let value = value.trim();
if let (Ok(name), Ok(val)) = (
HeaderName::from_bytes(key.as_bytes()),
HeaderValue::from_str(value),
) {
header_map.insert(name, val);
}
}
}
Ok(Self {
client,
url,
method,
headers: header_map,
payload,
user_agent_manager,
stats_collector,
auth_method,
http2_config,
tls_config,
})
}
pub async fn send_request(&self) -> Result<()> {
let start = Instant::now();
let timestamp = Utc::now();
let user_agent = self.user_agent_manager.get_user_agent().to_string();
let mut url = self.url.clone();
if let Some((key, value)) = self.auth_method.get_query_params().await {
let separator = if url.contains('?') { "&" } else { "?" };
url = format!("{url}{separator}{key}={value}");
}
let mut request_builder = self.client.request(self.method.clone(), &url);
request_builder = request_builder.header("User-Agent", &user_agent);
if let Some((key, value)) = self.auth_method.get_auth_header().await {
request_builder = request_builder.header(key, value);
}
for (key, value) in &self.headers {
request_builder = request_builder.header(key, value);
}
if let Some(payload) = &self.payload {
if self.method == Method::POST
|| self.method == Method::PUT
|| self.method == Method::PATCH
{
request_builder = request_builder.body(payload.clone());
if !self.headers.contains_key("content-type")
&& (payload.trim_start().starts_with('{')
|| payload.trim_start().starts_with('['))
{
request_builder = request_builder.header("Content-Type", "application/json");
}
}
}
let request = request_builder.build()?;
match self.client.execute(request).await {
Ok(response) => {
let status_code = response.status().as_u16();
let content_length = response.content_length().unwrap_or(0);
let duration = start.elapsed().as_millis() as u64;
let error = if status_code >= 400 {
Some(format!(
"HTTP {}: {}",
status_code,
response.status().canonical_reason().unwrap_or("Unknown")
))
} else {
None
};
let result = RequestResult {
timestamp,
duration_ms: duration,
status_code: Some(status_code),
error,
user_agent: Some(user_agent),
bytes_received: content_length,
};
self.stats_collector.record_request(result).await;
Ok(())
}
Err(e) => {
let duration = start.elapsed().as_millis() as u64;
let result = RequestResult {
timestamp,
duration_ms: duration,
status_code: None,
error: Some(e.to_string()),
user_agent: Some(user_agent),
bytes_received: 0,
};
self.stats_collector.record_request(result).await;
Err(e.into())
}
}
}
pub fn get_http2_config(&self) -> &Http2Config {
&self.http2_config
}
pub fn get_tls_config(&self) -> &TlsConfig {
&self.tls_config
}
pub fn get_protocol_info(&self) -> crate::network::ProtocolInfo {
self.http2_config.get_protocol_info()
}
pub fn get_tls_info(&self) -> crate::network::TlsInfo {
self.tls_config.get_summary()
}
}