From cb0eb91f2b162265e88f39201b09106fd7a38de5 Mon Sep 17 00:00:00 2001 From: Darien Raymond Date: Tue, 26 Jun 2018 21:57:41 +0200 Subject: [PATCH] strmatcher --- app/router/condition.go | 92 +++++++----------------------- app/router/condition_test.go | 40 ------------- common/strmatcher/matchers.go | 36 ++++++++++++ common/strmatcher/matchers_test.go | 68 ++++++++++++++++++++++ common/strmatcher/strmatcher.go | 89 +++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 111 deletions(-) create mode 100644 common/strmatcher/matchers.go create mode 100644 common/strmatcher/matchers_test.go create mode 100644 common/strmatcher/strmatcher.go diff --git a/app/router/condition.go b/app/router/condition.go index 08e37a2c..15577b11 100644 --- a/app/router/condition.go +++ b/app/router/condition.go @@ -2,13 +2,12 @@ package router import ( "context" - "regexp" - "strings" "sync" "time" "v2ray.com/core/common/net" "v2ray.com/core/common/protocol" + "v2ray.com/core/common/strmatcher" "v2ray.com/core/proxy" ) @@ -73,44 +72,41 @@ type timedResult struct { type CachableDomainMatcher struct { sync.Mutex - matchers []domainMatcher + matchers *strmatcher.MatcherGroup cache map[string]timedResult lastScan time.Time } func NewCachableDomainMatcher() *CachableDomainMatcher { return &CachableDomainMatcher{ - matchers: make([]domainMatcher, 0, 64), + matchers: strmatcher.NewMatcherGroup(), cache: make(map[string]timedResult, 512), } } +var matcherTypeMap = map[Domain_Type]strmatcher.Type{ + Domain_Plain: strmatcher.Substr, + Domain_Regex: strmatcher.Regex, + Domain_Domain: strmatcher.Domain, +} + func (m *CachableDomainMatcher) Add(domain *Domain) error { - switch domain.Type { - case Domain_Plain: - m.matchers = append(m.matchers, NewPlainDomainMatcher(domain.Value)) - case Domain_Regex: - rm, err := NewRegexpDomainMatcher(domain.Value) - if err != nil { - return err - } - m.matchers = append(m.matchers, rm) - case Domain_Domain: - m.matchers = append(m.matchers, NewSubDomainMatcher(domain.Value)) - default: - return newError("unknown domain type: ", domain.Type).AtWarning() + matcherType, f := matcherTypeMap[domain.Type] + if !f { + return newError("unsupported domain type", domain.Type) } + + matcher, err := matcherType.New(domain.Value) + if err != nil { + return newError("failed to create domain matcher").Base(err) + } + + m.matchers.Add(matcher) return nil } func (m *CachableDomainMatcher) applyInternal(domain string) bool { - for _, matcher := range m.matchers { - if matcher.Apply(domain) { - return true - } - } - - return false + return m.matchers.Match(domain) > 0 } type cacheResult int @@ -139,7 +135,7 @@ func (m *CachableDomainMatcher) findInCache(domain string) cacheResult { } func (m *CachableDomainMatcher) ApplyDomain(domain string) bool { - if len(m.matchers) < 64 { + if m.matchers.Size() < 64 { return m.applyInternal(domain) } @@ -190,52 +186,6 @@ func (m *CachableDomainMatcher) Apply(ctx context.Context) bool { return m.ApplyDomain(dest.Address.Domain()) } -type domainMatcher interface { - Apply(domain string) bool -} - -type PlainDomainMatcher string - -func NewPlainDomainMatcher(pattern string) PlainDomainMatcher { - return PlainDomainMatcher(pattern) -} - -func (v PlainDomainMatcher) Apply(domain string) bool { - return strings.Contains(domain, string(v)) -} - -type RegexpDomainMatcher struct { - pattern *regexp.Regexp -} - -func NewRegexpDomainMatcher(pattern string) (*RegexpDomainMatcher, error) { - r, err := regexp.Compile(pattern) - if err != nil { - return nil, err - } - return &RegexpDomainMatcher{ - pattern: r, - }, nil -} - -func (v *RegexpDomainMatcher) Apply(domain string) bool { - return v.pattern.MatchString(strings.ToLower(domain)) -} - -type SubDomainMatcher string - -func NewSubDomainMatcher(p string) SubDomainMatcher { - return SubDomainMatcher(p) -} - -func (m SubDomainMatcher) Apply(domain string) bool { - pattern := string(m) - if !strings.HasSuffix(domain, pattern) { - return false - } - return len(domain) == len(pattern) || domain[len(domain)-len(pattern)-1] == '.' -} - type CIDRMatcher struct { cidr *net.IPNet onSource bool diff --git a/app/router/condition_test.go b/app/router/condition_test.go index e4a09753..317aac2d 100644 --- a/app/router/condition_test.go +++ b/app/router/condition_test.go @@ -20,46 +20,6 @@ import ( "v2ray.com/ext/sysio" ) -func TestSubDomainMatcher(t *testing.T) { - assert := With(t) - - cases := []struct { - pattern string - input string - output bool - }{ - { - pattern: "v2ray.com", - input: "www.v2ray.com", - output: true, - }, - { - pattern: "v2ray.com", - input: "v2ray.com", - output: true, - }, - { - pattern: "v2ray.com", - input: "www.v3ray.com", - output: false, - }, - { - pattern: "v2ray.com", - input: "2ray.com", - output: false, - }, - { - pattern: "v2ray.com", - input: "xv2ray.com", - output: false, - }, - } - for _, test := range cases { - matcher := NewSubDomainMatcher(test.pattern) - assert(matcher.Apply(test.input) == test.output, IsTrue) - } -} - func TestRoutingRule(t *testing.T) { assert := With(t) diff --git a/common/strmatcher/matchers.go b/common/strmatcher/matchers.go new file mode 100644 index 00000000..73d9e7b8 --- /dev/null +++ b/common/strmatcher/matchers.go @@ -0,0 +1,36 @@ +package strmatcher + +import ( + "regexp" + "strings" +) + +type fullMatcher string + +func (m fullMatcher) Match(s string) bool { + return string(m) == s +} + +type substrMatcher string + +func (m substrMatcher) Match(s string) bool { + return strings.Contains(s, string(m)) +} + +type domainMatcher string + +func (m domainMatcher) Match(s string) bool { + pattern := string(m) + if !strings.HasSuffix(s, pattern) { + return false + } + return len(s) == len(pattern) || s[len(s)-len(pattern)-1] == '.' +} + +type regexMatcher struct { + pattern *regexp.Regexp +} + +func (m *regexMatcher) Match(s string) bool { + return m.pattern.MatchString(s) +} diff --git a/common/strmatcher/matchers_test.go b/common/strmatcher/matchers_test.go new file mode 100644 index 00000000..d2524c3f --- /dev/null +++ b/common/strmatcher/matchers_test.go @@ -0,0 +1,68 @@ +package strmatcher_test + +import ( + "testing" + + "v2ray.com/core/common" + . "v2ray.com/core/common/strmatcher" + ast "v2ray.com/ext/assert" +) + +func TestMatcher(t *testing.T) { + assert := ast.With(t) + + cases := []struct { + pattern string + mType Type + input string + output bool + }{ + { + pattern: "v2ray.com", + mType: Domain, + input: "www.v2ray.com", + output: true, + }, + { + pattern: "v2ray.com", + mType: Domain, + input: "v2ray.com", + output: true, + }, + { + pattern: "v2ray.com", + mType: Domain, + input: "www.v3ray.com", + output: false, + }, + { + pattern: "v2ray.com", + mType: Domain, + input: "2ray.com", + output: false, + }, + { + pattern: "v2ray.com", + mType: Domain, + input: "xv2ray.com", + output: false, + }, + { + pattern: "v2ray.com", + mType: Full, + input: "v2ray.com", + output: true, + }, + { + pattern: "v2ray.com", + mType: Full, + input: "xv2ray.com", + output: false, + }, + } + for _, test := range cases { + matcher, err := test.mType.New(test.pattern) + common.Must(err) + assert(matcher.Match(test.input) == test.output, ast.IsTrue) + } +} diff --git a/common/strmatcher/strmatcher.go b/common/strmatcher/strmatcher.go new file mode 100644 index 00000000..80e7d34e --- /dev/null +++ b/common/strmatcher/strmatcher.go @@ -0,0 +1,89 @@ +package strmatcher + +import "regexp" + +type Matcher interface { + Match(string) bool +} + +type Type byte + +const ( + Full Type = iota + Substr + Domain + Regex +) + +func (t Type) New(pattern string) (Matcher, error) { + switch t { + case Full: + return fullMatcher(pattern), nil + case Substr: + return substrMatcher(pattern), nil + case Domain: + return domainMatcher(pattern), nil + case Regex: + r, err := regexp.Compile(pattern) + if err != nil { + return nil, err + } + return ®exMatcher{ + pattern: r, + }, nil + default: + panic("Unknown type") + } +} + +type matcherEntry struct { + m Matcher + id uint32 +} + +type MatcherGroup struct { + count uint32 + fullMatchers map[string]uint32 + otherMatchers []matcherEntry +} + +func NewMatcherGroup() *MatcherGroup { + return &MatcherGroup{ + count: 1, + fullMatchers: make(map[string]uint32), + } +} + +func (g *MatcherGroup) Add(m Matcher) uint32 { + c := g.count + g.count++ + + if fm, ok := m.(fullMatcher); ok { + g.fullMatchers[string(fm)] = c + } else { + g.otherMatchers = append(g.otherMatchers, matcherEntry{ + m: m, + id: c, + }) + } + + return c +} + +func (g *MatcherGroup) Match(pattern string) uint32 { + if c, f := g.fullMatchers[pattern]; f { + return c + } + + for _, e := range g.otherMatchers { + if e.m.Match(pattern) { + return e.id + } + } + + return 0 +} + +func (g *MatcherGroup) Size() uint32 { + return g.count +}