Skip to content

Commit 3afb7a9

Browse files
committed
routing/http: implement provide and provide peer
1 parent 0552c9c commit 3afb7a9

File tree

7 files changed

+422
-258
lines changed

7 files changed

+422
-258
lines changed

Diff for: routing/http/client/client.go

+139-73
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/libp2p/go-libp2p/core/crypto"
2626
"github.com/libp2p/go-libp2p/core/peer"
2727
"github.com/multiformats/go-multiaddr"
28+
"github.com/multiformats/go-multibase"
2829
)
2930

3031
var (
@@ -51,22 +52,20 @@ type Client struct {
5152
clock clock.Clock
5253
accepts string
5354

54-
peerID peer.ID
55-
addrs []types.Multiaddr
56-
identity crypto.PrivKey
55+
identity crypto.PrivKey
56+
peerID peer.ID
57+
addrs []types.Multiaddr
58+
protocols []string
5759

58-
// Called immediately after signing a provide request. It is used
60+
// Called immediately after signing a provide (peer) request. It is used
5961
// for testing, e.g., testing the server with a mangled signature.
60-
//lint:ignore SA1019 // ignore staticcheck
61-
afterSignCallback func(req *types.WriteBitswapRecord)
62+
afterSignCallback func(req *types.AnnouncementRecord)
6263
}
6364

6465
// defaultUserAgent is used as a fallback to inform HTTP server which library
6566
// version sent a request
6667
var defaultUserAgent = moduleVersion()
6768

68-
var _ contentrouter.Client = &Client{}
69-
7069
type httpClient interface {
7170
Do(req *http.Request) (*http.Response, error)
7271
}
@@ -102,9 +101,10 @@ func WithUserAgent(ua string) Option {
102101
}
103102
}
104103

105-
func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr) Option {
104+
func WithProviderInfo(peerID peer.ID, addrs []multiaddr.Multiaddr, protocols []string) Option {
106105
return func(c *Client) {
107106
c.peerID = peerID
107+
c.protocols = protocols
108108
for _, a := range addrs {
109109
c.addrs = append(c.addrs, types.Multiaddr{Multiaddr: a})
110110
}
@@ -236,102 +236,121 @@ func (c *Client) FindProviders(ctx context.Context, key cid.Cid) (providers iter
236236
return &measuringIter[iter.Result[types.Record]]{Iter: it, ctx: ctx, m: m}, nil
237237
}
238238

239-
// Deprecated: protocol-agnostic provide is being worked on in [IPIP-378]:
240-
//
241-
// [IPIP-378]: https://github.com/ipfs/specs/pull/378
242-
func (c *Client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Duration) (time.Duration, error) {
243-
if c.identity == nil {
244-
return 0, errors.New("cannot provide Bitswap records without an identity")
245-
}
246-
if c.peerID.Size() == 0 {
247-
return 0, errors.New("cannot provide Bitswap records without a peer ID")
248-
}
249-
250-
ks := make([]types.CID, len(keys))
251-
for i, c := range keys {
252-
ks[i] = types.CID{Cid: c}
239+
func (c *Client) Provide(ctx context.Context, announcements ...types.AnnouncementRequest) (iter.ResultIter[*types.AnnouncementRecord], error) {
240+
if err := c.canProvide(); err != nil {
241+
return nil, err
253242
}
254243

255244
now := c.clock.Now()
245+
records := make([]types.Record, len(announcements))
246+
247+
for i, announcement := range announcements {
248+
record := &types.AnnouncementRecord{
249+
Schema: types.SchemaAnnouncement,
250+
Payload: types.AnnouncementPayload{
251+
CID: announcement.CID,
252+
Scope: announcement.Scope,
253+
Timestamp: now,
254+
TTL: announcement.TTL,
255+
ID: &c.peerID,
256+
Addrs: c.addrs,
257+
Protocols: c.protocols,
258+
},
259+
}
256260

257-
req := types.WriteBitswapRecord{
258-
Protocol: "transport-bitswap",
259-
Schema: types.SchemaBitswap,
260-
Payload: types.BitswapPayload{
261-
Keys: ks,
262-
AdvisoryTTL: &types.Duration{Duration: ttl},
263-
Timestamp: &types.Time{Time: now},
264-
ID: &c.peerID,
265-
Addrs: c.addrs,
266-
},
267-
}
268-
err := req.Sign(c.peerID, c.identity)
269-
if err != nil {
270-
return 0, err
271-
}
261+
if len(announcement.Metadata) != 0 {
262+
var err error
263+
record.Payload.Metadata, err = multibase.Encode(multibase.Base64, announcement.Metadata)
264+
if err != nil {
265+
return nil, fmt.Errorf("multibase-encoding metadata: %w", err)
266+
}
267+
}
272268

273-
if c.afterSignCallback != nil {
274-
c.afterSignCallback(&req)
269+
err := record.Sign(c.peerID, c.identity)
270+
if err != nil {
271+
return nil, err
272+
}
273+
274+
if c.afterSignCallback != nil {
275+
c.afterSignCallback(record)
276+
}
277+
278+
records[i] = record
275279
}
276280

277-
advisoryTTL, err := c.provideSignedBitswapRecord(ctx, &req)
278-
if err != nil {
279-
return 0, err
281+
// TODO: trailing slash?
282+
url := c.baseURL + "/routing/v1/providers"
283+
req := jsontypes.AnnounceProvidersRequest{
284+
Providers: records,
280285
}
281286

282-
return advisoryTTL, err
287+
return c.provide(ctx, url, req)
283288
}
284289

285-
// ProvideAsync makes a provide request to a delegated router
286-
//
287-
//lint:ignore SA1019 // ignore staticcheck
288-
func (c *Client) provideSignedBitswapRecord(ctx context.Context, bswp *types.WriteBitswapRecord) (time.Duration, error) {
289-
//lint:ignore SA1019 // ignore staticcheck
290-
req := jsontypes.WriteProvidersRequest{Providers: []types.Record{bswp}}
291-
292-
url := c.baseURL + "/routing/v1/providers/"
293-
290+
func (c *Client) provide(ctx context.Context, url string, req interface{}) (iter.ResultIter[*types.AnnouncementRecord], error) {
294291
b, err := drjson.MarshalJSONBytes(req)
295292
if err != nil {
296-
return 0, err
293+
return nil, err
297294
}
298295

299296
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPut, url, bytes.NewBuffer(b))
300297
if err != nil {
301-
return 0, err
298+
return nil, err
302299
}
303300

304301
resp, err := c.httpClient.Do(httpReq)
305302
if err != nil {
306-
return 0, fmt.Errorf("making HTTP req to provide a signed record: %w", err)
303+
return nil, fmt.Errorf("making HTTP req to provide a signed peer record: %w", err)
307304
}
308-
defer resp.Body.Close()
309305

310306
if resp.StatusCode != http.StatusOK {
311-
return 0, httpError(resp.StatusCode, resp.Body)
307+
resp.Body.Close()
308+
return nil, httpError(resp.StatusCode, resp.Body)
312309
}
313310

314-
//lint:ignore SA1019 // ignore staticcheck
315-
var provideResult jsontypes.WriteProvidersResponse
316-
err = json.NewDecoder(resp.Body).Decode(&provideResult)
311+
respContentType := resp.Header.Get("Content-Type")
312+
mediaType, _, err := mime.ParseMediaType(respContentType)
317313
if err != nil {
318-
return 0, err
319-
}
320-
if len(provideResult.ProvideResults) != 1 {
321-
return 0, fmt.Errorf("expected 1 result but got %d", len(provideResult.ProvideResults))
314+
resp.Body.Close()
315+
return nil, fmt.Errorf("parsing Content-Type: %w", err)
322316
}
323317

324-
//lint:ignore SA1019 // ignore staticcheck
325-
v, ok := provideResult.ProvideResults[0].(*types.WriteBitswapRecordResponse)
326-
if !ok {
327-
return 0, errors.New("expected AdvisoryTTL field")
328-
}
318+
var skipBodyClose bool
319+
defer func() {
320+
if !skipBodyClose {
321+
resp.Body.Close()
322+
}
323+
}()
329324

330-
if v.AdvisoryTTL != nil {
331-
return v.AdvisoryTTL.Duration, nil
325+
var it iter.ResultIter[*types.AnnouncementRecord]
326+
switch mediaType {
327+
case mediaTypeJSON:
328+
parsedResp := &jsontypes.AnnouncePeersResponse{}
329+
err = json.NewDecoder(resp.Body).Decode(parsedResp)
330+
if err != nil {
331+
return nil, err
332+
}
333+
var sliceIt iter.Iter[*types.AnnouncementRecord] = iter.FromSlice(parsedResp.ProvideResults)
334+
it = iter.ToResultIter(sliceIt)
335+
case mediaTypeNDJSON:
336+
skipBodyClose = true
337+
it = ndjson.NewAnnouncementRecordsIter(resp.Body)
338+
default:
339+
logger.Errorw("unknown media type", "MediaType", mediaType, "ContentType", respContentType)
340+
return nil, errors.New("unknown content type")
332341
}
333342

334-
return 0, nil
343+
return it, nil
344+
}
345+
346+
func (c *Client) canProvide() error {
347+
if c.identity == nil {
348+
return errors.New("cannot provide without identity")
349+
}
350+
if c.peerID.Size() == 0 {
351+
return errors.New("cannot provide without peer ID")
352+
}
353+
return nil
335354
}
336355

337356
// FindPeers searches for information for the given [peer.ID].
@@ -395,6 +414,9 @@ func (c *Client) FindPeers(ctx context.Context, pid peer.ID) (peers iter.ResultI
395414
case mediaTypeJSON:
396415
parsedResp := &jsontypes.PeersResponse{}
397416
err = json.NewDecoder(resp.Body).Decode(parsedResp)
417+
if err != nil {
418+
return nil, err
419+
}
398420
var sliceIt iter.Iter[*types.PeerRecord] = iter.FromSlice(parsedResp.Peers)
399421
it = iter.ToResultIter(sliceIt)
400422
case mediaTypeNDJSON:
@@ -408,6 +430,50 @@ func (c *Client) FindPeers(ctx context.Context, pid peer.ID) (peers iter.ResultI
408430
return &measuringIter[iter.Result[*types.PeerRecord]]{Iter: it, ctx: ctx, m: m}, nil
409431
}
410432

433+
// ProvidePeer provides information regarding your own peer, setup with [WithProviderInfo].
434+
func (c *Client) ProvidePeer(ctx context.Context, ttl time.Duration, metadata []byte) (iter.ResultIter[*types.AnnouncementRecord], error) {
435+
if err := c.canProvide(); err != nil {
436+
return nil, err
437+
}
438+
439+
record := &types.AnnouncementRecord{
440+
Schema: types.SchemaAnnouncement,
441+
Payload: types.AnnouncementPayload{
442+
// TODO: CID, Scope not present for /routing/v1/peers, right?
443+
Timestamp: time.Now(),
444+
TTL: ttl,
445+
ID: &c.peerID,
446+
Addrs: c.addrs,
447+
Protocols: c.protocols,
448+
},
449+
}
450+
451+
if len(metadata) != 0 {
452+
var err error
453+
record.Payload.Metadata, err = multibase.Encode(multibase.Base64, metadata)
454+
if err != nil {
455+
return nil, fmt.Errorf("multibase-encoding metadata: %w", err)
456+
}
457+
}
458+
459+
err := record.Sign(c.peerID, c.identity)
460+
if err != nil {
461+
return nil, err
462+
}
463+
464+
if c.afterSignCallback != nil {
465+
c.afterSignCallback(record)
466+
}
467+
468+
// TODO: trailing slash?
469+
url := c.baseURL + "/routing/v1/peers"
470+
req := jsontypes.AnnouncePeersRequest{
471+
Providers: []types.Record{record},
472+
}
473+
474+
return c.provide(ctx, url, req)
475+
}
476+
411477
// GetIPNS tries to retrieve the [ipns.Record] for the given [ipns.Name]. The record is
412478
// validated against the given name. If validation fails, an error is returned, but no
413479
// record.

0 commit comments

Comments
 (0)