Skip to main content

rust_ipfs/ipns/
mod.rs

1//! IPNS functionality around [`Ipfs`].
2
3use futures_timeout::TimeoutExt;
4use std::borrow::Borrow;
5
6use crate::path::{IpfsPath, PathRoot};
7use crate::repo::DataStore;
8use crate::Ipfs;
9
10#[cfg(feature = "dns")]
11mod dnslink;
12
13#[cfg(feature = "dns")]
14use connexa::prelude::transport::dns::DnsResolver;
15
16/// IPNS facade around [`Ipns`].
17#[derive(Clone, Debug)]
18pub struct Ipns {
19    ipfs: Ipfs,
20    #[cfg(feature = "dns")]
21    resolver: DnsResolver,
22}
23
24#[derive(Clone, Copy, Debug, Default)]
25pub enum IpnsOption {
26    Local,
27    #[default]
28    DHT,
29}
30
31impl Ipns {
32    pub fn new(ipfs: Ipfs) -> Self {
33        Ipns {
34            ipfs,
35            #[cfg(feature = "dns")]
36            resolver: DnsResolver::default(),
37        }
38    }
39
40    /// Set dns resolver
41    #[cfg(feature = "dns")]
42    pub fn set_resolver(&mut self, resolver: DnsResolver) {
43        self.resolver = resolver;
44    }
45
46    /// Resolves a ipns path to an ipld path.
47    // TODO: Implement ipns pubsub
48    // TODO: Maybe implement a check to the dht store itself too?
49    pub async fn resolve(&self, path: impl Borrow<IpfsPath>) -> Result<IpfsPath, IpnsError> {
50        let path = path.borrow();
51        match path.root() {
52            PathRoot::Ipld(_) => Ok(path.clone()),
53            PathRoot::Ipns(peer) => {
54                use std::str::FromStr;
55                use std::time::Duration;
56
57                use connexa::prelude::PeerId;
58                use futures::StreamExt;
59                use ipld_core::cid::Cid;
60                use multihash::Multihash;
61
62                let mut path_iter = path.iter();
63
64                let hash = Multihash::from_bytes(&peer.to_bytes()).map_err(anyhow::Error::from)?;
65
66                let cid = Cid::new_v1(0x72, hash);
67
68                let mb = format!(
69                    "/ipns/{}",
70                    cid.to_string_of_base(multibase::Base::Base36Lower)
71                        .map_err(anyhow::Error::from)?
72                );
73
74                //TODO: Determine if we want to encode the cid of the multihash in base32 or if we can just use the peer id instead
75                // let mb = format!("/ipns/{}", peer);
76
77                let repo = self.ipfs.repo();
78                let datastore = repo.data_store();
79
80                if let Ok(Some(data)) = datastore.get(mb.as_bytes()).await {
81                    if let Ok(path) = rust_ipns::Record::decode(data).and_then(|record| {
82                        //Although stored locally, we should verify the record anyway
83                        record.verify(*peer)?;
84                        let data = record.data()?;
85                        let path = String::from_utf8_lossy(data.value());
86                        IpfsPath::from_str(&path)
87                            .and_then(|mut internal_path| {
88                                internal_path.path.push_split(path_iter.by_ref()).map_err(
89                                    |_| crate::path::IpfsPathError::InvalidPath(path.to_string()),
90                                )?;
91                                Ok(internal_path)
92                            })
93                            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
94                    }) {
95                        return Ok(path);
96                    }
97                }
98
99                let stream = self.ipfs.dht_get(mb).await?;
100
101                //TODO: Implement configurable timeout
102                let mut records = stream
103                    .filter_map(|record| async move {
104                        let key = &record.key.as_ref()[6..];
105                        let record = rust_ipns::Record::decode(&record.value).ok()?;
106                        let peer_id = PeerId::from_bytes(key).ok()?;
107                        record.verify(peer_id).ok()?;
108                        Some(record)
109                    })
110                    .collect::<Vec<_>>()
111                    .timeout(Duration::from_secs(60 * 2))
112                    .await
113                    .unwrap_or_default();
114
115                if records.is_empty() {
116                    return Err(anyhow::anyhow!("No records found").into());
117                }
118
119                records.sort_by_key(|record| record.sequence());
120
121                let record = records.last().ok_or(anyhow::anyhow!("No records found"))?;
122
123                let data = record.data()?;
124
125                let path = String::from_utf8_lossy(data.value()).to_string();
126
127                IpfsPath::from_str(&path)
128                    .map_err(IpnsError::from)
129                    .and_then(|mut internal_path| {
130                        internal_path
131                            .path
132                            .push_split(path_iter)
133                            .map_err(|_| crate::path::IpfsPathError::InvalidPath(path))?;
134                        Ok(internal_path)
135                    })
136            }
137            #[cfg(feature = "dns")]
138            PathRoot::Dns(domain) => {
139                let path_iter = path.iter();
140                dnslink::resolve(self.resolver, domain, path_iter)
141                    .await
142                    .map_err(IpnsError::from)
143            }
144        }
145    }
146
147    pub async fn publish(
148        &self,
149        key: Option<&str>,
150        path: impl Borrow<IpfsPath>,
151        option: IpnsOption,
152    ) -> Result<IpfsPath, IpnsError> {
153        use connexa::prelude::dht::Quorum;
154        use ipld_core::cid::Cid;
155        use multihash::Multihash;
156        use std::str::FromStr;
157
158        let path = path.borrow();
159
160        let keypair = match key {
161            Some(key) => self.ipfs.keystore().get_keypair(key).await?,
162            None => self.ipfs.keypair().clone(),
163        };
164
165        let peer_id = keypair.public().to_peer_id();
166
167        let hash = Multihash::from_bytes(&peer_id.to_bytes()).map_err(anyhow::Error::from)?;
168
169        let cid = Cid::new_v1(0x72, hash);
170
171        let mb = format!(
172            "/ipns/{}",
173            cid.to_string_of_base(multibase::Base::Base36Lower)
174                .map_err(anyhow::Error::from)?
175        );
176
177        let repo = self.ipfs.repo();
178
179        let datastore = repo.data_store();
180
181        let record_data = datastore.get(mb.as_bytes()).await.unwrap_or_default();
182
183        let mut seq = 0;
184
185        if let Some(record) = record_data.as_ref() {
186            let record = rust_ipns::Record::decode(record)?;
187            //Although stored locally, we should verify the record anyway
188            record.verify(peer_id)?;
189
190            let data = record.data()?;
191
192            let ipfs_path = IpfsPath::from_str(&String::from_utf8_lossy(data.value()))?;
193
194            if ipfs_path.eq(path) {
195                return IpfsPath::from_str(&mb).map_err(IpnsError::from);
196            }
197
198            // inc req of the record
199            seq = record.sequence() + 1;
200        }
201
202        let path_bytes = path.to_string();
203
204        let record = rust_ipns::Record::new(
205            &keypair,
206            path_bytes.as_bytes(),
207            chrono::Duration::try_hours(48).expect("shouldnt panic"),
208            seq,
209            60000,
210        )?;
211
212        let bytes = record.encode()?;
213
214        datastore.put(mb.as_bytes(), &bytes).await?;
215
216        match option {
217            IpnsOption::DHT => self.ipfs.dht_put(&mb, bytes, Quorum::One).await?,
218            IpnsOption::Local => {}
219        };
220
221        IpfsPath::from_str(&mb).map_err(IpnsError::from)
222    }
223}
224
225#[non_exhaustive]
226#[derive(thiserror::Error, Debug)]
227pub enum IpnsError {
228    #[error(transparent)]
229    IpfsPath(#[from] crate::path::IpfsPathError),
230    #[error(transparent)]
231    Io(#[from] std::io::Error),
232    #[error(transparent)]
233    Any(#[from] anyhow::Error),
234}