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 }