package namesys import ( "bytes" "context" "errors" "fmt" "time" dag "github.com/ipfs/go-ipfs/merkledag" pb "github.com/ipfs/go-ipfs/namesys/pb" path "github.com/ipfs/go-ipfs/path" pin "github.com/ipfs/go-ipfs/pin" dshelp "github.com/ipfs/go-ipfs/thirdparty/ds-help" ft "github.com/ipfs/go-ipfs/unixfs" ds "gx/ipfs/QmRWDav6mzWseLWeYfVd5fvUKiVe9xNH29YfMF438fG364/go-datastore" proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto" u "gx/ipfs/Qmb912gdngC1UWwTkhuW8knyRbcWeu5kqkxBpveLmW8bSr/go-ipfs-util" routing "gx/ipfs/QmbkGVaN9W6RYJK4Ws5FvMKXKDqdRQ5snhtaa92qP6L8eU/go-libp2p-routing" record "gx/ipfs/QmdM4ohF7cr4MvAECVeD3hRA3HtZrk1ngaek4n8ojVT87h/go-libp2p-record" dhtpb "gx/ipfs/QmdM4ohF7cr4MvAECVeD3hRA3HtZrk1ngaek4n8ojVT87h/go-libp2p-record/pb" peer "gx/ipfs/QmfMmLGoKzCHDN7cGgk64PJr4iipzidDRME8HABSJqvmhC/go-libp2p-peer" ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto" ) // ErrExpiredRecord should be returned when an ipns record is // invalid due to being too old var ErrExpiredRecord = errors.New("expired record") // ErrUnrecognizedValidity is returned when an IpnsRecord has an // unknown validity type. var ErrUnrecognizedValidity = errors.New("unrecognized validity type") var PublishPutValTimeout = time.Minute var DefaultRecortTTL = 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 } // NewRoutingPublisher constructs a publisher for the IPFS Routing name system. func NewRoutingPublisher(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(DefaultRecortTTL)) } // 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 { id, err := peer.IDFromPrivateKey(k) if err != nil { return err } _, ipnskey := IpnsKeysForID(id) // get previous records sequence number seqnum, err := p.getPreviousSeqNo(ctx, ipnskey) if err != nil { return err } // increment it seqnum++ return PutRecordToRouting(ctx, k, value, seqnum, eol, p.routing, id) } func (p *ipnsPublisher) getPreviousSeqNo(ctx context.Context, ipnskey string) (uint64, error) { prevrec, err := p.ds.Get(dshelp.NewKeyFromBinary([]byte(ipnskey))) if err != nil && err != ds.ErrNotFound { // None found, lets start at zero! return 0, err } var val []byte if err == nil { prbytes, ok := prevrec.([]byte) if !ok { return 0, fmt.Errorf("unexpected type returned from datastore: %#v", prevrec) } dhtrec := new(dhtpb.Record) err := proto.Unmarshal(prbytes, dhtrec) if err != nil { return 0, err } val = dhtrec.GetValue() } else { // try and check the dht for a record ctx, cancel := context.WithTimeout(ctx, time.Second*30) defer cancel() rv, err := p.routing.GetValue(ctx, ipnskey) if err != nil { // no such record found, start at zero! return 0, nil } val = rv } e := new(pb.IpnsEntry) err = proto.Unmarshal(val, e) if err != nil { return 0, err } return e.GetSequence(), nil } // 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, k ci.PrivKey, value path.Path, seqnum uint64, eol time.Time, r routing.ValueStore, id peer.ID) error { ctx, cancel := context.WithCancel(ctx) defer cancel() namekey, ipnskey := IpnsKeysForID(id) entry, err := CreateRoutingEntryData(k, value, seqnum, eol) if err != nil { return err } ttl, ok := checkCtxTTL(ctx) if ok { entry.Ttl = proto.Uint64(uint64(ttl.Nanoseconds())) } errs := make(chan error, 2) go func() { errs <- PublishEntry(ctx, r, ipnskey, entry) }() go func() { errs <- PublishPublicKey(ctx, r, namekey, k.GetPublic()) }() err = waitOnErrChan(ctx, errs) if err != nil { return err } err = waitOnErrChan(ctx, errs) if err != nil { return err } return nil } 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() err = r.PutValue(timectx, k, pkbytes) if err != nil { return err } return nil } 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/"+b58(h(pubkey)) if err := r.PutValue(timectx, ipnskey, data); err != nil { return err } return nil } func CreateRoutingEntryData(pk ci.PrivKey, val path.Path, seq uint64, eol time.Time) (*pb.IpnsEntry, error) { entry := new(pb.IpnsEntry) entry.Value = []byte(val) typ := pb.IpnsEntry_EOL entry.ValidityType = &typ entry.Sequence = proto.Uint64(seq) entry.Validity = []byte(u.FormatRFC3339(eol)) sig, err := pk.Sign(ipnsEntryDataForSig(entry)) if err != nil { return nil, err } entry.Signature = sig return entry, nil } func ipnsEntryDataForSig(e *pb.IpnsEntry) []byte { return bytes.Join([][]byte{ e.Value, e.Validity, []byte(fmt.Sprint(e.GetValidityType())), }, []byte{}) } var IpnsRecordValidator = &record.ValidChecker{ Func: ValidateIpnsRecord, Sign: true, } func IpnsSelectorFunc(k string, vals [][]byte) (int, error) { var recs []*pb.IpnsEntry for _, v := range vals { e := new(pb.IpnsEntry) err := proto.Unmarshal(v, e) if err == nil { recs = append(recs, e) } else { recs = append(recs, nil) } } return selectRecord(recs, vals) } func selectRecord(recs []*pb.IpnsEntry, vals [][]byte) (int, error) { var best_seq uint64 best_i := -1 for i, r := range recs { if r == nil || r.GetSequence() < best_seq { continue } if best_i == -1 || r.GetSequence() > best_seq { best_seq = r.GetSequence() best_i = i } else if r.GetSequence() == best_seq { rt, err := u.ParseRFC3339(string(r.GetValidity())) if err != nil { continue } bestt, err := u.ParseRFC3339(string(recs[best_i].GetValidity())) if err != nil { continue } if rt.After(bestt) { best_i = i } else if rt == bestt { if bytes.Compare(vals[i], vals[best_i]) > 0 { best_i = i } } } } if best_i == -1 { return 0, errors.New("no usable records in given set") } return best_i, nil } // ValidateIpnsRecord implements ValidatorFunc and verifies that the // given 'val' is an IpnsEntry and that that entry is valid. func ValidateIpnsRecord(k string, val []byte) error { entry := new(pb.IpnsEntry) err := proto.Unmarshal(val, entry) if err != nil { return err } switch entry.GetValidityType() { case pb.IpnsEntry_EOL: t, err := u.ParseRFC3339(string(entry.GetValidity())) if err != nil { log.Debug("failed parsing time for ipns record EOL") return err } if time.Now().After(t) { return ErrExpiredRecord } default: return ErrUnrecognizedValidity } return nil } // 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, ds dag.DAGService, pub Publisher, pins pin.Pinner, key ci.PrivKey) error { emptyDir := ft.EmptyDirNode() nodek, err := ds.Add(emptyDir) if err != nil { return err } // 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 } err = pub.Publish(ctx, key, path.FromCid(nodek)) if err != nil { return err } return nil } func IpnsKeysForID(id peer.ID) (name, ipns string) { namekey := "/pk/" + string(id) ipnskey := "/ipns/" + string(id) return namekey, ipnskey }