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

View file

@ -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,
})

View file

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

View file

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