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
caddyhttp: lower-case path patterns with placeholders at request time (#8343)
* caddyhttp: lower-case path patterns with placeholders at request time Patterns with placeholders were lower-cased at provisioning, which changed case-sensitive placeholder names such as {env.FOO}, while the replaced values were never lower-cased and so could not match the lower-cased request path. Leave such patterns untouched at provisioning and lower-case the expanded pattern at request time. Fixes #6387 * caddyhttp: restore the encoded separator path matcher tests
D'Ali Ben Macha committed
Nov 4, 2026 at 07:37 UTC
2147da70f7a5a3d6e889b5e7fbc824b97533f7a4
2 files changed
+45
-6
modules/caddyhttp/matchers.go
+10
-3
@@ -406,7 +406,10 @@ func (MatchPath) CaddyModule() caddy.ModuleInfo {
406
}
407
}
408
409
-// Provision lower-cases the paths in m to ensure case-insensitive matching.
409
+// Provision lower-cases the static paths in m to ensure case-insensitive
410
+// matching. Paths with placeholders are left untouched, since placeholder
411
+// names can be case-sensitive (e.g. environment variables); they are
412
+// lower-cased after replacement, at request time.
413
func (m MatchPath) Provision(_ caddy.Context) error {
414
for i := range m {
415
if m[i] == "*" && i > 0 {
@@ -414,7 +417,9 @@ func (m MatchPath) Provision(_ caddy.Context) error {
417
m[0] = m[i]
418
break
419
}
417
- m[i] = strings.ToLower(m[i])
420
+ if !strings.Contains(m[i], "{") {
421
+ m[i] = strings.ToLower(m[i])
422
+ }
423
}
424
return nil
425
}
@@ -438,7 +443,9 @@ func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
443
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
444
445
for _, matchPattern := range m {
441
- matchPattern = repl.ReplaceAll(matchPattern, "")
446
+ if strings.Contains(matchPattern, "{") {
447
+ matchPattern = strings.ToLower(repl.ReplaceAll(matchPattern, ""))
448
+ }
449
450
// special case: whole path is wildcard; this is unnecessary
451
// as it matches all requests, which is the same as no matcher
modules/caddyhttp/matchers_test.go
+35
-3
@@ -170,9 +170,12 @@ func TestHostMatcher(t *testing.T) {
170
}
171
172
func TestPathMatcher(t *testing.T) {
173
+ t.Setenv("CADDY_TEST_PATH_PREFIX", "admin")
174
+
175
for i, tc := range []struct {
174
- match MatchPath // not URI-encoded because not parsing from a URI
175
- input string // should be valid URI encoding (escaped) since it will become part of a request
176
+ match MatchPath // not URI-encoded because not parsing from a URI
177
+ input string // should be valid URI encoding (escaped) since it will become part of a request
178
+ headers http.Header // request headers, for header placeholders in the match
179
expect bool
180
provisionErr bool
181
}{
@@ -454,6 +457,34 @@ func TestPathMatcher(t *testing.T) {
457
input: "/admin%2fpanelX",
458
expect: false,
459
},
460
+ {
461
+ match: MatchPath{"/{env.CADDY_TEST_PATH_PREFIX}/*"},
462
+ input: "/admin/panel",
463
+ expect: true,
464
+ },
465
+ {
466
+ match: MatchPath{"/{env.CADDY_TEST_PATH_PREFIX}/*"},
467
+ input: "/other/panel",
468
+ expect: false,
469
+ },
470
+ {
471
+ match: MatchPath{"/{http.request.header.X-Prefix}/*"},
472
+ input: "/admin/panel",
473
+ headers: http.Header{"X-Prefix": []string{"Admin"}},
474
+ expect: true,
475
+ },
476
+ {
477
+ match: MatchPath{"/{http.request.header.X-Prefix}/*"},
478
+ input: "/ADMIN/panel",
479
+ headers: http.Header{"X-Prefix": []string{"Admin"}},
480
+ expect: true,
481
+ },
482
+ {
483
+ match: MatchPath{"/{http.request.header.X-Prefix}%2f*"},
484
+ input: "/admin%2fpanel",
485
+ headers: http.Header{"X-Prefix": []string{"Admin"}},
486
+ expect: true,
487
+ },
488
} {
489
err := tc.match.Provision(caddy.Context{})
490
if err == nil && tc.provisionErr {
@@ -470,10 +501,11 @@ func TestPathMatcher(t *testing.T) {
501
if err != nil {
502
t.Fatalf("Test %d (%v): Invalid request URI (should be rejected by Go's HTTP server): %v", i, tc.input, err)
503
}
473
- req := &http.Request{URL: u}
504
+ req := &http.Request{URL: u, Header: tc.headers}
505
repl := caddy.NewReplacer()
506
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
507
req = req.WithContext(ctx)
508
+ addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
509
510
actual, err := tc.match.MatchWithError(req)
511
if err != nil {