Skip to content

Commit 117edfe

Browse files
committed
routing/http/server: add support for GetClosestPeers
This adds the SERVER-side for GetClosestPeers. Since FindPeers also returns PeerRecords, it is essentially a copy-paste, minus things like addrFilters which don't apply here, plus `count` and `closerThan` parsing from the query URL. The tests as well. We leave all logic regarding count/closerThan to the ContentRouter (the DHT, or the Kubo wrapper around it). Spec: ipfs/specs#476
1 parent a2d90a9 commit 117edfe

File tree

2 files changed

+467
-28
lines changed

2 files changed

+467
-28
lines changed

routing/http/server/server.go

Lines changed: 121 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"io"
1010
"mime"
1111
"net/http"
12+
"strconv"
1213
"strings"
1314
"sync/atomic"
1415
"time"
@@ -42,15 +43,17 @@ const (
4243
DefaultRecordsLimit = 20
4344
DefaultStreamingRecordsLimit = 0
4445
DefaultRoutingTimeout = 30 * time.Second
46+
DefaultGetClosestPeersCount = 20
4547
)
4648

4749
var logger = logging.Logger("routing/http/server")
4850

4951
const (
50-
providePath = "/routing/v1/providers/"
51-
findProvidersPath = "/routing/v1/providers/{cid}"
52-
findPeersPath = "/routing/v1/peers/{peer-id}"
53-
getIPNSPath = "/routing/v1/ipns/{cid}"
52+
providePath = "/routing/v1/providers/"
53+
findProvidersPath = "/routing/v1/providers/{cid}"
54+
findPeersPath = "/routing/v1/peers/{peer-id}"
55+
getIPNSPath = "/routing/v1/ipns/{cid}"
56+
getClosestPeersPath = "/routing/v1/dht/closest/peers/{peer-id}"
5457
)
5558

5659
type FindProvidersAsyncResponse struct {
@@ -78,6 +81,10 @@ type ContentRouter interface {
7881
// PutIPNS stores the provided [ipns.Record] for the given [ipns.Name].
7982
// It is guaranteed that the record matches the provided name.
8083
PutIPNS(ctx context.Context, name ipns.Name, record *ipns.Record) error
84+
85+
// GetClosestPeers returns the DHT closest peers to the given peer ID.
86+
// If empty, it will use the content router's peer ID (self). `closerThan` (optional) forces resulting records to be closer to `PeerID` than to `closerThan`. `count` specifies how many records to return ([1,100], with 20 as default when set to 0).
87+
GetClosestPeers(ctx context.Context, peerID, closerThan peer.ID, count int) (iter.ResultIter[*types.PeerRecord], error)
8188
}
8289

8390
// Deprecated: protocol-agnostic provide is being worked on in [IPIP-378]:
@@ -183,6 +190,7 @@ func Handler(svc ContentRouter, opts ...Option) http.Handler {
183190
r.Handle(findPeersPath, middlewarestd.Handler(findPeersPath, mdlw, http.HandlerFunc(server.findPeers))).Methods(http.MethodGet)
184191
r.Handle(getIPNSPath, middlewarestd.Handler(getIPNSPath, mdlw, http.HandlerFunc(server.GetIPNS))).Methods(http.MethodGet)
185192
r.Handle(getIPNSPath, middlewarestd.Handler(getIPNSPath, mdlw, http.HandlerFunc(server.PutIPNS))).Methods(http.MethodPut)
193+
r.Handle(getClosestPeersPath, middlewarestd.Handler(getClosestPeersPath, mdlw, http.HandlerFunc(server.getClosestPeers))).Methods(http.MethodGet)
186194

187195
return r
188196
}
@@ -308,30 +316,7 @@ func (s *server) findProvidersNDJSON(w http.ResponseWriter, provIter iter.Result
308316

309317
func (s *server) findPeers(w http.ResponseWriter, r *http.Request) {
310318
pidStr := mux.Vars(r)["peer-id"]
311-
312-
// While specification states that peer-id is expected to be in CIDv1 format, reality
313-
// is the clients will often learn legacy PeerID string from other sources,
314-
// and try to use it.
315-
// See https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
316-
// We are liberal in inputs here, and uplift legacy PeerID to CID if necessary.
317-
// Rationale: it is better to fix this common mistake than to error and break peer routing.
318-
319-
// Attempt to parse PeerID
320-
pid, err := peer.Decode(pidStr)
321-
if err != nil {
322-
// Retry by parsing PeerID as CID, then setting codec to libp2p-key
323-
// and turning that back to PeerID.
324-
// This is necessary to make sure legacy keys like:
325-
// - RSA QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
326-
// - ED25519 12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA
327-
// are parsed correctly.
328-
pidAsCid, err2 := cid.Decode(pidStr)
329-
if err2 == nil {
330-
pidAsCid = cid.NewCidV1(cid.Libp2pKey, pidAsCid.Hash())
331-
pid, err = peer.FromCid(pidAsCid)
332-
}
333-
}
334-
319+
pid, err := parsePeerID(pidStr)
335320
if err != nil {
336321
writeErr(w, "FindPeers", http.StatusBadRequest, fmt.Errorf("unable to parse PeerID %q: %w", pidStr, err))
337322
return
@@ -596,6 +581,88 @@ func (s *server) PutIPNS(w http.ResponseWriter, r *http.Request) {
596581
w.WriteHeader(http.StatusOK)
597582
}
598583

584+
func (s *server) getClosestPeers(w http.ResponseWriter, r *http.Request) {
585+
pidStr := mux.Vars(r)["peer-id"]
586+
pid, err := parsePeerID(pidStr)
587+
if err != nil {
588+
writeErr(w, "GetClosestPeers", http.StatusBadRequest, fmt.Errorf("unable to parse PeerID %q: %w", pidStr, err))
589+
return
590+
}
591+
592+
query := r.URL.Query()
593+
closerThanStr := query.Get("closerThan")
594+
var closerThanPid peer.ID
595+
if closerThanStr != "" { // it is fine to omit. We will pass an empty peer.ID then.
596+
closerThanPid, err = parsePeerID(closerThanStr)
597+
if err != nil {
598+
writeErr(w, "GetClosestPeers", http.StatusBadRequest, fmt.Errorf("unable to parse closer-than PeerID %q: %w", pidStr, err))
599+
return
600+
}
601+
}
602+
603+
countStr := query.Get("count")
604+
count, err := strconv.Atoi(countStr)
605+
if err != nil {
606+
count = 0
607+
}
608+
if count > 100 {
609+
count = 100
610+
}
611+
// If limit is still 0, set THE default.
612+
if count <= 0 {
613+
count = DefaultGetClosestPeersCount
614+
}
615+
616+
mediaType, err := s.detectResponseType(r)
617+
if err != nil {
618+
writeErr(w, "GetClosestPeers", http.StatusBadRequest, err)
619+
return
620+
}
621+
622+
var (
623+
handlerFunc func(w http.ResponseWriter, provIter iter.ResultIter[*types.PeerRecord])
624+
)
625+
626+
if mediaType == mediaTypeNDJSON {
627+
handlerFunc = s.getClosestPeersNDJSON
628+
} else {
629+
handlerFunc = s.getClosestPeersJSON
630+
}
631+
632+
// Add timeout to the routing operation
633+
ctx, cancel := context.WithTimeout(r.Context(), s.routingTimeout)
634+
defer cancel()
635+
636+
provIter, err := s.svc.GetClosestPeers(ctx, pid, closerThanPid, count)
637+
if err != nil {
638+
if errors.Is(err, routing.ErrNotFound) {
639+
// handlerFunc takes care of setting the 404 and necessary headers
640+
provIter = iter.FromSlice([]iter.Result[*types.PeerRecord]{})
641+
} else {
642+
writeErr(w, "GetClosestPeers", http.StatusInternalServerError, fmt.Errorf("delegate error: %w", err))
643+
return
644+
}
645+
}
646+
handlerFunc(w, provIter)
647+
}
648+
649+
func (s *server) getClosestPeersJSON(w http.ResponseWriter, peersIter iter.ResultIter[*types.PeerRecord]) {
650+
defer peersIter.Close()
651+
peers, err := iter.ReadAllResults(peersIter)
652+
if err != nil {
653+
writeErr(w, "GetClosestPeers", http.StatusInternalServerError, fmt.Errorf("delegate error: %w", err))
654+
return
655+
}
656+
657+
writeJSONResult(w, "FindPeers", jsontypes.PeersResponse{
658+
Peers: peers,
659+
})
660+
}
661+
662+
func (s *server) getClosestPeersNDJSON(w http.ResponseWriter, peersIter iter.ResultIter[*types.PeerRecord]) {
663+
writeResultsIterNDJSON(w, peersIter)
664+
}
665+
599666
var (
600667
// Rule-of-thumb Cache-Control policy is to work well with caching proxies and load balancers.
601668
// If there are any results, cache on the client for longer, and hint any in-between caches to
@@ -606,6 +673,32 @@ var (
606673
maxStale = int((48 * time.Hour).Seconds()) // allow stale results as long within Amino DHT Expiration window
607674
)
608675

676+
func parsePeerID(pidStr string) (peer.ID, error) {
677+
// While specification states that peer-id is expected to be in CIDv1 format, reality
678+
// is the clients will often learn legacy PeerID string from other sources,
679+
// and try to use it.
680+
// See https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
681+
// We are liberal in inputs here, and uplift legacy PeerID to CID if necessary.
682+
// Rationale: it is better to fix this common mistake than to error and break peer routing.
683+
684+
// Attempt to parse PeerID
685+
pid, err := peer.Decode(pidStr)
686+
if err != nil {
687+
// Retry by parsing PeerID as CID, then setting codec to libp2p-key
688+
// and turning that back to PeerID.
689+
// This is necessary to make sure legacy keys like:
690+
// - RSA QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
691+
// - ED25519 12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA
692+
// are parsed correctly.
693+
pidAsCid, err2 := cid.Decode(pidStr)
694+
if err2 == nil {
695+
pidAsCid = cid.NewCidV1(cid.Libp2pKey, pidAsCid.Hash())
696+
pid, err = peer.FromCid(pidAsCid)
697+
}
698+
}
699+
return pid, err
700+
}
701+
609702
func setCacheControl(w http.ResponseWriter, maxAge int, stale int) {
610703
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d, stale-while-revalidate=%d, stale-if-error=%d", maxAge, stale, stale))
611704
}

0 commit comments

Comments
 (0)