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,
#[cfg(feature = "local-network")]
local: LocalNetwork,
}
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))
.no_proxy()
.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,
#[cfg(feature = "local-network")]
local: LocalNetwork::NONE,
})
}
pub const fn from_client(client: reqwest::Client) -> Self {
Self {
client,
max_body: Self::DEFAULT_MAX_BODY,
#[cfg(feature = "local-network")]
local: LocalNetwork::NONE,
}
}
#[cfg(feature = "local-network")]
#[cfg_attr(docsrs, doc(cfg(feature = "local-network")))]
pub const fn builder() -> ReqwestClientBuilder {
ReqwestClientBuilder {
local: LocalNetwork::NONE,
}
}
#[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> {
#[cfg(feature = "local-network")]
if let Some(response) = self.get_local(url).await? {
return Ok(response);
}
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 response = self
.client
.get(url.as_str())
.send()
.await
.map_err(HttpError::new)?;
read(response, self.max_body).await
}
}
async fn read(mut response: reqwest::Response, max_body: usize) -> Result<HttpResponse, HttpError> {
let status = response.status().as_u16();
if response
.content_length()
.is_some_and(|len| len > max_body as u64)
{
return Err(HttpError::msg(format!(
"response body exceeds {max_body} bytes"
)));
}
let mut body = Vec::new();
while let Some(chunk) = response.chunk().await.map_err(HttpError::new)? {
if body.len() + chunk.len() > max_body {
return Err(HttpError::msg(format!(
"response body exceeds {max_body} bytes"
)));
}
body.extend_from_slice(&chunk);
}
Ok(HttpResponse::new(status, body))
}
#[cfg(feature = "local-network")]
pub use local::{LocalNetwork, ReqwestClientBuilder};
#[cfg(feature = "local-network")]
mod local {
use std::net::{IpAddr, SocketAddr};
use url::{Host, Url};
use super::{HttpError, HttpResponse, ReqwestClient, is_public, read};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct LocalNetwork {
pub loopback: bool,
pub private: bool,
}
impl LocalNetwork {
pub const NONE: Self = Self {
loopback: false,
private: false,
};
pub fn allows(&self, ip: IpAddr) -> bool {
let ip = match ip {
IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(ip, IpAddr::V4),
IpAddr::V4(_) => ip,
};
let loopback = ip.is_loopback();
let private = match ip {
IpAddr::V4(v4) => v4.is_private(),
IpAddr::V6(v6) => v6.is_unique_local(),
};
(self.loopback && loopback) || (self.private && private)
}
pub fn all_local(&self, addresses: &[IpAddr]) -> bool {
!addresses.is_empty() && addresses.iter().all(|ip| self.allows(*ip))
}
}
#[derive(Debug, Clone)]
#[must_use]
pub struct ReqwestClientBuilder {
pub(super) local: LocalNetwork,
}
impl ReqwestClientBuilder {
pub const fn dangerous_allow_loopback(mut self) -> Self {
self.local.loopback = true;
self
}
pub const fn dangerous_allow_private_network(mut self) -> Self {
self.local.private = true;
self
}
pub fn build(self) -> Result<ReqwestClient, HttpError> {
let mut client = ReqwestClient::new()?;
client.local = self.local;
Ok(client)
}
}
impl ReqwestClient {
pub const fn local_network(&self) -> LocalNetwork {
self.local
}
pub(super) async fn get_local(&self, url: &Url) -> Result<Option<HttpResponse>, HttpError> {
if self.local == LocalNetwork::NONE || !matches!(url.scheme(), "https" | "http") {
return Ok(None);
}
let port = url.port_or_known_default().unwrap_or(443);
let host = url.host().expect("an `http` or `https` URL has a host");
let (name, addresses): (Option<&str>, Vec<IpAddr>) = match host {
Host::Ipv4(ip) => (None, vec![IpAddr::V4(ip)]),
Host::Ipv6(ip) => (None, vec![IpAddr::V6(ip)]),
Host::Domain(name) => {
let addresses = tokio::net::lookup_host((name, port))
.await
.map_err(HttpError::new)?
.map(|address| address.ip())
.collect();
(Some(name), addresses)
}
};
if !self.local.all_local(&addresses) {
return Ok(None);
}
debug_assert!(addresses.iter().all(|ip| !is_public(*ip)));
let target = plain_http(url, port);
let mut builder = reqwest::Client::builder()
.no_proxy()
.redirect(reqwest::redirect::Policy::none())
.timeout(ReqwestClient::DEFAULT_TIMEOUT)
.user_agent(concat!("identity-kit/", env!("CARGO_PKG_VERSION")));
if let Some(name) = name {
let pinned: Vec<SocketAddr> = addresses
.iter()
.map(|ip| SocketAddr::new(*ip, port))
.collect();
builder = builder.resolve_to_addrs(name, &pinned);
}
let client = builder.build().map_err(HttpError::new)?;
let response = client
.get(target.as_str())
.send()
.await
.map_err(HttpError::new)?;
read(response, self.max_body).await.map(Some)
}
}
pub(super) fn plain_http(url: &Url, port: u16) -> Url {
let mut target = url.clone();
target
.set_scheme("http")
.expect("an `http` or `https` URL can be `http`");
target
.set_port(Some(port))
.expect("a URL with a host takes a port");
target
}
}
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::*;
#[cfg(feature = "local-network")]
#[test]
fn plain_http_keeps_the_port() {
for (url, port, expected) in [
(
"https://localhost/.well-known/did.json",
443,
"http://localhost:443/.well-known/did.json",
),
(
"https://issuer/user/alice/did.json",
443,
"http://issuer:443/user/alice/did.json",
),
(
"https://127.0.0.1/did.json",
443,
"http://127.0.0.1:443/did.json",
),
("https://[::1]/did.json", 443, "http://[::1]:443/did.json"),
(
"https://localhost:8443/did.json",
8443,
"http://localhost:8443/did.json",
),
(
"https://localhost:80/did.json",
80,
"http://localhost/did.json",
),
(
"http://localhost:8080/did.json",
8080,
"http://localhost:8080/did.json",
),
("http://localhost/did.json", 80, "http://localhost/did.json"),
] {
let url = url::Url::parse(url).unwrap();
let rewritten = local::plain_http(&url, port);
assert_eq!(rewritten.as_str(), expected, "{url}");
assert_eq!(rewritten.port_or_known_default(), Some(port), "{url}");
}
}
#[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}");
}
}
}