Skip to main content

didbot_http/
resolve.rs

1//! Name resolution for every client built here.
2//!
3//! RFC 6761 reserves `localhost` and every name under it for loopback, and
4//! asks resolvers to answer them without asking DNS. systemd-resolved does;
5//! glibc's own resolver, which a container usually has, does not, and
6//! `layout.localhost` fails there. A development zone and every test stack
7//! live under `.localhost`, so this answers those names itself and hands
8//! every other name to the system.
9
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
11
12/// Whether `name` is `localhost` or a name under it.
13pub fn is_localhost_name(name: &str) -> bool {
14    let name = name.strip_suffix('.').unwrap_or(name).to_ascii_lowercase();
15    name == "localhost" || name.ends_with(".localhost")
16}
17
18/// Every address `name` answers to at `port`: loopback for a
19/// [localhost name](is_localhost_name), and the system's answer for any
20/// other.
21pub async fn lookup_host(name: &str, port: u16) -> std::io::Result<Vec<SocketAddr>> {
22    if is_localhost_name(name) {
23        return Ok([
24            IpAddr::V4(Ipv4Addr::LOCALHOST),
25            IpAddr::V6(Ipv6Addr::LOCALHOST),
26        ]
27        .into_iter()
28        .map(|address| SocketAddr::new(address, port))
29        .collect());
30    }
31    Ok(tokio::net::lookup_host((name, port)).await?.collect())
32}
33
34/// [`lookup_host`] as the resolver every client built here uses.
35pub(crate) struct Resolver;
36
37impl reqwest::dns::Resolve for Resolver {
38    fn resolve(&self, name: reqwest::dns::Name) -> reqwest::dns::Resolving {
39        Box::pin(async move {
40            let addresses = lookup_host(name.as_str(), 0).await?;
41            Ok(Box::new(addresses.into_iter()) as reqwest::dns::Addrs)
42        })
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn localhost_and_every_name_under_it() {
52        for name in [
53            "localhost",
54            "LocalHost",
55            "layout.localhost",
56            "a.b.localhost.",
57        ] {
58            assert!(is_localhost_name(name), "{name}");
59        }
60        for name in ["notlocalhost", "localhost.example", "example.com", "host"] {
61            assert!(!is_localhost_name(name), "{name}");
62        }
63    }
64}