Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rewrite: Implement uri query operations #6120

Merged
91 changes: 91 additions & 0 deletions caddytest/integration/caddyfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,97 @@ func TestUriReplace(t *testing.T) {
tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D")
}

func TestUriOps(t *testing.T) {
tester := caddytest.NewTester(t)

tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
:9080
uri query +foo bar
uri query -baz
uri query taz test
uri query key=value example
uri query changethis>changed

respond "{query}"`, "caddyfile")

tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest&changethis=val", 200, "changed=val&foo=bar0&foo=bar&key%3Dvalue=example&taz=test")
}

func TestSetThenAddQueryParams(t *testing.T) {
tester := caddytest.NewTester(t)

tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
:9080
uri query foo bar
uri query +foo baz

respond "{query}"`, "caddyfile")

tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz")
}

func TestSetThenDeleteParams(t *testing.T) {
tester := caddytest.NewTester(t)

tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
:9080
uri query bar foo{query.foo}
uri query -foo

respond "{query}"`, "caddyfile")

tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar")
}

func TestRenameAndOtherOps(t *testing.T) {
tester := caddytest.NewTester(t)

tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
:9080
uri query foo>bar
uri query bar taz
uri query +bar baz

respond "{query}"`, "caddyfile")

tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=taz&bar=baz")
}

func TestUriOpsBlock(t *testing.T) {
tester := caddytest.NewTester(t)

tester.InitServer(`
{
admin localhost:2999
http_port 9080
}
:9080
uri query {
+foo bar
-baz
taz test
}
respond "{query}"`, "caddyfile")

tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test")
}

func TestHandleErrorSimpleCodes(t *testing.T) {
tester := caddytest.NewTester(t)
tester.InitServer(`{
Expand Down
60 changes: 59 additions & 1 deletion modules/caddyhttp/rewrite/caddyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
h.Next() // consume directive name

args := h.RemainingArgs()
if len(args) < 2 {
if len(args) < 1 {
return nil, h.ArgErr()
}

Expand Down Expand Up @@ -158,12 +158,70 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
Replace: replace,
})

case "query":
if len(args) > 4 {
return nil, h.ArgErr()
}
rewr.Query = &queryOps{}
var hasArgs bool
if len(args) > 1 {
hasArgs = true
err := applyQueryOps(h, rewr.Query, args[1:])
if err != nil {
return nil, err
}
}

for h.NextBlock(0) {
if hasArgs {
return nil, h.Err("Cannot specify uri query rewrites in both argument and block")
}
queryArgs := []string{h.Val()}
queryArgs = append(queryArgs, h.RemainingArgs()...)
err := applyQueryOps(h, rewr.Query, queryArgs)
if err != nil {
return nil, err
}
}

default:
return nil, h.Errf("unrecognized URI manipulation '%s'", args[0])
}
return rewr, nil
}

func applyQueryOps(h httpcaddyfile.Helper, qo *queryOps, args []string) error {
key := args[0]
switch {
case strings.HasPrefix(key, "-"):
if len(args) != 1 {
return h.ArgErr()
}
qo.Delete = append(qo.Delete, strings.TrimLeft(key, "-"))

case strings.HasPrefix(key, "+"):
if len(args) != 2 {
return h.ArgErr()
}
param := strings.TrimLeft(key, "+")
qo.Add = append(qo.Add, queryOpsArguments{Key: param, Val: args[1]})

case strings.Contains(key, ">"):
if len(args) != 1 {
return h.ArgErr()
}
renameValKey := strings.Split(key, ">")
qo.Rename = append(qo.Rename, queryOpsArguments{Key: renameValKey[0], Val: renameValKey[1]})

default:
if len(args) != 2 {
return h.ArgErr()
}
qo.Set = append(qo.Set, queryOpsArguments{Key: key, Val: args[1]})
}
return nil
}

// parseCaddyfileHandlePath parses the handle_path directive. Syntax:
//
// handle_path [<matcher>] {
Expand Down
76 changes: 76 additions & 0 deletions modules/caddyhttp/rewrite/rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ type Rewrite struct {
// Performs regular expression replacements on the URI path.
PathRegexp []*regexReplacer `json:"path_regexp,omitempty"`

// Mutates the query string of the URI.
Query *queryOps `json:"query,omitempty"`

logger *zap.Logger
}

Expand Down Expand Up @@ -269,6 +272,11 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
rep.do(r, repl)
}

// apply query operations
if rewr.Query != nil {
rewr.Query.do(r, repl)
}

// update the encoded copy of the URI
r.RequestURI = r.URL.RequestURI()

Expand Down Expand Up @@ -470,5 +478,73 @@ func changePath(req *http.Request, newVal func(pathOrRawPath string) string) {
}
}

// queryOps describes the operations to perform on query keys: add, set, rename and delete.
type queryOps struct {
// Renames a query key from Key to Val, without affecting the value.
Rename []queryOpsArguments `json:"rename,omitempty"`

// Sets query parameters; overwrites a query key with the given value.
Set []queryOpsArguments `json:"set,omitempty"`

// Adds query parameters; does not overwrite an existing query field,
// and only appends an additional value for that key if any already exist.
Add []queryOpsArguments `json:"add,omitempty"`

// Deletes a given query key by name.
Delete []string `json:"delete,omitempty"`
}

func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
query := r.URL.Query()

for _, renameParam := range q.Rename {
key := repl.ReplaceAll(renameParam.Key, "")
val := repl.ReplaceAll(renameParam.Val, "")
if key == "" || val == "" {
continue
}
query[val] = query[key]
delete(query, key)
}

for _, setParam := range q.Set {
key := repl.ReplaceAll(setParam.Key, "")
if key == "" {
continue
}
val := repl.ReplaceAll(setParam.Val, "")
query[key] = []string{val}
}

for _, addParam := range q.Add {
key := repl.ReplaceAll(addParam.Key, "")
francislavoie marked this conversation as resolved.
Show resolved Hide resolved
if key == "" {
continue
}
val := repl.ReplaceAll(addParam.Val, "")
query[key] = append(query[key], val)
}

for _, deleteParam := range q.Delete {
param := repl.ReplaceAll(deleteParam, "")
francislavoie marked this conversation as resolved.
Show resolved Hide resolved
if param == "" {
continue
}
delete(query, param)
}

r.URL.RawQuery = query.Encode()
}

type queryOpsArguments struct {
// A key in the query string. Note that query string keys may appear multiple times.
Key string `json:"key,omitempty"`
armadi1809 marked this conversation as resolved.
Show resolved Hide resolved

// The value for the given operation; for add and set, this is
// simply the value of the query, and for rename this is the
// query key to rename to.
Val string `json:"val,omitempty"`
armadi1809 marked this conversation as resolved.
Show resolved Hide resolved
}

// Interface guard
var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil)
Loading