rewrite: scope keyed query replace to its named key (#7818)

* rewrite: scope keyed query replace to its named key

* rewrite: cover keyed search_regexp query replace in test

* rewrite: provision query replace test via Provision path
pull/7825/head
alhuda 2026-06-15 20:31:05 +05:30 committed by GitHub
parent 39c9a85f80
commit ae9bc028e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 69 additions and 7 deletions

View File

@ -606,13 +606,15 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
continue
}
for fieldName, vals := range query {
for i := range vals {
if replaceParam.re != nil {
query[fieldName][i] = replaceParam.re.ReplaceAllString(query[fieldName][i], replace)
} else {
query[fieldName][i] = strings.ReplaceAll(query[fieldName][i], search, replace)
}
vals, ok := query[key]
if !ok {
continue
}
for i := range vals {
if replaceParam.re != nil {
query[key][i] = replaceParam.re.ReplaceAllString(query[key][i], replace)
} else {
query[key][i] = strings.ReplaceAll(query[key][i], search, replace)
}
}
}

View File

@ -483,6 +483,66 @@ func TestQueryOpsRenameNoOpCases(t *testing.T) {
}
}
func TestQueryOpsReplaceScopedToKey(t *testing.T) {
repl := caddy.NewReplacer()
for i, tc := range []struct {
input *http.Request
expect map[string][]string
ops *queryOps
}{
{
// a keyed replace must only touch the named key, even when
// other keys hold the same value
ops: &queryOps{
Replace: []*queryOpsReplacement{{Key: "a", Search: "foo", Replace: "bar"}},
},
input: newRequest(t, "GET", "/?a=foo&b=foo"),
expect: map[string][]string{"a": {"bar"}, "b": {"foo"}},
},
{
// "*" still replaces across every key
ops: &queryOps{
Replace: []*queryOpsReplacement{{Key: "*", Search: "foo", Replace: "bar"}},
},
input: newRequest(t, "GET", "/?a=foo&b=foo"),
expect: map[string][]string{"a": {"bar"}, "b": {"bar"}},
},
{
// a keyed replace against a missing key is a no-op
ops: &queryOps{
Replace: []*queryOpsReplacement{{Key: "missing", Search: "foo", Replace: "bar"}},
},
input: newRequest(t, "GET", "/?a=foo&b=foo"),
expect: map[string][]string{"a": {"foo"}, "b": {"foo"}},
},
{
// the regexp branch must also stay scoped to the named key
ops: &queryOps{
Replace: []*queryOpsReplacement{{Key: "a", SearchRegexp: "f.o", Replace: "bar"}},
},
input: newRequest(t, "GET", "/?a=foo&b=foo"),
expect: map[string][]string{"a": {"bar"}, "b": {"foo"}},
},
} {
repl.Set("http.request.uri", tc.input.RequestURI)
repl.Set("http.request.uri.path", tc.input.URL.Path)
repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
for _, rep := range tc.ops.Replace {
if err := rep.Provision(caddy.Context{}); err != nil {
t.Fatal(err)
}
}
tc.ops.do(tc.input, repl)
if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
}
}
}
func newRequest(t *testing.T, method, uri string) *http.Request {
req, err := http.NewRequest(method, uri, nil)
if err != nil {