From dd04c432e9b5a3a224b130ade6cc20e86f20f005 Mon Sep 17 00:00:00 2001 From: Julien Salleyron Date: Mon, 31 May 2021 18:58:05 +0200 Subject: [PATCH] Support not in rules definition --- docs/content/routing/routers/index.md | 4 ++ pkg/rules/parser.go | 35 ++++++++++- pkg/rules/rules.go | 21 +++++++ pkg/rules/rules_test.go | 85 +++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 3 deletions(-) diff --git a/docs/content/routing/routers/index.md b/docs/content/routing/routers/index.md index 914f5008c..8f0b20f1f 100644 --- a/docs/content/routing/routers/index.md +++ b/docs/content/routing/routers/index.md @@ -256,6 +256,10 @@ The table below lists all the available matchers: You can combine multiple matchers using the AND (`&&`) and OR (`||`) operators. You can also use parenthesis. +!!! info "Invert a matcher" + + You can invert a matcher by using the `!` operator. + !!! important "Rule, Middleware, and Services" The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service. diff --git a/pkg/rules/parser.go b/pkg/rules/parser.go index 99b410e52..f1224aaad 100644 --- a/pkg/rules/parser.go +++ b/pkg/rules/parser.go @@ -7,6 +7,11 @@ import ( "github.com/vulcand/predicate" ) +const ( + and = "and" + or = "or" +) + type treeBuilder func() *tree // ParseDomains extract domains from rule. @@ -60,7 +65,7 @@ func lower(slice []string) []string { func parseDomain(tree *tree) []string { switch tree.matcher { - case "and", "or": + case and, or: return append(parseDomain(tree.ruleLeft), parseDomain(tree.ruleRight)...) case "Host", "HostSNI": return tree.value @@ -72,7 +77,7 @@ func parseDomain(tree *tree) []string { func andFunc(left, right treeBuilder) treeBuilder { return func() *tree { return &tree{ - matcher: "and", + matcher: and, ruleLeft: left(), ruleRight: right(), } @@ -82,13 +87,36 @@ func andFunc(left, right treeBuilder) treeBuilder { func orFunc(left, right treeBuilder) treeBuilder { return func() *tree { return &tree{ - matcher: "or", + matcher: or, ruleLeft: left(), ruleRight: right(), } } } +func invert(t *tree) *tree { + switch t.matcher { + case or: + t.matcher = and + t.ruleLeft = invert(t.ruleLeft) + t.ruleRight = invert(t.ruleRight) + case and: + t.matcher = or + t.ruleLeft = invert(t.ruleLeft) + t.ruleRight = invert(t.ruleRight) + default: + t.not = !t.not + } + + return t +} + +func notFunc(elem treeBuilder) treeBuilder { + return func() *tree { + return invert(elem()) + } +} + func newParser() (predicate.Parser, error) { parserFuncs := make(map[string]interface{}) @@ -112,6 +140,7 @@ func newParser() (predicate.Parser, error) { Operators: predicate.Operators{ AND: andFunc, OR: orFunc, + NOT: notFunc, }, Functions: parserFuncs, }) diff --git a/pkg/rules/rules.go b/pkg/rules/rules.go index 08b0ebaae..daacb72ca 100644 --- a/pkg/rules/rules.go +++ b/pkg/rules/rules.go @@ -72,6 +72,7 @@ func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error type tree struct { matcher string + not bool value []string ruleLeft *tree ruleRight *tree @@ -215,10 +216,27 @@ func addRuleOnRouter(router *mux.Router, rule *tree) error { return err } + if rule.not { + return not(funcs[rule.matcher])(router.NewRoute(), rule.value...) + } return funcs[rule.matcher](router.NewRoute(), rule.value...) } } +func not(m func(*mux.Route, ...string) error) func(*mux.Route, ...string) error { + return func(r *mux.Route, v ...string) error { + router := mux.NewRouter() + err := m(router.NewRoute(), v...) + if err != nil { + return err + } + r.MatcherFunc(func(req *http.Request, ma *mux.RouteMatch) bool { + return !router.Match(req, ma) + }) + return nil + } +} + func addRuleOnRoute(route *mux.Route, rule *tree) error { switch rule.matcher { case "and": @@ -243,6 +261,9 @@ func addRuleOnRoute(route *mux.Route, rule *tree) error { return err } + if rule.not { + return not(funcs[rule.matcher])(route, rule.value...) + } return funcs[rule.matcher](route, rule.value...) } } diff --git a/pkg/rules/rules_test.go b/pkg/rules/rules_test.go index a73f78f10..4f7e3b368 100644 --- a/pkg/rules/rules_test.go +++ b/pkg/rules/rules_test.go @@ -435,10 +435,95 @@ func Test_addRoute(t *testing.T) { rule: `Host("tchouk") && Path("", "/titi")`, expectedError: true, }, + { + desc: "Rule with not", + rule: `!Host("tchouk")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://test/powpow": http.StatusOK, + }, + }, + { + desc: "Rule with not on Path", + rule: `!Path("/titi")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/powpow": http.StatusOK, + }, + }, + { + desc: "Rule with not on multiple route with or", + rule: `!(Host("tchouk") || Host("toto"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://toto/powpow": http.StatusNotFound, + "http://test/powpow": http.StatusOK, + }, + }, + { + desc: "Rule with not on multiple route with and", + rule: `!(Host("tchouk") && Path("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/toto": http.StatusOK, + "http://test/titi": http.StatusOK, + }, + }, + { + desc: "Rule with not on multiple route with and and another not", + rule: `!(Host("tchouk") && !Path("/titi"))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://toto/titi": http.StatusOK, + "http://tchouk/toto": http.StatusNotFound, + }, + }, + { + desc: "Rule with not on two rule", + rule: `!Host("tchouk") || !Path("/titi")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/toto": http.StatusOK, + "http://test/titi": http.StatusOK, + }, + }, + { + desc: "Rule case with double not", + rule: `!(!(Host("tchouk") && Pathprefix("/titi")))`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + "http://test/titi": http.StatusNotFound, + }, + }, + { + desc: "Rule case with not domain", + rule: `!Host("tchouk") && Pathprefix("/titi")`, + expected: map[string]int{ + "http://tchouk/titi": http.StatusNotFound, + "http://tchouk/powpow": http.StatusNotFound, + "http://toto/powpow": http.StatusNotFound, + "http://toto/titi": http.StatusOK, + }, + }, + { + desc: "Rule with multiple host AND multiple path AND not", + rule: `!(Host("tchouk","pouet") && Path("/powpow", "/titi"))`, + expected: map[string]int{ + "http://tchouk/toto": http.StatusOK, + "http://tchouk/powpow": http.StatusNotFound, + "http://pouet/powpow": http.StatusNotFound, + "http://tchouk/titi": http.StatusNotFound, + "http://pouet/titi": http.StatusNotFound, + "http://pouet/toto": http.StatusOK, + "http://plopi/a": http.StatusOK, + }, + }, } for _, test := range testCases { test := test + t.Run(test.desc, func(t *testing.T) { t.Parallel()