Skip to content

Commit

Permalink
feat(examples): routing v1 client cli
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Oct 5, 2023
1 parent 4295a4d commit b388690
Show file tree
Hide file tree
Showing 7 changed files with 322 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/delegated-routing-client/)
1 change: 1 addition & 0 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
1 change: 1 addition & 0 deletions examples/routing/delegated-routing-client/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
delegated-routing-client
71 changes: 71 additions & 0 deletions examples/routing/delegated-routing-client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# 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 delegated-routing-client
```

## Usage

First, you will need a HTTP endpoint compatible with [Delegated Routing V1 Specification][Specification].
For that, you can potentially use [Kubo], which supports [exposing][kubo-conf]
a `/routing/v1` endpoint. For the commands below, we assume the HTTP server that
provides the endpoint is `http://127.0.0.1:8080`.

### Find CID Providers

To find providers, provide the flag `-cid` with the [CID] of the content you're looking for:

```console
$ ./delegated-routing-client -e http://127.0.0.1:8080 -cid 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 `-peer` with the [Peer ID] of the peer you're looking for:


```console
$ ./delegated-routing-client -e http://127.0.0.1:8080 -peer 12D3KooWC9L4RjPGgqpzBUBkcVpKjJYofCkC5i5QdQftg1LdsFb2

12D3KooWC9L4RjPGgqpzBUBkcVpKjJYofCkC5i5QdQftg1LdsFb2
Protocols: []
Addresses: [/ip4/198.244.201.187/tcp/4001]
```

### Get an IPNS Record

To find an IPNS record, provide the flag `-ipns` with the [IPNS Name] you're trying to find a record for:

```console
$ ./delegated-routing-client -e http://127.0.0.1:8080 -ipns /ipns/k51qzi5uqu5diuz0h5tjqama8qbmyxusvqz2hfgn5go5l07l9k2ubqa09m7toe

/ipns/k51qzi5uqu5diuz0h5tjqama8qbmyxusvqz2hfgn5go5l07l9k2ubqa09m7toe
Value: /ipfs/QmUGMoVz62ZARyxkrdEiwmFZanTwVWLLu6EAWvbWHNcwR8
```

[Specification]: https://specs.ipfs.tech/routing/http-routing-v1/
[Kubo]: https://github.com/ipfs/kubo
[kubo-conf]: https://github.com/ipfs/kubo/blob/master/docs/config.md#gatewayexposeroutingapi
[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
148 changes: 148 additions & 0 deletions examples/routing/delegated-routing-client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
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("cid", "", "cid to find")
pidPtr := flag.String("peer", "", "peer to find")
namePtr := flag.String("ipns", "", "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
}

// Fetch an IPNS record for the given name. [client.Client.GetIPNS] verifies
// if the retrieved record is valid against the given name, and errors otherwise.
record, err := client.GetIPNS(ctx, name)
if err != nil {
return err
}

fmt.Fprintf(w, "/ipns/%s\n", name)
v, err := record.Value()
if err != nil {
return err
}

// Since [client.Client.GetIPNS] verifies if the retrieved record is valid, we
// do not need to verify it again. However, if you were not using this specific
// client, but using some other tool, you should always validate the IPNS Record
// using the [ipns.Validate] or [ipns.ValidateWithName] functions.
fmt.Fprintln(w, "\tSignature: VALID")
fmt.Fprintln(w, "\tValue:", v.String())
return nil
}
96 changes: 96 additions & 0 deletions examples/routing/delegated-routing-client/main_test.go
Original file line number Diff line number Diff line change
@@ -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\tSignature: VALID\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
}

0 comments on commit b388690

Please sign in to comment.