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}