identity_kit/methods/web/
client.rs1use std::net::{IpAddr, SocketAddr};
4use std::sync::Arc;
5use std::time::Duration;
6
7use url::{Host, Url};
8
9use super::{HttpClient, HttpError, HttpResponse};
10
11#[derive(Debug, Clone)]
27pub struct ReqwestClient {
28 client: reqwest::Client,
29 max_body: usize,
30}
31
32impl ReqwestClient {
33 pub const DEFAULT_MAX_BODY: usize = 1 << 20;
35
36 pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
38
39 pub fn new() -> Result<Self, HttpError> {
45 let _ = rustls::crypto::ring::default_provider().install_default();
48 let client = reqwest::Client::builder()
49 .https_only(true)
50 .redirect(reqwest::redirect::Policy::none())
51 .dns_resolver(Arc::new(PublicAddresses))
52 .timeout(Self::DEFAULT_TIMEOUT)
53 .user_agent(concat!("identity-kit/", env!("CARGO_PKG_VERSION")))
54 .build()
55 .map_err(HttpError::new)?;
56 Ok(Self {
57 client,
58 max_body: Self::DEFAULT_MAX_BODY,
59 })
60 }
61
62 pub const fn from_client(client: reqwest::Client) -> Self {
66 Self {
67 client,
68 max_body: Self::DEFAULT_MAX_BODY,
69 }
70 }
71
72 #[must_use]
74 pub const fn with_max_body(mut self, max_body: usize) -> Self {
75 self.max_body = max_body;
76 self
77 }
78
79 pub const fn max_body(&self) -> usize {
81 self.max_body
82 }
83
84 pub const fn client(&self) -> &reqwest::Client {
86 &self.client
87 }
88}
89
90#[async_trait::async_trait]
91impl HttpClient for ReqwestClient {
92 async fn get(&self, url: &Url) -> Result<HttpResponse, HttpError> {
93 if url.scheme() != "https" {
94 return Err(HttpError::msg(format!("`{url}` is not an HTTPS URL")));
95 }
96 let literal = match url.host() {
98 Some(Host::Ipv4(ip)) => Some(IpAddr::V4(ip)),
99 Some(Host::Ipv6(ip)) => Some(IpAddr::V6(ip)),
100 _ => None,
101 };
102 if literal.is_some_and(|ip| !is_public(ip)) {
103 return Err(HttpError::msg(format!("`{url}` is not a public address")));
104 }
105 let mut response = self
106 .client
107 .get(url.as_str())
108 .send()
109 .await
110 .map_err(HttpError::new)?;
111 let status = response.status().as_u16();
112 if response
113 .content_length()
114 .is_some_and(|len| len > self.max_body as u64)
115 {
116 return Err(HttpError::msg(format!(
117 "response body exceeds {} bytes",
118 self.max_body
119 )));
120 }
121 let mut body = Vec::new();
122 while let Some(chunk) = response.chunk().await.map_err(HttpError::new)? {
123 if body.len() + chunk.len() > self.max_body {
124 return Err(HttpError::msg(format!(
125 "response body exceeds {} bytes",
126 self.max_body
127 )));
128 }
129 body.extend_from_slice(&chunk);
130 }
131 Ok(HttpResponse::new(status, body))
132 }
133}
134
135struct PublicAddresses;
137
138impl reqwest::dns::Resolve for PublicAddresses {
139 fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
140 let host = name.as_str().to_owned();
141 Box::pin(async move {
142 let addresses: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), 0))
143 .await?
144 .filter(|address| is_public(address.ip()))
145 .collect();
146 if addresses.is_empty() {
147 return Err(format!("`{host}` resolves to no public address").into());
148 }
149 Ok(Box::new(addresses.into_iter()) as reqwest::dns::Addrs)
150 })
151 }
152}
153
154pub(crate) fn is_public(ip: IpAddr) -> bool {
157 match ip {
158 IpAddr::V4(v4) => {
159 let [a, b, ..] = v4.octets();
160 !(v4.is_unspecified()
161 || v4.is_loopback()
162 || v4.is_private()
163 || v4.is_link_local()
164 || v4.is_broadcast()
165 || v4.is_documentation()
166 || v4.is_multicast()
167 || a == 0
168 || (a == 100 && (64..=127).contains(&b)) || (a == 192 && b == 0 && v4.octets()[2] == 0) || (a == 198 && (18..=19).contains(&b)) || a >= 240) }
173 IpAddr::V6(v6) => {
174 if let Some(v4) = v6.to_ipv4_mapped() {
175 return is_public(IpAddr::V4(v4));
176 }
177 let segments = v6.segments();
178 !(v6.is_unspecified()
179 || v6.is_loopback()
180 || v6.is_multicast()
181 || v6.is_unique_local()
182 || v6.is_unicast_link_local()
183 || segments[0] == 0x2001 && segments[1] == 0x0db8 || segments[0] == 0x0064 && segments[1] == 0xff9b) }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn public_addresses() {
195 for public in [
196 "93.184.216.34",
197 "2606:2800:220:1:248:1893:25c8:1946",
198 "1.1.1.1",
199 ] {
200 assert!(is_public(public.parse().unwrap()), "{public}");
201 }
202 for private in [
203 "127.0.0.1",
204 "127.1.2.3",
205 "10.0.0.1",
206 "172.16.0.1",
207 "172.31.255.255",
208 "192.168.1.1",
209 "169.254.169.254",
210 "0.0.0.0",
211 "100.64.0.1",
212 "192.0.0.8",
213 "198.18.0.1",
214 "198.51.100.1",
215 "224.0.0.1",
216 "255.255.255.255",
217 "::1",
218 "::",
219 "fc00::1",
220 "fd12::1",
221 "fe80::1",
222 "ff02::1",
223 "2001:db8::1",
224 "64:ff9b::7f00:1",
225 "::ffff:127.0.0.1",
226 "::ffff:10.0.0.1",
227 ] {
228 assert!(!is_public(private.parse().unwrap()), "{private}");
229 }
230 }
231}