Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(gw): extract logical functions to improve readability #8885

Merged
merged 3 commits into from
Apr 15, 2022
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 149 additions & 73 deletions core/corehttp/gateway_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
prometheus "github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"go.uber.org/zap"
)

const (
Expand Down Expand Up @@ -85,6 +86,25 @@ type statusResponseWriter struct {
http.ResponseWriter
}

// Custom type for collecting error details to be handled by `webRequestError`
type requestError struct {
Message string
StatusCode int
Err error
}

func (r *requestError) Error() string {
return r.Err.Error()
}

func newRequestError(message string, err error, statusCode int) error {
return &requestError{
Message: message,
Err: err,
StatusCode: statusCode,
}
}

func (sw *statusResponseWriter) WriteHeader(code int) {
// Check if we need to adjust Status Code to account for scheduled redirect
// This enables us to return payload along with HTTP 301
Expand Down Expand Up @@ -324,61 +344,34 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
logger := log.With("from", r.RequestURI)
logger.Debug("http request received")

// X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/go-ipfs/issues/7702)
// TODO: remove this after go-ipfs 0.13 ships
if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" {
err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/go-ipfs/issues/7702")
webError(w, "unsupported HTTP header", err, http.StatusBadRequest)
var (
err error
shouldReturn bool
Jorropo marked this conversation as resolved.
Show resolved Hide resolved
)

if err := handleUnsupportedHeaders(r); err != nil {
webRequestError(w, err)
return
}

// ?uri query param support for requests produced by web browsers
// via navigator.registerProtocolHandler Web API
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
// TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val
if uriParam := r.URL.Query().Get("uri"); uriParam != "" {
u, err := url.Parse(uriParam)
if err != nil {
webError(w, "failed to parse uri query parameter", err, http.StatusBadRequest)
return
}
if u.Scheme != "ipfs" && u.Scheme != "ipns" {
webError(w, "uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest)
return
}
path := u.Path
if u.RawQuery != "" { // preserve query if present
path = path + "?" + u.RawQuery
}

redirectURL := gopath.Join("/", u.Scheme, u.Host, path)
logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently)
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
err, shouldReturn = handleProtocolHandlerRedirect(w, r, logger)
Jorropo marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
webRequestError(w, err)
Jorropo marked this conversation as resolved.
Show resolved Hide resolved
}
if shouldReturn {
return
}
Jorropo marked this conversation as resolved.
Show resolved Hide resolved

// Service Worker registration request
if r.Header.Get("Service-Worker") == "script" {
// Disallow Service Worker registration on namespace roots
// https://github.com/ipfs/go-ipfs/issues/4025
matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path)
if matched {
err := fmt.Errorf("registration is not allowed for this scope")
webError(w, "navigator.serviceWorker", err, http.StatusBadRequest)
return
}
if err := handleServiceWorkerRegistration(r); err != nil {
webRequestError(w, err)
return
}

contentPath := ipath.New(r.URL.Path)
if pathErr := contentPath.IsValid(); pathErr != nil {
if fixupSuperfluousNamespace(w, r.URL.Path, r.URL.RawQuery) {
// the error was due to redundant namespace, which we were able to fix
// by returning error/redirect page, nothing left to do here
logger.Debugw("redundant namespace; noop")
return
}
// unable to fix path, returning error
webError(w, "invalid ipfs path", pathErr, http.StatusBadRequest)
if err, shouldReturn = handleSuperfluousNamespace(w, r, contentPath); err != nil {
Jorropo marked this conversation as resolved.
Show resolved Hide resolved
webRequestError(w, err)
}
if shouldReturn {
return
}

Expand Down Expand Up @@ -416,26 +409,13 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request
return
}

// Update the global metric of the time it takes to read the final root block of the requested resource
// NOTE: for legacy reasons this happens before we go into content-type specific code paths
_, err = i.api.Block().Get(r.Context(), resolvedPath)
if err != nil {
webError(w, "ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError)
if err = i.handleGettingFirstBlock(r, begin, contentPath, resolvedPath); err != nil {
webRequestError(w, err)
return
}
ns := contentPath.Namespace()
timeToGetFirstContentBlock := time.Since(begin).Seconds()
i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead
i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock)

// HTTP Headers
i.addUserHeaders(w) // ok, _now_ write user's headers.
w.Header().Set("X-Ipfs-Path", contentPath.String())

if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil {
w.Header().Set("X-Ipfs-Roots", rootCids)
} else { // this should never happen, as we resolved the contentPath already
webError(w, "error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
if err = i.setCommonHeaders(w, r, contentPath); err != nil {
webRequestError(w, err)
return
}

Expand Down Expand Up @@ -785,6 +765,11 @@ func (i *gatewayHandler) buildIpfsRootsHeader(contentPath string, r *http.Reques
return rootCidList, nil
}

func webRequestError(w http.ResponseWriter, err error) {
re := err.(*requestError)
webError(w, re.Message, re.Err, re.StatusCode)
}
Jorropo marked this conversation as resolved.
Show resolved Hide resolved

func webError(w http.ResponseWriter, message string, err error, defaultCode int) {
if _, ok := err.(resolver.ErrNoLink); ok {
webErrorWithCode(w, message, err, http.StatusNotFound)
Expand Down Expand Up @@ -911,32 +896,123 @@ func debugStr(path string) string {
return q
}

func handleUnsupportedHeaders(r *http.Request) (err error) {
// X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/go-ipfs/issues/7702)
// TODO: remove this after go-ipfs 0.13 ships
if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" {
err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/go-ipfs/issues/7702")
return newRequestError("unsupported HTTP header", err, http.StatusBadRequest)
}
return nil
}

// ?uri query param support for requests produced by web browsers
// via navigator.registerProtocolHandler Web API
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler
// TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val
func handleProtocolHandlerRedirect(w http.ResponseWriter, r *http.Request, logger *zap.SugaredLogger) (err error, shouldReturn bool) {
if uriParam := r.URL.Query().Get("uri"); uriParam != "" {
u, err := url.Parse(uriParam)
if err != nil {
return newRequestError("failed to parse uri query parameter", err, http.StatusBadRequest), true
}
if u.Scheme != "ipfs" && u.Scheme != "ipns" {
return newRequestError("uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest), true
}
path := u.Path
if u.RawQuery != "" { // preserve query if present
path = path + "?" + u.RawQuery
}

redirectURL := gopath.Join("/", u.Scheme, u.Host, path)
logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently)
http.Redirect(w, r, redirectURL, http.StatusMovedPermanently)
return nil, true
}

return nil, false
}

// Disallow Service Worker registration on namespace roots
// https://github.com/ipfs/go-ipfs/issues/4025
func handleServiceWorkerRegistration(r *http.Request) (err error) {
if r.Header.Get("Service-Worker") == "script" {
matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path)
if matched {
err := fmt.Errorf("registration is not allowed for this scope")
return newRequestError("navigator.serviceWorker", err, http.StatusBadRequest)
}
}

return nil
}

// Attempt to fix redundant /ipfs/ namespace as long as resulting
// 'intended' path is valid. This is in case gremlins were tickled
// wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id}
// like in bafybeien3m7mdn6imm425vc2s22erzyhbvk5n3ofzgikkhmdkh5cuqbpbq :^))
func fixupSuperfluousNamespace(w http.ResponseWriter, urlPath string, urlQuery string) bool {
if !(strings.HasPrefix(urlPath, "/ipfs/ipfs/") || strings.HasPrefix(urlPath, "/ipfs/ipns/")) {
return false // not a superfluous namespace
func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) (err error, shouldReturn bool) {
// If the path is valid, there's nothing to do
if pathErr := contentPath.IsValid(); pathErr == nil {
return nil, false
}

// If there's no superflous namespace, there's nothing to do
if !(strings.HasPrefix(r.URL.Path, "/ipfs/ipfs/") || strings.HasPrefix(r.URL.Path, "/ipfs/ipns/")) {
return nil, false
}
intendedPath := ipath.New(strings.TrimPrefix(urlPath, "/ipfs"))

// Attempt to fix the superflous namespace
intendedPath := ipath.New(strings.TrimPrefix(r.URL.Path, "/ipfs"))
if err := intendedPath.IsValid(); err != nil {
return false // not a valid path
return newRequestError("invalid ipfs path", err, http.StatusBadRequest), true
}
intendedURL := intendedPath.String()
if urlQuery != "" {
if r.URL.RawQuery != "" {
// we render HTML, so ensure query entries are properly escaped
q, _ := url.ParseQuery(urlQuery)
q, _ := url.ParseQuery(r.URL.RawQuery)
intendedURL = intendedURL + "?" + q.Encode()
}
// return HTTP 400 (Bad Request) with HTML error page that:
// - points at correct canonical path via <link> header
// - displays human-readable error
// - redirects to intendedURL after a short delay

w.WriteHeader(http.StatusBadRequest)
return redirectTemplate.Execute(w, redirectTemplateData{
if err := redirectTemplate.Execute(w, redirectTemplateData{
RedirectURL: intendedURL,
SuggestedPath: intendedPath.String(),
ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", urlPath, intendedPath.String()),
}) == nil
ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", r.URL.Path, intendedPath.String()),
}); err != nil {
return newRequestError("failed to redirect when fixing superfluous namespace", err, http.StatusBadRequest), true
}

return nil, true
}

func (i *gatewayHandler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) error {
// Update the global metric of the time it takes to read the final root block of the requested resource
// NOTE: for legacy reasons this happens before we go into content-type specific code paths
_, err := i.api.Block().Get(r.Context(), resolvedPath)
if err != nil {
return newRequestError("ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError)
}
ns := contentPath.Namespace()
timeToGetFirstContentBlock := time.Since(begin).Seconds()
i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead
i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock)
return nil
}

func (i *gatewayHandler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) error {
i.addUserHeaders(w) // ok, _now_ write user's headers.
w.Header().Set("X-Ipfs-Path", contentPath.String())

if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil {
w.Header().Set("X-Ipfs-Roots", rootCids)
} else { // this should never happen, as we resolved the contentPath already
return newRequestError("error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError)
}

return nil
}