Skip to content

Commit

Permalink
feat(gateway): IPNS record response format (IPIP-351)
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Dec 8, 2022
1 parent 5e5d15a commit 1bdb9f5
Show file tree
Hide file tree
Showing 15 changed files with 287 additions and 117 deletions.
1 change: 1 addition & 0 deletions core/commands/commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ func TestCommands(t *testing.T) {
"/name/pubsub/state",
"/name/pubsub/subs",
"/name/resolve",
"/name/verify-record",
"/object",
"/object/data",
"/object/diff",
Expand Down
90 changes: 86 additions & 4 deletions core/commands/name/name.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
package name

import (
"github.com/ipfs/go-ipfs-cmds"
"bytes"
"fmt"
"io"
"strings"
"text/tabwriter"
"time"

"github.com/gogo/protobuf/proto"
cmds "github.com/ipfs/go-ipfs-cmds"
"github.com/ipfs/go-ipns"
ipns_pb "github.com/ipfs/go-ipns/pb"
cmdenv "github.com/ipfs/kubo/core/commands/cmdenv"
"github.com/libp2p/go-libp2p/core/peer"
)

type IpnsEntry struct {
Expand Down Expand Up @@ -59,8 +71,78 @@ Resolve the value of a dnslink:
},

Subcommands: map[string]*cmds.Command{
"publish": PublishCmd,
"resolve": IpnsCmd,
"pubsub": IpnsPubsubCmd,
"publish": PublishCmd,
"resolve": IpnsCmd,
"pubsub": IpnsPubsubCmd,
"verify-record": IpnsVerifyRecordCmd,
},
}

var IpnsVerifyRecordCmd = &cmds.Command{
Helptext: cmds.HelpText{
Tagline: "Verifies an IPNS Record.",
},
Arguments: []cmds.Argument{
cmds.StringArg("key", true, false, "The IPNS key to validate against."),
cmds.FileArg("record", true, false, "The path to a file with IPNS record to be verified.").EnableStdin(),
},
Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
key := strings.TrimPrefix(req.Arguments[0], "/ipns/")

file, err := cmdenv.GetFileArg(req.Files.Entries())
if err != nil {
return err
}
defer file.Close()

var b bytes.Buffer

_, err = io.Copy(&b, file)
if err != nil {
return err
}

var entry ipns_pb.IpnsEntry
err = proto.Unmarshal(b.Bytes(), &entry)
if err != nil {
return err
}

id, err := peer.Decode(key)
if err != nil {
return err
}

pub, err := id.ExtractPublicKey()
if err != nil {
return err
}

err = ipns.Validate(pub, &entry)
if err != nil {
return err
}

return cmds.EmitOnce(res, &entry)
},
Type: &ipns_pb.IpnsEntry{},
Encoders: cmds.EncoderMap{
cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *ipns_pb.IpnsEntry) error {
tw := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
defer tw.Flush()

fmt.Fprintf(w, "Record is valid:\n\n")
fmt.Fprintf(tw, "Value:\t%q\n", string(out.Value))

if out.Ttl != nil {
fmt.Fprintf(tw, "TTL:\t%d\n", *out.Ttl)
}

validity, err := ipns.GetEOL(out)
if err == nil {
fmt.Fprintf(tw, "Validity:\t%s\n", validity.Format(time.RFC3339))
}
return nil
}),
},
}
118 changes: 16 additions & 102 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 @@ -366,71 +365,25 @@ Different key types can specify other 'best' rules.
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{
Expand Down Expand Up @@ -463,16 +416,7 @@ identified by QmFoo.
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 +432,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 @@ -34,6 +34,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 @@ -445,6 +445,9 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
"application/cbor", "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 @@ -886,6 +889,8 @@ func customResponseFormat(r *http.Request) (mediaType string, params map[string]
return "application/vnd.ipld.dag-cbor", nil, nil
case "cbor":
return "application/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 @@ -896,7 +901,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
Loading

0 comments on commit 1bdb9f5

Please sign in to comment.