1use 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)]
56pub struct ReqwestClient {
57 client: reqwest::Client,
58 max_body: usize,
59 #[cfg(feature = "local-network")]
60 local: LocalNetwork,
61}
62
63impl ReqwestClient {
64 pub const DEFAULT_MAX_BODY: usize = 1 << 20;
66
67 pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
69
70 pub fn new() -> Result<Self, HttpError> {
76 let _ = rustls::crypto::ring::default_provider().install_default();
79 let client = reqwest::Client::builder()
80 .https_only(true)
81 .redirect(reqwest::redirect::Policy::none())
82 .dns_resolver(Arc::new(PublicAddresses))
83 .no_proxy()
84 .timeout(Self::DEFAULT_TIMEOUT)
85 .user_agent(concat!("identity-kit/", env!("CARGO_PKG_VERSION")))
86 .build()
87 .map_err(HttpError::new)?;
88 Ok(Self {
89 client,
90 max_body: Self::DEFAULT_MAX_BODY,
91 #[cfg(feature = "local-network")]
92 local: LocalNetwork::NONE,
93 })
94 }
95
96 pub const fn from_client(client: reqwest::Client) -> Self {
105 Self {
106 client,
107 max_body: Self::DEFAULT_MAX_BODY,
108 #[cfg(feature = "local-network")]
109 local: LocalNetwork::NONE,
110 }
111 }
112
113 #[cfg(feature = "local-network")]
116 #[cfg_attr(docsrs, doc(cfg(feature = "local-network")))]
117 pub const fn builder() -> ReqwestClientBuilder {
118 ReqwestClientBuilder {
119 local: LocalNetwork::NONE,
120 }
121 }
122
123 #[must_use]
125 pub const fn with_max_body(mut self, max_body: usize) -> Self {
126 self.max_body = max_body;
127 self
128 }
129
130 pub const fn max_body(&self) -> usize {
132 self.max_body
133 }
134
135 pub const fn client(&self) -> &reqwest::Client {
137 &self.client
138 }
139}
140
141#[async_trait::async_trait]
142impl HttpClient for ReqwestClient {
143 async fn get(&self, url: &Url) -> Result<HttpResponse, HttpError> {
144 self.fetch(url, None).await
145 }
146
147 async fn get_accepting(&self, url: &Url, media_type: &str) -> Result<HttpResponse, HttpError> {
148 self.fetch(url, Some(media_type)).await
149 }
150}
151
152impl ReqwestClient {
153 async fn fetch(&self, url: &Url, accept: Option<&str>) -> Result<HttpResponse, HttpError> {
155 #[cfg(feature = "local-network")]
156 if let Some(response) = self.get_local(url, accept).await? {
157 return Ok(response);
158 }
159 if url.scheme() != "https" {
160 return Err(HttpError::msg(format!("`{url}` is not an HTTPS URL")));
161 }
162 let literal = match url.host() {
164 Some(Host::Ipv4(ip)) => Some(IpAddr::V4(ip)),
165 Some(Host::Ipv6(ip)) => Some(IpAddr::V6(ip)),
166 _ => None,
167 };
168 if literal.is_some_and(|ip| !is_public(ip)) {
169 return Err(HttpError::msg(format!("`{url}` is not a public address")));
170 }
171 let response = request(&self.client, url, accept)
172 .send()
173 .await
174 .map_err(HttpError::new)?;
175 read(response, self.max_body).await
176 }
177}
178
179fn request(client: &reqwest::Client, url: &Url, accept: Option<&str>) -> reqwest::RequestBuilder {
181 let request = client.get(url.as_str());
182 match accept {
183 Some(media_type) => request.header(reqwest::header::ACCEPT, media_type),
184 None => request,
185 }
186}
187
188async fn read(mut response: reqwest::Response, max_body: usize) -> Result<HttpResponse, HttpError> {
191 let status = response.status().as_u16();
192 let location = if response.status().is_redirection() {
193 response
194 .headers()
195 .get(reqwest::header::LOCATION)
196 .and_then(|location| location.to_str().ok())
197 .map(str::to_owned)
198 } else {
199 None
200 };
201 if response
202 .content_length()
203 .is_some_and(|len| len > max_body as u64)
204 {
205 return Err(HttpError::msg(format!(
206 "response body exceeds {max_body} bytes"
207 )));
208 }
209 let mut body = Vec::new();
210 while let Some(chunk) = response.chunk().await.map_err(HttpError::new)? {
211 if body.len() + chunk.len() > max_body {
212 return Err(HttpError::msg(format!(
213 "response body exceeds {max_body} bytes"
214 )));
215 }
216 body.extend_from_slice(&chunk);
217 }
218 let response = HttpResponse::new(status, body);
219 Ok(match location {
220 Some(location) => response.with_location(location),
221 None => response,
222 })
223}
224
225#[cfg(feature = "local-network")]
226pub use local::{LocalNetwork, ReqwestClientBuilder};
227
228#[cfg(feature = "local-network")]
229mod local {
230 use std::net::{IpAddr, SocketAddr};
231
232 use url::{Host, Url};
233
234 use super::{HttpError, HttpResponse, ReqwestClient, is_public, read, request};
235
236 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
238 #[non_exhaustive]
239 pub struct LocalNetwork {
240 pub loopback: bool,
242 pub private: bool,
244 }
245
246 impl LocalNetwork {
247 pub const NONE: Self = Self {
249 loopback: false,
250 private: false,
251 };
252
253 pub fn allows(&self, ip: IpAddr) -> bool {
256 let ip = match ip {
257 IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(ip, IpAddr::V4),
258 IpAddr::V4(_) => ip,
259 };
260 let loopback = ip.is_loopback();
261 let private = match ip {
262 IpAddr::V4(v4) => v4.is_private(),
263 IpAddr::V6(v6) => v6.is_unique_local(),
264 };
265 (self.loopback && loopback) || (self.private && private)
266 }
267
268 pub fn all_local(&self, addresses: &[IpAddr]) -> bool {
272 !addresses.is_empty() && addresses.iter().all(|ip| self.allows(*ip))
273 }
274 }
275
276 #[derive(Debug, Clone)]
289 #[must_use]
290 pub struct ReqwestClientBuilder {
291 pub(super) local: LocalNetwork,
292 }
293
294 impl ReqwestClientBuilder {
295 pub const fn dangerous_allow_loopback(mut self) -> Self {
298 self.local.loopback = true;
299 self
300 }
301
302 pub const fn dangerous_allow_private_network(mut self) -> Self {
305 self.local.private = true;
306 self
307 }
308
309 pub fn build(self) -> Result<ReqwestClient, HttpError> {
315 let mut client = ReqwestClient::new()?;
316 client.local = self.local;
317 Ok(client)
318 }
319 }
320
321 impl ReqwestClient {
322 pub const fn local_network(&self) -> LocalNetwork {
325 self.local
326 }
327
328 pub(super) async fn get_local(
331 &self,
332 url: &Url,
333 accept: Option<&str>,
334 ) -> Result<Option<HttpResponse>, HttpError> {
335 if self.local == LocalNetwork::NONE || !matches!(url.scheme(), "https" | "http") {
336 return Ok(None);
337 }
338 let port = url.port_or_known_default().unwrap_or(443);
340 let host = url.host().expect("an `http` or `https` URL has a host");
341 let (name, addresses): (Option<&str>, Vec<IpAddr>) = match host {
342 Host::Ipv4(ip) => (None, vec![IpAddr::V4(ip)]),
343 Host::Ipv6(ip) => (None, vec![IpAddr::V6(ip)]),
344 Host::Domain(name) => {
345 let addresses = tokio::net::lookup_host((name, port))
346 .await
347 .map_err(HttpError::new)?
348 .map(|address| address.ip())
349 .collect();
350 (Some(name), addresses)
351 }
352 };
353 if !self.local.all_local(&addresses) {
354 return Ok(None);
355 }
356 debug_assert!(addresses.iter().all(|ip| !is_public(*ip)));
357 let target = plain_http(url, port);
358 let mut builder = reqwest::Client::builder()
359 .no_proxy()
360 .redirect(reqwest::redirect::Policy::none())
361 .timeout(ReqwestClient::DEFAULT_TIMEOUT)
362 .user_agent(concat!("identity-kit/", env!("CARGO_PKG_VERSION")));
363 if let Some(name) = name {
364 let pinned: Vec<SocketAddr> = addresses
365 .iter()
366 .map(|ip| SocketAddr::new(*ip, port))
367 .collect();
368 builder = builder.resolve_to_addrs(name, &pinned);
369 }
370 let client = builder.build().map_err(HttpError::new)?;
371 let response = request(&client, &target, accept)
372 .send()
373 .await
374 .map_err(HttpError::new)?;
375 read(response, self.max_body).await.map(Some)
376 }
377 }
378
379 pub(super) fn plain_http(url: &Url, port: u16) -> Url {
382 let mut target = url.clone();
383 target
386 .set_scheme("http")
387 .expect("an `http` or `https` URL can be `http`");
388 target
389 .set_port(Some(port))
390 .expect("a URL with a host takes a port");
391 target
392 }
393}
394
395struct PublicAddresses;
397
398impl reqwest::dns::Resolve for PublicAddresses {
399 fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
400 let host = name.as_str().to_owned();
401 Box::pin(async move {
402 let addresses: Vec<SocketAddr> = tokio::net::lookup_host((host.as_str(), 0))
403 .await?
404 .filter(|address| is_public(address.ip()))
405 .collect();
406 if addresses.is_empty() {
407 return Err(format!("`{host}` resolves to no public address").into());
408 }
409 Ok(Box::new(addresses.into_iter()) as reqwest::dns::Addrs)
410 })
411 }
412}
413
414pub(crate) fn is_public(ip: IpAddr) -> bool {
417 match ip {
418 IpAddr::V4(v4) => {
419 let [a, b, ..] = v4.octets();
420 !(v4.is_unspecified()
421 || v4.is_loopback()
422 || v4.is_private()
423 || v4.is_link_local()
424 || v4.is_broadcast()
425 || v4.is_documentation()
426 || v4.is_multicast()
427 || a == 0
428 || (a == 100 && (64..=127).contains(&b)) || (a == 192 && b == 0 && v4.octets()[2] == 0) || (a == 198 && (18..=19).contains(&b)) || a >= 240) }
433 IpAddr::V6(v6) => {
434 if let Some(v4) = v6.to_ipv4_mapped() {
435 return is_public(IpAddr::V4(v4));
436 }
437 let segments = v6.segments();
438 !(v6.is_unspecified()
439 || v6.is_loopback()
440 || v6.is_multicast()
441 || v6.is_unique_local()
442 || v6.is_unicast_link_local()
443 || segments[0] == 0x2001 && segments[1] == 0x0db8 || segments[0] == 0x0064 && segments[1] == 0xff9b) }
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452
453 #[cfg(feature = "local-network")]
454 #[test]
455 fn plain_http_keeps_the_port() {
456 for (url, port, expected) in [
457 (
459 "https://localhost/.well-known/did.json",
460 443,
461 "http://localhost:443/.well-known/did.json",
462 ),
463 (
464 "https://issuer/user/alice/did.json",
465 443,
466 "http://issuer:443/user/alice/did.json",
467 ),
468 (
469 "https://127.0.0.1/did.json",
470 443,
471 "http://127.0.0.1:443/did.json",
472 ),
473 ("https://[::1]/did.json", 443, "http://[::1]:443/did.json"),
474 (
476 "https://localhost:8443/did.json",
477 8443,
478 "http://localhost:8443/did.json",
479 ),
480 (
481 "https://localhost:80/did.json",
482 80,
483 "http://localhost/did.json",
484 ),
485 (
487 "http://localhost:8080/did.json",
488 8080,
489 "http://localhost:8080/did.json",
490 ),
491 ("http://localhost/did.json", 80, "http://localhost/did.json"),
492 ] {
493 let url = url::Url::parse(url).unwrap();
494 let rewritten = local::plain_http(&url, port);
495 assert_eq!(rewritten.as_str(), expected, "{url}");
496 assert_eq!(rewritten.port_or_known_default(), Some(port), "{url}");
497 }
498 }
499
500 #[test]
501 fn public_addresses() {
502 for public in [
503 "93.184.216.34",
504 "2606:2800:220:1:248:1893:25c8:1946",
505 "1.1.1.1",
506 "8.64.0.1",
508 "100.128.0.1",
509 "8.0.0.8",
510 "8.8.0.8",
511 "192.0.1.1",
512 "8.18.0.1",
513 "2001:4860:4860::8888",
514 "2600:db8::1",
515 "64:1::1",
516 "2600:ff9b::1",
517 ] {
518 assert!(is_public(public.parse().unwrap()), "{public}");
519 }
520 for private in [
521 "127.0.0.1",
522 "127.1.2.3",
523 "10.0.0.1",
524 "172.16.0.1",
525 "172.31.255.255",
526 "192.168.1.1",
527 "169.254.169.254",
528 "0.0.0.0",
529 "100.64.0.1",
530 "192.0.0.8",
531 "198.18.0.1",
532 "198.51.100.1",
533 "224.0.0.1",
534 "255.255.255.255",
535 "::1",
536 "::",
537 "fc00::1",
538 "fd12::1",
539 "fe80::1",
540 "ff02::1",
541 "2001:db8::1",
542 "64:ff9b::7f00:1",
543 "::ffff:127.0.0.1",
544 "::ffff:10.0.0.1",
545 ] {
546 assert!(!is_public(private.parse().unwrap()), "{private}");
547 }
548 }
549}