caddyhttp: 'not' matcher now accepts multiple matcher sets and OR's them (#3208)

See https://caddy.community/t/v2-matcher-or-in-not/7355/
This commit is contained in:
Matt Holt 2020-04-01 10:58:29 -06:00 committed by GitHub
parent 809e72792c
commit 73643ea736
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 172 additions and 42 deletions

View file

@ -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.

View file

@ -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()

View file

@ -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{"*/"}),
},
},
}),
}