rust_ipfs/ipns/
mod.rs

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