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 Jul 24, 2023
1 parent f6b448b commit 59ce238
Show file tree
Hide file tree
Showing 9 changed files with 430 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The following emojis are used to highlight certain changes:
* The only change to the default behavior on CAR responses is that we follow
IPIP-412 and make `order=dfs;dups=n` explicit in the returned
`Content-Type` HTTP header.
* The `routing/http` client and server now support Delegated IPNS as per [IPIP-379](https://specs.ipfs.tech/ipips/ipip-0379/).

### Changed

Expand Down
2 changes: 1 addition & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/ipfs/go-cid v0.4.1
github.com/ipfs/go-datastore v0.6.0
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33
github.com/ipld/go-ipld-prime v0.20.0
github.com/ipld/go-ipld-prime v0.20.1-0.20230619045951-499e9b9c2024
github.com/libp2p/go-libp2p v0.26.3
github.com/libp2p/go-libp2p-routing-helpers v0.7.0
github.com/multiformats/go-multiaddr v0.8.0
Expand Down
8 changes: 4 additions & 4 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,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 @@ -332,8 +332,8 @@ github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33 h1:0OZwzSYWIuiKE
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33/go.mod h1:sQEkXVM3csejlb1kCCb+vQ/pWBKX9QtvsrysMQjOgOg=
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 v0.20.1-0.20230619045951-499e9b9c2024 h1:HfSCtpmGw0fzCfhZKzASxVjNk8tA1oSCDEF/To3eVg0=
github.com/ipld/go-ipld-prime v0.20.1-0.20230619045951-499e9b9c2024/go.mod h1:PRQpXNcJypaPiiSdarsrJABPkYrBvafwDl0B9HjujZ8=
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
Expand Down Expand Up @@ -625,7 +625,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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ require (
github.com/ipfs/go-unixfsnode v1.7.1
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33
github.com/ipld/go-codec-dagpb v1.6.0
github.com/ipld/go-ipld-prime v0.20.0
github.com/ipld/go-ipld-prime v0.20.1-0.20230619045951-499e9b9c2024
github.com/jbenet/goprocess v0.1.4
github.com/libp2p/go-buffer-pool v0.1.0
github.com/libp2p/go-doh-resolver v0.4.0
Expand Down
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 @@ -338,8 +338,8 @@ github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33 h1:0OZwzSYWIuiKE
github.com/ipld/go-car/v2 v2.10.2-0.20230622090957-499d0c909d33/go.mod h1:sQEkXVM3csejlb1kCCb+vQ/pWBKX9QtvsrysMQjOgOg=
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 v0.20.1-0.20230619045951-499e9b9c2024 h1:HfSCtpmGw0fzCfhZKzASxVjNk8tA1oSCDEF/To3eVg0=
github.com/ipld/go-ipld-prime v0.20.1-0.20230619045951-499e9b9c2024/go.mod h1:PRQpXNcJypaPiiSdarsrJABPkYrBvafwDl0B9HjujZ8=
github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo=
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
Expand Down Expand Up @@ -635,7 +635,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
}

Check warning on line 336 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L335-L336

Added lines #L335 - L336 were not covered by tests
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)
}

Check warning on line 347 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L346-L347

Added lines #L346 - L347 were not covered by tests

// 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)
}

Check warning on line 353 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L352-L353

Added lines #L352 - L353 were not covered by tests

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

Check warning on line 358 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L357-L358

Added lines #L357 - L358 were not covered by tests

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
}

Check warning on line 374 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L373-L374

Added lines #L373 - L374 were not covered by tests

httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewReader(rawRecord))
if err != nil {
return err
}

Check warning on line 379 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L378-L379

Added lines #L378 - L379 were not covered by tests
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)
}

Check warning on line 385 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L384-L385

Added lines #L384 - L385 were not covered by tests
defer resp.Body.Close()

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

Check warning on line 390 in routing/http/client/client.go

View check run for this annotation

Codecov / codecov/patch

routing/http/client/client.go#L389-L390

Added lines #L389 - L390 were not covered by tests

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 59ce238

Please sign in to comment.