@@ -233,6 +233,8 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
233233 return
234234 }
235235
236+ i .handlePathAffinityHints (w , r , contentPath , logger )
237+
236238 // Detect when explicit Accept header or ?format parameter are present
237239 responseFormat , formatParams , err := customResponseFormat (r )
238240 if err != nil {
@@ -752,7 +754,7 @@ func (i *handler) handleWebRequestErrors(w http.ResponseWriter, r *http.Request,
752754}
753755
754756// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore.
755- // https://github.com/ ipfs/specs/blob/main/ http-gateways/PATH_GATEWAY.md #cache-control-request-header
757+ // https://specs. ipfs.tech/ http-gateways/path-gateway/ #cache-control-request-header
756758func (i * handler ) handleOnlyIfCached (w http.ResponseWriter , r * http.Request , contentPath path.Path ) bool {
757759 if r .Header .Get ("Cache-Control" ) == "only-if-cached" {
758760 if ! i .backend .IsCached (r .Context (), contentPath ) {
@@ -887,6 +889,103 @@ func (i *handler) handleSuperfluousNamespace(w http.ResponseWriter, r *http.Requ
887889 return true
888890}
889891
892+ // Detect 'Ipfs-Path-Affinity' (IPIP-462) headers in request and use values as a content
893+ // routing hints if passed paths are not already in the local datastore.
894+ // These optional hints are mostly useful for trustless block requests.
895+ // See https://github.com/ipfs/specs/pull/462
896+ func (i * handler ) handlePathAffinityHints (w http.ResponseWriter , r * http.Request , contentPath path.Path , logger * zap.SugaredLogger ) {
897+ headerName := "Ipfs-Path-Affinity"
898+ // Skip if no header
899+ if r .Header .Get (headerName ) == "" {
900+ return
901+ }
902+ // Skip if contentPath is already locally cached
903+ if i .backend .IsCached (r .Context (), contentPath ) {
904+ return
905+ }
906+ // Check canonical header name
907+ // NOTE: we don't use r.Header.Get() because client can send this header more than once
908+ headerValues := r .Header [headerName ]
909+ // If not found, try lowercase version.
910+ // NOTE: this is done manually because direct key access does not come with canonicalization, like Header.Get() does
911+ if len (headerValues ) == 0 {
912+ headerValues = r .Header [strings .ToLower (headerName )]
913+ }
914+
915+ // Limit the headerValues to the first 3 items (abuse protection)
916+ if len (headerValues ) > 3 {
917+ headerValues = headerValues [:3 ]
918+ }
919+
920+ // Process affinity hints
921+ for _ , headerValue := range headerValues {
922+ // Non-ascii paths are percent-encoded.
923+ // Decode if the value starts with %2F (percent-encoded '/')
924+ if strings .HasPrefix (headerValue , "%2F" ) {
925+ decodedValue , err := url .PathUnescape (headerValue )
926+ if err != nil {
927+ logger .Debugw ("skipping invalid Ipfs-Path-Affinity hint" , "error" , err )
928+ continue
929+ }
930+ headerValue = decodedValue
931+ }
932+ // Confirm it is a valid content path
933+ affinityPath , err := path .NewPath (headerValue )
934+ if err != nil {
935+ logger .Debugw ("skipping invalid Ipfs-Path-Affinity hint" , "error" , err )
936+ continue
937+ }
938+
939+ // Skip duplicated work if immutable affinity hint is a subset of requested immutable contentPath
940+ // (protect against broken clients that use affinity incorrectly)
941+ if ! contentPath .Mutable () && ! affinityPath .Mutable () && strings .HasPrefix (contentPath .String (), affinityPath .String ()) {
942+ logger .Debugw ("skipping redundant Ipfs-Path-Affinity hint" , "affinity" , affinityPath )
943+ continue
944+ }
945+
946+ // Process hint in background without blocking response logic for contentPath
947+ go func (contentPath path.Path , affinityPath path.Path , logger * zap.SugaredLogger ) {
948+ var immutableAffinityPath path.ImmutablePath
949+ logger .Debugw ("async processing of Ipfs-Path-Affinity hint" , "affinity" , affinityPath )
950+ if affinityPath .Mutable () {
951+ // Skip work if mutable affinity hint is a subset of mutable contentPath
952+ if contentPath .Mutable () && strings .HasPrefix (contentPath .String (), affinityPath .String ()) {
953+ logger .Debugw ("skipping redundant Ipfs-Path-Affinity hint" , "affinity" , affinityPath )
954+ return
955+ }
956+ immutableAffinityPath , _ , _ , err = i .backend .ResolveMutable (r .Context (), affinityPath )
957+ if err != nil {
958+ logger .Debugw ("error while resolving mutable Ipfs-Path-Affinity hint" , "affinity" , affinityPath , "error" , err )
959+ return
960+ }
961+ } else {
962+ ipath , ok := affinityPath .(path.ImmutablePath )
963+ if ! ok {
964+ return
965+ }
966+ immutableAffinityPath = ipath
967+ }
968+ // Skip if affinity path is already cached
969+ if ! i .backend .IsCached (r .Context (), immutableAffinityPath ) {
970+ // The intention of below code is to asynchronously preconnect
971+ // gateway with providers of the affinityPath in
972+ // Ipfs-Path-Affinity hint. Once connected, these peers can be
973+ // asked directly (via mechanism like bitswap) for blocks
974+ // related to main request for contentPath, and retrieve them,
975+ // even when no other routing system had them announced. If
976+ // original contentPath was received and returned to HTTP
977+ // client before below get is done, the work is cancelled.
978+
979+ logger .Debugw ("started async search for providers of Ipfs-Path-Affinity hint" , "affinity" , affinityPath )
980+ _ , _ , err = i .backend .GetBlock (r .Context (), immutableAffinityPath )
981+ logger .Debugw ("ended async search for providers of Ipfs-Path-Affinity hint" , "affinity" , affinityPath , "error" , err )
982+ } else {
983+ logger .Debugw ("skipping Ipfs-Path-Affinity hint due to data being locally cached" , "affinity" , affinityPath )
984+ }
985+ }(contentPath , affinityPath , logger )
986+ }
987+ }
988+
890989// getTemplateGlobalData returns the global data necessary by most templates.
891990func (i * handler ) getTemplateGlobalData (r * http.Request , contentPath path.Path ) assets.GlobalData {
892991 // gatewayURL is used to link to other root CIDs. THis will be blank unless
0 commit comments