Support not in rules definition

This commit is contained in:
Julien Salleyron 2021-05-31 18:58:05 +02:00 committed by GitHub
parent b1fd3b8fc7
commit dd04c432e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 142 additions and 3 deletions

View file

@ -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. 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" !!! 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. The rule is evaluated "before" any middleware has the opportunity to work, and "before" the request is forwarded to the service.

View file

@ -7,6 +7,11 @@ import (
"github.com/vulcand/predicate" "github.com/vulcand/predicate"
) )
const (
and = "and"
or = "or"
)
type treeBuilder func() *tree type treeBuilder func() *tree
// ParseDomains extract domains from rule. // ParseDomains extract domains from rule.
@ -60,7 +65,7 @@ func lower(slice []string) []string {
func parseDomain(tree *tree) []string { func parseDomain(tree *tree) []string {
switch tree.matcher { switch tree.matcher {
case "and", "or": case and, or:
return append(parseDomain(tree.ruleLeft), parseDomain(tree.ruleRight)...) return append(parseDomain(tree.ruleLeft), parseDomain(tree.ruleRight)...)
case "Host", "HostSNI": case "Host", "HostSNI":
return tree.value return tree.value
@ -72,7 +77,7 @@ func parseDomain(tree *tree) []string {
func andFunc(left, right treeBuilder) treeBuilder { func andFunc(left, right treeBuilder) treeBuilder {
return func() *tree { return func() *tree {
return &tree{ return &tree{
matcher: "and", matcher: and,
ruleLeft: left(), ruleLeft: left(),
ruleRight: right(), ruleRight: right(),
} }
@ -82,13 +87,36 @@ func andFunc(left, right treeBuilder) treeBuilder {
func orFunc(left, right treeBuilder) treeBuilder { func orFunc(left, right treeBuilder) treeBuilder {
return func() *tree { return func() *tree {
return &tree{ return &tree{
matcher: "or", matcher: or,
ruleLeft: left(), ruleLeft: left(),
ruleRight: right(), 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) { func newParser() (predicate.Parser, error) {
parserFuncs := make(map[string]interface{}) parserFuncs := make(map[string]interface{})
@ -112,6 +140,7 @@ func newParser() (predicate.Parser, error) {
Operators: predicate.Operators{ Operators: predicate.Operators{
AND: andFunc, AND: andFunc,
OR: orFunc, OR: orFunc,
NOT: notFunc,
}, },
Functions: parserFuncs, Functions: parserFuncs,
}) })

View file

@ -72,6 +72,7 @@ func (r *Router) AddRoute(rule string, priority int, handler http.Handler) error
type tree struct { type tree struct {
matcher string matcher string
not bool
value []string value []string
ruleLeft *tree ruleLeft *tree
ruleRight *tree ruleRight *tree
@ -215,10 +216,27 @@ func addRuleOnRouter(router *mux.Router, rule *tree) error {
return err return err
} }
if rule.not {
return not(funcs[rule.matcher])(router.NewRoute(), rule.value...)
}
return 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 { func addRuleOnRoute(route *mux.Route, rule *tree) error {
switch rule.matcher { switch rule.matcher {
case "and": case "and":
@ -243,6 +261,9 @@ func addRuleOnRoute(route *mux.Route, rule *tree) error {
return err return err
} }
if rule.not {
return not(funcs[rule.matcher])(route, rule.value...)
}
return funcs[rule.matcher](route, rule.value...) return funcs[rule.matcher](route, rule.value...)
} }
} }

View file

@ -435,10 +435,95 @@ func Test_addRoute(t *testing.T) {
rule: `Host("tchouk") && Path("", "/titi")`, rule: `Host("tchouk") && Path("", "/titi")`,
expectedError: true, 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 { for _, test := range testCases {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()