namesys: Add recursive resolution

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.
This commit is contained in:
W. Trevor King 2015-05-07 14:31:14 -07:00
parent 04a969835e
commit 3ead2443e5
15 changed files with 231 additions and 124 deletions

View File

@ -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

View File

@ -22,6 +22,10 @@ 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
@ -29,11 +33,6 @@ func (m mockNamesys) Resolve(ctx context.Context, name string) (value path.Path,
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")
}

View File

@ -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)
})

View File

@ -2,7 +2,6 @@ package core
import (
"errors"
"fmt"
"strings"
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
@ -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))
seg := p.Segments()
extensions := seg[2:]
resolvable, err := path.FromSegments("/", seg[0], seg[1])
if err != nil {
return nil, err
}
ipnsPath := seg[1]
extensions := seg[2:]
respath, err := r.n.Namesys.Resolve(r.ctx, ipnsPath)
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)
}

View File

@ -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() {
@ -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
@ -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)
}

View File

@ -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")

View File

@ -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

54
namesys/base.go Normal file
View File

@ -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--
}
}
}

View File

@ -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)
}
// Resolve implements Resolver
// ResolveN implements Resolver.
func (r *DNSResolver) ResolveN(ctx context.Context, name string, depth int) (path.Path, error) {
return resolve(ctx, r, name, depth, "/ipns/")
}
// 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
@ -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
}

View File

@ -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.")
@ -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.

View File

@ -1,59 +1,83 @@
package namesys
import (
"strings"
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
ci "github.com/ipfs/go-ipfs/p2p/crypto"
path "github.com/ipfs/go-ipfs/path"
routing "github.com/ipfs/go-ipfs/routing"
)
// ipnsNameSystem implements IPNS naming.
// mpns (a multi-protocol NameSystem) implements generic IPFS naming.
//
// Uses three Resolvers:
// Uses several Resolvers:
// (a) ipfs routing naming: SFS-like PKI names.
// (b) dns domains: resolves using links in DNS TXT records
// (c) proquints: interprets string as the raw byte data.
//
// It can only publish to: (a) ipfs routing naming.
//
type ipns struct {
resolvers []Resolver
publisher Publisher
type mpns struct {
resolvers map[string]resolver
publishers map[string]Publisher
}
// NewNameSystem will construct the IPFS naming system based on Routing
func NewNameSystem(r routing.IpfsRouting) NameSystem {
return &ipns{
resolvers: []Resolver{
new(DNSResolver),
new(ProquintResolver),
NewRoutingResolver(r),
return &mpns{
resolvers: map[string]resolver{
"dns": new(DNSResolver),
"proquint": new(ProquintResolver),
"dht": newRoutingResolver(r),
},
publishers: map[string]Publisher{
"/ipns/": NewRoutingPublisher(r),
},
publisher: NewRoutingPublisher(r),
}
}
// Resolve implements Resolver
func (ns *ipns) Resolve(ctx context.Context, name string) (path.Path, error) {
for _, r := range ns.resolvers {
if r.CanResolve(name) {
return r.Resolve(ctx, name)
// Resolve implements Resolver.
func (ns *mpns) Resolve(ctx context.Context, name string) (path.Path, error) {
return ns.ResolveN(ctx, name, DefaultDepthLimit)
}
// ResolveN implements Resolver.
func (ns *mpns) ResolveN(ctx context.Context, name string, depth int) (path.Path, error) {
if strings.HasPrefix(name, "/ipfs/") {
return path.ParsePath(name)
}
if !strings.HasPrefix(name, "/") {
return path.ParsePath("/ipfs/" + name)
}
return resolve(ctx, ns, name, depth, "/ipns/")
}
// resolveOnce implements resolver.
func (ns *mpns) resolveOnce(ctx context.Context, name string) (path.Path, error) {
if !strings.HasPrefix(name, "/ipns/") {
name = "/ipns/" + name
}
segments := strings.SplitN(name, "/", 3)
if len(segments) < 3 || segments[0] != "" {
log.Warningf("Invalid name syntax for %s", name)
return "", ErrResolveFailed
}
for protocol, resolver := range ns.resolvers {
log.Debugf("Attempting to resolve %s with %s", name, protocol)
p, err := resolver.resolveOnce(ctx, segments[2])
if err == nil {
return p, err
}
}
log.Warningf("No resolver found for %s", name)
return "", ErrResolveFailed
}
// CanResolve implements Resolver
func (ns *ipns) CanResolve(name string) bool {
for _, r := range ns.resolvers {
if r.CanResolve(name) {
return true
}
}
return false
}
// Publish implements Publisher
func (ns *ipns) Publish(ctx context.Context, name ci.PrivKey, value path.Path) error {
return ns.publisher.Publish(ctx, name, value)
func (ns *mpns) Publish(ctx context.Context, name ci.PrivKey, value path.Path) error {
return ns.publishers["/ipns/"].Publish(ctx, name, value)
}

View File

@ -10,16 +10,20 @@ import (
type ProquintResolver struct{}
// CanResolve implements Resolver. Checks whether the name is a proquint string.
func (r *ProquintResolver) CanResolve(name string) bool {
ok, err := proquint.IsProquint(name)
return err == nil && ok
// Resolve implements Resolver.
func (r *ProquintResolver) Resolve(ctx context.Context, name string) (path.Path, error) {
return r.ResolveN(ctx, name, DefaultDepthLimit)
}
// Resolve implements Resolver. Decodes the proquint string.
func (r *ProquintResolver) Resolve(ctx context.Context, name string) (path.Path, error) {
ok := r.CanResolve(name)
if !ok {
// ResolveN implements Resolver.
func (r *ProquintResolver) ResolveN(ctx context.Context, name string, depth int) (path.Path, error) {
return resolve(ctx, r, name, depth, "/ipns/")
}
// resolveOnce implements resolver. Decodes the proquint string.
func (r *ProquintResolver) resolveOnce(ctx context.Context, name string) (path.Path, error) {
ok, err := proquint.IsProquint(name)
if err != nil || !ok {
return "", errors.New("not a valid proquint string")
}
return path.FromString(string(proquint.Decode(name))), nil

View File

@ -30,15 +30,28 @@ func NewRoutingResolver(route routing.IpfsRouting) Resolver {
return &routingResolver{routing: route}
}
// CanResolve implements Resolver. Checks whether name is a b58 encoded string.
func (r *routingResolver) CanResolve(name string) bool {
_, err := mh.FromB58String(name)
return err == nil
// newRoutingResolver returns a resolver instead of a Resolver.
func newRoutingResolver(route routing.IpfsRouting) resolver {
if route == nil {
panic("attempt to create resolver with nil routing system")
}
return &routingResolver{routing: route}
}
// Resolve implements Resolver. Uses the IPFS routing system to resolve SFS-like
// names.
// Resolve implements Resolver.
func (r *routingResolver) Resolve(ctx context.Context, name string) (path.Path, error) {
return r.ResolveN(ctx, name, DefaultDepthLimit)
}
// ResolveN implements Resolver.
func (r *routingResolver) ResolveN(ctx context.Context, name string, depth int) (path.Path, error) {
return resolve(ctx, r, name, depth, "/ipns/")
}
// resolveOnce implements resolver. Uses the IPFS routing system to
// resolve SFS-like names.
func (r *routingResolver) resolveOnce(ctx context.Context, name string) (path.Path, error) {
log.Debugf("RoutingResolve: '%s'", name)
hash, err := mh.FromB58String(name)
if err != nil {

View File

@ -44,12 +44,8 @@ func (p Path) String() string {
return string(p)
}
func FromSegments(seg ...string) (Path, error) {
var pref string
if seg[0] == "ipfs" || seg[0] == "ipns" {
pref = "/"
}
return ParsePath(pref + strings.Join(seg, "/"))
func FromSegments(prefix string, seg ...string) (Path, error) {
return ParsePath(prefix + strings.Join(seg, "/"))
}
func ParsePath(txt string) (Path, error) {
@ -68,15 +64,15 @@ func ParsePath(txt string) (Path, error) {
return "", ErrBadPath
}
if parts[1] != "ipfs" && parts[1] != "ipns" {
if parts[1] == "ipfs" {
_, err := ParseKeyToPath(parts[2])
if err != nil {
return "", err
}
} else if parts[1] != "ipns" {
return "", ErrBadPath
}
_, err := ParseKeyToPath(parts[2])
if err != nil {
return "", err
}
return Path(txt), nil
}

View File

@ -59,8 +59,8 @@ func TestRecurivePathResolution(t *testing.T) {
t.Fatal(err)
}
segments := []string{"", "ipfs", aKey.String(), "child", "grandchild"}
p, err := path.FromSegments(segments...)
segments := []string{aKey.String(), "child", "grandchild"}
p, err := path.FromSegments("/ipfs/", segments...)
if err != nil {
t.Fatal(err)
}