diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index e47198174..0275c873d 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -3,6 +3,7 @@ package corehttp import ( "context" "fmt" + "html/template" "io" "mime" "net/http" @@ -36,6 +37,26 @@ const ( var onlyAscii = regexp.MustCompile("[[:^ascii:]]") +// HTML-based redirect for errors which can be recovered from, but we want +// to provide hint to people that they should fix things on their end. +var redirectTemplate = template.Must(template.New("redirect").Parse(` + + + + + + + +
{{.ErrorMsg}}
(if a redirect does not happen in 10 seconds, use "{{.SuggestedPath}}" instead)
+ +`)) + +type redirectTemplateData struct { + RedirectURL string + SuggestedPath string + ErrorMsg string +} + // gatewayHandler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) // (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) type gatewayHandler struct { @@ -216,8 +237,14 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } parsedPath := ipath.New(urlPath) - if err := parsedPath.IsValid(); err != nil { - webError(w, "invalid ipfs path", err, http.StatusBadRequest) + if pathErr := parsedPath.IsValid(); pathErr != nil { + if prefix == "" && 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 + return + } + // unable to fix path, returning error + webError(w, "invalid ipfs path", pathErr, http.StatusBadRequest) return } @@ -781,3 +808,33 @@ func preferred404Filename(acceptHeaders []string) (string, string, error) { return "", "", fmt.Errorf("there is no 404 file for the requested content types") } + +// 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 + } + intendedPath := ipath.New(strings.TrimPrefix(urlPath, "/ipfs")) + if err := intendedPath.IsValid(); err != nil { + return false // not a valid path + } + intendedURL := intendedPath.String() + if urlQuery != "" { + // we render HTML, so ensure query entries are properly escaped + q, _ := url.ParseQuery(urlQuery) + intendedURL = intendedURL + "?" + q.Encode() + } + // return HTTP 400 (Bad Request) with HTML error page that: + // - points at correct canonical path via header + // - displays human-readable error + // - redirects to intendedURL after a short delay + w.WriteHeader(http.StatusBadRequest) + return redirectTemplate.Execute(w, redirectTemplateData{ + RedirectURL: intendedURL, + SuggestedPath: intendedPath.String(), + ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", urlPath, intendedPath.String()), + }) == nil +} diff --git a/test/sharness/t0110-gateway.sh b/test/sharness/t0110-gateway.sh index cb0b0cd67..84bc96341 100755 --- a/test/sharness/t0110-gateway.sh +++ b/test/sharness/t0110-gateway.sh @@ -84,6 +84,12 @@ test_expect_success "GET IPFS nonexistent file returns code expected (404)" ' test_curl_resp_http_code "http://127.0.0.1:$port/ipfs/$HASH2/pleaseDontAddMe" "HTTP/1.1 404 Not Found" ' +test_expect_success "GET /ipfs/ipfs/{cid} returns redirect to the valid path" ' + curl -sD - "http://127.0.0.1:$port/ipfs/ipfs/bafkqaaa?query=to-remember" > response_with_double_ipfs_ns && + test_should_contain "" response_with_double_ipfs_ns && + test_should_contain "" response_with_double_ipfs_ns +' + test_expect_failure "GET IPNS path succeeds" ' ipfs name publish --allow-offline "$HASH" && PEERID=$(ipfs config Identity.PeerID) && @@ -95,6 +101,13 @@ test_expect_failure "GET IPNS path output looks good" ' test_cmp expected actual ' +test_expect_success "GET /ipfs/ipns/{peerid} returns redirect to the valid path" ' + PEERID=$(ipfs config Identity.PeerID) && + curl -sD - "http://127.0.0.1:$port/ipfs/ipns/${PEERID}?query=to-remember" > response_with_ipfs_ipns_ns && + test_should_contain "" response_with_ipfs_ipns_ns && + test_should_contain "" response_with_ipfs_ipns_ns +' + test_expect_success "GET invalid IPFS path errors" ' test_must_fail curl -sf "http://127.0.0.1:$port/ipfs/12345" '