From 460658620792eb433331142b1435dad89630b15a Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Fri, 25 Aug 2023 17:30:04 +0200 Subject: [PATCH] feat(gateway): expose /routing/v1 server (opt-in) (#9877) --- cmd/ipfs/daemon.go | 10 ++ config/gateway.go | 5 + core/corehttp/routing.go | 129 +++++++++++++++ core/node/libp2p/routingopt.go | 3 +- docs/changelogs/v0.23.md | 8 + docs/config.md | 10 ++ docs/examples/kubo-as-a-library/go.mod | 3 +- docs/examples/kubo-as-a-library/go.sum | 5 +- go.mod | 2 +- go.sum | 4 +- routing/delegated.go | 2 + routing/wrapper.go | 21 +-- test/cli/content_routing_http_test.go | 25 +-- ... delegated_routing_v1_http_client_test.go} | 36 +++-- .../delegated_routing_v1_http_proxy_test.go | 147 ++++++++++++++++++ .../delegated_routing_v1_http_server_test.go | 145 +++++++++++++++++ test/dependencies/go.mod | 2 +- test/dependencies/go.sum | 4 +- 18 files changed, 505 insertions(+), 56 deletions(-) create mode 100644 core/corehttp/routing.go rename test/cli/{delegated_routing_http_test.go => delegated_routing_v1_http_client_test.go} (87%) create mode 100644 test/cli/delegated_routing_v1_http_proxy_test.go create mode 100644 test/cli/delegated_routing_v1_http_server_test.go diff --git a/cmd/ipfs/daemon.go b/cmd/ipfs/daemon.go index 7be97e23ba1..55ecaf2c09e 100644 --- a/cmd/ipfs/daemon.go +++ b/cmd/ipfs/daemon.go @@ -832,6 +832,12 @@ func serveHTTPGateway(req *cmds.Request, cctx *oldcmds.Context) (<-chan error, e fmt.Printf("Gateway server listening on %s\n", listener.Multiaddr()) } + if cfg.Gateway.ExposeRoutingAPI.WithDefault(config.DefaultExposeRoutingAPI) { + for _, listener := range listeners { + fmt.Printf("Routing V1 API exposed at http://%s/routing/v1\n", listener.Addr()) + } + } + cmdctx := *cctx cmdctx.Gateway = true @@ -848,6 +854,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 a655828496e..dee0a93e22c 100644 --- a/config/gateway.go +++ b/config/gateway.go @@ -3,6 +3,7 @@ package config const ( DefaultInlineDNSLink = false DefaultDeserializedResponses = true + DefaultExposeRoutingAPI = false ) type GatewaySpec struct { @@ -72,4 +73,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 port to expose + // routing system as HTTP API at /routing/v1 (https://specs.ipfs.tech/routing/http-routing-v1/). + ExposeRoutingAPI Flag } diff --git a/core/corehttp/routing.go b/core/corehttp/routing.go new file mode 100644 index 00000000000..357122a459a --- /dev/null +++ b/core/corehttp/routing.go @@ -0,0 +1,129 @@ +package corehttp + +import ( + "context" + "net" + "net/http" + "time" + + "github.com/ipfs/boxo/ipns" + "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" +) + +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.Record], error) { + ctx, cancel := context.WithCancel(ctx) + ch := r.n.Routing.FindProvidersAsync(ctx, key, limit) + return iter.ToResultIter[types.Record](&peerChanIter{ + ch: ch, + cancel: cancel, + }), nil +} + +// nolint deprecated +func (r *contentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) { + return 0, routing.ErrNotSupported +} + +func (r *contentRouter) FindPeers(ctx context.Context, pid peer.ID, limit int) (iter.ResultIter[types.Record], error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + addr, err := r.n.Routing.FindPeer(ctx, pid) + if err != nil { + return nil, err + } + + rec := &types.PeerRecord{ + Schema: types.SchemaPeer, + ID: &addr.ID, + } + + for _, addr := range addr.Addrs { + rec.Addrs = append(rec.Addrs, types.Multiaddr{Multiaddr: addr}) + } + + return iter.ToResultIter[types.Record](iter.FromSlice[types.Record]([]types.Record{rec})), nil +} + +func (r *contentRouter) GetIPNS(ctx context.Context, name ipns.Name) (*ipns.Record, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + raw, err := r.n.Routing.GetValue(ctx, string(name.RoutingKey())) + if err != nil { + return nil, err + } + + return ipns.UnmarshalRecord(raw) +} + +func (r *contentRouter) PutIPNS(ctx context.Context, name ipns.Name, record *ipns.Record) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + raw, err := ipns.MarshalRecord(record) + if err != nil { + return err + } + + // The caller guarantees that name matches the record. This is double checked + // by the internals of PutValue. + return r.n.Routing.PutValue(ctx, string(name.RoutingKey()), raw) +} + +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.Record { + if it.next == nil { + return nil + } + + rec := &types.PeerRecord{ + Schema: types.SchemaPeer, + ID: &it.next.ID, + } + + for _, addr := range it.next.Addrs { + rec.Addrs = append(rec.Addrs, types.Multiaddr{Multiaddr: addr}) + } + + return rec +} + +func (it *peerChanIter) Close() error { + it.cancel() + return nil +} diff --git a/core/node/libp2p/routingopt.go b/core/node/libp2p/routingopt.go index 82b68361d64..a58a8c49885 100644 --- a/core/node/libp2p/routingopt.go +++ b/core/node/libp2p/routingopt.go @@ -139,7 +139,8 @@ func ConstructDelegatedRouting(routers config.Routers, methods config.Methods, p PeerID: peerID, Addrs: httpAddrsFromConfig(addrs), PrivKeyB64: privKey, - }) + }, + ) } } diff --git a/docs/changelogs/v0.23.md b/docs/changelogs/v0.23.md index c2267a8e198..df6a1c0316e 100644 --- a/docs/changelogs/v0.23.md +++ b/docs/changelogs/v0.23.md @@ -9,6 +9,7 @@ - [Mplex deprecation](#mplex-deprecation) - [Gateway: meaningful CAR responses on Not Found errors](#gateway-meaningful-car-responses-on-not-found-errors) - [Binary characters in file names: no longer works with old clients and new Kubo servers](#binary-characters-in-file-names-no-longer-works-with-old-clients-and-new-kubo-servers) + - [Self-hosting `/routing/v1` endpoint for delegated routing needs](#self-hosting-routingv1-endpoint-for-delegated-routing-needs) - [๐Ÿ“ Changelog](#-changelog) - [๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors](#-contributors) @@ -59,6 +60,13 @@ the compatibility table: *Old clients can only send Unicode file paths to the server. +#### Self-hosting `/routing/v1` endpoint for delegated routing needs + +The `Routing` system configured in Kubo can be now exposed on the gateway port as a standard +HTTP [Routing V1](https://specs.ipfs.tech/routing/http-routing-v1/) API endpoint. This allows +self-hosting and experimentation with custom delegated routers. This is disabled by default, +but can be enabled by setting [`Gateway.ExposeRoutingAPI`](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewayexposeroutingapi) to `true` . + ### ๐Ÿ“ Changelog ### ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ Contributors diff --git a/docs/config.md b/docs/config.md index c24b2b23f83..dd3eed39060 100644 --- a/docs/config.md +++ b/docs/config.md @@ -658,6 +658,16 @@ Default: `true` Type: `flag` +#### `Gateway.ExposeRoutingAPI` + +An optional flag to expose Kubo `Routing` system on the gateway port as a [Routing +V1](https://specs.ipfs.tech/routing/routing-v1/) endpoint. This only affects your +local gateway, at `127.0.0.1`. + +Default: `false` + +Type: `flag` + ### `Gateway.HTTPHeaders` Headers to set on gateway responses. diff --git a/docs/examples/kubo-as-a-library/go.mod b/docs/examples/kubo-as-a-library/go.mod index 29aa6c5a79b..bb88ad99696 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.20 replace github.com/ipfs/kubo => ./../../.. require ( - github.com/ipfs/boxo v0.12.1-0.20230822135301-303595bcdba7 + github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd github.com/ipfs/kubo v0.0.0-00010101000000-000000000000 github.com/libp2p/go-libp2p v0.30.0 github.com/multiformats/go-multiaddr v0.11.0 @@ -52,7 +52,6 @@ require ( github.com/google/gopacket v1.1.19 // indirect github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect github.com/google/uuid v1.3.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect github.com/hannahhoward/go-pubsub v0.0.0-20200423002714-8d62886cc36e // indirect diff --git a/docs/examples/kubo-as-a-library/go.sum b/docs/examples/kubo-as-a-library/go.sum index 0a262a49785..0ab47be1aef 100644 --- a/docs/examples/kubo-as-a-library/go.sum +++ b/docs/examples/kubo-as-a-library/go.sum @@ -270,7 +270,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= @@ -301,8 +300,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.12.1-0.20230822135301-303595bcdba7 h1:f7n4M8UIf+4BY6Q0kcZ5FbpkxKaIqq/BW3evqI87DNo= -github.com/ipfs/boxo v0.12.1-0.20230822135301-303595bcdba7/go.mod h1:btrtHy0lmO1ODMECbbEY1pxNtrLilvKSYLoGQt1yYCk= +github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd h1:uAp9W7FRQ7W16FENlURZqBh7/3PnakG0DjHpKPirKVY= +github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd/go.mod h1:btrtHy0lmO1ODMECbbEY1pxNtrLilvKSYLoGQt1yYCk= 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 caebd660e4e..d3ff353c676 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/google/uuid v1.3.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/ipfs/boxo v0.12.1-0.20230822135301-303595bcdba7 + github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd 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 2ffd7ff357a..e478ea73338 100644 --- a/go.sum +++ b/go.sum @@ -335,8 +335,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.12.1-0.20230822135301-303595bcdba7 h1:f7n4M8UIf+4BY6Q0kcZ5FbpkxKaIqq/BW3evqI87DNo= -github.com/ipfs/boxo v0.12.1-0.20230822135301-303595bcdba7/go.mod h1:btrtHy0lmO1ODMECbbEY1pxNtrLilvKSYLoGQt1yYCk= +github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd h1:uAp9W7FRQ7W16FENlURZqBh7/3PnakG0DjHpKPirKVY= +github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd/go.mod h1:btrtHy0lmO1ODMECbbEY1pxNtrLilvKSYLoGQt1yYCk= 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-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= diff --git a/routing/delegated.go b/routing/delegated.go index 6d34970f556..e830c1aa197 100644 --- a/routing/delegated.go +++ b/routing/delegated.go @@ -224,6 +224,8 @@ func httpRoutingFromConfig(conf config.Router, extraHTTP *ExtraHTTPParams) (rout return &httpRoutingWrapper{ ContentRouting: cr, + PeerRouting: cr, + ValueStore: cr, ProvideManyRouter: cr, }, nil } diff --git a/routing/wrapper.go b/routing/wrapper.go index 10df177e017..680aef263d2 100644 --- a/routing/wrapper.go +++ b/routing/wrapper.go @@ -4,7 +4,6 @@ import ( "context" routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" - "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" ) @@ -22,27 +21,11 @@ var ( // http delegated routing. type httpRoutingWrapper struct { routing.ContentRouting + routing.PeerRouting + routing.ValueStore routinghelpers.ProvideManyRouter } func (c *httpRoutingWrapper) Bootstrap(ctx context.Context) error { return nil } - -func (c *httpRoutingWrapper) FindPeer(ctx context.Context, id peer.ID) (peer.AddrInfo, error) { - return peer.AddrInfo{}, routing.ErrNotSupported -} - -func (c *httpRoutingWrapper) PutValue(context.Context, string, []byte, ...routing.Option) error { - return routing.ErrNotSupported -} - -func (c *httpRoutingWrapper) GetValue(context.Context, string, ...routing.Option) ([]byte, error) { - return nil, routing.ErrNotSupported -} - -func (c *httpRoutingWrapper) SearchValue(context.Context, string, ...routing.Option) (<-chan []byte, error) { - out := make(chan []byte) - close(out) - return out, routing.ErrNotSupported -} diff --git a/test/cli/content_routing_http_test.go b/test/cli/content_routing_http_test.go index 01b602bb7ed..652c8ac59eb 100644 --- a/test/cli/content_routing_http_test.go +++ b/test/cli/content_routing_http_test.go @@ -16,42 +16,45 @@ import ( "github.com/ipfs/go-cid" "github.com/ipfs/kubo/test/cli/harness" "github.com/ipfs/kubo/test/cli/testutils" + "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/routing" "github.com/stretchr/testify/assert" ) type fakeHTTPContentRouter struct { - m sync.Mutex - findProvidersCalls int - provideCalls int + m sync.Mutex + provideBitswapCalls int + findProvidersCalls int + findPeersCalls int } -func (r *fakeHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid, limit int) (iter.ResultIter[types.ProviderResponse], error) { +func (r *fakeHTTPContentRouter) FindProviders(ctx context.Context, key cid.Cid, limit int) (iter.ResultIter[types.Record], error) { r.m.Lock() defer r.m.Unlock() r.findProvidersCalls++ - return iter.FromSlice([]iter.Result[types.ProviderResponse]{}), nil + return iter.FromSlice([]iter.Result[types.Record]{}), nil } +// nolint deprecated func (r *fakeHTTPContentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) { r.m.Lock() defer r.m.Unlock() - r.provideCalls++ + r.provideBitswapCalls++ return 0, nil } -func (r *fakeHTTPContentRouter) Provide(ctx context.Context, req *server.WriteProvideRequest) (types.ProviderResponse, error) { +func (r *fakeHTTPContentRouter) FindPeers(ctx context.Context, pid peer.ID, limit int) (iter.ResultIter[types.Record], error) { r.m.Lock() defer r.m.Unlock() - r.provideCalls++ - return nil, nil + r.findPeersCalls++ + return iter.FromSlice([]iter.Result[types.Record]{}), nil } -func (r *fakeHTTPContentRouter) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) { +func (r *fakeHTTPContentRouter) GetIPNS(ctx context.Context, name ipns.Name) (*ipns.Record, error) { return nil, routing.ErrNotSupported } -func (r *fakeHTTPContentRouter) ProvideIPNSRecord(ctx context.Context, name ipns.Name, rec *ipns.Record) error { +func (r *fakeHTTPContentRouter) PutIPNS(ctx context.Context, name ipns.Name, rec *ipns.Record) error { return routing.ErrNotSupported } diff --git a/test/cli/delegated_routing_http_test.go b/test/cli/delegated_routing_v1_http_client_test.go similarity index 87% rename from test/cli/delegated_routing_http_test.go rename to test/cli/delegated_routing_v1_http_client_test.go index c02264f9482..44e62246bef 100644 --- a/test/cli/delegated_routing_http_test.go +++ b/test/cli/delegated_routing_v1_http_client_test.go @@ -88,12 +88,20 @@ func TestHTTPDelegatedRouting(t *testing.T) { t.Run("adding HTTP delegated routing endpoint to Routing.Routers config works", func(t *testing.T) { server := fakeServer("application/json", ToJSONStr(JSONObj{ - "Providers": []JSONObj{{ - "Protocol": "transport-bitswap", - "Schema": "bitswap", - "ID": provs[0], - "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, - }}, + "Providers": []JSONObj{ + { + "Schema": "bitswap", // Legacy bitswap schema. + "Protocol": "transport-bitswap", + "ID": provs[1], + "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, + }, + { + "Schema": "peer", + "Protocols": []string{"transport-bitswap"}, + "ID": provs[0], + "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, + }, + }, })) t.Cleanup(server.Close) @@ -117,21 +125,21 @@ func TestHTTPDelegatedRouting(t *testing.T) { node.StartDaemon() res = node.IPFS("routing", "findprovs", findProvsCID) - assert.Equal(t, provs[0], res.Stdout.Trimmed()) + assert.Equal(t, provs[1]+"\n"+provs[0], res.Stdout.Trimmed()) }) node.StopDaemon() t.Run("adding HTTP delegated routing endpoint to Routing.Routers config works (streaming)", func(t *testing.T) { server := fakeServer("application/x-ndjson", ToJSONStr(JSONObj{ - "Protocol": "transport-bitswap", - "Schema": "bitswap", - "ID": provs[1], - "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, + "Schema": "peer", + "Protocols": []string{"transport-bitswap"}, + "ID": provs[0], + "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, }), ToJSONStr(JSONObj{ + "Schema": "bitswap", // Legacy bitswap schema. "Protocol": "transport-bitswap", - "Schema": "bitswap", - "ID": provs[0], + "ID": provs[1], "Addrs": []string{"/ip4/0.0.0.0/tcp/4001", "/ip4/0.0.0.0/tcp/4002"}, })) t.Cleanup(server.Close) @@ -148,7 +156,7 @@ func TestHTTPDelegatedRouting(t *testing.T) { node.StartDaemon() res = node.IPFS("routing", "findprovs", findProvsCID) - assert.Equal(t, provs[1]+"\n"+provs[0], res.Stdout.Trimmed()) + assert.Equal(t, provs[0]+"\n"+provs[1], res.Stdout.Trimmed()) }) t.Run("HTTP client should emit OpenCensus metrics", func(t *testing.T) { diff --git a/test/cli/delegated_routing_v1_http_proxy_test.go b/test/cli/delegated_routing_v1_http_proxy_test.go new file mode 100644 index 00000000000..1d80ae50a5f --- /dev/null +++ b/test/cli/delegated_routing_v1_http_proxy_test.go @@ -0,0 +1,147 @@ +package cli + +import ( + "testing" + + "github.com/ipfs/boxo/ipns" + "github.com/ipfs/kubo/config" + "github.com/ipfs/kubo/test/cli/harness" + "github.com/ipfs/kubo/test/cli/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRoutingV1Proxy(t *testing.T) { + t.Parallel() + + setupNodes := func(t *testing.T) harness.Nodes { + nodes := harness.NewT(t).NewNodes(2).Init() + + // Node 0 uses DHT and exposes the Routing API. + nodes[0].UpdateConfig(func(cfg *config.Config) { + cfg.Gateway.ExposeRoutingAPI = config.True + cfg.Discovery.MDNS.Enabled = false + cfg.Routing.Type = config.NewOptionalString("dht") + }) + nodes[0].StartDaemon() + + // Node 1 uses Node 0 as Routing V1 source, no DHT. + nodes[1].UpdateConfig(func(cfg *config.Config) { + cfg.Discovery.MDNS.Enabled = false + cfg.Routing.Type = config.NewOptionalString("custom") + cfg.Routing.Methods = config.Methods{ + config.MethodNameFindPeers: {RouterName: "KuboA"}, + config.MethodNameFindProviders: {RouterName: "KuboA"}, + config.MethodNameGetIPNS: {RouterName: "KuboA"}, + config.MethodNamePutIPNS: {RouterName: "KuboA"}, + config.MethodNameProvide: {RouterName: "KuboA"}, + } + cfg.Routing.Routers = config.Routers{ + "KuboA": config.RouterParser{ + Router: config.Router{ + Type: config.RouterTypeHTTP, + Parameters: &config.HTTPRouterParams{ + Endpoint: nodes[0].GatewayURL(), + }, + }, + }, + } + }) + nodes[1].StartDaemon() + + // Connect them. + nodes.Connect() + + return nodes + } + + t.Run("Kubo can find provider for CID via Routing V1", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + cidStr := nodes[0].IPFSAddStr(testutils.RandomStr(1000)) + + res := nodes[1].IPFS("routing", "findprovs", cidStr) + assert.Equal(t, nodes[0].PeerID().String(), res.Stdout.Trimmed()) + }) + + t.Run("Kubo can find peer via Routing V1", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + // Start lonely node that is not connected to other nodes. + node := harness.NewT(t).NewNode().Init() + node.UpdateConfig(func(cfg *config.Config) { + cfg.Discovery.MDNS.Enabled = false + cfg.Routing.Type = config.NewOptionalString("dht") + }) + node.StartDaemon() + + // Connect Node 0 to Lonely Node. + nodes[0].Connect(node) + + // Node 1 must find Lonely Node through Node 0 Routing V1. + res := nodes[1].IPFS("routing", "findpeer", node.PeerID().String()) + assert.Equal(t, node.SwarmAddrs()[0].String(), res.Stdout.Trimmed()) + }) + + t.Run("Kubo can retrieve IPNS record via Routing V1", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + nodeName := "/ipns/" + ipns.NameFromPeer(nodes[0].PeerID()).String() + + // Can't resolve the name as isn't published yet. + res := nodes[1].RunIPFS("routing", "get", nodeName) + require.Error(t, res.ExitErr) + + // Publish record on Node 0. + path := "/ipfs/" + nodes[0].IPFSAddStr(testutils.RandomStr(1000)) + nodes[0].IPFS("name", "publish", "--allow-offline", path) + + // Get record on Node 1 (no DHT). + res = nodes[1].IPFS("routing", "get", nodeName) + record, err := ipns.UnmarshalRecord(res.Stdout.Bytes()) + require.NoError(t, err) + value, err := record.Value() + require.NoError(t, err) + require.Equal(t, path, value.String()) + }) + + t.Run("Kubo can resolve IPNS name via Routing V1", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + nodeName := "/ipns/" + ipns.NameFromPeer(nodes[0].PeerID()).String() + + // Can't resolve the name as isn't published yet. + res := nodes[1].RunIPFS("routing", "get", nodeName) + require.Error(t, res.ExitErr) + + // Publish name. + path := "/ipfs/" + nodes[0].IPFSAddStr(testutils.RandomStr(1000)) + nodes[0].IPFS("name", "publish", "--allow-offline", path) + + // Resolve IPNS name + res = nodes[1].IPFS("name", "resolve", nodeName) + require.Equal(t, path, res.Stdout.Trimmed()) + }) + + t.Run("Kubo can provide IPNS record via Routing V1", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + // Publish something on Node 1 (no DHT). + nodeName := "/ipns/" + ipns.NameFromPeer(nodes[1].PeerID()).String() + path := "/ipfs/" + nodes[1].IPFSAddStr(testutils.RandomStr(1000)) + nodes[1].IPFS("name", "publish", "--allow-offline", path) + + // Retrieve through Node 0. + res := nodes[0].IPFS("routing", "get", nodeName) + record, err := ipns.UnmarshalRecord(res.Stdout.Bytes()) + require.NoError(t, err) + value, err := record.Value() + require.NoError(t, err) + require.Equal(t, path, value.String()) + }) +} diff --git a/test/cli/delegated_routing_v1_http_server_test.go b/test/cli/delegated_routing_v1_http_server_test.go new file mode 100644 index 00000000000..440510c1f00 --- /dev/null +++ b/test/cli/delegated_routing_v1_http_server_test.go @@ -0,0 +1,145 @@ +package cli + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/ipfs/boxo/ipns" + "github.com/ipfs/boxo/routing/http/client" + "github.com/ipfs/boxo/routing/http/types" + "github.com/ipfs/boxo/routing/http/types/iter" + "github.com/ipfs/go-cid" + "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 TestRoutingV1Server(t *testing.T) { + t.Parallel() + + setupNodes := func(t *testing.T) harness.Nodes { + 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() + return nodes + } + + t.Run("Get Providers Responds With Correct Peers", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + text := "hello world " + uuid.New().String() + cidStr := nodes[2].IPFSAddStr(text) + _ = nodes[3].IPFSAddStr(text) + + cid, err := cid.Decode(cidStr) + assert.NoError(t, err) + + c, err := client.New(nodes[1].GatewayURL()) + assert.NoError(t, err) + + resultsIter, err := c.FindProviders(context.Background(), cid) + assert.NoError(t, err) + + records, err := iter.ReadAllResults(resultsIter) + assert.NoError(t, err) + + var peers []peer.ID + for _, record := range records { + assert.Equal(t, types.SchemaPeer, record.GetSchema()) + + peer, ok := record.(*types.PeerRecord) + assert.True(t, ok) + peers = append(peers, *peer.ID) + } + + assert.Contains(t, peers, nodes[2].PeerID()) + assert.Contains(t, peers, nodes[3].PeerID()) + }) + + t.Run("Get Peers Responds With Correct Peers", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + c, err := client.New(nodes[1].GatewayURL()) + assert.NoError(t, err) + + resultsIter, err := c.FindPeers(context.Background(), nodes[2].PeerID()) + assert.NoError(t, err) + + records, err := iter.ReadAllResults(resultsIter) + assert.NoError(t, err) + assert.Len(t, records, 1) + assert.IsType(t, records[0].GetSchema(), records[0].GetSchema()) + assert.IsType(t, records[0], &types.PeerRecord{}) + + peer := records[0].(*types.PeerRecord) + assert.Equal(t, nodes[2].PeerID().String(), peer.ID.String()) + assert.NotEmpty(t, peer.Addrs) + }) + + t.Run("Get IPNS Record Responds With Correct Record", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + text := "hello ipns test " + uuid.New().String() + cidStr := nodes[0].IPFSAddStr(text) + nodes[0].IPFS("name", "publish", "--allow-offline", cidStr) + + // Ask for record from a different peer. + c, err := client.New(nodes[1].GatewayURL()) + assert.NoError(t, err) + + record, err := c.GetIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID())) + assert.NoError(t, err) + + value, err := record.Value() + assert.NoError(t, err) + assert.Equal(t, "/ipfs/"+cidStr, value.String()) + }) + + t.Run("Put IPNS Record Succeeds", func(t *testing.T) { + t.Parallel() + nodes := setupNodes(t) + + // Publish a record and confirm the /routing/v1/ipns API exposes the IPNS record + text := "hello ipns test " + uuid.New().String() + cidStr := nodes[0].IPFSAddStr(text) + nodes[0].IPFS("name", "publish", "--allow-offline", cidStr) + c, err := client.New(nodes[0].GatewayURL()) + assert.NoError(t, err) + record, err := c.GetIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID())) + assert.NoError(t, err) + value, err := record.Value() + assert.NoError(t, err) + assert.Equal(t, "/ipfs/"+cidStr, value.String()) + + // Start lonely node that is not connected to other nodes. + node := harness.NewT(t).NewNode().Init() + node.UpdateConfig(func(cfg *config.Config) { + cfg.Gateway.ExposeRoutingAPI = config.True + cfg.Routing.Type = config.NewOptionalString("dht") + }) + node.StartDaemon() + + // Put IPNS record in lonely node. It should be accepted as it is a valid record. + c, err = client.New(node.GatewayURL()) + assert.NoError(t, err) + err = c.PutIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID()), record) + assert.NoError(t, err) + + // Get the record from lonely node and double check. + record, err = c.GetIPNS(context.Background(), ipns.NameFromPeer(nodes[0].PeerID())) + assert.NoError(t, err) + value, err = record.Value() + assert.NoError(t, err) + assert.Equal(t, "/ipfs/"+cidStr, value.String()) + }) +} diff --git a/test/dependencies/go.mod b/test/dependencies/go.mod index 0f4d6348213..094cd9797b2 100644 --- a/test/dependencies/go.mod +++ b/test/dependencies/go.mod @@ -7,7 +7,7 @@ replace github.com/ipfs/kubo => ../../ require ( github.com/Kubuxu/gocovmerge v0.0.0-20161216165753-7ecaa51963cd github.com/golangci/golangci-lint v1.54.1 - github.com/ipfs/boxo v0.12.1-0.20230822135301-303595bcdba7 + github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-cidutil v0.1.0 github.com/ipfs/go-datastore v0.6.0 diff --git a/test/dependencies/go.sum b/test/dependencies/go.sum index c662b3ea33a..3c8cb6f87e3 100644 --- a/test/dependencies/go.sum +++ b/test/dependencies/go.sum @@ -396,8 +396,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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.12.1-0.20230822135301-303595bcdba7 h1:f7n4M8UIf+4BY6Q0kcZ5FbpkxKaIqq/BW3evqI87DNo= -github.com/ipfs/boxo v0.12.1-0.20230822135301-303595bcdba7/go.mod h1:btrtHy0lmO1ODMECbbEY1pxNtrLilvKSYLoGQt1yYCk= +github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd h1:uAp9W7FRQ7W16FENlURZqBh7/3PnakG0DjHpKPirKVY= +github.com/ipfs/boxo v0.12.1-0.20230825151903-13569468babd/go.mod h1:btrtHy0lmO1ODMECbbEY1pxNtrLilvKSYLoGQt1yYCk= 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.1.2 h1:GAjkfhVx1f4YTODS6Esrj1wt2HhrtwTnhEr+DyPUaJo=