repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

commit
ccb7239
parent
2f441a5
author
Eric Bower
date
2024-04-05 18:54:28 +0000 UTC
feat(pgs): rewrite support

feat(pgs): rewrite proxy api support
3 files changed,  +194, -17
M pgs/api.go
+15, -0
 1@@ -212,6 +212,21 @@ func (h *AssetHandler) handle(w http.ResponseWriter, r *http.Request) {
 2 			)
 3 			http.Redirect(w, r, fp.Filepath, fp.Status)
 4 			return
 5+		} else if hasProtocol(fp.Filepath) {
 6+			// fetch content from url and serve it
 7+			resp, err := http.Get(fp.Filepath)
 8+			if err != nil {
 9+				http.Error(w, "404 not found", http.StatusNotFound)
10+				return
11+			}
12+
13+			w.Header().Set("content-type", resp.Header.Get("content-type"))
14+			w.WriteHeader(status)
15+			_, err = io.Copy(w, resp.Body)
16+			if err != nil {
17+				h.Logger.Error("io copy", "err", err.Error())
18+			}
19+			return
20 		}
21 
22 		attempts = append(attempts, fp.Filepath)
R pgs/cal_route.go => pgs/calc_route.go
+103, -11
  1@@ -3,6 +3,7 @@ package pgs
  2 import (
  3 	"fmt"
  4 	"net/http"
  5+	"net/url"
  6 	"path/filepath"
  7 	"regexp"
  8 	"strings"
  9@@ -73,6 +74,90 @@ func checkIsRedirect(status int) bool {
 10 	return status >= 300 && status <= 399
 11 }
 12 
 13+func correlatePlaceholder(orig, pattern string) string {
 14+	origList := splitFp(orig)
 15+	patternList := splitFp(pattern)
 16+	nextList := []string{}
 17+	for idx, item := range patternList {
 18+		if len(origList) <= idx {
 19+			continue
 20+		}
 21+
 22+		if strings.HasPrefix(item, ":") {
 23+			nextList = append(nextList, origList[idx])
 24+		} else if item == origList[idx] {
 25+			nextList = append(nextList, origList[idx])
 26+		}
 27+	}
 28+	return filepath.Join(nextList...)
 29+}
 30+
 31+func splitFp(str string) []string {
 32+	ls := strings.Split(str, "/")
 33+	fin := []string{}
 34+	for _, l := range ls {
 35+		if l == "" {
 36+			continue
 37+		}
 38+		fin = append(fin, l)
 39+	}
 40+	return fin
 41+}
 42+
 43+func genRedirectRoute(actual string, fromStr string, to string) string {
 44+	if to == "/" {
 45+		return to
 46+	}
 47+	actualList := splitFp(actual)
 48+	fromList := splitFp(fromStr)
 49+	prefix := ""
 50+	var toList []string
 51+	if hasProtocol(to) {
 52+		u, _ := url.Parse(to)
 53+		if u.Path == "" {
 54+			return to
 55+		}
 56+		toList = splitFp(u.Path)
 57+		prefix = u.Scheme + "://" + u.Host
 58+	} else {
 59+		toList = splitFp(to)
 60+	}
 61+
 62+	mapper := map[string]string{}
 63+	for idx, item := range fromList {
 64+		if len(actualList) <= idx {
 65+			continue
 66+		}
 67+
 68+		if strings.HasPrefix(item, ":") {
 69+			mapper[item] = actualList[idx]
 70+		}
 71+		if item == "*" {
 72+			splat := strings.Join(actualList[idx:], "/")
 73+			mapper[":splat"] = splat
 74+			break
 75+		}
 76+	}
 77+
 78+	fin := []string{"/"}
 79+
 80+	for _, item := range toList {
 81+		if item == ":splat" {
 82+			fin = append(fin, mapper[item])
 83+		} else if mapper[item] != "" {
 84+			fin = append(fin, mapper[item])
 85+		} else {
 86+			fin = append(fin, item)
 87+		}
 88+	}
 89+
 90+	result := prefix + filepath.Join(fin...)
 91+	if !strings.HasSuffix(result, "/") && (strings.HasSuffix(to, "/") || strings.HasSuffix(actual, "/")) {
 92+		result += "/"
 93+	}
 94+	return result
 95+}
 96+
 97 func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpReply {
 98 	rts := []*HttpReply{}
 99 	// add route as-is without expansion
100@@ -87,32 +172,33 @@ func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpRe
101 
102 	// user routes
103 	for _, redirect := range userRedirects {
104-		// this doesn't make sense and it forbidden
105+		// this doesn't make sense so it is forbidden
106 		if redirect.From == redirect.To {
107 			continue
108 		}
109 
110-		from := redirect.From
111-		if !strings.HasSuffix(redirect.From, "*") {
112-			from = strings.TrimSuffix(redirect.From, "/") + "/?"
113-		}
114-		rr := regexp.MustCompile(from)
115+		// hack: make suffix `/` optional when matching
116+		from := filepath.Clean(redirect.From)
117+		fromMatcher := correlatePlaceholder(fp, from)
118+		rr := regexp.MustCompile(fromMatcher)
119 		match := rr.FindStringSubmatch(fp)
120 		if len(match) > 0 {
121 			isRedirect := checkIsRedirect(redirect.Status)
122-			if !isRedirect {
123+			if !isRedirect && !hasProtocol(redirect.To) {
124+				route := genRedirectRoute(fp, from, redirect.To)
125 				// wipe redirect rules to prevent infinite loops
126 				// as such we only support a single hop for user defined redirects
127-				redirectRoutes := calcRoutes(projectName, redirect.To, []*RedirectRule{})
128+				redirectRoutes := calcRoutes(projectName, route, []*RedirectRule{})
129 				rts = append(rts, redirectRoutes...)
130 				return rts
131 			}
132 
133+			route := genRedirectRoute(fp, from, redirect.To)
134 			userReply := []*HttpReply{}
135 			var rule *HttpReply
136 			if redirect.To != "" {
137 				rule = &HttpReply{
138-					Filepath: redirect.To,
139+					Filepath: route,
140 					Status:   redirect.Status,
141 					Query:    redirect.Query,
142 				}
143@@ -124,8 +210,14 @@ func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpRe
144 			} else {
145 				rts = append(rts, userReply...)
146 			}
147-			// quit after first match
148-			break
149+
150+			if hasProtocol(redirect.To) {
151+				// redirecting to another site so we should bail early
152+				return rts
153+			} else {
154+				// quit after first match
155+				break
156+			}
157 		}
158 	}
159 
M pgs/calc_route_test.go
+76, -6
  1@@ -140,8 +140,6 @@ func TestCalcRoutes(t *testing.T) {
  2 				{Filepath: "test/tester1", Status: 200},
  3 				{Filepath: "test/tester1.html", Status: 200},
  4 				{Filepath: "https://pico.sh", Status: 301},
  5-				{Filepath: "/tester1/", Status: 301},
  6-				{Filepath: "test/404.html", Status: 404},
  7 			},
  8 		},
  9 		{
 10@@ -211,8 +209,6 @@ func TestCalcRoutes(t *testing.T) {
 11 			Expected: []*HttpReply{
 12 				{Filepath: "test/wow.html", Status: 200},
 13 				{Filepath: "https://pico.sh", Status: 301},
 14-				{Filepath: "/wow.html/", Status: 301},
 15-				{Filepath: "test/404.html", Status: 404},
 16 			},
 17 		},
 18 		{
 19@@ -232,8 +228,6 @@ func TestCalcRoutes(t *testing.T) {
 20 				{Filepath: "test/wow", Status: 200},
 21 				{Filepath: "test/wow.html", Status: 200},
 22 				{Filepath: "https://pico.sh", Status: 301},
 23-				{Filepath: "/wow/", Status: 301},
 24-				{Filepath: "test/404.html", Status: 404},
 25 			},
 26 		},
 27 		{
 28@@ -353,6 +347,82 @@ func TestCalcRoutes(t *testing.T) {
 29 				{Filepath: "public/404.html", Status: 404},
 30 			},
 31 		},
 32+		{
 33+			Name: "redirect-to-site",
 34+			Actual: calcRoutes(
 35+				"public",
 36+				"/",
 37+				[]*RedirectRule{
 38+					{
 39+						From:   "/*",
 40+						To:     "https://my-other-site.pgs.sh/:splat",
 41+						Status: 200,
 42+					},
 43+				},
 44+			),
 45+			Expected: []*HttpReply{
 46+				{Filepath: "public/index.html", Status: 200},
 47+				{Filepath: "https://my-other-site.pgs.sh/", Status: 200},
 48+			},
 49+		},
 50+		{
 51+			Name: "redirect-to-site-subdir",
 52+			Actual: calcRoutes(
 53+				"public",
 54+				"/plugin/nice/",
 55+				[]*RedirectRule{
 56+					{
 57+						From:   "/*",
 58+						To:     "https://my-other-site.pgs.sh/:splat",
 59+						Status: 200,
 60+					},
 61+				},
 62+			),
 63+			Expected: []*HttpReply{
 64+				{Filepath: "public/plugin/nice/index.html", Status: 200},
 65+				{Filepath: "https://my-other-site.pgs.sh/plugin/nice/", Status: 200},
 66+			},
 67+		},
 68+		{
 69+			Name: "redirect-to-another-pgs-site",
 70+			Actual: calcRoutes(
 71+				"public",
 72+				"/my-site/index.html",
 73+				[]*RedirectRule{
 74+					{
 75+						From:   "/my-site/*",
 76+						To:     "https://my-other-site.pgs.sh/:splat",
 77+						Status: 200,
 78+					},
 79+				},
 80+			),
 81+			Expected: []*HttpReply{
 82+				{Filepath: "public/my-site/index.html", Status: 200},
 83+				{Filepath: "https://my-other-site.pgs.sh/index.html", Status: 200},
 84+			},
 85+		},
 86+		{
 87+			Name: "redirect-placeholders",
 88+			Actual: calcRoutes(
 89+				"public",
 90+				"/news/02/12/2004/my-story",
 91+				[]*RedirectRule{
 92+					{
 93+						From:   "/news/:month/:date/:year/*",
 94+						To:     "/blog/:year/:month/:date/:splat",
 95+						Status: 200,
 96+					},
 97+				},
 98+			),
 99+			Expected: []*HttpReply{
100+				{Filepath: "public/news/02/12/2004/my-story", Status: 200},
101+				{Filepath: "public/news/02/12/2004/my-story.html", Status: 200},
102+				{Filepath: "public/blog/2004/02/12/my-story", Status: 200},
103+				{Filepath: "public/blog/2004/02/12/my-story.html", Status: 200},
104+				{Filepath: "/blog/2004/02/12/my-story/", Status: 301},
105+				{Filepath: "public/404.html", Status: 404},
106+			},
107+		},
108 	}
109 
110 	for _, fixture := range fixtures {