Skip to content

Commit 1356946

Browse files
authored
feat(routing/http)!: delegated peer routing server and client, IPIP 417 (#422)
1 parent 79159c3 commit 1356946

20 files changed

+1428
-594
lines changed

CHANGELOG.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,26 @@ The following emojis are used to highlight certain changes:
1616

1717
### Added
1818

19+
* ✨ The `routing/http` implements Delegated Peer Routing introduced in [IPIP-417](https://github.com/ipfs/specs/pull/417).
20+
1921
### Changed
2022

23+
* 🛠 The `routing/http` package received the following modifications:
24+
* Client `GetIPNSRecord` and `PutIPNSRecord` have been renamed to `GetIPNS` and
25+
`PutIPNS`, respectively. Similarly, the required function names in the server
26+
`ContentRouter` have also been updated.
27+
* `ReadBitswapProviderRecord` has been renamed to `BitswapRecord` and marked as deprecated.
28+
From now on, please use the protocol-agnostic `PeerRecord` for most use cases. The new
29+
Peer Schema has been introduced in [IPIP-417](https://github.com/ipfs/specs/pull/417).
30+
2131
### Removed
2232

33+
* 🛠 The `routing/http` package experienced following removals:
34+
* Server and client no longer support the experimental `Provide` method.
35+
`ProvideBitswap` is still usable, but marked as deprecated. A protocol-agnostic
36+
provide mechanism is being worked on in [IPIP-378](https://github.com/ipfs/specs/pull/378).
37+
* Server no longer exports `FindProvidersPath` and `ProvidePath`.
38+
2339
### Fixed
2440

2541
### Security
@@ -32,7 +48,7 @@ The following emojis are used to highlight certain changes:
3248
as per [IPIP-379](https://specs.ipfs.tech/ipips/ipip-0379/).
3349
* 🛠 The `verifycid` package has been updated with the new Allowlist interface as part of
3450
reducing globals efforts.
35-
* The `blockservice` and `provider` packages has been updated to accommodate for
51+
* The `blockservice` and `provider` packages has been updated to accommodate for
3652
changes in `verifycid`.
3753

3854
### Changed

routing/http/README.md

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,9 @@
1-
go-delegated-routing
1+
Routing V1 Server and Client
22
=======================
33

4-
> Delegated routing Client and Server over Reframe RPC
5-
6-
This package provides delegated routing implementation in Go:
7-
- Client (for IPFS nodes like [Kubo](https://github.com/ipfs/kubo/blob/master/docs/config.md#routingrouters-parameters)),
8-
- Server (for public indexers such as https://cid.contact)
4+
> Delegated Routing V1 Server and Client over HTTP API.
95
106
## Documentation
117

12-
- Go docs: https://pkg.go.dev/github.com/ipfs/boxo/routing/http/
13-
14-
## Lead Maintainer
15-
16-
🦗🎶
17-
18-
## Contributing
19-
20-
Contributions are welcome! This repository is part of the IPFS project and therefore governed by our [contributing guidelines](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md).
21-
22-
## License
23-
24-
[SPDX-License-Identifier: Apache-2.0 OR MIT](LICENSE.md)
8+
- Go Documentation: https://pkg.go.dev/github.com/ipfs/boxo/routing/http
9+
- Routing V1 Specification: https://specs.ipfs.tech/routing/http-routing-v1/

routing/http/client/client.go

Lines changed: 104 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,20 @@ import (
1616
ipns "github.com/ipfs/boxo/ipns"
1717
"github.com/ipfs/boxo/routing/http/contentrouter"
1818
"github.com/ipfs/boxo/routing/http/internal/drjson"
19-
"github.com/ipfs/boxo/routing/http/server"
2019
"github.com/ipfs/boxo/routing/http/types"
2120
"github.com/ipfs/boxo/routing/http/types/iter"
2221
jsontypes "github.com/ipfs/boxo/routing/http/types/json"
2322
"github.com/ipfs/boxo/routing/http/types/ndjson"
2423
"github.com/ipfs/go-cid"
2524
logging "github.com/ipfs/go-log/v2"
26-
record "github.com/libp2p/go-libp2p-record"
2725
"github.com/libp2p/go-libp2p/core/crypto"
2826
"github.com/libp2p/go-libp2p/core/peer"
2927
"github.com/multiformats/go-multiaddr"
3028
)
3129

3230
var (
3331
_ contentrouter.Client = &client{}
34-
logger = logging.Logger("service/delegatedrouting")
32+
logger = logging.Logger("routing/http/client")
3533
defaultHTTPClient = &http.Client{
3634
Transport: &ResponseBodyLimitedTransport{
3735
RoundTripper: http.DefaultTransport,
@@ -50,18 +48,17 @@ const (
5048
type client struct {
5149
baseURL string
5250
httpClient httpClient
53-
validator record.Validator
5451
clock clock.Clock
55-
56-
accepts string
52+
accepts string
5753

5854
peerID peer.ID
5955
addrs []types.Multiaddr
6056
identity crypto.PrivKey
6157

62-
// called immeidately after signing a provide req
63-
// used for testing, e.g. testing the server with a mangled signature
64-
afterSignCallback func(req *types.WriteBitswapProviderRecord)
58+
// Called immediately after signing a provide request. It is used
59+
// for testing, e.g., testing the server with a mangled signature.
60+
//lint:ignore SA1019 // ignore staticcheck
61+
afterSignCallback func(req *types.WriteBitswapRecord)
6562
}
6663

6764
// defaultUserAgent is used as a fallback to inform HTTP server which library
@@ -121,12 +118,11 @@ func WithStreamResultsRequired() Option {
121118
}
122119

123120
// New creates a content routing API client.
124-
// The Provider and identity parameters are option. If they are nil, the `Provide` method will not function.
121+
// The Provider and identity parameters are option. If they are nil, the [client.ProvideBitswap] method will not function.
125122
func New(baseURL string, opts ...Option) (*client, error) {
126123
client := &client{
127124
baseURL: baseURL,
128125
httpClient: defaultHTTPClient,
129-
validator: ipns.Validator{},
130126
clock: clock.New(),
131127
accepts: strings.Join([]string{mediaTypeNDJSON, mediaTypeJSON}, ","),
132128
}
@@ -164,11 +160,11 @@ func (c *measuringIter[T]) Close() error {
164160
return c.Iter.Close()
165161
}
166162

167-
func (c *client) FindProviders(ctx context.Context, key cid.Cid) (provs iter.ResultIter[types.ProviderResponse], err error) {
163+
func (c *client) FindProviders(ctx context.Context, key cid.Cid) (providers iter.ResultIter[types.Record], err error) {
168164
// TODO test measurements
169165
m := newMeasurement("FindProviders")
170166

171-
url := c.baseURL + server.ProvidePath + key.String()
167+
url := c.baseURL + "/routing/v1/providers/" + key.String()
172168
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
173169
if err != nil {
174170
return nil, err
@@ -192,7 +188,7 @@ func (c *client) FindProviders(ctx context.Context, key cid.Cid) (provs iter.Res
192188
if resp.StatusCode == http.StatusNotFound {
193189
resp.Body.Close()
194190
m.record(ctx)
195-
return iter.FromSlice[iter.Result[types.ProviderResponse]](nil), nil
191+
return iter.FromSlice[iter.Result[types.Record]](nil), nil
196192
}
197193

198194
if resp.StatusCode != http.StatusOK {
@@ -220,24 +216,27 @@ func (c *client) FindProviders(ctx context.Context, key cid.Cid) (provs iter.Res
220216
}
221217
}()
222218

223-
var it iter.ResultIter[types.ProviderResponse]
219+
var it iter.ResultIter[types.Record]
224220
switch mediaType {
225221
case mediaTypeJSON:
226-
parsedResp := &jsontypes.ReadProvidersResponse{}
222+
parsedResp := &jsontypes.ProvidersResponse{}
227223
err = json.NewDecoder(resp.Body).Decode(parsedResp)
228-
var sliceIt iter.Iter[types.ProviderResponse] = iter.FromSlice(parsedResp.Providers)
224+
var sliceIt iter.Iter[types.Record] = iter.FromSlice(parsedResp.Providers)
229225
it = iter.ToResultIter(sliceIt)
230226
case mediaTypeNDJSON:
231227
skipBodyClose = true
232-
it = ndjson.NewReadProvidersResponseIter(resp.Body)
228+
it = ndjson.NewRecordsIter(resp.Body)
233229
default:
234230
logger.Errorw("unknown media type", "MediaType", mediaType, "ContentType", respContentType)
235231
return nil, errors.New("unknown content type")
236232
}
237233

238-
return &measuringIter[iter.Result[types.ProviderResponse]]{Iter: it, ctx: ctx, m: m}, nil
234+
return &measuringIter[iter.Result[types.Record]]{Iter: it, ctx: ctx, m: m}, nil
239235
}
240236

237+
// Deprecated: protocol-agnostic provide is being worked on in [IPIP-378]:
238+
//
239+
// [IPIP-378]: https://github.com/ipfs/specs/pull/378
241240
func (c *client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Duration) (time.Duration, error) {
242241
if c.identity == nil {
243242
return 0, errors.New("cannot provide Bitswap records without an identity")
@@ -253,7 +252,7 @@ func (c *client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Du
253252

254253
now := c.clock.Now()
255254

256-
req := types.WriteBitswapProviderRecord{
255+
req := types.WriteBitswapRecord{
257256
Protocol: "transport-bitswap",
258257
Schema: types.SchemaBitswap,
259258
Payload: types.BitswapPayload{
@@ -282,10 +281,13 @@ func (c *client) ProvideBitswap(ctx context.Context, keys []cid.Cid, ttl time.Du
282281
}
283282

284283
// ProvideAsync makes a provide request to a delegated router
285-
func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.WriteBitswapProviderRecord) (time.Duration, error) {
286-
req := jsontypes.WriteProvidersRequest{Providers: []types.WriteProviderRecord{bswp}}
284+
//
285+
//lint:ignore SA1019 // ignore staticcheck
286+
func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.WriteBitswapRecord) (time.Duration, error) {
287+
//lint:ignore SA1019 // ignore staticcheck
288+
req := jsontypes.WriteProvidersRequest{Providers: []types.Record{bswp}}
287289

288-
url := c.baseURL + server.ProvidePath
290+
url := c.baseURL + "/routing/v1/providers/"
289291

290292
b, err := drjson.MarshalJSONBytes(req)
291293
if err != nil {
@@ -306,6 +308,8 @@ func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.Wri
306308
if resp.StatusCode != http.StatusOK {
307309
return 0, httpError(resp.StatusCode, resp.Body)
308310
}
311+
312+
//lint:ignore SA1019 // ignore staticcheck
309313
var provideResult jsontypes.WriteProvidersResponse
310314
err = json.NewDecoder(resp.Body).Decode(&provideResult)
311315
if err != nil {
@@ -315,7 +319,8 @@ func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.Wri
315319
return 0, fmt.Errorf("expected 1 result but got %d", len(provideResult.ProvideResults))
316320
}
317321

318-
v, ok := provideResult.ProvideResults[0].(*types.WriteBitswapProviderRecordResponse)
322+
//lint:ignore SA1019 // ignore staticcheck
323+
v, ok := provideResult.ProvideResults[0].(*types.WriteBitswapRecordResponse)
319324
if !ok {
320325
return 0, fmt.Errorf("expected AdvisoryTTL field")
321326
}
@@ -327,7 +332,80 @@ func (c *client) provideSignedBitswapRecord(ctx context.Context, bswp *types.Wri
327332
return 0, nil
328333
}
329334

330-
func (c *client) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Record, error) {
335+
func (c *client) FindPeers(ctx context.Context, pid peer.ID) (peers iter.ResultIter[types.Record], err error) {
336+
m := newMeasurement("FindPeers")
337+
338+
url := c.baseURL + "/routing/v1/peers/" + peer.ToCid(pid).String()
339+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
340+
if err != nil {
341+
return nil, err
342+
}
343+
req.Header.Set("Accept", c.accepts)
344+
345+
m.host = req.Host
346+
347+
start := c.clock.Now()
348+
resp, err := c.httpClient.Do(req)
349+
350+
m.err = err
351+
m.latency = c.clock.Since(start)
352+
353+
if err != nil {
354+
m.record(ctx)
355+
return nil, err
356+
}
357+
358+
m.statusCode = resp.StatusCode
359+
if resp.StatusCode == http.StatusNotFound {
360+
resp.Body.Close()
361+
m.record(ctx)
362+
return iter.FromSlice[iter.Result[types.Record]](nil), nil
363+
}
364+
365+
if resp.StatusCode != http.StatusOK {
366+
err := httpError(resp.StatusCode, resp.Body)
367+
resp.Body.Close()
368+
m.record(ctx)
369+
return nil, err
370+
}
371+
372+
respContentType := resp.Header.Get("Content-Type")
373+
mediaType, _, err := mime.ParseMediaType(respContentType)
374+
if err != nil {
375+
resp.Body.Close()
376+
m.err = err
377+
m.record(ctx)
378+
return nil, fmt.Errorf("parsing Content-Type: %w", err)
379+
}
380+
381+
m.mediaType = mediaType
382+
383+
var skipBodyClose bool
384+
defer func() {
385+
if !skipBodyClose {
386+
resp.Body.Close()
387+
}
388+
}()
389+
390+
var it iter.ResultIter[types.Record]
391+
switch mediaType {
392+
case mediaTypeJSON:
393+
parsedResp := &jsontypes.PeersResponse{}
394+
err = json.NewDecoder(resp.Body).Decode(parsedResp)
395+
var sliceIt iter.Iter[types.Record] = iter.FromSlice(parsedResp.Peers)
396+
it = iter.ToResultIter(sliceIt)
397+
case mediaTypeNDJSON:
398+
skipBodyClose = true
399+
it = ndjson.NewRecordsIter(resp.Body)
400+
default:
401+
logger.Errorw("unknown media type", "MediaType", mediaType, "ContentType", respContentType)
402+
return nil, errors.New("unknown content type")
403+
}
404+
405+
return &measuringIter[iter.Result[types.Record]]{Iter: it, ctx: ctx, m: m}, nil
406+
}
407+
408+
func (c *client) GetIPNS(ctx context.Context, name ipns.Name) (*ipns.Record, error) {
331409
url := c.baseURL + "/routing/v1/ipns/" + name.String()
332410

333411
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
@@ -365,7 +443,7 @@ func (c *client) FindIPNSRecord(ctx context.Context, name ipns.Name) (*ipns.Reco
365443
return record, nil
366444
}
367445

368-
func (c *client) ProvideIPNSRecord(ctx context.Context, name ipns.Name, record *ipns.Record) error {
446+
func (c *client) PutIPNS(ctx context.Context, name ipns.Name, record *ipns.Record) error {
369447
url := c.baseURL + "/routing/v1/ipns/" + name.String()
370448

371449
rawRecord, err := ipns.MarshalRecord(record)

0 commit comments

Comments
 (0)