diff --git a/routing/http/contentrouter/contentrouter.go b/routing/http/contentrouter/contentrouter.go index a85b341d8..255963f24 100644 --- a/routing/http/contentrouter/contentrouter.go +++ b/routing/http/contentrouter/contentrouter.go @@ -3,7 +3,9 @@ package contentrouter import ( "context" "reflect" + "strings" + "github.com/ipfs/boxo/ipns" "github.com/ipfs/boxo/routing/http/types" "github.com/ipfs/boxo/routing/http/types/iter" "github.com/ipfs/go-cid" @@ -20,6 +22,8 @@ var logger = logging.Logger("routing/http/contentrouter") type Client interface { GetProviders(ctx context.Context, key cid.Cid) (iter.ResultIter[types.Record], error) GetPeers(ctx context.Context, pid peer.ID) (peers iter.ResultIter[types.Record], err error) + GetIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) + PutIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error } type contentRouter struct { @@ -28,6 +32,7 @@ type contentRouter struct { var _ routing.ContentRouting = (*contentRouter)(nil) var _ routing.PeerRouting = (*contentRouter)(nil) +var _ routing.ValueStore = (*contentRouter)(nil) var _ routinghelpers.ProvideManyRouter = (*contentRouter)(nil) var _ routinghelpers.ReadyAbleRouter = (*contentRouter)(nil) @@ -143,3 +148,71 @@ func (c *contentRouter) FindPeer(ctx context.Context, pid peer.ID) (peer.AddrInf return peer.AddrInfo{}, err } + +func (c *contentRouter) PutValue(ctx context.Context, key string, data []byte, opts ...routing.Option) error { + if !strings.HasPrefix(key, "/ipns/") { + return routing.ErrNotSupported + } + + name, err := ipns.NameFromRoutingKey([]byte(key)) + if err != nil { + return err + } + + record, err := ipns.UnmarshalRecord(data) + if err != nil { + return err + } + + return c.client.PutIPNSRecord(ctx, name, record) +} + +func (c *contentRouter) GetValue(ctx context.Context, key string, opts ...routing.Option) ([]byte, error) { + if !strings.HasPrefix(key, "/ipns/") { + return nil, routing.ErrNotSupported + } + + name, err := ipns.NameFromRoutingKey([]byte(key)) + if err != nil { + return nil, err + } + + record, err := c.client.GetIPNSRecord(ctx, name) + if err != nil { + return nil, err + } + + return ipns.MarshalRecord(record) +} + +func (c *contentRouter) SearchValue(ctx context.Context, key string, opts ...routing.Option) (<-chan []byte, error) { + if !strings.HasPrefix(key, "/ipns/") { + return nil, routing.ErrNotSupported + } + + name, err := ipns.NameFromRoutingKey([]byte(key)) + if err != nil { + return nil, err + } + + ch := make(chan []byte) + + go func() { + record, err := c.client.GetIPNSRecord(ctx, name) + if err != nil { + close(ch) + return + } + + raw, err := ipns.MarshalRecord(record) + if err != nil { + close(ch) + return + } + + ch <- raw + close(ch) + }() + + return ch, nil +} diff --git a/routing/http/contentrouter/contentrouter_test.go b/routing/http/contentrouter/contentrouter_test.go index 84bfda6cb..e1d52fc22 100644 --- a/routing/http/contentrouter/contentrouter_test.go +++ b/routing/http/contentrouter/contentrouter_test.go @@ -4,11 +4,17 @@ import ( "context" "crypto/rand" "testing" + "time" + "github.com/ipfs/boxo/coreiface/path" + "github.com/ipfs/boxo/ipns" + ipfspath "github.com/ipfs/boxo/path" "github.com/ipfs/boxo/routing/http/types" "github.com/ipfs/boxo/routing/http/types/iter" "github.com/ipfs/go-cid" + "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/routing" "github.com/multiformats/go-multihash" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -28,6 +34,15 @@ func (m *mockClient) Ready(ctx context.Context) (bool, error) { args := m.Called(ctx) return args.Bool(0), args.Error(1) } +func (m *mockClient) GetIPNSRecord(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 *mockClient) PutIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error { + args := m.Called(ctx, name, record) + return args.Error(0) +} + func makeCID() cid.Cid { buf := make([]byte, 63) _, err := rand.Read(buf) @@ -108,3 +123,87 @@ func TestFindPeer(t *testing.T) { require.NoError(t, err) require.Equal(t, peer.ID, p1) } + +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, opts ...ipns.Option) (*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, opts...) + require.NoError(t, err) + + rawRecord, err := ipns.MarshalRecord(record) + require.NoError(t, err) + + return record, rawRecord +} + +func TestGetValue(t *testing.T) { + ctx := context.Background() + client := &mockClient{} + crc := NewContentRoutingClient(client) + + t.Run("Fail On Unsupported Key", func(t *testing.T) { + v, err := crc.GetValue(ctx, "/something/unsupported") + require.Nil(t, v) + require.ErrorIs(t, err, routing.ErrNotSupported) + }) + + t.Run("Fail On Invalid IPNS Name", func(t *testing.T) { + v, err := crc.GetValue(ctx, "/ipns/invalid") + require.Nil(t, v) + require.Error(t, err) + }) + + t.Run("Succeeds On Valid IPNS Name", func(t *testing.T) { + sk, name := makeName(t) + rec, rawRec := makeIPNSRecord(t, sk) + client.On("GetIPNSRecord", ctx, name).Return(rec, nil) + v, err := crc.GetValue(ctx, string(name.RoutingKey())) + require.NoError(t, err) + require.Equal(t, rawRec, v) + }) +} + +func TestPutValue(t *testing.T) { + ctx := context.Background() + client := &mockClient{} + crc := NewContentRoutingClient(client) + + sk, name := makeName(t) + _, rawRec := makeIPNSRecord(t, sk) + + t.Run("Fail On Unsupported Key", func(t *testing.T) { + err := crc.PutValue(ctx, "/something/unsupported", rawRec) + require.ErrorIs(t, err, routing.ErrNotSupported) + }) + + t.Run("Fail On Invalid IPNS Name", func(t *testing.T) { + err := crc.PutValue(ctx, "/ipns/invalid", rawRec) + require.Error(t, err) + }) + + t.Run("Fail On Invalid IPNS Record", func(t *testing.T) { + err := crc.PutValue(ctx, string(name.RoutingKey()), []byte("gibberish")) + require.Error(t, err) + }) + + t.Run("Succeeds On Valid IPNS Name & Record", func(t *testing.T) { + client.On("PutIPNSRecord", ctx, name, mock.Anything).Return(nil) + err := crc.PutValue(ctx, string(name.RoutingKey()), rawRec) + require.NoError(t, err) + }) +}