lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }