@cryptotaxi247 / kubo / commits / e07baf583

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 }