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 · 11 Sep 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/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 item == origList[idx] {
 89			nextList = append(nextList, origList[idx])
 90		}
 91	}
 92
 93	_type := "none"
 94	if len(nextList) > 0 && len(nextList) == len(patternList) {
 95		_type = "match"
 96	} else if strings.Contains(pattern, "*") {
 97		_type = "wildcard"
 98	} else if strings.Contains(pattern, ":") {
 99		_type = "variable"
100	}
101
102	return filepath.Join(nextList...), _type
103}
104
105func splitFp(str string) []string {
106	ls := strings.Split(str, "/")
107	fin := []string{}
108	for _, l := range ls {
109		if l == "" {
110			continue
111		}
112		fin = append(fin, l)
113	}
114	return fin
115}
116
117func genRedirectRoute(actual string, fromStr string, to string) string {
118	if to == "/" {
119		return to
120	}
121	actualList := splitFp(actual)
122	fromList := splitFp(fromStr)
123	prefix := ""
124	var toList []string
125	if hasProtocol(to) {
126		u, _ := url.Parse(to)
127		if u.Path == "" {
128			return to
129		}
130		toList = splitFp(u.Path)
131		prefix = u.Scheme + "://" + u.Host
132	} else {
133		toList = splitFp(to)
134	}
135
136	mapper := map[string]string{}
137	for idx, item := range fromList {
138		if len(actualList) <= idx {
139			continue
140		}
141
142		if strings.HasPrefix(item, ":") {
143			mapper[item] = actualList[idx]
144		}
145		if item == "*" {
146			splat := strings.Join(actualList[idx:], "/")
147			mapper[":splat"] = splat
148			break
149		}
150	}
151
152	fin := []string{"/"}
153
154	for _, item := range toList {
155		if item == ":splat" {
156			fin = append(fin, mapper[item])
157		} else if mapper[item] != "" {
158			fin = append(fin, mapper[item])
159		} else {
160			fin = append(fin, item)
161		}
162	}
163
164	result := prefix + filepath.Join(fin...)
165	if !strings.HasSuffix(result, "/") && (strings.HasSuffix(to, "/") || strings.HasSuffix(actual, "/")) {
166		result += "/"
167	}
168	return result
169}
170
171func calcRoutes(projectName, fp string, userRedirects []*RedirectRule) []*HttpReply {
172	rts := []*HttpReply{}
173	// add route as-is without expansion
174	if fp != "" && !strings.HasSuffix(fp, "/") {
175		defRoute := shared.GetAssetFileName(&utils.FileEntry{
176			Filepath: filepath.Join(projectName, fp),
177		})
178		rts = append(rts, &HttpReply{Filepath: defRoute, Status: http.StatusOK})
179	}
180	expts := expandRoute(projectName, fp, http.StatusOK)
181	rts = append(rts, expts...)
182
183	// user routes
184	for _, redirect := range userRedirects {
185		// this doesn't make sense so it is forbidden
186		if redirect.From == redirect.To {
187			continue
188		}
189
190		// hack: make suffix `/` optional when matching
191		from := filepath.Clean(redirect.From)
192		match := []string{}
193		fromMatcher, matcherType := correlatePlaceholder(fp, from)
194		switch matcherType {
195		case "match":
196			fallthrough
197		case "wildcard":
198			fallthrough
199		case "variable":
200			rr := regexp.MustCompile(fromMatcher)
201			match = rr.FindStringSubmatch(fp)
202		case "none":
203			fallthrough
204		default:
205			break
206		}
207
208		if len(match) > 0 {
209			isRedirect := checkIsRedirect(redirect.Status)
210			if !isRedirect && !hasProtocol(redirect.To) {
211				route := genRedirectRoute(fp, from, redirect.To)
212				// wipe redirect rules to prevent infinite loops
213				// as such we only support a single hop for user defined redirects
214				redirectRoutes := calcRoutes(projectName, route, []*RedirectRule{})
215				rts = append(rts, redirectRoutes...)
216				return rts
217			}
218
219			route := genRedirectRoute(fp, from, redirect.To)
220			userReply := []*HttpReply{}
221			var rule *HttpReply
222			if redirect.To != "" {
223				rule = &HttpReply{
224					Filepath: route,
225					Status:   redirect.Status,
226					Query:    redirect.Query,
227				}
228				userReply = append(userReply, rule)
229			}
230
231			if redirect.Force {
232				rts = userReply
233			} else {
234				rts = append(rts, userReply...)
235			}
236
237			if hasProtocol(redirect.To) {
238				// redirecting to another site so we should bail early
239				return rts
240			} else {
241				// quit after first match
242				break
243			}
244		}
245	}
246
247	// we might have a directory so add a trailing slash with a 301
248	// we can't check for file extention because route could have a dot
249	// and ext parsing gets confused
250	if fp != "" && !strings.HasSuffix(fp, "/") {
251		redirectRoute := shared.GetAssetFileName(&utils.FileEntry{
252			Filepath: fp + "/",
253		})
254		rts = append(
255			rts,
256			&HttpReply{Filepath: redirectRoute, Status: http.StatusMovedPermanently},
257		)
258	}
259
260	notFound := &HttpReply{
261		Filepath: filepath.Join(projectName, "404.html"),
262		Status:   http.StatusNotFound,
263	}
264
265	rts = append(rts,
266		notFound,
267	)
268
269	return rts
270}