diff --git a/core/corehttp/gateway_handler.go b/core/corehttp/gateway_handler.go index eca2efff6101..adfc09e6952f 100644 --- a/core/corehttp/gateway_handler.go +++ b/core/corehttp/gateway_handler.go @@ -267,6 +267,7 @@ 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") @@ -316,9 +317,25 @@ func (i *gatewayHandler) getOrHeadHandler(w http.ResponseWriter, r *http.Request } } - contentPath := ipath.New(r.URL.Path) + 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(urlPath) if pathErr := contentPath.IsValid(); pathErr != nil { - if fixupSuperfluousNamespace(w, r.URL.Path, r.URL.RawQuery) { + 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") @@ -400,6 +417,46 @@ 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 { @@ -795,6 +852,25 @@ 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 { diff --git a/core/corehttp/redirect.go b/core/corehttp/redirect.go index e7b961e604ef..e75384647770 100644 --- a/core/corehttp/redirect.go +++ b/core/corehttp/redirect.go @@ -1,8 +1,14 @@ package corehttp import ( + "bufio" + "fmt" + "io" "net" "net/http" + "regexp" + "strconv" + "strings" core "github.com/ipfs/go-ipfs/core" ) @@ -26,3 +32,66 @@ type redirectHandler struct { func (i *redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, i.path, 302) } + +type redirLine struct { + matcher string + to string + code int +} + +func (rdl redirLine) match(s string) (bool, error) { + re, err := regexp.Compile(rdl.matcher) + if err != nil { + return false, fmt.Errorf("Failed to compile %v: %v", rdl.matcher, err) + } + + match := re.FindString(s) + if match == "" { + return false, nil + } + + return true, nil +} + +type redirs []redirLine + +func newRedirs(f io.Reader) *redirs { + ret := redirs{} + scanner := bufio.NewScanner(f) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + t := scanner.Text() + if len(t) > 0 && t[0] == '#' { + // comment, skip line + continue + } + groups := strings.Fields(scanner.Text()) + if len(groups) >= 2 { + matcher := groups[0] + to := groups[1] + // default to 302 (temporary redirect) + code := 302 + if len(groups) >= 3 { + c, err := strconv.Atoi(groups[2]) + if err == nil { + code = c + } + } + ret = append(ret, redirLine{matcher, to, code}) + } + } + + return &ret +} + +// returns "" if no redir +func (r redirs) search(path string) (string, int) { + for _, rdir := range r { + m, err := rdir.match(path) + if m && err == nil { + return rdir.to, rdir.code + } + } + + return "", 0 +} diff --git a/core/corehttp/redirect_test.go b/core/corehttp/redirect_test.go new file mode 100644 index 000000000000..0c9a6255199c --- /dev/null +++ b/core/corehttp/redirect_test.go @@ -0,0 +1,37 @@ +package corehttp + +import ( + "fmt" + "testing" +) + +func TestRedirline(t *testing.T) { + for _, tc := range []struct { + matcher string + s string + exp bool + errExp bool + }{ + {"hi", "hi", true, false}, + {"hi", "hithere", true, false}, + {"^hi$", "hithere", false, false}, + {"^hi$", "hi", true, false}, + {"hi.*", "hithere", true, false}, + {"/hi", "/hi/there", true, false}, + {"^/hi/", "/hi/there/now", true, false}, + {"^/hi/", "/hithere", false, false}, + {"^/hi/(.*", "/hi/there/now", false, true}, + } { + r := redirLine{tc.matcher, "to", 200} + ok, err := r.match(tc.s) + if ok != tc.exp { + t.Errorf("%v %v, expected %v, got %v", tc.matcher, tc.s, tc.exp, + ok) + } + + if err != nil != tc.errExp { + fmt.Printf("regexp error %v\n", err) + t.Errorf("%v %v, expected error %v, got %v", tc.matcher, tc.s, tc.errExp, err == nil) + } + } +}