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::Ipfs;
7use crate::path::{IpfsPath, PathRoot};
8use crate::repo::DataStore;
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                    && let Ok(path) = rust_ipns::Record::decode(data)
82                        .map_err(std::io::Error::from)
83                        .and_then(|record| {
84                            //Although stored locally, we should verify the record anyway
85                            record.verify(*peer)?;
86                            let data = record.data()?;
87                            let path = String::from_utf8_lossy(data.value());
88                            IpfsPath::from_str(&path)
89                                .and_then(|mut internal_path| {
90                                    internal_path.path.push_split(path_iter.by_ref()).map_err(
91                                        |_| {
92                                            crate::path::IpfsPathError::InvalidPath(
93                                                path.to_string(),
94                                            )
95                                        },
96                                    )?;
97                                    Ok(internal_path)
98                                })
99                                .map_err(|e| {
100                                    std::io::Error::new(std::io::ErrorKind::InvalidData, e)
101                                })
102                        })
103                {
104                    return Ok(path);
105                }
106
107                let stream = self.ipfs.dht_get(mb).await?;
108
109                //TODO: Implement configurable timeout
110                let records = stream
111                    .filter_map(|record| async move {
112                        let key = &record.key.as_ref()[6..];
113                        let record = rust_ipns::Record::decode(&record.value).ok()?;
114                        let peer_id = PeerId::from_bytes(key).ok()?;
115                        record.verify(peer_id).ok()?;
116                        Some(record)
117                    })
118                    .collect::<Vec<_>>()
119                    .timeout(Duration::from_secs(60 * 2))
120                    .await
121                    .unwrap_or_default();
122
123                let record = records
124                    .iter()
125                    .max_by(|a, b| a.compare(b).unwrap_or(std::cmp::Ordering::Equal))
126                    .ok_or_else(|| anyhow::anyhow!("No records found"))?;
127
128                let data = record.data()?;
129
130                let path = String::from_utf8_lossy(data.value()).to_string();
131
132                IpfsPath::from_str(&path)
133                    .map_err(IpnsError::from)
134                    .and_then(|mut internal_path| {
135                        internal_path
136                            .path
137                            .push_split(path_iter)
138                            .map_err(|_| crate::path::IpfsPathError::InvalidPath(path))?;
139                        Ok(internal_path)
140                    })
141            }
142            #[cfg(feature = "dns")]
143            PathRoot::Dns(domain) => {
144                let path_iter = path.iter();
145                dnslink::resolve(self.resolver, domain, path_iter)
146                    .await
147                    .map_err(IpnsError::from)
148            }
149        }
150    }
151
152    pub async fn publish(
153        &self,
154        key: Option<&str>,
155        path: impl Borrow<IpfsPath>,
156        option: IpnsOption,
157    ) -> Result<IpfsPath, IpnsError> {
158        use connexa::prelude::dht::Quorum;
159        use ipld_core::cid::Cid;
160        use multihash::Multihash;
161        use std::str::FromStr;
162
163        let path = path.borrow();
164
165        let keypair = match key {
166            Some(key) => self
167                .ipfs
168                .keychain()
169                .get(key)
170                .await
171                .map_err(anyhow::Error::from)?,
172            None => self.ipfs.keypair().clone(),
173        };
174
175        let peer_id = keypair.public().to_peer_id();
176
177        let hash = Multihash::from_bytes(&peer_id.to_bytes()).map_err(anyhow::Error::from)?;
178
179        let cid = Cid::new_v1(0x72, hash);
180
181        let mb = format!(
182            "/ipns/{}",
183            cid.to_string_of_base(multibase::Base::Base36Lower)
184                .map_err(anyhow::Error::from)?
185        );
186
187        let repo = self.ipfs.repo();
188
189        let datastore = repo.data_store();
190
191        let record_data = datastore.get(mb.as_bytes()).await.unwrap_or_default();
192
193        let mut seq = 0;
194
195        if let Some(record) = record_data.as_ref() {
196            let record = rust_ipns::Record::decode(record)?;
197            //Although stored locally, we should verify the record anyway
198            record.verify(peer_id)?;
199
200            let data = record.data()?;
201
202            let ipfs_path = IpfsPath::from_str(&String::from_utf8_lossy(data.value()))?;
203
204            if ipfs_path.eq(path) {
205                return IpfsPath::from_str(&mb).map_err(IpnsError::from);
206            }
207
208            // inc req of the record
209            seq = record.sequence() + 1;
210        }
211
212        let path_bytes = path.to_string();
213
214        let record = rust_ipns::Record::new(
215            &keypair,
216            path_bytes.as_bytes(),
217            chrono::Utc::now() + chrono::Duration::try_hours(48).expect("shouldnt panic"),
218            seq,
219            // IPNS TTL is a caching hint; 60s.
220            std::time::Duration::from_secs(60),
221        )?;
222
223        let bytes = record.encode()?;
224
225        datastore.put(mb.as_bytes(), &bytes).await?;
226
227        match option {
228            IpnsOption::DHT => self.ipfs.dht_put(&mb, bytes, Quorum::One).await?,
229            IpnsOption::Local => {}
230        };
231
232        IpfsPath::from_str(&mb).map_err(IpnsError::from)
233    }
234}
235
236#[non_exhaustive]
237#[derive(thiserror::Error, Debug)]
238pub enum IpnsError {
239    #[error(transparent)]
240    IpfsPath(#[from] crate::path::IpfsPathError),
241    #[error(transparent)]
242    Io(#[from] std::io::Error),
243    #[error(transparent)]
244    Ipns(#[from] rust_ipns::Error),
245    #[error(transparent)]
246    Any(#[from] anyhow::Error),
247}