uri.rs (20978B)
1 //! Validated `nostrconnect://` and `bunker://` URI models. 2 3 use crate::error::RadrootsNostrConnectError; 4 use crate::permission::Permissions; 5 use radroots_identity::PublicKey; 6 use serde::{Deserialize, Serialize}; 7 use std::fmt; 8 use std::str::FromStr; 9 use url::Url; 10 11 pub const URI_SCHEME: &str = "nostrconnect"; 12 pub const BUNKER_URI_SCHEME: &str = "bunker"; 13 pub const CLIENT_NAME_MAX_BYTES: usize = 128; 14 pub const CLIENT_URL_MAX_BYTES: usize = 2_048; 15 pub const CLIENT_METADATA_JSON_MAX_BYTES: usize = 4_352; 16 pub const URI_MAX_BYTES: usize = 16_384; 17 pub const RELAY_COUNT_MAX: usize = 32; 18 pub const SECRET_MAX_BYTES: usize = 1_024; 19 20 /// A validated WebSocket relay URL owned by the NIP-46 protocol package. 21 #[derive(Clone, PartialEq, Eq, Hash)] 22 pub struct RelayUrl(nostr::RelayUrl); 23 24 impl RelayUrl { 25 /// Parses a relay URL accepted by the underlying Nostr wire protocol. 26 pub fn parse(value: &str) -> Result<Self, RadrootsNostrConnectError> { 27 nostr::RelayUrl::parse(value).map(Self).map_err(|error| { 28 RadrootsNostrConnectError::InvalidRelayUrl { 29 value: value.to_owned(), 30 reason: error.to_string(), 31 } 32 }) 33 } 34 } 35 36 impl fmt::Debug for RelayUrl { 37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 38 formatter.debug_tuple("RelayUrl").field(&self.0).finish() 39 } 40 } 41 42 impl fmt::Display for RelayUrl { 43 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 44 self.0.fmt(formatter) 45 } 46 } 47 48 impl FromStr for RelayUrl { 49 type Err = RadrootsNostrConnectError; 50 51 fn from_str(value: &str) -> Result<Self, Self::Err> { 52 Self::parse(value) 53 } 54 } 55 56 impl Serialize for RelayUrl { 57 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 58 where 59 S: serde::Serializer, 60 { 61 serializer.serialize_str(&self.to_string()) 62 } 63 } 64 65 impl<'de> Deserialize<'de> for RelayUrl { 66 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 67 where 68 D: serde::Deserializer<'de>, 69 { 70 let value = String::deserialize(deserializer)?; 71 Self::parse(&value).map_err(serde::de::Error::custom) 72 } 73 } 74 75 #[derive(Clone, PartialEq, Eq)] 76 pub struct BunkerUri { 77 remote_signer_public_key: PublicKey, 78 relays: Vec<RelayUrl>, 79 secret: Option<String>, 80 } 81 82 impl BunkerUri { 83 #[must_use] 84 pub const fn remote_signer_public_key(&self) -> PublicKey { 85 self.remote_signer_public_key 86 } 87 88 #[must_use] 89 pub fn relays(&self) -> &[RelayUrl] { 90 &self.relays 91 } 92 93 #[must_use] 94 pub fn secret(&self) -> Option<&str> { 95 self.secret.as_deref() 96 } 97 } 98 99 impl fmt::Debug for BunkerUri { 100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 101 formatter 102 .debug_struct("BunkerUri") 103 .field("remote_signer_public_key", &self.remote_signer_public_key) 104 .field("relays", &self.relays) 105 .field("secret", &self.secret.as_ref().map(|_| "[redacted]")) 106 .finish() 107 } 108 } 109 110 #[derive(Debug, Clone, Default, PartialEq, Eq)] 111 pub struct ClientMetadata { 112 #[doc(hidden)] 113 pub requested_permissions: Permissions, 114 #[doc(hidden)] 115 pub name: Option<String>, 116 #[doc(hidden)] 117 pub url: Option<String>, 118 #[doc(hidden)] 119 pub image: Option<String>, 120 } 121 122 #[derive(Debug, Serialize, Deserialize)] 123 struct ClientMetadataSerde { 124 #[serde(default, skip_serializing_if = "Permissions::is_empty")] 125 requested_permissions: Permissions, 126 #[serde(default, skip_serializing_if = "Option::is_none")] 127 name: Option<String>, 128 #[serde(default, skip_serializing_if = "Option::is_none")] 129 url: Option<String>, 130 #[serde(default, skip_serializing_if = "Option::is_none")] 131 image: Option<String>, 132 } 133 134 #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] 135 struct ClientMetadataWire { 136 #[serde(default, skip_serializing_if = "Option::is_none")] 137 name: Option<String>, 138 #[serde(default, skip_serializing_if = "Option::is_none")] 139 url: Option<String>, 140 #[serde(default, skip_serializing_if = "Option::is_none")] 141 image: Option<String>, 142 } 143 144 impl ClientMetadata { 145 #[must_use] 146 pub fn new() -> Self { 147 Self::default() 148 } 149 150 #[must_use] 151 pub fn requested_permissions(&self) -> &Permissions { 152 &self.requested_permissions 153 } 154 155 #[must_use] 156 pub fn name(&self) -> Option<&str> { 157 self.name.as_deref() 158 } 159 160 #[must_use] 161 pub fn url(&self) -> Option<&str> { 162 self.url.as_deref() 163 } 164 165 #[must_use] 166 pub fn image(&self) -> Option<&str> { 167 self.image.as_deref() 168 } 169 170 #[must_use] 171 pub fn with_requested_permissions(mut self, permissions: Permissions) -> Self { 172 self.requested_permissions = permissions; 173 self 174 } 175 176 pub fn with_name( 177 mut self, 178 value: impl Into<String>, 179 ) -> Result<Self, RadrootsNostrConnectError> { 180 self.name = Some(normalize_client_name(&value.into())?); 181 Ok(self) 182 } 183 184 pub fn with_url(mut self, value: impl Into<String>) -> Result<Self, RadrootsNostrConnectError> { 185 self.url = Some(normalize_client_url("url", &value.into())?); 186 Ok(self) 187 } 188 189 pub fn with_image( 190 mut self, 191 value: impl Into<String>, 192 ) -> Result<Self, RadrootsNostrConnectError> { 193 self.image = Some(normalize_client_url("image", &value.into())?); 194 Ok(self) 195 } 196 197 pub fn normalized(self) -> Result<Self, RadrootsNostrConnectError> { 198 Ok(Self { 199 requested_permissions: self.requested_permissions, 200 name: self 201 .name 202 .map(|value| normalize_client_name(&value)) 203 .transpose()?, 204 url: self 205 .url 206 .map(|value| normalize_client_url("url", &value)) 207 .transpose()?, 208 image: self 209 .image 210 .map(|value| normalize_client_url("image", &value)) 211 .transpose()?, 212 }) 213 } 214 215 pub fn to_connect_param(&self) -> Result<String, RadrootsNostrConnectError> { 216 let normalized = self.clone().normalized()?; 217 let wire = ClientMetadataWire { 218 name: normalized.name, 219 url: normalized.url, 220 image: normalized.image, 221 }; 222 let value = serde_json::to_string(&wire)?; 223 validate_metadata_size(&value)?; 224 Ok(value) 225 } 226 227 pub fn from_connect_param(value: &str) -> Result<Self, RadrootsNostrConnectError> { 228 validate_metadata_size(value)?; 229 let wire: ClientMetadataWire = serde_json::from_str(value).map_err(|error| { 230 RadrootsNostrConnectError::InvalidClientMetadata { 231 field: "payload", 232 reason: error.to_string(), 233 } 234 })?; 235 Self { 236 requested_permissions: Permissions::default(), 237 name: wire.name, 238 url: wire.url, 239 image: wire.image, 240 } 241 .normalized() 242 } 243 244 pub fn is_display_empty(&self) -> bool { 245 self.name.is_none() && self.url.is_none() && self.image.is_none() 246 } 247 } 248 249 impl Serialize for ClientMetadata { 250 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 251 where 252 S: serde::Serializer, 253 { 254 let normalized = self 255 .clone() 256 .normalized() 257 .map_err(serde::ser::Error::custom)?; 258 ClientMetadataSerde { 259 requested_permissions: normalized.requested_permissions, 260 name: normalized.name, 261 url: normalized.url, 262 image: normalized.image, 263 } 264 .serialize(serializer) 265 } 266 } 267 268 impl<'de> Deserialize<'de> for ClientMetadata { 269 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 270 where 271 D: serde::Deserializer<'de>, 272 { 273 let metadata = ClientMetadataSerde::deserialize(deserializer)?; 274 Self { 275 requested_permissions: metadata.requested_permissions, 276 name: metadata.name, 277 url: metadata.url, 278 image: metadata.image, 279 } 280 .normalized() 281 .map_err(serde::de::Error::custom) 282 } 283 } 284 285 #[derive(Clone, PartialEq, Eq)] 286 pub struct ClientUri { 287 client_public_key: PublicKey, 288 relays: Vec<RelayUrl>, 289 secret: String, 290 metadata: ClientMetadata, 291 } 292 293 impl ClientUri { 294 /// Constructs a validated client-origin URI from canonical protocol values. 295 pub fn try_new( 296 client_public_key: PublicKey, 297 relays: impl IntoIterator<Item = RelayUrl>, 298 secret: impl Into<String>, 299 metadata: ClientMetadata, 300 ) -> Result<Self, RadrootsNostrConnectError> { 301 let mut normalized_relays = Vec::new(); 302 for relay in relays { 303 if normalized_relays.contains(&relay) { 304 continue; 305 } 306 if normalized_relays.len() == RELAY_COUNT_MAX { 307 return Err(RadrootsNostrConnectError::InvalidUri); 308 } 309 normalized_relays.push(relay); 310 } 311 if normalized_relays.is_empty() { 312 return Err(RadrootsNostrConnectError::MissingRelay); 313 } 314 let secret = secret.into(); 315 if secret.is_empty() { 316 return Err(RadrootsNostrConnectError::MissingSecret); 317 } 318 validate_secret(&secret)?; 319 Ok(Self { 320 client_public_key, 321 relays: normalized_relays, 322 secret, 323 metadata: metadata.normalized()?, 324 }) 325 } 326 327 #[must_use] 328 pub const fn client_public_key(&self) -> PublicKey { 329 self.client_public_key 330 } 331 332 #[must_use] 333 pub fn relays(&self) -> &[RelayUrl] { 334 &self.relays 335 } 336 337 #[must_use] 338 pub fn secret(&self) -> &str { 339 &self.secret 340 } 341 342 #[must_use] 343 pub fn metadata(&self) -> &ClientMetadata { 344 &self.metadata 345 } 346 } 347 348 impl fmt::Debug for ClientUri { 349 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 350 formatter 351 .debug_struct("ClientUri") 352 .field("client_public_key", &self.client_public_key) 353 .field("relays", &self.relays) 354 .field("secret", &"[redacted]") 355 .field("metadata", &self.metadata) 356 .finish() 357 } 358 } 359 360 #[derive(Debug, Clone, PartialEq, Eq)] 361 pub enum Uri { 362 Bunker(BunkerUri), 363 Client(ClientUri), 364 } 365 366 impl Uri { 367 pub fn parse(value: &str) -> Result<Self, RadrootsNostrConnectError> { 368 if value.len() > URI_MAX_BYTES { 369 return Err(RadrootsNostrConnectError::InvalidUri); 370 } 371 let url = Url::parse(value).map_err(|error| RadrootsNostrConnectError::InvalidUrl { 372 value: "[redacted NIP-46 URI]".to_owned(), 373 reason: error.to_string(), 374 })?; 375 let host = url 376 .host_str() 377 .ok_or(RadrootsNostrConnectError::MissingPublicKey)?; 378 379 match url.scheme() { 380 BUNKER_URI_SCHEME => { 381 let remote_signer_public_key = parse_public_key(host)?; 382 let mut relays = Vec::new(); 383 let mut secret = None; 384 385 for (key, value) in url.query_pairs() { 386 match key.as_ref() { 387 "relay" => push_relay(&mut relays, value.as_ref())?, 388 "secret" => set_once(&mut secret, value.into_owned())?, 389 _ => {} 390 } 391 } 392 393 if relays.is_empty() { 394 return Err(RadrootsNostrConnectError::MissingRelay); 395 } 396 397 validate_optional_secret(secret.as_deref())?; 398 Ok(Self::Bunker(BunkerUri { 399 remote_signer_public_key, 400 relays, 401 secret, 402 })) 403 } 404 URI_SCHEME => { 405 let client_public_key = parse_public_key(host)?; 406 let mut relays = Vec::new(); 407 let mut secret = None; 408 let mut metadata = ClientMetadata::default(); 409 let mut permissions_seen = false; 410 411 for (key, value) in url.query_pairs() { 412 match key.as_ref() { 413 "relay" => push_relay(&mut relays, value.as_ref())?, 414 "secret" => set_once(&mut secret, value.into_owned())?, 415 "perms" => { 416 if permissions_seen { 417 return Err(RadrootsNostrConnectError::InvalidUri); 418 } 419 permissions_seen = true; 420 metadata.requested_permissions = Permissions::from_str(value.as_ref())?; 421 } 422 "name" => set_once(&mut metadata.name, value.into_owned())?, 423 "url" => set_once(&mut metadata.url, value.into_owned())?, 424 "image" => set_once(&mut metadata.image, value.into_owned())?, 425 _ => {} 426 } 427 } 428 429 if relays.is_empty() { 430 return Err(RadrootsNostrConnectError::MissingRelay); 431 } 432 433 let secret = secret.ok_or(RadrootsNostrConnectError::MissingSecret)?; 434 if secret.is_empty() { 435 return Err(RadrootsNostrConnectError::MissingSecret); 436 } 437 validate_secret(&secret)?; 438 let metadata = metadata.normalized()?; 439 440 Ok(Self::Client(ClientUri { 441 client_public_key, 442 relays, 443 secret, 444 metadata, 445 })) 446 } 447 scheme => Err(RadrootsNostrConnectError::InvalidUriScheme( 448 scheme.to_owned(), 449 )), 450 } 451 } 452 } 453 454 impl FromStr for Uri { 455 type Err = RadrootsNostrConnectError; 456 457 fn from_str(value: &str) -> Result<Self, Self::Err> { 458 Self::parse(value) 459 } 460 } 461 462 impl fmt::Display for Uri { 463 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 464 match self { 465 Self::Bunker(uri) => uri.fmt(f), 466 Self::Client(uri) => uri.fmt(f), 467 } 468 } 469 } 470 471 impl fmt::Display for BunkerUri { 472 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 473 let mut serializer = url::form_urlencoded::Serializer::new(String::new()); 474 for relay in &self.relays { 475 serializer.append_pair("relay", &relay.to_string()); 476 } 477 if let Some(secret) = &self.secret { 478 serializer.append_pair("secret", secret); 479 } 480 let query = serializer.finish(); 481 write!( 482 formatter, 483 "{BUNKER_URI_SCHEME}://{}?{query}", 484 self.remote_signer_public_key 485 ) 486 } 487 } 488 489 impl fmt::Display for ClientUri { 490 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 491 let mut serializer = url::form_urlencoded::Serializer::new(String::new()); 492 for relay in &self.relays { 493 serializer.append_pair("relay", &relay.to_string()); 494 } 495 serializer.append_pair("secret", &self.secret); 496 if !self.metadata.requested_permissions.is_empty() { 497 serializer.append_pair("perms", &self.metadata.requested_permissions.to_string()); 498 } 499 if let Some(name) = &self.metadata.name { 500 serializer.append_pair("name", name); 501 } 502 if let Some(url) = &self.metadata.url { 503 serializer.append_pair("url", url); 504 } 505 if let Some(image) = &self.metadata.image { 506 serializer.append_pair("image", image); 507 } 508 let query = serializer.finish(); 509 write!( 510 formatter, 511 "{URI_SCHEME}://{}?{query}", 512 self.client_public_key 513 ) 514 } 515 } 516 517 macro_rules! impl_uri_serde { 518 ($type:ty, $variant:path) => { 519 impl Serialize for $type { 520 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 521 where 522 S: serde::Serializer, 523 { 524 serializer.serialize_str(&self.to_string()) 525 } 526 } 527 528 impl<'de> Deserialize<'de> for $type { 529 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 530 where 531 D: serde::Deserializer<'de>, 532 { 533 let value = String::deserialize(deserializer)?; 534 match Uri::parse(&value).map_err(serde::de::Error::custom)? { 535 $variant(uri) => Ok(uri), 536 _ => Err(serde::de::Error::custom("unexpected NIP-46 URI scheme")), 537 } 538 } 539 } 540 }; 541 } 542 543 impl_uri_serde!(BunkerUri, Uri::Bunker); 544 impl_uri_serde!(ClientUri, Uri::Client); 545 546 impl Serialize for Uri { 547 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> 548 where 549 S: serde::Serializer, 550 { 551 serializer.serialize_str(&self.to_string()) 552 } 553 } 554 555 impl<'de> Deserialize<'de> for Uri { 556 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 557 where 558 D: serde::Deserializer<'de>, 559 { 560 let value = String::deserialize(deserializer)?; 561 Self::parse(&value).map_err(serde::de::Error::custom) 562 } 563 } 564 565 fn parse_public_key(value: &str) -> Result<PublicKey, RadrootsNostrConnectError> { 566 radroots_nostr::key::parse_public_key(value).map_err(|error| { 567 RadrootsNostrConnectError::InvalidPublicKey { 568 value: value.to_owned(), 569 reason: error.to_string(), 570 } 571 }) 572 } 573 574 fn parse_relay_url(value: &str) -> Result<RelayUrl, RadrootsNostrConnectError> { 575 RelayUrl::parse(value) 576 } 577 578 fn push_relay(relays: &mut Vec<RelayUrl>, value: &str) -> Result<(), RadrootsNostrConnectError> { 579 let relay = parse_relay_url(value)?; 580 if relays.contains(&relay) { 581 return Ok(()); 582 } 583 if relays.len() == RELAY_COUNT_MAX { 584 return Err(RadrootsNostrConnectError::InvalidUri); 585 } 586 relays.push(relay); 587 Ok(()) 588 } 589 590 fn set_once(slot: &mut Option<String>, value: String) -> Result<(), RadrootsNostrConnectError> { 591 if slot.replace(value).is_some() { 592 return Err(RadrootsNostrConnectError::InvalidUri); 593 } 594 Ok(()) 595 } 596 597 fn validate_optional_secret(secret: Option<&str>) -> Result<(), RadrootsNostrConnectError> { 598 match secret { 599 Some("") => Err(RadrootsNostrConnectError::InvalidUri), 600 Some(secret) => validate_secret(secret), 601 None => Ok(()), 602 } 603 } 604 605 fn validate_secret(secret: &str) -> Result<(), RadrootsNostrConnectError> { 606 if secret.len() > SECRET_MAX_BYTES || secret.chars().any(char::is_control) { 607 return Err(RadrootsNostrConnectError::InvalidUri); 608 } 609 Ok(()) 610 } 611 612 fn normalize_client_name(value: &str) -> Result<String, RadrootsNostrConnectError> { 613 let normalized = value.trim(); 614 if normalized.is_empty() { 615 return Err(invalid_client_metadata("name", "must not be empty")); 616 } 617 if normalized.len() > CLIENT_NAME_MAX_BYTES { 618 return Err(invalid_client_metadata( 619 "name", 620 format!("must not exceed {CLIENT_NAME_MAX_BYTES} UTF-8 bytes"), 621 )); 622 } 623 if normalized.chars().any(char::is_control) { 624 return Err(invalid_client_metadata( 625 "name", 626 "must not contain control characters", 627 )); 628 } 629 Ok(normalized.to_owned()) 630 } 631 632 fn normalize_client_url( 633 field: &'static str, 634 value: &str, 635 ) -> Result<String, RadrootsNostrConnectError> { 636 if value.len() > CLIENT_URL_MAX_BYTES { 637 return Err(invalid_client_metadata( 638 field, 639 format!("must not exceed {CLIENT_URL_MAX_BYTES} UTF-8 bytes"), 640 )); 641 } 642 if value.chars().any(char::is_control) { 643 return Err(invalid_client_metadata( 644 field, 645 "must not contain control characters", 646 )); 647 } 648 let parsed = 649 Url::parse(value).map_err(|error| invalid_client_metadata(field, error.to_string()))?; 650 if !matches!(parsed.scheme(), "http" | "https") { 651 return Err(invalid_client_metadata( 652 field, 653 "must use the http or https scheme", 654 )); 655 } 656 if !parsed.username().is_empty() || parsed.password().is_some() { 657 return Err(invalid_client_metadata( 658 field, 659 "must not contain credentials", 660 )); 661 } 662 Ok(parsed.to_string()) 663 } 664 665 fn validate_metadata_size(value: &str) -> Result<(), RadrootsNostrConnectError> { 666 let received = value.len(); 667 if received > CLIENT_METADATA_JSON_MAX_BYTES { 668 return Err(RadrootsNostrConnectError::ClientMetadataTooLarge { 669 max: CLIENT_METADATA_JSON_MAX_BYTES, 670 received, 671 }); 672 } 673 Ok(()) 674 } 675 676 fn invalid_client_metadata( 677 field: &'static str, 678 reason: impl Into<String>, 679 ) -> RadrootsNostrConnectError { 680 RadrootsNostrConnectError::InvalidClientMetadata { 681 field, 682 reason: reason.into(), 683 } 684 }