Skip to content

Commit

Permalink
rewrite: Implement uri query operations (#6120)
Browse files Browse the repository at this point in the history
* Implemented basic uri query operations

* Added support for query operations block

* Applied Replacer on all query keys and values

* Implemented rename query key opration

* Rewrite struct: Changed QueryOperations field to Query and comments cleanup

* Cleaned up comments, changed the order of operations and added more tests

* Changed order of fields in queryOps struct to match the operations order
  • Loading branch information
armadi1809 committed Mar 6, 2024
1 parent 277472d commit 69290d2
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 1 deletion.
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, "")
if key == "" {
continue
}
val := repl.ReplaceAll(addParam.Val, "")
query[key] = append(query[key], val)
}

for _, deleteParam := range q.Delete {
param := repl.ReplaceAll(deleteParam, "")
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"`

// 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"`
}

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

0 comments on commit 69290d2

Please sign in to comment.