From b0731f7d2af3fa6c11b0b4364fdaef5b23bcffd8 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 10 Nov 2026 18:57:37 -0700 Subject: [PATCH] httpcaddyfile: allocate route groups across named and site routes Share the adaptation group counter with named-route extraction because group satisfaction is request-wide. Cover nested invoke and handle_path while retaining sibling exclusivity. Addresses scan report fb62a11d-5561-49e3-87df-c175ea5a1f4d. Code written by GitHub Copilot (GPT-6.1 Sol, Claude Opus 5.5); reviewed by mholt. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- caddyconfig/httpcaddyfile/httptype.go | 4 +- .../httpcaddyfile/named_groups_test.go | 98 +++++++++++++++++++ 2 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 caddyconfig/httpcaddyfile/named_groups_test.go diff --git a/caddyconfig/httpcaddyfile/httptype.go b/caddyconfig/httpcaddyfile/httptype.go index 43cd1a184..96f85453e 100644 --- a/caddyconfig/httpcaddyfile/httptype.go +++ b/caddyconfig/httpcaddyfile/httptype.go @@ -104,7 +104,7 @@ func (st ServerType) Setup( // with actual identifiers used by Caddy replacer := NewShorthandReplacer() - originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer) + originalServerBlocks, err = st.extractNamedRoutes(originalServerBlocks, options, &warnings, replacer, gc) if err != nil { return nil, warnings, err } @@ -477,11 +477,11 @@ func (ServerType) extractNamedRoutes( options map[string]any, warnings *[]caddyconfig.Warning, replacer ShorthandReplacer, + gc counter, ) ([]serverBlock, error) { namedRoutes := map[string]*caddyhttp.Route{} namedRouteInvokes := map[string][]string{} - gc := counter{new(int)} state := make(map[string]any) // copy the server blocks so we can diff --git a/caddyconfig/httpcaddyfile/named_groups_test.go b/caddyconfig/httpcaddyfile/named_groups_test.go new file mode 100644 index 000000000..018868181 --- /dev/null +++ b/caddyconfig/httpcaddyfile/named_groups_test.go @@ -0,0 +1,98 @@ +package httpcaddyfile_test + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/caddy/v2/modules/caddyhttp" + _ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite" +) + +func TestNamedRouteGroupsAreDistinct(t *testing.T) { + for _, tc := range []struct { + name, named, site string + }{ + { + name: "handle", + named: `&(guard) { + handle /api/admin { + respond "denied" 403 + } + handle { + respond "public" + } +}`, + site: `handle /api/* { + invoke guard + respond "allowed" +}`, + }, + { + name: "handle_path", + named: `&(guard) { + handle /admin { + respond "denied" 403 + } + handle { + respond "public" + } +}`, + site: `handle_path /api/* { + invoke guard + respond "allowed" +}`, + }, + { + name: "nested invoke", + named: `&(inner) { + handle /api/admin { + respond "denied" 403 + } + handle { + respond "public" + } +} +&(guard) { + handle /api/* { + invoke inner + } + handle { + respond "outer" + } +}`, + site: `handle /api/* { + invoke guard + respond "allowed" +}`, + }, + } { + t.Run(tc.name, func(t *testing.T) { + server, handler := adaptedRouteHandler(t, "{\nadmin off\nauto_https off\n}\n"+tc.named+"\n:9080 {\n"+tc.site+` + handle { + respond "fallback" 404 + } +}`) + for _, request := range []struct { + path string + code int + body string + }{ + {"/api/admin", http.StatusForbidden, "denied"}, + {"/api/public", http.StatusOK, "public"}, + {"/other", http.StatusNotFound, "fallback"}, + } { + req := httptest.NewRequest(http.MethodGet, request.path, nil) + req = caddyhttp.PrepareRequest(req, caddy.NewReplacer(), nil, server) + rec := httptest.NewRecorder() + if err := handler.ServeHTTP(rec, req); err != nil { + t.Fatal(err) + } + if rec.Code != request.code || rec.Body.String() != request.body { + t.Errorf("%s: got %d %q, want %d %q", request.path, rec.Code, rec.Body.String(), request.code, request.body) + } + } + }) + } +}