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 16, 2023
1 parent 54043d3 commit ec2da81
Show file tree
Hide file tree
Showing 7 changed files with 427 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ The following emojis are used to highlight certain changes:

### Added

- The `routing/http` client and server now support Delegated IPNS as per [IPIP-379](https://github.com/ipfs/specs/pull/379).

### Changed

* 🛠 The `ipns` package has been refactored. You should no longer use the direct Protobuf
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,5 @@ require (
lukechampine.com/blake3 v1.1.7 // indirect
nhooyr.io/websocket v1.8.7 // indirect
)

replace github.com/ipld/go-ipld-prime => github.com/hacdias/go-ipld-prime v0.20.1-0.20230616075357-ec8c75299f3b
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.1 h1:TRWk7se+TOjCYgRth7+1/OYLNiRNIotknkFtf/dnN7Q=
github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M=
Expand Down Expand Up @@ -263,6 +263,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QG
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/gxed/hashland/keccakpg v0.0.1/go.mod h1:kRzw3HkwxFU1mpmPP8v1WyQzwdGfmKFJ6tItnhQ67kU=
github.com/gxed/hashland/murmur3 v0.0.1/go.mod h1:KjXop02n4/ckmZSnY2+HKcLud/tcmvhST0bie/0lS48=
github.com/hacdias/go-ipld-prime v0.20.1-0.20230616075357-ec8c75299f3b h1:pM6vvfZGJPbvr8JTlVoura6gaN/MuBeRX9QjCM+qGUE=
github.com/hacdias/go-ipld-prime v0.20.1-0.20230616075357-ec8c75299f3b/go.mod h1:PRQpXNcJypaPiiSdarsrJABPkYrBvafwDl0B9HjujZ8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
Expand Down Expand Up @@ -345,8 +347,6 @@ github.com/ipld/go-car/v2 v2.9.1-0.20230325062757-fff0e4397a3d h1:22g+x1tgWSXK34
github.com/ipld/go-car/v2 v2.9.1-0.20230325062757-fff0e4397a3d/go.mod h1:SH2pi/NgfGBsV/CGBAQPxMfghIgwzbh5lQ2N+6dNRI8=
github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc=
github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s=
github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g=
github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M=
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
Expand Down Expand Up @@ -649,7 +649,7 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U=
github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s=
github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
Expand Down
71 changes: 69 additions & 2 deletions routing/http/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"strings"
Expand Down Expand Up @@ -41,8 +42,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 +326,68 @@ func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.Wri

return 0, nil
}

func (c *client) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) {
url := c.baseURL + "/routing/v1/ipns/" + name.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)
}

// Limit the reader to the maximum record size.
rawRecord, err := io.ReadAll(io.LimitReader(resp.Body, int64(ipns.MaxRecordSize)))
if err != nil {
return nil, fmt.Errorf("making HTTP req to get IPNS record: %w", err)
}

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

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

return record, nil
}

func (c *client) ProvideIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error {
url := c.baseURL + "/routing/v1/ipns/" + name.String()

rawRecord, err := ipns.MarshalRecord(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
}
103 changes: 103 additions & 0 deletions routing/http/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package client
import (
"context"
"crypto/rand"
"errors"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"

"github.com/benbjohnson/clock"
"github.com/ipfs/boxo/coreiface/path"
ipns "github.com/ipfs/boxo/ipns"
ipfspath "github.com/ipfs/boxo/path"
"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 +35,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 +46,16 @@ func (m *mockContentRouter) Provide(ctx context.Context, req *server.WriteProvid
return args.Get(0).(types.ProviderResponse), args.Error(1)
}

func (m *mockContentRouter) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) {
args := m.Called(ctx, name)
return args.Get(0).(*ipns.Record), args.Error(1)
}

func (m *mockContentRouter) ProvideIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error {
args := m.Called(ctx, name, record)
return args.Error(0)
}

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

func makeName(t *testing.T) (crypto.PrivKey, ipns.Name) {
sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
require.NoError(t, err)

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

return sk, ipns.NameFromPeer(pid)
}

func makeIPNSRecord(t *testing.T, sk crypto.PrivKey) (*ipns.Record, []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.NewRecord(sk, ipfspath.FromString(path.String()), 1, eol, ttl)
require.NoError(t, err)

rawRecord, err := ipns.MarshalRecord(record)
require.NoError(t, err)

return record, rawRecord
}

func TestClient_IPNS(t *testing.T) {
t.Run("Find IPNS Record", func(t *testing.T) {
sk, name := makeName(t)
record, _ := makeIPNSRecord(t, sk)

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

router.On("FindIPNSRecord", mock.Anything, name).Return(record, nil)

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

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

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

router.On("FindIPNSRecord", mock.Anything, name2).Return(record, nil)

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

t.Run("Find IPNS Record returns error if server errors", func(t *testing.T) {
_, name := makeName(t)

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

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

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

t.Run("Provide IPNS Record", func(t *testing.T) {
sk, name := makeName(t)
record, _ := makeIPNSRecord(t, sk)

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

router.On("ProvideIPNSRecord", mock.Anything, name, record).Return(nil)

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

0 comments on commit ec2da81

Please sign in to comment.