Skip to content
This repository was archived by the owner on Oct 23, 2022. It is now read-only.

Implement /dns and /resolve #353

Merged
merged 9 commits into from
Sep 8, 2020
12 changes: 10 additions & 2 deletions conformance/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,16 @@ const factory = createFactory(options)
// Phase 1.0-ish
//
tests.miscellaneous(factory, { skip: [
'dns',
'resolve',
// the cidBase param is not implemented yet
'should resolve an IPFS hash and return a base64url encoded CID in path',
// different Cid, the /path/to/testfile.txt suffix shouldn't be there
'should resolve an IPFS path link',
// different Cid, missing "/path/to" in the middle
'should resolve up to the last node across multiple nodes',
// expected "true", got "false"
'should resolve an IPNS DNS link',
// HTTP: not implemented
'should resolve IPNS link recursively',
// these cause a hang 20% of time:
'should respect timeout option when getting the node id',
'should respect timeout option when getting the node version',
Expand Down
6 changes: 3 additions & 3 deletions examples/ipfs_ipns_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ async fn main() {
.unwrap();

// Resolve a Block
let new_ipfs_path = ipfs.resolve_ipns(&ipns_path).await.unwrap();
let new_ipfs_path = ipfs.resolve_ipns(&ipns_path, false).await.unwrap();
assert_eq!(ipfs_path, new_ipfs_path);

// Resolve dnslink
let ipfs_path = IpfsPath::from_str("/ipns/ipfs.io").unwrap();
println!("Resolving {:?}", ipfs_path.to_string());
let ipfs_path = ipfs.resolve_ipns(&ipfs_path).await.unwrap();
let ipfs_path = ipfs.resolve_ipns(&ipfs_path, false).await.unwrap();
println!("Resolved stage 1: {:?}", ipfs_path.to_string());
let ipfs_path = ipfs.resolve_ipns(&ipfs_path).await.unwrap();
let ipfs_path = ipfs.resolve_ipns(&ipfs_path, false).await.unwrap();
println!("Resolved stage 2: {:?}", ipfs_path.to_string());

ipfs.exit_daemon().await;
Expand Down
3 changes: 3 additions & 0 deletions http/src/v0.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod block;
pub mod dag;
pub mod dht;
pub mod id;
pub mod ipns;
pub mod pin;
pub mod pubsub;
pub mod refs;
Expand Down Expand Up @@ -87,9 +88,11 @@ pub fn routes<T: IpfsTypes>(
and_boxed!(warp::path!("id"), id::identity(ipfs)),
and_boxed!(warp::path!("add"), root_files::add(ipfs)),
and_boxed!(warp::path!("cat"), root_files::cat(ipfs)),
and_boxed!(warp::path!("dns"), ipns::dns(ipfs)),
and_boxed!(warp::path!("get"), root_files::get(ipfs)),
and_boxed!(warp::path!("refs" / "local"), refs::local(ipfs)),
and_boxed!(warp::path!("refs"), refs::refs(ipfs)),
and_boxed!(warp::path!("resolve"), ipns::resolve(ipfs)),
warp::path!("version")
.and(query::<version::Query>())
.and_then(version::version),
Expand Down
90 changes: 90 additions & 0 deletions http/src/v0/ipns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::v0::support::{with_ipfs, StringError, StringSerialized};
use ipfs::{Ipfs, IpfsPath, IpfsTypes};
use serde::{Deserialize, Serialize};
use warp::{query, Filter, Rejection, Reply};

#[derive(Debug, Deserialize)]
pub struct ResolveQuery {
// the name to resolve
arg: StringSerialized<IpfsPath>,
#[serde(rename = "dht-record-count")]
dht_record_count: Option<usize>,
#[serde(rename = "dht-timeout")]
dht_timeout: Option<String>,
}

pub fn resolve<T: IpfsTypes>(
ipfs: &Ipfs<T>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
with_ipfs(ipfs)
.and(query::<ResolveQuery>())
.and_then(resolve_query)
}

async fn resolve_query<T: IpfsTypes>(
ipfs: Ipfs<T>,
query: ResolveQuery,
) -> Result<impl Reply, Rejection> {
let ResolveQuery { arg, .. } = query;
let name = arg.into_inner();
let path = ipfs
.resolve_ipns(&name, false)
.await
.map_err(StringError::from)?
.to_string();

let response = ResolveResponse { path };

Ok(warp::reply::json(&response))
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
struct ResolveResponse {
path: String,
}

#[derive(Debug, Deserialize)]
pub struct DnsQuery {
// the name to resolve
arg: String,
recursive: Option<bool>,
}

pub fn dns<T: IpfsTypes>(
ipfs: &Ipfs<T>,
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone {
with_ipfs(ipfs).and(query::<DnsQuery>()).and_then(dns_query)
}

async fn dns_query<T: IpfsTypes>(ipfs: Ipfs<T>, query: DnsQuery) -> Result<impl Reply, Rejection> {
let DnsQuery { arg, recursive } = query;
// attempt to parse the argument prepended with "/ipns/" if it fails to parse like a compliant
// IpfsPath and there is no leading slash
let path = if !arg.starts_with('/') {
if let Ok(parsed) = arg.parse() {
Ok(parsed)
} else {
format!("/ipns/{}", arg).parse()
}
} else {
arg.parse()
}
.map_err(StringError::from)?;

let path = ipfs
.resolve_ipns(&path, recursive.unwrap_or(false))
.await
.map_err(StringError::from)?
.to_string();

let response = DnsResponse { path };

Ok(warp::reply::json(&response))
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "PascalCase")]
struct DnsResponse {
path: String,
}
40 changes: 29 additions & 11 deletions src/dag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ pub enum ResolveError {
/// Path attempted to resolve through a property, index or link which did not exist.
#[error("no link named {:?} under {0}", .1.iter().last().unwrap())]
NotFound(Cid, SlashedPath),

/// Tried to use a path neiter containing nor resolving to a Cid.
#[error("the path neiter contains nor resolves to a Cid")]
NoCid(IpfsPath),

/// Couldn't resolve a path via IPNS.
#[error("can't resolve an IPNS path")]
IpnsResolutionFailed(IpfsPath),
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -178,19 +186,24 @@ impl<Types: RepoTypes> IpldDag<Types> {
///
/// Returns the resolved node as `Ipld`.
pub async fn get(&self, path: IpfsPath) -> Result<Ipld, ResolveError> {
// FIXME: do ipns resolve first
let cid = match path.root().cid() {
let resolved_path = self
.ipfs
.resolve_ipns(&path, true)
.await
.map_err(|_| ResolveError::IpnsResolutionFailed(path))?;

let cid = match resolved_path.root().cid() {
Some(cid) => cid,
None => panic!("Ipns resolution not implemented; expected a Cid-based path"),
None => return Err(ResolveError::NoCid(resolved_path)),
};

let mut iter = path.iter().peekable();
let mut iter = resolved_path.iter().peekable();

let (node, _) = match self.resolve0(cid, &mut iter, true).await {
Ok(t) => t,
Err(e) => {
drop(iter);
return Err(e.with_path(path));
return Err(e.with_path(resolved_path));
}
};

Expand All @@ -213,27 +226,32 @@ impl<Types: RepoTypes> IpldDag<Types> {
path: IpfsPath,
follow_links: bool,
) -> Result<(ResolvedNode, SlashedPath), ResolveError> {
// FIXME: do ipns resolve first
let cid = match path.root().cid() {
let resolved_path = self
.ipfs
.resolve_ipns(&path, true)
.await
.map_err(|_| ResolveError::IpnsResolutionFailed(path))?;

let cid = match resolved_path.root().cid() {
Some(cid) => cid,
None => panic!("Ipns resolution not implemented; expected a Cid-based path"),
None => return Err(ResolveError::NoCid(resolved_path)),
};

let (node, matched_segments) = {
let mut iter = path.iter().peekable();
let mut iter = resolved_path.iter().peekable();
match self.resolve0(cid, &mut iter, follow_links).await {
Ok(t) => t,
Err(e) => {
drop(iter);
return Err(e.with_path(path));
return Err(e.with_path(resolved_path));
}
}
};

// we only care about returning this remaining_path with segments up until the last
// document but it can and should contain all of the following segments (if any). there
// could be more segments when `!follow_links`.
let remaining_path = path.into_shifted(matched_segments);
let remaining_path = resolved_path.into_shifted(matched_segments);

Ok((node, remaining_path))
}
Expand Down
24 changes: 16 additions & 8 deletions src/ipns/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,23 @@ use domain_resolv::{stub::Answer, StubResolver};
use futures::future::{select_ok, SelectOk};
use futures::pin_mut;
use std::future::Future;
use std::io;
use std::pin::Pin;
use std::str::FromStr;
use std::task::{Context, Poll};
use thiserror::Error;
use std::{fmt, io};

#[derive(Debug, Error)]
#[error("no dnslink entry")]
pub struct DnsLinkError;
#[derive(Debug)]
pub struct DnsLinkError(String);

type FutureAnswer = Pin<Box<dyn Future<Output = Result<Answer, io::Error>>>>;
Copy link
Member Author

@ljedrz ljedrz Sep 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this one gave me a bit of a headache - the compiler was pointing to a very long issue with the HTTP warp call, but this was actually the root cause of the problem

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I wonder if the future should be just written open as async fn to be simpler in every way, but haven't tried this myself ... for a while at least. Though, there's nothing obvious causing this, perhaps the path inside domain-resolve is just so long.

impl fmt::Display for DnsLinkError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "DNS error: {}", self.0)
}
}

impl std::error::Error for DnsLinkError {}

type FutureAnswer = Pin<Box<dyn Future<Output = Result<Answer, io::Error>> + Send>>;

pub struct DnsLinkFuture {
query: SelectOk<FutureAnswer>,
Expand Down Expand Up @@ -46,11 +52,13 @@ impl Future for DnsLinkFuture {
if !rest.is_empty() {
_self.query = select_ok(rest);
} else {
return Poll::Ready(Err(DnsLinkError.into()));
return Poll::Ready(Err(
DnsLinkError("no DNS records found".to_owned()).into()
));
}
}
Poll::Pending => return Poll::Pending,
Poll::Ready(Err(_)) => return Poll::Ready(Err(DnsLinkError.into())),
Poll::Ready(Err(e)) => return Poll::Ready(Err(DnsLinkError(e.to_string()).into())),
}
}
}
Expand Down
25 changes: 20 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,11 +446,26 @@ impl<Types: IpfsTypes> Ipfs<Types> {
}

/// Resolves a ipns path to an ipld path.
pub async fn resolve_ipns(&self, path: &IpfsPath) -> Result<IpfsPath, Error> {
self.ipns()
.resolve(path)
.instrument(self.span.clone())
.await
pub async fn resolve_ipns(&self, path: &IpfsPath, recursive: bool) -> Result<IpfsPath, Error> {
let ipns = self.ipns();
let mut resolved = ipns.resolve(path).await;

if recursive {
let mut previous = None;
while let Ok(ref res) = resolved {
if let Some(ref prev) = previous {
if prev == res {
break;
}
}
previous = Some(res.clone());
resolved = ipns.resolve(&res).await;
}

resolved
} else {
resolved
}
}

/// Publishes an ipld path.
Expand Down