Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
rewrite: Implement regex path replacements
https://caddy.community/t/collapsing-multiple-forward-slashes-in-path-only/11626
Matthew Holt committed
Mar 1, 2021 at 18:27 UTC
ad8d01cb66316cf04ea49ec277316d6f83b9abb6
3 files changed
+103
-26
modules/caddyhttp/rewrite/caddyfile.go
+14
-3
@@ -54,12 +54,14 @@ func parseCaddyfileRewrite(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler,
54
// parseCaddyfileURI sets up a handler for manipulating (but not "rewriting") the
55
// URI from Caddyfile tokens. Syntax:
56
//
57
-// uri [<matcher>] strip_prefix|strip_suffix|replace <target> [<replacement> [<limit>]]
57
+// uri [<matcher>] strip_prefix|strip_suffix|replace|path_regexp <target> [<replacement> [<limit>]]
58
//
59
// If strip_prefix or strip_suffix are used, then <target> will be stripped
60
// only if it is the beginning or the end, respectively, of the URI path. If
61
// replace is used, then <target> will be replaced with <replacement> across
62
-// the whole URI, up to <limit> times (or unlimited if unspecified).
62
+// the whole URI, up to <limit> times (or unlimited if unspecified). If
63
+// path_regexp is used, then regular expression replacements will be performed
64
+// on the path portion of the URI (and a limit cannot be set).
65
func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
66
var rewr Rewrite
67
for h.Next() {
@@ -103,11 +105,20 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
105
}
106
}
107
106
- rewr.URISubstring = append(rewr.URISubstring, replacer{
108
+ rewr.URISubstring = append(rewr.URISubstring, substrReplacer{
109
Find: find,
110
Replace: replace,
111
Limit: limInt,
112
})
113
+ case "path_regexp":
114
+ if len(args) != 3 {
115
+ return nil, h.ArgErr()
116
+ }
117
+ find, replace := args[1], args[2]
118
+ rewr.PathRegexp = append(rewr.PathRegexp, ®exReplacer{
119
+ Find: find,
120
+ Replace: replace,
121
+ })
122
default:
123
return nil, h.Errf("unrecognized URI manipulation '%s'", args[0])
124
}
modules/caddyhttp/rewrite/rewrite.go
+69
-20
@@ -18,6 +18,7 @@ import (
18
"fmt"
19
"net/http"
20
"net/url"
21
+ "regexp"
22
"strconv"
23
"strings"
24
@@ -64,7 +65,10 @@ type Rewrite struct {
65
StripPathSuffix string `json:"strip_path_suffix,omitempty"`
66
67
// Performs substring replacements on the URI.
67
- URISubstring []replacer `json:"uri_substring,omitempty"`
68
+ URISubstring []substrReplacer `json:"uri_substring,omitempty"`
69
+
70
+ // Performs regular expression replacements on the URI path.
71
+ PathRegexp []*regexReplacer `json:"path_regexp,omitempty"`
72
73
logger *zap.Logger
74
}
@@ -80,6 +84,18 @@ func (Rewrite) CaddyModule() caddy.ModuleInfo {
84
// Provision sets up rewr.
85
func (rewr *Rewrite) Provision(ctx caddy.Context) error {
86
rewr.logger = ctx.Logger(rewr)
87
+
88
+ for i, rep := range rewr.PathRegexp {
89
+ if rep.Find == "" {
90
+ return fmt.Errorf("path_regexp find cannot be empty")
91
+ }
92
+ re, err := regexp.Compile(rep.Find)
93
+ if err != nil {
94
+ return fmt.Errorf("compiling regular expression %d: %v", i, err)
95
+ }
96
+ rep.re = re
97
+ }
98
+
99
return nil
100
}
101
@@ -200,12 +216,9 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
216
}
217
if rewr.StripPathSuffix != "" {
218
suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
203
- r.URL.RawPath = strings.TrimSuffix(r.URL.RawPath, suffix)
204
- if p, err := url.PathUnescape(r.URL.RawPath); err == nil && p != "" {
205
- r.URL.Path = p
206
- } else {
207
- r.URL.Path = strings.TrimSuffix(r.URL.Path, suffix)
208
- }
219
+ changePath(r, func(pathOrRawPath string) string {
220
+ return strings.TrimSuffix(pathOrRawPath, suffix)
221
+ })
222
}
223
224
// substring replacements in URI
@@ -213,6 +226,11 @@ func (rewr Rewrite) rewrite(r *http.Request, repl *caddy.Replacer, logger *zap.L
226
rep.do(r, repl)
227
}
228
229
+ // regular expression replacements on the path
230
+ for _, rep := range rewr.PathRegexp {
231
+ rep.do(r, repl)
232
+ }
233
+
234
// update the encoded copy of the URI
235
r.RequestURI = r.URL.RequestURI()
236
@@ -286,12 +304,12 @@ func buildQueryString(qs string, repl *caddy.Replacer) string {
304
return sb.String()
305
}
306
289
-// replacer describes a simple and fast substring replacement.
290
-type replacer struct {
291
- // The substring to find. Supports placeholders.
307
+// substrReplacer describes either a simple and fast substring replacement.
308
+type substrReplacer struct {
309
+ // A substring to find. Supports placeholders.
310
Find string `json:"find,omitempty"`
311
294
- // The substring to replace. Supports placeholders.
312
+ // The substring to replace with. Supports placeholders.
313
Replace string `json:"replace,omitempty"`
314
315
// Maximum number of replacements per string.
@@ -299,9 +317,9 @@ type replacer struct {
317
Limit int `json:"limit,omitempty"`
318
}
319
302
-// do performs the replacement on r.
303
-func (rep replacer) do(r *http.Request, repl *caddy.Replacer) {
304
- if rep.Find == "" || rep.Replace == "" {
320
+// do performs the substring replacement on r.
321
+func (rep substrReplacer) do(r *http.Request, repl *caddy.Replacer) {
322
+ if rep.Find == "" {
323
return
324
}
325
@@ -313,15 +331,46 @@ func (rep replacer) do(r *http.Request, repl *caddy.Replacer) {
331
find := repl.ReplaceAll(rep.Find, "")
332
replace := repl.ReplaceAll(rep.Replace, "")
333
316
- r.URL.RawPath = strings.Replace(r.URL.RawPath, find, replace, lim)
317
- if p, err := url.PathUnescape(r.URL.RawPath); err == nil && p != "" {
318
- r.URL.Path = p
319
- } else {
320
- r.URL.Path = strings.Replace(r.URL.Path, find, replace, lim)
321
- }
334
+ changePath(r, func(pathOrRawPath string) string {
335
+ return strings.Replace(pathOrRawPath, find, replace, lim)
336
+ })
337
338
r.URL.RawQuery = strings.Replace(r.URL.RawQuery, find, replace, lim)
339
}
340
341
+// regexReplacer describes a replacement using a regular expression.
342
+type regexReplacer struct {
343
+ // The regular expression to find.
344
+ Find string `json:"find,omitempty"`
345
+
346
+ // The substring to replace with. Supports placeholders and
347
+ // regular expression capture groups.
348
+ Replace string `json:"replace,omitempty"`
349
+
350
+ re *regexp.Regexp
351
+}
352
+
353
+func (rep regexReplacer) do(r *http.Request, repl *caddy.Replacer) {
354
+ if rep.Find == "" || rep.re == nil {
355
+ return
356
+ }
357
+ replace := repl.ReplaceAll(rep.Replace, "")
358
+ changePath(r, func(pathOrRawPath string) string {
359
+ return rep.re.ReplaceAllString(pathOrRawPath, replace)
360
+ })
361
+}
362
+
363
+// changePath updates the path on the request URL. It first executes newVal on
364
+// req.URL.RawPath, and if the result is a valid escaping, it will be copied
365
+// into req.URL.Path; otherwise newVal is evaluated only on req.URL.Path.
366
+func changePath(req *http.Request, newVal func(pathOrRawPath string) string) {
367
+ req.URL.RawPath = newVal(req.URL.RawPath)
368
+ if p, err := url.PathUnescape(req.URL.RawPath); err == nil && p != "" {
369
+ req.URL.Path = p
370
+ } else {
371
+ req.URL.Path = newVal(req.URL.Path)
372
+ }
373
+}
374
+
375
// Interface guard
376
var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil)
modules/caddyhttp/rewrite/rewrite_test.go
+20
-3
@@ -16,6 +16,7 @@ package rewrite
16
17
import (
18
"net/http"
19
+ "regexp"
20
"testing"
21
22
"github.com/caddyserver/caddy/v2"
@@ -232,20 +233,26 @@ func TestRewrite(t *testing.T) {
233
},
234
235
{
235
- rule: Rewrite{URISubstring: []replacer{{Find: "findme", Replace: "replaced"}}},
236
+ rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},
237
input: newRequest(t, "GET", "/foo/bar"),
238
expect: newRequest(t, "GET", "/foo/bar"),
239
},
240
{
240
- rule: Rewrite{URISubstring: []replacer{{Find: "findme", Replace: "replaced"}}},
241
+ rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},
242
input: newRequest(t, "GET", "/foo/findme/bar"),
243
expect: newRequest(t, "GET", "/foo/replaced/bar"),
244
},
245
{
245
- rule: Rewrite{URISubstring: []replacer{{Find: "findme", Replace: "replaced"}}},
246
+ rule: Rewrite{URISubstring: []substrReplacer{{Find: "findme", Replace: "replaced"}}},
247
input: newRequest(t, "GET", "/foo/findme%2Fbar"),
248
expect: newRequest(t, "GET", "/foo/replaced%2Fbar"),
249
},
250
+
251
+ {
252
+ rule: Rewrite{PathRegexp: []*regexReplacer{{Find: "/{2,}", Replace: "/"}}},
253
+ input: newRequest(t, "GET", "/foo//bar///baz?a=b//c"),
254
+ expect: newRequest(t, "GET", "/foo/bar/baz?a=b//c"),
255
+ },
256
} {
257
// copy the original input just enough so that we can
258
// compare it after the rewrite to see if it changed
@@ -260,6 +267,16 @@ func TestRewrite(t *testing.T) {
267
repl.Set("http.request.uri.path", tc.input.URL.Path)
268
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
269
270
+ // we can't directly call Provision() without a valid caddy.Context
271
+ // (TODO: fix that) so here we ad-hoc compile the regex
272
+ for _, rep := range tc.rule.PathRegexp {
273
+ re, err := regexp.Compile(rep.Find)
274
+ if err != nil {
275
+ t.Fatal(err)
276
+ }
277
+ rep.re = re
278
+ }
279
+
280
changed := tc.rule.rewrite(tc.input, repl, nil)
281
282
if expected, actual := !reqEqual(originalInput, tc.input), changed; expected != actual {