use std::net::{IpAddr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use url::{Host, Url};
use super::{HttpClient, HttpError, HttpResponse};
#[derive(Debug, Clone)]
pub struct ReqwestClient {
client: reqwest::Client,
max_body: usize,
}
impl ReqwestClient {
pub const DEFAULT_MAX_BODY: usize = 1 << 20;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
pub fn new() -> Result<Self, HttpError> {
let _ = rustls::crypto::ring::default_provider().install_default();
let client = reqwest::Client::builder()
.https_only(true)
.redirect(reqwest::redirect::Policy::none())
.dns_resolver(Arc::new(PublicAddresses))
.timeout(Self::DEFAULT_TIMEOUT)
.user_agent(concat!("identity-kit/", env!("CARGO_PKG_VERSION")))
.build()
.map_err(HttpError::new)?;
Ok(Self {
client,
max_body: Self::DEFAULT_MAX_BODY,
})
}
pub const fn from_client(client: reqwest::Client) -> Self {
Self {
client,
max_body: Self::DEFAULT_MAX_BODY,
}
}
#[must_use]
pub const fn with_max_body(mut self, max_body: usize) -> Self {
self.max_body = max_body;
self
}
pub const fn max_body(&self) -> usize {
self.max_body
}
pub const fn client(&self) -> &reqwest::Client {
&self.client
}
}
#[async_trait::async_trait]
impl HttpClient for ReqwestClient {
async fn get(&self, url: &Url) -> Result<HttpResponse, HttpError> {
if url.scheme() != "https" {
return Err(HttpError::msg(format!("`{url}` is not an HTTPS URL")));
}
let literal = match url.host() {
Some(Host::Ipv4(ip)) => Some(IpAddr::V4(ip)),
Some(Host::Ipv6(ip)) => Some(IpAddr::V6(ip)),
_ => None,
};
if literal.is_some_and(|ip| !is_public(ip)) {
return Err(HttpError::msg(format!("`{url}` is not a public address")));
}
let mut response = self
.client
.get(url.as_str())
.send()
.await
.map_err(HttpError::new)?;
let status = response.status().as_u16();
if response
.content_length()
.is_some_and(|len| len > self.max_body as u64)
{
return Err(HttpError::msg(format!(
"response body exceeds {} bytes",
self.max_body
)));
}
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(HttpError::new)? {
if body.len() + chunk.len() > self.max_body {
return Err(HttpError::msg(format!(
"response body exceeds {} bytes",
self.max_body
)));
}
body.extend_from_slice(&chunk);
}
Ok(HttpResponse::new(status, body))
}
}
struct PublicAddresses;
impl reqwest::dns::Resolve for PublicAddresses {
fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
let host = name.as_str().to_owned();
Box::pin(async move {
let addresses: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), 0))
.await?
.filter(|address| is_public(address.ip()))
.collect();
if addresses.is_empty() {
return Err(format!("`{host}` resolves to no public address").into());
}
Ok(Box::new(addresses.into_iter()) as reqwest::dns::Addrs)
})
}
}
pub(crate) fn is_public(ip: IpAddr) -> bool {
match ip {
IpAddr::V4(v4) => {
let [a, b, ..] = v4.octets();
!(v4.is_unspecified()
|| v4.is_loopback()
|| v4.is_private()
|| v4.is_link_local()
|| v4.is_broadcast()
|| v4.is_documentation()
|| v4.is_multicast()
|| a == 0
|| (a == 100 && (64..=127).contains(&b)) || (a == 192 && b == 0 && v4.octets()[2] == 0) || (a == 198 && (18..=19).contains(&b)) || a >= 240) }
IpAddr::V6(v6) => {
if let Some(v4) = v6.to_ipv4_mapped() {
return is_public(IpAddr::V4(v4));
}
let segments = v6.segments();
!(v6.is_unspecified()
|| v6.is_loopback()
|| v6.is_multicast()
|| v6.is_unique_local()
|| v6.is_unicast_link_local()
|| segments[0] == 0x2001 && segments[1] == 0x0db8 || segments[0] == 0x0064 && segments[1] == 0xff9b) }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn public_addresses() {
for public in [
"93.184.216.34",
"2606:2800:220:1:248:1893:25c8:1946",
"1.1.1.1",
] {
assert!(is_public(public.parse().unwrap()), "{public}");
}
for private in [
"127.0.0.1",
"127.1.2.3",
"10.0.0.1",
"172.16.0.1",
"172.31.255.255",
"192.168.1.1",
"169.254.169.254",
"0.0.0.0",
"100.64.0.1",
"192.0.0.8",
"198.18.0.1",
"198.51.100.1",
"224.0.0.1",
"255.255.255.255",
"::1",
"::",
"fc00::1",
"fd12::1",
"fe80::1",
"ff02::1",
"2001:db8::1",
"64:ff9b::7f00:1",
"::ffff:127.0.0.1",
"::ffff:10.0.0.1",
] {
assert!(!is_public(private.parse().unwrap()), "{private}");
}
}
}