Skip to content

Commit a01c4f0

Browse files
committed
feat: expose routing v1 server via optional setting
1 parent e3126eb commit a01c4f0

File tree

4 files changed

+206
-1
lines changed

4 files changed

+206
-1
lines changed

cmd/ipfs/daemon.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,10 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e
847847
opts = append(opts, corehttp.P2PProxyOption())
848848
}
849849

850+
if cfg.Gateway.ExposeRoutingAPI.WithDefault(config.DefaultExposeRoutingAPI) {
851+
opts = append(opts, corehttp.RoutingOption())
852+
}
853+
850854
if len(cfg.Gateway.RootRedirect) > 0 {
851855
opts = append(opts, corehttp.RedirectOption("", cfg.Gateway.RootRedirect))
852856
}

config/gateway.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package config
22

3-
const DefaultInlineDNSLink = false
3+
const (
4+
DefaultInlineDNSLink = false
5+
DefaultExposeRoutingAPI = false
6+
)
47

58
type GatewaySpec struct {
69
// Paths is explicit list of path prefixes that should be handled by
@@ -59,4 +62,8 @@ type Gateway struct {
5962
// PublicGateways configures behavior of known public gateways.
6063
// Each key is a fully qualified domain name (FQDN).
6164
PublicGateways map[string]*GatewaySpec
65+
66+
// ExposeRoutingAPI configures the gateway to expose a Routing v1 HTTP Server
67+
// under /routing/v1: https://specs.ipfs.tech/routing/routing-v1/.
68+
ExposeRoutingAPI Flag
6269
}

core/corehttp/routing.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package corehttp
2+
3+
import (
4+
"context"
5+
"net"
6+
"net/http"
7+
"time"
8+
9+
"github.com/ipfs/boxo/routing/http/server"
10+
"github.com/ipfs/boxo/routing/http/types"
11+
"github.com/ipfs/boxo/routing/http/types/iter"
12+
cid "github.com/ipfs/go-cid"
13+
core "github.com/ipfs/kubo/core"
14+
"github.com/libp2p/go-libp2p/core/peer"
15+
"github.com/libp2p/go-libp2p/core/routing"
16+
"github.com/multiformats/go-multiaddr"
17+
)
18+
19+
func RoutingOption() ServeOption {
20+
return func(n *core.IpfsNode, _ net.Listener, mux *http.ServeMux) (*http.ServeMux, error) {
21+
handler := server.Handler(&contentRouter{n})
22+
mux.Handle("/routing/v1/", handler)
23+
return mux, nil
24+
}
25+
}
26+
27+
type contentRouter struct {
28+
n *core.IpfsNode
29+
}
30+
31+
func (r *contentRouter) FindProviders(ctx context.Context, key cid.Cid, limit int) (iter.ResultIter[types.ProviderResponse], error) {
32+
ctx, cancel := context.WithCancel(ctx)
33+
ch := r.n.Routing.FindProvidersAsync(ctx, key, limit)
34+
return iter.ToResultIter[types.ProviderResponse](&peerChanIter{
35+
ch: ch,
36+
cancel: cancel,
37+
}), nil
38+
}
39+
40+
func (r *contentRouter) Provide(ctx context.Context, req *server.WriteProvideRequest) (types.ProviderResponse, error) {
41+
// Kubo /routing/v1 endpoint does not support write operations.
42+
return nil, routing.ErrNotSupported
43+
}
44+
45+
func (r *contentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) {
46+
// Kubo /routing/v1 endpoint does not support write operations.
47+
return 0, routing.ErrNotSupported
48+
}
49+
50+
type peerChanIter struct {
51+
ch <-chan peer.AddrInfo
52+
cancel context.CancelFunc
53+
next *peer.AddrInfo
54+
}
55+
56+
func (it *peerChanIter) Next() bool {
57+
addr, ok := <-it.ch
58+
if ok {
59+
it.next = &addr
60+
return true
61+
} else {
62+
it.next = nil
63+
return false
64+
}
65+
}
66+
67+
func (it *peerChanIter) Val() types.ProviderResponse {
68+
if it.next == nil {
69+
return nil
70+
}
71+
72+
// We don't know what type of protocol this peer provides. It is likely Bitswap
73+
// but it might not be. Therefore, we set an unknown protocol with an unknown schema.
74+
rec := &providerRecord{
75+
Protocol: "transport-unknown",
76+
Schema: "unknown",
77+
ID: it.next.ID,
78+
Addrs: it.next.Addrs,
79+
}
80+
81+
return rec
82+
}
83+
84+
func (it *peerChanIter) Close() error {
85+
it.cancel()
86+
return nil
87+
}
88+
89+
type providerRecord struct {
90+
Protocol string
91+
Schema string
92+
ID peer.ID
93+
Addrs []multiaddr.Multiaddr
94+
}
95+
96+
func (pr *providerRecord) GetProtocol() string {
97+
return pr.Protocol
98+
}
99+
100+
func (pr *providerRecord) GetSchema() string {
101+
return pr.Schema
102+
}

test/cli/routing_http_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package cli
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"io"
7+
"net/http"
8+
"strings"
9+
"testing"
10+
11+
"github.com/ipfs/kubo/config"
12+
"github.com/ipfs/kubo/test/cli/harness"
13+
"github.com/libp2p/go-libp2p/core/peer"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestRoutingV1(t *testing.T) {
18+
t.Parallel()
19+
nodes := harness.NewT(t).NewNodes(5).Init()
20+
nodes.ForEachPar(func(node *harness.Node) {
21+
node.UpdateConfig(func(cfg *config.Config) {
22+
cfg.Gateway.ExposeRoutingAPI = config.True
23+
cfg.Routing.Type = config.NewOptionalString("dht")
24+
})
25+
})
26+
nodes.StartDaemons().Connect()
27+
28+
type record struct {
29+
Protocol string
30+
Schema string
31+
ID peer.ID
32+
Addrs []string
33+
}
34+
35+
type providers struct {
36+
Providers []record
37+
}
38+
39+
t.Run("Non-streaming response with Accept: application/json", func(t *testing.T) {
40+
t.Parallel()
41+
42+
cid := nodes[2].IPFSAddStr("hello world")
43+
_ = nodes[3].IPFSAddStr("hello world")
44+
45+
resp := nodes[1].GatewayClient().Get("/routing/v1/providers/"+cid, func(r *http.Request) {
46+
r.Header.Set("Accept", "application/json")
47+
})
48+
assert.Equal(t, resp.Headers.Get("Content-Type"), "application/json")
49+
assert.Equal(t, http.StatusOK, resp.StatusCode)
50+
51+
var providers *providers
52+
err := json.Unmarshal([]byte(resp.Body), &providers)
53+
assert.NoError(t, err)
54+
55+
var peers []peer.ID
56+
for _, prov := range providers.Providers {
57+
peers = append(peers, prov.ID)
58+
}
59+
assert.Contains(t, peers, nodes[2].PeerID())
60+
assert.Contains(t, peers, nodes[3].PeerID())
61+
})
62+
63+
t.Run("Streaming response with Accept: application/x-ndjson", func(t *testing.T) {
64+
t.Parallel()
65+
66+
cid := nodes[1].IPFSAddStr("hello world")
67+
_ = nodes[4].IPFSAddStr("hello world")
68+
69+
resp := nodes[0].GatewayClient().Get("/routing/v1/providers/"+cid, func(r *http.Request) {
70+
r.Header.Set("Accept", "application/x-ndjson")
71+
})
72+
assert.Equal(t, resp.Headers.Get("Content-Type"), "application/x-ndjson")
73+
assert.Equal(t, http.StatusOK, resp.StatusCode)
74+
75+
var peers []peer.ID
76+
dec := json.NewDecoder(strings.NewReader(resp.Body))
77+
78+
for {
79+
var record *record
80+
err := dec.Decode(&record)
81+
if errors.Is(err, io.EOF) {
82+
break
83+
}
84+
85+
assert.NoError(t, err)
86+
peers = append(peers, record.ID)
87+
}
88+
89+
assert.Contains(t, peers, nodes[1].PeerID())
90+
assert.Contains(t, peers, nodes[4].PeerID())
91+
})
92+
}

0 commit comments

Comments
 (0)