@marcel91200 / caddy Mirror · read-only / commits / 2147da70
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

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 {