forked from cerc-io/ipld-eth-server
313 lines
8.2 KiB
Go
313 lines
8.2 KiB
Go
package namesys
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
pin "github.com/ipfs/go-ipfs/pin"
|
|
|
|
proto "github.com/gogo/protobuf/proto"
|
|
ds "github.com/ipfs/go-datastore"
|
|
dsquery "github.com/ipfs/go-datastore/query"
|
|
ipns "github.com/ipfs/go-ipns"
|
|
pb "github.com/ipfs/go-ipns/pb"
|
|
path "github.com/ipfs/go-path"
|
|
ft "github.com/ipfs/go-unixfs"
|
|
ci "github.com/libp2p/go-libp2p-crypto"
|
|
peer "github.com/libp2p/go-libp2p-peer"
|
|
routing "github.com/libp2p/go-libp2p-routing"
|
|
base32 "github.com/whyrusleeping/base32"
|
|
)
|
|
|
|
const ipnsPrefix = "/ipns/"
|
|
|
|
const PublishPutValTimeout = time.Minute
|
|
const DefaultRecordEOL = 24 * time.Hour
|
|
|
|
// IpnsPublisher is capable of publishing and resolving names to the IPFS
|
|
// routing system.
|
|
type IpnsPublisher struct {
|
|
routing routing.ValueStore
|
|
ds ds.Datastore
|
|
|
|
// Used to ensure we assign IPNS records *sequential* sequence numbers.
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// NewIpnsPublisher constructs a publisher for the IPFS Routing name system.
|
|
func NewIpnsPublisher(route routing.ValueStore, ds ds.Datastore) *IpnsPublisher {
|
|
if ds == nil {
|
|
panic("nil datastore")
|
|
}
|
|
return &IpnsPublisher{routing: route, ds: ds}
|
|
}
|
|
|
|
// 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("Publish %s", value)
|
|
return p.PublishWithEOL(ctx, k, value, time.Now().Add(DefaultRecordEOL))
|
|
}
|
|
|
|
func IpnsDsKey(id peer.ID) ds.Key {
|
|
return ds.NewKey("/ipns/" + base32.RawStdEncoding.EncodeToString([]byte(id)))
|
|
}
|
|
|
|
// PublishedNames returns the latest IPNS records published by this node and
|
|
// their expiration times.
|
|
//
|
|
// This method will not search the routing system for records published by other
|
|
// nodes.
|
|
func (p *IpnsPublisher) ListPublished(ctx context.Context) (map[peer.ID]*pb.IpnsEntry, error) {
|
|
query, err := p.ds.Query(dsquery.Query{
|
|
Prefix: ipnsPrefix,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer query.Close()
|
|
|
|
records := make(map[peer.ID]*pb.IpnsEntry)
|
|
for {
|
|
select {
|
|
case result, ok := <-query.Next():
|
|
if !ok {
|
|
return records, nil
|
|
}
|
|
if result.Error != nil {
|
|
return nil, result.Error
|
|
}
|
|
e := new(pb.IpnsEntry)
|
|
if err := proto.Unmarshal(result.Value, e); err != nil {
|
|
// Might as well return what we can.
|
|
log.Error("found an invalid IPNS entry:", err)
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(result.Key, ipnsPrefix) {
|
|
log.Errorf("datastore query for keys with prefix %s returned a key: %s", ipnsPrefix, result.Key)
|
|
continue
|
|
}
|
|
k := result.Key[len(ipnsPrefix):]
|
|
pid, err := base32.RawStdEncoding.DecodeString(k)
|
|
if err != nil {
|
|
log.Errorf("ipns ds key invalid: %s", result.Key)
|
|
continue
|
|
}
|
|
records[peer.ID(pid)] = e
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetPublished returns the record this node has published corresponding to the
|
|
// given peer ID.
|
|
//
|
|
// If `checkRouting` is true and we have no existing record, this method will
|
|
// check the routing system for any existing records.
|
|
func (p *IpnsPublisher) GetPublished(ctx context.Context, id peer.ID, checkRouting bool) (*pb.IpnsEntry, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
|
defer cancel()
|
|
|
|
value, err := p.ds.Get(IpnsDsKey(id))
|
|
switch err {
|
|
case nil:
|
|
case ds.ErrNotFound:
|
|
if !checkRouting {
|
|
return nil, nil
|
|
}
|
|
ipnskey := ipns.RecordKey(id)
|
|
value, err = p.routing.GetValue(ctx, ipnskey)
|
|
if err != nil {
|
|
// Not found or other network issue. Can't really do
|
|
// anything about this case.
|
|
if err != routing.ErrNotFound {
|
|
log.Debugf("error when determining the last published IPNS record for %s: %s", id, err)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
default:
|
|
return nil, err
|
|
}
|
|
e := new(pb.IpnsEntry)
|
|
if err := proto.Unmarshal(value, e); err != nil {
|
|
return nil, err
|
|
}
|
|
return e, nil
|
|
}
|
|
|
|
func (p *IpnsPublisher) updateRecord(ctx context.Context, k ci.PrivKey, value path.Path, eol time.Time) (*pb.IpnsEntry, error) {
|
|
id, err := peer.IDFromPrivateKey(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
// get previous records sequence number
|
|
rec, err := p.GetPublished(ctx, id, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
seqno := rec.GetSequence() // returns 0 if rec is nil
|
|
if rec != nil && value != path.Path(rec.GetValue()) {
|
|
// Don't bother incrementing the sequence number unless the
|
|
// value changes.
|
|
seqno++
|
|
}
|
|
|
|
// Create record
|
|
entry, err := ipns.Create(k, []byte(value), seqno, eol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set the TTL
|
|
// TODO: Make this less hacky.
|
|
ttl, ok := checkCtxTTL(ctx)
|
|
if ok {
|
|
entry.Ttl = proto.Uint64(uint64(ttl.Nanoseconds()))
|
|
}
|
|
|
|
data, err := proto.Marshal(entry)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Put the new record.
|
|
if err := p.ds.Put(IpnsDsKey(id), data); err != nil {
|
|
return nil, err
|
|
}
|
|
return entry, nil
|
|
}
|
|
|
|
// PublishWithEOL is a temporary stand in for the ipns records implementation
|
|
// see here for more details: https://github.com/ipfs/specs/tree/master/records
|
|
func (p *IpnsPublisher) PublishWithEOL(ctx context.Context, k ci.PrivKey, value path.Path, eol time.Time) error {
|
|
record, err := p.updateRecord(ctx, k, value, eol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return PutRecordToRouting(ctx, p.routing, k.GetPublic(), record)
|
|
}
|
|
|
|
// setting the TTL on published records is an experimental feature.
|
|
// as such, i'm using the context to wire it through to avoid changing too
|
|
// much code along the way.
|
|
func checkCtxTTL(ctx context.Context) (time.Duration, bool) {
|
|
v := ctx.Value("ipns-publish-ttl")
|
|
if v == nil {
|
|
return 0, false
|
|
}
|
|
|
|
d, ok := v.(time.Duration)
|
|
return d, ok
|
|
}
|
|
|
|
func PutRecordToRouting(ctx context.Context, r routing.ValueStore, k ci.PubKey, entry *pb.IpnsEntry) error {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
|
|
errs := make(chan error, 2) // At most two errors (IPNS, and public key)
|
|
|
|
if err := ipns.EmbedPublicKey(k, entry); err != nil {
|
|
return err
|
|
}
|
|
|
|
id, err := peer.IDFromPublicKey(k)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
go func() {
|
|
errs <- PublishEntry(ctx, r, ipns.RecordKey(id), entry)
|
|
}()
|
|
|
|
// Publish the public key if a public key cannot be extracted from the ID
|
|
// TODO: once v0.4.16 is widespread enough, we can stop doing this
|
|
// and at that point we can even deprecate the /pk/ namespace in the dht
|
|
//
|
|
// NOTE: This check actually checks if the public key has been embedded
|
|
// in the IPNS entry. This check is sufficient because we embed the
|
|
// public key in the IPNS entry if it can't be extracted from the ID.
|
|
if entry.PubKey != nil {
|
|
go func() {
|
|
errs <- PublishPublicKey(ctx, r, PkKeyForID(id), k)
|
|
}()
|
|
|
|
if err := waitOnErrChan(ctx, errs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return waitOnErrChan(ctx, errs)
|
|
}
|
|
|
|
func waitOnErrChan(ctx context.Context, errs chan error) error {
|
|
select {
|
|
case err := <-errs:
|
|
return err
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
func PublishPublicKey(ctx context.Context, r routing.ValueStore, k string, pubk ci.PubKey) error {
|
|
log.Debugf("Storing pubkey at: %s", k)
|
|
pkbytes, err := pubk.Bytes()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store associated public key
|
|
timectx, cancel := context.WithTimeout(ctx, PublishPutValTimeout)
|
|
defer cancel()
|
|
return r.PutValue(timectx, k, pkbytes)
|
|
}
|
|
|
|
func PublishEntry(ctx context.Context, r routing.ValueStore, ipnskey string, rec *pb.IpnsEntry) error {
|
|
timectx, cancel := context.WithTimeout(ctx, PublishPutValTimeout)
|
|
defer cancel()
|
|
|
|
data, err := proto.Marshal(rec)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debugf("Storing ipns entry at: %s", ipnskey)
|
|
// Store ipns entry at "/ipns/"+h(pubkey)
|
|
return r.PutValue(timectx, ipnskey, data)
|
|
}
|
|
|
|
// InitializeKeyspace sets the ipns record for the given key to
|
|
// point to an empty directory.
|
|
// TODO: this doesnt feel like it belongs here
|
|
func InitializeKeyspace(ctx context.Context, pub Publisher, pins pin.Pinner, key ci.PrivKey) error {
|
|
emptyDir := ft.EmptyDirNode()
|
|
|
|
// pin recursively because this might already be pinned
|
|
// and doing a direct pin would throw an error in that case
|
|
err := pins.Pin(ctx, emptyDir, true)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = pins.Flush()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return pub.Publish(ctx, key, path.FromCid(emptyDir.Cid()))
|
|
}
|
|
|
|
// PkKeyForID returns the public key routing key for the given peer ID.
|
|
func PkKeyForID(id peer.ID) string {
|
|
return "/pk/" + string(id)
|
|
}
|