Skip to content

Commit 56c5cc9

Browse files
committed
feat(routing/http): delegated IPNS client and server implementation
1 parent fe4c5b3 commit 56c5cc9

5 files changed

Lines changed: 428 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
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 as per [IPIP-379](https://github.com/ipfs/specs/pull/379).
20+
1921
### Changed
2022

2123
### Removed

routing/http/client/client.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io"
910
"mime"
1011
"net/http"
1112
"strings"
1213
"time"
1314

1415
"github.com/benbjohnson/clock"
16+
"github.com/gogo/protobuf/proto"
1517
ipns "github.com/ipfs/boxo/ipns"
18+
ipns_pb "github.com/ipfs/boxo/ipns/pb"
1619
"github.com/ipfs/boxo/routing/http/contentrouter"
1720
"github.com/ipfs/boxo/routing/http/internal/drjson"
1821
"github.com/ipfs/boxo/routing/http/server"
@@ -41,8 +44,9 @@ var (
4144
)
4245

4346
const (
44-
mediaTypeJSON = "application/json"
45-
mediaTypeNDJSON = "application/x-ndjson"
47+
mediaTypeJSON = "application/json"
48+
mediaTypeNDJSON = "application/x-ndjson"
49+
mediaTypeIPNSRecord = "application/vnd.ipfs.ipns-record"
4650
)
4751

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

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

routing/http/client/client_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@ 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/gogo/protobuf/proto"
15+
"github.com/ipfs/boxo/coreiface/path"
16+
ipns "github.com/ipfs/boxo/ipns"
17+
ipns_pb "github.com/ipfs/boxo/ipns/pb"
1318
"github.com/ipfs/boxo/routing/http/server"
1419
"github.com/ipfs/boxo/routing/http/types"
1520
"github.com/ipfs/boxo/routing/http/types/iter"
@@ -31,6 +36,7 @@ func (m *mockContentRouter) FindProviders(ctx context.Context, key cid.Cid, limi
3136
args := m.Called(ctx, key, limit)
3237
return args.Get(0).(iter.ResultIter[types.ProviderResponse]), args.Error(1)
3338
}
39+
3440
func (m *mockContentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) {
3541
args := m.Called(ctx, req)
3642
return args.Get(0).(time.Duration), args.Error(1)
@@ -41,6 +47,16 @@ func (m *mockContentRouter) Provide(ctx context.Context, req *server.WriteProvid
4147
return args.Get(0).(types.ProviderResponse), args.Error(1)
4248
}
4349

50+
func (m *mockContentRouter) GetIPNSRecord(ctx context.Context, pid peer.ID) (*ipns_pb.IpnsEntry, error) {
51+
args := m.Called(ctx, pid)
52+
return args.Get(0).(*ipns_pb.IpnsEntry), args.Error(1)
53+
}
54+
55+
func (m *mockContentRouter) PutIPNSRecord(ctx context.Context, pid peer.ID, record *ipns_pb.IpnsEntry) error {
56+
args := m.Called(ctx, pid, record)
57+
return args.Error(0)
58+
}
59+
4460
type testDeps struct {
4561
// recordingHandler records requests received on the server side
4662
recordingHandler *recordingHandler
@@ -441,3 +457,91 @@ func TestClient_Provide(t *testing.T) {
441457
})
442458
}
443459
}
460+
461+
func makePeerID(t *testing.T) (crypto.PrivKey, peer.ID) {
462+
sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
463+
require.NoError(t, err)
464+
465+
pid, err := peer.IDFromPrivateKey(sk)
466+
require.NoError(t, err)
467+
468+
return sk, pid
469+
}
470+
471+
func makeIPNSRecord(t *testing.T, sk crypto.PrivKey) (*ipns_pb.IpnsEntry, []byte) {
472+
cid, err := cid.Decode("bafkreifjjcie6lypi6ny7amxnfftagclbuxndqonfipmb64f2km2devei4")
473+
require.NoError(t, err)
474+
475+
path := path.IpfsPath(cid)
476+
eol := time.Now().Add(time.Hour * 48)
477+
ttl := time.Second * 20
478+
479+
record, err := ipns.Create(sk, []byte(path.String()), 1, eol, ttl)
480+
require.NoError(t, err)
481+
482+
rawRecord, err := proto.Marshal(record)
483+
require.NoError(t, err)
484+
485+
return record, rawRecord
486+
}
487+
488+
func TestClient_IPNS(t *testing.T) {
489+
t.Run("Get IPNS Record", func(t *testing.T) {
490+
sk, pid := makePeerID(t)
491+
record, _ := makeIPNSRecord(t, sk)
492+
493+
deps := makeTestDeps(t, nil, nil)
494+
client := deps.client
495+
router := deps.router
496+
497+
router.On("GetIPNSRecord", mock.Anything, pid).Return(record, nil)
498+
499+
receivedRecord, err := client.GetIPNSRecord(context.Background(), pid)
500+
require.NoError(t, err)
501+
require.Equal(t, record, receivedRecord)
502+
})
503+
504+
t.Run("Get IPNS Record returns error if server sends bad data", func(t *testing.T) {
505+
sk, _ := makePeerID(t)
506+
record, _ := makeIPNSRecord(t, sk)
507+
_, pid2 := makePeerID(t)
508+
509+
deps := makeTestDeps(t, nil, nil)
510+
client := deps.client
511+
router := deps.router
512+
513+
router.On("GetIPNSRecord", mock.Anything, pid2).Return(record, nil)
514+
515+
receivedRecord, err := client.GetIPNSRecord(context.Background(), pid2)
516+
require.Error(t, err)
517+
require.Nil(t, receivedRecord)
518+
})
519+
520+
t.Run("Get IPNS Record returns error if server errors", func(t *testing.T) {
521+
_, pid := makePeerID(t)
522+
523+
deps := makeTestDeps(t, nil, nil)
524+
client := deps.client
525+
router := deps.router
526+
527+
router.On("GetIPNSRecord", mock.Anything, pid).Return(nil, errors.New("something wrong happened"))
528+
529+
receivedRecord, err := client.GetIPNSRecord(context.Background(), pid)
530+
require.Error(t, err)
531+
require.Nil(t, receivedRecord)
532+
})
533+
534+
t.Run("Put IPNS Record", func(t *testing.T) {
535+
sk, pid := makePeerID(t)
536+
record, _ := makeIPNSRecord(t, sk)
537+
538+
deps := makeTestDeps(t, nil, nil)
539+
client := deps.client
540+
router := deps.router
541+
542+
router.On("PutIPNSRecord", mock.Anything, pid, record).Return(nil)
543+
544+
err := client.PutIPNSRecord(context.Background(), pid, record)
545+
require.NoError(t, err)
546+
})
547+
}

0 commit comments

Comments
 (0)