use std::error::Error;
use std::net::{IpAddr, SocketAddr};
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
use reqwest::redirect::Policy;
use crate::HttpConfigError;
use crate::{HttpClient, HttpClientOptions, HttpError, HttpResult};
use qubit_config::ConfigReader;
#[derive(Debug, Clone, Copy, Default)]
struct Ipv4OnlyResolver;
impl Resolve for Ipv4OnlyResolver {
fn resolve(&self, name: Name) -> Resolving {
let host = name.as_str().to_string();
Box::pin(async move {
let resolved = tokio::net::lookup_host((host.as_str(), 0))
.await
.map_err(|error| Box::new(error) as Box<dyn std::error::Error + Send + Sync>)?;
filter_ipv4_addrs(&host, resolved)
})
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct HttpClientFactory;
impl HttpClientFactory {
pub fn new() -> Self {
Self
}
pub fn create_default(&self) -> HttpResult<HttpClient> {
self.create(HttpClientOptions::default())
}
pub fn create(&self, options: HttpClientOptions) -> HttpResult<HttpClient> {
options.validate().map_err(map_validation_error)?;
let mut builder = reqwest::Client::builder();
builder = builder.connect_timeout(options.timeouts.connect_timeout);
if let Some(request_timeout) = options.timeouts.request_timeout {
builder = builder.timeout(request_timeout);
}
if let Some(user_agent) = options.user_agent.as_deref() {
builder = builder.user_agent(user_agent);
}
if let Some(max_redirects) = options.max_redirects {
builder = builder.redirect(Policy::limited(max_redirects));
}
if let Some(pool_idle_timeout) = options.pool_idle_timeout {
builder = builder.pool_idle_timeout(pool_idle_timeout);
}
if let Some(pool_max_idle_per_host) = options.pool_max_idle_per_host {
builder = builder.pool_max_idle_per_host(pool_max_idle_per_host);
}
if options.ipv4_only {
builder = builder.dns_resolver(Ipv4OnlyResolver);
}
if options.proxy.enabled {
let host = options
.proxy
.host
.clone()
.expect("proxy.host must exist after HttpClientOptions::validate");
if options.ipv4_only && is_ipv6_literal_host(&host) {
return Err(HttpError::proxy_config(format!(
"Proxy host '{host}' is IPv6, which is not allowed when ipv4_only=true",
)));
}
let port = options
.proxy
.port
.expect("proxy.port must exist after HttpClientOptions::validate");
let proxy_url = format!("{}://{}:{}", options.proxy.proxy_type.scheme(), host, port);
let mut proxy = reqwest::Proxy::all(&proxy_url).map_err(|error| {
HttpError::proxy_config(format!("Invalid proxy URL '{}': {}", proxy_url, error))
})?;
if let Some(username) = options.proxy.username.clone() {
let password = options.proxy.password.as_deref().unwrap_or("");
proxy = proxy.basic_auth(&username, password);
}
builder = builder.proxy(proxy);
} else if !options.use_env_proxy {
builder = builder.no_proxy();
}
let backend = builder.build().map_err(HttpError::from)?;
Ok(HttpClient::new(backend, options))
}
pub fn create_from_config<R>(&self, config: &R) -> Result<HttpClient, HttpConfigError>
where
R: ConfigReader + ?Sized,
{
let options =
HttpClientOptions::from_config(config).map_err(|e| resolve_config_error(config, e))?;
options
.validate()
.map_err(|e| resolve_config_error(config, e))?;
self.create(options).map_err(|e| {
HttpConfigError::new(
crate::HttpConfigErrorKind::InvalidValue,
config.resolve_key(""),
e.to_string(),
)
})
}
}
fn resolve_config_error<R>(config: &R, mut error: HttpConfigError) -> HttpConfigError
where
R: ConfigReader + ?Sized,
{
error.path = if error.path.is_empty() {
config.resolve_key("")
} else {
config.resolve_key(&error.path)
};
error
}
fn filter_ipv4_addrs<I>(host: &str, resolved: I) -> Result<Addrs, Box<dyn Error + Send + Sync>>
where
I: IntoIterator<Item = SocketAddr>,
{
let ipv4_addrs: Vec<SocketAddr> = resolved.into_iter().filter(SocketAddr::is_ipv4).collect();
if ipv4_addrs.is_empty() {
let error = std::io::Error::new(
std::io::ErrorKind::AddrNotAvailable,
format!("No IPv4 address found for host '{host}'"),
);
return Err(Box::new(error) as Box<dyn Error + Send + Sync>);
}
Ok(Box::new(ipv4_addrs.into_iter()) as Addrs)
}
fn map_validation_error(error: HttpConfigError) -> HttpError {
if error.path.starts_with("proxy.") {
HttpError::proxy_config(error.to_string())
} else {
HttpError::other(format!("Invalid HTTP client options: {error}"))
}
}
fn is_ipv6_literal_host(host: &str) -> bool {
let trimmed = host.trim().trim_start_matches('[').trim_end_matches(']');
matches!(trimmed.parse::<IpAddr>(), Ok(IpAddr::V6(_)))
}
#[cfg(coverage)]
#[doc(hidden)]
pub(crate) fn coverage_exercise_factory_paths() -> Vec<String> {
let no_ipv4_error = filter_ipv4_addrs(
"coverage-host",
[SocketAddr::new(
IpAddr::V6(std::net::Ipv6Addr::LOCALHOST),
0,
)],
)
.err()
.expect("IPv6-only addresses should be rejected")
.to_string();
let ipv4_count = filter_ipv4_addrs(
"coverage-host",
[SocketAddr::new(IpAddr::from([127, 0, 0, 1]), 0)],
)
.expect("IPv4 address should be accepted")
.count()
.to_string();
let config = qubit_config::Config::new();
let scoped_path = resolve_config_error(
&config.prefix_view("coverage"),
HttpConfigError::invalid_value("", "coverage error"),
)
.path;
let proxy_kind = map_validation_error(HttpConfigError::invalid_value("proxy.host", "bad")).kind;
let other_kind = map_validation_error(HttpConfigError::invalid_value("user_agent", "bad")).kind;
vec![
no_ipv4_error,
ipv4_count,
scoped_path,
format!("{proxy_kind:?}"),
format!("{other_kind:?}"),
is_ipv6_literal_host("[::1]").to_string(),
is_ipv6_literal_host("127.0.0.1").to_string(),
]
}