repos / pico

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

pico / pgs
Eric Bower · 13 Oct 24

calc_route.go

  1package pgs
  2
  3import (
  4	"fmt"
  5	"net/http"
  6	"net/url"
  7	"path/filepath"
  8	"regexp"
  9	"strings"
 10
 11	"github.com/picosh/pico/shared"
 12	"github.com/picosh/pico/shared/storage"
 13	"github.com/picosh/send/utils"
 14)
 15
 16type HttpReply struct {
 17	Filepath string
 18	Query    map[string]string
 19	Status   int
 20}
 21
 22func expandRoute(projectName, fp string, status int) []*HttpReply {
 23	if fp == "" {
 24		fp = "/"
 25	}
 26	mimeType := storage.GetMimeType(fp)
 27	fname := filepath.Base(fp)
 28	fdir := filepath.Dir(fp)
 29	fext := filepath.Ext(fp)
 30	routes := []*HttpReply{}
 31
 32	if mimeType != "text/plain" {
 33		return routes
 34	}
 35
 36	if fext == ".txt" {
 37		return routes
 38	}
 39
 40	// we know it's a directory so send the index.html for it
 41	if strings.HasSuffix(fp, "/") {
 42		dirRoute := shared.GetAssetFileName(&utils.FileEntry{
 43			Filepath: filepath.Join(projectName, fp, "index.html"),
 44		})
 45
 46		routes = append(
 47			routes,
 48			&HttpReply{Filepath: dirRoute, Status: status},
 49		)
 50	} else {
 51		if fname == "." {
 52			return routes
 53		}
 54
 55		// pretty urls where we just append .html to base of fp
 56		nameRoute := shared.GetAssetFileName(&utils.FileEntry{
 57			Filepath: filepath.Join(
 58				projectName,
 59				fdir,
 60				fmt.Sprintf("%s.html", fname),
 61			),
 62		})
 63
 64		routes = append(
 65			routes,
 66			&HttpReply{Filepath: nameRoute, Status: status},
 67		)
 68	}
 69
 70	return routes
 71}
 72
 73func checkIsRedirect(status int) bool {
 74	return status >= 300 && status <= 399
 75}
 76
 77func correlatePlaceholder(orig, pattern string) (string, string) {
 78	origList := splitFp(orig)
 79	patternList := splitFp(pattern)
 80	nextList := []string{}
 81	for idx, item := range patternList {
 82		if len(origList) <= idx {
 83			continue
 84		}
 85
 86		if strings.HasPrefix(item, ":") {
 87			nextList = append(nextList, origList[idx])
 88		} else if strings.Contains(item, "*") {
 89			nextList = append(nextList, strings.ReplaceAll(item, "*", "(.+)"))
 90		} else if item == origList[idx] {
 91			nextList = append(nextList, origList[idx])
 92		}
 93	}
 94
 95	_type := "none"
 96	if len(nextList) > 0 && len(nextList) == len(patternList) {
 97		_type = "match"
 98	} else if strings.Contains(pattern, "*") {
 99		_type = "wildcard"
100	} else if strings.Contains(pattern, ":") {
101		_type = "variable"
102	}
103
104	return filepath.Join(nextList...), _type
105}
106
107func splitFp(str string) []string {
108	ls := strings.Split(str, "/")
109	fin := []string{}
110	for _, l := range ls {
111		if l == "" {
112			continue
113		}
114		fin = append(fin, l)
115	}
116	return fin
117}
118
119func genRedirectRoute(actual string, fromStr string, to string) string {
120	if to == "/" {
121		return to
122	}
123	actualList := splitFp(actual)
124	fromList := splitFp(fromStr)
125	prefix := ""
126	var toList []string
127	if hasProtocol(to) {
128		u, _ := url.Parse(to)
129		if u.Path == "" {
130			return to
131		}
132		toList = splitFp(u.Path)
133		prefix = u.Scheme + "://" + u.Host
134	} else {
135		toList = splitFp(to)
136	}
137
138	mapper := map[string]string{}
139	for idx, item := range fromList {
140		if len(actualList) < idx {
141			continue
142		}
143
144		if strings.HasPrefix(item, ":") {
145			mapper[item] = actualList[idx]
146		}
147		if strings.HasSuffix(item, "*") {
148			ls := actualList[idx:]
149			// if the * is part of other text in the segment (e.g. `/files*`)
150			// then we don't want to include "files" in the destination
151			if len(item) > 1 {
152				ls = actualList[idx+1:]
153			}
154			splat := strings.Join(ls, "/")
155			mapper[":splat"] = splat
156			break
157		}
158	}
159
160	fin := []string{"/"}
161
162	for _, item := range toList {
163		if item == ":splat" {
164			fin = append(fin, mapper[item])
165		} else if mapper[item] != "" {
166			fin = append(fin, mapper[item])
167		} else {
168			fin = append(fin, item)
169		}
170	}
171
172	result := prefix + filepath.Join(fin...)
173	if !strings.HasSuffix(result, "/") && (strings.HasSuffix(to, "/") || strings.HasSuffix(actual, "/")) {
174		result += "/"
175	}
176	return result
177}
178
179func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpReply {
180	rts := []*HttpReply{}
181	// add route as-is without expansion
182	if fp != "" && !strings.HasSuffix(fp, "/") {
183		defRoute := shared.GetAssetFileName(&utils.FileEntry{
184			Filepath: filepath.Join(projectName, fp),
185		})
186		rts = append(rts, &HttpReply{Filepath: defRoute, Status: http.StatusOK})
187	}
188	expts := expandRoute(projectName, fp, http.StatusOK)
189	rts = append(rts, expts...)
190
191	// user routes
192	for _, redirect := range userRedirects {
193		// this doesn't make sense so it is forbidden
194		if redirect.From == redirect.To {
195			continue
196		}
197
198		// hack: make suffix `/` optional when matching
199		from := filepath.Clean(redirect.From)
200		match := []string{}
201		fromMatcher, matcherType := correlatePlaceholder(fp, from)
202		switch matcherType {
203		case "match":
204			fallthrough
205		case "wildcard":
206			fallthrough
207		case "variable":
208			rr := regexp.MustCompile(fromMatcher)
209			match = rr.FindStringSubmatch(fp)
210		case "none":
211			fallthrough
212		default:
213			break
214		}
215
216		if len(match) > 0 {
217			isRedirect := checkIsRedirect(redirect.Status)
218			if !isRedirect && !hasProtocol(redirect.To) {
219				route := genRedirectRoute(fp, from, redirect.To)
220				// wipe redirect rules to prevent infinite loops
221				// as such we only support a single hop for user defined redirects
222				redirectRoutes := calcRoutes(projectName, route, []*RedirectRule{})
223				rts = append(rts, redirectRoutes...)
224				return rts
225			}
226
227			route := genRedirectRoute(fp, from, redirect.To)
228			userReply := []*HttpReply{}
229			var rule *HttpReply
230			if redirect.To != "" {
231				rule = &HttpReply{
232					Filepath: route,
233					Status:   redirect.Status,
234					Query:    redirect.Query,
235				}
236				userReply = append(userReply, rule)
237			}
238
239			if redirect.Force {
240				rts = userReply
241			} else {
242				rts = append(rts, userReply...)
243			}
244
245			if hasProtocol(redirect.To) {
246				// redirecting to another site so we should bail early
247				return rts
248			} else {
249				// quit after first match
250				break
251			}
252		}
253	}
254
255	// we might have a directory so add a trailing slash with a 301
256	// we can't check for file extention because route could have a dot
257	// and ext parsing gets confused
258	if fp != "" && !strings.HasSuffix(fp, "/") {
259		redirectRoute := shared.GetAssetFileName(&utils.FileEntry{
260			Filepath: fp + "/",
261		})
262		rts = append(
263			rts,
264			&HttpReply{Filepath: redirectRoute, Status: http.StatusMovedPermanently},
265		)
266	}
267
268	notFound := &HttpReply{
269		Filepath: filepath.Join(projectName, "404.html"),
270		Status:   http.StatusNotFound,
271	}
272
273	rts = append(rts,
274		notFound,
275	)
276
277	return rts
278}