lib

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

uri.rs (13968B)


      1 use crate::error::RadrootsSimplexSmpProtoError;
      2 use crate::version::{
      3     RADROOTS_SIMPLEX_SMP_INITIAL_CLIENT_VERSION,
      4     RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION,
      5     RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION, RadrootsSimplexSmpVersionRange,
      6 };
      7 use alloc::string::{String, ToString};
      8 use alloc::vec::Vec;
      9 use base64::Engine as _;
     10 use core::fmt;
     11 use core::str::FromStr;
     12 
     13 pub const RADROOTS_SIMPLEX_SMP_URI_SCHEME: &str = "smp";
     14 pub const RADROOTS_SIMPLEX_SMP_DEFAULT_PORT: u16 = 5223;
     15 
     16 #[derive(Debug, Clone, PartialEq, Eq)]
     17 pub struct RadrootsSimplexSmpServerAddress {
     18     pub server_identity: String,
     19     pub hosts: Vec<String>,
     20     pub port: Option<u16>,
     21 }
     22 
     23 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     24 pub enum RadrootsSimplexSmpQueueMode {
     25     Messaging,
     26     Contact,
     27 }
     28 
     29 impl RadrootsSimplexSmpQueueMode {
     30     const fn as_query_value(self) -> &'static str {
     31         match self {
     32             Self::Messaging => "m",
     33             Self::Contact => "c",
     34         }
     35     }
     36 }
     37 
     38 #[derive(Debug, Clone, PartialEq, Eq)]
     39 pub struct RadrootsSimplexSmpQueueUri {
     40     pub server: RadrootsSimplexSmpServerAddress,
     41     pub sender_id: String,
     42     pub version_range: RadrootsSimplexSmpVersionRange,
     43     pub recipient_dh_public_key: String,
     44     pub queue_mode: Option<RadrootsSimplexSmpQueueMode>,
     45 }
     46 
     47 impl RadrootsSimplexSmpQueueUri {
     48     pub const fn sender_can_secure(&self) -> bool {
     49         matches!(
     50             self.queue_mode,
     51             Some(RadrootsSimplexSmpQueueMode::Messaging)
     52         )
     53     }
     54 
     55     pub fn parse(value: &str) -> Result<Self, RadrootsSimplexSmpProtoError> {
     56         let without_scheme = value
     57             .strip_prefix("smp://")
     58             .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
     59         let (authority, sender_and_fragment) = without_scheme
     60             .split_once('/')
     61             .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
     62         let server = parse_server_address(authority)?;
     63         let (sender_id, fragment) = sender_and_fragment
     64             .split_once('#')
     65             .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
     66         let sender_id = sender_id.strip_suffix('/').unwrap_or(sender_id).to_string();
     67         validate_base64_url("sender_id", &sender_id)?;
     68         let (fragment_dh_public_key, query) = parse_fragment_query(fragment, value)?;
     69 
     70         let mut version_range = if query.is_none() {
     71             Some(RadrootsSimplexSmpVersionRange::single(
     72                 RADROOTS_SIMPLEX_SMP_INITIAL_CLIENT_VERSION,
     73             ))
     74         } else {
     75             None
     76         };
     77         let mut recipient_dh_public_key: Option<String> = fragment_dh_public_key;
     78         let mut queue_mode: Option<RadrootsSimplexSmpQueueMode> = None;
     79         let mut extra_hosts: Option<Vec<String>> = None;
     80 
     81         if let Some(query) = query {
     82             version_range = None;
     83             for pair in query.split('&') {
     84                 if pair.is_empty() {
     85                     continue;
     86                 }
     87 
     88                 let (key, raw_value) = pair
     89                     .split_once('=')
     90                     .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()))?;
     91 
     92                 match key {
     93                     "v" => {
     94                         version_range = Some(raw_value.parse()?);
     95                     }
     96                     "dh" => {
     97                         validate_base64_url("recipient_dh_public_key", raw_value)?;
     98                         if recipient_dh_public_key
     99                             .replace(raw_value.to_string())
    100                             .is_some()
    101                         {
    102                             return Err(RadrootsSimplexSmpProtoError::InvalidUri(
    103                                 value.to_string(),
    104                             ));
    105                         }
    106                     }
    107                     "q" => {
    108                         let next_mode = match raw_value {
    109                             "m" => RadrootsSimplexSmpQueueMode::Messaging,
    110                             "c" => RadrootsSimplexSmpQueueMode::Contact,
    111                             _ => {
    112                                 return Err(RadrootsSimplexSmpProtoError::InvalidUri(
    113                                     value.to_string(),
    114                                 ));
    115                             }
    116                         };
    117                         if queue_mode.replace(next_mode).is_some() {
    118                             return Err(RadrootsSimplexSmpProtoError::InvalidUri(
    119                                 value.to_string(),
    120                             ));
    121                         }
    122                     }
    123                     "k" if raw_value == "s" => {
    124                         if queue_mode
    125                             .replace(RadrootsSimplexSmpQueueMode::Messaging)
    126                             .is_some()
    127                         {
    128                             return Err(RadrootsSimplexSmpProtoError::InvalidUri(
    129                                 value.to_string(),
    130                             ));
    131                         }
    132                     }
    133                     "srv" => {
    134                         if extra_hosts
    135                             .replace(parse_host_list(raw_value, value)?)
    136                             .is_some()
    137                         {
    138                             return Err(RadrootsSimplexSmpProtoError::InvalidUri(
    139                                 value.to_string(),
    140                             ));
    141                         }
    142                     }
    143                     _ => {
    144                         return Err(RadrootsSimplexSmpProtoError::InvalidUri(value.to_string()));
    145                     }
    146                 }
    147             }
    148         }
    149 
    150         let mut server = server;
    151         if let Some(hosts) = extra_hosts {
    152             server.hosts.extend(hosts);
    153         }
    154 
    155         Ok(Self {
    156             server,
    157             sender_id,
    158             version_range: version_range
    159                 .ok_or(RadrootsSimplexSmpProtoError::MissingField("version_range"))?,
    160             recipient_dh_public_key: recipient_dh_public_key.ok_or(
    161                 RadrootsSimplexSmpProtoError::MissingField("recipient_dh_public_key"),
    162             )?,
    163             queue_mode,
    164         })
    165     }
    166 }
    167 
    168 impl fmt::Display for RadrootsSimplexSmpQueueUri {
    169     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    170         let authority_hosts =
    171             if self.version_range.min >= RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION {
    172                 self.server.hosts.join(",")
    173             } else {
    174                 self.server.hosts.first().cloned().ok_or(fmt::Error)?
    175             };
    176         write!(
    177             f,
    178             "{RADROOTS_SIMPLEX_SMP_URI_SCHEME}://{}@{}",
    179             self.server.server_identity, authority_hosts,
    180         )?;
    181         if let Some(port) = self.server.port {
    182             write!(f, ":{port}")?;
    183         }
    184         write!(f, "/{}#/?v={}", self.sender_id, self.version_range)?;
    185         write!(f, "&dh={}", self.recipient_dh_public_key)?;
    186         if self.version_range.min >= RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION {
    187             if let Some(queue_mode) = self.queue_mode {
    188                 write!(f, "&q={}", queue_mode.as_query_value())?;
    189             }
    190         } else if self.sender_can_secure() {
    191             write!(f, "&k=s")?;
    192         }
    193         if self.version_range.min < RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION
    194             && self.server.hosts.len() > 1
    195         {
    196             write!(f, "&srv={}", self.server.hosts[1..].join(","))?;
    197         }
    198         Ok(())
    199     }
    200 }
    201 
    202 impl FromStr for RadrootsSimplexSmpQueueUri {
    203     type Err = RadrootsSimplexSmpProtoError;
    204 
    205     fn from_str(value: &str) -> Result<Self, Self::Err> {
    206         Self::parse(value)
    207     }
    208 }
    209 
    210 fn parse_server_address(
    211     authority: &str,
    212 ) -> Result<RadrootsSimplexSmpServerAddress, RadrootsSimplexSmpProtoError> {
    213     let (server_identity, host_part) = authority
    214         .split_once('@')
    215         .ok_or_else(|| RadrootsSimplexSmpProtoError::InvalidUri(authority.to_string()))?;
    216     validate_base64_url("server_identity", server_identity)?;
    217 
    218     let (hosts_raw, port) = match host_part.rsplit_once(':') {
    219         Some((hosts, port)) if port.chars().all(|ch| ch.is_ascii_digit()) => {
    220             let port = port
    221                 .parse::<u16>()
    222                 .map_err(|_| RadrootsSimplexSmpProtoError::InvalidPort(port.to_string()))?;
    223             (hosts, Some(port))
    224         }
    225         _ => (host_part, None),
    226     };
    227 
    228     if hosts_raw.is_empty() {
    229         return Err(RadrootsSimplexSmpProtoError::InvalidHostList(
    230             hosts_raw.to_string(),
    231         ));
    232     }
    233 
    234     let hosts = hosts_raw
    235         .split(',')
    236         .map(|host| host.trim().to_string())
    237         .collect::<Vec<_>>();
    238     if hosts.iter().any(|host| host.is_empty()) {
    239         return Err(RadrootsSimplexSmpProtoError::InvalidHostList(
    240             hosts_raw.to_string(),
    241         ));
    242     }
    243 
    244     Ok(RadrootsSimplexSmpServerAddress {
    245         server_identity: server_identity.to_string(),
    246         hosts,
    247         port,
    248     })
    249 }
    250 
    251 fn parse_fragment_query<'a>(
    252     fragment: &'a str,
    253     original: &str,
    254 ) -> Result<(Option<String>, Option<&'a str>), RadrootsSimplexSmpProtoError> {
    255     let fragment = fragment.strip_prefix('/').unwrap_or(fragment);
    256     if let Some(query) = fragment.strip_prefix('?') {
    257         return Ok((None, Some(query)));
    258     }
    259     if let Some((dh_public_key, query)) = fragment.split_once("/?") {
    260         validate_base64_url("recipient_dh_public_key", dh_public_key)?;
    261         return Ok((Some(dh_public_key.to_string()), Some(query)));
    262     }
    263     if let Some((dh_public_key, query)) = fragment.split_once('?') {
    264         validate_base64_url("recipient_dh_public_key", dh_public_key)?;
    265         return Ok((Some(dh_public_key.to_string()), Some(query)));
    266     }
    267     if !fragment.is_empty() {
    268         validate_base64_url("recipient_dh_public_key", fragment)?;
    269         return Ok((Some(fragment.to_string()), None));
    270     }
    271     Err(RadrootsSimplexSmpProtoError::InvalidUri(
    272         original.to_string(),
    273     ))
    274 }
    275 
    276 fn parse_host_list(
    277     value: &str,
    278     original: &str,
    279 ) -> Result<Vec<String>, RadrootsSimplexSmpProtoError> {
    280     let hosts = value
    281         .split(',')
    282         .map(|host| host.trim().to_string())
    283         .collect::<Vec<_>>();
    284     if hosts.is_empty() || hosts.iter().any(|host| host.is_empty()) {
    285         return Err(RadrootsSimplexSmpProtoError::InvalidHostList(
    286             original.to_string(),
    287         ));
    288     }
    289     Ok(hosts)
    290 }
    291 
    292 fn validate_base64_url(
    293     field: &'static str,
    294     value: &str,
    295 ) -> Result<(), RadrootsSimplexSmpProtoError> {
    296     base64::engine::general_purpose::URL_SAFE_NO_PAD
    297         .decode(value)
    298         .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(value))
    299         .map(|_| ())
    300         .map_err(|_| RadrootsSimplexSmpProtoError::InvalidBase64Url {
    301             field,
    302             value: value.to_string(),
    303         })
    304 }
    305 
    306 #[cfg(test)]
    307 mod tests {
    308     use super::*;
    309 
    310     #[test]
    311     fn parses_and_formats_queue_uri() {
    312         let uri = RadrootsSimplexSmpQueueUri::parse(
    313             "smp://YWJjZA@server1.example,server2.example:5223/cXVldWU#/?v=4&dh=ZGhLZXk&q=m",
    314         )
    315         .unwrap();
    316 
    317         assert_eq!(uri.server.server_identity, "YWJjZA");
    318         assert_eq!(
    319             uri.server.hosts,
    320             vec!["server1.example".to_string(), "server2.example".to_string()]
    321         );
    322         assert_eq!(uri.server.port, Some(5223));
    323         assert_eq!(uri.sender_id, "cXVldWU");
    324         assert_eq!(uri.version_range, RadrootsSimplexSmpVersionRange::single(4));
    325         assert_eq!(uri.recipient_dh_public_key, "ZGhLZXk");
    326         assert_eq!(uri.queue_mode, Some(RadrootsSimplexSmpQueueMode::Messaging));
    327         assert!(uri.sender_can_secure());
    328         assert_eq!(
    329             uri.to_string(),
    330             "smp://YWJjZA@server1.example,server2.example:5223/cXVldWU#/?v=4&dh=ZGhLZXk&q=m"
    331         );
    332     }
    333 
    334     #[test]
    335     fn rejects_invalid_base64_fields() {
    336         let error =
    337             RadrootsSimplexSmpQueueUri::parse("smp://***@server.example/cXVldWU#/?v=4&dh=ZGhLZXk")
    338                 .unwrap_err();
    339         assert!(matches!(
    340             error,
    341             RadrootsSimplexSmpProtoError::InvalidBase64Url {
    342                 field: "server_identity",
    343                 ..
    344             }
    345         ));
    346     }
    347 
    348     #[test]
    349     fn parses_padded_server_identity_and_dh_public_key() {
    350         let uri = RadrootsSimplexSmpQueueUri::parse(
    351             "smp://YWJjZA==@server.example/cXVldWU=#/?v=4&dh=ZGhLZXk=",
    352         )
    353         .unwrap();
    354 
    355         assert_eq!(uri.server.server_identity, "YWJjZA==");
    356         assert_eq!(uri.sender_id, "cXVldWU=");
    357         assert_eq!(uri.recipient_dh_public_key, "ZGhLZXk=");
    358     }
    359 
    360     #[test]
    361     fn parses_legacy_sender_secure_queue_uri() {
    362         let uri = RadrootsSimplexSmpQueueUri::parse(
    363             "smp://YWJjZA@server1.example:5223/cXVldWU#/?v=1-3&dh=ZGhLZXk&k=s&srv=server2.example",
    364         )
    365         .unwrap();
    366 
    367         assert_eq!(
    368             uri.server.hosts,
    369             vec!["server1.example".to_string(), "server2.example".to_string()]
    370         );
    371         assert_eq!(uri.queue_mode, Some(RadrootsSimplexSmpQueueMode::Messaging));
    372         assert_eq!(
    373             uri.version_range,
    374             RadrootsSimplexSmpVersionRange::new(1, 3).unwrap()
    375         );
    376         assert_eq!(
    377             uri.to_string(),
    378             "smp://YWJjZA@server1.example:5223/cXVldWU#/?v=1-3&dh=ZGhLZXk&k=s&srv=server2.example"
    379         );
    380     }
    381 
    382     #[test]
    383     fn parses_legacy_unversioned_queue_uri() {
    384         let uri =
    385             RadrootsSimplexSmpQueueUri::parse("smp://YWJjZA@server1.example/cXVldWU/#ZGhLZXk")
    386                 .unwrap();
    387 
    388         assert_eq!(uri.version_range, RadrootsSimplexSmpVersionRange::single(1));
    389         assert_eq!(uri.recipient_dh_public_key, "ZGhLZXk");
    390         assert_eq!(uri.queue_mode, None);
    391         assert_eq!(
    392             uri.to_string(),
    393             "smp://YWJjZA@server1.example/cXVldWU#/?v=1&dh=ZGhLZXk"
    394         );
    395     }
    396 }