@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 801 lines 24.9 KB
Raw
1 // Copyright 2015 Matthew Holt and The Caddy Authors
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 package rewrite
16
17 import (
18 "fmt"
19 "net/http"
20 "net/url"
21 "regexp"
22 "strconv"
23 "strings"
24
25 "go.uber.org/zap"
26
27 "github.com/caddyserver/caddy/v2"
28 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
29 )
30
31 func init() {
32 caddy.RegisterModule(Rewrite{})
33 }
34
35 // Rewrite is a middleware which can rewrite/mutate HTTP requests.
36 //
37 // The Method and URI properties are "setters" (the request URI
38 // will be overwritten with the given values). Other properties are
39 // "modifiers" (they modify existing values in a differentiable
40 // way). It is atypical to combine the use of setters and
41 // modifiers in a single rewrite.
42 //
43 // To ensure consistent behavior, prefix and suffix stripping is
44 // performed in the URL-decoded (unescaped, normalized) space by
45 // default except for the specific bytes where an escape sequence
46 // is used in the prefix or suffix pattern.
47 //
48 // For all modifiers, paths are cleaned before being modified so that
49 // multiple, consecutive slashes are collapsed into a single slash,
50 // and dot elements are resolved and removed. In the special case
51 // of a prefix, suffix, or substring containing "//" (repeated slashes),
52 // slashes will not be merged while cleaning the path so that
53 // the rewrite can be interpreted literally.
54 type Rewrite struct {
55 // Changes the request's HTTP verb.
56 Method string `json:"method,omitempty"`
57
58 // Changes the request's URI, which consists of path and query string.
59 // Only components of the URI that are specified will be changed.
60 // For example, a value of "/foo.html" or "foo.html" will only change
61 // the path and will preserve any existing query string. Similarly, a
62 // value of "?a=b" will only change the query string and will not affect
63 // the path. Both can also be changed: "/foo?a=b" - this sets both the
64 // path and query string at the same time.
65 //
66 // You can also use placeholders. For example, to preserve the existing
67 // query string, you might use: "?{http.request.uri.query}&a=b". Any
68 // key-value pairs you add to the query string will not overwrite
69 // existing values (individual pairs are append-only).
70 //
71 // To clear the query string, explicitly set an empty one: "?"
72 URI string `json:"uri,omitempty"`
73
74 // Strips the given prefix from the beginning of the URI path.
75 // The prefix should be written in normalized (unescaped) form,
76 // but if an escaping (`%xx`) is used, the path will be required
77 // to have that same escape at that position in order to match.
78 StripPathPrefix string `json:"strip_path_prefix,omitempty"`
79
80 // Strips the given suffix from the end of the URI path.
81 // The suffix should be written in normalized (unescaped) form,
82 // but if an escaping (`%xx`) is used, the path will be required
83 // to have that same escape at that position in order to match.
84 StripPathSuffix string `json:"strip_path_suffix,omitempty"`
85
86 // Performs substring replacements on the URI.
87 URISubstring []substrReplacer `json:"uri_substring,omitempty"`
88
89 // Performs regular expression replacements on the URI path.
90 PathRegexp []*regexReplacer `json:"path_regexp,omitempty"`
91
92 // Mutates the query string of the URI.
93 Query *queryOps `json:"query,omitempty"`
94
95 logger *zap.Logger
96 }
97
98 // CaddyModule returns the Caddy module information.
99 func (Rewrite) CaddyModule() caddy.ModuleInfo {
100 return caddy.ModuleInfo{
101 ID: "http.handlers.rewrite",
102 New: func() caddy.Module { return new(Rewrite) },
103 }
104 }
105
106 // Provision sets up rewr.
107 func (rewr *Rewrite) Provision(ctx caddy.Context) error {
108 rewr.logger = ctx.Logger()
109
110 for i, rep := range rewr.PathRegexp {
111 if rep.Find == "" {
112 return fmt.Errorf("path_regexp find cannot be empty")
113 }
114 re, err := regexp.Compile(rep.Find)
115 if err != nil {
116 return fmt.Errorf("compiling regular expression %d: %v", i, err)
117 }
118 rep.re = re
119 }
120 if rewr.Query != nil {
121 for _, replacementOp := range rewr.Query.Replace {
122 err := replacementOp.Provision(ctx)
123 if err != nil {
124 return fmt.Errorf("compiling regular expression %s in query rewrite replace operation: %v", replacementOp.SearchRegexp, err)
125 }
126 }
127 }
128
129 return nil
130 }
131
132 func (rewr Rewrite) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
133 repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
134 const message = "rewrote request"
135
136 c := rewr.logger.Check(zap.DebugLevel, message)
137 if c == nil {
138 rewr.Rewrite(r, repl)
139 return next.ServeHTTP(w, r)
140 }
141
142 changed := rewr.Rewrite(r, repl)
143
144 if changed {
145 c.Write(
146 zap.Object("request", caddyhttp.LoggableHTTPRequest{Request: r}),
147 zap.String("method", r.Method),
148 zap.String("uri", r.RequestURI),
149 )
150 }
151
152 return next.ServeHTTP(w, r)
153 }
154
155 // rewrite performs the rewrites on r using repl, which should
156 // have been obtained from r, but is passed in for efficiency.
157 // It returns true if any changes were made to r.
158 func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
159 oldMethod := r.Method
160 oldURI := r.RequestURI
161
162 // method
163 if rewr.Method != "" {
164 r.Method = strings.ToUpper(repl.ReplaceAll(rewr.Method, ""))
165 }
166
167 // uri (path, query string and... fragment, because why not)
168 if uri := rewr.URI; uri != "" {
169 // find the bounds of each part of the URI that exist
170 pathStart, qsStart, fragStart := -1, -1, -1
171 pathEnd, qsEnd := -1, -1
172 loop:
173 for i, ch := range uri {
174 switch {
175 case ch == '?' && qsStart < 0:
176 pathEnd, qsStart = i, i+1
177 case ch == '#' && fragStart < 0: // everything after fragment is fragment (very clear in RFC 3986 section 4.2)
178 if qsStart < 0 {
179 pathEnd = i
180 } else {
181 qsEnd = i
182 }
183 fragStart = i + 1
184 break loop
185 case pathStart < 0 && qsStart < 0:
186 pathStart = i
187 }
188 }
189 if pathStart >= 0 && pathEnd < 0 {
190 pathEnd = len(uri)
191 }
192 if qsStart >= 0 && qsEnd < 0 {
193 qsEnd = len(uri)
194 }
195
196 // isolate the three main components of the URI
197 var path, query, frag string
198 if pathStart > -1 {
199 path = uri[pathStart:pathEnd]
200 }
201 if qsStart > -1 {
202 query = uri[qsStart:qsEnd]
203 }
204 if fragStart > -1 {
205 frag = uri[fragStart:]
206 }
207
208 // build components which are specified, and store them
209 // in a temporary variable so that they all read the
210 // same version of the URI
211 var newPath, newQuery, newFrag string
212
213 if path != "" {
214 path = escapePathPlaceholders(path, r, repl)
215 newPath = repl.ReplaceAll(path, "")
216 }
217
218 // a fragment may have snuck into the path component during
219 // replacements; everything after the first '#' is fragment
220 // (RFC 3986 section 4.2), which is never sent to the server,
221 // so drop it. this mirrors how a literal '#' in the configured
222 // URI is handled by the scan above, and prevents the fragment
223 // from being mistaken for part of the path or query below.
224 if before, _, found := strings.Cut(newPath, "#"); found {
225 newPath = before
226 }
227
228 // before continuing, we need to check if a query string
229 // snuck into the path component during replacements
230 queryInjected := false
231 if before, after, found := strings.Cut(newPath, "?"); found {
232 // recompute; new path contains a query string
233 var injectedQuery string
234 newPath, injectedQuery = before, after
235 // don't overwrite explicitly-configured query string
236 if query == "" {
237 // the injected query came from the first-pass placeholder
238 // expansion above, which means any '{' or '}' bytes in it
239 // must have come from replacement values (e.g. a request
240 // header), not from operator-written placeholder syntax.
241 // escape them so buildQueryString does not re-expand them,
242 // which would allow attacker input like {env.SECRET} to be
243 // evaluated (see GHSA-j8px-rmrx-76h9).
244 injectedQuery = strings.ReplaceAll(injectedQuery, "{", "%7B")
245 injectedQuery = strings.ReplaceAll(injectedQuery, "}", "%7D")
246 query = injectedQuery
247
248 // the replacement value spoke about the query string, so the
249 // query must be written back even though the configured URI
250 // had no literal '?' to set qsStart. an injected query that
251 // is empty (a value ending in '?') clears the query, which is
252 // consistent with configuring a bare '?'.
253 queryInjected = true
254 }
255 }
256
257 if query != "" {
258 newQuery = buildQueryString(query, repl)
259 }
260 if frag != "" {
261 newFrag = repl.ReplaceAll(frag, "")
262 }
263
264 // update the URI with the new components
265 // only after building them
266 if pathStart >= 0 {
267 if path, err := url.PathUnescape(newPath); err != nil {
268 r.URL.Path = newPath
269 } else {
270 r.URL.Path = path
271 }
272 r.URL.RawPath = "" // force recomputing when EscapedPath() is called
273 }
274 if qsStart >= 0 || queryInjected {
275 r.URL.RawQuery = newQuery
276 }
277 if fragStart >= 0 {
278 r.URL.Fragment = newFrag
279 }
280 }
281
282 // strip path prefix or suffix
283 if rewr.StripPathPrefix != "" {
284 prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
285 if !strings.HasPrefix(prefix, "/") {
286 prefix = "/" + prefix
287 }
288 mergeSlashes := !strings.Contains(prefix, "//")
289 stripped := false
290 changePath(r, func(escapedPath string) string {
291 escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
292 trimmed := trimPathPrefix(escapedPath, prefix)
293 stripped = stripped || trimmed != escapedPath
294 return trimmed
295 })
296 if stripped {
297 canonicalizePath(r)
298 }
299 }
300 if rewr.StripPathSuffix != "" {
301 suffix := repl.ReplaceAll(rewr.StripPathSuffix, "")
302 mergeSlashes := !strings.Contains(suffix, "//")
303 stripped := false
304 changePath(r, func(escapedPath string) string {
305 escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
306 trimmed := trimPathSuffix(escapedPath, suffix)
307 stripped = stripped || trimmed != escapedPath
308 return trimmed
309 })
310 if stripped {
311 canonicalizePath(r)
312 }
313 }
314
315 // substring replacements in URI
316 for _, rep := range rewr.URISubstring {
317 rep.do(r, repl)
318 }
319
320 // regular expression replacements on the path
321 for _, rep := range rewr.PathRegexp {
322 rep.do(r, repl)
323 }
324
325 // apply query operations
326 if rewr.Query != nil {
327 rewr.Query.do(r, repl)
328 }
329
330 // update the encoded copy of the URI
331 r.RequestURI = r.URL.RequestURI()
332
333 // return true if anything changed
334 return r.Method != oldMethod || r.RequestURI != oldURI
335 }
336
337 func escapePathPlaceholders(path string, r *http.Request, repl *caddy.Replacer) string {
338 // Replace path-valued placeholders in escaped form before the URI is parsed,
339 // otherwise literal '?' and '%' bytes from the path can be interpreted as URI
340 // delimiters or percent-escape sequences during the rewrite.
341 pathPlaceholder := "{http.request.uri.path}"
342 if strings.Contains(path, pathPlaceholder) {
343 path = strings.ReplaceAll(path, pathPlaceholder, r.URL.EscapedPath())
344 }
345
346 fileMatchRelativePlaceholder := "{http.matchers.file.relative}"
347 if strings.Contains(path, fileMatchRelativePlaceholder) {
348 if val, ok := repl.Get("http.matchers.file.relative"); ok {
349 if relativePath, ok := val.(string); ok {
350 path = strings.ReplaceAll(path, fileMatchRelativePlaceholder, escapePathPreservingSlashes(relativePath))
351 }
352 }
353 }
354
355 fileMatchRemainderPlaceholder := "{http.matchers.file.remainder}"
356 if strings.Contains(path, fileMatchRemainderPlaceholder) {
357 if val, ok := repl.Get("http.matchers.file.remainder"); ok {
358 if remainder, ok := val.(string); ok {
359 path = strings.ReplaceAll(path, fileMatchRemainderPlaceholder, escapePathPreservingSlashes(remainder))
360 }
361 }
362 }
363
364 return path
365 }
366
367 func escapePathPreservingSlashes(path string) string {
368 return strings.ReplaceAll(url.PathEscape(path), "%2F", "/")
369 }
370
371 // buildQueryString takes an input query string and
372 // performs replacements on each component, returning
373 // the resulting query string. This function appends
374 // duplicate keys rather than replaces.
375 func buildQueryString(qs string, repl *caddy.Replacer) string {
376 var sb strings.Builder
377 sb.Grow(len(qs))
378
379 // first component must be key, which is the same
380 // as if we just wrote a value in previous iteration
381 wroteVal := true
382
383 for len(qs) > 0 {
384 // determine the end of this component, which will be at
385 // the next equal sign or ampersand, whichever comes first
386 nextEq, nextAmp := strings.Index(qs, "="), strings.Index(qs, "&")
387 if !wroteVal {
388 // we are consuming a value, and '=' only delimits a key from
389 // its value; any further '=' bytes are literal data, such as
390 // base64 padding in a signature. only '&' ends a value.
391 nextEq = -1
392 }
393 ampIsNext := nextAmp >= 0 && (nextAmp < nextEq || nextEq < 0)
394 end := len(qs) // assume no delimiter remains...
395 if ampIsNext {
396 end = nextAmp // ...unless ampersand is first...
397 } else if nextEq >= 0 && (nextEq < nextAmp || nextAmp < 0) {
398 end = nextEq // ...or unless equal is first.
399 }
400
401 // consume the component and write the result
402 comp := qs[:end]
403 comp, _ = repl.ReplaceFunc(comp, func(name string, val any) (any, error) {
404 if name == "http.request.uri.query" && wroteVal {
405 return val, nil // already escaped
406 }
407 var valStr string
408 switch v := val.(type) {
409 case string:
410 valStr = v
411 case fmt.Stringer:
412 valStr = v.String()
413 case int:
414 valStr = strconv.Itoa(v)
415 default:
416 valStr = fmt.Sprintf("%+v", v)
417 }
418 return url.QueryEscape(valStr), nil
419 })
420 if end < len(qs) {
421 end++ // consume delimiter
422 }
423 qs = qs[end:]
424
425 // if previous iteration wrote a value,
426 // that means we are writing a key
427 if wroteVal {
428 if sb.Len() > 0 && len(comp) > 0 {
429 sb.WriteRune('&')
430 }
431 } else {
432 sb.WriteRune('=')
433 }
434 sb.WriteString(comp)
435
436 // remember for the next iteration that we just wrote a value,
437 // which means the next iteration MUST write a key
438 wroteVal = ampIsNext
439 }
440
441 return sb.String()
442 }
443
444 // trimPathPrefix is like strings.TrimPrefix, but customized for advanced URI
445 // path prefix matching. The string prefix will be trimmed from the beginning
446 // of escapedPath if escapedPath starts with prefix. Rather than a naive 1:1
447 // comparison of each byte to determine if escapedPath starts with prefix,
448 // both strings are iterated in lock-step, and if prefix has a '%' encoding
449 // at a particular position, escapedPath must also have the same encoding
450 // representation for that character. In other words, if the prefix string
451 // uses the escaped form for a character, escapedPath must literally use the
452 // same escape at that position. Otherwise, all character comparisons are
453 // performed in normalized/unescaped space.
454 func trimPathPrefix(escapedPath, prefix string) string {
455 var iPath, iPrefix int
456 for iPath < len(escapedPath) && iPrefix < len(prefix) {
457 prefixCh := prefix[iPrefix]
458 ch := string(escapedPath[iPath])
459
460 if ch == "%" && !hasValidPercentEncoding(prefix, iPrefix) && len(escapedPath) >= iPath+3 {
461 var err error
462 ch, err = url.PathUnescape(escapedPath[iPath : iPath+3])
463 if err != nil {
464 // should be impossible unless EscapedPath() is returning invalid values!
465 return escapedPath
466 }
467 iPath += 2
468 }
469
470 // prefix comparisons are case-insensitive to consistency with
471 // path matcher, which is case-insensitive for good reasons
472 if !strings.EqualFold(ch, string(prefixCh)) {
473 return escapedPath
474 }
475
476 iPath++
477 iPrefix++
478 }
479
480 // if we iterated through the entire prefix, we found it, so trim it
481 if iPrefix >= len(prefix) {
482 return escapedPath[iPath:]
483 }
484
485 // otherwise we did not find the prefix
486 return escapedPath
487 }
488
489 // trimPathSuffix is the suffix counterpart of trimPathPrefix: it trims suffix
490 // from the end of escapedPath using the same escape-aware, case-insensitive
491 // comparison semantics. Both strings are iterated in lock-step from their ends,
492 // and if escapedPath has a '%' encoding at a particular position where the
493 // suffix pattern uses the decoded character, escapedPath's escape is decoded so
494 // the comparison happens in normalized/unescaped space. Conversely, if the
495 // suffix pattern itself uses an escape (`%xx`), escapedPath must literally use
496 // the same escape at that position (the escapes are then compared byte-for-byte).
497 //
498 // A naive reverse-then-trimPathPrefix approach cannot be used here: reversing
499 // the strings moves the '%' to the end of each escape sequence, which defeats
500 // trimPathPrefix's escape detection (it expects '%' to precede the two hex
501 // digits) and makes escaped path bytes compare unequal to their decoded form.
502 func trimPathSuffix(escapedPath, suffix string) string {
503 iPath, iSuffix := len(escapedPath), len(suffix)
504 for iPath > 0 && iSuffix > 0 {
505 suffixCh := suffix[iSuffix-1]
506 ch := string(escapedPath[iPath-1])
507 step := 1
508
509 // if escapedPath uses a percent-encoding that ends at this position but
510 // the suffix pattern does not encode this position, decode escapedPath's
511 // escape so the comparison happens in normalized/unescaped space.
512 // Walking right-to-left, the suffix is inside one of its own escapes
513 // either when an escape ends here (a '%' two bytes back) or when the
514 // cursor has reached that escape's introducing '%' itself.
515 pathHasEscape := iPath >= 3 && escapedPath[iPath-3] == '%'
516 suffixHasEscape := hasValidPercentEncoding(suffix, iSuffix-3) ||
517 hasValidPercentEncoding(suffix, iSuffix-1)
518 if pathHasEscape && !suffixHasEscape {
519 decoded, err := url.PathUnescape(escapedPath[iPath-3 : iPath])
520 if err != nil {
521 // should be impossible unless EscapedPath() is returning invalid values!
522 return escapedPath
523 }
524 ch = decoded
525 step = 3
526 }
527
528 // suffix comparisons are case-insensitive for consistency with
529 // trimPathPrefix, which is case-insensitive for good reasons
530 if !strings.EqualFold(ch, string(suffixCh)) {
531 return escapedPath
532 }
533
534 iPath -= step
535 iSuffix--
536 }
537
538 // if we iterated through the entire suffix, we found it, so trim it
539 if iSuffix <= 0 {
540 return escapedPath[:iPath]
541 }
542
543 // otherwise we did not find the suffix
544 return escapedPath
545 }
546
547 func hasValidPercentEncoding(s string, i int) bool {
548 if i < 0 || i+3 > len(s) || s[i] != '%' {
549 return false
550 }
551 _, err := url.PathUnescape(s[i : i+3])
552 return err == nil
553 }
554
555 // substrReplacer describes either a simple and fast substring replacement.
556 type substrReplacer struct {
557 // A substring to find. Supports placeholders.
558 Find string `json:"find,omitempty"`
559
560 // The substring to replace with. Supports placeholders.
561 Replace string `json:"replace,omitempty"`
562
563 // Maximum number of replacements per string.
564 // Set to <= 0 for no limit (default).
565 Limit int `json:"limit,omitempty"`
566 }
567
568 // do performs the substring replacement on r.
569 func (rep substrReplacer) do(r *http.Request, repl *caddy.Replacer) {
570 if rep.Find == "" {
571 return
572 }
573
574 lim := rep.Limit
575 if lim == 0 {
576 lim = -1
577 }
578
579 find := repl.ReplaceAll(rep.Find, "")
580 replace := repl.ReplaceAll(rep.Replace, "")
581
582 mergeSlashes := !strings.Contains(rep.Find, "//")
583
584 changePath(r, func(pathOrRawPath string) string {
585 return strings.Replace(caddyhttp.CleanPath(pathOrRawPath, mergeSlashes), find, replace, lim)
586 })
587
588 r.URL.RawQuery = strings.Replace(r.URL.RawQuery, find, replace, lim)
589 }
590
591 // regexReplacer describes a replacement using a regular expression.
592 type regexReplacer struct {
593 // The regular expression to find.
594 Find string `json:"find,omitempty"`
595
596 // The substring to replace with. Supports placeholders and
597 // regular expression capture groups.
598 Replace string `json:"replace,omitempty"`
599
600 re *regexp.Regexp
601 }
602
603 func (rep regexReplacer) do(r *http.Request, repl *caddy.Replacer) {
604 if rep.Find == "" || rep.re == nil {
605 return
606 }
607 replace := repl.ReplaceAll(rep.Replace, "")
608 changePath(r, func(pathOrRawPath string) string {
609 return rep.re.ReplaceAllString(pathOrRawPath, replace)
610 })
611 }
612
613 func changePath(req *http.Request, newVal func(pathOrRawPath string) string) {
614 req.URL.RawPath = newVal(req.URL.EscapedPath())
615 if p, err := url.PathUnescape(req.URL.RawPath); err == nil && p != "" {
616 req.URL.Path = p
617 } else {
618 req.URL.Path = newVal(req.URL.Path)
619 }
620 // RawPath is only needed if it is a valid, non-canonical encoding of Path;
621 // (see #6578). Mirror net/url.URL.setPath by comparing against the default
622 // escaping of Path instead.
623 if req.URL.RawPath == defaultEscapedPath(req.URL.Path) {
624 req.URL.RawPath = ""
625 }
626 }
627
628 // canonicalizePath anchors and cleans a stripped origin-form path while
629 // preserving an alternate RawPath encoding when it still represents the
630 // canonical Path. It is applied after prefix or suffix removal because stripping can
631 // expose a relative path or dot segments that downstream handlers interpret
632 // differently.
633 func canonicalizePath(req *http.Request) {
634 p := req.URL.Path
635 if !strings.HasPrefix(p, "/") {
636 p = "/" + p
637 }
638 cleaned := caddyhttp.CleanPath(defaultEscapedPath(p), false)
639 p, _ = url.PathUnescape(cleaned) // defaultEscapedPath always returns valid escapes
640
641 rawPath := req.URL.RawPath
642 if rawPath != "" {
643 if !strings.HasPrefix(rawPath, "/") {
644 rawPath = "/" + rawPath
645 }
646 decoded, err := url.PathUnescape(rawPath)
647 if err != nil || decoded != p || rawPath == defaultEscapedPath(p) {
648 rawPath = ""
649 }
650 }
651
652 req.URL.Path = p
653 req.URL.RawPath = rawPath
654 }
655
656 // defaultEscapedPath returns the canonical percent-encoding of p, matching
657 // what net/url.URL.EscapedPath() produces when RawPath is empty. It mirrors
658 // the comparison net/url.URL.setPath uses to decide whether RawPath is needed.
659 func defaultEscapedPath(p string) string {
660 return (&url.URL{Path: p}).EscapedPath()
661 }
662
663 // queryOps describes the operations to perform on query keys: add, set, rename and delete.
664 type queryOps struct {
665 // Renames a query key from Key to Val, without affecting the value.
666 Rename []queryOpsArguments `json:"rename,omitempty"`
667
668 // Sets query parameters; overwrites a query key with the given value.
669 Set []queryOpsArguments `json:"set,omitempty"`
670
671 // Adds query parameters; does not overwrite an existing query field,
672 // and only appends an additional value for that key if any already exist.
673 Add []queryOpsArguments `json:"add,omitempty"`
674
675 // Replaces query parameters.
676 Replace []*queryOpsReplacement `json:"replace,omitempty"`
677
678 // Deletes a given query key by name.
679 Delete []string `json:"delete,omitempty"`
680 }
681
682 // Provision compiles the query replace operation regex.
683 func (replacement *queryOpsReplacement) Provision(_ caddy.Context) error {
684 if replacement.SearchRegexp != "" {
685 re, err := regexp.Compile(replacement.SearchRegexp)
686 if err != nil {
687 return fmt.Errorf("replacement for query field '%s': %v", replacement.Key, err)
688 }
689 replacement.re = re
690 }
691 return nil
692 }
693
694 func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
695 query := r.URL.Query()
696 for _, renameParam := range q.Rename {
697 key := repl.ReplaceAll(renameParam.Key, "")
698 val := repl.ReplaceAll(renameParam.Val, "")
699 if key == "" || val == "" {
700 continue
701 }
702 if key == val {
703 continue
704 }
705 originalValues, ok := query[key]
706 if !ok {
707 continue
708 }
709 query[val] = originalValues
710 delete(query, key)
711 }
712
713 for _, setParam := range q.Set {
714 key := repl.ReplaceAll(setParam.Key, "")
715 if key == "" {
716 continue
717 }
718 val := repl.ReplaceAll(setParam.Val, "")
719 query[key] = []string{val}
720 }
721
722 for _, addParam := range q.Add {
723 key := repl.ReplaceAll(addParam.Key, "")
724 if key == "" {
725 continue
726 }
727 val := repl.ReplaceAll(addParam.Val, "")
728 query[key] = append(query[key], val)
729 }
730
731 for _, replaceParam := range q.Replace {
732 key := repl.ReplaceAll(replaceParam.Key, "")
733 search := repl.ReplaceKnown(replaceParam.Search, "")
734 replace := repl.ReplaceKnown(replaceParam.Replace, "")
735
736 // replace all query keys...
737 if key == "*" {
738 for fieldName, vals := range query {
739 for i := range vals {
740 if replaceParam.re != nil {
741 query[fieldName][i] = replaceParam.re.ReplaceAllString(query[fieldName][i], replace)
742 } else {
743 query[fieldName][i] = strings.ReplaceAll(query[fieldName][i], search, replace)
744 }
745 }
746 }
747 continue
748 }
749
750 vals, ok := query[key]
751 if !ok {
752 continue
753 }
754 for i := range vals {
755 if replaceParam.re != nil {
756 query[key][i] = replaceParam.re.ReplaceAllString(query[key][i], replace)
757 } else {
758 query[key][i] = strings.ReplaceAll(query[key][i], search, replace)
759 }
760 }
761 }
762
763 for _, deleteParam := range q.Delete {
764 param := repl.ReplaceAll(deleteParam, "")
765 if param == "" {
766 continue
767 }
768 delete(query, param)
769 }
770
771 r.URL.RawQuery = query.Encode()
772 }
773
774 type queryOpsArguments struct {
775 // A key in the query string. Note that query string keys may appear multiple times.
776 Key string `json:"key,omitempty"`
777
778 // The value for the given operation; for add and set, this is
779 // simply the value of the query, and for rename this is the
780 // query key to rename to.
781 Val string `json:"val,omitempty"`
782 }
783
784 type queryOpsReplacement struct {
785 // The key to replace in the query string.
786 Key string `json:"key,omitempty"`
787
788 // The substring to search for.
789 Search string `json:"search,omitempty"`
790
791 // The regular expression to search with.
792 SearchRegexp string `json:"search_regexp,omitempty"`
793
794 // The string with which to replace matches.
795 Replace string `json:"replace,omitempty"`
796
797 re *regexp.Regexp
798 }
799
800 // Interface guard
801 var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil)