diff --git a/routing/http/client/client.go b/routing/http/client/client.go index b3a74150c..f4aadcee7 100644 --- a/routing/http/client/client.go +++ b/routing/http/client/client.go @@ -8,6 +8,7 @@ import ( "fmt" "mime" "net/http" + "net/url" "strings" "time" @@ -54,7 +55,7 @@ type client struct { accepts string peerID peer.ID - addrs []types.Multiaddr + addrs func() ([]types.Multiaddr, error) identity crypto.PrivKey // called immeidately after signing a provide req @@ -104,10 +105,37 @@ func WithUserAgent(ua string) Option { } func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr) Option { + taddrs := make([]types.Multiaddr, len(addrs)) + for i, v := range addrs { + taddrs[i] = types.Multiaddr{Multiaddr: v} + } + return func(c *client) { c.peerID = peerID - for _, a := range addrs { - c.addrs = append(c.addrs, types.Multiaddr{Multiaddr: a}) + c.addrs = func() ([]types.Multiaddr, error) { + return taddrs, nil + } + } +} + +// WithDynamicProviderInfo is like [WithProviderInfo] but the addresses will be queried on each publish operation. +// This is usefull for nodes with changing addresses, like P2P daemons behind NATs. +// Note: due to API limitations can't trivially batch update previous records with new addresses, so you are still relient +// on an consumers using a PeerRouter able to follow your new addresses, for example the IPFS DHT. +func WithDynamicProviderInfo(peerID peer.ID, addrs func() ([]multiaddr.Multiaddr, error)) Option { + return func(c *client) { + c.peerID = peerID + c.addrs = func() ([]types.Multiaddr, error) { + addrs, err := addrs() + if err != nil { + return nil, err + } + + taddrs := make([]types.Multiaddr, len(addrs)) + for i, v := range addrs { + taddrs[i] = types.Multiaddr{Multiaddr: v} + } + return taddrs, nil } } } @@ -120,6 +148,7 @@ func WithStreamResultsRequired() Option { // New creates a content routing API client. // The Provider and identity parameters are option. If they are nil, the `Provide` method will not function. +// Consider using the more type-safe option [NewURL]. func New(baseURL string, opts ...Option) (*client, error) { client := &client{ baseURL: baseURL, @@ -140,6 +169,11 @@ func New(baseURL string, opts ...Option) (*client, error) { return client, nil } +// NewURL is a more type-safe version of [New], it takes in an [url.URL]. +func NewURL(baseURL url.URL, opts ...Option) (*client, error) { + return New(baseURL.String(), opts...) +} + // measuringIter measures the length of the iter and then publishes metrics about the whole req once the iter is closed. // Of course, if the caller forgets to close the iter, this won't publish anything. type measuringIter[T any] struct { @@ -251,6 +285,11 @@ func (c *client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Du now := c.clock.Now() + addrs, err := c.addrs() + if err != nil { + return 0, fmt.Errorf("failed to query our addresses: %w", err) + } + req := types.WriteBitswapProviderRecord{ Protocol: "transport-bitswap", Schema: types.SchemaBitswap, @@ -259,10 +298,10 @@ func (c *client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Du AdvisoryTTL: &types.Duration{Duration: ttl}, Timestamp: &types.Time{Time: now}, ID: &c.peerID, - Addrs: c.addrs, + Addrs: addrs, }, } - err := req.Sign(c.peerID, c.identity) + err = req.Sign(c.peerID, c.identity) if err != nil { return 0, err } diff --git a/routing/http/client/client_test.go b/routing/http/client/client_test.go index 880fa33e1..1f83b6021 100644 --- a/routing/http/client/client_test.go +++ b/routing/http/client/client_test.go @@ -143,7 +143,7 @@ func drAddrsToAddrs(drmas []types.Multiaddr) (addrs []multiaddr.Multiaddr) { return } -func makeBSReadProviderResp() types.ReadBitswapProviderRecord { +func makeBSReadProviderResp(t *testing.T) types.ReadBitswapProviderRecord { peerID, addrs, _ := makeProviderAndIdentity() return types.ReadBitswapProviderRecord{ Protocol: "transport-bitswap", @@ -193,7 +193,7 @@ func (e *osErrContains) errContains(t *testing.T, err error) { } func TestClient_FindProviders(t *testing.T) { - bsReadProvResp := makeBSReadProviderResp() + bsReadProvResp := makeBSReadProviderResp(t) bitswapProvs := []iter.Result[types.ProviderResponse]{ {Val: &bsReadProvResp}, } @@ -411,11 +411,18 @@ func TestClient_Provide(t *testing.T) { } } + var addrs []types.Multiaddr + if f := client.addrs; f != nil { + var err error + addrs, err = client.addrs() + require.NoError(t, err) + } + expectedProvReq := &server.BitswapWriteProvideRequest{ Keys: c.cids, Timestamp: clock.Now().Truncate(time.Millisecond), AdvisoryTTL: c.ttl, - Addrs: drAddrsToAddrs(client.addrs), + Addrs: drAddrsToAddrs(addrs), ID: client.peerID, } @@ -441,3 +448,63 @@ func TestClient_Provide(t *testing.T) { }) } } + +func TestWithDynamicClient(t *testing.T) { + t.Parallel() + + const ttl = time.Hour + + const testUserAgent = "testUserAgent" + peerID, addrs, identity := makeProviderAndIdentity() + router := &mockContentRouter{} + recordingHandler := &recordingHandler{ + Handler: server.Handler(router), + f: []func(*http.Request){ + func(r *http.Request) { + assert.Equal(t, testUserAgent, r.Header.Get("User-Agent")) + }, + }, + } + srv := httptest.NewServer(recordingHandler) + t.Cleanup(srv.Close) + serverAddr := "http://" + srv.Listener.Addr().String() + recordingHTTPClient := &recordingHTTPClient{httpClient: defaultHTTPClient} + var rAddrs []multiaddr.Multiaddr + client, err := New(serverAddr, + WithDynamicProviderInfo(peerID, func() ([]multiaddr.Multiaddr, error) { return rAddrs, nil }), + WithIdentity(identity), + WithUserAgent(testUserAgent), + WithHTTPClient(recordingHTTPClient), + ) + require.NoError(t, err) + + c := makeCID() + rAddrs = addrs[:1] + + clock := clock.NewMock() + clock.Set(time.Now()) + client.clock = clock + + expectedProvReq := &server.BitswapWriteProvideRequest{ + Keys: []cid.Cid{c}, + Timestamp: clock.Now().Truncate(time.Millisecond), + AdvisoryTTL: ttl, + Addrs: rAddrs, + ID: peerID, + } + router.On("ProvideBitswap", mock.Anything, expectedProvReq).Return(ttl, nil) + + ctx := context.Background() + _, err = client.ProvideBitswap(ctx, []cid.Cid{c}, ttl) + require.NoError(t, err) + + c = makeCID() + rAddrs = addrs[1:] + + expectedProvReq.Keys[0] = c + expectedProvReq.Addrs = rAddrs + + _, err = client.ProvideBitswap(ctx, []cid.Cid{c}, ttl) + require.NoError(t, err) + +} diff --git a/routing/http/contentrouter/contentrouter.go b/routing/http/contentrouter/contentrouter.go index 8318a3163..070b5dc56 100644 --- a/routing/http/contentrouter/contentrouter.go +++ b/routing/http/contentrouter/contentrouter.go @@ -124,9 +124,9 @@ func readProviderResponses(iter iter.ResultIter[types.ProviderResponse], ch chan continue } - var addrs []multiaddr.Multiaddr - for _, a := range result.Addrs { - addrs = append(addrs, a.Multiaddr) + addrs := make([]multiaddr.Multiaddr, len(result.Addrs)) + for i, a := range result.Addrs { + addrs[i] = a.Multiaddr } ch <- peer.AddrInfo{ diff --git a/routing/http/contentrouter/contentrouter_test.go b/routing/http/contentrouter/contentrouter_test.go index 4ca620c5d..d6bc4db26 100644 --- a/routing/http/contentrouter/contentrouter_test.go +++ b/routing/http/contentrouter/contentrouter_test.go @@ -10,6 +10,7 @@ import ( "github.com/ipfs/boxo/routing/http/types/iter" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/peer" + "github.com/multiformats/go-multiaddr" "github.com/multiformats/go-multihash" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -134,8 +135,8 @@ func TestFindProvidersAsync(t *testing.T) { } expected := []peer.AddrInfo{ - {ID: p1}, - {ID: p2}, + {ID: p1, Addrs: []multiaddr.Multiaddr{}}, + {ID: p2, Addrs: []multiaddr.Multiaddr{}}, } require.Equal(t, expected, actualAIs)