From 73643ea736ca7c6a9ef32dbf78b38fbcdf5e92b2 Mon Sep 17 00:00:00 2001 From: Matt Holt Date: Wed, 1 Apr 2020 10:58:29 -0600 Subject: caddyhttp: 'not' matcher now accepts multiple matcher sets and OR's them (#3208) See https://caddy.community/t/v2-matcher-or-in-not/7355/ --- modules/caddyhttp/matchers.go | 95 +++++++++-------- modules/caddyhttp/matchers_test.go | 113 +++++++++++++++++++++ .../caddyhttp/reverseproxy/fastcgi/caddyfile.go | 6 +- 3 files changed, 172 insertions(+), 42 deletions(-) diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go index 2bcdc85..19f6bf2 100644 --- a/modules/caddyhttp/matchers.go +++ b/modules/caddyhttp/matchers.go @@ -99,13 +99,15 @@ type ( cidrs []*net.IPNet } - // MatchNot matches requests by negating its matchers' results. - // To use, simply specify a set of matchers like you normally would; - // the only difference is that their result will be negated. + // MatchNot matches requests by negating the results of its matcher + // sets. A single "not" matcher takes one or more matcher sets. Each + // matcher set is OR'ed; in other words, if any matcher set returns + // true, the final result of the "not" matcher is false. Individual + // matchers within a set work the same (i.e. different matchers in + // the same set are AND'ed). MatchNot struct { - MatchersRaw caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"` - - Matchers MatcherSet `json:"-"` + MatcherSetsRaw []caddy.ModuleMap `json:"-" caddy:"namespace=http.matchers"` + MatcherSets []MatcherSet `json:"-"` } ) @@ -538,25 +540,16 @@ func (MatchNot) CaddyModule() caddy.ModuleInfo { } } -// UnmarshalJSON unmarshals data into m's unexported map field. -// This is done because we cannot embed the map directly into -// the struct, but we need a struct because we need another -// field just for the provisioned modules. -func (m *MatchNot) UnmarshalJSON(data []byte) error { - return json.Unmarshal(data, &m.MatchersRaw) -} - -// MarshalJSON marshals m's matchers. -func (m MatchNot) MarshalJSON() ([]byte, error) { - return json.Marshal(m.MatchersRaw) -} - // UnmarshalCaddyfile implements caddyfile.Unmarshaler. func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { // first, unmarshal each matcher in the set from its tokens - - matcherMap := make(map[string]RequestMatcher) + type matcherPair struct { + raw caddy.ModuleMap + decoded MatcherSet + } for d.Next() { + var mp matcherPair + matcherMap := make(map[string]RequestMatcher) for d.NextBlock(0) { matcherName := d.Val() mod, err := caddy.GetModule("http.matchers." + matcherName) @@ -572,42 +565,64 @@ func (m *MatchNot) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { return err } rm := unm.(RequestMatcher) - m.Matchers = append(m.Matchers, rm) matcherMap[matcherName] = rm + mp.decoded = append(mp.decoded, rm) } - } - // we should now be functional, but we also need - // to be able to marshal as JSON, otherwise config - // adaptation won't work properly - m.MatchersRaw = make(caddy.ModuleMap) - for name, matchers := range matcherMap { - jsonBytes, err := json.Marshal(matchers) - if err != nil { - return fmt.Errorf("marshaling matcher %s: %v", name, err) + // we should now have a functional 'not' matcher, but we also + // need to be able to marshal as JSON, otherwise config + // adaptation will be missing the matchers! + mp.raw = make(caddy.ModuleMap) + for name, matcher := range matcherMap { + jsonBytes, err := json.Marshal(matcher) + if err != nil { + return fmt.Errorf("marshaling %T matcher: %v", matcher, err) + } + mp.raw[name] = jsonBytes } - m.MatchersRaw[name] = jsonBytes + m.MatcherSetsRaw = append(m.MatcherSetsRaw, mp.raw) } - return nil } +// UnmarshalJSON satisfies json.Unmarshaler. It puts the JSON +// bytes directly into m's MatcherSetsRaw field. +func (m *MatchNot) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.MatcherSetsRaw) +} + +// MarshalJSON satisfies json.Marshaler by marshaling +// m's raw matcher sets. +func (m MatchNot) MarshalJSON() ([]byte, error) { + return json.Marshal(m.MatcherSetsRaw) +} + // Provision loads the matcher modules to be negated. func (m *MatchNot) Provision(ctx caddy.Context) error { - mods, err := ctx.LoadModule(m, "MatchersRaw") + matcherSets, err := ctx.LoadModule(m, "MatcherSetsRaw") if err != nil { - return fmt.Errorf("loading matchers: %v", err) + return fmt.Errorf("loading matcher sets: %v", err) } - for _, modIface := range mods.(map[string]interface{}) { - m.Matchers = append(m.Matchers, modIface.(RequestMatcher)) + for _, modMap := range matcherSets.([]map[string]interface{}) { + var ms MatcherSet + for _, modIface := range modMap { + ms = append(ms, modIface.(RequestMatcher)) + } + m.MatcherSets = append(m.MatcherSets, ms) } return nil } -// Match returns true if r matches m. Since this matcher negates the -// embedded matchers, false is returned if any of its matchers match. +// Match returns true if r matches m. Since this matcher negates +// the embedded matchers, false is returned if any of its matcher +// sets return true. func (m MatchNot) Match(r *http.Request) bool { - return !m.Matchers.Match(r) + for _, ms := range m.MatcherSets { + if ms.Match(r) { + return false + } + } + return true } // CaddyModule returns the Caddy module information. diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go index 1e50776..021bb98 100644 --- a/modules/caddyhttp/matchers_test.go +++ b/modules/caddyhttp/matchers_test.go @@ -823,6 +823,119 @@ func TestResponseMatcher(t *testing.T) { } } +func TestNotMatcher(t *testing.T) { + for i, tc := range []struct { + host, path string + match MatchNot + expect bool + }{ + { + host: "example.com", path: "/", + match: MatchNot{}, + expect: true, + }, + { + host: "example.com", path: "/foo", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/foo"}, + }, + }, + }, + expect: false, + }, + { + host: "example.com", path: "/bar", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/foo"}, + }, + }, + }, + expect: true, + }, + { + host: "example.com", path: "/bar", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/foo"}, + }, + { + MatchHost{"example.com"}, + }, + }, + }, + expect: false, + }, + { + host: "example.com", path: "/bar", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/bar"}, + }, + { + MatchHost{"example.com"}, + }, + }, + }, + expect: false, + }, + { + host: "example.com", path: "/foo", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/bar"}, + }, + { + MatchHost{"sub.example.com"}, + }, + }, + }, + expect: true, + }, + { + host: "example.com", path: "/foo", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/foo"}, + MatchHost{"example.com"}, + }, + }, + }, + expect: false, + }, + { + host: "example.com", path: "/foo", + match: MatchNot{ + MatcherSets: []MatcherSet{ + { + MatchPath{"/bar"}, + MatchHost{"example.com"}, + }, + }, + }, + expect: true, + }, + } { + req := &http.Request{Host: tc.host, URL: &url.URL{Path: tc.path}} + repl := caddy.NewReplacer() + ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl) + req = req.WithContext(ctx) + + actual := tc.match.Match(req) + if actual != tc.expect { + t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path) + continue + } + } +} + func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) { req := &http.Request{Host: "localhost"} repl := caddy.NewReplacer() diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go index 2eb1f3d..fdf9818 100644 --- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go +++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go @@ -128,8 +128,10 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error TryFiles: []string{"{http.request.uri.path}/index.php"}, }), "not": h.JSON(caddyhttp.MatchNot{ - MatchersRaw: caddy.ModuleMap{ - "path": h.JSON(caddyhttp.MatchPath{"*/"}), + MatcherSetsRaw: []caddy.ModuleMap{ + { + "path": h.JSON(caddyhttp.MatchPath{"*/"}), + }, }, }), } -- cgit v1.2.3