From e57dd076e7071330253f4e5bf6e464c2d79b6cd8 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Fri, 12 May 2023 15:15:14 +0200 Subject: [PATCH] feat: expose routing v1 server via optional setting --- cmd/ipfs/daemon.go | 4 + config/gateway.go | 9 +- core/corehttp/routing.go | 111 +++++++++++++++++++++++++ docs/examples/kubo-as-a-library/go.mod | 2 +- docs/examples/kubo-as-a-library/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- test/cli/content_routing_http_test.go | 2 +- test/cli/routing_http_test.go | 92 ++++++++++++++++++++ 9 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 core/corehttp/routing.go create mode 100644 test/cli/routing_http_test.go diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 1e03a8264a8e..c6f3e027390c 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -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)) } diff --git a/config/gateway.go b/config/gateway.go index 8ae312b59aee..7c3446d87e0b 100644 --- a/config/gateway.go +++ b/config/gateway.go @@ -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 @@ -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 } diff --git a/core/corehttp/routing.go b/core/corehttp/routing.go new file mode 100644 index 000000000000..9d1c3248ba3e --- /dev/null +++ b/core/corehttp/routing.go @@ -0,0 +1,111 @@ +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" +) + +const ( + streamingProvidersCount = 0 + nonStreamingProvidersCount = 20 +) + +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, streaming bool) (iter.ResultIter[types.ProviderResponse], error) { + ctx, cancel := context.WithCancel(ctx) + count := nonStreamingProvidersCount + if streaming { + count = streamingProvidersCount + } + ch := r.n.Routing.FindProvidersAsync(ctx, key, count) + 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 +} diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 52e3c064699c..23fcff328e24 100644 --- a/docs/examples/kubo-as-a-library/go.mod +++ b/docs/examples/kubo-as-a-library/go.mod @@ -7,7 +7,7 @@ go 1.18 replace github.com/ipfs/kubo => ./../../.. require ( - github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b + github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44 github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 github.com/libp2p/go-libp2p v0.27.3 github.com/multiformats/go-multiaddr v0.9.0 diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 3111d683f532..f503ff40672c 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -321,8 +321,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b h1:6EVpfwbBgwhfZOA19i55jOGokKOy+OaQAm1dg4RbXmc= -github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y= +github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44 h1:C5U/SZW51/AiY3t4dgC0BWvP/4U5v5zgrHIWS7N5OeM= +github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY= diff --git a/go.mod b/go.mod index a756f0f32068..396e00f123fd 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/gogo/protobuf v1.3.2 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b + github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44 github.com/ipfs/go-block-format v0.1.2 github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-cidutil v0.1.0 diff --git a/go.sum b/go.sum index af7ec2fc01ae..bd8cbe004d7c 100644 --- a/go.sum +++ b/go.sum @@ -356,8 +356,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b h1:6EVpfwbBgwhfZOA19i55jOGokKOy+OaQAm1dg4RbXmc= -github.com/ipfs/boxo v0.8.2-0.20230510114019-33e3f0cd052b/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y= +github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44 h1:C5U/SZW51/AiY3t4dgC0BWvP/4U5v5zgrHIWS7N5OeM= +github.com/ipfs/boxo v0.8.2-0.20230515105410-d96e912ecb44/go.mod h1:Ej2r08Z4VIaFKqY08UXMNhwcLf6VekHhK8c+KqA1B9Y= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY= diff --git a/test/cli/content_routing_http_test.go b/test/cli/content_routing_http_test.go index acdea7029dfc..f48f655d9c8f 100644 --- a/test/cli/content_routing_http_test.go +++ b/test/cli/content_routing_http_test.go @@ -24,7 +24,7 @@ type fakeHTTPContentRouter struct { provideCalls int } -func (r *fakeHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid) (iter.ResultIter[types.ProviderResponse], error) { +func (r *fakeHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid, stream bool) (iter.ResultIter[types.ProviderResponse], error) { r.m.Lock() defer r.m.Unlock() r.findProvidersCalls++ diff --git a/test/cli/routing_http_test.go b/test/cli/routing_http_test.go new file mode 100644 index 000000000000..c4bd4cb86938 --- /dev/null +++ b/test/cli/routing_http_test.go @@ -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()) + }) +}