diff --git a/examples/README.md b/examples/README.md index 1c63f087d1..4c496e6a66 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,3 +9,4 @@ Let us know if you find any issue or if you want to contribute and add a new tut - [Fetching a UnixFS file by CID](./unixfs-file-cid) - [Gateway backed by a CAR file](./gateway/car) - [Gateway backed by a remote blockstore and IPNS resolver](./gateway/proxy) +- [Delegated Routing V1 Command Line Client](./routing-v1/client/) diff --git a/examples/go.mod b/examples/go.mod index 790573a69f..24bcbe929d 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -125,6 +125,7 @@ require ( github.com/quic-go/quic-go v0.38.0 // indirect github.com/quic-go/webtransport-go v0.5.3 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect + github.com/samber/lo v1.36.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb // indirect github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc // indirect diff --git a/examples/go.sum b/examples/go.sum index eaec6f954e..93ee14dd35 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -216,6 +216,7 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 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/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= @@ -478,6 +479,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.36.0 h1:4LaOxH1mHnbDGhTVE0i1z8v/lWaQW8AIfOD3HU4mSaw= +github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= @@ -530,6 +533,7 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ= github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM= diff --git a/examples/routing-v1/client/.gitignore b/examples/routing-v1/client/.gitignore new file mode 100644 index 0000000000..b3a725f971 --- /dev/null +++ b/examples/routing-v1/client/.gitignore @@ -0,0 +1 @@ +routing-client \ No newline at end of file diff --git a/examples/routing-v1/client/README.md b/examples/routing-v1/client/README.md new file mode 100644 index 0000000000..e655ad13d8 --- /dev/null +++ b/examples/routing-v1/client/README.md @@ -0,0 +1,68 @@ +# Delegated Routing V1 Command Line Client + +This is an example of how to use the Delegated Routing V1 HTTP client from Boxo. +In this package, we build a small command line tool that allows you to connect to +a Routing V1 endpoint and fetch content providers, peer information, as well as +IPNS records for a certain IPNS name. + +## Build + +```bash +> go build -o routing-client +``` + +## Usage + +First, you will need a Routing V1 server. For that, you can potentially use [Kubo], +which supports [exposing](https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewayexposeroutingapi) +a Routing V1 endpoint. For the commands below, we assume the endpoint is `http://127.0.0.1:8080`. + +### Find CID Providers + +To find providers, provide the flag `-c` with the [CID] of the content you're looking for: + +```console +$ ./routing-client -e http://127.0.0.1:8080 -c bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4 + +12D3KooWEfL19QqRGGLraaAYw1XA3dtDdVRYaHt6jymFxcuQo3Zm + Protocols: [] + Addresses: [/ip4/163.47.51.218/tcp/28131] +12D3KooWK53GAx2g2UUYfJHHjxDbVLeDgGxNMHXDWeJa5KgMhTD2 + Protocols: [] + Addresses: [/ip4/195.167.147.43/udp/8888/quic /ip4/195.167.147.43/tcp/8888] +12D3KooWCpr8kACTRLKrPy4LPpSX7LXvKQ7eYqTmY8CBvgK5HZgB + Protocols: [] + Addresses: [/ip4/163.47.49.234/tcp/28102] +12D3KooWC9L4RjPGgqpzBUBkcVpKjJYofCkC5i5QdQftg1LdsFb2 + Protocols: [] + Addresses: [/ip4/198.244.201.187/tcp/4001] +``` + +### Find Peer Information + +To find a peer, provide the flag `-p` with the [Peer ID] of the peer you're looking for: + + +```console +$ ./routing-client -e http://127.0.0.1:8080 -p 12D3KooWC9L4RjPGgqpzBUBkcVpKjJYofCkC5i5QdQftg1LdsFb2 + +12D3KooWC9L4RjPGgqpzBUBkcVpKjJYofCkC5i5QdQftg1LdsFb2 + Protocols: [] + Addresses: [/ip4/198.244.201.187/tcp/4001] +``` + +### Get an IPNS Record + +To find an IPNS record, provide the flag `-n` with the [IPNS Name] you're trying to find a record for: + +```console +$ ./routing-client -e http://127.0.0.1:8080 -n /ipns/k51qzi5uqu5diuz0h5tjqama8qbmyxusvqz2hfgn5go5l07l9k2ubqa09m7toe + +/ipns/k51qzi5uqu5diuz0h5tjqama8qbmyxusvqz2hfgn5go5l07l9k2ubqa09m7toe + Value: /ipfs/QmUGMoVz62ZARyxkrdEiwmFZanTwVWLLu6EAWvbWHNcwR8 +``` + +[Kubo]: https://github.com/ipfs/kubo +[CID]: https://docs.ipfs.tech/concepts/content-addressing/#what-is-a-cid +[Peer ID]: https://docs.libp2p.io/concepts/fundamentals/peers/#peer-id +[IPNS Name]: https://specs.ipfs.tech/ipns/ipns-record/#ipns-name diff --git a/examples/routing-v1/client/main.go b/examples/routing-v1/client/main.go new file mode 100644 index 0000000000..35a53b680a --- /dev/null +++ b/examples/routing-v1/client/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "io" + "log" + "os" + "time" + + "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/libp2p/go-libp2p/core/peer" +) + +func main() { + gatewayUrlPtr := flag.String("e", "", "routing v1 endpoint to use") + timeoutPtr := flag.Int("t", 10, "timeout in seconds for lookup") + cidPtr := flag.String("c", "", "cid to find") + pidPtr := flag.String("p", "", "peer to find") + namePtr := flag.String("n", "", "ipns name to retrieve record for") + flag.Parse() + + if err := run(os.Stdout, *gatewayUrlPtr, *cidPtr, *pidPtr, *namePtr, *timeoutPtr); err != nil { + log.Fatal(err) + } +} + +func run(w io.Writer, gatewayURL, cidStr, pidStr, nameStr string, timeoutSeconds int) error { + // Creates a new Delegated Routing V1 client. + client, err := client.New(gatewayURL) + if err != nil { + return err + } + + timeout := time.Duration(timeoutSeconds) * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + if cidStr != "" { + return findProviders(w, ctx, client, cidStr) + } else if pidStr != "" { + return findPeers(w, ctx, client, pidStr) + } else if nameStr != "" { + return findIPNS(w, ctx, client, nameStr) + } else { + return errors.New("cid or peer must be provided") + } +} + +func findProviders(w io.Writer, ctx context.Context, client *client.Client, cidStr string) error { + // Parses the given CID to lookup the providers for. + contentCid, err := cid.Parse(cidStr) + if err != nil { + return err + } + + // Ask for providers providing the given content CID. + recordsIter, err := client.FindProviders(ctx, contentCid) + if err != nil { + return err + } + defer recordsIter.Close() + return printIter(w, recordsIter) +} + +func findPeers(w io.Writer, ctx context.Context, client *client.Client, pidStr string) error { + // Parses the given Peer ID to lookup the information for. + pid, err := peer.Decode(pidStr) + if err != nil { + return err + } + + // Ask for information about the peer with the given peer ID. + recordsIter, err := client.FindPeers(ctx, pid) + if err != nil { + return err + } + defer recordsIter.Close() + return printIter(w, recordsIter) +} + +func printIter(w io.Writer, iter iter.ResultIter[types.Record]) error { + // The response is streamed. Alternatively, you could use [iter.ReadAll] + // to fetch all the results all at once, instead of iterating as they are + // streamed. + for iter.Next() { + res := iter.Val() + + // Check for error, but do not complain if we exceeded the timeout. We are + // expecting that to happen: we explicitly defined a timeout. + if res.Err != nil { + if !errors.Is(res.Err, context.DeadlineExceeded) { + return res.Err + } + + return nil + } + + switch res.Val.GetSchema() { + case types.SchemaPeer: + record := res.Val.(*types.PeerRecord) + fmt.Fprintln(w, record.ID) + fmt.Fprintln(w, "\tProtocols:", record.Protocols) + fmt.Fprintln(w, "\tAddresses:", record.Addrs) + default: + // You may not want to fail here, it's up to you. You can just handle + // the schemas you want, or that you know, but not fail. + log.Printf("unrecognized schema: %s", res.Val.GetSchema()) + } + } + + return nil +} + +func findIPNS(w io.Writer, ctx context.Context, client *client.Client, nameStr string) error { + // Parses the given name string to get a record for. + name, err := ipns.NameFromString(nameStr) + if err != nil { + return err + } + + // Ask for a record. [client.GetIPNS] validates the record for us. + record, err := client.GetIPNS(ctx, name) + if err != nil { + return err + } + + v, err := record.Value() + if err != nil { + return err + } + + fmt.Fprintf(w, "/ipns/%s\n", name) + fmt.Fprintln(w, "\tValue:", v.String()) + return nil +} diff --git a/examples/routing-v1/client/main_test.go b/examples/routing-v1/client/main_test.go new file mode 100644 index 0000000000..350d25032a --- /dev/null +++ b/examples/routing-v1/client/main_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "bytes" + "crypto/rand" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/ipns" + ipfspath "github.com/ipfs/boxo/path" + "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindProviders(t *testing.T) { + cidStr := "bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/routing/v1/providers/"+cidStr { + w.Header().Set("Content-Type", "application/x-ndjson") + w.Write([]byte(`{"Schema":"peer","ID":"12D3KooWM8sovaEGU1bmiWGWAzvs47DEcXKZZTuJnpQyVTkRs2Vn","Addrs":["/ip4/111.222.222.111/tcp/5734"],"Protocols":["transport-bitswap"]}` + "\n")) + w.Write([]byte(`{"Schema":"peer","ID":"12D3KooWB6RAWgcmHAP7TGEGK7utV2ZuqSzX1DNjRa97TtJ7139n","Addrs":["/ip4/127.0.0.1/tcp/5734"],"Protocols":["transport-horse"]}` + "\n")) + } + })) + t.Cleanup(ts.Close) + + out := &bytes.Buffer{} + err := run(out, ts.URL, cidStr, "", "", 1) + assert.Contains(t, out.String(), "12D3KooWM8sovaEGU1bmiWGWAzvs47DEcXKZZTuJnpQyVTkRs2Vn\n\tProtocols: [transport-bitswap]\n\tAddresses: [/ip4/111.222.222.111/tcp/5734]\n") + assert.Contains(t, out.String(), "12D3KooWB6RAWgcmHAP7TGEGK7utV2ZuqSzX1DNjRa97TtJ7139n\n\tProtocols: [transport-horse]\n\tAddresses: [/ip4/127.0.0.1/tcp/5734]\n") + assert.NoError(t, err) +} + +func TestFindPeers(t *testing.T) { + pidStr := "bafzaajaiaejcbkboq2tin6dkdc2vinbbn2dgowzn3u5izpjwxejheogw23scafkz" + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/routing/v1/peers/"+pidStr { + w.Header().Set("Content-Type", "application/x-ndjson") + w.Write([]byte(`{"Schema":"peer","ID":"12D3KooWM8sovaEGU1bmiWGWAzvs47DEcXKZZTuJnpQyVTkRs2Vn","Addrs":["/ip4/111.222.222.111/tcp/5734"],"Protocols":["transport-bitswap"]}` + "\n")) + } + })) + t.Cleanup(ts.Close) + + out := &bytes.Buffer{} + err := run(out, ts.URL, "", pidStr, "", 1) + assert.Contains(t, out.String(), "12D3KooWM8sovaEGU1bmiWGWAzvs47DEcXKZZTuJnpQyVTkRs2Vn\n\tProtocols: [transport-bitswap]\n\tAddresses: [/ip4/111.222.222.111/tcp/5734]\n") + assert.NoError(t, err) +} + +func TestGetIPNS(t *testing.T) { + name, rec := makeNameAndRecord(t) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/routing/v1/ipns/"+name.String() { + w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record") + w.Write(rec) + } + })) + t.Cleanup(ts.Close) + + out := &bytes.Buffer{} + err := run(out, ts.URL, "", "", name.String(), 1) + assert.Contains(t, out.String(), fmt.Sprintf("/ipns/%s\n\tValue: /ipfs/bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4\n", name.String())) + assert.NoError(t, err) +} + +func makeNameAndRecord(t *testing.T) (ipns.Name, []byte) { + sk, _, err := crypto.GenerateEd25519Key(rand.Reader) + require.NoError(t, err) + + pid, err := peer.IDFromPrivateKey(sk) + require.NoError(t, err) + + cid, err := cid.Decode("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4") + require.NoError(t, err) + + path := path.IpfsPath(cid) + eol := time.Now().Add(time.Hour * 48) + ttl := time.Second * 20 + + record, err := ipns.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl) + require.NoError(t, err) + + rawRecord, err := ipns.MarshalRecord(record) + require.NoError(t, err) + + return ipns.NameFromPeer(pid), rawRecord +}