master
go 652 lines 17.5 KB
Raw
1 package commands
2
3 import (
4 "context"
5 "encoding/base64"
6 "errors"
7 "fmt"
8 "io"
9 "strings"
10 "time"
11
12 "github.com/ipfs/kubo/config"
13 cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
14 "github.com/ipfs/kubo/core/commands/cmdutils"
15 "github.com/ipfs/kubo/core/node"
16 mh "github.com/multiformats/go-multihash"
17
18 dag "github.com/ipfs/boxo/ipld/merkledag"
19 "github.com/ipfs/boxo/ipns"
20 "github.com/ipfs/boxo/provider"
21 cid "github.com/ipfs/go-cid"
22 cmds "github.com/ipfs/go-ipfs-cmds"
23 ipld "github.com/ipfs/go-ipld-format"
24 iface "github.com/ipfs/kubo/core/coreiface"
25 "github.com/ipfs/kubo/core/coreiface/options"
26 peer "github.com/libp2p/go-libp2p/core/peer"
27 routing "github.com/libp2p/go-libp2p/core/routing"
28 )
29
30 var errAllowOffline = errors.New("can't put while offline: pass `--allow-offline` to override")
31
32 const (
33 dhtVerboseOptionName = "verbose"
34 numProvidersOptionName = "num-providers"
35 allowOfflineOptionName = "allow-offline"
36 )
37
38 var RoutingCmd = &cmds.Command{
39 Helptext: cmds.HelpText{
40 Tagline: "Issue routing commands.",
41 ShortDescription: ``,
42 },
43
44 Subcommands: map[string]*cmds.Command{
45 "findprovs": findProvidersRoutingCmd,
46 "findpeer": findPeerRoutingCmd,
47 "get": getValueRoutingCmd,
48 "put": putValueRoutingCmd,
49 "provide": provideRefRoutingCmd,
50 "reprovide": reprovideRoutingCmd,
51 },
52 }
53
54 var findProvidersRoutingCmd = &cmds.Command{
55 Helptext: cmds.HelpText{
56 Tagline: "Find peers that can provide a specific value, given a key.",
57 ShortDescription: "Outputs a list of newline-delimited provider Peer IDs.",
58 },
59
60 Arguments: []cmds.Argument{
61 cmds.StringArg("key", true, true, "The key to find providers for."),
62 },
63 Options: []cmds.Option{
64 cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
65 cmds.IntOption(numProvidersOptionName, "n", "The number of providers to find.").WithDefault(20),
66 },
67 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
68 n, err := cmdenv.GetNode(env)
69 if err != nil {
70 return err
71 }
72
73 if !n.IsOnline {
74 return ErrNotOnline
75 }
76
77 numProviders, _ := req.Options[numProvidersOptionName].(int)
78 if numProviders < 1 {
79 return errors.New("number of providers must be greater than 0")
80 }
81
82 c, err := cid.Parse(req.Arguments[0])
83 if err != nil {
84 return err
85 }
86
87 ctx, cancel := context.WithCancel(req.Context)
88 ctx, events := routing.RegisterForQueryEvents(ctx)
89
90 go func() {
91 defer cancel()
92 pchan := n.Routing.FindProvidersAsync(ctx, c, numProviders)
93 for p := range pchan {
94 np := cmdutils.CloneAddrInfo(p)
95 routing.PublishQueryEvent(ctx, &routing.QueryEvent{
96 Type: routing.Provider,
97 Responses: []*peer.AddrInfo{&np},
98 })
99 }
100 }()
101 for e := range events {
102 if err := res.Emit(e); err != nil {
103 return err
104 }
105 }
106
107 return nil
108 },
109 Encoders: cmds.EncoderMap{
110 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
111 pfm := pfuncMap{
112 routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
113 if verbose {
114 fmt.Fprintf(out, "* closest peer %s\n", obj.ID)
115 }
116 return nil
117 },
118 routing.Provider: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
119 prov := obj.Responses[0]
120 if verbose {
121 fmt.Fprintf(out, "provider: ")
122 }
123 fmt.Fprintf(out, "%s\n", prov.ID)
124 if verbose {
125 for _, a := range prov.Addrs {
126 fmt.Fprintf(out, "\t%s\n", a)
127 }
128 }
129 return nil
130 },
131 }
132
133 verbose, _ := req.Options[dhtVerboseOptionName].(bool)
134 return printEvent(out, w, verbose, pfm)
135 }),
136 },
137 Type: routing.QueryEvent{},
138 }
139
140 const (
141 recursiveOptionName = "recursive"
142 )
143
144 var provideRefRoutingCmd = &cmds.Command{
145 Status: cmds.Deprecated,
146 Helptext: cmds.HelpText{
147 Tagline: "Deprecated, use 'ipfs provide once' instead.",
148 ShortDescription: `
149 'ipfs routing provide' has moved to 'ipfs provide once'. This command keeps
150 its existing behavior so existing scripts continue to work, but will be
151 removed in a future release.
152
153 Compared to 'ipfs provide once', this command:
154
155 - Buffers all CIDs from arguments and stdin before doing any work,
156 instead of streaming them as they arrive.
157 - Emits no per-CID output: there is no JSON event stream and the -v
158 flag's per-peer events do not actually propagate to the encoder.
159 - With -r, re-walks subtrees shared between roots and re-announces
160 shared blocks; 'ipfs provide once' deduplicates across all inputs.
161 - Issues an extra synchronous DHT lookup per CID on top of the
162 provider system, which defeats sweep batching.
163
164 Prefer 'ipfs provide once' for new scripts and any large input.
165 `,
166 },
167
168 Arguments: []cmds.Argument{
169 cmds.StringArg("key", true, true, "The key[s] to send provide records for.").EnableStdin(),
170 },
171 Options: []cmds.Option{
172 cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
173 cmds.BoolOption(recursiveOptionName, "r", "Recursively provide entire graph."),
174 },
175 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
176 nd, err := cmdenv.GetNode(env)
177 if err != nil {
178 return err
179 }
180
181 if !nd.IsOnline {
182 return ErrNotOnline
183 }
184 // respect global config
185 cfg, err := nd.Repo.Config()
186 if err != nil {
187 return err
188 }
189 if !cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled) {
190 return errors.New("invalid configuration: Provide.Enabled is set to 'false'")
191 }
192
193 if len(nd.PeerHost.Network().Conns()) == 0 && !cfg.HasHTTPProviderConfigured() {
194 // Node is depending on DHT for providing (no custom HTTP provider
195 // configured) and currently has no connected peers.
196 return errors.New("cannot provide, no connected peers")
197 }
198
199 // If we reach here with no connections but HTTP provider configured,
200 // we proceed with the provide operation via HTTP
201
202 // Needed to parse stdin args.
203 // TODO: Lazy Load
204 err = req.ParseBodyArgs()
205 if err != nil {
206 return err
207 }
208
209 rec, _ := req.Options[recursiveOptionName].(bool)
210
211 var cids []cid.Cid
212 for _, arg := range req.Arguments {
213 c, err := cid.Decode(arg)
214 if err != nil {
215 return err
216 }
217
218 has, err := nd.Blockstore.Has(req.Context, c)
219 if err != nil {
220 return err
221 }
222
223 if !has {
224 return fmt.Errorf("block %s not found locally, cannot provide", c)
225 }
226
227 cids = append(cids, c)
228 }
229
230 ctx, cancel := context.WithCancel(req.Context)
231 ctx, events := routing.RegisterForQueryEvents(ctx)
232
233 var provideErr error
234 // TODO: not sure if necessary to call StartProviding for `ipfs routing
235 // provide <cid>`, since either cid is already being provided, or it will
236 // be garbage collected and not reprovided anyway. So we may simply stick
237 // with a single (optimistic) provide, and skip StartProviding call.
238 go func() {
239 defer cancel()
240 if rec {
241 provideErr = provideCidsRec(ctx, nd.Provider, nd.DAG, cids)
242 } else {
243 provideErr = provideCids(nd.Provider, cids)
244 }
245 if provideErr != nil {
246 routing.PublishQueryEvent(ctx, &routing.QueryEvent{
247 Type: routing.QueryError,
248 Extra: provideErr.Error(),
249 })
250 }
251 }()
252
253 if nd.HasActiveDHTClient() {
254 // If node has a DHT client, provide immediately the supplied cids before
255 // returning.
256 for _, c := range cids {
257 if err = provideCIDSync(req.Context, nd.DHTClient, c); err != nil {
258 return fmt.Errorf("error providing cid: %w", err)
259 }
260 }
261 }
262
263 for e := range events {
264 if err := res.Emit(e); err != nil {
265 return err
266 }
267 }
268
269 return provideErr
270 },
271 Encoders: cmds.EncoderMap{
272 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
273 pfm := pfuncMap{
274 routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
275 if verbose {
276 fmt.Fprintf(out, "sending provider record to peer %s\n", obj.ID)
277 }
278 return nil
279 },
280 }
281
282 verbose, _ := req.Options[dhtVerboseOptionName].(bool)
283 return printEvent(out, w, verbose, pfm)
284 }),
285 },
286 Type: routing.QueryEvent{},
287 }
288
289 var reprovideRoutingCmd = &cmds.Command{
290 Status: cmds.Deprecated,
291 Helptext: cmds.HelpText{
292 Tagline: "Trigger a reprovide cycle (legacy provider only).",
293 ShortDescription: `
294 Forces the legacy provider to reprovide all locally stored CIDs that match
295 Provide.Strategy.
296
297 Only works when Provide.DHT.SweepEnabled=false. With the default sweep
298 provider, reproviding is continuous and scheduled, so this command returns
299 an error. Use 'ipfs provide stat --all' to monitor sweep progress.
300 `,
301 },
302 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
303 nd, err := cmdenv.GetNode(env)
304 if err != nil {
305 return err
306 }
307
308 if !nd.IsOnline {
309 return ErrNotOnline
310 }
311
312 cfg, err := nd.Repo.Config()
313 if err != nil {
314 return err
315 }
316 if !cfg.Provide.Enabled.WithDefault(config.DefaultProvideEnabled) {
317 return errors.New("invalid configuration: Provide.Enabled is set to 'false'")
318 }
319 if cfg.Provide.DHT.Interval.WithDefault(config.DefaultProvideDHTInterval) == 0 {
320 return errors.New("invalid configuration: Provide.DHT.Interval is set to '0'")
321 }
322 provideSys, ok := nd.Provider.(provider.Reprovider)
323 if !ok {
324 err := errors.New("manual reprovide is not available with the sweep provider; set Provide.DHT.SweepEnabled=false to use the legacy provider, or run 'ipfs provide stat --all' to monitor the sweep schedule")
325 log.Error(err)
326 return err
327 }
328
329 err = provideSys.Reprovide(req.Context)
330 if err != nil {
331 return err
332 }
333
334 return nil
335 },
336 }
337
338 func provideCids(prov node.DHTProvider, cids []cid.Cid) error {
339 mhs := make([]mh.Multihash, len(cids))
340 for i, c := range cids {
341 mhs[i] = c.Hash()
342 }
343 // providing happens asynchronously
344 return prov.StartProviding(true, mhs...)
345 }
346
347 func provideCidsRec(ctx context.Context, prov node.DHTProvider, dserv ipld.DAGService, cids []cid.Cid) error {
348 for _, c := range cids {
349 kset := cid.NewSet()
350 err := dag.Walk(ctx, dag.GetLinksDirect(dserv), c, kset.Visit)
351 if err != nil {
352 return err
353 }
354 if err = provideCids(prov, kset.Keys()); err != nil {
355 return err
356 }
357 }
358 return nil
359 }
360
361 var findPeerRoutingCmd = &cmds.Command{
362 Helptext: cmds.HelpText{
363 Tagline: "Find the multiaddresses associated with a Peer ID.",
364 ShortDescription: "Outputs a list of newline-delimited multiaddresses.",
365 },
366
367 Arguments: []cmds.Argument{
368 cmds.StringArg("peerID", true, true, "The ID of the peer to search for."),
369 },
370 Options: []cmds.Option{
371 cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
372 },
373 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
374 nd, err := cmdenv.GetNode(env)
375 if err != nil {
376 return err
377 }
378
379 if !nd.IsOnline {
380 return ErrNotOnline
381 }
382
383 pid, err := peer.Decode(req.Arguments[0])
384 if err != nil {
385 return err
386 }
387
388 if pid == nd.Identity {
389 return ErrSelfUnsupported
390 }
391
392 ctx, cancel := context.WithCancel(req.Context)
393 ctx, events := routing.RegisterForQueryEvents(ctx)
394
395 var findPeerErr error
396 go func() {
397 defer cancel()
398 var pi peer.AddrInfo
399 pi, findPeerErr = nd.Routing.FindPeer(ctx, pid)
400 if findPeerErr != nil {
401 routing.PublishQueryEvent(ctx, &routing.QueryEvent{
402 Type: routing.QueryError,
403 Extra: findPeerErr.Error(),
404 })
405 return
406 }
407
408 routing.PublishQueryEvent(ctx, &routing.QueryEvent{
409 Type: routing.FinalPeer,
410 Responses: []*peer.AddrInfo{&pi},
411 })
412 }()
413
414 for e := range events {
415 if err := res.Emit(e); err != nil {
416 return err
417 }
418 }
419
420 return findPeerErr
421 },
422 Encoders: cmds.EncoderMap{
423 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
424 pfm := pfuncMap{
425 routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
426 pi := obj.Responses[0]
427 for _, a := range pi.Addrs {
428 fmt.Fprintf(out, "%s\n", a)
429 }
430 return nil
431 },
432 }
433
434 verbose, _ := req.Options[dhtVerboseOptionName].(bool)
435 return printEvent(out, w, verbose, pfm)
436 }),
437 },
438 Type: routing.QueryEvent{},
439 }
440
441 var getValueRoutingCmd = &cmds.Command{
442 Status: cmds.Experimental,
443 Helptext: cmds.HelpText{
444 Tagline: "Given a key, query the routing system for its best value.",
445 ShortDescription: `
446 Outputs the best value for the given key.
447
448 There may be several different values for a given key stored in the routing
449 system; in this context 'best' means the record that is most desirable. There is
450 no one metric for 'best': it depends entirely on the key type. For IPNS, 'best'
451 is the record that is both valid and has the highest sequence number (freshest).
452 Different key types can specify other 'best' rules.
453 `,
454 },
455
456 Arguments: []cmds.Argument{
457 cmds.StringArg("key", true, true, "The key to find a value for."),
458 },
459 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
460 api, err := cmdenv.GetApi(env, req)
461 if err != nil {
462 return err
463 }
464
465 r, err := api.Routing().Get(req.Context, req.Arguments[0])
466 if err != nil {
467 return err
468 }
469
470 return res.Emit(routing.QueryEvent{
471 Extra: base64.StdEncoding.EncodeToString(r),
472 Type: routing.Value,
473 })
474 },
475 Encoders: cmds.EncoderMap{
476 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, obj *routing.QueryEvent) error {
477 res, err := base64.StdEncoding.DecodeString(obj.Extra)
478 if err != nil {
479 return err
480 }
481 _, err = w.Write(res)
482 return err
483 }),
484 },
485 Type: routing.QueryEvent{},
486 }
487
488 var putValueRoutingCmd = &cmds.Command{
489 Status: cmds.Experimental,
490 Helptext: cmds.HelpText{
491 Tagline: "Write a key/value pair to the routing system.",
492 ShortDescription: `
493 Given a key of the form /foo/bar and a valid value for that key, this will write
494 that value to the routing system with that key.
495
496 Keys have two parts: a keytype (foo) and the key name (bar). IPNS uses the
497 /ipns keytype, and expects the key name to be a Peer ID. IPNS entries are
498 specifically formatted (protocol buffer).
499
500 You may only use keytypes that are supported in your ipfs binary: currently
501 this is only /ipns. Unless you have a relatively deep understanding of the
502 go-ipfs routing internals, you likely want to be using 'ipfs name publish' instead
503 of this.
504
505 The value must be a valid value for the given key type. For example, if the key
506 is /ipns/QmFoo, the value must be IPNS record (protobuf) signed with the key
507 identified by QmFoo.
508 `,
509 },
510
511 Arguments: []cmds.Argument{
512 cmds.StringArg("key", true, false, "The key to store the value at."),
513 cmds.FileArg("value-file", true, false, "A path to a file containing the value to store.").EnableStdin(),
514 },
515 Options: []cmds.Option{
516 cmds.BoolOption(allowOfflineOptionName, "When offline, save the IPNS record to the local datastore without broadcasting to the network instead of simply failing."),
517 },
518 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
519 api, err := cmdenv.GetApi(env, req)
520 if err != nil {
521 return err
522 }
523
524 file, err := cmdenv.GetFileArg(req.Files.Entries())
525 if err != nil {
526 return err
527 }
528 defer file.Close()
529
530 data, err := io.ReadAll(file)
531 if err != nil {
532 return err
533 }
534
535 allowOffline, _ := req.Options[allowOfflineOptionName].(bool)
536
537 opts := []options.RoutingPutOption{
538 options.Put.AllowOffline(allowOffline),
539 }
540
541 ipnsName, err := ipns.NameFromString(req.Arguments[0])
542 if err != nil {
543 return err
544 }
545
546 err = api.Routing().Put(req.Context, req.Arguments[0], data, opts...)
547 if err != nil {
548 if err == iface.ErrOffline {
549 err = errAllowOffline
550 }
551 return err
552 }
553
554 return res.Emit(routing.QueryEvent{
555 Type: routing.Value,
556 ID: ipnsName.Peer(),
557 })
558 },
559 Encoders: cmds.EncoderMap{
560 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
561 pfm := pfuncMap{
562 routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
563 if verbose {
564 fmt.Fprintf(out, "* closest peer %s\n", obj.ID)
565 }
566 return nil
567 },
568 routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
569 fmt.Fprintf(out, "%s\n", obj.ID)
570 return nil
571 },
572 }
573
574 verbose, _ := req.Options[dhtVerboseOptionName].(bool)
575
576 return printEvent(out, w, verbose, pfm)
577 }),
578 },
579 Type: routing.QueryEvent{},
580 }
581
582 type (
583 printFunc func(obj *routing.QueryEvent, out io.Writer, verbose bool) error
584 pfuncMap map[routing.QueryEventType]printFunc
585 )
586
587 func printEvent(obj *routing.QueryEvent, out io.Writer, verbose bool, override pfuncMap) error {
588 if verbose {
589 fmt.Fprintf(out, "%s: ", time.Now().Format("15:04:05.000"))
590 }
591
592 if override != nil {
593 if pf, ok := override[obj.Type]; ok {
594 return pf(obj, out, verbose)
595 }
596 }
597
598 switch obj.Type {
599 case routing.SendingQuery:
600 if verbose {
601 fmt.Fprintf(out, "* querying %s\n", obj.ID)
602 }
603 case routing.Value:
604 if verbose {
605 fmt.Fprintf(out, "got value: '%s'\n", obj.Extra)
606 } else {
607 fmt.Fprint(out, obj.Extra)
608 }
609 case routing.PeerResponse:
610 if verbose {
611 fmt.Fprintf(out, "* %s says use ", obj.ID)
612 for _, p := range obj.Responses {
613 fmt.Fprintf(out, "%s ", p.ID)
614 }
615 fmt.Fprintln(out)
616 }
617 case routing.QueryError:
618 if verbose {
619 fmt.Fprintf(out, "error: %s\n", obj.Extra)
620 }
621 case routing.DialingPeer:
622 if verbose {
623 fmt.Fprintf(out, "dialing peer: %s\n", obj.ID)
624 }
625 case routing.AddingPeer:
626 if verbose {
627 fmt.Fprintf(out, "adding peer to query: %s\n", obj.ID)
628 }
629 case routing.FinalPeer:
630 default:
631 if verbose {
632 fmt.Fprintf(out, "unrecognized event type: %d\n", obj.Type)
633 }
634 }
635 return nil
636 }
637
638 func escapeDhtKey(s string) (string, error) {
639 parts := strings.Split(s, "/")
640 if len(parts) != 3 ||
641 parts[0] != "" ||
642 !(parts[1] == "ipns" || parts[1] == "pk") {
643 return "", errors.New("invalid key")
644 }
645
646 k, err := peer.Decode(parts[2])
647 if err != nil {
648 return "", err
649 }
650
651 return strings.Join(append(parts[:2], string(k)), "/"), nil
652 }