chore(gw): extract logical functions to improve readability (#8885)
* Extract functions from getOrHeadHandler to improve readability and prepare for later refactorings * Address PR feedback on when to return errors or booleans * Be explicit about use of *requestError vs error
Justin Johnson committed
Apr 15, 2022 at 09:06 UTC
e07baf5835f646352df14fa21ed641053cd0f81b
1 file changed
+139
-73
core/corehttp/gateway_handler.go
+139
-73
@@ -28,6 +28,7 @@ import (
28
prometheus "github.com/prometheus/client_golang/prometheus"
29
"go.opentelemetry.io/otel/attribute"
30
"go.opentelemetry.io/otel/trace"
31
+ "go.uber.org/zap"
32
)
33
34
const (
@@ -85,6 +86,25 @@ type statusResponseWriter struct {
86
http.ResponseWriter
87
}
88
89
+// Custom type for collecting error details to be handled by `webRequestError`
90
+type requestError struct {
91
+ Message string
92
+ StatusCode int
93
+ Err error
94
+}
95
+
96
+func (r *requestError) Error() string {
97
+ return r.Err.Error()
98
+}
99
+
100
+func newRequestError(message string, err error, statusCode int) *requestError {
101
+ return &requestError{
102
+ Message: message,
103
+ Err: err,
104
+ StatusCode: statusCode,
105
+ }
106
+}
107
+
108
func (sw *statusResponseWriter) WriteHeader(code int) {
109
// Check if we need to adjust Status Code to account for scheduled redirect
110
// This enables us to return payload along with HTTP 301
@@ -324,61 +344,22 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
344
logger := log.With("from", r.RequestURI)
345
logger.Debug("http request received")
346
327
- // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/go-ipfs/issues/7702)
328
- // TODO: remove this after go-ipfs 0.13 ships
329
- if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" {
330
- err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/go-ipfs/issues/7702")
331
- webError(w, "unsupported HTTP header", err, http.StatusBadRequest)
347
+ if err := handleUnsupportedHeaders(r); err != nil {
348
+ webRequestError(w, err)
349
return
350
}
351
335
- // ?uri query param support for requests produced by web browsers
336
- // via navigator.registerProtocolHandler Web API
337
- // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
338
- // TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val
339
- if uriParam := r.URL.Query().Get("uri"); uriParam != "" {
340
- u, err := url.Parse(uriParam)
341
- if err != nil {
342
- webError(w, "failed to parse uri query parameter", err, http.StatusBadRequest)
343
- return
344
- }
345
- if u.Scheme != "ipfs" && u.Scheme != "ipns" {
346
- webError(w, "uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest)
347
- return
348
- }
349
- path := u.Path
350
- if u.RawQuery != "" { // preserve query if present
351
- path = path + "?" + u.RawQuery
352
- }
353
-
354
- redirectURL := gopath.Join("/", u.Scheme, u.Host, path)
355
- logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently)
356
- http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
352
+ if requestHandled := handleProtocolHandlerRedirect(w, r, logger); requestHandled {
353
return
354
}
355
360
- // Service Worker registration request
361
- if r.Header.Get("Service-Worker") == "script" {
362
- // Disallow Service Worker registration on namespace roots
363
- // https://github.com/ipfs/go-ipfs/issues/4025
364
- matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path)
365
- if matched {
366
- err := fmt.Errorf("registration is not allowed for this scope")
367
- webError(w, "navigator.serviceWorker", err, http.StatusBadRequest)
368
- return
369
- }
356
+ if err := handleServiceWorkerRegistration(r); err != nil {
357
+ webRequestError(w, err)
358
+ return
359
}
360
361
contentPath := ipath.New(r.URL.Path)
373
- if pathErr := contentPath.IsValid(); pathErr != nil {
374
- if fixupSuperfluousNamespace(w, r.URL.Path, r.URL.RawQuery) {
375
- // the error was due to redundant namespace, which we were able to fix
376
- // by returning error/redirect page, nothing left to do here
377
- logger.Debugw("redundant namespace; noop")
378
- return
379
- }
380
- // unable to fix path, returning error
381
- webError(w, "invalid ipfs path", pathErr, http.StatusBadRequest)
362
+ if requestHandled := handleSuperfluousNamespace(w, r, contentPath); requestHandled {
363
return
364
}
365
@@ -416,26 +397,13 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
397
return
398
}
399
419
- // Update the global metric of the time it takes to read the final root block of the requested resource
420
- // NOTE: for legacy reasons this happens before we go into content-type specific code paths
421
- _, err = i.api.Block().Get(r.Context(), resolvedPath)
422
- if err != nil {
423
- webError(w, "ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError)
400
+ if err := i.handleGettingFirstBlock(r, begin, contentPath, resolvedPath); err != nil {
401
+ webRequestError(w, err)
402
return
403
}
426
- ns := contentPath.Namespace()
427
- timeToGetFirstContentBlock := time.Since(begin).Seconds()
428
- i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead
429
- i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock)
404
431
- // HTTP Headers
432
- i.addUserHeaders(w) // ok, _now_ write user's headers.
433
- w.Header().Set("X-Ipfs-Path", contentPath.String())
434
-
435
- if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil {
436
- w.Header().Set("X-Ipfs-Roots", rootCids)
437
- } else { // this should never happen, as we resolved the contentPath already
438
- webError(w, "error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
405
+ if err := i.setCommonHeaders(w, r, contentPath); err != nil {
406
+ webRequestError(w, err)
407
return
408
}
409
@@ -785,6 +753,10 @@ func (i *gatewayHandler) buildIpfsRootsHeader(contentPath string, r *http.Reques
753
return rootCidList, nil
754
}
755
756
+func webRequestError(w http.ResponseWriter, err *requestError) {
757
+ webError(w, err.Message, err.Err, err.StatusCode)
758
+}
759
+
760
func webError(w http.ResponseWriter, message string, err error, defaultCode int) {
761
if _, ok := err.(resolver.ErrNoLink); ok {
762
webErrorWithCode(w, message, err, http.StatusNotFound)
@@ -911,32 +883,126 @@ func debugStr(path string) string {
883
return q
884
}
885
886
+func handleUnsupportedHeaders(r *http.Request) (err *requestError) {
887
+ // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/go-ipfs/issues/7702)
888
+ // TODO: remove this after go-ipfs 0.13 ships
889
+ if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" {
890
+ err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/go-ipfs/issues/7702")
891
+ return newRequestError("unsupported HTTP header", err, http.StatusBadRequest)
892
+ }
893
+ return nil
894
+}
895
+
896
+// ?uri query param support for requests produced by web browsers
897
+// via navigator.registerProtocolHandler Web API
898
+// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
899
+// TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val
900
+func handleProtocolHandlerRedirect(w http.ResponseWriter, r *http.Request, logger *zap.SugaredLogger) (requestHandled bool) {
901
+ if uriParam := r.URL.Query().Get("uri"); uriParam != "" {
902
+ u, err := url.Parse(uriParam)
903
+ if err != nil {
904
+ webError(w, "failed to parse uri query parameter", err, http.StatusBadRequest)
905
+ return true
906
+ }
907
+ if u.Scheme != "ipfs" && u.Scheme != "ipns" {
908
+ webError(w, "uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest)
909
+ return true
910
+ }
911
+ path := u.Path
912
+ if u.RawQuery != "" { // preserve query if present
913
+ path = path + "?" + u.RawQuery
914
+ }
915
+
916
+ redirectURL := gopath.Join("/", u.Scheme, u.Host, path)
917
+ logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently)
918
+ http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
919
+ return true
920
+ }
921
+
922
+ return false
923
+}
924
+
925
+// Disallow Service Worker registration on namespace roots
926
+// https://github.com/ipfs/go-ipfs/issues/4025
927
+func handleServiceWorkerRegistration(r *http.Request) (err *requestError) {
928
+ if r.Header.Get("Service-Worker") == "script" {
929
+ matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path)
930
+ if matched {
931
+ err := fmt.Errorf("registration is not allowed for this scope")
932
+ return newRequestError("navigator.serviceWorker", err, http.StatusBadRequest)
933
+ }
934
+ }
935
+
936
+ return nil
937
+}
938
+
939
// Attempt to fix redundant /ipfs/ namespace as long as resulting
940
// 'intended' path is valid. This is in case gremlins were tickled
941
// wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id}
942
// like in bafybeien3m7mdn6imm425vc2s22erzyhbvk5n3ofzgikkhmdkh5cuqbpbq :^))
918
-func fixupSuperfluousNamespace(w http.ResponseWriter, urlPath string, urlQuery string) bool {
919
- if !(strings.HasPrefix(urlPath, "/ipfs/ipfs/") || strings.HasPrefix(urlPath, "/ipfs/ipns/")) {
920
- return false // not a superfluous namespace
943
+func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) (requestHandled bool) {
944
+ // If the path is valid, there's nothing to do
945
+ if pathErr := contentPath.IsValid(); pathErr == nil {
946
+ return false
947
+ }
948
+
949
+ // If there's no superflous namespace, there's nothing to do
950
+ if !(strings.HasPrefix(r.URL.Path, "/ipfs/ipfs/") || strings.HasPrefix(r.URL.Path, "/ipfs/ipns/")) {
951
+ return false
952
}
922
- intendedPath := ipath.New(strings.TrimPrefix(urlPath, "/ipfs"))
953
+
954
+ // Attempt to fix the superflous namespace
955
+ intendedPath := ipath.New(strings.TrimPrefix(r.URL.Path, "/ipfs"))
956
if err := intendedPath.IsValid(); err != nil {
924
- return false // not a valid path
957
+ webError(w, "invalid ipfs path", err, http.StatusBadRequest)
958
+ return true
959
}
960
intendedURL := intendedPath.String()
927
- if urlQuery != "" {
961
+ if r.URL.RawQuery != "" {
962
// we render HTML, so ensure query entries are properly escaped
929
- q, _ := url.ParseQuery(urlQuery)
963
+ q, _ := url.ParseQuery(r.URL.RawQuery)
964
intendedURL = intendedURL + "?" + q.Encode()
965
}
966
// return HTTP 400 (Bad Request) with HTML error page that:
967
// - points at correct canonical path via <link> header
968
// - displays human-readable error
969
// - redirects to intendedURL after a short delay
970
+
971
w.WriteHeader(http.StatusBadRequest)
937
- return redirectTemplate.Execute(w, redirectTemplateData{
972
+ if err := redirectTemplate.Execute(w, redirectTemplateData{
973
RedirectURL: intendedURL,
974
SuggestedPath: intendedPath.String(),
940
- ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", urlPath, intendedPath.String()),
941
- }) == nil
975
+ ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", r.URL.Path, intendedPath.String()),
976
+ }); err != nil {
977
+ webError(w, "failed to redirect when fixing superfluous namespace", err, http.StatusBadRequest)
978
+ }
979
+
980
+ return true
981
+}
982
+
983
+func (i *gatewayHandler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) *requestError {
984
+ // Update the global metric of the time it takes to read the final root block of the requested resource
985
+ // NOTE: for legacy reasons this happens before we go into content-type specific code paths
986
+ _, err := i.api.Block().Get(r.Context(), resolvedPath)
987
+ if err != nil {
988
+ return newRequestError("ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError)
989
+ }
990
+ ns := contentPath.Namespace()
991
+ timeToGetFirstContentBlock := time.Since(begin).Seconds()
992
+ i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead
993
+ i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock)
994
+ return nil
995
+}
996
+
997
+func (i *gatewayHandler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) *requestError {
998
+ i.addUserHeaders(w) // ok, _now_ write user's headers.
999
+ w.Header().Set("X-Ipfs-Path", contentPath.String())
1000
+
1001
+ if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil {
1002
+ w.Header().Set("X-Ipfs-Roots", rootCids)
1003
+ } else { // this should never happen, as we resolved the contentPath already
1004
+ return newRequestError("error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
1005
+ }
1006
+
1007
+ return nil
1008
}