Skip to content

Commit e57dd07

Browse files
committed
feat: expose routing v1 server via optional setting
1 parent 4acadd4 commit e57dd07

File tree

9 files changed

+222
-8
lines changed

9 files changed

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

docs/examples/kubo-as-a-library/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ go 1.18
77
replace github.com/ipfs/kubo => ./../../..
88

99
require (
10-
github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b
10+
github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44
1111
github.com/ipfs/kubo v0.0.0-00010101000000-000000000000
1212
github.com/libp2p/go-libp2p v0.27.3
1313
github.com/multiformats/go-multiaddr v0.9.0

docs/examples/kubo-as-a-library/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,8 +321,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
321321
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
322322
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
323323
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
324-
github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b h1:6EVpfwbBgwhfZOA19i55jOGokKOy+OaQAm1dg4RbXmc=
325-
github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
324+
github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44 h1:C5U/SZW51/AiY3t4dgC0BWvP/4U5v5zgrHIWS7N5OeM=
325+
github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
326326
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
327327
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
328328
github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/gogo/protobuf v1.3.2
1717
github.com/google/uuid v1.3.0
1818
github.com/hashicorp/go-multierror v1.1.1
19-
github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b
19+
github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44
2020
github.com/ipfs/go-block-format v0.1.2
2121
github.com/ipfs/go-cid v0.4.1
2222
github.com/ipfs/go-cidutil v0.1.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
356356
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
357357
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
358358
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
359-
github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b h1:6EVpfwbBgwhfZOA19i55jOGokKOy+OaQAm1dg4RbXmc=
360-
github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
359+
github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44 h1:C5U/SZW51/AiY3t4dgC0BWvP/4U5v5zgrHIWS7N5OeM=
360+
github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y=
361361
github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA=
362362
github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU=
363363
github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY=

test/cli/content_routing_http_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type fakeHTTPContentRouter struct {
2424
provideCalls int
2525
}
2626

27-
func (r *fakeHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid) (iter.ResultIter[types.ProviderResponse], error) {
27+
func (r *fakeHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid, stream bool) (iter.ResultIter[types.ProviderResponse], error) {
2828
r.m.Lock()
2929
defer r.m.Unlock()
3030
r.findProvidersCalls++

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)