diff --git a/dagsync/httpsync/publisher.go b/dagsync/httpsync/publisher.go index 2347774..8a79397 100644 --- a/dagsync/httpsync/publisher.go +++ b/dagsync/httpsync/publisher.go @@ -131,6 +131,26 @@ func NewPublisherWithoutServer(address string, handlerPath string, lsys ipld.Lin }, nil } +// NewPublisherHandler returns a Publisher for use as an http.Handler. Doesn't listen or know about a url prefix. +func NewPublisherHandler(lsys ipld.LinkSystem, privKey ic.PrivKey) (*Publisher, error) { + if privKey == nil { + return nil, errors.New("private key required to sign head requests") + } + peerID, err := peer.IDFromPrivateKey(privKey) + if err != nil { + return nil, fmt.Errorf("could not get peer id from private key: %w", err) + } + + return &Publisher{ + addr: nil, + closer: io.NopCloser(nil), + lsys: lsys, + handlerPath: "", + peerID: peerID, + privKey: privKey, + }, nil +} + // Addrs returns the addresses, as []multiaddress, that the Publisher is // listening on. func (p *Publisher) Addrs() []multiaddr.Multiaddr { diff --git a/dagsync/httpsync/publisher_test.go b/dagsync/httpsync/publisher_test.go index e6fb11f..b99c0fe 100644 --- a/dagsync/httpsync/publisher_test.go +++ b/dagsync/httpsync/publisher_test.go @@ -15,16 +15,23 @@ import ( "github.com/ipld/go-ipld-prime" "github.com/ipld/go-ipld-prime/codec/dagjson" "github.com/ipld/go-ipld-prime/datamodel" + "github.com/ipld/go-ipld-prime/fluent" "github.com/ipld/go-ipld-prime/linking" cidlink "github.com/ipld/go-ipld-prime/linking/cid" "github.com/ipld/go-ipld-prime/node/basicnode" "github.com/ipld/go-ipld-prime/storage/memstore" "github.com/ipld/go-ipld-prime/traversal" + selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" "github.com/ipni/go-libipni/announce" "github.com/ipni/go-libipni/announce/message" "github.com/ipni/go-libipni/dagsync/httpsync" + "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + libp2phttp "github.com/libp2p/go-libp2p/p2p/http" "github.com/multiformats/go-multiaddr" + "github.com/multiformats/go-multicodec" "github.com/stretchr/testify/require" ) @@ -96,6 +103,129 @@ func TestNewPublisherForListener(t *testing.T) { } } +func TestPublisherWithLibp2pHTTP(t *testing.T) { + ctx := context.Background() + req := require.New(t) + + publisherStore := &correctedMemStore{&memstore.Store{ + Bag: make(map[string][]byte), + }} + publisherLsys := cidlink.DefaultLinkSystem() + publisherLsys.TrustedStorage = true + publisherLsys.SetReadStorage(publisherStore) + publisherLsys.SetWriteStorage(publisherStore) + + privKey, _, err := crypto.GenerateKeyPairWithReader(crypto.Ed25519, 256, rand.Reader) + req.NoError(err) + + publisher, err := httpsync.NewPublisherHandler(publisherLsys, privKey) + req.NoError(err) + + publisherStreamHost, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0")) + req.NoError(err) + + publisherHost, err := libp2phttp.New( + libp2phttp.StreamHost(publisherStreamHost), + libp2phttp.ListenAddrs([]multiaddr.Multiaddr{multiaddr.StringCast("/ip4/127.0.0.1/tcp/0/http")}), + ) + req.NoError(err) + + go publisherHost.Serve() + defer publisherHost.Close() + + protoID := protocol.ID("/ipni-sync/1") + + serverStreamMa := publisherHost.Addrs()[0] + serverHTTPMa := publisherHost.Addrs()[1] + req.Contains(serverHTTPMa.String(), "/http") + + publisherHost.SetHttpHandlerAtPath(protoID, "/ipni/", http.StripPrefix("/ipni/", publisher)) + + link, err := publisherLsys.Store( + ipld.LinkContext{Ctx: ctx}, + cidlink.LinkPrototype{ + Prefix: cid.Prefix{ + Version: 1, + Codec: uint64(multicodec.DagJson), + MhType: uint64(multicodec.Sha2_256), + MhLength: -1, + }, + }, + fluent.MustBuildMap(basicnode.Prototype.Map, 4, func(na fluent.MapAssembler) { + na.AssembleEntry("fish").AssignString("lobster") + na.AssembleEntry("fish1").AssignString("lobster1") + na.AssembleEntry("fish2").AssignString("lobster2") + na.AssembleEntry("fish0").AssignString("lobster0") + })) + req.NoError(err) + publisher.SetRoot(link.(cidlink.Link).Cid) + + testCases := []struct { + name string + newClient func(t *testing.T) *http.Client + }{ + {"HTTP transport", func(t *testing.T) *http.Client { + clientHost, err := libp2phttp.New() + req.NoError(err) + + c, err := clientHost.NamespacedClient(protoID, peer.AddrInfo{Addrs: []multiaddr.Multiaddr{serverHTTPMa}}) + req.NoError(err) + return &c + }}, + {"libp2p stream transport", func(t *testing.T) *http.Client { + clientStreamHost, err := libp2p.New(libp2p.NoListenAddrs) + req.NoError(err) + clientHost, err := libp2phttp.New(libp2phttp.StreamHost(clientStreamHost)) + req.NoError(err) + + c, err := clientHost.NamespacedClient(protoID, peer.AddrInfo{ID: publisherStreamHost.ID(), Addrs: []multiaddr.Multiaddr{serverStreamMa}}) + req.NoError(err) + + wk, err := clientHost.GetAndStorePeerProtoMap(c.Transport, publisherStreamHost.ID()) + req.NoError(err) + // Assert we see the ipni protocol in the well known map + req.Contains(wk, protoID) + + return &c + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + clientStore := &correctedMemStore{&memstore.Store{ + Bag: make(map[string][]byte), + }} + clientLsys := cidlink.DefaultLinkSystem() + clientLsys.TrustedStorage = true + clientLsys.SetReadStorage(clientStore) + clientLsys.SetWriteStorage(clientStore) + clientSync := httpsync.NewSync(clientLsys, tc.newClient(t), nil) + + clientSyncer, err := clientSync.NewSyncerWithoutAddrs(publisher.ID()) + req.NoError(err) + + headCid, err := clientSyncer.GetHead(ctx) + req.NoError(err) + + req.Equal(link.(cidlink.Link).Cid, headCid) + + clientSyncer.Sync(ctx, headCid, selectorparse.CommonSelector_MatchPoint) + require.NoError(t, err) + + // Assert that data is loadable from the link system. + wantLink := cidlink.Link{Cid: headCid} + node, err := clientLsys.Load(ipld.LinkContext{Ctx: ctx}, wantLink, basicnode.Prototype.Any) + require.NoError(t, err) + + // Assert synced node link matches the computed link, i.e. is spec-compliant. + gotLink, err := clientLsys.ComputeLink(wantLink.Prototype(), node) + require.NoError(t, err) + require.Equal(t, gotLink, wantLink, "computed %s but got %s", gotLink.String(), wantLink.String()) + }) + } +} + func mapKeys(t *testing.T, n ipld.Node) []string { var keys []string require.Equal(t, n.Kind(), datamodel.Kind_Map) diff --git a/dagsync/httpsync/sync.go b/dagsync/httpsync/sync.go index afae832..5b9cb68 100644 --- a/dagsync/httpsync/sync.go +++ b/dagsync/httpsync/sync.go @@ -69,6 +69,15 @@ func (s *Sync) NewSyncer(peerID peer.ID, peerAddrs []multiaddr.Multiaddr) (*Sync }, nil } +func (s *Sync) NewSyncerWithoutAddrs(peerID peer.ID) (*Syncer, error) { + return &Syncer{ + peerID: peerID, + rootURL: url.URL{Path: "/"}, + urls: nil, + sync: s, + }, nil +} + func (s *Sync) Close() { s.client.CloseIdleConnections() } diff --git a/go.mod b/go.mod index 5bed89f..56627b5 100644 --- a/go.mod +++ b/go.mod @@ -138,3 +138,5 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.1 // indirect ) + +replace github.com/libp2p/go-libp2p v0.29.0 => github.com/libp2p/go-libp2p v0.29.1-0.20230804182920-49d7db486c5e diff --git a/go.sum b/go.sum index 0453a1e..731f60b 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38y github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= -github.com/libp2p/go-libp2p v0.29.0 h1:QduJ2XQr/Crg4EnloueWDL0Jj86N3Ezhyyj7XH+XwHI= -github.com/libp2p/go-libp2p v0.29.0/go.mod h1:iNKL7mEnZ9wAss+03IjAwM9ZAQXfVUAPUUmOACQfQ/g= +github.com/libp2p/go-libp2p v0.29.1-0.20230804182920-49d7db486c5e h1:OGUuDNhPAEt58YoUW5fSSB1XeQB3k5OFPokuLc1KVeA= +github.com/libp2p/go-libp2p v0.29.1-0.20230804182920-49d7db486c5e/go.mod h1:iNKL7mEnZ9wAss+03IjAwM9ZAQXfVUAPUUmOACQfQ/g= github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= github.com/libp2p/go-libp2p-gostream v0.6.0 h1:QfAiWeQRce6pqnYfmIVWJFXNdDyfiR/qkCnjyaZUPYU=