generated from ipfs/ipfs-repository-template
-
Notifications
You must be signed in to change notification settings - Fork 100
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): routing v1 client cli
- Loading branch information
Showing
7 changed files
with
322 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
delegated-routing-client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |