diff --git a/core/coreapi/coreapi.go b/core/coreapi/coreapi.go index 5c0326b51e8..14e43483b87 100644 --- a/core/coreapi/coreapi.go +++ b/core/coreapi/coreapi.go @@ -29,6 +29,14 @@ func (api *CoreAPI) Dag() coreiface.DagAPI { return &DagAPI{api, nil} } +func (api *CoreAPI) Name() coreiface.NameAPI { + return &NameAPI{api, nil} +} + +func (api *CoreAPI) Key() coreiface.KeyAPI { + return &KeyAPI{api, nil} +} + func (api *CoreAPI) ResolveNode(ctx context.Context, p coreiface.Path) (coreiface.Node, error) { p, err := api.ResolvePath(ctx, p) if err != nil { diff --git a/core/coreapi/interface/interface.go b/core/coreapi/interface/interface.go index e51888e6012..cdbb2508bd0 100644 --- a/core/coreapi/interface/interface.go +++ b/core/coreapi/interface/interface.go @@ -6,6 +6,7 @@ import ( "context" "errors" "io" + "time" options "github.com/ipfs/go-ipfs/core/coreapi/interface/options" @@ -32,11 +33,23 @@ type Reader interface { io.Closer } +type IpnsEntry interface { + Name() string + Value() Path +} + +type Key interface { + Name() string + Path() Path +} + // CoreAPI defines an unified interface to IPFS for Go programs. type CoreAPI interface { // Unixfs returns an implementation of Unixfs API Unixfs() UnixfsAPI Dag() DagAPI + Name() NameAPI + Key() KeyAPI // ResolvePath resolves the path using Unixfs resolver ResolvePath(context.Context, Path) (Path, error) @@ -90,6 +103,81 @@ type DagAPI interface { WithDepth(depth int) options.DagTreeOption } +// NameAPI specifies the interface to IPNS. +// +// IPNS is a PKI namespace, where names are the hashes of public keys, and the +// private key enables publishing new (signed) values. In both publish and +// resolve, the default name used is the node's own PeerID, which is the hash of +// its public key. +// +// You can use .Key API to list and generate more names and their respective keys. +type NameAPI interface { + // Publish announces new IPNS name + Publish(ctx context.Context, path Path, opts ...options.NamePublishOption) (IpnsEntry, error) + + // WithValidTime is an option for Publish which specifies for how long the + // entry will remain valid. Default value is 24h + WithValidTime(validTime time.Duration) options.NamePublishOption + + // WithKey is an option for Publish which specifies the key to use for + // publishing. Default value is "self" which is the node's own PeerID. + // The key parameter must be either PeerID or keystore key alias. + // + // You can use KeyAPI to list and generate more names and their respective keys. + WithKey(key string) options.NamePublishOption + + // Resolve attempts to resolve the newest version of the specified name + Resolve(ctx context.Context, name string, opts ...options.NameResolveOption) (Path, error) + + // WithRecursive is an option for Resolve which specifies whether to perform a + // recursive lookup. Default value is false + WithRecursive(recursive bool) options.NameResolveOption + + // WithLocal is an option for Resolve which specifies if the lookup should be + // offline. Default value is false + WithLocal(local bool) options.NameResolveOption + + // WithCache is an option for Resolve which specifies if cache should be used. + // Default value is true + WithCache(cache bool) options.NameResolveOption +} + +// KeyAPI specifies the interface to Keystore +type KeyAPI interface { + // Generate generates new key, stores it in the keystore under the specified + // name and returns a base58 encoded multihash of it's public key + Generate(ctx context.Context, name string, opts ...options.KeyGenerateOption) (Key, error) + + // WithType is an option for Generate which specifies which algorithm + // should be used for the key. Default is options.RSAKey + // + // Supported key types: + // * options.RSAKey + // * options.Ed25519Key + WithType(algorithm string) options.KeyGenerateOption + + // WithSize is an option for Generate which specifies the size of the key to + // generated. Default is -1 + // + // value of -1 means 'use default size for key type': + // * 2048 for RSA + WithSize(size int) options.KeyGenerateOption + + // Rename renames oldName key to newName. Returns the key and whether another + // key was overwritten, or an error + Rename(ctx context.Context, oldName string, newName string, opts ...options.KeyRenameOption) (Key, bool, error) + + // WithForce is an option for Rename which specifies whether to allow to + // replace existing keys. + WithForce(force bool) options.KeyRenameOption + + // List lists keys stored in keystore + List(ctx context.Context) ([]Key, error) + + // Remove removes keys from keystore. Returns ipns path of the removed key + Remove(ctx context.Context, name string) (Path, error) +} + // type ObjectAPI interface { // New() (cid.Cid, Object) // Get(string) (Object, error) diff --git a/core/coreapi/interface/options/key.go b/core/coreapi/interface/options/key.go new file mode 100644 index 00000000000..114361875a5 --- /dev/null +++ b/core/coreapi/interface/options/key.go @@ -0,0 +1,72 @@ +package options + +const ( + RSAKey = "rsa" + Ed25519Key = "ed25519" + + DefaultRSALen = 2048 +) + +type KeyGenerateSettings struct { + Algorithm string + Size int +} + +type KeyRenameSettings struct { + Force bool +} + +type KeyGenerateOption func(*KeyGenerateSettings) error +type KeyRenameOption func(*KeyRenameSettings) error + +func KeyGenerateOptions(opts ...KeyGenerateOption) (*KeyGenerateSettings, error) { + options := &KeyGenerateSettings{ + Algorithm: RSAKey, + Size: -1, + } + + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + return options, nil +} + +func KeyRenameOptions(opts ...KeyRenameOption) (*KeyRenameSettings, error) { + options := &KeyRenameSettings{ + Force: false, + } + + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + return options, nil +} + +type KeyOptions struct{} + +func (api *KeyOptions) WithType(algorithm string) KeyGenerateOption { + return func(settings *KeyGenerateSettings) error { + settings.Algorithm = algorithm + return nil + } +} + +func (api *KeyOptions) WithSize(size int) KeyGenerateOption { + return func(settings *KeyGenerateSettings) error { + settings.Size = size + return nil + } +} + +func (api *KeyOptions) WithForce(force bool) KeyRenameOption { + return func(settings *KeyRenameSettings) error { + settings.Force = force + return nil + } +} diff --git a/core/coreapi/interface/options/name.go b/core/coreapi/interface/options/name.go new file mode 100644 index 00000000000..9f8aaafc83e --- /dev/null +++ b/core/coreapi/interface/options/name.go @@ -0,0 +1,93 @@ +package options + +import ( + "time" +) + +const ( + DefaultNameValidTime = 24 * time.Hour +) + +type NamePublishSettings struct { + ValidTime time.Duration + Key string +} + +type NameResolveSettings struct { + Recursive bool + Local bool + Cache bool +} + +type NamePublishOption func(*NamePublishSettings) error +type NameResolveOption func(*NameResolveSettings) error + +func NamePublishOptions(opts ...NamePublishOption) (*NamePublishSettings, error) { + options := &NamePublishSettings{ + ValidTime: DefaultNameValidTime, + Key: "self", + } + + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + + return options, nil +} + +func NameResolveOptions(opts ...NameResolveOption) (*NameResolveSettings, error) { + options := &NameResolveSettings{ + Recursive: false, + Local: false, + Cache: true, + } + + for _, opt := range opts { + err := opt(options) + if err != nil { + return nil, err + } + } + + return options, nil +} + +type NameOptions struct{} + +func (api *NameOptions) WithValidTime(validTime time.Duration) NamePublishOption { + return func(settings *NamePublishSettings) error { + settings.ValidTime = validTime + return nil + } +} + +func (api *NameOptions) WithKey(key string) NamePublishOption { + return func(settings *NamePublishSettings) error { + settings.Key = key + return nil + } +} + +func (api *NameOptions) WithRecursive(recursive bool) NameResolveOption { + return func(settings *NameResolveSettings) error { + settings.Recursive = recursive + return nil + } +} + +func (api *NameOptions) WithLocal(local bool) NameResolveOption { + return func(settings *NameResolveSettings) error { + settings.Local = local + return nil + } +} + +func (api *NameOptions) WithCache(cache bool) NameResolveOption { + return func(settings *NameResolveSettings) error { + settings.Cache = cache + return nil + } +} diff --git a/core/coreapi/key.go b/core/coreapi/key.go new file mode 100644 index 00000000000..b5a0e3308ba --- /dev/null +++ b/core/coreapi/key.go @@ -0,0 +1,201 @@ +package coreapi + +import ( + "context" + "crypto/rand" + "fmt" + "sort" + + coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface" + caopts "github.com/ipfs/go-ipfs/core/coreapi/interface/options" + ipfspath "github.com/ipfs/go-ipfs/path" + + peer "gx/ipfs/QmWNY7dV54ZDYmTA1ykVdwNCqC11mpU4zSUp6XDpLTH9eG/go-libp2p-peer" + crypto "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto" +) + +type KeyAPI struct { + *CoreAPI + *caopts.KeyOptions +} + +type key struct { + name string + peerId string +} + +func (k *key) Name() string { + return k.name +} + +func (k *key) Path() coreiface.Path { + return &path{path: ipfspath.FromString(ipfspath.Join([]string{"/ipns", k.peerId}))} +} + +func (api *KeyAPI) Generate(ctx context.Context, name string, opts ...caopts.KeyGenerateOption) (coreiface.Key, error) { + options, err := caopts.KeyGenerateOptions(opts...) + if err != nil { + return nil, err + } + + if name == "self" { + return nil, fmt.Errorf("cannot overwrite key with name 'self'") + } + + _, err = api.node.Repo.Keystore().Get(name) + if err == nil { + return nil, fmt.Errorf("key with name '%s' already exists", name) + } + + var sk crypto.PrivKey + var pk crypto.PubKey + + switch options.Algorithm { + case "rsa": + if options.Size == -1 { + options.Size = caopts.DefaultRSALen + } + + priv, pub, err := crypto.GenerateKeyPairWithReader(crypto.RSA, options.Size, rand.Reader) + if err != nil { + return nil, err + } + + sk = priv + pk = pub + case "ed25519": + priv, pub, err := crypto.GenerateEd25519Key(rand.Reader) + if err != nil { + return nil, err + } + + sk = priv + pk = pub + default: + return nil, fmt.Errorf("unrecognized key type: %s", options.Algorithm) + } + + err = api.node.Repo.Keystore().Put(name, sk) + if err != nil { + return nil, err + } + + pid, err := peer.IDFromPublicKey(pk) + if err != nil { + return nil, err + } + + return &key{name, pid.Pretty()}, nil +} + +func (api *KeyAPI) List(ctx context.Context) ([]coreiface.Key, error) { + keys, err := api.node.Repo.Keystore().List() + if err != nil { + return nil, err + } + + sort.Strings(keys) + + out := make([]coreiface.Key, len(keys)+1) + out[0] = &key{"self", api.node.Identity.Pretty()} + + for n, k := range keys { + privKey, err := api.node.Repo.Keystore().Get(k) + if err != nil { + return nil, err + } + + pubKey := privKey.GetPublic() + + pid, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return nil, err + } + + out[n+1] = &key{k, pid.Pretty()} + } + return out, nil +} + +func (api *KeyAPI) Rename(ctx context.Context, oldName string, newName string, opts ...caopts.KeyRenameOption) (coreiface.Key, bool, error) { + options, err := caopts.KeyRenameOptions(opts...) + if err != nil { + return nil, false, err + } + + ks := api.node.Repo.Keystore() + + if oldName == "self" { + return nil, false, fmt.Errorf("cannot rename key with name 'self'") + } + + if newName == "self" { + return nil, false, fmt.Errorf("cannot overwrite key with name 'self'") + } + + oldKey, err := ks.Get(oldName) + if err != nil { + return nil, false, fmt.Errorf("no key named %s was found", oldName) + } + + pubKey := oldKey.GetPublic() + + pid, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return nil, false, err + } + + overwrite := false + if options.Force { + exist, err := ks.Has(newName) + if err != nil { + return nil, false, err + } + + if exist { + overwrite = true + err := ks.Delete(newName) + if err != nil { + return nil, false, err + } + } + } + + err = ks.Put(newName, oldKey) + if err != nil { + return nil, false, err + } + + return &key{newName, pid.Pretty()}, overwrite, ks.Delete(oldName) +} + +func (api *KeyAPI) Remove(ctx context.Context, name string) (coreiface.Path, error) { + ks := api.node.Repo.Keystore() + + if name == "self" { + return nil, fmt.Errorf("cannot remove key with name 'self'") + } + + removed, err := ks.Get(name) + if err != nil { + return nil, fmt.Errorf("no key named %s was found", name) + } + + pubKey := removed.GetPublic() + + pid, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return nil, err + } + + err = ks.Delete(name) + if err != nil { + return nil, err + } + + return (&key{"", pid.Pretty()}).Path(), nil +} + +func (api *KeyAPI) core() coreiface.CoreAPI { + return api.CoreAPI +} diff --git a/core/coreapi/key_test.go b/core/coreapi/key_test.go new file mode 100644 index 00000000000..7e1b23ac9d0 --- /dev/null +++ b/core/coreapi/key_test.go @@ -0,0 +1,416 @@ +package coreapi_test + +import ( + "context" + "strings" + "testing" + + opts "github.com/ipfs/go-ipfs/core/coreapi/interface/options" +) + +func TestListSelf(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Fatal(err) + return + } + + keys, err := api.Key().List(ctx) + if err != nil { + t.Fatalf("failed to list keys: %s", err) + return + } + + if len(keys) != 1 { + t.Fatalf("there should be 1 key (self), got %d", len(keys)) + return + } + + if keys[0].Name() != "self" { + t.Errorf("expected the key to be called 'self', got '%s'", keys[0].Name()) + } + + if keys[0].Path().String() != "/ipns/Qmfoo" { + t.Errorf("expected the key to have path '/ipns/Qmfoo', got '%s'", keys[0].Path().String()) + } +} + +func TestRenameSelf(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Fatal(err) + return + } + + _, _, err = api.Key().Rename(ctx, "self", "foo") + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "cannot rename key with name 'self'" { + t.Fatalf("expected error 'cannot rename key with name 'self'', got '%s'", err.Error()) + } + } + + _, _, err = api.Key().Rename(ctx, "self", "foo", api.Key().WithForce(true)) + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "cannot rename key with name 'self'" { + t.Fatalf("expected error 'cannot rename key with name 'self'', got '%s'", err.Error()) + } + } +} + +func TestRemoveSelf(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Fatal(err) + return + } + + _, err = api.Key().Remove(ctx, "self") + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "cannot remove key with name 'self'" { + t.Fatalf("expected error 'cannot remove key with name 'self'', got '%s'", err.Error()) + } + } +} + +func TestGenerate(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + k, err := api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + if k.Name() != "foo" { + t.Errorf("expected the key to be called 'foo', got '%s'", k.Name()) + } + + if !strings.HasPrefix(k.Path().String(), "/ipns/Qm") { + t.Errorf("expected the key to be prefixed with '/ipns/Qm', got '%s'", k.Path().String()) + } +} + +func TestGenerateSize(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + k, err := api.Key().Generate(ctx, "foo", api.Key().WithSize(1024)) + if err != nil { + t.Fatal(err) + return + } + + if k.Name() != "foo" { + t.Errorf("expected the key to be called 'foo', got '%s'", k.Name()) + } + + if !strings.HasPrefix(k.Path().String(), "/ipns/Qm") { + t.Errorf("expected the key to be prefixed with '/ipns/Qm', got '%s'", k.Path().String()) + } +} + +func TestGenerateType(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + k, err := api.Key().Generate(ctx, "bar", api.Key().WithType(opts.Ed25519Key)) + if err != nil { + t.Fatal(err) + return + } + + if k.Name() != "bar" { + t.Errorf("expected the key to be called 'foo', got '%s'", k.Name()) + } + + if !strings.HasPrefix(k.Path().String(), "/ipns/Qm") { + t.Errorf("expected the key to be prefixed with '/ipns/Qm', got '%s'", k.Path().String()) + } +} + +func TestGenerateExisting(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + _, err = api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + _, err = api.Key().Generate(ctx, "foo") + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "key with name 'foo' already exists" { + t.Fatalf("expected error 'key with name 'foo' already exists', got '%s'", err.Error()) + } + } + + _, err = api.Key().Generate(ctx, "self") + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "cannot overwrite key with name 'self'" { + t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error()) + } + } +} + +func TestList(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + _, err = api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + l, err := api.Key().List(ctx) + if err != nil { + t.Fatal(err) + return + } + + if len(l) != 2 { + t.Fatalf("expected to get 2 keys, got %d", len(l)) + return + } + + if l[0].Name() != "self" { + t.Fatalf("expected key 0 to be called 'self', got '%s'", l[0].Name()) + return + } + + if l[1].Name() != "foo" { + t.Fatalf("expected key 1 to be called 'foo', got '%s'", l[1].Name()) + return + } + + if !strings.HasPrefix(l[0].Path().String(), "/ipns/Qm") { + t.Fatalf("expected key 0 to be prefixed with '/ipns/Qm', got '%s'", l[0].Name()) + return + } + + if !strings.HasPrefix(l[1].Path().String(), "/ipns/Qm") { + t.Fatalf("expected key 1 to be prefixed with '/ipns/Qm', got '%s'", l[1].Name()) + return + } +} + +func TestRename(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + _, err = api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + k, overwrote, err := api.Key().Rename(ctx, "foo", "bar") + if err != nil { + t.Fatal(err) + return + } + + if overwrote { + t.Error("overwrote should be false") + } + + if k.Name() != "bar" { + t.Errorf("returned key should be called 'bar', got '%s'", k.Name()) + } +} + +func TestRenameToSelf(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + _, err = api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + _, _, err = api.Key().Rename(ctx, "foo", "self") + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "cannot overwrite key with name 'self'" { + t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error()) + } + } +} + +func TestRenameToSelfForce(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + _, err = api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + _, _, err = api.Key().Rename(ctx, "foo", "self", api.Key().WithForce(true)) + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "cannot overwrite key with name 'self'" { + t.Fatalf("expected error 'cannot overwrite key with name 'self'', got '%s'", err.Error()) + } + } +} + +func TestRenameOverwriteNoForce(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + _, err = api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + _, err = api.Key().Generate(ctx, "bar") + if err != nil { + t.Fatal(err) + return + } + + _, _, err = api.Key().Rename(ctx, "foo", "bar") + if err == nil { + t.Error("expected error to not be nil") + } else { + if err.Error() != "key by that name already exists, refusing to overwrite" { + t.Fatalf("expected error 'key by that name already exists, refusing to overwrite', got '%s'", err.Error()) + } + } +} + +func TestRenameOverwrite(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + kfoo, err := api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + _, err = api.Key().Generate(ctx, "bar") + if err != nil { + t.Fatal(err) + return + } + + k, overwrote, err := api.Key().Rename(ctx, "foo", "bar", api.Key().WithForce(true)) + if err != nil { + t.Fatal(err) + return + } + + if !overwrote { + t.Error("overwrote should be true") + } + + if k.Name() != "bar" { + t.Errorf("returned key should be called 'bar', got '%s'", k.Name()) + } + + if k.Path().String() != kfoo.Path().String() { + t.Errorf("k and kfoo should have equal paths, '%s'!='%s'", k.Path().String(), kfoo.Path().String()) + } +} + +func TestRemove(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPI(ctx) + if err != nil { + t.Error(err) + } + + k, err := api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + l, err := api.Key().List(ctx) + if err != nil { + t.Fatal(err) + return + } + + if len(l) != 2 { + t.Fatalf("expected to get 2 keys, got %d", len(l)) + return + } + + p, err := api.Key().Remove(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + if k.Path().String() != p.String() { + t.Errorf("k and p should have equal paths, '%s'!='%s'", k.Path().String(), p.String()) + } + + l, err = api.Key().List(ctx) + if err != nil { + t.Fatal(err) + return + } + + if len(l) != 1 { + t.Fatalf("expected to get 1 key, got %d", len(l)) + return + } + + if l[0].Name() != "self" { + t.Errorf("expected the key to be called 'self', got '%s'", l[0].Name()) + } +} diff --git a/core/coreapi/name.go b/core/coreapi/name.go new file mode 100644 index 00000000000..fd61d250df4 --- /dev/null +++ b/core/coreapi/name.go @@ -0,0 +1,170 @@ +package coreapi + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + core "github.com/ipfs/go-ipfs/core" + coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface" + caopts "github.com/ipfs/go-ipfs/core/coreapi/interface/options" + keystore "github.com/ipfs/go-ipfs/keystore" + namesys "github.com/ipfs/go-ipfs/namesys" + ipath "github.com/ipfs/go-ipfs/path" + offline "github.com/ipfs/go-ipfs/routing/offline" + + peer "gx/ipfs/QmWNY7dV54ZDYmTA1ykVdwNCqC11mpU4zSUp6XDpLTH9eG/go-libp2p-peer" + crypto "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto" +) + +type NameAPI struct { + *CoreAPI + *caopts.NameOptions +} + +type ipnsEntry struct { + name string + value coreiface.Path +} + +func (e *ipnsEntry) Name() string { + return e.name +} + +func (e *ipnsEntry) Value() coreiface.Path { + return e.value +} + +func (api *NameAPI) Publish(ctx context.Context, p coreiface.Path, opts ...caopts.NamePublishOption) (coreiface.IpnsEntry, error) { + options, err := caopts.NamePublishOptions(opts...) + if err != nil { + return nil, err + } + n := api.node + + if !n.OnlineMode() { + err := n.SetupOfflineRouting() + if err != nil { + return nil, err + } + } + + if n.Mounts.Ipns != nil && n.Mounts.Ipns.IsActive() { + return nil, errors.New("cannot manually publish while IPNS is mounted") + } + + pth, err := ipath.ParsePath(p.String()) + if err != nil { + return nil, err + } + + k, err := keylookup(n, options.Key) + if err != nil { + return nil, err + } + + eol := time.Now().Add(options.ValidTime) + err = n.Namesys.PublishWithEOL(ctx, k, pth, eol) + if err != nil { + return nil, err + } + + pid, err := peer.IDFromPrivateKey(k) + if err != nil { + return nil, err + } + + return &ipnsEntry{ + name: pid.Pretty(), + value: p, + }, nil +} + +func (api *NameAPI) Resolve(ctx context.Context, name string, opts ...caopts.NameResolveOption) (coreiface.Path, error) { + options, err := caopts.NameResolveOptions(opts...) + if err != nil { + return nil, err + } + + n := api.node + + if !n.OnlineMode() { + err := n.SetupOfflineRouting() + if err != nil { + return nil, err + } + } + + var resolver namesys.Resolver = n.Namesys + + if options.Local && !options.Cache { + return nil, errors.New("cannot specify both local and nocache") + } + + if options.Local { + offroute := offline.NewOfflineRouter(n.Repo.Datastore(), n.PrivateKey) + resolver = namesys.NewRoutingResolver(offroute, 0) + } + + if !options.Cache { + resolver = namesys.NewNameSystem(n.Routing, n.Repo.Datastore(), 0) + } + + depth := 1 + if options.Recursive { + depth = namesys.DefaultDepthLimit + } + + if !strings.HasPrefix(name, "/ipns/") { + name = "/ipns/" + name + } + + output, err := resolver.ResolveN(ctx, name, depth) + if err != nil { + return nil, err + } + + return &path{path: output}, nil +} + +func (api *NameAPI) core() coreiface.CoreAPI { + return api.CoreAPI +} + +func keylookup(n *core.IpfsNode, k string) (crypto.PrivKey, error) { + res, err := n.GetKey(k) + if res != nil { + return res, nil + } + + if err != nil && err != keystore.ErrNoSuchKey { + return nil, err + } + + keys, err := n.Repo.Keystore().List() + if err != nil { + return nil, err + } + + for _, key := range keys { + privKey, err := n.Repo.Keystore().Get(key) + if err != nil { + return nil, err + } + + pubKey := privKey.GetPublic() + + pid, err := peer.IDFromPublicKey(pubKey) + if err != nil { + return nil, err + } + + if pid.Pretty() == k { + return privKey, nil + } + } + + return nil, fmt.Errorf("no key by the given name or PeerID was found") +} diff --git a/core/coreapi/name_test.go b/core/coreapi/name_test.go new file mode 100644 index 00000000000..74fdacda41b --- /dev/null +++ b/core/coreapi/name_test.go @@ -0,0 +1,144 @@ +package coreapi_test + +import ( + "context" + "io" + "math/rand" + "testing" + "time" + + ipath "github.com/ipfs/go-ipfs/path" + + coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface" +) + +var rnd = rand.New(rand.NewSource(0x62796532303137)) + +func addTestObject(ctx context.Context, api coreiface.CoreAPI) (coreiface.Path, error) { + return api.Unixfs().Add(ctx, &io.LimitedReader{R: rnd, N: 4092}) +} + +func TestBasicPublishResolve(t *testing.T) { + ctx := context.Background() + n, api, err := makeAPIIdent(ctx, true) + if err != nil { + t.Fatal(err) + return + } + + p, err := addTestObject(ctx, api) + if err != nil { + t.Fatal(err) + return + } + + e, err := api.Name().Publish(ctx, p) + if err != nil { + t.Fatal(err) + return + } + + if e.Name() != n.Identity.Pretty() { + t.Errorf("expected e.Name to equal '%s', got '%s'", n.Identity.Pretty(), e.Name()) + } + + if e.Value().String() != p.String() { + t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) + } + + resPath, err := api.Name().Resolve(ctx, e.Name()) + if err != nil { + t.Fatal(err) + return + } + + if resPath.String() != p.String() { + t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()) + } +} + +func TestBasicPublishResolveKey(t *testing.T) { + ctx := context.Background() + _, api, err := makeAPIIdent(ctx, true) + if err != nil { + t.Fatal(err) + return + } + + k, err := api.Key().Generate(ctx, "foo") + if err != nil { + t.Fatal(err) + return + } + + p, err := addTestObject(ctx, api) + if err != nil { + t.Fatal(err) + return + } + + e, err := api.Name().Publish(ctx, p, api.Name().WithKey(k.Name())) + if err != nil { + t.Fatal(err) + return + } + + if ipath.Join([]string{"/ipns", e.Name()}) != k.Path().String() { + t.Errorf("expected e.Name to equal '%s', got '%s'", e.Name(), k.Path().String()) + } + + if e.Value().String() != p.String() { + t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) + } + + resPath, err := api.Name().Resolve(ctx, e.Name()) + if err != nil { + t.Fatal(err) + return + } + + if resPath.String() != p.String() { + t.Errorf("expected paths to match, '%s'!='%s'", resPath.String(), p.String()) + } +} + +func TestBasicPublishResolveTimeout(t *testing.T) { + t.Skip("ValidTime doesn't appear to work at this time resolution") + + ctx := context.Background() + n, api, err := makeAPIIdent(ctx, true) + if err != nil { + t.Fatal(err) + return + } + + p, err := addTestObject(ctx, api) + if err != nil { + t.Fatal(err) + return + } + + e, err := api.Name().Publish(ctx, p, api.Name().WithValidTime(time.Millisecond*100)) + if err != nil { + t.Fatal(err) + return + } + + if e.Name() != n.Identity.Pretty() { + t.Errorf("expected e.Name to equal '%s', got '%s'", n.Identity.Pretty(), e.Name()) + } + + if e.Value().String() != p.String() { + t.Errorf("expected paths to match, '%s'!='%s'", e.Value().String(), p.String()) + } + + time.Sleep(time.Second) + + _, err = api.Name().Resolve(ctx, e.Name()) + if err == nil { + t.Fatal("Expected an error") + return + } +} + +//TODO: When swarm api is created, add multinode tests diff --git a/core/coreapi/unixfs_test.go b/core/coreapi/unixfs_test.go index 01beccc20b4..c4b00e4b650 100644 --- a/core/coreapi/unixfs_test.go +++ b/core/coreapi/unixfs_test.go @@ -3,6 +3,7 @@ package coreapi_test import ( "bytes" "context" + "encoding/base64" "io" "math" "strings" @@ -12,12 +13,15 @@ import ( coreapi "github.com/ipfs/go-ipfs/core/coreapi" coreiface "github.com/ipfs/go-ipfs/core/coreapi/interface" coreunix "github.com/ipfs/go-ipfs/core/coreunix" + keystore "github.com/ipfs/go-ipfs/keystore" mdag "github.com/ipfs/go-ipfs/merkledag" repo "github.com/ipfs/go-ipfs/repo" config "github.com/ipfs/go-ipfs/repo/config" ds2 "github.com/ipfs/go-ipfs/thirdparty/datastore2" unixfs "github.com/ipfs/go-ipfs/unixfs" + peer "gx/ipfs/QmWNY7dV54ZDYmTA1ykVdwNCqC11mpU4zSUp6XDpLTH9eG/go-libp2p-peer" + ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto" cbor "gx/ipfs/QmeZv9VXw2SfVbX55LV6kGTWASKBc9ZxAVqGBeJcDGdoXy/go-ipld-cbor" ) @@ -31,14 +35,40 @@ var emptyDir = coreapi.ResolvedPath("/ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbs // `echo -n | ipfs add` var emptyFile = coreapi.ResolvedPath("/ipfs/QmbFMke1KXqnYyBBWxB74N4c5SBnJMVAiMNRcGu6x1AwQH", nil, nil) -func makeAPI(ctx context.Context) (*core.IpfsNode, coreiface.CoreAPI, error) { +func makeAPIIdent(ctx context.Context, fullIdentity bool) (*core.IpfsNode, coreiface.CoreAPI, error) { + var ident config.Identity + if fullIdentity { + sk, pk, err := ci.GenerateKeyPair(ci.RSA, 512) + if err != nil { + return nil, nil, err + } + + id, err := peer.IDFromPublicKey(pk) + if err != nil { + return nil, nil, err + } + + kbytes, err := sk.Bytes() + if err != nil { + return nil, nil, err + } + + ident = config.Identity{ + PeerID: id.Pretty(), + PrivKey: base64.StdEncoding.EncodeToString(kbytes), + } + } else { + ident = config.Identity{ + PeerID: "Qmfoo", + } + } + r := &repo.Mock{ C: config.Config{ - Identity: config.Identity{ - PeerID: "Qmfoo", // required by offline node - }, + Identity: ident, }, D: ds2.ThreadSafeCloserMapDatastore(), + K: keystore.NewMemKeystore(), } node, err := core.NewNode(ctx, &core.BuildCfg{Repo: r}) if err != nil { @@ -48,6 +78,10 @@ func makeAPI(ctx context.Context) (*core.IpfsNode, coreiface.CoreAPI, error) { return node, api, nil } +func makeAPI(ctx context.Context) (*core.IpfsNode, coreiface.CoreAPI, error) { + return makeAPIIdent(ctx, false) +} + func TestAdd(t *testing.T) { ctx := context.Background() _, api, err := makeAPI(ctx) diff --git a/repo/mock.go b/repo/mock.go index 030c2ff28d1..8b72d92ddfc 100644 --- a/repo/mock.go +++ b/repo/mock.go @@ -44,7 +44,7 @@ func (m *Mock) Close() error { return errTODO } func (m *Mock) SetAPIAddr(addr ma.Multiaddr) error { return errTODO } -func (m *Mock) Keystore() keystore.Keystore { return nil } +func (m *Mock) Keystore() keystore.Keystore { return m.K } func (m *Mock) SwarmKey() ([]byte, error) { return nil, nil