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
4749var logger = logging .Logger ("routing/http/server" )
4850
4951const (
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
5659type 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
309317func (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+
599666var (
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+
609702func 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