+7
-4
src/manifest.go
+7
-4
src/manifest.go
···
257
257
// At the moment, there isn't a good way to report errors except to log them on the terminal.
258
258
// (Perhaps in the future they could be exposed at `.git-pages/status.txt`?)
259
259
func PrepareManifest(ctx context.Context, manifest *Manifest) error {
260
-
// Parse Netlify-style `_redirects`
260
+
// Parse Netlify-style `_redirects`.
261
261
if err := ProcessRedirectsFile(manifest); err != nil {
262
262
logc.Printf(ctx, "redirects err: %s\n", err)
263
263
} else if len(manifest.Redirects) > 0 {
264
264
logc.Printf(ctx, "redirects ok: %d rules\n", len(manifest.Redirects))
265
265
}
266
266
267
-
// Parse Netlify-style `_headers`
267
+
// Check if any redirects are unreachable.
268
+
LintRedirects(manifest)
269
+
270
+
// Parse Netlify-style `_headers`.
268
271
if err := ProcessHeadersFile(manifest); err != nil {
269
272
logc.Printf(ctx, "headers err: %s\n", err)
270
273
} else if len(manifest.Headers) > 0 {
271
274
logc.Printf(ctx, "headers ok: %d rules\n", len(manifest.Headers))
272
275
}
273
276
274
-
// Sniff content type like `http.ServeContent`
277
+
// Sniff content type like `http.ServeContent`.
275
278
DetectContentType(manifest)
276
279
277
-
// Opportunistically compress blobs (must be done last)
280
+
// Opportunistically compress blobs (must be done last).
278
281
CompressFiles(ctx, manifest)
279
282
280
283
return nil
+1
-1
src/pages.go
+1
-1
src/pages.go
···
262
262
redirectKind = RedirectForce
263
263
}
264
264
originalURL := (&url.URL{Host: r.Host}).ResolveReference(r.URL)
265
-
redirectURL, redirectStatus := ApplyRedirectRules(manifest, originalURL, redirectKind)
265
+
_, redirectURL, redirectStatus := ApplyRedirectRules(manifest, originalURL, redirectKind)
266
266
if Is3xxHTTPStatus(redirectStatus) {
267
267
writeRedirect(w, redirectStatus, redirectURL.String())
268
268
return nil
+58
-14
src/redirects.go
+58
-14
src/redirects.go
···
13
13
14
14
const RedirectsFileName string = "_redirects"
15
15
16
-
func unparseRule(rule redirects.Rule) string {
16
+
// Converts our Protobuf representation to tj/go-redirects.
17
+
func exportRedirectRule(rule *RedirectRule) *redirects.Rule {
18
+
return &redirects.Rule{
19
+
From: rule.GetFrom(),
20
+
To: rule.GetTo(),
21
+
Status: int(rule.GetStatus()),
22
+
Force: rule.GetForce(),
23
+
}
24
+
}
25
+
26
+
func unparseRedirectRule(rule *redirects.Rule) string {
17
27
var statusPart string
18
28
if rule.Force {
19
29
statusPart = fmt.Sprintf("%d!", rule.Status)
···
49
59
return status >= 300 && status <= 399
50
60
}
51
61
52
-
func validateRedirectRule(rule redirects.Rule) error {
62
+
func validateRedirectRule(rule *redirects.Rule) error {
53
63
if len(rule.Params) > 0 {
54
64
return fmt.Errorf("rules with parameters are not supported")
55
65
}
···
103
113
}
104
114
105
115
for index, rule := range rules {
106
-
if err := validateRedirectRule(rule); err != nil {
116
+
if err := validateRedirectRule(&rule); err != nil {
107
117
AddProblem(manifest, RedirectsFileName,
108
-
"rule #%d %q: %s", index+1, unparseRule(rule), err)
118
+
"rule #%d %q: %s", index+1, unparseRedirectRule(&rule), err)
109
119
continue
110
120
}
111
121
manifest.Redirects = append(manifest.Redirects, &RedirectRule{
···
121
131
func CollectRedirectsFile(manifest *Manifest) string {
122
132
var rules []string
123
133
for _, rule := range manifest.GetRedirects() {
124
-
rules = append(rules, unparseRule(redirects.Rule{
125
-
From: rule.GetFrom(),
126
-
To: rule.GetTo(),
127
-
Status: int(rule.GetStatus()),
128
-
Force: rule.GetForce(),
129
-
})+"\n")
134
+
rules = append(rules, unparseRedirectRule(exportRedirectRule(rule))+"\n")
130
135
}
131
136
return strings.Join(rules, "")
132
137
}
···
147
152
148
153
const (
149
154
RedirectAny RedirectKind = iota
155
+
RedirectNormal
150
156
RedirectForce
151
157
)
152
158
153
159
func ApplyRedirectRules(
154
160
manifest *Manifest, fromURL *url.URL, kind RedirectKind,
155
161
) (
156
-
toURL *url.URL, status int,
162
+
rule *RedirectRule, toURL *url.URL, status int,
157
163
) {
158
164
fromSegments := pathSegments(fromURL.Path)
159
165
next:
160
-
for _, rule := range manifest.Redirects {
161
-
if kind == RedirectForce && !*rule.Force {
166
+
for _, rule = range manifest.Redirects {
167
+
switch {
168
+
case kind == RedirectNormal && *rule.Force:
169
+
continue
170
+
case kind == RedirectForce && !*rule.Force:
162
171
continue
163
172
}
164
173
// check if the rule matches fromURL
···
205
214
RawQuery: fromURL.RawQuery,
206
215
}
207
216
status = int(*rule.Status)
208
-
break
217
+
return
209
218
}
210
219
// no redirect found
220
+
rule = nil
211
221
return
212
222
}
223
+
224
+
func redirectHasSplat(rule *RedirectRule) bool {
225
+
ruleFromURL, _ := url.Parse(*rule.From) // pre-validated in `validateRedirectRule`
226
+
ruleFromSegments := pathSegments(ruleFromURL.Path)
227
+
return slices.Contains(ruleFromSegments, "*")
228
+
}
229
+
230
+
func LintRedirects(manifest *Manifest) {
231
+
for name, entry := range manifest.GetContents() {
232
+
nameURL, err := url.Parse("/" + name)
233
+
if err != nil {
234
+
continue
235
+
}
236
+
237
+
// Check if the entry URL would trigger a non-forced redirect if the entry didn't exist.
238
+
// If the redirect matches exactly one URL (i.e. has no splat) then it will never be
239
+
// triggered and an issue is reported; if the rule has a splat, it will always be possible
240
+
// to trigger it, as it matches an infinite number of URLs.
241
+
rule, _, _ := ApplyRedirectRules(manifest, nameURL, RedirectNormal)
242
+
if rule != nil && !redirectHasSplat(rule) {
243
+
entryDesc := "file"
244
+
if entry.GetType() == Type_Directory {
245
+
entryDesc = "directory"
246
+
}
247
+
AddProblem(manifest, name,
248
+
"%s shadows redirect %q; remove the %s or use a %d! forced redirect instead",
249
+
entryDesc,
250
+
unparseRedirectRule(exportRedirectRule(rule)),
251
+
entryDesc,
252
+
rule.GetStatus(),
253
+
)
254
+
}
255
+
}
256
+
}