@@ -233,6 +233,8 @@ func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) {
233
233
return
234
234
}
235
235
236
+ i .handlePathAffinityHints (w , r , contentPath , logger )
237
+
236
238
// Detect when explicit Accept header or ?format parameter are present
237
239
responseFormat , formatParams , err := customResponseFormat (r )
238
240
if err != nil {
@@ -752,7 +754,7 @@ func (i *handler) handleWebRequestErrors(w http.ResponseWriter, r *http.Request,
752
754
}
753
755
754
756
// 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
756
758
func (i * handler ) handleOnlyIfCached (w http.ResponseWriter , r * http.Request , contentPath path.Path ) bool {
757
759
if r .Header .Get ("Cache-Control" ) == "only-if-cached" {
758
760
if ! i .backend .IsCached (r .Context (), contentPath ) {
@@ -887,6 +889,103 @@ func (i *handler) handleSuperfluousNamespace(w http.ResponseWriter, r *http.Requ
887
889
return true
888
890
}
889
891
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
+
890
989
// getTemplateGlobalData returns the global data necessary by most templates.
891
990
func (i * handler ) getTemplateGlobalData (r * http.Request , contentPath path.Path ) assets.GlobalData {
892
991
// gatewayURL is used to link to other root CIDs. THis will be blank unless
0 commit comments