1use std::future::Future;
9use std::io;
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
11use std::sync::Arc;
12
13use codoseo_core::crawl::AddressPolicy;
14use reqwest::dns::{Addrs, Name, Resolve, Resolving};
15use url::{Host, Url};
16
17#[derive(Debug, thiserror::Error)]
18pub enum GuardError {
19 #[error("{0} is a private or internal address")]
20 BlockedAddress(IpAddr),
21 #[error("{host} only resolves to private or internal addresses")]
22 OnlyBlockedAddresses { host: String },
23 #[error("could not resolve {host}: {reason}")]
24 Lookup { host: String, reason: String },
25}
26
27pub fn is_blocked(ip: IpAddr) -> bool {
29 match ip {
30 IpAddr::V4(v4) => is_blocked_v4(v4),
31 IpAddr::V6(v6) => is_blocked_v6(v6),
32 }
33}
34
35fn is_blocked_v4(ip: Ipv4Addr) -> bool {
36 let [a, b, c, _] = ip.octets();
37 a == 0 || ip.is_private() || ip.is_loopback() || ip.is_link_local() || (a == 100 && (b & 0xC0) == 64) || (a == 192 && b == 0 && c == 0) || ip.is_documentation()
44 || (a == 198 && (b == 18 || b == 19)) || ip.is_multicast() || a >= 240 }
48
49fn is_blocked_v6(ip: Ipv6Addr) -> bool {
50 if let Some(v4) = embedded_v4(ip) {
51 return is_blocked_v4(v4);
52 }
53 let s = ip.segments();
54 ip.is_unspecified()
55 || ip.is_loopback()
56 || ip.is_multicast()
57 || ip.is_unique_local() || ip.is_unicast_link_local() || (s[0] & 0xffc0) == 0xfec0 || (s[0] == 0x2001 && s[1] == 0x0db8) || (s[0] == 0x2001 && s[1] == 0x0000) || (s[0] == 0x0064 && s[1] == 0xff9b && s[2] == 0x0001) || (s[0] == 0x0100 && s[1] == 0 && s[2] == 0 && s[3] == 0) }
65
66fn embedded_v4(ip: Ipv6Addr) -> Option<Ipv4Addr> {
68 let s = ip.segments();
69 let tail =
70 |hi: u16, lo: u16| Ipv4Addr::new((hi >> 8) as u8, hi as u8, (lo >> 8) as u8, lo as u8);
71 if let Some(v4) = ip.to_ipv4_mapped() {
72 return Some(v4);
73 }
74 if s[..6].iter().all(|&x| x == 0) && !(s[6] == 0 && s[7] <= 1) {
75 return Some(tail(s[6], s[7])); }
77 if s[0] == 0x0064 && s[1] == 0xff9b && s[2..6].iter().all(|&x| x == 0) {
78 return Some(tail(s[6], s[7])); }
80 if s[0] == 0x2002 {
81 return Some(tail(s[1], s[2])); }
83 None
84}
85
86pub fn check_url(url: &Url, policy: AddressPolicy) -> Result<(), GuardError> {
89 if policy == AddressPolicy::AllowPrivate {
90 return Ok(());
91 }
92 let ip = match url.host() {
93 Some(Host::Ipv4(v4)) => IpAddr::V4(v4),
94 Some(Host::Ipv6(v6)) => IpAddr::V6(v6),
95 _ => return Ok(()),
96 };
97 if is_blocked(ip) {
98 return Err(GuardError::BlockedAddress(ip));
99 }
100 Ok(())
101}
102
103pub trait Lookup: Send + Sync + 'static {
105 fn lookup(&self, host: &str) -> impl Future<Output = io::Result<Vec<IpAddr>>> + Send;
106}
107
108#[derive(Debug, Default, Clone, Copy)]
110pub struct SystemLookup;
111
112impl Lookup for SystemLookup {
113 fn lookup(&self, host: &str) -> impl Future<Output = io::Result<Vec<IpAddr>>> + Send {
114 let host = host.to_owned();
115 async move {
116 let addrs = tokio::net::lookup_host((host.as_str(), 0)).await?;
117 Ok(addrs.map(|a| a.ip()).collect())
118 }
119 }
120}
121
122pub struct GuardedResolver<L: Lookup> {
124 lookup: Arc<L>,
125}
126
127impl<L: Lookup> GuardedResolver<L> {
128 pub fn new(lookup: L) -> Self {
129 GuardedResolver {
130 lookup: Arc::new(lookup),
131 }
132 }
133
134 pub async fn lookup_checked(&self, host: &str) -> Result<Vec<SocketAddr>, GuardError> {
135 checked(self.lookup.as_ref(), host).await
136 }
137}
138
139async fn checked<L: Lookup>(lookup: &L, host: &str) -> Result<Vec<SocketAddr>, GuardError> {
140 let ips = lookup.lookup(host).await.map_err(|e| GuardError::Lookup {
141 host: host.to_owned(),
142 reason: e.to_string(),
143 })?;
144 let allowed: Vec<SocketAddr> = ips
145 .into_iter()
146 .filter(|ip| !is_blocked(*ip))
147 .map(|ip| SocketAddr::new(ip, 0))
148 .collect();
149 if allowed.is_empty() {
150 return Err(GuardError::OnlyBlockedAddresses {
151 host: host.to_owned(),
152 });
153 }
154 Ok(allowed)
155}
156
157impl<L: Lookup> Resolve for GuardedResolver<L> {
158 fn resolve(&self, name: Name) -> Resolving {
159 let lookup = Arc::clone(&self.lookup);
160 Box::pin(async move {
161 let addrs = checked(lookup.as_ref(), name.as_str()).await?;
162 Ok(Box::new(addrs.into_iter()) as Addrs)
163 })
164 }
165}