target.rs (27996B)
1 //! Extensible transport identities and canonical operation targets. 2 3 use crate::{ 4 Error as TransportError, TransportId, 5 endpoint::{ENDPOINT_URI_MAX_BYTES, TARGET_LABEL_MAX_BYTES, TARGET_SCOPE_MAX_BYTES}, 6 }; 7 use alloc::collections::BTreeSet; 8 use alloc::format; 9 use alloc::string::{String, ToString}; 10 use alloc::vec::Vec; 11 use core::net::{IpAddr, Ipv6Addr}; 12 use core::str::FromStr; 13 use sha2::{Digest, Sha256}; 14 15 /// Maximum number of targets in one operation. 16 pub const TARGET_SET_MAX_ITEMS: usize = 64; 17 18 /// Construction policy for canonical Nostr relay targets. 19 /// 20 /// This policy controls only whether a cleartext relay identifier may be 21 /// represented. A concrete adapter must independently retain and enforce its 22 /// network policy before and after address resolution. 23 #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 24 #[non_exhaustive] 25 pub enum TargetNetworkPolicy { 26 /// TLS relay targets plus exact loopback cleartext targets. 27 TlsOrLoopback, 28 /// Exact RFC1918 IPv4 or ULA IPv6 device targets, with or without TLS. 29 PrivateDevice, 30 } 31 32 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 33 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 34 /// Canonical transport endpoint URI. 35 pub struct EndpointUri(String); 36 37 impl EndpointUri { 38 pub fn parse(raw: impl AsRef<str>) -> Result<Self, TransportError> { 39 let canonical = canonicalize_uri(raw.as_ref())?; 40 Ok(Self(canonical)) 41 } 42 43 fn parse_nostr_relay( 44 raw: impl AsRef<str>, 45 policy: TargetNetworkPolicy, 46 ) -> Result<Self, TransportError> { 47 let canonical = canonicalize_nostr_relay_uri(raw.as_ref(), policy)?; 48 Ok(Self(canonical)) 49 } 50 51 pub fn as_str(&self) -> &str { 52 &self.0 53 } 54 } 55 56 impl core::fmt::Display for EndpointUri { 57 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 58 f.write_str(&self.0) 59 } 60 } 61 62 impl AsRef<str> for EndpointUri { 63 fn as_ref(&self) -> &str { 64 self.as_str() 65 } 66 } 67 68 impl FromStr for EndpointUri { 69 type Err = TransportError; 70 71 fn from_str(value: &str) -> Result<Self, Self::Err> { 72 Self::parse(value) 73 } 74 } 75 76 impl TryFrom<&str> for EndpointUri { 77 type Error = TransportError; 78 79 fn try_from(value: &str) -> Result<Self, Self::Error> { 80 Self::parse(value) 81 } 82 } 83 84 #[cfg(feature = "serde")] 85 impl<'de> serde::Deserialize<'de> for EndpointUri { 86 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 87 where 88 D: serde::Deserializer<'de>, 89 { 90 let raw = <String as serde::Deserialize>::deserialize(deserializer)?; 91 let parsed = Self::parse(raw.as_str()).map_err(serde::de::Error::custom)?; 92 if parsed.as_str() != raw { 93 return Err(serde::de::Error::custom( 94 "transport target URI is not canonical", 95 )); 96 } 97 Ok(parsed) 98 } 99 } 100 101 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 102 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 103 /// Optional transport-neutral target scope. 104 pub struct TargetScope(String); 105 106 impl TargetScope { 107 pub fn parse(raw: impl AsRef<str>) -> Result<Self, TransportError> { 108 let value = raw.as_ref(); 109 if value.is_empty() { 110 return Err(TransportError::EmptyTargetScope); 111 } 112 if value.len() > TARGET_SCOPE_MAX_BYTES 113 || value != value.trim() 114 || value 115 .chars() 116 .any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.'))) 117 { 118 return Err(TransportError::InvalidTargetScope); 119 } 120 Ok(Self(value.to_string())) 121 } 122 123 pub fn as_str(&self) -> &str { 124 &self.0 125 } 126 } 127 128 impl core::fmt::Display for TargetScope { 129 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 130 f.write_str(&self.0) 131 } 132 } 133 134 impl AsRef<str> for TargetScope { 135 fn as_ref(&self) -> &str { 136 self.as_str() 137 } 138 } 139 140 impl FromStr for TargetScope { 141 type Err = TransportError; 142 143 fn from_str(value: &str) -> Result<Self, Self::Err> { 144 Self::parse(value) 145 } 146 } 147 148 impl TryFrom<&str> for TargetScope { 149 type Error = TransportError; 150 151 fn try_from(value: &str) -> Result<Self, Self::Error> { 152 Self::parse(value) 153 } 154 } 155 156 #[cfg(feature = "serde")] 157 impl<'de> serde::Deserialize<'de> for TargetScope { 158 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 159 where 160 D: serde::Deserializer<'de>, 161 { 162 let raw = <String as serde::Deserialize>::deserialize(deserializer)?; 163 Self::parse(raw).map_err(serde::de::Error::custom) 164 } 165 } 166 167 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 168 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 169 /// Optional human-readable target label excluded from target identity. 170 pub struct TargetLabel(String); 171 172 impl TargetLabel { 173 pub fn parse(raw: impl AsRef<str>) -> Result<Self, TransportError> { 174 let raw = raw.as_ref(); 175 let trimmed = raw.trim(); 176 if trimmed.is_empty() { 177 return Err(TransportError::EmptyTargetLabel); 178 } 179 if raw.len() > TARGET_LABEL_MAX_BYTES || trimmed.chars().any(char::is_control) { 180 return Err(TransportError::InvalidTargetLabel); 181 } 182 Ok(Self(trimmed.to_string())) 183 } 184 185 pub fn as_str(&self) -> &str { 186 &self.0 187 } 188 } 189 190 impl core::fmt::Display for TargetLabel { 191 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 192 f.write_str(&self.0) 193 } 194 } 195 196 impl AsRef<str> for TargetLabel { 197 fn as_ref(&self) -> &str { 198 self.as_str() 199 } 200 } 201 202 impl FromStr for TargetLabel { 203 type Err = TransportError; 204 205 fn from_str(value: &str) -> Result<Self, Self::Err> { 206 Self::parse(value) 207 } 208 } 209 210 impl TryFrom<&str> for TargetLabel { 211 type Error = TransportError; 212 213 fn try_from(value: &str) -> Result<Self, Self::Error> { 214 Self::parse(value) 215 } 216 } 217 218 #[cfg(feature = "serde")] 219 impl<'de> serde::Deserialize<'de> for TargetLabel { 220 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 221 where 222 D: serde::Deserializer<'de>, 223 { 224 let raw = <String as serde::Deserialize>::deserialize(deserializer)?; 225 let parsed = Self::parse(raw.as_str()).map_err(serde::de::Error::custom)?; 226 if parsed.as_str() != raw { 227 return Err(serde::de::Error::custom( 228 "transport target label is not canonical", 229 )); 230 } 231 Ok(parsed) 232 } 233 } 234 235 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 236 #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] 237 /// Deterministic SHA-256 fingerprint of canonical target identity fields. 238 pub struct TargetFingerprint(String); 239 240 impl TargetFingerprint { 241 pub fn from_target(kind: &TransportId, uri: &EndpointUri, scope: Option<&TargetScope>) -> Self { 242 let mut hasher = Sha256::new(); 243 hasher.update(kind.canonical_label().as_bytes()); 244 hasher.update([0]); 245 hasher.update(uri.as_str().as_bytes()); 246 if let Some(scope) = scope { 247 hasher.update([0]); 248 hasher.update(scope.as_str().as_bytes()); 249 } 250 let digest = hasher.finalize(); 251 Self(hex_encode(&digest)) 252 } 253 254 pub fn parse(raw: impl AsRef<str>) -> Result<Self, TransportError> { 255 let raw = raw.as_ref(); 256 if raw.len() != 64 || !raw.bytes().all(|byte| byte.is_ascii_hexdigit()) { 257 return Err(TransportError::InvalidTargetFingerprint); 258 } 259 Ok(Self(raw.to_ascii_lowercase())) 260 } 261 262 pub fn as_str(&self) -> &str { 263 &self.0 264 } 265 } 266 267 fn hex_encode(bytes: &[u8]) -> String { 268 const HEX: &[u8; 16] = b"0123456789abcdef"; 269 let mut out = String::with_capacity(bytes.len() * 2); 270 for byte in bytes { 271 out.push(HEX[(byte >> 4) as usize] as char); 272 out.push(HEX[(byte & 0x0f) as usize] as char); 273 } 274 out 275 } 276 277 impl core::fmt::Display for TargetFingerprint { 278 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 279 f.write_str(&self.0) 280 } 281 } 282 283 impl AsRef<str> for TargetFingerprint { 284 fn as_ref(&self) -> &str { 285 self.as_str() 286 } 287 } 288 289 impl FromStr for TargetFingerprint { 290 type Err = TransportError; 291 292 fn from_str(value: &str) -> Result<Self, Self::Err> { 293 Self::parse(value) 294 } 295 } 296 297 impl TryFrom<&str> for TargetFingerprint { 298 type Error = TransportError; 299 300 fn try_from(value: &str) -> Result<Self, Self::Error> { 301 Self::parse(value) 302 } 303 } 304 305 #[cfg(feature = "serde")] 306 impl<'de> serde::Deserialize<'de> for TargetFingerprint { 307 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 308 where 309 D: serde::Deserializer<'de>, 310 { 311 let raw = <String as serde::Deserialize>::deserialize(deserializer)?; 312 let parsed = Self::parse(raw.as_str()).map_err(serde::de::Error::custom)?; 313 if parsed.as_str() != raw { 314 return Err(serde::de::Error::custom( 315 "transport target fingerprint is not canonical", 316 )); 317 } 318 Ok(parsed) 319 } 320 } 321 322 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 323 #[derive(Clone, Debug, PartialEq, Eq)] 324 /// Validated transport-neutral target. 325 pub struct Target { 326 kind: TransportId, 327 uri: EndpointUri, 328 scope: Option<TargetScope>, 329 label: Option<TargetLabel>, 330 fingerprint: TargetFingerprint, 331 #[cfg_attr( 332 feature = "serde", 333 serde(skip_serializing_if = "private_device_cleartext_is_false") 334 )] 335 private_device_cleartext: bool, 336 } 337 338 impl Target { 339 pub fn new(kind: TransportId, uri: impl AsRef<str>) -> Result<Self, TransportError> { 340 Self::new_with_metadata(kind, uri, None, None) 341 } 342 343 pub fn nostr_relay(uri: impl AsRef<str>) -> Result<Self, TransportError> { 344 Self::nostr_relay_with_metadata(uri, None, None) 345 } 346 347 /// Creates a Nostr relay target under an explicit construction policy. 348 /// 349 /// The private-device policy accepts only literal RFC1918 IPv4 or ULA IPv6 350 /// destinations. Named, public, loopback, link-local, unspecified, and 351 /// multicast destinations remain denied. 352 pub fn nostr_relay_with_policy( 353 uri: impl AsRef<str>, 354 policy: TargetNetworkPolicy, 355 ) -> Result<Self, TransportError> { 356 Self::new_with_metadata_and_nostr_policy(uri, None, None, policy) 357 } 358 359 pub fn nostr_relay_with_metadata( 360 uri: impl AsRef<str>, 361 scope: Option<TargetScope>, 362 label: Option<TargetLabel>, 363 ) -> Result<Self, TransportError> { 364 Self::new_with_metadata_and_nostr_policy( 365 uri, 366 scope, 367 label, 368 TargetNetworkPolicy::TlsOrLoopback, 369 ) 370 } 371 372 pub fn local(uri: impl AsRef<str>) -> Result<Self, TransportError> { 373 Self::local_with_metadata(uri, None, None) 374 } 375 376 pub fn local_with_metadata( 377 uri: impl AsRef<str>, 378 scope: Option<TargetScope>, 379 label: Option<TargetLabel>, 380 ) -> Result<Self, TransportError> { 381 Self::new_with_metadata(TransportId::LOCAL, uri, scope, label) 382 } 383 384 pub fn new_with_metadata( 385 kind: TransportId, 386 uri: impl AsRef<str>, 387 scope: Option<TargetScope>, 388 label: Option<TargetLabel>, 389 ) -> Result<Self, TransportError> { 390 if kind == TransportId::NOSTR { 391 return Self::new_with_metadata_and_nostr_policy( 392 uri, 393 scope, 394 label, 395 TargetNetworkPolicy::TlsOrLoopback, 396 ); 397 } 398 let uri = EndpointUri::parse(uri)?; 399 let fingerprint = TargetFingerprint::from_target(&kind, &uri, scope.as_ref()); 400 Ok(Self { 401 kind, 402 uri, 403 scope, 404 label, 405 fingerprint, 406 private_device_cleartext: false, 407 }) 408 } 409 410 fn new_with_metadata_and_nostr_policy( 411 uri: impl AsRef<str>, 412 scope: Option<TargetScope>, 413 label: Option<TargetLabel>, 414 policy: TargetNetworkPolicy, 415 ) -> Result<Self, TransportError> { 416 let uri = EndpointUri::parse_nostr_relay(uri, policy)?; 417 let fingerprint = TargetFingerprint::from_target(&TransportId::NOSTR, &uri, scope.as_ref()); 418 Ok(Self { 419 kind: TransportId::NOSTR, 420 private_device_cleartext: policy == TargetNetworkPolicy::PrivateDevice 421 && uri.as_str().starts_with("ws://"), 422 uri, 423 scope, 424 label, 425 fingerprint, 426 }) 427 } 428 429 pub fn kind(&self) -> &TransportId { 430 &self.kind 431 } 432 433 pub fn uri(&self) -> &EndpointUri { 434 &self.uri 435 } 436 437 pub fn scope(&self) -> Option<&TargetScope> { 438 self.scope.as_ref() 439 } 440 441 pub fn label(&self) -> Option<&TargetLabel> { 442 self.label.as_ref() 443 } 444 445 pub fn fingerprint(&self) -> &TargetFingerprint { 446 &self.fingerprint 447 } 448 } 449 450 #[cfg(feature = "serde")] 451 #[derive(serde::Deserialize)] 452 #[serde(deny_unknown_fields)] 453 struct TargetWire { 454 kind: TransportId, 455 uri: String, 456 scope: Option<String>, 457 label: Option<String>, 458 fingerprint: String, 459 private_device_cleartext: Option<bool>, 460 } 461 462 #[cfg(feature = "serde")] 463 impl<'de> serde::Deserialize<'de> for Target { 464 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 465 where 466 D: serde::Deserializer<'de>, 467 { 468 let wire = TargetWire::deserialize(deserializer)?; 469 let scope = wire 470 .scope 471 .map(TargetScope::parse) 472 .transpose() 473 .map_err(serde::de::Error::custom)?; 474 let label = wire 475 .label 476 .map(|label| { 477 let parsed = TargetLabel::parse(label.as_str())?; 478 if parsed.as_str() != label { 479 return Err(TransportError::InvalidTargetLabel); 480 } 481 Ok(parsed) 482 }) 483 .transpose() 484 .map_err(serde::de::Error::custom)?; 485 let fingerprint = TargetFingerprint::parse(wire.fingerprint.as_str()) 486 .map_err(serde::de::Error::custom)?; 487 if fingerprint.as_str() != wire.fingerprint { 488 return Err(serde::de::Error::custom( 489 "transport target fingerprint is not canonical", 490 )); 491 } 492 if wire.private_device_cleartext == Some(false) { 493 return Err(serde::de::Error::custom( 494 "transport target private-device marker is not canonical", 495 )); 496 } 497 let target = 498 if wire.kind == TransportId::NOSTR && wire.private_device_cleartext == Some(true) { 499 Self::new_with_metadata_and_nostr_policy( 500 wire.uri.as_str(), 501 scope.clone(), 502 label.clone(), 503 TargetNetworkPolicy::PrivateDevice, 504 ) 505 } else { 506 Self::new_with_metadata(wire.kind, wire.uri.as_str(), scope.clone(), label.clone()) 507 } 508 .map_err(serde::de::Error::custom)?; 509 if target.uri.as_str() != wire.uri 510 || target.scope != scope 511 || target.label != label 512 || target.fingerprint != fingerprint 513 { 514 return Err(serde::de::Error::custom( 515 "transport target identity does not match its canonical fields", 516 )); 517 } 518 Ok(target) 519 } 520 } 521 522 #[cfg_attr(feature = "serde", derive(serde::Serialize))] 523 #[derive(Clone, Debug, PartialEq, Eq)] 524 /// Bounded non-empty set of targets with unique fingerprints. 525 pub struct TargetSet { 526 targets: Vec<Target>, 527 } 528 529 impl TargetSet { 530 pub fn new(targets: Vec<Target>) -> Result<Self, TransportError> { 531 if targets.is_empty() { 532 return Err(TransportError::EmptyTargetSet); 533 } 534 if targets.len() > TARGET_SET_MAX_ITEMS { 535 return Err(TransportError::TargetSetTooLarge); 536 } 537 let mut fingerprints = BTreeSet::new(); 538 for target in &targets { 539 if !fingerprints.insert(target.fingerprint.as_str().to_string()) { 540 return Err(TransportError::DuplicateTargetFingerprint); 541 } 542 } 543 Ok(Self { targets }) 544 } 545 546 pub fn targets(&self) -> &[Target] { 547 &self.targets 548 } 549 550 /// Returns whether this set contains the exact target fingerprint. 551 pub fn contains(&self, fingerprint: &TargetFingerprint) -> bool { 552 self.targets 553 .iter() 554 .any(|target| target.fingerprint() == fingerprint) 555 } 556 557 pub fn len(&self) -> usize { 558 self.targets.len() 559 } 560 561 pub fn is_empty(&self) -> bool { 562 self.targets.is_empty() 563 } 564 } 565 566 #[cfg(feature = "serde")] 567 #[derive(serde::Deserialize)] 568 #[serde(deny_unknown_fields)] 569 struct TargetSetWire { 570 targets: Vec<Target>, 571 } 572 573 #[cfg(feature = "serde")] 574 impl<'de> serde::Deserialize<'de> for TargetSet { 575 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> 576 where 577 D: serde::Deserializer<'de>, 578 { 579 let wire = TargetSetWire::deserialize(deserializer)?; 580 Self::new(wire.targets).map_err(serde::de::Error::custom) 581 } 582 } 583 584 fn canonicalize_uri(raw: &str) -> Result<String, TransportError> { 585 let trimmed = raw.trim(); 586 if trimmed.is_empty() { 587 return Err(TransportError::EmptyTargetUri); 588 } 589 if raw != trimmed { 590 return Err(TransportError::InvalidTargetUri); 591 } 592 if trimmed.len() > ENDPOINT_URI_MAX_BYTES { 593 return Err(TransportError::InvalidTargetUri); 594 } 595 if trimmed 596 .chars() 597 .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) 598 { 599 return Err(TransportError::InvalidTargetUri); 600 } 601 if let Some(colon) = trimmed.find(':') { 602 let scheme = &trimmed[..colon]; 603 if !is_valid_scheme(scheme) { 604 return Err(TransportError::InvalidTargetUri); 605 } 606 let rest = &trimmed[colon + 1..]; 607 if let Some(authority_rest) = rest.strip_prefix("//") { 608 let authority_end = authority_rest 609 .find(['/', '?', '#']) 610 .unwrap_or(authority_rest.len()); 611 let authority = &authority_rest[..authority_end]; 612 let suffix = &authority_rest[authority_end..]; 613 return Ok(format!( 614 "{}://{}{}", 615 scheme.to_ascii_lowercase(), 616 authority.to_ascii_lowercase(), 617 suffix 618 )); 619 } 620 return Ok(format!("{}:{rest}", scheme.to_ascii_lowercase())); 621 } 622 Ok(trimmed.to_string()) 623 } 624 625 fn is_valid_scheme(value: &str) -> bool { 626 let mut chars = value.chars(); 627 matches!(chars.next(), Some(first) if first.is_ascii_alphabetic()) 628 && chars.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '+' | '-' | '.')) 629 } 630 631 fn canonicalize_nostr_relay_uri( 632 raw: &str, 633 policy: TargetNetworkPolicy, 634 ) -> Result<String, TransportError> { 635 let trimmed = raw.trim(); 636 if trimmed.is_empty() { 637 return Err(TransportError::EmptyTargetUri); 638 } 639 if raw != trimmed { 640 return Err(TransportError::InvalidTargetUri); 641 } 642 if trimmed.len() > ENDPOINT_URI_MAX_BYTES { 643 return Err(TransportError::InvalidTargetUri); 644 } 645 if trimmed 646 .chars() 647 .any(|ch| ch.is_ascii_control() || ch.is_ascii_whitespace()) 648 { 649 return Err(TransportError::InvalidTargetUri); 650 } 651 if trimmed.contains('?') || trimmed.contains('#') || trimmed.contains('\\') { 652 return Err(TransportError::InvalidTargetUri); 653 } 654 let Some(scheme_end) = trimmed.find("://") else { 655 return Err(TransportError::InvalidTargetUri); 656 }; 657 let scheme = trimmed[..scheme_end].to_ascii_lowercase(); 658 if !matches!(scheme.as_str(), "wss" | "ws") { 659 return Err(TransportError::InvalidTargetUri); 660 } 661 let endpoint = &trimmed[scheme_end + 3..]; 662 let authority_end = endpoint.find('/').unwrap_or(endpoint.len()); 663 let authority = &endpoint[..authority_end]; 664 let path = &endpoint[authority_end..]; 665 let authority = canonicalize_nostr_relay_authority(authority, scheme.as_str(), policy)?; 666 validate_nostr_relay_path(path)?; 667 if path == "/" { 668 return Ok(format!("{scheme}://{authority}")); 669 } 670 Ok(format!("{scheme}://{authority}{path}")) 671 } 672 673 fn canonicalize_nostr_relay_authority( 674 authority: &str, 675 scheme: &str, 676 policy: TargetNetworkPolicy, 677 ) -> Result<String, TransportError> { 678 if authority.is_empty() || authority.contains('@') { 679 return Err(TransportError::InvalidTargetUri); 680 } 681 let (host, port) = if let Some(rest) = authority.strip_prefix('[') { 682 let Some(host_end) = rest.find(']') else { 683 return Err(TransportError::InvalidTargetUri); 684 }; 685 let host = &rest[..host_end]; 686 let suffix = &rest[host_end + 1..]; 687 if host.is_empty() 688 || host 689 .chars() 690 .any(|ch| matches!(ch, '[' | ']' | '/' | '?' | '#' | '@' | '\\')) 691 { 692 return Err(TransportError::InvalidTargetUri); 693 } 694 let canonical_host = canonicalize_nostr_relay_ipv6(host)?; 695 ( 696 format!("[{canonical_host}]"), 697 parse_nostr_relay_port(suffix)?, 698 ) 699 } else { 700 if authority.contains(['[', ']', '\\']) { 701 return Err(TransportError::InvalidTargetUri); 702 } 703 let mut parts = authority.splitn(2, ':'); 704 let host = parts.next().unwrap_or_default(); 705 let port = parts 706 .next() 707 .map(parse_nostr_relay_port_with_prefix) 708 .transpose()?; 709 (canonicalize_nostr_relay_host(host)?, port) 710 }; 711 match policy { 712 TargetNetworkPolicy::TlsOrLoopback => { 713 if scheme == "ws" && !is_local_ws_relay_host(host.as_str()) { 714 return Err(TransportError::InvalidTargetUri); 715 } 716 } 717 TargetNetworkPolicy::PrivateDevice => { 718 if !is_private_device_relay_host(host.as_str()) { 719 return Err(TransportError::InvalidTargetUri); 720 } 721 } 722 } 723 let port = 724 port.filter(|port| !matches!((scheme, port.as_str()), ("wss", "443") | ("ws", "80"))); 725 Ok(match port { 726 Some(port) => format!("{host}:{port}"), 727 None => host, 728 }) 729 } 730 731 fn parse_nostr_relay_port(suffix: &str) -> Result<Option<String>, TransportError> { 732 if suffix.is_empty() { 733 return Ok(None); 734 } 735 let Some(port) = suffix.strip_prefix(':') else { 736 return Err(TransportError::InvalidTargetUri); 737 }; 738 parse_nostr_relay_port_with_prefix(port).map(Some) 739 } 740 741 fn parse_nostr_relay_port_with_prefix(port: &str) -> Result<String, TransportError> { 742 if port.is_empty() 743 || !port.bytes().all(|byte| byte.is_ascii_digit()) 744 || port.len() > 1 && port.starts_with('0') 745 { 746 return Err(TransportError::InvalidTargetUri); 747 } 748 let value = port 749 .parse::<u32>() 750 .map_err(|_| TransportError::InvalidTargetUri)?; 751 if !(1..=u16::MAX as u32).contains(&value) { 752 return Err(TransportError::InvalidTargetUri); 753 } 754 Ok(port.to_string()) 755 } 756 757 fn canonicalize_nostr_relay_host(host: &str) -> Result<String, TransportError> { 758 let canonical = host.to_ascii_lowercase(); 759 if canonical_ipv4(canonical.as_str()) || canonical_dns_host(canonical.as_str()) { 760 return Ok(canonical); 761 } 762 Err(TransportError::InvalidTargetUri) 763 } 764 765 fn canonicalize_nostr_relay_ipv6(host: &str) -> Result<String, TransportError> { 766 if host.is_empty() 767 || !host 768 .bytes() 769 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f' | b'A'..=b'F' | b':')) 770 { 771 return Err(TransportError::InvalidTargetUri); 772 } 773 host.parse::<Ipv6Addr>() 774 .map(|address| address.to_string()) 775 .map_err(|_| TransportError::InvalidTargetUri) 776 } 777 778 fn canonical_ipv4(value: &str) -> bool { 779 let mut count = 0usize; 780 for part in value.split('.') { 781 if part.is_empty() 782 || !part.bytes().all(|byte| byte.is_ascii_digit()) 783 || part.len() > 1 && part.starts_with('0') 784 || part.parse::<u8>().is_err() 785 { 786 return false; 787 } 788 count += 1; 789 } 790 count == 4 791 } 792 793 fn canonical_dns_host(value: &str) -> bool { 794 if value.is_empty() || value.len() > 253 { 795 return false; 796 } 797 let mut final_label = ""; 798 for label in value.split('.') { 799 if label.is_empty() 800 || label.len() > 63 801 || label.starts_with("xn--") 802 || !label 803 .bytes() 804 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') 805 || !label 806 .as_bytes() 807 .first() 808 .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()) 809 || !label 810 .as_bytes() 811 .last() 812 .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit()) 813 { 814 return false; 815 } 816 final_label = label; 817 } 818 !dns_label_is_whatwg_number(final_label) 819 } 820 821 fn dns_label_is_whatwg_number(value: &str) -> bool { 822 value.bytes().all(|byte| byte.is_ascii_digit()) 823 || value 824 .strip_prefix("0x") 825 .is_some_and(|digits| digits.bytes().all(|byte| byte.is_ascii_hexdigit())) 826 } 827 828 fn validate_nostr_relay_path(path: &str) -> Result<(), TransportError> { 829 if path.is_empty() || path == "/" { 830 return Ok(()); 831 } 832 let Some(component) = path.strip_prefix('/') else { 833 return Err(TransportError::InvalidTargetUri); 834 }; 835 if relay_path_component_is_valid(component) { 836 Ok(()) 837 } else { 838 Err(TransportError::InvalidTargetUri) 839 } 840 } 841 842 fn relay_path_component_is_valid(value: &str) -> bool { 843 if value.split('/').any(relay_path_segment_is_dot) { 844 return false; 845 } 846 let bytes = value.as_bytes(); 847 let mut index = 0usize; 848 while index < bytes.len() { 849 if bytes[index] == b'%' { 850 if index + 2 >= bytes.len() 851 || !upper_hex_digit(bytes[index + 1]) 852 || !upper_hex_digit(bytes[index + 2]) 853 { 854 return false; 855 } 856 index += 3; 857 continue; 858 } 859 if !relay_path_character(bytes[index]) { 860 return false; 861 } 862 index += 1; 863 } 864 true 865 } 866 867 fn relay_path_segment_is_dot(segment: &str) -> bool { 868 let bytes = segment.as_bytes(); 869 let mut index = 0usize; 870 let mut dots = 0usize; 871 while index < bytes.len() { 872 if bytes[index] == b'.' { 873 dots += 1; 874 index += 1; 875 } else if bytes[index..].starts_with(b"%2E") { 876 dots += 1; 877 index += 3; 878 } else { 879 return false; 880 } 881 } 882 matches!(dots, 1 | 2) 883 } 884 885 fn relay_path_character(byte: u8) -> bool { 886 byte.is_ascii_alphanumeric() 887 || matches!( 888 byte, 889 b'-' | b'.' 890 | b'_' 891 | b'~' 892 | b'!' 893 | b'$' 894 | b'&' 895 | b'\'' 896 | b'(' 897 | b')' 898 | b'*' 899 | b'+' 900 | b',' 901 | b';' 902 | b'=' 903 | b':' 904 | b'@' 905 | b'/' 906 ) 907 } 908 909 fn upper_hex_digit(byte: u8) -> bool { 910 byte.is_ascii_digit() || matches!(byte, b'A'..=b'F') 911 } 912 913 fn is_local_ws_relay_host(host: &str) -> bool { 914 matches!(host, "localhost" | "127.0.0.1" | "[::1]") 915 } 916 917 fn is_private_device_relay_host(host: &str) -> bool { 918 match host.trim_matches(['[', ']']).parse::<IpAddr>() { 919 Ok(IpAddr::V4(address)) => address.is_private(), 920 Ok(IpAddr::V6(address)) => address.segments()[0] & 0xfe00 == 0xfc00, 921 Err(_) => false, 922 } 923 } 924 925 #[cfg(feature = "serde")] 926 const fn private_device_cleartext_is_false(value: &bool) -> bool { 927 !*value 928 }