Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 12, 15:45 UTC
rewrite: Implement `uri query` operations (#6120)
* 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
Aziz Rmadi committed
Mar 6, 2024 at 09:08 UTC
69290d232dde7306ddaa20172f03cb8860ea6adb
3 files changed
+226
-1
caddytest/integration/caddyfile_test.go
+91
@@ -497,6 +497,97 @@ func TestUriReplace(t *testing.T) {
497
tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D")
498
}
499
500
+func TestUriOps(t *testing.T) {
501
+ tester := caddytest.NewTester(t)
502
+
503
+ tester.InitServer(`
504
+ {
505
+ admin localhost:2999
506
+ http_port 9080
507
+ }
508
+ :9080
509
+ uri query +foo bar
510
+ uri query -baz
511
+ uri query taz test
512
+ uri query key=value example
513
+ uri query changethis>changed
514
+
515
+ respond "{query}"`, "caddyfile")
516
+
517
+ 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")
518
+}
519
+
520
+func TestSetThenAddQueryParams(t *testing.T) {
521
+ tester := caddytest.NewTester(t)
522
+
523
+ tester.InitServer(`
524
+ {
525
+ admin localhost:2999
526
+ http_port 9080
527
+ }
528
+ :9080
529
+ uri query foo bar
530
+ uri query +foo baz
531
+
532
+ respond "{query}"`, "caddyfile")
533
+
534
+ tester.AssertGetResponse("http://localhost:9080/endpoint", 200, "foo=bar&foo=baz")
535
+}
536
+
537
+func TestSetThenDeleteParams(t *testing.T) {
538
+ tester := caddytest.NewTester(t)
539
+
540
+ tester.InitServer(`
541
+ {
542
+ admin localhost:2999
543
+ http_port 9080
544
+ }
545
+ :9080
546
+ uri query bar foo{query.foo}
547
+ uri query -foo
548
+
549
+ respond "{query}"`, "caddyfile")
550
+
551
+ tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=foobar")
552
+}
553
+
554
+func TestRenameAndOtherOps(t *testing.T) {
555
+ tester := caddytest.NewTester(t)
556
+
557
+ tester.InitServer(`
558
+ {
559
+ admin localhost:2999
560
+ http_port 9080
561
+ }
562
+ :9080
563
+ uri query foo>bar
564
+ uri query bar taz
565
+ uri query +bar baz
566
+
567
+ respond "{query}"`, "caddyfile")
568
+
569
+ tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar", 200, "bar=taz&bar=baz")
570
+}
571
+
572
+func TestUriOpsBlock(t *testing.T) {
573
+ tester := caddytest.NewTester(t)
574
+
575
+ tester.InitServer(`
576
+ {
577
+ admin localhost:2999
578
+ http_port 9080
579
+ }
580
+ :9080
581
+ uri query {
582
+ +foo bar
583
+ -baz
584
+ taz test
585
+ }
586
+ respond "{query}"`, "caddyfile")
587
+
588
+ tester.AssertGetResponse("http://localhost:9080/endpoint?foo=bar0&baz=buz&taz=nottest", 200, "foo=bar0&foo=bar&taz=test")
589
+}
590
+
591
func TestHandleErrorSimpleCodes(t *testing.T) {
592
tester := caddytest.NewTester(t)
593
tester.InitServer(`{
modules/caddyhttp/rewrite/caddyfile.go
+59
-1
@@ -98,7 +98,7 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
98
h.Next() // consume directive name
99
100
args := h.RemainingArgs()
101
- if len(args) < 2 {
101
+ if len(args) < 1 {
102
return nil, h.ArgErr()
103
}
104
@@ -158,12 +158,70 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
158
Replace: replace,
159
})
160
161
+ case "query":
162
+ if len(args) > 4 {
163
+ return nil, h.ArgErr()
164
+ }
165
+ rewr.Query = &queryOps{}
166
+ var hasArgs bool
167
+ if len(args) > 1 {
168
+ hasArgs = true
169
+ err := applyQueryOps(h, rewr.Query, args[1:])
170
+ if err != nil {
171
+ return nil, err
172
+ }
173
+ }
174
+
175
+ for h.NextBlock(0) {
176
+ if hasArgs {
177
+ return nil, h.Err("Cannot specify uri query rewrites in both argument and block")
178
+ }
179
+ queryArgs := []string{h.Val()}
180
+ queryArgs = append(queryArgs, h.RemainingArgs()...)
181
+ err := applyQueryOps(h, rewr.Query, queryArgs)
182
+ if err != nil {
183
+ return nil, err
184
+ }
185
+ }
186
+
187
default:
188
return nil, h.Errf("unrecognized URI manipulation '%s'", args[0])
189
}
190
return rewr, nil
191
}
192
193
+func applyQueryOps(h httpcaddyfile.Helper, qo *queryOps, args []string) error {
194
+ key := args[0]
195
+ switch {
196
+ case strings.HasPrefix(key, "-"):
197
+ if len(args) != 1 {
198
+ return h.ArgErr()
199
+ }
200
+ qo.Delete = append(qo.Delete, strings.TrimLeft(key, "-"))
201
+
202
+ case strings.HasPrefix(key, "+"):
203
+ if len(args) != 2 {
204
+ return h.ArgErr()
205
+ }
206
+ param := strings.TrimLeft(key, "+")
207
+ qo.Add = append(qo.Add, queryOpsArguments{Key: param, Val: args[1]})
208
+
209
+ case strings.Contains(key, ">"):
210
+ if len(args) != 1 {
211
+ return h.ArgErr()
212
+ }
213
+ renameValKey := strings.Split(key, ">")
214
+ qo.Rename = append(qo.Rename, queryOpsArguments{Key: renameValKey[0], Val: renameValKey[1]})
215
+
216
+ default:
217
+ if len(args) != 2 {
218
+ return h.ArgErr()
219
+ }
220
+ qo.Set = append(qo.Set, queryOpsArguments{Key: key, Val: args[1]})
221
+ }
222
+ return nil
223
+}
224
+
225
// parseCaddyfileHandlePath parses the handle_path directive. Syntax:
226
//
227
// handle_path [<matcher>] {
modules/caddyhttp/rewrite/rewrite.go
+76
@@ -89,6 +89,9 @@ type Rewrite struct {
89
// Performs regular expression replacements on the URI path.
90
PathRegexp []*regexReplacer `json:"path_regexp,omitempty"`
91
92
+ // Mutates the query string of the URI.
93
+ Query *queryOps `json:"query,omitempty"`
94
+
95
logger *zap.Logger
96
}
97
@@ -269,6 +272,11 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
272
rep.do(r, repl)
273
}
274
275
+ // apply query operations
276
+ if rewr.Query != nil {
277
+ rewr.Query.do(r, repl)
278
+ }
279
+
280
// update the encoded copy of the URI
281
r.RequestURI = r.URL.RequestURI()
282
@@ -470,5 +478,73 @@ func changePath(req *http.Request, newVal func(pathOrRawPath string) string) {
478
}
479
}
480
481
+// queryOps describes the operations to perform on query keys: add, set, rename and delete.
482
+type queryOps struct {
483
+ // Renames a query key from Key to Val, without affecting the value.
484
+ Rename []queryOpsArguments `json:"rename,omitempty"`
485
+
486
+ // Sets query parameters; overwrites a query key with the given value.
487
+ Set []queryOpsArguments `json:"set,omitempty"`
488
+
489
+ // Adds query parameters; does not overwrite an existing query field,
490
+ // and only appends an additional value for that key if any already exist.
491
+ Add []queryOpsArguments `json:"add,omitempty"`
492
+
493
+ // Deletes a given query key by name.
494
+ Delete []string `json:"delete,omitempty"`
495
+}
496
+
497
+func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
498
+ query := r.URL.Query()
499
+
500
+ for _, renameParam := range q.Rename {
501
+ key := repl.ReplaceAll(renameParam.Key, "")
502
+ val := repl.ReplaceAll(renameParam.Val, "")
503
+ if key == "" || val == "" {
504
+ continue
505
+ }
506
+ query[val] = query[key]
507
+ delete(query, key)
508
+ }
509
+
510
+ for _, setParam := range q.Set {
511
+ key := repl.ReplaceAll(setParam.Key, "")
512
+ if key == "" {
513
+ continue
514
+ }
515
+ val := repl.ReplaceAll(setParam.Val, "")
516
+ query[key] = []string{val}
517
+ }
518
+
519
+ for _, addParam := range q.Add {
520
+ key := repl.ReplaceAll(addParam.Key, "")
521
+ if key == "" {
522
+ continue
523
+ }
524
+ val := repl.ReplaceAll(addParam.Val, "")
525
+ query[key] = append(query[key], val)
526
+ }
527
+
528
+ for _, deleteParam := range q.Delete {
529
+ param := repl.ReplaceAll(deleteParam, "")
530
+ if param == "" {
531
+ continue
532
+ }
533
+ delete(query, param)
534
+ }
535
+
536
+ r.URL.RawQuery = query.Encode()
537
+}
538
+
539
+type queryOpsArguments struct {
540
+ // A key in the query string. Note that query string keys may appear multiple times.
541
+ Key string `json:"key,omitempty"`
542
+
543
+ // The value for the given operation; for add and set, this is
544
+ // simply the value of the query, and for rename this is the
545
+ // query key to rename to.
546
+ Val string `json:"val,omitempty"`
547
+}
548
+
549
// Interface guard
550
var _ caddyhttp.MiddlewareHandler = (*Rewrite)(nil)