Skip to content

Commit a3c70a1

Browse files
hacdiaslidel
andauthored
feat(gateway): IPNS record response format (IPIP-351) (#9399)
* feat(gateway): IPNS record response format * docs(rpc): mark as experimental: routing provide, get, put Co-authored-by: Marcin Rataj <[email protected]>
1 parent 94e7f79 commit a3c70a1

File tree

13 files changed

+219
-117
lines changed

13 files changed

+219
-117
lines changed

core/commands/routing.go

+19-108
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package commands
22

33
import (
44
"context"
5-
"encoding/base64"
65
"errors"
76
"fmt"
87
"io"
@@ -135,6 +134,7 @@ const (
135134
)
136135

137136
var provideRefRoutingCmd = &cmds.Command{
137+
Status: cmds.Experimental,
138138
Helptext: cmds.HelpText{
139139
Tagline: "Announce to the network that you are providing given values.",
140140
},
@@ -346,6 +346,7 @@ var findPeerRoutingCmd = &cmds.Command{
346346
}
347347

348348
var getValueRoutingCmd = &cmds.Command{
349+
Status: cmds.Experimental,
349350
Helptext: cmds.HelpText{
350351
Tagline: "Given a key, query the routing system for its best value.",
351352
ShortDescription: `
@@ -362,78 +363,30 @@ Different key types can specify other 'best' rules.
362363
Arguments: []cmds.Argument{
363364
cmds.StringArg("key", true, true, "The key to find a value for."),
364365
},
365-
Options: []cmds.Option{
366-
cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
367-
},
368366
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
369-
nd, err := cmdenv.GetNode(env)
367+
api, err := cmdenv.GetApi(env, req)
370368
if err != nil {
371369
return err
372370
}
373371

374-
if !nd.IsOnline {
375-
return ErrNotOnline
376-
}
377-
378-
dhtkey, err := escapeDhtKey(req.Arguments[0])
372+
r, err := api.Routing().Get(req.Context, req.Arguments[0])
379373
if err != nil {
380374
return err
381375
}
382376

383-
ctx, cancel := context.WithCancel(req.Context)
384-
ctx, events := routing.RegisterForQueryEvents(ctx)
385-
386-
var getErr error
387-
go func() {
388-
defer cancel()
389-
var val []byte
390-
val, getErr = nd.Routing.GetValue(ctx, dhtkey)
391-
if getErr != nil {
392-
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
393-
Type: routing.QueryError,
394-
Extra: getErr.Error(),
395-
})
396-
} else {
397-
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
398-
Type: routing.Value,
399-
Extra: base64.StdEncoding.EncodeToString(val),
400-
})
401-
}
402-
}()
403-
404-
for e := range events {
405-
if err := res.Emit(e); err != nil {
406-
return err
407-
}
408-
}
409-
410-
return getErr
377+
return res.Emit(r)
411378
},
412379
Encoders: cmds.EncoderMap{
413-
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
414-
pfm := pfuncMap{
415-
routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
416-
if verbose {
417-
_, err := fmt.Fprintf(out, "got value: '%s'\n", obj.Extra)
418-
return err
419-
}
420-
res, err := base64.StdEncoding.DecodeString(obj.Extra)
421-
if err != nil {
422-
return err
423-
}
424-
_, err = out.Write(res)
425-
return err
426-
},
427-
}
428-
429-
verbose, _ := req.Options[dhtVerboseOptionName].(bool)
430-
return printEvent(out, w, verbose, pfm)
380+
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out []byte) error {
381+
_, err := w.Write(out)
382+
return err
431383
}),
432384
},
433-
Type: routing.QueryEvent{},
385+
Type: []byte{},
434386
}
435387

436388
var putValueRoutingCmd = &cmds.Command{
389+
Status: cmds.Experimental,
437390
Helptext: cmds.HelpText{
438391
Tagline: "Write a key/value pair to the routing system.",
439392
ShortDescription: `
@@ -459,20 +412,8 @@ identified by QmFoo.
459412
cmds.StringArg("key", true, false, "The key to store the value at."),
460413
cmds.FileArg("value-file", true, false, "A path to a file containing the value to store.").EnableStdin(),
461414
},
462-
Options: []cmds.Option{
463-
cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
464-
},
465415
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
466-
nd, err := cmdenv.GetNode(env)
467-
if err != nil {
468-
return err
469-
}
470-
471-
if !nd.IsOnline {
472-
return ErrNotOnline
473-
}
474-
475-
key, err := escapeDhtKey(req.Arguments[0])
416+
api, err := cmdenv.GetApi(env, req)
476417
if err != nil {
477418
return err
478419
}
@@ -488,50 +429,20 @@ identified by QmFoo.
488429
return err
489430
}
490431

491-
ctx, cancel := context.WithCancel(req.Context)
492-
ctx, events := routing.RegisterForQueryEvents(ctx)
493-
494-
var putErr error
495-
go func() {
496-
defer cancel()
497-
putErr = nd.Routing.PutValue(ctx, key, []byte(data))
498-
if putErr != nil {
499-
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
500-
Type: routing.QueryError,
501-
Extra: putErr.Error(),
502-
})
503-
}
504-
}()
505-
506-
for e := range events {
507-
if err := res.Emit(e); err != nil {
508-
return err
509-
}
432+
err = api.Routing().Put(req.Context, req.Arguments[0], data)
433+
if err != nil {
434+
return err
510435
}
511436

512-
return putErr
437+
return res.Emit([]byte(fmt.Sprintf("%s added", req.Arguments[0])))
513438
},
514439
Encoders: cmds.EncoderMap{
515-
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
516-
pfm := pfuncMap{
517-
routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
518-
if verbose {
519-
fmt.Fprintf(out, "* closest peer %s\n", obj.ID)
520-
}
521-
return nil
522-
},
523-
routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
524-
fmt.Fprintf(out, "%s\n", obj.ID.Pretty())
525-
return nil
526-
},
527-
}
528-
529-
verbose, _ := req.Options[dhtVerboseOptionName].(bool)
530-
531-
return printEvent(out, w, verbose, pfm)
440+
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out []byte) error {
441+
_, err := w.Write(out)
442+
return err
532443
}),
533444
},
534-
Type: routing.QueryEvent{},
445+
Type: []byte{},
535446
}
536447

537448
type printFunc func(obj *routing.QueryEvent, out io.Writer, verbose bool) error

core/coreapi/coreapi.go

+5
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ func (api *CoreAPI) PubSub() coreiface.PubSubAPI {
144144
return (*PubSubAPI)(api)
145145
}
146146

147+
// Routing returns the RoutingAPI interface implementation backed by the kubo node
148+
func (api *CoreAPI) Routing() coreiface.RoutingAPI {
149+
return (*RoutingAPI)(api)
150+
}
151+
147152
// WithOptions returns api with global options applied
148153
func (api *CoreAPI) WithOptions(opts ...options.ApiOption) (coreiface.CoreAPI, error) {
149154
settings := api.parentOpts // make sure to copy

core/coreapi/routing.go

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package coreapi
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/ipfs/go-path"
8+
coreiface "github.com/ipfs/interface-go-ipfs-core"
9+
peer "github.com/libp2p/go-libp2p/core/peer"
10+
)
11+
12+
type RoutingAPI CoreAPI
13+
14+
func (r *RoutingAPI) Get(ctx context.Context, key string) ([]byte, error) {
15+
if !r.nd.IsOnline {
16+
return nil, coreiface.ErrOffline
17+
}
18+
19+
dhtKey, err := normalizeKey(key)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
return r.routing.GetValue(ctx, dhtKey)
25+
}
26+
27+
func (r *RoutingAPI) Put(ctx context.Context, key string, value []byte) error {
28+
if !r.nd.IsOnline {
29+
return coreiface.ErrOffline
30+
}
31+
32+
dhtKey, err := normalizeKey(key)
33+
if err != nil {
34+
return err
35+
}
36+
37+
return r.routing.PutValue(ctx, dhtKey, value)
38+
}
39+
40+
func normalizeKey(s string) (string, error) {
41+
parts := path.SplitList(s)
42+
if len(parts) != 3 ||
43+
parts[0] != "" ||
44+
!(parts[1] == "ipns" || parts[1] == "pk") {
45+
return "", errors.New("invalid key")
46+
}
47+
48+
k, err := peer.Decode(parts[2])
49+
if err != nil {
50+
return "", err
51+
}
52+
return path.Join(append(parts[:2], string(k))), nil
53+
}

core/corehttp/gateway.go

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ type NodeAPI interface {
3333
// Dag returns an implementation of Dag API
3434
Dag() coreiface.APIDagService
3535

36+
// Routing returns an implementation of Routing API.
37+
// Used for returning signed IPNS records, see IPIP-0328
38+
Routing() coreiface.RoutingAPI
39+
3640
// ResolvePath resolves the path using Unixfs resolver
3741
ResolvePath(context.Context, path.Path) (path.Resolved, error)
3842
}

core/corehttp/gateway_handler.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,9 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
444444
case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor":
445445
logger.Debugw("serving codec", "path", contentPath)
446446
i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
447+
case "application/vnd.ipfs.ipns-record":
448+
logger.Debugw("serving ipns record", "path", contentPath)
449+
i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
447450
return
448451
default: // catch-all for unsuported application/vnd.*
449452
err := fmt.Errorf("unsupported format %q", responseFormat)
@@ -885,6 +888,8 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
885888
return "application/vnd.ipld.dag-json", nil, nil
886889
case "dag-cbor":
887890
return "application/vnd.ipld.dag-cbor", nil, nil
891+
case "ipns-record":
892+
return "application/vnd.ipfs.ipns-record", nil, nil
888893
}
889894
}
890895
// Browsers and other user agents will send Accept header with generic types like:
@@ -898,7 +903,8 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
898903
if strings.HasPrefix(accept, "application/vnd.ipld") ||
899904
strings.HasPrefix(accept, "application/x-tar") ||
900905
strings.HasPrefix(accept, "application/json") ||
901-
strings.HasPrefix(accept, "application/cbor") {
906+
strings.HasPrefix(accept, "application/cbor") ||
907+
strings.HasPrefix(accept, "application/vnd.ipfs") {
902908
mediatype, params, err := mime.ParseMediaType(accept)
903909
if err != nil {
904910
return "", nil, err
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package corehttp
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
"strings"
9+
"time"
10+
11+
"github.com/gogo/protobuf/proto"
12+
ipns_pb "github.com/ipfs/go-ipns/pb"
13+
path "github.com/ipfs/go-path"
14+
ipath "github.com/ipfs/interface-go-ipfs-core/path"
15+
"go.uber.org/zap"
16+
)
17+
18+
func (i *gatewayHandler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) {
19+
if contentPath.Namespace() != "ipns" {
20+
err := fmt.Errorf("%s is not an IPNS link", contentPath.String())
21+
webError(w, err.Error(), err, http.StatusBadRequest)
22+
return
23+
}
24+
25+
key := contentPath.String()
26+
key = strings.TrimSuffix(key, "/")
27+
if strings.Count(key, "/") > 2 {
28+
err := errors.New("cannot find ipns key for subpath")
29+
webError(w, err.Error(), err, http.StatusBadRequest)
30+
return
31+
}
32+
33+
rawRecord, err := i.api.Routing().Get(ctx, key)
34+
if err != nil {
35+
webError(w, err.Error(), err, http.StatusInternalServerError)
36+
return
37+
}
38+
39+
var record ipns_pb.IpnsEntry
40+
err = proto.Unmarshal(rawRecord, &record)
41+
if err != nil {
42+
webError(w, err.Error(), err, http.StatusInternalServerError)
43+
return
44+
}
45+
46+
// Set cache control headers based on the TTL set in the IPNS record. If the
47+
// TTL is not present, we use the Last-Modified tag. We are tracking IPNS
48+
// caching on: https://github.com/ipfs/kubo/issues/1818.
49+
// TODO: use addCacheControlHeaders once #1818 is fixed.
50+
w.Header().Set("Etag", getEtag(r, resolvedPath.Cid()))
51+
if record.Ttl != nil {
52+
seconds := int(time.Duration(*record.Ttl).Seconds())
53+
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds))
54+
} else {
55+
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
56+
}
57+
58+
// Set Content-Disposition
59+
var name string
60+
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
61+
name = urlFilename
62+
} else {
63+
name = path.SplitList(key)[2] + ".ipns-record"
64+
}
65+
setContentDispositionHeader(w, name, "attachment")
66+
67+
w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record")
68+
w.Header().Set("X-Content-Type-Options", "nosniff")
69+
70+
_, _ = w.Write(rawRecord)
71+
}

docs/examples/kubo-as-a-library/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ replace github.com/ipfs/kubo => ./../../..
88

99
require (
1010
github.com/ipfs/go-libipfs v0.3.0
11-
github.com/ipfs/interface-go-ipfs-core v0.9.0
11+
github.com/ipfs/interface-go-ipfs-core v0.10.0
1212
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
1313
github.com/libp2p/go-libp2p v0.24.2
1414
github.com/multiformats/go-multiaddr v0.8.0

docs/examples/kubo-as-a-library/go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,8 @@ github.com/ipfs/go-unixfsnode v1.5.1/go.mod h1:ed79DaG9IEuZITJVQn4U6MZDftv6I3ygU
594594
github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0=
595595
github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs=
596596
github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU=
597-
github.com/ipfs/interface-go-ipfs-core v0.9.0 h1:+RCouVtSU/SldgkqWufjIu1smmGaSyKgUIfbYwLukgI=
598-
github.com/ipfs/interface-go-ipfs-core v0.9.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0=
597+
github.com/ipfs/interface-go-ipfs-core v0.10.0 h1:b/psL1oqJcySdQAsIBfW5ZJJkOAsYlhWtC0/Qvr4WiM=
598+
github.com/ipfs/interface-go-ipfs-core v0.10.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0=
599599
github.com/ipld/edelweiss v0.2.0 h1:KfAZBP8eeJtrLxLhi7r3N0cBCo7JmwSRhOJp3WSpNjk=
600600
github.com/ipld/edelweiss v0.2.0/go.mod h1:FJAzJRCep4iI8FOFlRriN9n0b7OuX3T/S9++NpBDmA4=
601601
github.com/ipld/go-car v0.4.0 h1:U6W7F1aKF/OJMHovnOVdst2cpQE5GhmHibQkAixgNcQ=

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ require (
6262
github.com/ipfs/go-unixfs v0.4.2
6363
github.com/ipfs/go-unixfsnode v1.5.1
6464
github.com/ipfs/go-verifcid v0.0.2
65-
github.com/ipfs/interface-go-ipfs-core v0.9.0
65+
github.com/ipfs/interface-go-ipfs-core v0.10.0
6666
github.com/ipld/go-car v0.4.0
6767
github.com/ipld/go-car/v2 v2.5.1
6868
github.com/ipld/go-codec-dagpb v1.5.0

0 commit comments

Comments
 (0)