Skip to content

Commit

Permalink
feat(routing/http): delegated IPNS client and server implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
hacdias committed Jun 5, 2023
1 parent e2fc7f2 commit 3402424
Show file tree
Hide file tree
Showing 5 changed files with 427 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The following emojis are used to highlight certain changes:

## [Unreleased]

- The `routing/http` client and server now support Delegated IPNS as per [IPIP-379](https://github.com/ipfs/specs/pull/379).
- ✨ The gateway templates were updated to provide better features for users and gateway implementers:
- New human-friendly error messages.
- Updated, higher-definition icons in directory listings.
Expand Down
73 changes: 71 additions & 2 deletions routing/http/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"strings"
"time"

"github.com/benbjohnson/clock"
"github.com/gogo/protobuf/proto"
ipns "github.com/ipfs/boxo/ipns"
ipns_pb "github.com/ipfs/boxo/ipns/pb"
"github.com/ipfs/boxo/routing/http/contentrouter"
"github.com/ipfs/boxo/routing/http/internal/drjson"
"github.com/ipfs/boxo/routing/http/server"
Expand Down Expand Up @@ -41,8 +44,9 @@ var (
)

const (
mediaTypeJSON = "application/json"
mediaTypeNDJSON = "application/x-ndjson"
mediaTypeJSON = "application/json"
mediaTypeNDJSON = "application/x-ndjson"
mediaTypeIPNSRecord = "application/vnd.ipfs.ipns-record"
)

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

return 0, nil
}

func (c *client) GetIPNSRecord(ctx context.Context, pid peer.ID) (*ipns_pb.IpnsEntry, error) {
url := c.baseURL + "/routing/v1/ipns/" + peer.ToCid(pid).String()

httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
httpReq.Header.Set("Accept", mediaTypeIPNSRecord)

resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("making HTTP req to get IPNS record: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, httpError(resp.StatusCode, resp.Body)
}

// The record is at most 10 KiB.
rawRecord, err := io.ReadAll(io.LimitReader(resp.Body, 10240))
if err != nil {
return nil, fmt.Errorf("making HTTP req to get IPNS record: %w", err)
}

record, err := ipns.UnmarshalIpnsEntry(rawRecord)
if err != nil {
return nil, fmt.Errorf("IPNS record from remote endpoint is not valid: %w", err)
}

err = ipns.ValidateWithPeerID(pid, record)
if err != nil {
return nil, fmt.Errorf("IPNS record from remote endpoint is not valid: %w", err)
}

return record, nil
}

func (c *client) PutIPNSRecord(ctx context.Context, pid peer.ID, record *ipns_pb.IpnsEntry) error {
url := c.baseURL + "/routing/v1/ipns/" + peer.ToCid(pid).String()

rawRecord, err := proto.Marshal(record)
if err != nil {
return err
}

httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(rawRecord))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", mediaTypeIPNSRecord)

resp, err := c.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("making HTTP req to get IPNS record: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return httpError(resp.StatusCode, resp.Body)
}

return nil
}
104 changes: 104 additions & 0 deletions routing/http/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package client
import (
"context"
"crypto/rand"
"errors"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"

"github.com/benbjohnson/clock"
"github.com/gogo/protobuf/proto"
"github.com/ipfs/boxo/coreiface/path"
ipns "github.com/ipfs/boxo/ipns"
ipns_pb "github.com/ipfs/boxo/ipns/pb"
"github.com/ipfs/boxo/routing/http/server"
"github.com/ipfs/boxo/routing/http/types"
"github.com/ipfs/boxo/routing/http/types/iter"
Expand All @@ -31,6 +36,7 @@ func (m *mockContentRouter) FindProviders(ctx context.Context, key cid.Cid, limi
args := m.Called(ctx, key, limit)
return args.Get(0).(iter.ResultIter[types.ProviderResponse]), args.Error(1)
}

func (m *mockContentRouter) ProvideBitswap(ctx context.Context, req *server.BitswapWriteProvideRequest) (time.Duration, error) {
args := m.Called(ctx, req)
return args.Get(0).(time.Duration), args.Error(1)
Expand All @@ -41,6 +47,16 @@ func (m *mockContentRouter) Provide(ctx context.Context, req *server.WriteProvid
return args.Get(0).(types.ProviderResponse), args.Error(1)
}

func (m *mockContentRouter) GetIPNSRecord(ctx context.Context, pid peer.ID) (*ipns_pb.IpnsEntry, error) {
args := m.Called(ctx, pid)
return args.Get(0).(*ipns_pb.IpnsEntry), args.Error(1)
}

func (m *mockContentRouter) PutIPNSRecord(ctx context.Context, pid peer.ID, record *ipns_pb.IpnsEntry) error {
args := m.Called(ctx, pid, record)
return args.Error(0)
}

type testDeps struct {
// recordingHandler records requests received on the server side
recordingHandler *recordingHandler
Expand Down Expand Up @@ -441,3 +457,91 @@ func TestClient_Provide(t *testing.T) {
})
}
}

func makePeerID(t *testing.T) (crypto.PrivKey, peer.ID) {
sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)

pid, err := peer.IDFromPrivateKey(sk)
require.NoError(t, err)

return sk, pid
}

func makeIPNSRecord(t *testing.T, sk crypto.PrivKey) (*ipns_pb.IpnsEntry, []byte) {
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.Create(sk, []byte(path.String()), 1, eol, ttl)
require.NoError(t, err)

rawRecord, err := proto.Marshal(record)
require.NoError(t, err)

return record, rawRecord
}

func TestClient_IPNS(t *testing.T) {
t.Run("Get IPNS Record", func(t *testing.T) {
sk, pid := makePeerID(t)
record, _ := makeIPNSRecord(t, sk)

deps := makeTestDeps(t, nil, nil)
client := deps.client
router := deps.router

router.On("GetIPNSRecord", mock.Anything, pid).Return(record, nil)

receivedRecord, err := client.GetIPNSRecord(context.Background(), pid)
require.NoError(t, err)
require.Equal(t, record, receivedRecord)
})

t.Run("Get IPNS Record returns error if server sends bad data", func(t *testing.T) {
sk, _ := makePeerID(t)
record, _ := makeIPNSRecord(t, sk)
_, pid2 := makePeerID(t)

deps := makeTestDeps(t, nil, nil)
client := deps.client
router := deps.router

router.On("GetIPNSRecord", mock.Anything, pid2).Return(record, nil)

receivedRecord, err := client.GetIPNSRecord(context.Background(), pid2)
require.Error(t, err)
require.Nil(t, receivedRecord)
})

t.Run("Get IPNS Record returns error if server errors", func(t *testing.T) {
_, pid := makePeerID(t)

deps := makeTestDeps(t, nil, nil)
client := deps.client
router := deps.router

router.On("GetIPNSRecord", mock.Anything, pid).Return(nil, errors.New("something wrong happened"))

receivedRecord, err := client.GetIPNSRecord(context.Background(), pid)
require.Error(t, err)
require.Nil(t, receivedRecord)
})

t.Run("Put IPNS Record", func(t *testing.T) {
sk, pid := makePeerID(t)
record, _ := makeIPNSRecord(t, sk)

deps := makeTestDeps(t, nil, nil)
client := deps.client
router := deps.router

router.On("PutIPNSRecord", mock.Anything, pid, record).Return(nil)

err := client.PutIPNSRecord(context.Background(), pid, record)
require.NoError(t, err)
})
}
Loading

0 comments on commit 3402424

Please sign in to comment.