1use std::{
10 collections::{BTreeMap, HashMap, HashSet},
11 fmt,
12 future::Future,
13 pin::Pin,
14 str::{FromStr, Split},
15 sync::{Arc, Mutex},
16 time::{SystemTime, UNIX_EPOCH},
17};
18
19use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
20use iroh::{PublicKey, SecretKey};
21use iroh_blobs::{Hash, ticket::BlobTicket};
22use serde::{Deserialize, Serialize};
23
24use super::IndexingDatabase;
25use crate::{
26 error::{Result, SyncwebError},
27 gossip::SignedGossipMessage,
28 indexing::ProviderLease,
29};
30
31const LINK_SIGNATURE_CONTEXT: &[u8] = b"syncweb/name-pointer/v1\0";
32const LINK_SCHEME: &str = "syncweb://";
33
34#[must_use]
36pub fn current_epoch_seconds() -> u64 {
37 SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .map_or(0, |duration| duration.as_secs())
40}
41
42#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
44#[non_exhaustive]
45pub struct ContentLink {
46 pub hash: Hash,
48}
49
50impl ContentLink {
51 #[must_use]
53 pub const fn new(hash: Hash) -> Self {
54 Self { hash }
55 }
56
57 #[must_use]
59 pub const fn hash(self) -> Hash {
60 self.hash
61 }
62
63 #[must_use]
65 pub const fn create(hash: Hash) -> Self {
66 Self::new(hash)
67 }
68
69 pub fn parse(value: &str) -> Result<Self> {
75 value.parse()
76 }
77
78 #[must_use]
80 pub fn uri(self) -> String {
81 self.to_string()
82 }
83
84 pub const fn validate(self) -> Result<()> {
91 Ok(())
92 }
93}
94
95impl fmt::Display for ContentLink {
96 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(formatter, "{LINK_SCHEME}content/{}", self.hash)
98 }
99}
100
101impl FromStr for ContentLink {
102 type Err = SyncwebError;
103
104 fn from_str(value: &str) -> Result<Self> {
105 let hash_text = value
106 .strip_prefix("syncweb://content/")
107 .ok_or_else(|| SyncwebError::InvalidConfig("content link must use syncweb://content/".to_owned()))?;
108 if hash_text.is_empty() || hash_text.contains('/') || hash_text.contains('?') || hash_text.contains('#') {
109 return Err(SyncwebError::InvalidConfig(
110 "content link has an invalid hash".to_owned(),
111 ));
112 }
113 let hash = hash_text
114 .parse::<Hash>()
115 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid content link hash: {error}")))?;
116 Ok(Self::new(hash))
117 }
118}
119
120pub type ImmutableLink = ContentLink;
122
123#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
125#[non_exhaustive]
126pub struct NameLink {
127 pub publisher: PublicKey,
129 pub alias: String,
131}
132
133impl NameLink {
134 pub fn new(publisher: PublicKey, alias: impl Into<String>) -> Result<Self> {
141 let link = Self {
142 publisher,
143 alias: alias.into(),
144 };
145 link.validate()?;
146 Ok(link)
147 }
148
149 pub fn create(publisher: PublicKey, alias: impl Into<String>) -> Result<Self> {
155 Self::new(publisher, alias)
156 }
157
158 pub fn parse(value: &str) -> Result<Self> {
164 value.parse()
165 }
166
167 #[must_use]
169 pub fn uri(&self) -> String {
170 self.to_string()
171 }
172
173 pub fn validate(&self) -> Result<()> {
179 validate_path_segment(&self.alias, "mutable link alias")
180 }
181}
182
183impl fmt::Display for NameLink {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 write!(formatter, "{LINK_SCHEME}name/{}/{}", self.publisher, self.alias)
186 }
187}
188
189impl FromStr for NameLink {
190 type Err = SyncwebError;
191
192 fn from_str(value: &str) -> Result<Self> {
193 let remainder = value
194 .strip_prefix("syncweb://name/")
195 .ok_or_else(|| SyncwebError::InvalidConfig("name link must use syncweb://name/".to_owned()))?;
196 let mut parts = remainder.split('/');
197 let publisher_text = next_part(&mut parts, "name link publisher")?;
198 let alias = next_part(&mut parts, "name link alias")?;
199 if parts.next().is_some() {
200 return Err(SyncwebError::InvalidConfig(
201 "name link has too many path segments".to_owned(),
202 ));
203 }
204 let publisher = publisher_text
205 .parse::<PublicKey>()
206 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid name link publisher: {error}")))?;
207 Self::new(publisher, alias)
208 }
209}
210
211pub type MutableLink = NameLink;
213
214#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
220#[non_exhaustive]
221pub struct MutablePointer {
222 pub publisher: PublicKey,
224 pub alias: String,
226 pub sequence: u64,
228 pub manifest: Hash,
230 #[serde(default)]
232 pub version: Option<String>,
233 #[serde(default)]
235 pub providers: Vec<ProviderLease>,
236 #[serde(default)]
238 pub signature: Option<String>,
239}
240
241impl MutablePointer {
242 pub fn new(publisher: PublicKey, alias: impl Into<String>, manifest: Hash, sequence: u64) -> Result<Self> {
250 Self::new_with_providers(publisher, alias, manifest, sequence, Vec::new())
251 }
252
253 pub fn new_with_providers(
259 publisher: PublicKey,
260 alias: impl Into<String>,
261 manifest: Hash,
262 sequence: u64,
263 providers: Vec<ProviderLease>,
264 ) -> Result<Self> {
265 let pointer = Self {
266 publisher,
267 alias: alias.into(),
268 sequence,
269 manifest,
270 version: None,
271 providers,
272 signature: None,
273 };
274 pointer.validate()?;
275 Ok(pointer)
276 }
277
278 pub fn signed(
285 publisher: PublicKey,
286 alias: impl Into<String>,
287 manifest: Hash,
288 sequence: u64,
289 signing_key: &SigningKey,
290 ) -> Result<Self> {
291 let mut pointer = Self::new(publisher, alias, manifest, sequence)?;
292 pointer.sign(signing_key)?;
293 Ok(pointer)
294 }
295
296 pub fn signed_with_secret_key(
303 publisher: PublicKey,
304 alias: impl Into<String>,
305 manifest: Hash,
306 sequence: u64,
307 secret_key: &SecretKey,
308 ) -> Result<Self> {
309 let signing_key = SigningKey::from_bytes(&secret_key.to_bytes());
310 Self::signed(publisher, alias, manifest, sequence, &signing_key)
311 }
312
313 #[must_use]
315 pub fn with_version(mut self, version: impl Into<String>) -> Self {
316 self.version = Some(version.into());
317 self.signature = None;
318 self
319 }
320
321 #[must_use]
323 pub fn with_provider(mut self, provider: ProviderLease) -> Self {
324 self.providers.push(provider);
325 self.signature = None;
326 self
327 }
328
329 #[must_use]
331 pub fn with_providers(mut self, providers: impl IntoIterator<Item = ProviderLease>) -> Self {
332 self.providers.extend(providers);
333 self.signature = None;
334 self
335 }
336
337 pub fn link(&self) -> Result<NameLink> {
343 NameLink::new(self.publisher, self.alias.clone())
344 }
345
346 #[must_use]
348 pub const fn manifest_hash(&self) -> Hash {
349 self.manifest
350 }
351
352 pub fn unsigned_bytes(&self) -> Result<Vec<u8>> {
358 let mut unsigned = self.clone();
359 unsigned.signature = None;
360 unsigned.validate()?;
361 let mut bytes = LINK_SIGNATURE_CONTEXT.to_vec();
362 let encoded = serde_json::to_vec(&unsigned)
363 .map_err(|error| SyncwebError::operation("failed to serialize mutable link pointer", error))?;
364 bytes.extend_from_slice(&encoded);
365 Ok(bytes)
366 }
367
368 pub fn sign(&mut self, signing_key: &SigningKey) -> Result<()> {
375 if signing_key.verifying_key().to_bytes() != *self.publisher.as_bytes() {
376 return Err(SyncwebError::InvalidIdentity(
377 "mutable link signer does not match publisher".to_owned(),
378 ));
379 }
380 self.validate()?;
381 self.signature = Some(hex::encode(signing_key.sign(&self.unsigned_bytes()?).to_bytes()));
382 Ok(())
383 }
384
385 pub fn verify_signature(&self) -> Result<()> {
392 self.validate()?;
393 let signature_text = self
394 .signature
395 .as_deref()
396 .ok_or_else(|| SyncwebError::InvalidConfig("mutable link pointer must be signed".to_owned()))?;
397 let signature_bytes = hex::decode(signature_text)
398 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid mutable link signature: {error}")))?;
399 let signature = Signature::from_slice(&signature_bytes)
400 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid mutable link signature: {error}")))?;
401 let key = VerifyingKey::from_bytes(self.publisher.as_bytes())
402 .map_err(|error| SyncwebError::InvalidIdentity(format!("invalid mutable link publisher: {error}")))?;
403 key.verify(&self.unsigned_bytes()?, &signature)
404 .map_err(|error| SyncwebError::InvalidConfig(format!("mutable link signature is invalid: {error}")))?;
405 for provider in &self.providers {
406 provider.verify_signature()?;
407 }
408 Ok(())
409 }
410
411 pub fn validate(&self) -> Result<()> {
417 validate_path_segment(&self.alias, "mutable link alias")?;
418 if self.sequence == 0 {
419 return Err(SyncwebError::InvalidConfig(
420 "mutable link sequence must be greater than zero".to_owned(),
421 ));
422 }
423 if let Some(version) = &self.version
424 && version.trim().is_empty()
425 {
426 return Err(SyncwebError::InvalidConfig(
427 "mutable link version cannot be empty".to_owned(),
428 ));
429 }
430 for provider in &self.providers {
431 provider.validate()?;
432 if provider.hash != self.manifest {
433 return Err(SyncwebError::InvalidTicket(
434 "mutable link provider lease does not match its manifest".to_owned(),
435 ));
436 }
437 }
438 Ok(())
439 }
440
441 pub fn to_bytes(&self) -> Result<Vec<u8>> {
448 self.verify_signature()?;
449 serde_json::to_vec(self)
450 .map_err(|error| SyncwebError::operation("failed to serialize mutable link pointer", error))
451 }
452
453 pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Result<Self> {
459 let pointer: Self = serde_json::from_slice(bytes.as_ref())
460 .map_err(|error| SyncwebError::operation("failed to deserialize mutable link pointer", error))?;
461 pointer.verify_signature()?;
462 Ok(pointer)
463 }
464}
465
466pub type SignedMutablePointer = MutablePointer;
468
469#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
471#[non_exhaustive]
472pub struct PrivateLink {
473 pub manifest: Hash,
475 pub capability: [u8; 32],
477 pub expires_at: u64,
479}
480
481impl PrivateLink {
482 pub fn generate(manifest: Hash, expires_at: u64) -> Result<Self> {
489 let capability = rand::random::<[u8; 32]>();
490 Self::new(manifest, capability, expires_at)
491 }
492
493 pub fn create(manifest: Hash, expires_at: u64) -> Result<Self> {
499 Self::generate(manifest, expires_at)
500 }
501
502 pub fn parse(value: &str) -> Result<Self> {
509 value.parse()
510 }
511
512 pub fn new(manifest: Hash, capability: [u8; 32], expires_at: u64) -> Result<Self> {
518 let link = Self {
519 manifest,
520 capability,
521 expires_at,
522 };
523 link.validate()?;
524 Ok(link)
525 }
526
527 #[must_use]
529 pub fn uri(&self) -> String {
530 self.to_string()
531 }
532
533 pub fn validate(&self) -> Result<()> {
539 if self.expires_at == 0 {
540 return Err(SyncwebError::InvalidConfig(
541 "private link expiration must be greater than zero".to_owned(),
542 ));
543 }
544 Ok(())
545 }
546
547 #[must_use]
549 pub const fn is_expired_at(&self, now: u64) -> bool {
550 self.expires_at <= now
551 }
552
553 #[must_use]
555 pub fn revocation_key(&self) -> (Hash, Vec<u8>) {
556 (self.manifest, self.capability.to_vec())
557 }
558}
559
560impl fmt::Display for PrivateLink {
561 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
562 write!(
563 formatter,
564 "{LINK_SCHEME}private/{}/{}?expires={}",
565 self.manifest,
566 hex::encode(self.capability),
567 self.expires_at
568 )
569 }
570}
571
572impl FromStr for PrivateLink {
573 type Err = SyncwebError;
574
575 fn from_str(value: &str) -> Result<Self> {
576 let (path, query) = value
577 .strip_prefix("syncweb://private/")
578 .ok_or_else(|| SyncwebError::InvalidConfig("private link must use syncweb://private/".to_owned()))?
579 .split_once('?')
580 .ok_or_else(|| SyncwebError::InvalidConfig("private link must contain an expiration".to_owned()))?;
581 let mut parts = path.split('/');
582 let manifest = next_part(&mut parts, "private link manifest")?;
583 let capability_hex = next_part(&mut parts, "private link capability")?;
584 if parts.next().is_some() {
585 return Err(SyncwebError::InvalidConfig(
586 "private link has too many path segments".to_owned(),
587 ));
588 }
589 let manifest_hash = manifest
590 .parse::<Hash>()
591 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link manifest: {error}")))?;
592 let capability_bytes = hex::decode(capability_hex)
593 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link capability: {error}")))?;
594 let capability: [u8; 32] = capability_bytes.try_into().map_err(|bytes: Vec<u8>| {
595 SyncwebError::InvalidConfig(format!("private link capability must be 32 bytes, got {}", bytes.len()))
596 })?;
597 let expires_text = query
598 .strip_prefix("expires=")
599 .ok_or_else(|| SyncwebError::InvalidConfig("private link query must contain expires".to_owned()))?;
600 let expires_at = expires_text
601 .parse::<u64>()
602 .map_err(|error| SyncwebError::InvalidConfig(format!("invalid private link expiration: {error}")))?;
603 Self::new(manifest_hash, capability, expires_at)
604 }
605}
606
607pub type CapabilityLink = PrivateLink;
609
610#[derive(Clone, Debug, Eq, PartialEq)]
612#[non_exhaustive]
613pub struct Mirror {
614 pub hash: Hash,
616 pub ticket: BlobTicket,
618}
619
620impl Mirror {
621 pub fn new(ticket: BlobTicket) -> Result<Self> {
627 let mirror = Self {
628 hash: ticket.hash(),
629 ticket,
630 };
631 mirror.validate()?;
632 Ok(mirror)
633 }
634
635 pub fn validate(&self) -> Result<()> {
641 if self.ticket.hash() != self.hash {
642 return Err(SyncwebError::InvalidTicket(
643 "mirror ticket does not match its content hash".to_owned(),
644 ));
645 }
646 Ok(())
647 }
648}
649
650#[derive(Clone, Debug, Eq, PartialEq)]
652#[non_exhaustive]
653pub struct LinkResolution {
654 pub manifest: Hash,
656 pub version: Option<String>,
658 pub sequence: Option<u64>,
660 pub providers: Vec<ProviderLease>,
662 pub tickets: Vec<BlobTicket>,
664}
665
666impl LinkResolution {
667 #[must_use]
669 pub const fn new(manifest: Hash) -> Self {
670 Self {
671 manifest,
672 version: None,
673 sequence: None,
674 providers: Vec::new(),
675 tickets: Vec::new(),
676 }
677 }
678
679 #[must_use]
681 pub const fn hash(&self) -> Hash {
682 self.manifest
683 }
684
685 #[must_use]
687 pub const fn manifest_hash(&self) -> Hash {
688 self.manifest
689 }
690
691 #[must_use]
693 pub fn provider_tickets(&self) -> &[BlobTicket] {
694 &self.tickets
695 }
696
697 pub fn fetch_with<T, Fetch>(&self, mut fetch: Fetch) -> Result<T>
703 where
704 Fetch: FnMut(&BlobTicket) -> Result<T>,
705 {
706 fetch_from_mirrors(&self.tickets, &mut fetch)
707 }
708}
709
710pub type ResolvedLink = LinkResolution;
712
713#[derive(Clone, Debug, Eq, PartialEq, Hash)]
715#[non_exhaustive]
716pub enum Link {
717 Content(ContentLink),
719 Name(NameLink),
721 Private(PrivateLink),
723}
724
725pub type SyncwebLink = Link;
727
728impl Link {
729 pub fn parse(value: &str) -> Result<Self> {
735 value.parse()
736 }
737
738 #[must_use]
740 pub fn uri(&self) -> String {
741 self.to_string()
742 }
743}
744
745impl fmt::Display for Link {
746 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
747 match self {
748 Self::Content(link) => link.fmt(formatter),
749 Self::Name(link) => link.fmt(formatter),
750 Self::Private(link) => link.fmt(formatter),
751 }
752 }
753}
754
755impl FromStr for Link {
756 type Err = SyncwebError;
757
758 fn from_str(value: &str) -> Result<Self> {
759 if value.starts_with("syncweb://content/") {
760 return Ok(Self::Content(value.parse()?));
761 }
762 if value.starts_with("syncweb://name/") {
763 return Ok(Self::Name(value.parse()?));
764 }
765 if value.starts_with("syncweb://private/") {
766 return Ok(Self::Private(value.parse()?));
767 }
768 Err(SyncwebError::InvalidConfig(
769 "unsupported syncweb link; expected content, name, or private".to_owned(),
770 ))
771 }
772}
773
774#[derive(Clone, Debug, Default, Eq, PartialEq)]
776#[non_exhaustive]
777pub struct ResolveOptions {
778 pub version: Option<String>,
780 pub now: Option<u64>,
782}
783
784impl ResolveOptions {
785 #[must_use]
787 pub fn version(version: impl Into<String>) -> Self {
788 Self {
789 version: Some(version.into()),
790 now: None,
791 }
792 }
793
794 #[must_use]
796 pub const fn at(now: u64) -> Self {
797 Self {
798 version: None,
799 now: Some(now),
800 }
801 }
802
803 #[must_use]
805 pub fn with_version(mut self, version: impl Into<String>) -> Self {
806 self.version = Some(version.into());
807 self
808 }
809}
810
811#[derive(Clone, Debug, Default)]
812struct ResolverState {
813 mirrors: HashMap<Hash, Vec<Mirror>>,
814 pointers: HashMap<(PublicKey, String), PointerHistory>,
815 revoked: HashSet<(Hash, Vec<u8>)>,
816}
817
818#[derive(Clone, Debug, Default)]
819struct PointerHistory {
820 current: Option<MutablePointer>,
821 versions: BTreeMap<String, MutablePointer>,
822}
823
824pub type ProviderFetch =
826 Arc<dyn Fn(NameLink) -> Pin<Box<dyn Future<Output = Result<LinkResolution>> + Send>> + Send + Sync>;
827
828#[derive(Clone)]
830pub struct LinkResolver {
831 state: Arc<Mutex<ResolverState>>,
832 database: Option<IndexingDatabase>,
833 provider_fetch: Option<ProviderFetch>,
834}
835
836impl fmt::Debug for LinkResolver {
837 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
838 f.debug_struct("LinkResolver")
839 .field("state", &self.state)
840 .field("database", &self.database)
841 .finish_non_exhaustive()
842 }
843}
844
845impl Default for LinkResolver {
846 fn default() -> Self {
847 Self {
848 state: Arc::new(Mutex::new(ResolverState::default())),
849 database: None,
850 provider_fetch: None,
851 }
852 }
853}
854
855impl LinkResolver {
856 #[must_use]
858 pub fn new() -> Self {
859 Self::default()
860 }
861
862 #[must_use]
867 pub fn with_provider_fetch(fetch: ProviderFetch) -> Self {
868 Self {
869 state: Arc::new(Mutex::new(ResolverState::default())),
870 database: None,
871 provider_fetch: Some(fetch),
872 }
873 }
874
875 pub async fn resolve_remote(&self, link: &Link) -> Result<Option<LinkResolution>> {
885 match link {
886 Link::Name(name) => {
887 if let Some(ref fetch) = self.provider_fetch {
888 fetch(name.clone()).await.map(Some)
889 } else {
890 Ok(None)
891 }
892 }
893 Link::Content(_) | Link::Private(_) => Ok(None),
894 }
895 }
896
897 pub fn with_database(database: IndexingDatabase) -> Result<Self> {
906 let result = database.load_links()?;
907 let mut state = ResolverState::default();
908 for pointer in result.0 {
909 let key = (pointer.publisher, pointer.alias.clone());
910 let history = state.pointers.entry(key).or_default();
911 if let Some(current) = &history.current
912 && pointer.sequence <= current.sequence
913 {
914 continue;
915 }
916 if let Some(version) = &pointer.version {
917 history.versions.insert(version.clone(), pointer.clone());
918 }
919 history.current = Some(pointer);
920 }
921 for ticket_str in result.1 {
922 if let Ok(ticket) = ticket_str.parse::<iroh_blobs::ticket::BlobTicket>()
923 && let Ok(mirror) = Mirror::new(ticket)
924 {
925 let mirrors = state.mirrors.entry(mirror.hash).or_default();
926 if !mirrors.iter().any(|existing| existing.ticket == mirror.ticket) {
927 mirrors.push(mirror);
928 }
929 }
930 }
931 for link in result.2 {
932 state.revoked.insert(link.revocation_key());
933 }
934 Ok(Self {
935 state: Arc::new(Mutex::new(state)),
936 database: Some(database),
937 provider_fetch: None,
938 })
939 }
940
941 pub fn register_mirror(&self, ticket: BlobTicket) -> Result<()> {
947 let mirror = Mirror::new(ticket)?;
948 let mut state = self.lock_state()?;
949 let mirrors = state.mirrors.entry(mirror.hash).or_default();
950 if !mirrors.iter().any(|existing| existing.ticket == mirror.ticket) {
951 mirrors.push(mirror.clone());
952 if let Some(ref database) = self.database {
953 let _ = database.upsert_link_mirror(&mirror.ticket.to_string());
954 }
955 }
956 drop(state);
957 Ok(())
958 }
959
960 pub fn add_mirror(&self, ticket: BlobTicket) -> Result<()> {
966 self.register_mirror(ticket)
967 }
968
969 pub fn register_provider_ticket(&self, ticket: BlobTicket) -> Result<()> {
975 self.register_mirror(ticket)
976 }
977
978 pub fn register_provider_lease(&self, lease: ProviderLease) -> Result<()> {
984 let now = current_epoch_seconds();
985 let validated_lease = checked_lease(lease, now)?;
986 let ticket = lease_ticket(&validated_lease)?;
987 let mut state = self.lock_state()?;
988 let mirrors = state.mirrors.entry(validated_lease.hash).or_default();
989 if !mirrors.iter().any(|existing| existing.ticket == ticket) {
990 mirrors.push(Mirror {
991 hash: validated_lease.hash,
992 ticket,
993 });
994 }
995 drop(state);
996 Ok(())
997 }
998
999 pub fn mirrors(&self, hash: Hash) -> Result<Vec<BlobTicket>> {
1005 let state = self.lock_state()?;
1006 Ok(state
1007 .mirrors
1008 .get(&hash)
1009 .map(|mirrors| mirrors.iter().map(|mirror| mirror.ticket.clone()).collect())
1010 .unwrap_or_default())
1011 }
1012
1013 pub fn publish(&self, pointer: &MutablePointer) -> Result<()> {
1020 pointer.verify_signature()?;
1021 let key = (pointer.publisher, pointer.alias.clone());
1022 let mut state = self.lock_state()?;
1023 let history = state.pointers.entry(key).or_default();
1024 if let Some(current) = &history.current
1025 && pointer.sequence <= current.sequence
1026 {
1027 return Err(SyncwebError::InvalidConfig(format!(
1028 "mutable link sequence {} must be greater than {}",
1029 pointer.sequence, current.sequence
1030 )));
1031 }
1032 if let Some(version) = &pointer.version {
1033 if history.versions.contains_key(version) {
1034 return Err(SyncwebError::InvalidConfig(format!(
1035 "mutable link version {version:?} is already published"
1036 )));
1037 }
1038 history.versions.insert(version.clone(), pointer.clone());
1039 }
1040 history.current = Some(pointer.clone());
1041 if let Some(ref database) = self.database {
1042 let _ = database.upsert_link_pointer(pointer);
1043 }
1044 drop(state);
1045 Ok(())
1046 }
1047
1048 pub fn register_pointer(&self, pointer: &MutablePointer) -> Result<()> {
1054 self.publish(pointer)
1055 }
1056
1057 pub fn update(&self, pointer: &MutablePointer) -> Result<()> {
1063 self.publish(pointer)
1064 }
1065
1066 pub fn current_pointer(&self, link: &NameLink) -> Result<Option<MutablePointer>> {
1073 link.validate()?;
1074 let state = self.lock_state()?;
1075 Ok(state
1076 .pointers
1077 .get(&(link.publisher, link.alias.clone()))
1078 .and_then(|history| history.current.clone()))
1079 }
1080
1081 pub fn revoke(&self, link: &PrivateLink) -> Result<()> {
1088 link.validate()?;
1089 self.lock_state()?.revoked.insert(link.revocation_key());
1090 Ok(())
1091 }
1092
1093 pub fn revoke_private(&self, link: &PrivateLink) -> Result<()> {
1100 self.revoke(link)
1101 }
1102
1103 pub fn is_revoked(&self, link: &PrivateLink) -> Result<bool> {
1110 link.validate()?;
1111 Ok(self.lock_state()?.revoked.contains(&link.revocation_key()))
1112 }
1113
1114 pub fn resolve(&self, link: &Link) -> Result<LinkResolution> {
1121 self.resolve_with_options(link, &ResolveOptions::default())
1122 }
1123
1124 pub fn resolve_at(&self, link: &Link, now: u64) -> Result<LinkResolution> {
1131 self.resolve_with_options(link, &ResolveOptions::at(now))
1132 }
1133
1134 pub fn resolve_version(&self, link: &NameLink, version: &str) -> Result<LinkResolution> {
1140 self.resolve_with_options(&Link::Name(link.clone()), &ResolveOptions::version(version))
1141 }
1142
1143 pub fn resolve_with_options(&self, link: &Link, options: &ResolveOptions) -> Result<LinkResolution> {
1150 let now = options.now.unwrap_or_else(current_epoch_seconds);
1151 let state = self.lock_state()?;
1152 let resolution = match link {
1153 Link::Content(content) => {
1154 content.validate()?;
1155 resolution_for_hash(content.hash, None, None, Vec::new(), &state, now)
1156 }
1157 Link::Private(private) => {
1158 private.validate()?;
1159 if private.is_expired_at(now) {
1160 return Err(SyncwebError::InvalidConfig("private link has expired".to_owned()));
1161 }
1162 if state.revoked.contains(&private.revocation_key()) {
1163 return Err(SyncwebError::InvalidConfig("private link has been revoked".to_owned()));
1164 }
1165 resolution_for_hash(private.manifest, None, None, Vec::new(), &state, now)
1166 }
1167 Link::Name(name) => {
1168 name.validate()?;
1169 let history = state
1170 .pointers
1171 .get(&(name.publisher, name.alias.clone()))
1172 .ok_or_else(|| SyncwebError::InvalidConfig("mutable link has no published pointer".to_owned()))?;
1173 let pointer = if let Some(version) = &options.version {
1174 history.versions.get(version).ok_or_else(|| {
1175 SyncwebError::InvalidConfig(format!("mutable link version {version:?} not found"))
1176 })?
1177 } else {
1178 history
1179 .current
1180 .as_ref()
1181 .ok_or_else(|| SyncwebError::InvalidConfig("mutable link has no current pointer".to_owned()))?
1182 };
1183 resolution_for_hash(
1184 pointer.manifest,
1185 pointer.version.clone(),
1186 Some(pointer.sequence),
1187 pointer.providers.clone(),
1188 &state,
1189 now,
1190 )
1191 }
1192 };
1193 drop(state);
1194 resolution
1195 }
1196
1197 pub fn resolve_uri(&self, uri: &str) -> Result<LinkResolution> {
1204 let link = uri.parse::<Link>()?;
1205 self.resolve(&link)
1206 }
1207
1208 pub fn fetch_with_mirrors<T, Fetch>(&self, link: &Link, fetch: Fetch) -> Result<T>
1215 where
1216 Fetch: FnMut(&BlobTicket) -> Result<T>,
1217 {
1218 self.resolve(link)?.fetch_with(fetch)
1219 }
1220
1221 fn lock_state(&self) -> Result<std::sync::MutexGuard<'_, ResolverState>> {
1222 self.state
1223 .lock()
1224 .map_err(|error| SyncwebError::operation("link resolver state lock poisoned", error))
1225 }
1226}
1227
1228pub fn fetch_from_mirrors<T, Fetch>(tickets: &[BlobTicket], fetch: &mut Fetch) -> Result<T>
1234where
1235 Fetch: FnMut(&BlobTicket) -> Result<T>,
1236{
1237 if tickets.is_empty() {
1238 return Err(SyncwebError::InvalidConfig("link has no provider tickets".to_owned()));
1239 }
1240 let mut failures = Vec::new();
1241 for ticket in tickets {
1242 match fetch(ticket) {
1243 Ok(value) => return Ok(value),
1244 Err(error) => failures.push(error.to_string()),
1245 }
1246 }
1247 Err(SyncwebError::InvalidConfig(format!(
1248 "all mirror providers failed: {}",
1249 failures.join("; ")
1250 )))
1251}
1252
1253fn resolution_for_hash(
1254 manifest: Hash,
1255 version: Option<String>,
1256 sequence: Option<u64>,
1257 providers: Vec<ProviderLease>,
1258 state: &ResolverState,
1259 now: u64,
1260) -> Result<LinkResolution> {
1261 let mut tickets = Vec::new();
1262 let mut active_providers = Vec::new();
1263 for provider in providers {
1264 if provider.expires_at <= now {
1265 continue;
1266 }
1267 provider.verify_at(now)?;
1268 let ticket = lease_ticket(&provider)?;
1269 if !tickets.contains(&ticket) {
1270 tickets.push(ticket);
1271 }
1272 active_providers.push(provider);
1273 }
1274 if let Some(mirrors) = state.mirrors.get(&manifest) {
1275 for mirror in mirrors {
1276 if !tickets.contains(&mirror.ticket) {
1277 tickets.push(mirror.ticket.clone());
1278 }
1279 }
1280 }
1281 Ok(LinkResolution {
1282 manifest,
1283 version,
1284 sequence,
1285 providers: active_providers,
1286 tickets,
1287 })
1288}
1289
1290fn lease_ticket(lease: &ProviderLease) -> Result<BlobTicket> {
1291 lease
1292 .ticket
1293 .parse::<BlobTicket>()
1294 .map_err(|error| SyncwebError::InvalidTicket(format!("invalid provider ticket: {error}")))
1295}
1296
1297fn checked_lease(lease: ProviderLease, now: u64) -> Result<ProviderLease> {
1298 lease.verify_at(now)?;
1299 Ok(lease)
1300}
1301
1302fn validate_path_segment(value: &str, field: &str) -> Result<()> {
1303 if value.is_empty() || value == "." || value == ".." {
1304 return Err(SyncwebError::InvalidConfig(format!(
1305 "{field} cannot be empty or dot-like"
1306 )));
1307 }
1308 if value
1309 .chars()
1310 .any(|character| character.is_control() || matches!(character, '/' | '?' | '#' | '%'))
1311 {
1312 return Err(SyncwebError::InvalidConfig(format!("{field} contains URI delimiters")));
1313 }
1314 Ok(())
1315}
1316
1317fn next_part<'a>(parts: &mut Split<'a, char>, field: &str) -> Result<&'a str> {
1318 let part = parts
1319 .next()
1320 .ok_or_else(|| SyncwebError::InvalidConfig(format!("{field} is missing")))?;
1321 if part.is_empty() {
1322 return Err(SyncwebError::InvalidConfig(format!("{field} is empty")));
1323 }
1324 Ok(part)
1325}
1326
1327impl SignedGossipMessage for PrivateLink {
1328 fn verify_signature(&self) -> Result<()> {
1329 Ok(())
1330 }
1331}
1332
1333pub const REVOCATION_GOSSIP_TOPIC: &[u8] = b"syncweb/link-revocations/v1";
1335
1336#[must_use]
1338pub fn revocation_topic_id() -> iroh_gossip::TopicId {
1339 iroh_gossip::TopicId::from_bytes(*blake3::hash(REVOCATION_GOSSIP_TOPIC).as_bytes())
1340}
1341
1342#[cfg(test)]
1343mod tests {
1344 use super::*;
1345 use iroh::{EndpointAddr, SecretKey};
1346 use iroh_blobs::BlobFormat;
1347
1348 fn ticket(seed: u8, hash: Hash) -> BlobTicket {
1349 let secret = SecretKey::from_bytes(&[seed; 32]);
1350 BlobTicket::new(EndpointAddr::new(secret.public()), hash, BlobFormat::Raw)
1351 }
1352
1353 fn pointer(seed: u8, alias: &str, hash: Hash, sequence: u64, version: &str) -> Result<MutablePointer> {
1354 let secret = SecretKey::from_bytes(&[seed; 32]);
1355 let mut pointer = MutablePointer::new(secret.public(), alias, hash, sequence)?.with_version(version);
1356 pointer.sign(&SigningKey::from_bytes(&secret.to_bytes()))?;
1357 Ok(pointer)
1358 }
1359
1360 #[test]
1361 fn immutable_links_round_trip() {
1362 let hash = Hash::from_bytes([7; 32]);
1363 let link = ContentLink::new(hash);
1364 assert_eq!(link.to_string(), format!("syncweb://content/{hash}"));
1365 assert_eq!(
1366 link.to_string()
1367 .parse::<ContentLink>()
1368 .expect("content link should parse"),
1369 link
1370 );
1371 assert_eq!(
1372 link.to_string().parse::<Link>().expect("link should parse"),
1373 Link::Content(link)
1374 );
1375 }
1376
1377 #[test]
1378 fn signed_names_are_monotonic_and_version_pinnable() {
1379 let hash_one = Hash::from_bytes([1; 32]);
1380 let hash_two = Hash::from_bytes([2; 32]);
1381 let resolver = LinkResolver::new();
1382 let first = pointer(1, "dataset", hash_one, 1, "1.0.0").expect("pointer should build");
1383 let name = first.link().expect("pointer should have a link");
1384 resolver.publish(&first).expect("first pointer should publish");
1385 assert_eq!(
1386 resolver
1387 .resolve(&Link::Name(name.clone()))
1388 .expect("name should resolve")
1389 .manifest,
1390 hash_one
1391 );
1392 let pinned = resolver
1393 .resolve_version(&name, "1.0.0")
1394 .expect("version should resolve");
1395 assert_eq!(pinned.manifest, hash_one);
1396 let second = pointer(1, "dataset", hash_two, 2, "2.0.0").expect("pointer should build");
1397 resolver.publish(&second).expect("second pointer should publish");
1398 assert_eq!(
1399 resolver
1400 .resolve(&Link::Name(name.clone()))
1401 .expect("name should resolve")
1402 .manifest,
1403 hash_two
1404 );
1405 assert!(resolver.publish(&first).is_err());
1406 assert_eq!(
1407 resolver
1408 .resolve_version(&name, "1.0.0")
1409 .expect("version should resolve")
1410 .manifest,
1411 hash_one
1412 );
1413 }
1414
1415 #[test]
1416 fn mutable_resolution_includes_verified_provider_tickets() {
1417 let hash = Hash::from_bytes([8; 32]);
1418 let provider_secret = SecretKey::from_bytes(&[8; 32]);
1419 let provider_ticket = ticket(8, hash);
1420 let mut lease = ProviderLease::new_with_times(hash, provider_ticket.to_string(), 1, 0, u64::MAX)
1421 .expect("provider lease should build");
1422 lease
1423 .sign_with_secret_key(&provider_secret)
1424 .expect("provider lease should sign");
1425
1426 let publisher_secret = SecretKey::from_bytes(&[9; 32]);
1427 let mut pointer =
1428 MutablePointer::new_with_providers(publisher_secret.public(), "dataset", hash, 1, vec![lease])
1429 .expect("pointer should build")
1430 .with_version("1.0.0");
1431 pointer
1432 .sign(&SigningKey::from_bytes(&publisher_secret.to_bytes()))
1433 .expect("pointer should sign");
1434
1435 let resolver = LinkResolver::new();
1436 let name = pointer.link().expect("pointer should have a link");
1437 resolver.publish(&pointer).expect("pointer should publish");
1438 let resolution = resolver.resolve(&Link::Name(name)).expect("name should resolve");
1439 assert_eq!(resolution.manifest_hash(), hash);
1440 assert_eq!(resolution.providers.len(), 1);
1441 assert_eq!(resolution.provider_tickets(), &[provider_ticket]);
1442 }
1443
1444 #[test]
1445 fn private_links_expire_and_revoke() {
1446 let hash = Hash::from_bytes([3; 32]);
1447 let cap: [u8; 32] = [1_u8; 32];
1448 let link = PrivateLink::new(hash, cap, 20).expect("private link should build");
1449 let resolver = LinkResolver::new();
1450 assert_eq!(
1451 resolver
1452 .resolve_at(&Link::Private(link.clone()), 10)
1453 .expect("private link should resolve")
1454 .manifest,
1455 hash
1456 );
1457 resolver.revoke(&link).expect("private link should revoke");
1458 assert!(resolver.resolve_at(&Link::Private(link.clone()), 10).is_err());
1459 assert!(resolver.resolve_at(&Link::Private(link), 20).is_err());
1460 }
1461
1462 #[test]
1463 fn private_link_generates_random_capability() {
1464 let hash = Hash::from_bytes([4; 32]);
1465 let link = PrivateLink::generate(hash, 100).expect("should generate");
1466 assert_ne!(link.capability, [0_u8; 32], "capability should not be zeroed");
1467 assert_eq!(link.manifest, hash);
1468 assert_eq!(link.expires_at, 100);
1469 let link2 = PrivateLink::generate(hash, 100).expect("should generate");
1471 assert_ne!(link.capability, link2.capability, "capabilities should be random");
1472 }
1473
1474 #[test]
1475 fn private_link_uri_round_trip() {
1476 let hash = Hash::from_bytes([5; 32]);
1477 let cap: [u8; 32] = [
1478 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12,
1479 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
1480 ];
1481 let link = PrivateLink::new(hash, cap, 200).expect("should build");
1482 let uri = link.to_string();
1483 assert!(uri.starts_with("syncweb://private/"));
1484 assert!(uri.contains("?expires=200"));
1485 let parsed: PrivateLink = uri.parse().expect("should parse");
1486 assert_eq!(parsed.manifest, hash);
1487 assert_eq!(parsed.capability, cap);
1488 assert_eq!(parsed.expires_at, 200);
1489 }
1490
1491 #[test]
1492 fn mirror_fallback_tries_alternates() {
1493 let hash = Hash::from_bytes([4; 32]);
1494 let resolver = LinkResolver::new();
1495 let first = ticket(4, hash);
1496 let second = ticket(5, hash);
1497 resolver
1498 .register_mirror(first.clone())
1499 .expect("first mirror should register");
1500 resolver.register_mirror(second).expect("second mirror should register");
1501 let link = Link::Content(ContentLink::new(hash));
1502 let value = resolver
1503 .fetch_with_mirrors(&link, |candidate| {
1504 if candidate == &first {
1505 Err(SyncwebError::InvalidConfig("first mirror unavailable".to_owned()))
1506 } else {
1507 Ok("second mirror")
1508 }
1509 })
1510 .expect("fallback should find a mirror");
1511 assert_eq!(value, "second mirror");
1512 }
1513}