Skip to content

Commit

Permalink
feat(gateway): IPNS record response format (IPIP-351) (#9399)
Browse files Browse the repository at this point in the history
* feat(gateway): IPNS record response format
* docs(rpc): mark as experimental: routing provide, get, put

Co-authored-by: Marcin Rataj <[email protected]>
  • Loading branch information
hacdias and lidel authored Jan 27, 2023
1 parent 94e7f79 commit a3c70a1
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 117 deletions.
127 changes: 19 additions & 108 deletions core/commands/routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package commands

import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -135,6 +134,7 @@ const (
)

var provideRefRoutingCmd = &cmds.Command{
Status: cmds.Experimental,
Helptext: cmds.HelpText{
Tagline: "Announce to the network that you are providing given values.",
},
Expand Down Expand Up @@ -346,6 +346,7 @@ var findPeerRoutingCmd = &cmds.Command{
}

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

if !nd.IsOnline {
return ErrNotOnline
}

dhtkey, err := escapeDhtKey(req.Arguments[0])
r, err := api.Routing().Get(req.Context, req.Arguments[0])
if err != nil {
return err
}

ctx, cancel := context.WithCancel(req.Context)
ctx, events := routing.RegisterForQueryEvents(ctx)

var getErr error
go func() {
defer cancel()
var val []byte
val, getErr = nd.Routing.GetValue(ctx, dhtkey)
if getErr != nil {
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
Type: routing.QueryError,
Extra: getErr.Error(),
})
} else {
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
Type: routing.Value,
Extra: base64.StdEncoding.EncodeToString(val),
})
}
}()

for e := range events {
if err := res.Emit(e); err != nil {
return err
}
}

return getErr
return res.Emit(r)
},
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
pfm := pfuncMap{
routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
if verbose {
_, err := fmt.Fprintf(out, "got value: '%s'\n", obj.Extra)
return err
}
res, err := base64.StdEncoding.DecodeString(obj.Extra)
if err != nil {
return err
}
_, err = out.Write(res)
return err
},
}

verbose, _ := req.Options[dhtVerboseOptionName].(bool)
return printEvent(out, w, verbose, pfm)
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out []byte) error {
_, err := w.Write(out)
return err
}),
},
Type: routing.QueryEvent{},
Type: []byte{},
}

var putValueRoutingCmd = &cmds.Command{
Status: cmds.Experimental,
Helptext: cmds.HelpText{
Tagline: "Write a key/value pair to the routing system.",
ShortDescription: `
Expand All @@ -459,20 +412,8 @@ identified by QmFoo.
cmds.StringArg("key", true, false, "The key to store the value at."),
cmds.FileArg("value-file", true, false, "A path to a file containing the value to store.").EnableStdin(),
},
Options: []cmds.Option{
cmds.BoolOption(dhtVerboseOptionName, "v", "Print extra information."),
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
nd, err := cmdenv.GetNode(env)
if err != nil {
return err
}

if !nd.IsOnline {
return ErrNotOnline
}

key, err := escapeDhtKey(req.Arguments[0])
api, err := cmdenv.GetApi(env, req)
if err != nil {
return err
}
Expand All @@ -488,50 +429,20 @@ identified by QmFoo.
return err
}

ctx, cancel := context.WithCancel(req.Context)
ctx, events := routing.RegisterForQueryEvents(ctx)

var putErr error
go func() {
defer cancel()
putErr = nd.Routing.PutValue(ctx, key, []byte(data))
if putErr != nil {
routing.PublishQueryEvent(ctx, &routing.QueryEvent{
Type: routing.QueryError,
Extra: putErr.Error(),
})
}
}()

for e := range events {
if err := res.Emit(e); err != nil {
return err
}
err = api.Routing().Put(req.Context, req.Arguments[0], data)
if err != nil {
return err
}

return putErr
return res.Emit([]byte(fmt.Sprintf("%s added", req.Arguments[0])))
},
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *routing.QueryEvent) error {
pfm := pfuncMap{
routing.FinalPeer: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
if verbose {
fmt.Fprintf(out, "* closest peer %s\n", obj.ID)
}
return nil
},
routing.Value: func(obj *routing.QueryEvent, out io.Writer, verbose bool) error {
fmt.Fprintf(out, "%s\n", obj.ID.Pretty())
return nil
},
}

verbose, _ := req.Options[dhtVerboseOptionName].(bool)

return printEvent(out, w, verbose, pfm)
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out []byte) error {
_, err := w.Write(out)
return err
}),
},
Type: routing.QueryEvent{},
Type: []byte{},
}

type printFunc func(obj *routing.QueryEvent, out io.Writer, verbose bool) error
Expand Down
5 changes: 5 additions & 0 deletions core/coreapi/coreapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ func (api *CoreAPI) PubSub() coreiface.PubSubAPI {
return (*PubSubAPI)(api)
}

// Routing returns the RoutingAPI interface implementation backed by the kubo node
func (api *CoreAPI) Routing() coreiface.RoutingAPI {
return (*RoutingAPI)(api)
}

// WithOptions returns api with global options applied
func (api *CoreAPI) WithOptions(opts ...options.ApiOption) (coreiface.CoreAPI, error) {
settings := api.parentOpts // make sure to copy
Expand Down
53 changes: 53 additions & 0 deletions core/coreapi/routing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package coreapi

import (
"context"
"errors"

"github.com/ipfs/go-path"
coreiface "github.com/ipfs/interface-go-ipfs-core"
peer "github.com/libp2p/go-libp2p/core/peer"
)

type RoutingAPI CoreAPI

func (r *RoutingAPI) Get(ctx context.Context, key string) ([]byte, error) {
if !r.nd.IsOnline {
return nil, coreiface.ErrOffline
}

dhtKey, err := normalizeKey(key)
if err != nil {
return nil, err
}

return r.routing.GetValue(ctx, dhtKey)
}

func (r *RoutingAPI) Put(ctx context.Context, key string, value []byte) error {
if !r.nd.IsOnline {
return coreiface.ErrOffline
}

dhtKey, err := normalizeKey(key)
if err != nil {
return err
}

return r.routing.PutValue(ctx, dhtKey, value)
}

func normalizeKey(s string) (string, error) {
parts := path.SplitList(s)
if len(parts) != 3 ||
parts[0] != "" ||
!(parts[1] == "ipns" || parts[1] == "pk") {
return "", errors.New("invalid key")
}

k, err := peer.Decode(parts[2])
if err != nil {
return "", err
}
return path.Join(append(parts[:2], string(k))), nil
}
4 changes: 4 additions & 0 deletions core/corehttp/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type NodeAPI interface {
// Dag returns an implementation of Dag API
Dag() coreiface.APIDagService

// Routing returns an implementation of Routing API.
// Used for returning signed IPNS records, see IPIP-0328
Routing() coreiface.RoutingAPI

// ResolvePath resolves the path using Unixfs resolver
ResolvePath(context.Context, path.Path) (path.Resolved, error)
}
Expand Down
8 changes: 7 additions & 1 deletion core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,9 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor":
logger.Debugw("serving codec", "path", contentPath)
i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat)
case "application/vnd.ipfs.ipns-record":
logger.Debugw("serving ipns record", "path", contentPath)
i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger)
return
default: // catch-all for unsuported application/vnd.*
err := fmt.Errorf("unsupported format %q", responseFormat)
Expand Down Expand Up @@ -885,6 +888,8 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
return "application/vnd.ipld.dag-json", nil, nil
case "dag-cbor":
return "application/vnd.ipld.dag-cbor", nil, nil
case "ipns-record":
return "application/vnd.ipfs.ipns-record", nil, nil
}
}
// Browsers and other user agents will send Accept header with generic types like:
Expand All @@ -898,7 +903,8 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
if strings.HasPrefix(accept, "application/vnd.ipld") ||
strings.HasPrefix(accept, "application/x-tar") ||
strings.HasPrefix(accept, "application/json") ||
strings.HasPrefix(accept, "application/cbor") {
strings.HasPrefix(accept, "application/cbor") ||
strings.HasPrefix(accept, "application/vnd.ipfs") {
mediatype, params, err := mime.ParseMediaType(accept)
if err != nil {
return "", nil, err
Expand Down
71 changes: 71 additions & 0 deletions core/corehttp/gateway_handler_ipns_record.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package corehttp

import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"

"github.com/gogo/protobuf/proto"
ipns_pb "github.com/ipfs/go-ipns/pb"
path "github.com/ipfs/go-path"
ipath "github.com/ipfs/interface-go-ipfs-core/path"
"go.uber.org/zap"
)

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) {
if contentPath.Namespace() != "ipns" {
err := fmt.Errorf("%s is not an IPNS link", contentPath.String())
webError(w, err.Error(), err, http.StatusBadRequest)
return
}

key := contentPath.String()
key = strings.TrimSuffix(key, "/")
if strings.Count(key, "/") > 2 {
err := errors.New("cannot find ipns key for subpath")
webError(w, err.Error(), err, http.StatusBadRequest)
return
}

rawRecord, err := i.api.Routing().Get(ctx, key)
if err != nil {
webError(w, err.Error(), err, http.StatusInternalServerError)
return
}

var record ipns_pb.IpnsEntry
err = proto.Unmarshal(rawRecord, &record)
if err != nil {
webError(w, err.Error(), err, http.StatusInternalServerError)
return
}

// Set cache control headers based on the TTL set in the IPNS record. If the
// TTL is not present, we use the Last-Modified tag. We are tracking IPNS
// caching on: https://github.com/ipfs/kubo/issues/1818.
// TODO: use addCacheControlHeaders once #1818 is fixed.
w.Header().Set("Etag", getEtag(r, resolvedPath.Cid()))
if record.Ttl != nil {
seconds := int(time.Duration(*record.Ttl).Seconds())
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds))
} else {
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
}

// Set Content-Disposition
var name string
if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" {
name = urlFilename
} else {
name = path.SplitList(key)[2] + ".ipns-record"
}
setContentDispositionHeader(w, name, "attachment")

w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record")
w.Header().Set("X-Content-Type-Options", "nosniff")

_, _ = w.Write(rawRecord)
}
2 changes: 1 addition & 1 deletion docs/examples/kubo-as-a-library/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ replace github.com/ipfs/kubo => ./../../..

require (
github.com/ipfs/go-libipfs v0.3.0
github.com/ipfs/interface-go-ipfs-core v0.9.0
github.com/ipfs/interface-go-ipfs-core v0.10.0
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
github.com/libp2p/go-libp2p v0.24.2
github.com/multiformats/go-multiaddr v0.8.0
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/kubo-as-a-library/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -594,8 +594,8 @@ github.com/ipfs/go-unixfsnode v1.5.1/go.mod h1:ed79DaG9IEuZITJVQn4U6MZDftv6I3ygU
github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0=
github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs=
github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU=
github.com/ipfs/interface-go-ipfs-core v0.9.0 h1:+RCouVtSU/SldgkqWufjIu1smmGaSyKgUIfbYwLukgI=
github.com/ipfs/interface-go-ipfs-core v0.9.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0=
github.com/ipfs/interface-go-ipfs-core v0.10.0 h1:b/psL1oqJcySdQAsIBfW5ZJJkOAsYlhWtC0/Qvr4WiM=
github.com/ipfs/interface-go-ipfs-core v0.10.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0=
github.com/ipld/edelweiss v0.2.0 h1:KfAZBP8eeJtrLxLhi7r3N0cBCo7JmwSRhOJp3WSpNjk=
github.com/ipld/edelweiss v0.2.0/go.mod h1:FJAzJRCep4iI8FOFlRriN9n0b7OuX3T/S9++NpBDmA4=
github.com/ipld/go-car v0.4.0 h1:U6W7F1aKF/OJMHovnOVdst2cpQE5GhmHibQkAixgNcQ=
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ require (
github.com/ipfs/go-unixfs v0.4.2
github.com/ipfs/go-unixfsnode v1.5.1
github.com/ipfs/go-verifcid v0.0.2
github.com/ipfs/interface-go-ipfs-core v0.9.0
github.com/ipfs/interface-go-ipfs-core v0.10.0
github.com/ipld/go-car v0.4.0
github.com/ipld/go-car/v2 v2.5.1
github.com/ipld/go-codec-dagpb v1.5.0
Expand Down
Loading

0 comments on commit a3c70a1

Please sign in to comment.