Skip to content

Commit

Permalink
namesys: Add recursive resolution
Browse files Browse the repository at this point in the history
This allows direct access to the earlier protocol-specific Resolve
implementations.  The guts of each protocol-specific resolver are in
the internal resolveOnce method, and we've added a new:

  ResolveN(ctx, name, depth)

method to the public interface.  There's also:

  Resolve(ctx, name)

which wraps ResolveN using DefaultDepthLimit.  The extra API endpoint
is intended to reduce the likelyhood of clients accidentally calling
the more dangerous ResolveN with a nonsensically high or infinite
depth.  On IRC on 2015-05-17, Juan said:

15:34 <jbenet> If 90% of uses is the reduced API with no chance to
  screw it up, that's a huge win.
15:34 <wking> Why would those 90% not just set depth=0 or depth=1,
  depending on which they need?
15:34 <jbenet> Because people will start writing `r.Resolve(ctx, name,
  d)` where d is a variable.
15:35 <wking> And then accidentally set that variable to some huge
  number?
15:35 <jbenet> Grom experience, i've seen this happen _dozens_ of
  times. people screw trivial things up.
15:35 <wking> Why won't those same people be using ResolveN?
15:36 <jbenet> Because almost every example they see will tell them to
  use Resolve(), and they will mostly stay away from ResolveN.

The per-prodocol versions also resolve recursively within their
protocol.  For example:

  DNSResolver.Resolve(ctx, "ipfs.io", 0)

will recursively resolve DNS links until the referenced value is no
longer a DNS link.

I also renamed the multi-protocol ipfs NameSystem (defined in
namesys/namesys.go) to 'mpns' (for Multi-Protocol Name System),
because I wasn't clear on whether IPNS applied to the whole system or
just to to the DHT-based system.  The new name is unambiguously
multi-protocol, which is good.  It would be nice to have a distinct
name for the DHT-based link system.

Now that resolver output is always prefixed with a namespace and
unprefixed mpns resolver input is interpreted as /ipfs/,
core/corehttp/ipns_hostname.go can dispense with it's old manual
/ipfs/ injection.

Now that the Resolver interface handles recursion, we don't need the
resolveRecurse helper in core/pathresolver.go.  The pathresolver
cleanup also called for an adjustment to FromSegments to more easily
get slash-prefixed paths.

Now that recursive resolution with the namesys/namesys.go composite
resolver always gets you to an /ipfs/... path, there's no need for the
/ipns/ special case in fuse/ipns/ipns_unix.go.

Now that DNS links can be things other than /ipfs/ or DHT-link
references (e.g. they could be /ipns/<domain-name> references) I've
also loosened the ParsePath logic to only attempt multihash validation
on IPFS paths.  It checks to ensure that other paths have a
known-protocol prefix, but otherwise leaves them alone.

I also changed some key-stringification from .Pretty() to .String()
following the potential deprecation mentioned in util/key.go.
  • Loading branch information
wking committed May 20, 2015
1 parent 04a9698 commit 3ead244
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 122 deletions.
2 changes: 1 addition & 1 deletion core/commands/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Resolve te value of another name:
name = req.Arguments()[0]
}

output, err := n.Namesys.Resolve(n.Context(), name)
output, err := n.Namesys.Resolve(n.Context(), "/ipns/"+name)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
Expand Down
9 changes: 4 additions & 5 deletions core/corehttp/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,17 @@ import (
type mockNamesys map[string]path.Path

func (m mockNamesys) Resolve(ctx context.Context, name string) (value path.Path, err error) {
return m.ResolveN(ctx, name, namesys.DefaultDepthLimit)
}

func (m mockNamesys) ResolveN(ctx context.Context, name string, depth int) (value path.Path, err error) {
p, ok := m[name]
if !ok {
return "", namesys.ErrResolveFailed
}
return p, nil
}

func (m mockNamesys) CanResolve(name string) bool {
_, ok := m[name]
return ok
}

func (m mockNamesys) Publish(ctx context.Context, name ci.PrivKey, value path.Path) error {
return errors.New("not implemented for mockNamesys")
}
Expand Down
2 changes: 1 addition & 1 deletion core/corehttp/ipns_hostname.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func IPNSHostnameOption() ServeOption {

host := strings.SplitN(r.Host, ":", 2)[0]
if p, err := n.Namesys.Resolve(ctx, host); err == nil {
r.URL.Path = "/ipfs/" + p.String() + r.URL.Path
r.URL.Path = p.String() + r.URL.Path
}
childMux.ServeHTTP(w, r)
})
Expand Down
63 changes: 20 additions & 43 deletions core/pathresolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package core

import (
"errors"
"fmt"
"strings"

context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
Expand All @@ -11,64 +10,42 @@ import (
path "github.com/ipfs/go-ipfs/path"
)

const maxLinks = 32
// ErrNoNamesys is an explicit error for when an IPFS node doesn't
// (yet) have a name system
var ErrNoNamesys = errors.New(
"core/resolve: no Namesys on IpfsNode - can't resolve ipns entry")

// errors returned by Resolve function
var (
ErrTooManyLinks = errors.New("core/resolve: exceeded maximum number of links in ipns entry")
ErrNoNamesys = errors.New("core/resolve: no Namesys on IpfsNode - can't resolve ipns entry")
)

// Resolve resolves the given path by parsing out /ipns/ entries and then going
// through the /ipfs/ entries and returning the final merkledage node.
// Effectively enables /ipns/ in CLI commands.
// Resolve resolves the given path by parsing out protocol-specific
// entries (e.g. /ipns/<node-key>) and then going through the /ipfs/
// entries and returning the final merkledage node. Effectively
// enables /ipns/, /dns/, etc. in commands.
func Resolve(ctx context.Context, n *IpfsNode, p path.Path) (*merkledag.Node, error) {
r := resolver{ctx, n, p}
return r.resolveRecurse(0)
}

type resolver struct {
ctx context.Context
n *IpfsNode
p path.Path
}

func (r *resolver) resolveRecurse(depth int) (*merkledag.Node, error) {
if depth >= maxLinks {
return nil, ErrTooManyLinks
}
// for now, we only try to resolve ipns paths if
// they begin with "/ipns/". Otherwise, ambiguity
// emerges when resolving just a <hash>. Is it meant
// to be an ipfs or an ipns resolution?

if strings.HasPrefix(r.p.String(), "/ipns/") {
if strings.HasPrefix(p.String(), "/") {
// namespaced path (/ipfs/..., /ipns/..., etc.)
// TODO(cryptix): we sould be able to query the local cache for the path
if r.n.Namesys == nil {
if n.Namesys == nil {
return nil, ErrNoNamesys
}
// if it's an ipns path, try to resolve it.
// if we can't, we can give that error back to the user.
seg := r.p.Segments()
if len(seg) < 2 || seg[1] == "" { // just "/ipns/"
return nil, fmt.Errorf("invalid path: %s", string(r.p))
}

ipnsPath := seg[1]
seg := p.Segments()
extensions := seg[2:]
respath, err := r.n.Namesys.Resolve(r.ctx, ipnsPath)
resolvable, err := path.FromSegments("/", seg[0], seg[1])
if err != nil {
return nil, err
}

respath, err := n.Namesys.Resolve(ctx, resolvable.String())
if err != nil {
return nil, err
}

segments := append(respath.Segments(), extensions...)
r.p, err = path.FromSegments(segments...)
p, err = path.FromSegments("/", segments...)
if err != nil {
return nil, err
}
return r.resolveRecurse(depth + 1)
}

// ok, we have an ipfs path now (or what we'll treat as one)
return r.n.Resolver.ResolvePath(r.ctx, r.p)
return n.Resolver.ResolvePath(ctx, p)
}
8 changes: 4 additions & 4 deletions fuse/ipns/ipns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ func TestFastRepublish(t *testing.T) {
if err != nil {
t.Fatal(err)
}
pubkeyHash := u.Key(h).Pretty()
pubkeyPath := "/ipns/" + u.Key(h).String()
// set them back
defer func() {
Expand All @@ -482,9 +482,9 @@ func TestFastRepublish(t *testing.T) {
writeFileData(t, dataA, fname) // random
<-time.After(shortRepublishTimeout * 2)
log.Debug("resolving first hash")
resolvedHash, err := node.Namesys.Resolve(context.Background(), pubkeyHash)
resolvedHash, err := node.Namesys.Resolve(context.Background(), pubkeyPath)
if err != nil {
t.Fatal("resolve err:", pubkeyHash, err)
t.Fatal("resolve err:", pubkeyPath, err)
}
// constantly keep writing to the file
Expand All @@ -501,7 +501,7 @@ func TestFastRepublish(t *testing.T) {
}(shortRepublishTimeout)
hasPublished := func() bool {
res, err := node.Namesys.Resolve(context.Background(), pubkeyHash)
res, err := node.Namesys.Resolve(context.Background(), pubkeyPath)
if err != nil {
t.Fatalf("resolve err: %v", err)
}
Expand Down
3 changes: 0 additions & 3 deletions fuse/ipns/ipns_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,9 +150,6 @@ func (s *Root) Lookup(ctx context.Context, name string) (fs.Node, error) {
if segments[0] == "ipfs" {
p := strings.Join(resolved.Segments()[1:], "/")
return &Link{s.IpfsRoot + "/" + p}, nil
} else if segments[0] == "ipns" {
p := strings.Join(resolved.Segments()[1:], "/")
return &Link{s.IpnsRoot + "/" + p}, nil
} else {
log.Error("Invalid path.Path: ", resolved)
return nil, errors.New("invalid path from ipns record")
Expand Down
2 changes: 1 addition & 1 deletion ipnsfs/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (fs *Filesystem) newKeyRoot(parent context.Context, k ci.PrivKey) (*KeyRoot
return nil, err
}

name := u.Key(hash).Pretty()
name := "/ipns/" + u.Key(hash).String()

root := new(KeyRoot)
root.key = k
Expand Down
54 changes: 54 additions & 0 deletions namesys/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package namesys

import (
"strings"

context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"

path "github.com/ipfs/go-ipfs/path"
)

type resolver interface {
// resolveOnce looks up a name once (without recursion).
resolveOnce(ctx context.Context, name string) (value path.Path, err error)
}

// resolve is a helper for implementing Resolver.ResolveN using resolveOnce.
func resolve(ctx context.Context, r resolver, name string, depth int, prefixes ...string) (path.Path, error) {
for {
p, err := r.resolveOnce(ctx, name)
if err != nil {
log.Warningf("Could not resolve %s", name)
return "", err
}
log.Debugf("Resolved %s to %s", name, p.String())

if strings.HasPrefix(p.String(), "/ipfs/") {
// we've bottomed out with an IPFS path
return p, nil
}

if depth == 1 {
return p, ErrResolveRecursion
}

matched := false
for _, prefix := range prefixes {
if strings.HasPrefix(p.String(), prefix) {
matched = true
if len(prefixes) == 1 {
name = strings.TrimPrefix(p.String(), prefix)
}
break
}
}

if !matched {
return p, nil
}

if depth > 1 {
depth--
}
}
}
23 changes: 16 additions & 7 deletions namesys/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,25 @@ type DNSResolver struct {
// cache would need a timeout
}

// CanResolve implements Resolver
func (r *DNSResolver) CanResolve(name string) bool {
return isd.IsDomain(name)
// Resolve implements Resolver.
func (r *DNSResolver) Resolve(ctx context.Context, name string) (path.Path, error) {
return r.ResolveN(ctx, name, DefaultDepthLimit)
}

// ResolveN implements Resolver.
func (r *DNSResolver) ResolveN(ctx context.Context, name string, depth int) (path.Path, error) {
return resolve(ctx, r, name, depth, "/ipns/")
}

// Resolve implements Resolver
// resolveOnce implements resolver.
// TXT records for a given domain name should contain a b58
// encoded multihash.
func (r *DNSResolver) Resolve(ctx context.Context, name string) (path.Path, error) {
log.Info("DNSResolver resolving %v", name)
func (r *DNSResolver) resolveOnce(ctx context.Context, name string) (path.Path, error) {
if !isd.IsDomain(name) {
return "", errors.New("not a valid domain name")
}

log.Infof("DNSResolver resolving %s", name)
txt, err := net.LookupTXT(name)
if err != nil {
return "", err
Expand All @@ -43,7 +52,7 @@ func (r *DNSResolver) Resolve(ctx context.Context, name string) (path.Path, erro
}

func parseEntry(txt string) (path.Path, error) {
p, err := path.ParseKeyToPath(txt)
p, err := path.ParseKeyToPath(txt) // bare IPFS multihashes
if err == nil {
return p, nil
}
Expand Down
40 changes: 37 additions & 3 deletions namesys/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,24 @@ import (
path "github.com/ipfs/go-ipfs/path"
)

const (
// DefaultDepthLimit is the default depth limit used by Resolve.
DefaultDepthLimit = 32

// UnlimitedDepth allows infinite recursion in ResolveN. You
// probably don't want to use this, but it's here if you absolutely
// trust resolution to eventually complete and can't put an upper
// limit on how many steps it will take.
UnlimitedDepth = 0
)

// ErrResolveFailed signals an error when attempting to resolve.
var ErrResolveFailed = errors.New("could not resolve name.")

// ErrResolveRecursion signals a recursion-depth limit.
var ErrResolveRecursion = errors.New(
"could not resolve name (recursion limit exceeded).")

// ErrPublishFailed signals an error when attempting to publish.
var ErrPublishFailed = errors.New("could not publish name.")

Expand All @@ -58,11 +73,30 @@ type NameSystem interface {
// Resolver is an object capable of resolving names.
type Resolver interface {

// Resolve looks up a name, and returns the value previously published.
// Resolve performs a recursive lookup, returning the dereferenced
// path. For example, if ipfs.io has a DNS TXT record pointing to
// /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
// and there is a DHT IPNS entry for
// QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
// -> /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
// then
// Resolve(ctx, "/ipns/ipfs.io")
// will resolve both names, returning
// /ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
//
// There is a default depth-limit to avoid infinite recursion. Most
// users will be fine with this default limit, but if you need to
// adjust the limit you can use ResolveN.
Resolve(ctx context.Context, name string) (value path.Path, err error)

// CanResolve checks whether this Resolver can resolve a name
CanResolve(name string) bool
// ResolveN performs a recursive lookup, returning the dereferenced
// path. The only difference from Resolve is that the depth limit
// is configurable. You can use DefaultDepthLimit, UnlimitedDepth,
// or a depth limit of your own choosing.
//
// Most users should use Resolve, since the default limit works well
// in most real-world situations.
ResolveN(ctx context.Context, name string, depth int) (value path.Path, err error)
}

// Publisher is an object capable of publishing particular names.
Expand Down
Loading

0 comments on commit 3ead244

Please sign in to comment.