296
mergeSlashes := !strings.Contains(suffix, "//")
297
changePath(r, func(escapedPath string) string {
298
escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
299
- return reverse(trimPathPrefix(reverse(escapedPath), reverse(suffix)))
299
+ return trimPathSuffix(escapedPath, suffix)
300
})
301
}
302
464
return escapedPath
465
}
466
467
-func reverse(s string) string {
468
- r := []rune(s)
469
- for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
470
- r[i], r[j] = r[j], r[i]
467
+// trimPathSuffix is the suffix counterpart of trimPathPrefix: it trims suffix
468
+// from the end of escapedPath using the same escape-aware, case-insensitive
469
+// comparison semantics. Both strings are iterated in lock-step from their ends,
470
+// and if escapedPath has a '%' encoding at a particular position where the
471
+// suffix pattern uses the decoded character, escapedPath's escape is decoded so
472
+// the comparison happens in normalized/unescaped space. Conversely, if the
473
+// suffix pattern itself uses an escape (`%xx`), escapedPath must literally use
474
+// the same escape at that position (the escapes are then compared byte-for-byte).
475
+//
476
+// A naive reverse-then-trimPathPrefix approach cannot be used here: reversing
477
+// the strings moves the '%' to the end of each escape sequence, which defeats
478
+// trimPathPrefix's escape detection (it expects '%' to precede the two hex
479
+// digits) and makes escaped path bytes compare unequal to their decoded form.
480
+func trimPathSuffix(escapedPath, suffix string) string {
481
+ iPath, iSuffix := len(escapedPath), len(suffix)
482
+ for iPath > 0 && iSuffix > 0 {
483
+ suffixCh := suffix[iSuffix-1]
484
+ ch := string(escapedPath[iPath-1])
485
+ step := 1
486
+
487
+ // if escapedPath uses a percent-encoding that ends at this position but
488
+ // the suffix pattern does not encode this position, decode escapedPath's
489
+ // escape so the comparison happens in normalized/unescaped space
490
+ pathHasEscape := iPath >= 3 && escapedPath[iPath-3] == '%'
491
+ suffixHasEscape := iSuffix >= 3 && suffix[iSuffix-3] == '%'
492
+ if pathHasEscape && !suffixHasEscape {
493
+ decoded, err := url.PathUnescape(escapedPath[iPath-3 : iPath])
494
+ if err != nil {
495
+ // should be impossible unless EscapedPath() is returning invalid values!
496
+ return escapedPath
497
+ }
498
+ ch = decoded
499
+ step = 3
500
+ }
501
+
502
+ // suffix comparisons are case-insensitive for consistency with
503
+ // trimPathPrefix, which is case-insensitive for good reasons
504
+ if !strings.EqualFold(ch, string(suffixCh)) {
505
+ return escapedPath
506
+ }
507
+
508
+ iPath -= step
509
+ iSuffix--
510
}
472
- return string(r)
511
+
512
+ // if we iterated through the entire suffix, we found it, so trim it
513
+ if iSuffix <= 0 {
514
+ return escapedPath[:iPath]
515
+ }
516
+
517
+ // otherwise we did not find the suffix
518
+ return escapedPath
519
}
520
521
// substrReplacer describes either a simple and fast substring replacement.