Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 14, 05:15 UTC
v2: Implement RegExp Vars Matcher (#2997)
* implement regexp var matcher * use subtests pattern for tests * be more consistent with naming: MatchVarRE -> MatchVarsRE, var_regexp -> vars_regexp
Mohammed Al Sahaf committed
Feb 8, 2020 at 22:26 UTC
9bdd6caa0bcced5caf30872548700277f2db1877
2 files changed
+155
modules/caddyhttp/matchers_test.go
+86
@@ -545,6 +545,92 @@ func TestHeaderREMatcher(t *testing.T) {
545
}
546
}
547
548
+func TestVarREMatcher(t *testing.T) {
549
+ for i, tc := range []struct {
550
+ desc string
551
+ match MatchVarsRE
552
+ input VarsMiddleware
553
+ expect bool
554
+ expectRepl map[string]string
555
+ }{
556
+ {
557
+ desc: "match static value within var set by the VarsMiddleware succeeds",
558
+ match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "foo"}},
559
+ input: VarsMiddleware{"Var1": "here is foo val"},
560
+ expect: true,
561
+ },
562
+ {
563
+ desc: "value set by VarsMiddleware not satisfying regexp matcher fails to match",
564
+ match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "$foo^"}},
565
+ input: VarsMiddleware{"Var1": "foobar"},
566
+ expect: false,
567
+ },
568
+ {
569
+ desc: "successfully matched value is captured and its placeholder is added to replacer",
570
+ match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}},
571
+ input: VarsMiddleware{"Var1": "foobar"},
572
+ expect: true,
573
+ expectRepl: map[string]string{"name.1": "bar"},
574
+ },
575
+ {
576
+ desc: "matching against a value of standard variables succeeds",
577
+ match: MatchVarsRE{"{http.request.method}": &MatchRegexp{Pattern: "^G.[tT]$"}},
578
+ input: VarsMiddleware{},
579
+ expect: true,
580
+ },
581
+ {
582
+ desc: "matching agaist value of var set by the VarsMiddleware and referenced by its placeholder succeeds",
583
+ match: MatchVarsRE{"{http.vars.Var1}": &MatchRegexp{Pattern: "[vV]ar[0-9]"}},
584
+ input: VarsMiddleware{"Var1": "var1Value"},
585
+ expect: true,
586
+ },
587
+ } {
588
+ tc := tc // capture range value
589
+ t.Run(tc.desc, func(t *testing.T) {
590
+ t.Parallel()
591
+ // compile the regexp and validate its name
592
+ err := tc.match.Provision(caddy.Context{})
593
+ if err != nil {
594
+ t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err)
595
+ return
596
+ }
597
+ err = tc.match.Validate()
598
+ if err != nil {
599
+ t.Errorf("Test %d %v: Validating: %v", i, tc.match, err)
600
+ return
601
+ }
602
+
603
+ // set up the fake request and its Replacer
604
+ req := &http.Request{URL: new(url.URL), Method: http.MethodGet}
605
+ repl := caddy.NewReplacer()
606
+ ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
607
+ ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]interface{}))
608
+ req = req.WithContext(ctx)
609
+
610
+ addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
611
+
612
+ tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler)
613
+
614
+ actual := tc.match.Match(req)
615
+ if actual != tc.expect {
616
+ t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
617
+ i, tc.match, tc.expect, actual, tc.input)
618
+ return
619
+ }
620
+
621
+ for key, expectVal := range tc.expectRepl {
622
+ placeholder := fmt.Sprintf("{http.regexp.%s}", key)
623
+ actualVal := repl.ReplaceAll(placeholder, "<empty>")
624
+ if actualVal != expectVal {
625
+ t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'",
626
+ i, tc.match, key, expectVal, actualVal)
627
+ return
628
+ }
629
+ }
630
+ })
631
+ }
632
+}
633
+
634
func TestResponseMatcher(t *testing.T) {
635
for i, tc := range []struct {
636
require ResponseMatcher
modules/caddyhttp/vars.go
+69
@@ -25,6 +25,7 @@ import (
25
func init() {
26
caddy.RegisterModule(VarsMiddleware{})
27
caddy.RegisterModule(VarsMatcher{})
28
+ caddy.RegisterModule(MatchVarsRE{})
29
}
30
31
// VarsMiddleware is an HTTP middleware which sets variables
@@ -88,6 +89,74 @@ func (m VarsMatcher) Match(r *http.Request) bool {
89
return true
90
}
91
92
+// MatchVarsRE matches the value of the context variables by a given regular expression.
93
+//
94
+// Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}`
95
+// where `name` is the regular expression's name, and `capture_group` is either
96
+// the named or positional capture group from the expression itself. If no name
97
+// is given, then the placeholder omits the name: `{http.regexp.capture_group}`
98
+// (potentially leading to collisions).
99
+type MatchVarsRE map[string]*MatchRegexp
100
+
101
+// CaddyModule returns the Caddy module information.
102
+func (MatchVarsRE) CaddyModule() caddy.ModuleInfo {
103
+ return caddy.ModuleInfo{
104
+ ID: "http.matchers.vars_regexp",
105
+ New: func() caddy.Module { return new(MatchVarsRE) },
106
+ }
107
+}
108
+
109
+// Provision compiles m's regular expressions.
110
+func (m MatchVarsRE) Provision(ctx caddy.Context) error {
111
+ for _, rm := range m {
112
+ err := rm.Provision(ctx)
113
+ if err != nil {
114
+ return err
115
+ }
116
+ }
117
+ return nil
118
+}
119
+
120
+// Match returns true if r matches m.
121
+func (m MatchVarsRE) Match(r *http.Request) bool {
122
+ vars := r.Context().Value(VarsCtxKey).(map[string]interface{})
123
+ repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
124
+ for k, rm := range m {
125
+ var varStr string
126
+ switch vv := vars[k].(type) {
127
+ case string:
128
+ varStr = vv
129
+ case fmt.Stringer:
130
+ varStr = vv.String()
131
+ case error:
132
+ varStr = vv.Error()
133
+ default:
134
+ varStr = fmt.Sprintf("%v", vv)
135
+ }
136
+ valExpanded := repl.ReplaceAll(varStr, "")
137
+ if match := rm.Match(valExpanded, repl); match {
138
+ return match
139
+ }
140
+
141
+ replacedVal := repl.ReplaceAll(k, "")
142
+ if match := rm.Match(replacedVal, repl); match {
143
+ return match
144
+ }
145
+ }
146
+ return false
147
+}
148
+
149
+// Validate validates m's regular expressions.
150
+func (m MatchVarsRE) Validate() error {
151
+ for _, rm := range m {
152
+ err := rm.Validate()
153
+ if err != nil {
154
+ return err
155
+ }
156
+ }
157
+ return nil
158
+}
159
+
160
// GetVar gets a value out of the context's variable table by key.
161
// If the key does not exist, the return value will be nil.
162
func GetVar(ctx context.Context, key string) interface{} {