1
+package namesys
2
+
3
+import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "strings"
8
+ "sync"
9
+ "time"
10
+
11
+ pb "github.com/ipfs/go-ipfs/namesys/pb"
12
+ path "github.com/ipfs/go-ipfs/path"
13
+ dshelp "github.com/ipfs/go-ipfs/thirdparty/ds-help"
14
+
15
+ cid "gx/ipfs/QmNp85zy9RLrQ5oQD4hPyS39ezrrXpcaa7R4Y9kxdWQLLQ/go-cid"
16
+ routing "gx/ipfs/QmPR2JzfKd9poHx9XBhzoFeBBC31ZM3W5iUPKJZWyaoZZm/go-libp2p-routing"
17
+ pstore "gx/ipfs/QmPgDWmTmuzvP7QE5zwo1TmjbJme9pmZHNujB2453jkCTr/go-libp2p-peerstore"
18
+ p2phost "gx/ipfs/QmRS46AyqtpJBsf1zmQdeizSDEzo1qkWR7rdEuPFAv8237/go-libp2p-host"
19
+ u "gx/ipfs/QmSU6eubNdhXjFBJBSksTp8kv8YRub8mGAPv8tVJHmL2EU/go-ipfs-util"
20
+ mh "gx/ipfs/QmU9a9NV9RdPNwZQDYd5uKsm6N6LJLSvLbywDDYFbaaC6P/go-multihash"
21
+ floodsub "gx/ipfs/QmVNv1WV6XxzQV4MBuiLX5729wMazaf8TNzm2Sq6ejyHh7/go-libp2p-floodsub"
22
+ ds "gx/ipfs/QmVSase1JP7cq9QkPT46oNwdp9pT6kBkG3oqS14y3QcZjG/go-datastore"
23
+ dssync "gx/ipfs/QmVSase1JP7cq9QkPT46oNwdp9pT6kBkG3oqS14y3QcZjG/go-datastore/sync"
24
+ peer "gx/ipfs/QmXYjuNuxVzXKJCfWasQk1RqkhVLDM9jtUKhqc2WPQmFSB/go-libp2p-peer"
25
+ proto "gx/ipfs/QmZ4Qi3GaRbjcx28Sme5eMH7RQjGkt8wHxt2a65oLaeFEV/gogo-protobuf/proto"
26
+ ci "gx/ipfs/QmaPbCnUMBohSGo3KnxEa2bHqyJVVeEEcwtqJAYxerieBo/go-libp2p-crypto"
27
+ record "gx/ipfs/QmbxkgUceEcuSZ4ZdBA3x74VUDSSYjHYmmeEqkjxbtZ6Jg/go-libp2p-record"
28
+ dhtpb "gx/ipfs/QmbxkgUceEcuSZ4ZdBA3x74VUDSSYjHYmmeEqkjxbtZ6Jg/go-libp2p-record/pb"
29
+)
30
+
31
+// PubsubPublisher is a publisher that distributes IPNS records through pubsub
32
+type PubsubPublisher struct {
33
+ ctx context.Context
34
+ ds ds.Datastore
35
+ host p2phost.Host
36
+ cr routing.ContentRouting
37
+ ps *floodsub.PubSub
38
+
39
+ mx sync.Mutex
40
+ subs map[string]struct{}
41
+}
42
+
43
+// PubsubResolver is a resolver that receives IPNS records through pubsub
44
+type PubsubResolver struct {
45
+ ctx context.Context
46
+ ds ds.Datastore
47
+ host p2phost.Host
48
+ cr routing.ContentRouting
49
+ pkf routing.PubKeyFetcher
50
+ ps *floodsub.PubSub
51
+
52
+ mx sync.Mutex
53
+ subs map[string]*floodsub.Subscription
54
+}
55
+
56
+// NewPubsubPublisher constructs a new Publisher that publishes IPNS records through pubsub.
57
+// The constructor interface is complicated by the need to bootstrap the pubsub topic.
58
+// This could be greatly simplified if the pubsub implementation handled bootstrap itself
59
+func NewPubsubPublisher(ctx context.Context, host p2phost.Host, ds ds.Datastore, cr routing.ContentRouting, ps *floodsub.PubSub) *PubsubPublisher {
60
+ return &PubsubPublisher{
61
+ ctx: ctx,
62
+ ds: ds,
63
+ host: host, // needed for pubsub bootstrap
64
+ cr: cr, // needed for pubsub bootstrap
65
+ ps: ps,
66
+ subs: make(map[string]struct{}),
67
+ }
68
+}
69
+
70
+// NewPubsubResolver constructs a new Resolver that resolves IPNS records through pubsub.
71
+// same as above for pubsub bootstrap dependencies
72
+func NewPubsubResolver(ctx context.Context, host p2phost.Host, cr routing.ContentRouting, pkf routing.PubKeyFetcher, ps *floodsub.PubSub) *PubsubResolver {
73
+ return &PubsubResolver{
74
+ ctx: ctx,
75
+ ds: dssync.MutexWrap(ds.NewMapDatastore()),
76
+ host: host, // needed for pubsub bootstrap
77
+ cr: cr, // needed for pubsub bootstrap
78
+ pkf: pkf,
79
+ ps: ps,
80
+ subs: make(map[string]*floodsub.Subscription),
81
+ }
82
+}
83
+
84
+// Publish publishes an IPNS record through pubsub with default TTL
85
+func (p *PubsubPublisher) Publish(ctx context.Context, k ci.PrivKey, value path.Path) error {
86
+ return p.PublishWithEOL(ctx, k, value, time.Now().Add(DefaultRecordTTL))
87
+}
88
+
89
+// PublishWithEOL publishes an IPNS record through pubsub
90
+func (p *PubsubPublisher) PublishWithEOL(ctx context.Context, k ci.PrivKey, value path.Path, eol time.Time) error {
91
+ id, err := peer.IDFromPrivateKey(k)
92
+ if err != nil {
93
+ return err
94
+ }
95
+
96
+ _, ipnskey := IpnsKeysForID(id)
97
+
98
+ seqno, err := p.getPreviousSeqNo(ctx, ipnskey)
99
+ if err != nil {
100
+ return err
101
+ }
102
+
103
+ seqno++
104
+
105
+ return p.publishRecord(ctx, k, value, seqno, eol, ipnskey, id)
106
+}
107
+
108
+func (p *PubsubPublisher) getPreviousSeqNo(ctx context.Context, ipnskey string) (uint64, error) {
109
+ // the datastore is shared with the routing publisher to properly increment and persist
110
+ // ipns record sequence numbers.
111
+ prevrec, err := p.ds.Get(dshelp.NewKeyFromBinary([]byte(ipnskey)))
112
+ if err != nil {
113
+ if err == ds.ErrNotFound {
114
+ // None found, lets start at zero!
115
+ return 0, nil
116
+ }
117
+ return 0, err
118
+ }
119
+
120
+ prbytes, ok := prevrec.([]byte)
121
+ if !ok {
122
+ return 0, fmt.Errorf("unexpected type returned from datastore: %#v", prevrec)
123
+ }
124
+
125
+ var dsrec dhtpb.Record
126
+ err = proto.Unmarshal(prbytes, &dsrec)
127
+ if err != nil {
128
+ return 0, err
129
+ }
130
+
131
+ var entry pb.IpnsEntry
132
+ err = proto.Unmarshal(dsrec.GetValue(), &entry)
133
+ if err != nil {
134
+ return 0, err
135
+ }
136
+
137
+ return entry.GetSequence(), nil
138
+}
139
+
140
+func (p *PubsubPublisher) publishRecord(ctx context.Context, k ci.PrivKey, value path.Path, seqno uint64, eol time.Time, ipnskey string, ID peer.ID) error {
141
+ entry, err := CreateRoutingEntryData(k, value, seqno, eol)
142
+ if err != nil {
143
+ return err
144
+ }
145
+
146
+ data, err := proto.Marshal(entry)
147
+ if err != nil {
148
+ return err
149
+ }
150
+
151
+ // the datastore is shared with the routing publisher to properly increment and persist
152
+ // ipns record sequence numbers; so we need to Record our new entry in the datastore
153
+ dsrec, err := record.MakePutRecord(k, ipnskey, data, true)
154
+ if err != nil {
155
+ return err
156
+ }
157
+
158
+ dsdata, err := proto.Marshal(dsrec)
159
+ if err != nil {
160
+ return err
161
+ }
162
+
163
+ err = p.ds.Put(dshelp.NewKeyFromBinary([]byte(ipnskey)), dsdata)
164
+ if err != nil {
165
+ return err
166
+ }
167
+
168
+ // now we publish, but we also need to bootstrap pubsub for our messages to propagate
169
+ topic := "/ipns/" + ID.Pretty()
170
+
171
+ p.mx.Lock()
172
+ _, ok := p.subs[topic]
173
+
174
+ if !ok {
175
+ p.subs[topic] = struct{}{}
176
+ p.mx.Unlock()
177
+
178
+ bootstrapPubsub(p.ctx, p.cr, p.host, topic)
179
+ } else {
180
+ p.mx.Unlock()
181
+ }
182
+
183
+ log.Debugf("PubsubPublish: publish IPNS record for %s (%d)", topic, seqno)
184
+ return p.ps.Publish(topic, data)
185
+}
186
+
187
+// Resolve resolves a name through pubsub and default depth limit
188
+func (r *PubsubResolver) Resolve(ctx context.Context, name string) (path.Path, error) {
189
+ return r.ResolveN(ctx, name, DefaultDepthLimit)
190
+}
191
+
192
+// ResolveN resolves a name through pubsub with the specified depth limit
193
+func (r *PubsubResolver) ResolveN(ctx context.Context, name string, depth int) (path.Path, error) {
194
+ return resolve(ctx, r, name, depth, "/ipns/")
195
+}
196
+
197
+func (r *PubsubResolver) resolveOnce(ctx context.Context, name string) (path.Path, error) {
198
+ log.Debugf("PubsubResolve: resolve '%s'", name)
199
+
200
+ // retrieve the public key once (for verifying messages)
201
+ xname := strings.TrimPrefix(name, "/ipns/")
202
+ hash, err := mh.FromB58String(xname)
203
+ if err != nil {
204
+ log.Warningf("PubsubResolve: bad input hash: [%s]", xname)
205
+ return "", err
206
+ }
207
+
208
+ id := peer.ID(hash)
209
+ if r.host.Peerstore().PrivKey(id) != nil {
210
+ return "", errors.New("Cannot resolve own name through pubsub")
211
+ }
212
+
213
+ pubk := id.ExtractPublicKey()
214
+ if pubk == nil {
215
+ pubk, err = r.pkf.GetPublicKey(ctx, id)
216
+ if err != nil {
217
+ log.Warningf("PubsubResolve: error fetching public key: %s [%s]", err.Error(), xname)
218
+ return "", err
219
+ }
220
+ }
221
+
222
+ // the topic is /ipns/Qmhash
223
+ if !strings.HasPrefix(name, "/ipns/") {
224
+ name = "/ipns/" + name
225
+ }
226
+
227
+ r.mx.Lock()
228
+ // see if we already have a pubsub subscription; if not, subscribe
229
+ sub, ok := r.subs[name]
230
+ if !ok {
231
+ sub, err = r.ps.Subscribe(name)
232
+ if err != nil {
233
+ r.mx.Unlock()
234
+ return "", err
235
+ }
236
+
237
+ log.Debugf("PubsubResolve: subscribed to %s", name)
238
+
239
+ r.subs[name] = sub
240
+
241
+ ctx, cancel := context.WithCancel(r.ctx)
242
+ go r.handleSubscription(sub, name, pubk, cancel)
243
+ go bootstrapPubsub(ctx, r.cr, r.host, name)
244
+ }
245
+ r.mx.Unlock()
246
+
247
+ // resolve to what we may already have in the datastore
248
+ dsval, err := r.ds.Get(dshelp.NewKeyFromBinary([]byte(name)))
249
+ if err != nil {
250
+ if err == ds.ErrNotFound {
251
+ return "", ErrResolveFailed
252
+ }
253
+ return "", err
254
+ }
255
+
256
+ data := dsval.([]byte)
257
+ entry := new(pb.IpnsEntry)
258
+
259
+ err = proto.Unmarshal(data, entry)
260
+ if err != nil {
261
+ return "", err
262
+ }
263
+
264
+ // check EOL; if the entry has expired, delete from datastore and return ds.ErrNotFound
265
+ eol, ok := checkEOL(entry)
266
+ if ok && eol.Before(time.Now()) {
267
+ err = r.ds.Delete(dshelp.NewKeyFromBinary([]byte(name)))
268
+ if err != nil {
269
+ log.Warningf("PubsubResolve: error deleting stale value for %s: %s", name, err.Error())
270
+ }
271
+
272
+ return "", ErrResolveFailed
273
+ }
274
+
275
+ value, err := path.ParsePath(string(entry.GetValue()))
276
+ return value, err
277
+}
278
+
279
+// GetSubscriptions retrieves a list of active topic subscriptions
280
+func (r *PubsubResolver) GetSubscriptions() []string {
281
+ r.mx.Lock()
282
+ defer r.mx.Unlock()
283
+
284
+ var res []string
285
+ for sub := range r.subs {
286
+ res = append(res, sub)
287
+ }
288
+
289
+ return res
290
+}
291
+
292
+// Cancel cancels a topic subscription; returns true if an active
293
+// subscription was canceled
294
+func (r *PubsubResolver) Cancel(name string) bool {
295
+ r.mx.Lock()
296
+ defer r.mx.Unlock()
297
+
298
+ sub, ok := r.subs[name]
299
+ if ok {
300
+ sub.Cancel()
301
+ delete(r.subs, name)
302
+ }
303
+
304
+ return ok
305
+}
306
+
307
+func (r *PubsubResolver) handleSubscription(sub *floodsub.Subscription, name string, pubk ci.PubKey, cancel func()) {
308
+ defer sub.Cancel()
309
+ defer cancel()
310
+
311
+ for {
312
+ msg, err := sub.Next(r.ctx)
313
+ if err != nil {
314
+ if err != context.Canceled {
315
+ log.Warningf("PubsubResolve: subscription error in %s: %s", name, err.Error())
316
+ }
317
+ return
318
+ }
319
+
320
+ err = r.receive(msg, name, pubk)
321
+ if err != nil {
322
+ log.Warningf("PubsubResolve: error proessing update for %s: %s", name, err.Error())
323
+ }
324
+ }
325
+}
326
+
327
+func (r *PubsubResolver) receive(msg *floodsub.Message, name string, pubk ci.PubKey) error {
328
+ data := msg.GetData()
329
+ if data == nil {
330
+ return errors.New("empty message")
331
+ }
332
+
333
+ entry := new(pb.IpnsEntry)
334
+ err := proto.Unmarshal(data, entry)
335
+ if err != nil {
336
+ return err
337
+ }
338
+
339
+ ok, err := pubk.Verify(ipnsEntryDataForSig(entry), entry.GetSignature())
340
+ if err != nil || !ok {
341
+ return errors.New("signature verification failed")
342
+ }
343
+
344
+ _, err = path.ParsePath(string(entry.GetValue()))
345
+ if err != nil {
346
+ return err
347
+ }
348
+
349
+ eol, ok := checkEOL(entry)
350
+ if ok && eol.Before(time.Now()) {
351
+ return errors.New("stale update; EOL exceeded")
352
+ }
353
+
354
+ // check the sequence number against what we may already have in our datastore
355
+ oval, err := r.ds.Get(dshelp.NewKeyFromBinary([]byte(name)))
356
+ if err == nil {
357
+ odata := oval.([]byte)
358
+ oentry := new(pb.IpnsEntry)
359
+
360
+ err = proto.Unmarshal(odata, oentry)
361
+ if err != nil {
362
+ return err
363
+ }
364
+
365
+ if entry.GetSequence() <= oentry.GetSequence() {
366
+ return errors.New("stale update; sequence number too small")
367
+ }
368
+ }
369
+
370
+ log.Debugf("PubsubResolve: receive IPNS record for %s", name)
371
+
372
+ return r.ds.Put(dshelp.NewKeyFromBinary([]byte(name)), data)
373
+}
374
+
375
+// rendezvous with peers in the name topic through provider records
376
+// Note: rendezbous/boostrap should really be handled by the pubsub implementation itself!
377
+func bootstrapPubsub(ctx context.Context, cr routing.ContentRouting, host p2phost.Host, name string) {
378
+ topic := "floodsub:" + name
379
+ hash := u.Hash([]byte(topic))
380
+ rz := cid.NewCidV1(cid.Raw, hash)
381
+
382
+ err := cr.Provide(ctx, rz, true)
383
+ if err != nil {
384
+ log.Warningf("bootstrapPubsub: error providing rendezvous for %s: %s", topic, err.Error())
385
+ }
386
+
387
+ go func() {
388
+ for {
389
+ select {
390
+ case <-time.After(8 * time.Hour):
391
+ err := cr.Provide(ctx, rz, true)
392
+ if err != nil {
393
+ log.Warningf("bootstrapPubsub: error providing rendezvous for %s: %s", topic, err.Error())
394
+ }
395
+ case <-ctx.Done():
396
+ return
397
+ }
398
+ }
399
+ }()
400
+
401
+ rzctx, cancel := context.WithTimeout(ctx, time.Second*10)
402
+ defer cancel()
403
+
404
+ wg := &sync.WaitGroup{}
405
+ for pi := range cr.FindProvidersAsync(rzctx, rz, 10) {
406
+ if pi.ID == host.ID() {
407
+ continue
408
+ }
409
+ wg.Add(1)
410
+ go func(pi pstore.PeerInfo) {
411
+ defer wg.Done()
412
+
413
+ ctx, cancel := context.WithTimeout(ctx, time.Second*10)
414
+ defer cancel()
415
+
416
+ err := host.Connect(ctx, pi)
417
+ if err != nil {
418
+ log.Debugf("Error connecting to pubsub peer %s: %s", pi.ID, err.Error())
419
+ return
420
+ }
421
+
422
+ // delay to let pubsub perform its handshake
423
+ time.Sleep(time.Millisecond * 250)
424
+
425
+ log.Debugf("Connected to pubsub peer %s", pi.ID)
426
+ }(pi)
427
+ }
428
+
429
+ wg.Wait()
430
+}