Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gateway): IPNS record response format (IPIP-351) #9399

Merged
merged 2 commits into from
Jan 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 This changes both /api/v0/dht/get and /api/v0/routing/get (https://docs.ipfs.tech/reference/kubo/rpc/):

-{
-  "Extra": "<string>",
-  "ID": "<peer-id>",
-  "Responses": [
-    {
-      "Addrs": [
-        "<multiaddr-string>"
-      ],
-      "ID": "peer-id"
-    }
-  ],
-  "Type": "<int>"
-}
+"<base64-string>"

and breaks --verbose flag in both.

I've removed flag and marked routing/get as Experimental (dht/get was already deprecated)

}

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{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 This changes both /api/v0/dht/put and /api/v0/routing/put (https://docs.ipfs.tech/reference/kubo/rpc/):

-{
-  "Extra": "<string>",
-  "ID": "<peer-id>",
-  "Responses": [
-    {
-      "Addrs": [
-        "<multiaddr-string>"
-      ],
-      "ID": "peer-id"
-    }
-  ],
-  "Type": "<int>"
-}
+"<base64-string>"

and breaks --verbose flag in both.

I've removed flag and marked routing/put as Experimental (dht/put was already deprecated)

}

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