From 51ee5b04a9bcd6b635b4ff189719b0b360e5f6b0 Mon Sep 17 00:00:00 2001 From: Justin Johnson Date: Fri, 1 Apr 2022 16:42:17 -0500 Subject: [PATCH] - Update getOrHeadHandler to dispatch to getOrHeadHandlerUnixFs for Unixfs reponse format - Write functions for logic in getOrHeadHandler, to make it easier to read and to enable reuse in getOrHeadHandlerUnixfs - Move Unixfs specific functions to gateway_handler_unixfs.go - Check for _redirects file if we have origin isolation --- core/corehttp/gateway_handler.go | 355 ++++++++-------------- core/corehttp/gateway_handler_unixfs.go | 230 +++++++++++++- test/sharness/t0114-gateway-subdomains.sh | 36 +++ 3 files changed, 391 insertions(+), 230 deletions(-) diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index adfc09e6952f..650abde2a975 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -4,14 +4,12 @@ import ( "context" "fmt" "html/template" - "io" "net/http" "net/url" "os" gopath "path" "regexp" "runtime/debug" - "strconv" "strings" "time" @@ -25,6 +23,7 @@ import ( ipath "github.com/ipfs/interface-go-ipfs-core/path" routing "github.com/libp2p/go-libp2p-core/routing" prometheus "github.com/prometheus/client_golang/prometheus" + "go.uber.org/zap" ) const ( @@ -267,141 +266,65 @@ func (i *gatewayHandler) optionsHandler(w http.ResponseWriter, r *http.Request) func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { begin := time.Now() - urlPath := r.URL.Path 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) + if !validateSupportedHeaders(w, r) { 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) + if !protocolHandlerRegistration(w, r, logger) { return } - // 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 !serviceWorkerRegistration(w, r) { + return } - redirects, err := i.searchUpTreeForRedirects(r, urlPath) - if err == nil { - redirected, newPath, err := i.redirect(w, r, redirects) - if err != nil { - // FIXME what to do here with errors ... - } - - if redirected { - return - } - - if newPath != "" { - urlPath = newPath - } + contentPath := ipath.New(r.URL.Path) + if !fixSuperfluousNamespaces(w, r, contentPath, logger) { + return } - contentPath := ipath.New(urlPath) - if pathErr := contentPath.IsValid(); pathErr != nil { - if fixupSuperfluousNamespace(w, urlPath, 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) + // Support custom response formats passed via ?format or Accept HTTP header + responseFormat := customResponseFormat(r) + + // For Unixfs, when a path can't be resolved we need to check for redirects and pretty 404 page files. + if responseFormat == "" { + logger.Debugw("dispatching to getOrHeadHandlerUnixfs") + i.getOrHeadHandlerUnixfs(w, r, begin, logger) return } // Resolve path to the final DAG node for the ETag resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath) + switch err { case nil: case coreiface.ErrOffline: webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable) return default: - // if Accept is text/html, see if ipfs-404.html is present - if i.servePretty404IfPresent(w, r, contentPath) { - logger.Debugw("serve pretty 404 if present") - return - } - webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusNotFound) return } - // Detect when explicit Accept header or ?format parameter are present - responseFormat := customResponseFormat(r) - - // Finish early if client already has matching Etag - if r.Header.Get("If-None-Match") == getEtag(r, resolvedPath.Cid()) { - w.WriteHeader(http.StatusNotModified) + if i.finishEarlyForMatchingETag(w, r, resolvedPath) { 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 !i.updateGlobalMetrics(w, r, begin, contentPath, resolvedPath) { 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 !i.setHeaders(w, r, contentPath) { return } // Support custom response formats passed via ?format or Accept HTTP header + // Note that we handle Unixfs (e.g. responseFormat of "") above. switch responseFormat { - case "": // The implicit response format is UnixFS - logger.Debugw("serving unixfs", "path", contentPath) - i.serveUnixFs(w, r, resolvedPath, contentPath, begin, logger) - return case "application/vnd.ipld.raw": logger.Debugw("serving raw block", "path", contentPath) i.serveRawBlock(w, r, resolvedPath.Cid(), contentPath, begin) @@ -417,76 +340,6 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } } -// redirect returns redirected, newPath (if rewrite), error -func (i *gatewayHandler) redirect(w http.ResponseWriter, r *http.Request, path ipath.Resolved) (bool, string, error) { - node, err := i.api.Unixfs().Get(r.Context(), path) - if err != nil { - return false, "", fmt.Errorf("could not get redirects file: %v", err) - } - - defer node.Close() - - f, ok := node.(files.File) - - if !ok { - return false, "", fmt.Errorf("redirect, could not convert node to file") - } - - redirs := newRedirs(f) - - // extract "file" part of URL, typically the part after /ipfs/CID/... - g := strings.Split(r.URL.Path, "/") - - if len(g) > 3 { - filePartPath := "/" + strings.Join(g[3:], "/") - - to, code := redirs.search(filePartPath) - if code > 0 { - if code == 200 { - // rewrite - newPath := strings.Join(g[0:3], "/") + "/" + to - return false, newPath, nil - } - - // redirect - http.Redirect(w, r, to, code) - return true, "", nil - } - } - - return false, "", nil -} - -func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { - resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath) - if err != nil { - return false - } - - dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path) - if err != nil { - return false - } - defer dr.Close() - - f, ok := dr.(files.File) - if !ok { - return false - } - - size, err := f.Size() - if err != nil { - return false - } - - log.Debugw("using pretty 404 file", "path", contentPath) - w.Header().Set("Content-Type", ctype) - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - w.WriteHeader(http.StatusNotFound) - _, err = io.CopyN(w, f, size) - return err == nil -} - func (i *gatewayHandler) postHandler(w http.ResponseWriter, r *http.Request) { p, err := i.api.Unixfs().Add(r.Context(), files.NewReaderFile(r.Body)) if err != nil { @@ -852,67 +705,6 @@ func customResponseFormat(r *http.Request) string { return "" } -func (i *gatewayHandler) searchUpTreeForRedirects(r *http.Request, path string) (ipath.Resolved, error) { - pathComponents := strings.Split(path, "/") - - for idx := len(pathComponents); idx >= 3; idx-- { - rdir := gopath.Join(append(pathComponents[0:idx], "_redirects")...) - rdirPath := ipath.New("/" + rdir) - if rdirPath.IsValid() != nil { - break - } - resolvedPath, err := i.api.ResolvePath(r.Context(), rdirPath) - if err != nil { - continue - } - return resolvedPath, nil - } - - return nil, fmt.Errorf("no redirects in any parent folder") -} - -func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) { - filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) - if err != nil { - return nil, "", err - } - - pathComponents := strings.Split(contentPath.String(), "/") - - for idx := len(pathComponents); idx >= 3; idx-- { - pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...) - parsed404Path := ipath.New("/" + pretty404) - if parsed404Path.IsValid() != nil { - break - } - resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path) - if err != nil { - continue - } - return resolvedPath, ctype, nil - } - - return nil, "", fmt.Errorf("no pretty 404 in any parent folder") -} - -func preferred404Filename(acceptHeaders []string) (string, string, error) { - // If we ever want to offer a 404 file for a different content type - // then this function will need to parse q weightings, but for now - // the presence of anything matching HTML is enough. - for _, acceptHeader := range acceptHeaders { - accepted := strings.Split(acceptHeader, ",") - for _, spec := range accepted { - contentType := strings.SplitN(spec, ";", 1)[0] - switch contentType { - case "*/*", "text/*", "text/html": - return "ipfs-404.html", "text/html", nil - } - } - } - - return "", "", fmt.Errorf("there is no 404 file for the requested content types") -} - // returns unquoted path with all special characters revealed as \u codes func debugStr(path string) string { q := fmt.Sprintf("%+q", path) @@ -951,3 +743,110 @@ func fixupSuperfluousNamespace(w http.ResponseWriter, urlPath string, urlQuery s ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", urlPath, intendedPath.String()), }) == nil } + +func validateSupportedHeaders(w http.ResponseWriter, r *http.Request) bool { + // 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) + return false + } else { + return true + } +} + +func protocolHandlerRegistration(w http.ResponseWriter, r *http.Request, logger *zap.SugaredLogger) bool { + 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 false + } + if u.Scheme != "ipfs" && u.Scheme != "ipns" { + webError(w, "uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest) + return false + } + 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 false + } + + return true +} + +// Disallow Service Worker registration on namespace roots +// https://github.com/ipfs/go-ipfs/issues/4025 +func serviceWorkerRegistration(w http.ResponseWriter, r *http.Request) bool { + 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") + webError(w, "navigator.serviceWorker", err, http.StatusBadRequest) + return false + } + } + + return true +} + +func fixSuperfluousNamespaces(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) bool { + 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 false + } + // unable to fix path, returning error + webError(w, "invalid ipfs path", pathErr, http.StatusBadRequest) + return false + } + return true +} + +func (i *gatewayHandler) finishEarlyForMatchingETag(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved) bool { + + // Finish early if client already has matching Etag + if r.Header.Get("If-None-Match") == getEtag(r, resolvedPath.Cid()) { + w.WriteHeader(http.StatusNotModified) + return true + } + + return false +} + +// Update the global metric of the time it takes to read the final root block of the requested resource +func (i *gatewayHandler) updateGlobalMetrics(w http.ResponseWriter, r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) bool { + // 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) + return false + } + 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 true +} + +func (i *gatewayHandler) setHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { + 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) + return false + } + + return true +} diff --git a/core/corehttp/gateway_handler_unixfs.go b/core/corehttp/gateway_handler_unixfs.go index ed15f41393b7..c3aa11ccc741 100644 --- a/core/corehttp/gateway_handler_unixfs.go +++ b/core/corehttp/gateway_handler_unixfs.go @@ -3,15 +3,125 @@ package corehttp import ( "fmt" "html" + "io" "net/http" + gopath "path" + "strconv" + "strings" "time" files "github.com/ipfs/go-ipfs-files" + "github.com/ipfs/go-path/resolver" + coreiface "github.com/ipfs/interface-go-ipfs-core" ipath "github.com/ipfs/interface-go-ipfs-core/path" "go.uber.org/zap" ) -func (i *gatewayHandler) serveUnixFs(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { +func (i *gatewayHandler) getOrHeadHandlerUnixfs(w http.ResponseWriter, r *http.Request, begin time.Time, logger *zap.SugaredLogger) { + urlPath := r.URL.Path + // Only look for _redirects file if we have Origin isolation + if hasOriginIsolation(r) { + // Check for _redirects file and redirect as needed + redirectsFile, err := i.getRedirectsFile(r) + if err != nil { + switch err.(type) { + case resolver.ErrNoLink: + // _redirects files doesn't exist, so don't error + default: + // TODO(JJ): During tests we get multibase.ErrUnsupportedEncoding + // This comes from multibase and I assume is due to a fake or otherwise bad CID being in the test. + // So for now any errors getting the redirect file are silently ignored. + // internalWebError(w, err) + // return + } + } else { + // _redirects file exists, so parse it and redirect + redirected, newPath, err := i.redirect(w, r, redirectsFile) + if err != nil { + // TODO(JJ): How should we handle parse or redirect errors? + internalWebError(w, err) + return + } + + if redirected { + return + } + + // 200 is treated as a rewrite, so update the path and continue + if newPath != "" { + urlPath = newPath + } + } + } + + contentPath := ipath.New(urlPath) + + resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath) + + switch err { + case nil: + case coreiface.ErrOffline: + webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable) + return + default: + // if Accept is text/html, see if ipfs-404.html is present + if i.servePretty404IfPresent(w, r, contentPath) { + logger.Debugw("serve pretty 404 if present") + return + } + + webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusNotFound) + return + } + + if i.finishEarlyForMatchingETag(w, r, resolvedPath) { + return + } + + if !i.updateGlobalMetrics(w, r, begin, contentPath, resolvedPath) { + return + } + + if !i.setHeaders(w, r, contentPath) { + return + } + + logger.Debugw("serving unixfs", "path", contentPath) + i.serveUnixfs(w, r, resolvedPath, contentPath, begin, logger) + return +} + +func (i *gatewayHandler) servePretty404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { + resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath) + if err != nil { + return false + } + + dr, err := i.api.Unixfs().Get(r.Context(), resolved404Path) + if err != nil { + return false + } + defer dr.Close() + + f, ok := dr.(files.File) + if !ok { + return false + } + + size, err := f.Size() + if err != nil { + return false + } + + log.Debugw("using pretty 404 file", "path", contentPath) + w.Header().Set("Content-Type", ctype) + w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + w.WriteHeader(http.StatusNotFound) + _, err = io.CopyN(w, f, size) + return err == nil +} + +func (i *gatewayHandler) serveUnixfs(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) { // Handling UnixFS dr, err := i.api.Unixfs().Get(r.Context(), resolvedPath) if err != nil { @@ -30,9 +140,125 @@ func (i *gatewayHandler) serveUnixFs(w http.ResponseWriter, r *http.Request, res // Handling Unixfs directory dir, ok := dr.(files.Directory) if !ok { - internalWebError(w, fmt.Errorf("unsupported UnixFs type")) + internalWebError(w, fmt.Errorf("unsupported Unixfs type")) return } logger.Debugw("serving unixfs directory", "path", contentPath) i.serveDirectory(w, r, resolvedPath, contentPath, dir, begin, logger) } + +// redirect returns redirected, newPath (if rewrite), error +func (i *gatewayHandler) redirect(w http.ResponseWriter, r *http.Request, path ipath.Resolved) (bool, string, error) { + node, err := i.api.Unixfs().Get(r.Context(), path) + if err != nil { + return false, "", fmt.Errorf("could not get redirects file: %v", err) + } + + defer node.Close() + + f, ok := node.(files.File) + + if !ok { + return false, "", fmt.Errorf("redirect, could not convert node to file") + } + + redirs := newRedirs(f) + + // extract "file" part of URL, typically the part after /ipfs/CID/... + g := strings.Split(r.URL.Path, "/") + + if len(g) > 3 { + filePartPath := "/" + strings.Join(g[3:], "/") + + to, code := redirs.search(filePartPath) + if code > 0 { + if code == 200 { + // rewrite + newPath := strings.Join(g[0:3], "/") + "/" + to + return false, newPath, nil + } + + // redirect + http.Redirect(w, r, to, code) + return true, "", nil + } + } + + return false, "", nil +} + +// Returns a resolved path to the _redirects file located in the root CID path of the requested path +func (i *gatewayHandler) getRedirectsFile(r *http.Request) (ipath.Resolved, error) { + // r.URL.Path is the full ipfs path to the requested resource, + // regardless of whether path or subdomain resolution is used. + rootPath := getRootPath(r.URL.Path) + // TODO(JJ): handle error + path := ipath.New(gopath.Join(rootPath, "_redirects")) + resolvedPath, err := i.api.ResolvePath(r.Context(), path) + if err != nil { + return nil, err + } + return resolvedPath, nil +} + +// Returns the root CID path for the given path +func getRootPath(path string) string { + if strings.HasPrefix(path, ipfsPathPrefix) && strings.Count(gopath.Clean(path), "/") >= 2 { + parts := strings.Split(path, "/") + return gopath.Join(ipfsPathPrefix, parts[2]) + } else { + return "" + } +} + +// TODO(JJ): I was thinking about changing this to just look at the root path as well, but the docs say it searches up +func (i *gatewayHandler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) { + filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) + if err != nil { + return nil, "", err + } + + pathComponents := strings.Split(contentPath.String(), "/") + + for idx := len(pathComponents); idx >= 3; idx-- { + pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...) + parsed404Path := ipath.New("/" + pretty404) + if parsed404Path.IsValid() != nil { + break + } + resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path) + if err != nil { + continue + } + return resolvedPath, ctype, nil + } + + return nil, "", fmt.Errorf("no pretty 404 in any parent folder") +} + +func preferred404Filename(acceptHeaders []string) (string, string, error) { + // If we ever want to offer a 404 file for a different content type + // then this function will need to parse q weightings, but for now + // the presence of anything matching HTML is enough. + for _, acceptHeader := range acceptHeaders { + accepted := strings.Split(acceptHeader, ",") + for _, spec := range accepted { + contentType := strings.SplitN(spec, ";", 1)[0] + switch contentType { + case "*/*", "text/*", "text/html": + return "ipfs-404.html", "text/html", nil + } + } + } + + return "", "", fmt.Errorf("there is no 404 file for the requested content types") +} + +// TODO(JJ): Pretty sure this is incorrect. Validate the correct approach. +func hasOriginIsolation(r *http.Request) bool { + if _, ok := r.Context().Value("gw-hostname").(string); ok { + return true + } else { + return false + } +} diff --git a/test/sharness/t0114-gateway-subdomains.sh b/test/sharness/t0114-gateway-subdomains.sh index 41abd4a87749..75f55cfe0335 100755 --- a/test/sharness/t0114-gateway-subdomains.sh +++ b/test/sharness/t0114-gateway-subdomains.sh @@ -144,6 +144,42 @@ test_expect_success 'start daemon with empty config for Gateway.PublicGateways' test_launch_ipfs_daemon_without_network ' +## ============================================================================ +## Test _redirects file support +## ============================================================================ + +# Directory tree crafted to test _redirects file support +test_expect_success "Add the _redirects file test directory" ' + mkdir -p testredirect/ && + echo "index.html" > testredirect/index.html && + echo "one.html" > testredirect/one.html && + echo "two.html" > testredirect/two.html && + echo "^/301-redirect-one$ /one.html 301" > testredirect/_redirects && + echo "^/302-redirect-two$ /two.html 302" >> testredirect/_redirects && + echo "^/200-index$ /index.html 200" >> testredirect/_redirects && + REDIRECTS_DIR_CID=$(ipfs add -Qr --cid-version 1 testredirect) +' + +REDIRECTS_DIR_HOSTNAME="${REDIRECTS_DIR_CID}.ipfs.localhost:$GWAY_PORT" + +test_expect_success "_redirects - /301-redirect-one" ' + curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/301-redirect-one" > response && + test_should_contain "one.html" response && + test_should_contain "301 Moved Permanently" response +' + +test_expect_success "_redirects - /302-redirect-two" ' + curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/302-redirect-two" > response && + test_should_contain "two.html" response && + test_should_contain "302 Found" response +' + +test_expect_success "_redirects - /200-index" ' + curl -sD - --resolve $REDIRECTS_DIR_HOSTNAME:127.0.0.1 "http://$REDIRECTS_DIR_HOSTNAME/200-index" > response && + test_should_contain "index.html" response && + test_should_contain "200 OK" response +' + ## ============================================================================ ## Test path-based requests to a local gateway with default config ## (forced redirects to http://*.localhost)