relay.rs (8042B)
1 #![forbid(unsafe_code)] 2 3 use crate::RadrootsRelayTransportError; 4 use serde::{Deserialize, Serialize}; 5 use std::fmt; 6 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 7 use url::Url; 8 9 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 10 pub enum RadrootsRelayUrlPolicy { 11 Public, 12 Localhost, 13 } 14 15 impl RadrootsRelayUrlPolicy { 16 fn accepts_ws_host(self, host: &str) -> bool { 17 matches!(self, Self::Localhost) 18 && matches!(host, "localhost" | "127.0.0.1" | "::1" | "[::1]") 19 } 20 } 21 22 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] 23 pub struct RadrootsRelayUrl(String); 24 25 impl RadrootsRelayUrl { 26 pub fn parse( 27 value: impl AsRef<str>, 28 policy: RadrootsRelayUrlPolicy, 29 ) -> Result<Self, RadrootsRelayTransportError> { 30 let original = value.as_ref().trim(); 31 let parsed = 32 Url::parse(original).map_err(|error| RadrootsRelayTransportError::RelayUrlParse { 33 url: original.to_owned(), 34 reason: error.to_string(), 35 })?; 36 if !parsed.username().is_empty() || parsed.password().is_some() { 37 return Err(RadrootsRelayTransportError::RelayUrlUserinfo { 38 url: original.to_owned(), 39 }); 40 } 41 let Some(host) = parsed.host_str().filter(|host| !host.is_empty()) else { 42 return Err(RadrootsRelayTransportError::EmptyRelayHost { 43 url: original.to_owned(), 44 }); 45 }; 46 validate_host_destination(original, host, policy)?; 47 if parsed.query().is_some() || parsed.fragment().is_some() { 48 return Err(RadrootsRelayTransportError::RelayUrlQueryOrFragment { 49 url: original.to_owned(), 50 }); 51 } 52 let scheme = parsed.scheme(); 53 match scheme { 54 "wss" => {} 55 "ws" if policy.accepts_ws_host(host) => {} 56 "ws" => { 57 return Err(RadrootsRelayTransportError::WsRequiresLocalhostPolicy { 58 url: original.to_owned(), 59 }); 60 } 61 other => { 62 return Err(RadrootsRelayTransportError::UnsupportedRelayScheme { 63 url: original.to_owned(), 64 scheme: other.to_owned(), 65 }); 66 } 67 } 68 let mut normalized = parsed.to_string(); 69 if parsed.path() == "/" { 70 normalized.pop(); 71 } 72 Ok(Self(normalized)) 73 } 74 75 pub fn validate_public_resolved_ip_addrs<I>( 76 &self, 77 addrs: I, 78 ) -> Result<(), RadrootsRelayTransportError> 79 where 80 I: IntoIterator<Item = IpAddr>, 81 { 82 for address in addrs { 83 if let Some(reason) = forbidden_public_ip_reason(address) { 84 return Err( 85 RadrootsRelayTransportError::RelayUrlResolvedForbiddenDestination { 86 url: self.0.clone(), 87 address: address.to_string(), 88 reason: reason.to_owned(), 89 }, 90 ); 91 } 92 } 93 Ok(()) 94 } 95 96 pub fn as_str(&self) -> &str { 97 self.0.as_str() 98 } 99 100 pub fn into_string(self) -> String { 101 self.0 102 } 103 } 104 105 fn validate_host_destination( 106 original: &str, 107 host: &str, 108 policy: RadrootsRelayUrlPolicy, 109 ) -> Result<(), RadrootsRelayTransportError> { 110 let host = host 111 .strip_prefix('[') 112 .and_then(|value| value.strip_suffix(']')) 113 .unwrap_or(host); 114 if matches!(policy, RadrootsRelayUrlPolicy::Public) 115 && let Ok(address) = host.parse::<IpAddr>() 116 && let Some(reason) = forbidden_public_ip_reason(address) 117 { 118 return Err(RadrootsRelayTransportError::RelayUrlForbiddenDestination { 119 url: original.to_owned(), 120 reason: reason.to_owned(), 121 }); 122 } 123 Ok(()) 124 } 125 126 fn forbidden_public_ip_reason(address: IpAddr) -> Option<&'static str> { 127 match address { 128 IpAddr::V4(address) => forbidden_public_ipv4_reason(address), 129 IpAddr::V6(address) => forbidden_public_ipv6_reason(address), 130 } 131 } 132 133 fn forbidden_public_ipv4_reason(address: Ipv4Addr) -> Option<&'static str> { 134 let octets = address.octets(); 135 if address.is_unspecified() || octets[0] == 0 { 136 Some("unspecified or this-network IPv4 address") 137 } else if address.is_loopback() { 138 Some("loopback IPv4 address") 139 } else if address.is_private() { 140 Some("private IPv4 address") 141 } else if address.is_link_local() { 142 Some("link-local IPv4 address") 143 } else if address.is_multicast() { 144 Some("multicast IPv4 address") 145 } else if address.is_broadcast() { 146 Some("broadcast IPv4 address") 147 } else if address.is_documentation() { 148 Some("documentation IPv4 address") 149 } else if octets[0] == 100 && (64..=127).contains(&octets[1]) { 150 Some("shared IPv4 address space") 151 } else if octets[0] == 192 && octets[1] == 0 && octets[2] == 0 { 152 Some("IETF protocol-assignment IPv4 address") 153 } else if octets[0] == 198 && matches!(octets[1], 18 | 19) { 154 Some("benchmark IPv4 address") 155 } else if octets[0] >= 240 { 156 Some("reserved IPv4 address") 157 } else { 158 None 159 } 160 } 161 162 fn forbidden_public_ipv6_reason(address: Ipv6Addr) -> Option<&'static str> { 163 let segments = address.segments(); 164 if let Some(mapped) = address.to_ipv4_mapped() { 165 return forbidden_public_ipv4_reason(mapped); 166 } 167 if address.is_unspecified() { 168 Some("unspecified IPv6 address") 169 } else if address.is_loopback() { 170 Some("loopback IPv6 address") 171 } else if address.is_multicast() { 172 Some("multicast IPv6 address") 173 } else if (segments[0] & 0xfe00) == 0xfc00 { 174 Some("unique-local IPv6 address") 175 } else if (segments[0] & 0xffc0) == 0xfe80 { 176 Some("link-local IPv6 address") 177 } else if segments[0] == 0x2001 && segments[1] == 0x0db8 { 178 Some("documentation IPv6 address") 179 } else if segments[0] == 0x2001 && segments[1] < 0x0200 { 180 Some("IETF protocol-assignment IPv6 address") 181 } else { 182 None 183 } 184 } 185 186 impl fmt::Display for RadrootsRelayUrl { 187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 188 f.write_str(self.0.as_str()) 189 } 190 } 191 192 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] 193 pub struct RadrootsRelayTargetSet { 194 relays: Vec<RadrootsRelayUrl>, 195 } 196 197 impl RadrootsRelayTargetSet { 198 pub fn new<I, S>( 199 relays: I, 200 policy: RadrootsRelayUrlPolicy, 201 ) -> Result<Self, RadrootsRelayTransportError> 202 where 203 I: IntoIterator<Item = S>, 204 S: AsRef<str>, 205 { 206 let mut ordered_relays = Vec::new(); 207 for relay in relays { 208 let relay = RadrootsRelayUrl::parse(relay, policy)?; 209 if !ordered_relays.iter().any(|existing| existing == &relay) { 210 ordered_relays.push(relay); 211 } 212 } 213 let relays = ordered_relays; 214 if relays.is_empty() { 215 return Err(RadrootsRelayTransportError::EmptyTargetSet); 216 } 217 Ok(Self { relays }) 218 } 219 220 pub fn from_urls(relays: Vec<RadrootsRelayUrl>) -> Result<Self, RadrootsRelayTransportError> { 221 let mut ordered_relays = Vec::new(); 222 for relay in relays { 223 if !ordered_relays.iter().any(|existing| existing == &relay) { 224 ordered_relays.push(relay); 225 } 226 } 227 let relays = ordered_relays; 228 if relays.is_empty() { 229 return Err(RadrootsRelayTransportError::EmptyTargetSet); 230 } 231 Ok(Self { relays }) 232 } 233 234 pub fn relays(&self) -> &[RadrootsRelayUrl] { 235 &self.relays 236 } 237 238 pub fn relay_strings(&self) -> Vec<String> { 239 self.relays 240 .iter() 241 .map(|relay| relay.as_str().to_owned()) 242 .collect() 243 } 244 245 pub fn len(&self) -> usize { 246 self.relays.len() 247 } 248 249 pub fn is_empty(&self) -> bool { 250 self.relays.is_empty() 251 } 252 }