Merge pull request #1208 from wking/dns-resolver

Rework mutable namespace resolution to handle recursion
This commit is contained in:
Juan Batiz-Benet 2015-05-20 12:42:18 -04:00
commit 01e1e71221
24 changed files with 658 additions and 176 deletions

82
core/commands/dns.go Normal file
View File

@ -0,0 +1,82 @@
package commands
import (
"io"
"strings"
cmds "github.com/ipfs/go-ipfs/commands"
namesys "github.com/ipfs/go-ipfs/namesys"
util "github.com/ipfs/go-ipfs/util"
)
var DNSCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "DNS link resolver",
ShortDescription: `
Multihashes are hard to remember, but domain names are usually easy to
remember. To create memorable aliases for multihashes, DNS TXT
records can point to other DNS links, IPFS objects, IPNS keys, etc.
This command resolves those links to the referenced object.
`,
LongDescription: `
Multihashes are hard to remember, but domain names are usually easy to
remember. To create memorable aliases for multihashes, DNS TXT
records can point to other DNS links, IPFS objects, IPNS keys, etc.
This command resolves those links to the referenced object.
For example, with this DNS TXT record:
ipfs.io. TXT "dnslink=/ipfs/QmRzTuh2Lpuz7Gr39stNr6mTFdqAghsZec1JoUnfySUzcy ..."
The resolver will give:
> ipfs dns ipfs.io
/ipfs/QmRzTuh2Lpuz7Gr39stNr6mTFdqAghsZec1JoUnfySUzcy
And with this DNS TXT record:
ipfs.ipfs.io. TXT "dnslink=/dns/ipfs.io ..."
The resolver will give:
> ipfs dns ipfs.io
/dns/ipfs.io
> ipfs dns --recursive
/ipfs/QmRzTuh2Lpuz7Gr39stNr6mTFdqAghsZec1JoUnfySUzcy
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("domain-name", true, false, "The domain-name name to resolve.").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption("recursive", "r", "Resolve until the result is not a DNS link"),
},
Run: func(req cmds.Request, res cmds.Response) {
recursive, _, _ := req.Option("recursive").Bool()
name := req.Arguments()[0]
resolver := namesys.NewDNSResolver()
depth := 1
if recursive {
depth = namesys.DefaultDepthLimit
}
output, err := resolver.ResolveN(req.Context().Context, name, depth)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
res.SetOutput(&ResolvedPath{output})
},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
output, ok := res.Output().(*ResolvedPath)
if !ok {
return nil, util.ErrCast()
}
return strings.NewReader(output.Path.String()), nil
},
},
Type: ResolvedPath{},
}

104
core/commands/ipns.go Normal file
View File

@ -0,0 +1,104 @@
package commands
import (
"errors"
"io"
"strings"
cmds "github.com/ipfs/go-ipfs/commands"
namesys "github.com/ipfs/go-ipfs/namesys"
u "github.com/ipfs/go-ipfs/util"
)
var ipnsCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Gets the value currently published at an IPNS name",
ShortDescription: `
IPNS is a PKI namespace, where names are the hashes of public keys, and
the private key enables publishing new (signed) values. In resolve, the
default value of <name> is your own identity public key.
`,
LongDescription: `
IPNS is a PKI namespace, where names are the hashes of public keys, and
the private key enables publishing new (signed) values. In resolve, the
default value of <name> is your own identity public key.
Examples:
Resolve the value of your identity:
> ipfs name resolve
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Resolve the value of another name:
> ipfs name resolve QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("name", false, false, "The IPNS name to resolve. Defaults to your node's peerID.").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption("recursive", "r", "Resolve until the result is not an IPNS name"),
},
Run: func(req cmds.Request, res cmds.Response) {
n, err := req.Context().GetNode()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
if !n.OnlineMode() {
err := n.SetupOfflineRouting()
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
}
var name string
if len(req.Arguments()) == 0 {
if n.Identity == "" {
res.SetError(errors.New("Identity not loaded!"), cmds.ErrNormal)
return
}
name = n.Identity.Pretty()
} else {
name = req.Arguments()[0]
}
recursive, _, _ := req.Option("recursive").Bool()
depth := 1
if recursive {
depth = namesys.DefaultDepthLimit
}
resolver := namesys.NewRoutingResolver(n.Routing)
output, err := resolver.ResolveN(n.Context(), name, depth)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
// TODO: better errors (in the case of not finding the name, we get "failed to find any peer in table")
res.SetOutput(&ResolvedPath{output})
},
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
output, ok := res.Output().(*ResolvedPath)
if !ok {
return nil, u.ErrCast()
}
return strings.NewReader(output.Path.String()), nil
},
},
Type: ResolvedPath{},
}

View File

@ -27,31 +27,31 @@ and resolve, the default value of <name> is your own identity public key.
Examples:
Publish a <ref> to your identity name:
Publish an <ipfs-path> to your identity name:
> ipfs name publish QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
> ipfs name publish /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Publish a <ref> to another public key:
Publish an <ipfs-path> to another public key:
> ipfs name publish QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
> ipfs name publish /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Resolve the value of your identity:
> ipfs name resolve
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Resolve the value of another name:
> ipfs name resolve QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
`,
},
Subcommands: map[string]*cmds.Command{
"publish": publishCmd,
"resolve": resolveCmd,
"resolve": ipnsCmd,
},
}

View File

@ -35,12 +35,13 @@ Examples:
Publish an <ipfs-path> to your identity name:
> ipfs name publish /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Publish an <ipfs-path> to another public key (not implemented):
> ipfs name publish QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
published name QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n to QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
> ipfs name publish /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
Published to QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n: /ipfs/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
`,
},
@ -102,7 +103,7 @@ Publish an <ipfs-path> to another public key (not implemented):
Marshalers: cmds.MarshalerMap{
cmds.Text: func(res cmds.Response) (io.Reader, error) {
v := res.Output().(*IpnsEntry)
s := fmt.Sprintf("Published name %s to %s\n", v.Name, v.Value)
s := fmt.Sprintf("Published to %s: %s\n", v.Name, v.Value)
return strings.NewReader(s), nil
},
},

View File

@ -1,11 +1,11 @@
package commands
import (
"errors"
"io"
"strings"
cmds "github.com/ipfs/go-ipfs/commands"
namesys "github.com/ipfs/go-ipfs/namesys"
path "github.com/ipfs/go-ipfs/path"
u "github.com/ipfs/go-ipfs/util"
)
@ -14,37 +14,46 @@ type ResolvedPath struct {
Path path.Path
}
var resolveCmd = &cmds.Command{
var ResolveCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Gets the value currently published at an IPNS name",
Tagline: "Resolve the value of names to IPFS",
ShortDescription: `
IPNS is a PKI namespace, where names are the hashes of public keys, and
the private key enables publishing new (signed) values. In resolve, the
default value of <name> is your own identity public key.
There are a number of mutable name protocols that can link among
themselves and into IPNS. This command accepts any of these
identifiers and resolves them to the referenced item.
`,
LongDescription: `
IPNS is a PKI namespace, where names are the hashes of public keys, and
the private key enables publishing new (signed) values. In resolve, the
default value of <name> is your own identity public key.
There are a number of mutable name protocols that can link among
themselves and into IPNS. For example IPNS references can (currently)
point at IPFS object, and DNS links can point at other DNS links, IPNS
entries, or IPFS objects. This command accepts any of these
identifiers and resolves them to the referenced item.
Examples:
Resolve the value of your identity:
> ipfs name resolve
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
> ipfs resolve /ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
Resolve te value of another name:
Resolve the value of another name:
> ipfs name resolve QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
> ipfs resolve /ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
Resolve the value of another name recursively:
> ipfs resolve -r /ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n
/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
`,
},
Arguments: []cmds.Argument{
cmds.StringArg("name", false, false, "The IPNS name to resolve. Defaults to your node's peerID.").EnableStdin(),
cmds.StringArg("name", true, false, "The name to resolve.").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption("recursive", "r", "Resolve until the result is an IPFS name"),
},
Run: func(req cmds.Request, res cmds.Response) {
@ -62,27 +71,19 @@ Resolve te value of another name:
}
}
var name string
if len(req.Arguments()) == 0 {
if n.Identity == "" {
res.SetError(errors.New("Identity not loaded!"), cmds.ErrNormal)
return
}
name = n.Identity.Pretty()
} else {
name = req.Arguments()[0]
name := req.Arguments()[0]
recursive, _, _ := req.Option("recursive").Bool()
depth := 1
if recursive {
depth = namesys.DefaultDepthLimit
}
output, err := n.Namesys.Resolve(n.Context(), name)
output, err := n.Namesys.ResolveN(n.Context(), name, depth)
if err != nil {
res.SetError(err, cmds.ErrNormal)
return
}
// TODO: better errors (in the case of not finding the name, we get "failed to find any peer in table")
res.SetOutput(&ResolvedPath{output})
},
Marshalers: cmds.MarshalerMap{

View File

@ -40,7 +40,9 @@ ADVANCED COMMANDS
daemon Start a long-running daemon process
mount Mount an ipfs read-only mountpoint
resolve Resolve any type of name
name Publish or resolve IPNS names
dns Resolve DNS links
pin Pin objects to local storage
repo gc Garbage collect unpinned objects
@ -84,6 +86,7 @@ var rootSubcommands = map[string]*cmds.Command{
"config": ConfigCmd,
"dht": DhtCmd,
"diag": DiagCmd,
"dns": DNSCmd,
"get": GetCmd,
"id": IDCmd,
"log": LogCmd,
@ -95,6 +98,7 @@ var rootSubcommands = map[string]*cmds.Command{
"ping": PingCmd,
"refs": RefsCmd,
"repo": RepoCmd,
"resolve": ResolveCmd,
"stats": StatsCmd,
"swarm": SwarmCmd,
"update": UpdateCmd,

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

@ -11,23 +11,46 @@ import (
path "github.com/ipfs/go-ipfs/path"
)
type LookupTXTFunc func(name string) (txt []string, err error)
// DNSResolver implements a Resolver on DNS domains
type DNSResolver struct {
lookupTXT LookupTXTFunc
// TODO: maybe some sort of caching?
// cache would need a timeout
}
// CanResolve implements Resolver
func (r *DNSResolver) CanResolve(name string) bool {
return isd.IsDomain(name)
// NewDNSResolver constructs a name resolver using DNS TXT records.
func NewDNSResolver() Resolver {
return &DNSResolver{lookupTXT: net.LookupTXT}
}
// Resolve implements Resolver
// newDNSResolver constructs a name resolver using DNS TXT records,
// returning a resolver instead of NewDNSResolver's Resolver.
func newDNSResolver() resolver {
return &DNSResolver{lookupTXT: net.LookupTXT}
}
// 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/")
}
// 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)
txt, err := net.LookupTXT(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 := r.lookupTXT(name)
if err != nil {
return "", err
}
@ -43,7 +66,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
}
@ -52,10 +75,10 @@ func parseEntry(txt string) (path.Path, error) {
}
func tryParseDnsLink(txt string) (path.Path, error) {
parts := strings.Split(txt, "=")
if len(parts) == 1 || parts[0] != "dnslink" {
return "", errors.New("not a valid dnslink entry")
parts := strings.SplitN(txt, "=", 2)
if len(parts) == 2 && parts[0] == "dnslink" {
return path.ParsePath(parts[1])
}
return path.ParsePath(parts[1])
return "", errors.New("not a valid dnslink entry")
}

View File

@ -1,9 +1,22 @@
package namesys
import (
"fmt"
"testing"
)
type mockDNS struct {
entries map[string][]string
}
func (m *mockDNS) lookupTXT(name string) (txt []string, err error) {
txt, ok := m.entries[name]
if !ok {
return nil, fmt.Errorf("No TXT entry for %s", name)
}
return txt, nil
}
func TestDnsEntryParsing(t *testing.T) {
goodEntries := []string{
"QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
@ -40,3 +53,60 @@ func TestDnsEntryParsing(t *testing.T) {
}
}
}
func newMockDNS() *mockDNS {
return &mockDNS{
entries: map[string][]string{
"multihash.example.com": []string{
"dnslink=QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
},
"ipfs.example.com": []string{
"dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD",
},
"dns1.example.com": []string{
"dnslink=/ipns/ipfs.example.com",
},
"dns2.example.com": []string{
"dnslink=/ipns/dns1.example.com",
},
"multi.example.com": []string{
"some stuff",
"dnslink=/ipns/dns1.example.com",
"masked dnslink=/ipns/example.invalid",
},
"equals.example.com": []string{
"dnslink=/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals",
},
"loop1.example.com": []string{
"dnslink=/ipns/loop2.example.com",
},
"loop2.example.com": []string{
"dnslink=/ipns/loop1.example.com",
},
"bad.example.com": []string{
"dnslink=",
},
},
}
}
func TestDNSResolution(t *testing.T) {
mock := newMockDNS()
r := &DNSResolver{lookupTXT: mock.lookupTXT}
testResolution(t, r, "multihash.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "ipfs.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "dns1.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "dns1.example.com", 1, "/ipns/ipfs.example.com", ErrResolveRecursion)
testResolution(t, r, "dns2.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "dns2.example.com", 1, "/ipns/dns1.example.com", ErrResolveRecursion)
testResolution(t, r, "dns2.example.com", 2, "/ipns/ipfs.example.com", ErrResolveRecursion)
testResolution(t, r, "multi.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", nil)
testResolution(t, r, "multi.example.com", 1, "/ipns/dns1.example.com", ErrResolveRecursion)
testResolution(t, r, "multi.example.com", 2, "/ipns/ipfs.example.com", ErrResolveRecursion)
testResolution(t, r, "equals.example.com", DefaultDepthLimit, "/ipfs/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD/=equals", nil)
testResolution(t, r, "loop1.example.com", 1, "/ipns/loop2.example.com", ErrResolveRecursion)
testResolution(t, r, "loop1.example.com", 2, "/ipns/loop1.example.com", ErrResolveRecursion)
testResolution(t, r, "loop1.example.com", 3, "/ipns/loop2.example.com", ErrResolveRecursion)
testResolution(t, r, "loop1.example.com", DefaultDepthLimit, "/ipns/loop1.example.com", ErrResolveRecursion)
testResolution(t, r, "bad.example.com", DefaultDepthLimit, "", ErrResolveFailed)
}

View File

@ -1,4 +1,32 @@
// package namesys implements various functionality for the ipns naming system.
/*
Package namesys implements resolvers and publishers for the IPFS
naming system (IPNS).
The core of IPFS is an immutable, content-addressable Merkle graph.
That works well for many use cases, but doesn't allow you to answer
questions like "what is Alice's current homepage?". The mutable name
system allows Alice to publish information like:
The current homepage for alice.example.com is
/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
or:
The current homepage for node
QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy
is
/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj
The mutable name system also allows users to resolve those references
to find the immutable IPFS object currently referenced by a given
mutable name.
For command-line bindings to this functionality, see:
ipfs name
ipfs dns
ipfs resolve
*/
package namesys
import (
@ -9,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.")
@ -30,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": newDNSResolver(),
"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)
}

71
namesys/namesys_test.go Normal file
View File

@ -0,0 +1,71 @@
package namesys
import (
"fmt"
"testing"
context "github.com/ipfs/go-ipfs/Godeps/_workspace/src/golang.org/x/net/context"
path "github.com/ipfs/go-ipfs/path"
)
type mockResolver struct {
entries map[string]string
}
func testResolution(t *testing.T, resolver Resolver, name string, depth int, expected string, expError error) {
p, err := resolver.ResolveN(context.Background(), name, depth)
if err != expError {
t.Fatal(fmt.Errorf(
"Expected %s with a depth of %d to have a '%s' error, but got '%s'",
name, depth, expError, err))
}
if p.String() != expected {
t.Fatal(fmt.Errorf(
"%s with depth %d resolved to %s != %s",
name, depth, p.String(), expected))
}
}
func (r *mockResolver) resolveOnce(ctx context.Context, name string) (path.Path, error) {
return path.ParsePath(r.entries[name])
}
func mockResolverOne() *mockResolver {
return &mockResolver{
entries: map[string]string{
"QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy": "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj",
"QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n": "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy",
"QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD": "/ipns/ipfs.io",
},
}
}
func mockResolverTwo() *mockResolver {
return &mockResolver{
entries: map[string]string{
"ipfs.io": "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n",
},
}
}
func TestNamesysResolution(t *testing.T) {
r := &mpns{
resolvers: map[string]resolver{
"one": mockResolverOne(),
"two": mockResolverTwo(),
},
}
testResolution(t, r, "Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
testResolution(t, r, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
testResolution(t, r, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", 1, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
testResolution(t, r, "/ipns/ipfs.io", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
testResolution(t, r, "/ipns/ipfs.io", 1, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
testResolution(t, r, "/ipns/ipfs.io", 2, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", DefaultDepthLimit, "/ipfs/Qmcqtw8FfrVSBaRmbWwHxt3AuySBhJLcvmFYi3Lbc4xnwj", nil)
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 1, "/ipns/ipfs.io", ErrResolveRecursion)
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 2, "/ipns/QmbCMUZw6JFeZ7Wp9jkzbye3Fzp2GGcPgC3nmeUjfVF87n", ErrResolveRecursion)
testResolution(t, r, "/ipns/QmY3hE8xgFCjGcz6PHgnvJz5HZi1BaKRfPkn1ghZUcYMjD", 3, "/ipns/QmatmE9msSfkKxoffpHwNLNKgwZG8eT9Bud6YoPab52vpy", ErrResolveRecursion)
}

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

@ -42,7 +42,7 @@ func NewRoutingPublisher(route routing.IpfsRouting) Publisher {
// Publish implements Publisher. Accepts a keypair and a value,
// and publishes it out to the routing system
func (p *ipnsPublisher) Publish(ctx context.Context, k ci.PrivKey, value path.Path) error {
log.Debugf("namesys: Publish %s", value)
log.Debugf("Publish %s", value)
data, err := createRoutingEntryData(k, value)
if err != 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)
}

View File

@ -14,11 +14,11 @@ test_init_ipfs
test_expect_success "'ipfs name publish' succeeds" '
PEERID=`ipfs id --format="<id>"` &&
ipfs name publish "$HASH_WELCOME_DOCS" >publish_out
ipfs name publish "/ipfs/$HASH_WELCOME_DOCS" >publish_out
'
test_expect_success "publish output looks good" '
echo "Published name $PEERID to /ipfs/$HASH_WELCOME_DOCS" >expected1 &&
echo "Published to ${PEERID}: /ipfs/$HASH_WELCOME_DOCS" >expected1 &&
test_cmp publish_out expected1
'
@ -39,7 +39,7 @@ test_expect_success "'ipfs name publish' succeeds" '
'
test_expect_success "publish a path looks good" '
echo "Published name $PEERID to /ipfs/$HASH_WELCOME_DOCS/help" >expected3 &&
echo "Published to ${PEERID}: /ipfs/$HASH_WELCOME_DOCS/help" >expected3 &&
test_cmp publish_out expected3
'