mirror of
https://github.com/alibaba/higress.git
synced 2026-05-11 14:27:27 +08:00
feat: Add ext-auth plugin support for authentication blacklists/whitelists (#1694)
This commit is contained in:
79
plugins/wasm-go/extensions/ext-auth/expr/match_rules.go
Normal file
79
plugins/wasm-go/extensions/ext-auth/expr/match_rules.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
ModeWhitelist = "whitelist"
|
||||
ModeBlacklist = "blacklist"
|
||||
)
|
||||
|
||||
type MatchRules struct {
|
||||
Mode string
|
||||
RuleList []Rule
|
||||
}
|
||||
|
||||
type Rule struct {
|
||||
Domain string
|
||||
Path Matcher
|
||||
}
|
||||
|
||||
func MatchRulesDefaults() MatchRules {
|
||||
return MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{},
|
||||
}
|
||||
}
|
||||
|
||||
// IsAllowedByMode checks if the given domain and path are allowed based on the configuration mode.
|
||||
func (config *MatchRules) IsAllowedByMode(domain, path string) bool {
|
||||
switch config.Mode {
|
||||
case ModeWhitelist:
|
||||
for _, rule := range config.RuleList {
|
||||
if rule.matchDomainAndPath(domain, path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
case ModeBlacklist:
|
||||
for _, rule := range config.RuleList {
|
||||
if rule.matchDomainAndPath(domain, path) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// matchDomainAndPath checks if the given domain and path match the rule.
|
||||
// If rule.Domain is empty, it only checks rule.Path.
|
||||
// If rule.Path is empty, it only checks rule.Domain.
|
||||
// If both are empty, it returns false.
|
||||
func (rule *Rule) matchDomainAndPath(domain, path string) bool {
|
||||
if rule.Domain == "" && rule.Path == nil {
|
||||
return false
|
||||
}
|
||||
domainMatch := rule.Domain == "" || matchDomain(domain, rule.Domain)
|
||||
pathMatch := rule.Path == nil || rule.Path.Match(path)
|
||||
return domainMatch && pathMatch
|
||||
}
|
||||
|
||||
// matchDomain checks if the given domain matches the pattern.
|
||||
func matchDomain(domain string, pattern string) bool {
|
||||
// Convert wildcard pattern to regex pattern
|
||||
regexPattern := convertWildcardToRegex(pattern)
|
||||
matched, _ := regexp.MatchString(regexPattern, domain)
|
||||
return matched
|
||||
}
|
||||
|
||||
// convertWildcardToRegex converts a wildcard pattern to a regex pattern.
|
||||
func convertWildcardToRegex(pattern string) string {
|
||||
pattern = regexp.QuoteMeta(pattern)
|
||||
pattern = "^" + strings.ReplaceAll(pattern, "\\*", ".*") + "$"
|
||||
return pattern
|
||||
}
|
||||
259
plugins/wasm-go/extensions/ext-auth/expr/match_rules_test.go
Normal file
259
plugins/wasm-go/extensions/ext-auth/expr/match_rules_test.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsAllowedByMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config MatchRules
|
||||
domain string
|
||||
path string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Whitelist mode, rule matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist mode, rule does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/bar",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, rule matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, rule does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/bar",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Domain matches, Path is empty",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{Domain: "example.com", Path: nil},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Domain is empty, Path matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Both Domain and Path are empty",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{Domain: "", Path: nil},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid mode",
|
||||
config: MatchRules{
|
||||
Mode: "invalid",
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Whitelist mode, generic domain matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "sub.example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Whitelist mode, generic domain does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeWhitelist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, generic domain matches",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "sub.example.com",
|
||||
path: "/foo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Blacklist mode, generic domain does not match",
|
||||
config: MatchRules{
|
||||
Mode: ModeBlacklist,
|
||||
RuleList: []Rule{
|
||||
{
|
||||
Domain: "*.example.com",
|
||||
Path: func() Matcher {
|
||||
pathMatcher, err := newStringExactMatcher("/foo", true)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create Matcher: %v", err)
|
||||
}
|
||||
return pathMatcher
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
domain: "example.com",
|
||||
path: "/foo",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.config.IsAllowedByMode(tt.domain, tt.path)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,17 @@ import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
regexp "github.com/wasilibs/go-re2"
|
||||
)
|
||||
|
||||
const (
|
||||
matchPatternExact string = "exact"
|
||||
matchPatternPrefix string = "prefix"
|
||||
matchPatternSuffix string = "suffix"
|
||||
matchPatternContains string = "contains"
|
||||
matchPatternRegex string = "regex"
|
||||
MatchPatternExact string = "exact"
|
||||
MatchPatternPrefix string = "prefix"
|
||||
MatchPatternSuffix string = "suffix"
|
||||
MatchPatternContains string = "contains"
|
||||
MatchPatternRegex string = "regex"
|
||||
|
||||
matchIgnoreCase string = "ignore_case"
|
||||
MatchIgnoreCase string = "ignore_case"
|
||||
)
|
||||
|
||||
type Matcher interface {
|
||||
@@ -78,78 +77,16 @@ func (m *stringRegexMatcher) Match(s string) bool {
|
||||
return m.regex.MatchString(s)
|
||||
}
|
||||
|
||||
type repeatedStringMatcher struct {
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
func (rsm *repeatedStringMatcher) Match(s string) bool {
|
||||
for _, m := range rsm.matchers {
|
||||
if m.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
|
||||
builtMatchers := make([]Matcher, len(matchers))
|
||||
|
||||
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, matcherType MatcherConstructor) (Matcher, error) {
|
||||
result := json.Get(targetKey)
|
||||
if result.Exists() && result.String() != "" {
|
||||
target := result.String()
|
||||
return matcherType(target, ignoreCase)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i, item := range matchers {
|
||||
var matcher Matcher
|
||||
var err error
|
||||
|
||||
// If allIgnoreCase is true, it takes precedence over any user configuration,
|
||||
// forcing case-insensitive matching regardless of individual item settings.
|
||||
ignoreCase := allIgnoreCase
|
||||
if !allIgnoreCase {
|
||||
ignoreCaseResult := item.Get(matchIgnoreCase)
|
||||
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
|
||||
ignoreCase = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, matcherType := range []struct {
|
||||
key string
|
||||
creator MatcherConstructor
|
||||
}{
|
||||
{matchPatternExact, newStringExactMatcher},
|
||||
{matchPatternPrefix, newStringPrefixMatcher},
|
||||
{matchPatternSuffix, newStringSuffixMatcher},
|
||||
{matchPatternContains, newStringContainsMatcher},
|
||||
{matchPatternRegex, newStringRegexMatcher},
|
||||
} {
|
||||
if matcher, err = createMatcher(item, matcherType.key, ignoreCase, matcherType.creator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matcher == nil {
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
builtMatchers[i] = matcher
|
||||
|
||||
}
|
||||
|
||||
return &repeatedStringMatcher{
|
||||
matchers: builtMatchers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MatcherConstructor func(string, bool) (Matcher, error)
|
||||
|
||||
var matcherConstructors = map[string]MatcherConstructor{
|
||||
MatchPatternExact: newStringExactMatcher,
|
||||
MatchPatternPrefix: newStringPrefixMatcher,
|
||||
MatchPatternSuffix: newStringSuffixMatcher,
|
||||
MatchPatternContains: newStringContainsMatcher,
|
||||
MatchPatternRegex: newStringRegexMatcher,
|
||||
}
|
||||
|
||||
func newStringExactMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
if ignoreCase {
|
||||
target = strings.ToLower(target)
|
||||
@@ -189,14 +126,11 @@ func newStringRegexMatcher(target string, ignoreCase bool) (Matcher, error) {
|
||||
return &stringRegexMatcher{regex: re}, nil
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, true)
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, false)
|
||||
}
|
||||
|
||||
func BuildStringMatcher(matcher gjson.Result) (Matcher, error) {
|
||||
return BuildRepeatedStringMatcher([]gjson.Result{matcher})
|
||||
func BuildStringMatcher(matchType, target string, ignoreCase bool) (Matcher, error) {
|
||||
for constructorType, constructor := range matcherConstructors {
|
||||
if constructorType == matchType {
|
||||
return constructor(target, ignoreCase)
|
||||
}
|
||||
}
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
@@ -4,78 +4,96 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestStringMatcher(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg string
|
||||
matchType string
|
||||
target string
|
||||
ignoreCase bool
|
||||
matched []string
|
||||
mismatched []string
|
||||
}{
|
||||
{
|
||||
name: "exact",
|
||||
cfg: `{"exact": "foo"}`,
|
||||
matchType: MatchPatternExact,
|
||||
target: "foo",
|
||||
matched: []string{"foo"},
|
||||
mismatched: []string{"fo", "fooo"},
|
||||
},
|
||||
{
|
||||
name: "exact, ignore_case",
|
||||
cfg: `{"exact": "foo", "ignore_case": true}`,
|
||||
matched: []string{"Foo", "foo"},
|
||||
name: "exact, ignore_case",
|
||||
matchType: MatchPatternExact,
|
||||
target: "foo",
|
||||
ignoreCase: true,
|
||||
matched: []string{"Foo", "foo"},
|
||||
},
|
||||
{
|
||||
name: "prefix",
|
||||
cfg: `{"prefix": "/p"}`,
|
||||
matchType: MatchPatternPrefix,
|
||||
target: "/p",
|
||||
matched: []string{"/p", "/pa"},
|
||||
mismatched: []string{"/P"},
|
||||
},
|
||||
{
|
||||
name: "prefix, ignore_case",
|
||||
cfg: `{"prefix": "/p", "ignore_case": true}`,
|
||||
matchType: MatchPatternPrefix,
|
||||
target: "/p",
|
||||
ignoreCase: true,
|
||||
matched: []string{"/P", "/p", "/pa", "/Pa"},
|
||||
mismatched: []string{"/"},
|
||||
},
|
||||
{
|
||||
name: "suffix",
|
||||
cfg: `{"suffix": "foo"}`,
|
||||
matchType: MatchPatternSuffix,
|
||||
target: "foo",
|
||||
matched: []string{"foo", "0foo"},
|
||||
mismatched: []string{"fo", "fooo", "aFoo"},
|
||||
},
|
||||
{
|
||||
name: "suffix, ignore_case",
|
||||
cfg: `{"suffix": "foo", "ignore_case": true}`,
|
||||
matchType: MatchPatternSuffix,
|
||||
target: "foo",
|
||||
ignoreCase: true,
|
||||
matched: []string{"aFoo", "foo"},
|
||||
mismatched: []string{"fo", "fooo"},
|
||||
},
|
||||
{
|
||||
name: "contains",
|
||||
cfg: `{"contains": "foo"}`,
|
||||
matchType: MatchPatternContains,
|
||||
target: "foo",
|
||||
matched: []string{"foo", "0foo", "fooo"},
|
||||
mismatched: []string{"fo", "aFoo"},
|
||||
},
|
||||
{
|
||||
name: "contains, ignore_case",
|
||||
cfg: `{"contains": "foo", "ignore_case": true}`,
|
||||
matchType: MatchPatternContains,
|
||||
target: "foo",
|
||||
ignoreCase: true,
|
||||
matched: []string{"aFoo", "foo", "FoO"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
{
|
||||
name: "regex",
|
||||
cfg: `{"regex": "fo{2}"}`,
|
||||
matchType: MatchPatternRegex,
|
||||
target: "fo{2}",
|
||||
matched: []string{"foo", "0foo", "fooo"},
|
||||
mismatched: []string{"aFoo", "fo"},
|
||||
},
|
||||
{
|
||||
name: "regex, ignore_case",
|
||||
cfg: `{"regex": "fo{2}", "ignore_case": true}`,
|
||||
matchType: MatchPatternRegex,
|
||||
target: "fo{2}",
|
||||
ignoreCase: true,
|
||||
matched: []string{"foo", "0foo", "fooo", "aFoo"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
{
|
||||
name: "regex, ignore_case & case insensitive specified in regex",
|
||||
cfg: `{"regex": "(?i)fo{2}", "ignore_case": true}`,
|
||||
matchType: MatchPatternRegex,
|
||||
target: "(?i)fo{2}",
|
||||
ignoreCase: true,
|
||||
matched: []string{"foo", "0foo", "fooo", "aFoo"},
|
||||
mismatched: []string{"fo"},
|
||||
},
|
||||
@@ -83,7 +101,7 @@ func TestStringMatcher(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
built, _ := BuildStringMatcher(gjson.Parse(tt.cfg))
|
||||
built, _ := BuildStringMatcher(tt.matchType, tt.target, tt.ignoreCase)
|
||||
for _, s := range tt.matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
@@ -93,30 +111,3 @@ func TestStringMatcher(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
|
||||
cfgs := []string{
|
||||
`{"exact":"foo"}`,
|
||||
`{"prefix":"pre"}`,
|
||||
`{"regex":"^Cache"}`,
|
||||
}
|
||||
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
|
||||
mismatched := []string{"afoo", "fo"}
|
||||
ms := []gjson.Result{}
|
||||
for _, cfg := range cfgs {
|
||||
ms = append(ms, gjson.Parse(cfg))
|
||||
}
|
||||
built, _ := BuildRepeatedStringMatcherIgnoreCase(ms)
|
||||
for _, s := range matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
for _, s := range mismatched {
|
||||
assert.False(t, built.Match(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassOutRegexCompileErr(t *testing.T) {
|
||||
cfg := `{"regex":"(?!)aa"}`
|
||||
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type repeatedStringMatcher struct {
|
||||
matchers []Matcher
|
||||
}
|
||||
|
||||
func (rsm *repeatedStringMatcher) Match(s string) bool {
|
||||
for _, m := range rsm.matchers {
|
||||
if m.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func buildRepeatedStringMatcher(matchers []gjson.Result, allIgnoreCase bool) (Matcher, error) {
|
||||
builtMatchers := make([]Matcher, len(matchers))
|
||||
|
||||
createMatcher := func(json gjson.Result, targetKey string, ignoreCase bool, constructor MatcherConstructor) (Matcher, error) {
|
||||
result := json.Get(targetKey)
|
||||
if result.Exists() && result.String() != "" {
|
||||
target := result.String()
|
||||
return constructor(target, ignoreCase)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for i, item := range matchers {
|
||||
var matcher Matcher
|
||||
var err error
|
||||
|
||||
// If allIgnoreCase is true, it takes precedence over any user configuration,
|
||||
// forcing case-insensitive matching regardless of individual item settings.
|
||||
ignoreCase := allIgnoreCase
|
||||
if !allIgnoreCase {
|
||||
ignoreCaseResult := item.Get(MatchIgnoreCase)
|
||||
if ignoreCaseResult.Exists() && ignoreCaseResult.Bool() {
|
||||
ignoreCase = true
|
||||
}
|
||||
}
|
||||
|
||||
for key, creator := range matcherConstructors {
|
||||
if matcher, err = createMatcher(item, key, ignoreCase, creator); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if matcher != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matcher == nil {
|
||||
return nil, errors.New("unknown string matcher type")
|
||||
}
|
||||
|
||||
builtMatchers[i] = matcher
|
||||
|
||||
}
|
||||
|
||||
return &repeatedStringMatcher{
|
||||
matchers: builtMatchers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcherIgnoreCase(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, true)
|
||||
}
|
||||
|
||||
func BuildRepeatedStringMatcher(matchers []gjson.Result) (Matcher, error) {
|
||||
return buildRepeatedStringMatcher(matchers, false)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package expr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBuildRepeatedStringMatcherIgnoreCase(t *testing.T) {
|
||||
cfg := `[
|
||||
{"exact":"foo"},
|
||||
{"prefix":"pre"},
|
||||
{"regex":"^Cache"}
|
||||
]`
|
||||
matched := []string{"Foo", "foO", "foo", "PreA", "cache-control", "Cache-Control"}
|
||||
mismatched := []string{"afoo", "fo"}
|
||||
jsonArray := gjson.Parse(cfg).Array()
|
||||
built, err := BuildRepeatedStringMatcherIgnoreCase(jsonArray)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build RepeatedStringMatcher: %v", err)
|
||||
}
|
||||
|
||||
for _, s := range matched {
|
||||
assert.True(t, built.Match(s))
|
||||
}
|
||||
for _, s := range mismatched {
|
||||
assert.False(t, built.Match(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPassOutRegexCompileErr(t *testing.T) {
|
||||
cfg := `{"regex":"(?!)aa"}`
|
||||
_, err := BuildRepeatedStringMatcher([]gjson.Result{gjson.Parse(cfg)})
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user