From 9bdd6caa0bcced5caf30872548700277f2db1877 Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Sat, 8 Feb 2020 22:26:31 +0300 Subject: 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 --- modules/caddyhttp/matchers_test.go | 86 ++++++++++++++++++++++++++++++++++++++ modules/caddyhttp/vars.go | 69 ++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) (limited to 'modules') diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 06f137e..58df171 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -545,6 +545,92 @@ func TestHeaderREMatcher(t *testing.T) { } } +func TestVarREMatcher(t *testing.T) { + for i, tc := range []struct { + desc string + match MatchVarsRE + input VarsMiddleware + expect bool + expectRepl map[string]string + }{ + { + desc: "match static value within var set by the VarsMiddleware succeeds", + match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "foo"}}, + input: VarsMiddleware{"Var1": "here is foo val"}, + expect: true, + }, + { + desc: "value set by VarsMiddleware not satisfying regexp matcher fails to match", + match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "$foo^"}}, + input: VarsMiddleware{"Var1": "foobar"}, + expect: false, + }, + { + desc: "successfully matched value is captured and its placeholder is added to replacer", + match: MatchVarsRE{"Var1": &MatchRegexp{Pattern: "^foo(.*)$", Name: "name"}}, + input: VarsMiddleware{"Var1": "foobar"}, + expect: true, + expectRepl: map[string]string{"name.1": "bar"}, + }, + { + desc: "matching against a value of standard variables succeeds", + match: MatchVarsRE{"{http.request.method}": &MatchRegexp{Pattern: "^G.[tT]$"}}, + input: VarsMiddleware{}, + expect: true, + }, + { + desc: "matching agaist value of var set by the VarsMiddleware and referenced by its placeholder succeeds", + match: MatchVarsRE{"{http.vars.Var1}": &MatchRegexp{Pattern: "[vV]ar[0-9]"}}, + input: VarsMiddleware{"Var1": "var1Value"}, + expect: true, + }, + } { + tc := tc // capture range value + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + // compile the regexp and validate its name + err := tc.match.Provision(caddy.Context{}) + if err != nil { + t.Errorf("Test %d %v: Provisioning: %v", i, tc.match, err) + return + } + err = tc.match.Validate() + if err != nil { + t.Errorf("Test %d %v: Validating: %v", i, tc.match, err) + return + } + + // set up the fake request and its Replacer + req := &http.Request{URL: new(url.URL), Method: http.MethodGet} + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + ctx = context.WithValue(ctx, VarsCtxKey, make(map[string]interface{})) + req = req.WithContext(ctx) + + addHTTPVarsToReplacer(repl, req, httptest.NewRecorder()) + + tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler) + + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'", + i, tc.match, tc.expect, actual, tc.input) + return + } + + for key, expectVal := range tc.expectRepl { + placeholder := fmt.Sprintf("{http.regexp.%s}", key) + actualVal := repl.ReplaceAll(placeholder, "") + if actualVal != expectVal { + t.Errorf("Test %d [%v]: Expected placeholder {http.regexp.%s} to be '%s' but got '%s'", + i, tc.match, key, expectVal, actualVal) + return + } + } + }) + } +} + func TestResponseMatcher(t *testing.T) { for i, tc := range []struct { require ResponseMatcher diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go index 1208d9c..9561d46 100644 --- a/modules/caddyhttp/vars.go +++ b/modules/caddyhttp/vars.go @@ -25,6 +25,7 @@ import ( func init() { caddy.RegisterModule(VarsMiddleware{}) caddy.RegisterModule(VarsMatcher{}) + caddy.RegisterModule(MatchVarsRE{}) } // VarsMiddleware is an HTTP middleware which sets variables @@ -88,6 +89,74 @@ func (m VarsMatcher) Match(r *http.Request) bool { return true } +// MatchVarsRE matches the value of the context variables by a given regular expression. +// +// Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}` +// where `name` is the regular expression's name, and `capture_group` is either +// the named or positional capture group from the expression itself. If no name +// is given, then the placeholder omits the name: `{http.regexp.capture_group}` +// (potentially leading to collisions). +type MatchVarsRE map[string]*MatchRegexp + +// CaddyModule returns the Caddy module information. +func (MatchVarsRE) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "http.matchers.vars_regexp", + New: func() caddy.Module { return new(MatchVarsRE) }, + } +} + +// Provision compiles m's regular expressions. +func (m MatchVarsRE) Provision(ctx caddy.Context) error { + for _, rm := range m { + err := rm.Provision(ctx) + if err != nil { + return err + } + } + return nil +} + +// Match returns true if r matches m. +func (m MatchVarsRE) Match(r *http.Request) bool { + vars := r.Context().Value(VarsCtxKey).(map[string]interface{}) + repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer) + for k, rm := range m { + var varStr string + switch vv := vars[k].(type) { + case string: + varStr = vv + case fmt.Stringer: + varStr = vv.String() + case error: + varStr = vv.Error() + default: + varStr = fmt.Sprintf("%v", vv) + } + valExpanded := repl.ReplaceAll(varStr, "") + if match := rm.Match(valExpanded, repl); match { + return match + } + + replacedVal := repl.ReplaceAll(k, "") + if match := rm.Match(replacedVal, repl); match { + return match + } + } + return false +} + +// Validate validates m's regular expressions. +func (m MatchVarsRE) Validate() error { + for _, rm := range m { + err := rm.Validate() + if err != nil { + return err + } + } + return nil +} + // GetVar gets a value out of the context's variable table by key. // If the key does not exist, the return value will be nil. func GetVar(ctx context.Context, key string) interface{} { -- cgit v1.2.3