Skip to content
This repository has been 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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ tracing = { default-features = false, features = ["log"], version = "0.1" }
tracing-futures = { default-features = false, features = ["std", "futures-03"], version = "0.2" }
void = { default-features = false, version = "1.0" }

[target.'cfg(windows)'.dependencies]
# required for DNS resolution
ipconfig = { default-features = false, version = "0.2" }

[build-dependencies]
prost-build = { default-features = false, version = "0.6" }

Expand Down
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
65 changes: 53 additions & 12 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,28 +52,63 @@ 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())),
}
}
}
}

#[cfg(not(target_os = "windows"))]
fn create_resolver() -> Result<StubResolver, Error> {
Ok(StubResolver::default())
}

#[cfg(target_os = "windows")]
fn create_resolver() -> Result<StubResolver, Error> {
use domain_resolv::stub::conf::ResolvConf;
use std::{collections::HashSet, io::Cursor};

let mut config = ResolvConf::new();
let mut name_servers = String::new();

let mut dns_servers = HashSet::new();
for adapter in ipconfig::get_adapters()? {
for dns in adapter.dns_servers() {
dns_servers.insert(dns.to_owned());
}
}

for dns in &dns_servers {
name_servers.push_str(&format!("nameserver {}\n", dns));
}

let mut name_servers = Cursor::new(name_servers.into_bytes());
config.parse(&mut name_servers)?;
config.finalize();

Ok(StubResolver::from_conf(config))
}

pub async fn resolve(domain: &str) -> Result<IpfsPath, Error> {
let mut dnslink = "_dnslink.".to_string();
dnslink.push_str(domain);
let resolver = create_resolver()?;

let qname = Dname::<Bytes>::from_str(&domain)?;
let question = Question::new_in(qname, Rtype::Txt);
let resolver = StubResolver::new();
let query1 = Box::pin(async move { resolver.query(question).await });
let resolver1 = resolver.clone();
let query1 = Box::pin(async move { resolver1.query(question).await });

let qname = Dname::<Bytes>::from_str(&dnslink)?;
let question = Question::new_in(qname, Rtype::Txt);
let resolver = StubResolver::new();
let query2 = Box::pin(async move { resolver.query(question).await });
let resolver2 = resolver;
let query2 = Box::pin(async move { resolver2.query(question).await });

Ok(DnsLinkFuture {
query: select_ok(vec![query1 as FutureAnswer, query2]),
Expand Down
Loading