Skip to content

Commit

Permalink
feat: expose routing v1 server via optional setting
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed May 29, 2023
1 parent e3126eb commit a01c4f0
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 1 deletion.
4 changes: 4 additions & 0 deletions cmd/ipfs/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,10 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
opts = append(opts, corehttp.P2PProxyOption())
}

if cfg.Gateway.ExposeRoutingAPI.WithDefault(config.DefaultExposeRoutingAPI) {
opts = append(opts, corehttp.RoutingOption())
}

if len(cfg.Gateway.RootRedirect) > 0 {
opts = append(opts, corehttp.RedirectOption("", cfg.Gateway.RootRedirect))
}
Expand Down
9 changes: 8 additions & 1 deletion config/gateway.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package config

const DefaultInlineDNSLink = false
const (
DefaultInlineDNSLink = false
DefaultExposeRoutingAPI = false
)

type GatewaySpec struct {
// Paths is explicit list of path prefixes that should be handled by
Expand Down Expand Up @@ -59,4 +62,8 @@ type Gateway struct {
// PublicGateways configures behavior of known public gateways.
// Each key is a fully qualified domain name (FQDN).
PublicGateways map[string]*GatewaySpec

// ExposeRoutingAPI configures the gateway to expose a Routing v1 HTTP Server
// under /routing/v1: https://specs.ipfs.tech/routing/routing-v1/.
ExposeRoutingAPI Flag
}
102 changes: 102 additions & 0 deletions core/corehttp/routing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package corehttp

import (
"context"
"net"
"net/http"
"time"

"github.com/ipfs/boxo/routing/http/server"
"github.com/ipfs/boxo/routing/http/types"
"github.com/ipfs/boxo/routing/http/types/iter"
cid "github.com/ipfs/go-cid"
core "github.com/ipfs/kubo/core"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/routing"
"github.com/multiformats/go-multiaddr"
)

func RoutingOption() ServeOption {
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
handler := server.Handler(&contentRouter{n})
mux.Handle("/routing/v1/", handler)
return mux, nil
}
}

type contentRouter struct {
n *core.IpfsNode
}

func (r *contentRouter) FindProviders(ctx context.Context, key cid.Cid, limit int) (iter.ResultIter[types.ProviderResponse], error) {
ctx, cancel := context.WithCancel(ctx)
ch := r.n.Routing.FindProvidersAsync(ctx, key, limit)
return iter.ToResultIter[types.ProviderResponse](&peerChanIter{
ch: ch,
cancel: cancel,
}), nil
}

func (r *contentRouter) Provide(ctx context.Context, req *server.WriteProvideRequest) (types.ProviderResponse, error) {
// Kubo /routing/v1 endpoint does not support write operations.
return nil, routing.ErrNotSupported
}

func (r *contentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) {
// Kubo /routing/v1 endpoint does not support write operations.
return 0, routing.ErrNotSupported
}

type peerChanIter struct {
ch <-chan peer.AddrInfo
cancel context.CancelFunc
next *peer.AddrInfo
}

func (it *peerChanIter) Next() bool {
addr, ok := <-it.ch
if ok {
it.next = &addr
return true
} else {
it.next = nil
return false
}
}

func (it *peerChanIter) Val() types.ProviderResponse {
if it.next == nil {
return nil
}

// We don't know what type of protocol this peer provides. It is likely Bitswap
// but it might not be. Therefore, we set an unknown protocol with an unknown schema.
rec := &providerRecord{
Protocol: "transport-unknown",
Schema: "unknown",
ID: it.next.ID,
Addrs: it.next.Addrs,
}

return rec
}

func (it *peerChanIter) Close() error {
it.cancel()
return nil
}

type providerRecord struct {
Protocol string
Schema string
ID peer.ID
Addrs []multiaddr.Multiaddr
}

func (pr *providerRecord) GetProtocol() string {
return pr.Protocol
}

func (pr *providerRecord) GetSchema() string {
return pr.Schema
}
92 changes: 92 additions & 0 deletions test/cli/routing_http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package cli

import (
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"testing"

"github.com/ipfs/kubo/config"
"github.com/ipfs/kubo/test/cli/harness"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/stretchr/testify/assert"
)

func TestRoutingV1(t *testing.T) {
t.Parallel()
nodes := harness.NewT(t).NewNodes(5).Init()
nodes.ForEachPar(func(node *harness.Node) {
node.UpdateConfig(func(cfg *config.Config) {
cfg.Gateway.ExposeRoutingAPI = config.True
cfg.Routing.Type = config.NewOptionalString("dht")
})
})
nodes.StartDaemons().Connect()

type record struct {
Protocol string
Schema string
ID peer.ID
Addrs []string
}

type providers struct {
Providers []record
}

t.Run("Non-streaming response with Accept: application/json", func(t *testing.T) {
t.Parallel()

cid := nodes[2].IPFSAddStr("hello world")
_ = nodes[3].IPFSAddStr("hello world")

resp := nodes[1].GatewayClient().Get("/routing/v1/providers/"+cid, func(r *http.Request) {
r.Header.Set("Accept", "application/json")
})
assert.Equal(t, resp.Headers.Get("Content-Type"), "application/json")
assert.Equal(t, http.StatusOK, resp.StatusCode)

var providers *providers
err := json.Unmarshal([]byte(resp.Body), &providers)
assert.NoError(t, err)

var peers []peer.ID
for _, prov := range providers.Providers {
peers = append(peers, prov.ID)
}
assert.Contains(t, peers, nodes[2].PeerID())
assert.Contains(t, peers, nodes[3].PeerID())
})

t.Run("Streaming response with Accept: application/x-ndjson", func(t *testing.T) {
t.Parallel()

cid := nodes[1].IPFSAddStr("hello world")
_ = nodes[4].IPFSAddStr("hello world")

resp := nodes[0].GatewayClient().Get("/routing/v1/providers/"+cid, func(r *http.Request) {
r.Header.Set("Accept", "application/x-ndjson")
})
assert.Equal(t, resp.Headers.Get("Content-Type"), "application/x-ndjson")
assert.Equal(t, http.StatusOK, resp.StatusCode)

var peers []peer.ID
dec := json.NewDecoder(strings.NewReader(resp.Body))

for {
var record *record
err := dec.Decode(&record)
if errors.Is(err, io.EOF) {
break
}

assert.NoError(t, err)
peers = append(peers, record.ID)
}

assert.Contains(t, peers, nodes[1].PeerID())
assert.Contains(t, peers, nodes[4].PeerID())
})
}

0 comments on commit a01c4f0

Please sign in to comment.