From 07f2211df0b1e71a0245c1eb2d363cb6f4a3e795 Mon Sep 17 00:00:00 2001 From: Eric Bower Date: Fri, 5 Apr 2024 14:54:28 -0400 Subject: [PATCH] feat(pgs): rewrite support feat(pgs): rewrite proxy api support --- pgs/api.go | 15 +++++ pgs/{cal_route.go => calc_route.go} | 90 +++++++++++++++++++++++++---- pgs/calc_route_test.go | 46 +++++++++++++-- 3 files changed, 134 insertions(+), 17 deletions(-) rename pgs/{cal_route.go => calc_route.go} (61%) diff --git a/pgs/api.go b/pgs/api.go index fca1861d..f73c5eb5 100644 --- a/pgs/api.go +++ b/pgs/api.go @@ -212,6 +212,21 @@ func (h *AssetHandler) handle(w http.ResponseWriter, r *http.Request) { ) http.Redirect(w, r, fp.Filepath, fp.Status) return + } else if hasProtocol(fp.Filepath) { + // fetch content from url and serve it + resp, err := http.Get(fp.Filepath) + if err != nil { + http.Error(w, "404 not found", http.StatusNotFound) + return + } + + w.Header().Set("content-type", resp.Header.Get("content-type")) + w.WriteHeader(status) + _, err = io.Copy(w, resp.Body) + if err != nil { + h.Logger.Error("io copy", "err", err.Error()) + } + return } attempts = append(attempts, fp.Filepath) diff --git a/pgs/cal_route.go b/pgs/calc_route.go similarity index 61% rename from pgs/cal_route.go rename to pgs/calc_route.go index 19272b07..e33cc44e 100644 --- a/pgs/cal_route.go +++ b/pgs/calc_route.go @@ -3,6 +3,7 @@ package pgs import ( "fmt" "net/http" + "net/url" "path/filepath" "regexp" "strings" @@ -73,6 +74,66 @@ func checkIsRedirect(status int) bool { return status >= 300 && status <= 399 } +func correlatePlaceholder(orig, pattern string) string { + origList := strings.Split(orig, "/") + patternList := strings.Split(pattern, "/") + nextList := []string{} + for idx, item := range patternList { + if strings.HasPrefix(item, ":") { + nextList = append(nextList, origList[idx]) + } else if item == origList[idx] { + nextList = append(nextList, origList[idx]) + } + } + return filepath.Join(nextList...) +} + +func genRedirectRoute(actual string, fromStr string, to string) string { + if to == "/" { + return to + } + actualList := strings.Split(actual, "/") + fromList := strings.Split(fromStr, "/") + prefix := "" + var toList []string + if hasProtocol(to) { + u, _ := url.Parse(to) + if u.Path == "" { + return to + } + toList = strings.Split(u.Path, "/") + prefix = u.Scheme + "://" + u.Host + } else { + toList = strings.Split(to, "/") + } + + mapper := map[string]string{} + for idx, item := range fromList { + if strings.HasPrefix(item, ":") { + mapper[item] = actualList[idx] + } + if item == "*" { + mapper[":splat"] = actualList[idx] + } + } + + fin := []string{"/"} + + for _, item := range toList { + if mapper[item] != "" { + fin = append(fin, mapper[item]) + } else { + fin = append(fin, item) + } + } + + result := prefix + filepath.Join(fin...) + if strings.HasSuffix(to, "/") { + result += "/" + } + return result +} + func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpReply { rts := []*HttpReply{} // add route as-is without expansion @@ -87,32 +148,33 @@ func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpRe // user routes for _, redirect := range userRedirects { - // this doesn't make sense and it forbidden + // this doesn't make sense so it is forbidden if redirect.From == redirect.To { continue } - from := redirect.From - if !strings.HasSuffix(redirect.From, "*") { - from = strings.TrimSuffix(redirect.From, "/") + "/?" - } - rr := regexp.MustCompile(from) + // hack: make suffix `/` optional when matching + from := filepath.Clean(redirect.From) + fromMatcher := correlatePlaceholder(fp, from) + rr := regexp.MustCompile(fromMatcher) match := rr.FindStringSubmatch(fp) if len(match) > 0 { isRedirect := checkIsRedirect(redirect.Status) - if !isRedirect { + if !isRedirect && !hasProtocol(redirect.To) { + route := genRedirectRoute(fp, from, redirect.To) // wipe redirect rules to prevent infinite loops // as such we only support a single hop for user defined redirects - redirectRoutes := calcRoutes(projectName, redirect.To, []*RedirectRule{}) + redirectRoutes := calcRoutes(projectName, route, []*RedirectRule{}) rts = append(rts, redirectRoutes...) return rts } + route := genRedirectRoute(fp, from, redirect.To) userReply := []*HttpReply{} var rule *HttpReply if redirect.To != "" { rule = &HttpReply{ - Filepath: redirect.To, + Filepath: route, Status: redirect.Status, Query: redirect.Query, } @@ -124,8 +186,14 @@ func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpRe } else { rts = append(rts, userReply...) } - // quit after first match - break + + if hasProtocol(redirect.To) { + // redirecting to another site so we should bail early + return rts + } else { + // quit after first match + break + } } } diff --git a/pgs/calc_route_test.go b/pgs/calc_route_test.go index a240715a..c78b6f28 100644 --- a/pgs/calc_route_test.go +++ b/pgs/calc_route_test.go @@ -140,8 +140,6 @@ func TestCalcRoutes(t *testing.T) { {Filepath: "test/tester1", Status: 200}, {Filepath: "test/tester1.html", Status: 200}, {Filepath: "https://pico.sh", Status: 301}, - {Filepath: "/tester1/", Status: 301}, - {Filepath: "test/404.html", Status: 404}, }, }, { @@ -211,8 +209,6 @@ func TestCalcRoutes(t *testing.T) { Expected: []*HttpReply{ {Filepath: "test/wow.html", Status: 200}, {Filepath: "https://pico.sh", Status: 301}, - {Filepath: "/wow.html/", Status: 301}, - {Filepath: "test/404.html", Status: 404}, }, }, { @@ -232,8 +228,6 @@ func TestCalcRoutes(t *testing.T) { {Filepath: "test/wow", Status: 200}, {Filepath: "test/wow.html", Status: 200}, {Filepath: "https://pico.sh", Status: 301}, - {Filepath: "/wow/", Status: 301}, - {Filepath: "test/404.html", Status: 404}, }, }, { @@ -353,6 +347,46 @@ func TestCalcRoutes(t *testing.T) { {Filepath: "public/404.html", Status: 404}, }, }, + { + Name: "redirect-to-another-pgs-site", + Actual: calcRoutes( + "public", + "/my-site/index.html", + []*RedirectRule{ + { + From: "/my-site/*", + To: "https://my-other-site.pgs.sh/:splat", + Status: 200, + }, + }, + ), + Expected: []*HttpReply{ + {Filepath: "public/my-site/index.html", Status: 200}, + {Filepath: "https://my-other-site.pgs.sh/index.html", Status: 200}, + }, + }, + { + Name: "redirect-placeholders", + Actual: calcRoutes( + "public", + "/news/02/12/2004/my-story", + []*RedirectRule{ + { + From: "/news/:month/:date/:year/*", + To: "/blog/:year/:month/:date/:splat", + Status: 200, + }, + }, + ), + Expected: []*HttpReply{ + {Filepath: "public/news/02/12/2004/my-story", Status: 200}, + {Filepath: "public/news/02/12/2004/my-story.html", Status: 200}, + {Filepath: "public/blog/2004/02/12/my-story", Status: 200}, + {Filepath: "public/blog/2004/02/12/my-story.html", Status: 200}, + {Filepath: "/blog/2004/02/12/my-story/", Status: 301}, + {Filepath: "public/404.html", Status: 404}, + }, + }, } for _, fixture := range fixtures {