Monorepo for Tangled tangled.org
6

Configure Feed

Select the types of activity you want to include in your feed.

1use std::error::Error as StdError; 2use std::fmt; 3use std::io; 4use std::net::SocketAddr; 5 6use reqwest::dns::{Addrs, Name, Resolve, Resolving}; 7 8use crate::host::{PrivateHostReason, classify_ip}; 9 10pub struct PrivateAddressFilter { 11 allow_private: bool, 12} 13 14impl PrivateAddressFilter { 15 pub fn new(allow_private: bool) -> Self { 16 Self { allow_private } 17 } 18} 19 20impl Resolve for PrivateAddressFilter { 21 fn resolve(&self, name: Name) -> Resolving { 22 let allow_private = self.allow_private; 23 let host = name.as_str().to_owned(); 24 Box::pin(async move { 25 let resolved: Vec<SocketAddr> = 26 tokio::net::lookup_host((host.as_str(), 0)).await?.collect(); 27 partition_safe(host, allow_private, resolved) 28 }) 29 } 30} 31 32fn partition_safe( 33 host: String, 34 allow_private: bool, 35 resolved: Vec<SocketAddr>, 36) -> Result<Addrs, Box<dyn StdError + Send + Sync>> { 37 if !allow_private && let Some(reason) = resolved.iter().find_map(|sa| classify_ip(&sa.ip())) { 38 return Err(Box::new(BlockedAddressError { host, reason })); 39 } 40 if resolved.is_empty() { 41 return Err(Box::new(io::Error::other(format!( 42 "no resolvable addresses for {host}" 43 )))); 44 } 45 Ok(Box::new(resolved.into_iter())) 46} 47 48#[derive(Debug)] 49pub(crate) struct BlockedAddressError { 50 host: String, 51 reason: PrivateHostReason, 52} 53 54impl fmt::Display for BlockedAddressError { 55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 56 write!( 57 f, 58 "dns resolution for {} returned {} address", 59 self.host, self.reason, 60 ) 61 } 62} 63 64impl StdError for BlockedAddressError {} 65 66#[cfg(test)] 67mod tests { 68 use super::*; 69 use std::net::{IpAddr, Ipv4Addr}; 70 71 fn sa(ip: &str, port: u16) -> SocketAddr { 72 SocketAddr::new(ip.parse().unwrap(), port) 73 } 74 75 #[test] 76 fn blocks_when_any_resolved_address_is_private_under_strict() { 77 let mixed = vec![sa("8.8.8.8", 0), sa("127.0.0.1", 0)]; 78 let res = partition_safe("mixed.example".into(), false, mixed); 79 assert!(res.is_err(), "any private address must fail strict resolve"); 80 } 81 82 #[test] 83 fn allows_all_when_permissive() { 84 let mixed = vec![sa("8.8.8.8", 0), sa("127.0.0.1", 0)]; 85 let res = partition_safe("mixed.example".into(), true, mixed).expect("permissive"); 86 let collected: Vec<SocketAddr> = res.collect(); 87 assert_eq!(collected.len(), 2); 88 } 89 90 #[test] 91 fn permits_public_only_resolution_under_strict() { 92 let public = vec![sa("8.8.8.8", 0), sa("1.1.1.1", 0)]; 93 let res = partition_safe("public.example".into(), false, public).expect("public"); 94 let collected: Vec<SocketAddr> = res.collect(); 95 assert_eq!(collected.len(), 2); 96 } 97 98 #[test] 99 fn empty_resolution_is_an_error() { 100 let res = partition_safe("nx.example".into(), false, vec![]); 101 assert!(res.is_err(), "empty address list must surface an error"); 102 } 103 104 #[test] 105 fn classify_ip_matches_url_classifier() { 106 assert!(classify_ip(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))).is_some()); 107 assert!(classify_ip(&IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))).is_none()); 108 } 109}