Skip to content

Commit f061418

Browse files
authored
feat(routing/http): delegated IPNS server and client, IPIP 379 (#333)
1 parent aa7add0 commit f061418

File tree

9 files changed

+451
-15
lines changed

9 files changed

+451
-15
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ The following emojis are used to highlight certain changes:
1616

1717
### Added
1818

19+
* The `routing/http` client and server now support Delegated IPNS at `/routing/v1`
20+
as per [IPIP-379](https://specs.ipfs.tech/ipips/ipip-0379/).
1921
* The `verifycid` package has been updated with the new Allowlist interface as part of
2022
reducing globals efforts. Still, existing global accessor funcs are kept for
2123
backwards-compatibility.

examples/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ require (
88
github.com/ipfs/go-cid v0.4.1
99
github.com/ipfs/go-datastore v0.6.0
1010
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33
11-
github.com/ipld/go-ipld-prime v0.20.0
11+
github.com/ipld/go-ipld-prime v0.21.0
1212
github.com/libp2p/go-libp2p v0.26.3
1313
github.com/libp2p/go-libp2p-routing-helpers v0.7.0
1414
github.com/multiformats/go-multiaddr v0.8.0

examples/go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
122122
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
123123
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
124124
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
125-
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
125+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
126126
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
127127
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
128128
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
@@ -332,8 +332,8 @@ github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33 h1:0OZwzSYWIuiKE
332332
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33/go.mod h1:sQEkXVM3csejlb1kCCb+vQ/pWBKX9QtvsrysMQjOgOg=
333333
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
334334
github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
335-
github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g=
336-
github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M=
335+
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
336+
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
337337
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
338338
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
339339
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
@@ -625,7 +625,7 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
625625
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
626626
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
627627
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
628-
github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U=
628+
github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s=
629629
github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
630630
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
631631
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ require (
3333
github.com/ipfs/go-unixfsnode v1.7.1
3434
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33
3535
github.com/ipld/go-codec-dagpb v1.6.0
36-
github.com/ipld/go-ipld-prime v0.20.0
36+
github.com/ipld/go-ipld-prime v0.21.0
3737
github.com/jbenet/goprocess v0.1.4
3838
github.com/libp2p/go-buffer-pool v0.1.0
3939
github.com/libp2p/go-doh-resolver v0.4.0

go.sum

+4-4
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
120120
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
121121
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
122122
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
123-
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
123+
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
124124
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
125125
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
126126
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
@@ -338,8 +338,8 @@ github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33 h1:0OZwzSYWIuiKE
338338
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33/go.mod h1:sQEkXVM3csejlb1kCCb+vQ/pWBKX9QtvsrysMQjOgOg=
339339
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
340340
github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
341-
github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g=
342-
github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M=
341+
github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E=
342+
github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ=
343343
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
344344
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
345345
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
@@ -635,7 +635,7 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
635635
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
636636
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
637637
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
638-
github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U=
638+
github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s=
639639
github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
640640
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
641641
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=

routing/http/client/client.go

+69-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io"
910
"mime"
1011
"net/http"
1112
"strings"
@@ -41,8 +42,9 @@ var (
4142
)
4243

4344
const (
44-
mediaTypeJSON = "application/json"
45-
mediaTypeNDJSON = "application/x-ndjson"
45+
mediaTypeJSON = "application/json"
46+
mediaTypeNDJSON = "application/x-ndjson"
47+
mediaTypeIPNSRecord = "application/vnd.ipfs.ipns-record"
4648
)
4749

4850
type client struct {
@@ -324,3 +326,68 @@ func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.Wri
324326

325327
return 0, nil
326328
}
329+
330+
func (c *client) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) {
331+
url := c.baseURL + "/routing/v1/ipns/" + name.String()
332+
333+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
334+
if err != nil {
335+
return nil, err
336+
}
337+
httpReq.Header.Set("Accept", mediaTypeIPNSRecord)
338+
339+
resp, err := c.httpClient.Do(httpReq)
340+
if err != nil {
341+
return nil, fmt.Errorf("making HTTP req to get IPNS record: %w", err)
342+
}
343+
defer resp.Body.Close()
344+
345+
if resp.StatusCode != http.StatusOK {
346+
return nil, httpError(resp.StatusCode, resp.Body)
347+
}
348+
349+
// Limit the reader to the maximum record size.
350+
rawRecord, err := io.ReadAll(io.LimitReader(resp.Body, int64(ipns.MaxRecordSize)))
351+
if err != nil {
352+
return nil, fmt.Errorf("making HTTP req to get IPNS record: %w", err)
353+
}
354+
355+
record, err := ipns.UnmarshalRecord(rawRecord)
356+
if err != nil {
357+
return nil, fmt.Errorf("IPNS record from remote endpoint is not valid: %w", err)
358+
}
359+
360+
err = ipns.ValidateWithName(record, name)
361+
if err != nil {
362+
return nil, fmt.Errorf("IPNS record from remote endpoint is not valid: %w", err)
363+
}
364+
365+
return record, nil
366+
}
367+
368+
func (c *client) ProvideIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error {
369+
url := c.baseURL + "/routing/v1/ipns/" + name.String()
370+
371+
rawRecord, err := ipns.MarshalRecord(record)
372+
if err != nil {
373+
return err
374+
}
375+
376+
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(rawRecord))
377+
if err != nil {
378+
return err
379+
}
380+
httpReq.Header.Set("Content-Type", mediaTypeIPNSRecord)
381+
382+
resp, err := c.httpClient.Do(httpReq)
383+
if err != nil {
384+
return fmt.Errorf("making HTTP req to get IPNS record: %w", err)
385+
}
386+
defer resp.Body.Close()
387+
388+
if resp.StatusCode != http.StatusOK {
389+
return httpError(resp.StatusCode, resp.Body)
390+
}
391+
392+
return nil
393+
}

routing/http/client/client_test.go

+112
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ package client
33
import (
44
"context"
55
"crypto/rand"
6+
"errors"
67
"net/http"
78
"net/http/httptest"
89
"runtime"
910
"testing"
1011
"time"
1112

1213
"github.com/benbjohnson/clock"
14+
"github.com/ipfs/boxo/coreiface/path"
15+
ipns "github.com/ipfs/boxo/ipns"
16+
ipfspath "github.com/ipfs/boxo/path"
1317
"github.com/ipfs/boxo/routing/http/server"
1418
"github.com/ipfs/boxo/routing/http/types"
1519
"github.com/ipfs/boxo/routing/http/types/iter"
@@ -42,6 +46,16 @@ func (m *mockContentRouter) Provide(ctx context.Context, req *server.WriteProvid
4246
return args.Get(0).(types.ProviderResponse), args.Error(1)
4347
}
4448

49+
func (m *mockContentRouter) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) {
50+
args := m.Called(ctx, name)
51+
return args.Get(0).(*ipns.Record), args.Error(1)
52+
}
53+
54+
func (m *mockContentRouter) ProvideIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error {
55+
args := m.Called(ctx, name, record)
56+
return args.Error(0)
57+
}
58+
4559
type testDeps struct {
4660
// recordingHandler records requests received on the server side
4761
recordingHandler *recordingHandler
@@ -442,3 +456,101 @@ func TestClient_Provide(t *testing.T) {
442456
})
443457
}
444458
}
459+
460+
func makeName(t *testing.T) (crypto.PrivKey, ipns.Name) {
461+
sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
462+
require.NoError(t, err)
463+
464+
pid, err := peer.IDFromPrivateKey(sk)
465+
require.NoError(t, err)
466+
467+
return sk, ipns.NameFromPeer(pid)
468+
}
469+
470+
func makeIPNSRecord(t *testing.T, sk crypto.PrivKey, opts ...ipns.Option) (*ipns.Record, []byte) {
471+
cid, err := cid.Decode("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4")
472+
require.NoError(t, err)
473+
474+
path := path.IpfsPath(cid)
475+
eol := time.Now().Add(time.Hour * 48)
476+
ttl := time.Second * 20
477+
478+
record, err := ipns.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl, opts...)
479+
require.NoError(t, err)
480+
481+
rawRecord, err := ipns.MarshalRecord(record)
482+
require.NoError(t, err)
483+
484+
return record, rawRecord
485+
}
486+
487+
func TestClient_IPNS(t *testing.T) {
488+
t.Run("Find IPNS Record returns error if server errors", func(t *testing.T) {
489+
_, name := makeName(t)
490+
491+
deps := makeTestDeps(t, nil, nil)
492+
client := deps.client
493+
router := deps.router
494+
495+
router.On("FindIPNSRecord", mock.Anything, name).Return(nil, errors.New("something wrong happened"))
496+
497+
receivedRecord, err := client.FindIPNSRecord(context.Background(), name)
498+
require.Error(t, err)
499+
require.Nil(t, receivedRecord)
500+
})
501+
502+
runWithRecordOptions := func(t *testing.T, opts ...ipns.Option) {
503+
t.Run("Find IPNS Record", func(t *testing.T) {
504+
sk, name := makeName(t)
505+
record, _ := makeIPNSRecord(t, sk, opts...)
506+
507+
deps := makeTestDeps(t, nil, nil)
508+
client := deps.client
509+
router := deps.router
510+
511+
router.On("FindIPNSRecord", mock.Anything, name).Return(record, nil)
512+
513+
receivedRecord, err := client.FindIPNSRecord(context.Background(), name)
514+
require.NoError(t, err)
515+
require.Equal(t, record, receivedRecord)
516+
})
517+
518+
t.Run("Find IPNS Record returns error if server sends bad data", func(t *testing.T) {
519+
sk, _ := makeName(t)
520+
record, _ := makeIPNSRecord(t, sk, opts...)
521+
_, name2 := makeName(t)
522+
523+
deps := makeTestDeps(t, nil, nil)
524+
client := deps.client
525+
router := deps.router
526+
527+
router.On("FindIPNSRecord", mock.Anything, name2).Return(record, nil)
528+
529+
receivedRecord, err := client.FindIPNSRecord(context.Background(), name2)
530+
require.Error(t, err)
531+
require.Nil(t, receivedRecord)
532+
})
533+
534+
t.Run("Provide IPNS Record", func(t *testing.T) {
535+
sk, name := makeName(t)
536+
record, _ := makeIPNSRecord(t, sk, opts...)
537+
538+
deps := makeTestDeps(t, nil, nil)
539+
client := deps.client
540+
router := deps.router
541+
542+
router.On("ProvideIPNSRecord", mock.Anything, name, record).Return(nil)
543+
544+
err := client.ProvideIPNSRecord(context.Background(), name, record)
545+
require.NoError(t, err)
546+
})
547+
}
548+
549+
t.Run("V1+V2 IPNS Records", func(t *testing.T) {
550+
runWithRecordOptions(t, ipns.WithV1Compatibility(true))
551+
})
552+
553+
t.Run("V2 IPNS Records", func(t *testing.T) {
554+
runWithRecordOptions(t, ipns.WithV1Compatibility(false))
555+
})
556+
}

0 commit comments

Comments
 (0)