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}