Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Routing V1 CLI Example #479

Merged
merged 2 commits into from
Oct 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The following emojis are used to highlight certain changes:
* 🛠 The `IPFSBackend` interface was updated to make the responses of the
`Head` method more explicit. It now returns a `HeadResponse` instead of a
`files.Node`.
* `boxo/routing/http/client.Client` is now exported. This means you can now pass
it around functions, or add it to a struct if you want.

### Removed

Expand Down
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)
}

Check warning on line 31 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L21-L31

Added lines #L21 - L31 were not covered by tests
}

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
}

Check warning on line 39 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L38-L39

Added lines #L38 - L39 were not covered by tests

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")
}

Check warning on line 53 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L52-L53

Added lines #L52 - L53 were not covered by tests
}

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
}

Check warning on line 61 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L60-L61

Added lines #L60 - L61 were not covered by tests

// Ask for providers providing the given content CID.
recordsIter, err := client.FindProviders(ctx, contentCid)
if err != nil {
return err
}

Check warning on line 67 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L66-L67

Added lines #L66 - L67 were not covered by tests
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
}

Check warning on line 77 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L76-L77

Added lines #L76 - L77 were not covered by tests

// Ask for information about the peer with the given peer ID.
recordsIter, err := client.FindPeers(ctx, pid)
if err != nil {
return err
}

Check warning on line 83 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L82-L83

Added lines #L82 - L83 were not covered by tests
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
}

Check warning on line 100 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L98-L100

Added lines #L98 - L100 were not covered by tests

return nil

Check warning on line 102 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L102

Added line #L102 was not covered by tests
}

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())

Check warning on line 114 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L111-L114

Added lines #L111 - L114 were not covered by tests
}
}

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
}

Check warning on line 126 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L125-L126

Added lines #L125 - L126 were not covered by tests

// 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
}

Check warning on line 133 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L132-L133

Added lines #L132 - L133 were not covered by tests

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

Check warning on line 139 in examples/routing/delegated-routing-client/main.go

View check run for this annotation

Codecov / codecov/patch

examples/routing/delegated-routing-client/main.go#L138-L139

Added lines #L138 - L139 were not covered by tests

// 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
}
Loading