Просмотр исходного кода

Support wildcard matches in filters

- Support simple ('*' term only) wildcard
  matching for API parameter values in
  traffic rules and tactics.

- Intended to support simple prefix and
  suffix matching.
Rod Hynes 7 лет назад
Родитель
Сommit
7f62309008

+ 2 - 1
psiphon/common/tactics/tactics.go

@@ -273,6 +273,7 @@ type Filter struct {
 	// APIParameters specifies API, e.g. handshake, parameter names and
 	// a list of values, one of which must be specified to match this
 	// filter. Only scalar string API parameters may be filtered.
+	// Values may be patterns containing the '*' wildcard.
 	APIParameters map[string][]string
 
 	// SpeedTestRTTMilliseconds specifies a Range filter field that the
@@ -711,7 +712,7 @@ func (server *Server) getTactics(
 			mismatch := false
 			for name, values := range filteredTactics.Filter.APIParameters {
 				clientValue, err := getStringRequestParam(apiParams, name)
-				if err != nil || !common.Contains(values, clientValue) {
+				if err != nil || !common.ContainsWildcard(values, clientValue) {
 					mismatch = true
 					break
 				}

+ 14 - 0
psiphon/common/utils.go

@@ -33,6 +33,8 @@ import (
 	"runtime"
 	"strings"
 	"time"
+
+	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/wildcard"
 )
 
 const RFC3339Milli = "2006-01-02T15:04:05.000Z07:00"
@@ -48,6 +50,18 @@ func Contains(list []string, target string) bool {
 	return false
 }
 
+// ContainsWildcard returns true if target matches
+// any of the patterns. Patterns may contain the
+// '*' wildcard.
+func ContainsWildcard(patterns []string, target string) bool {
+	for _, pattern := range patterns {
+		if wildcard.Match(pattern, target) {
+			return true
+		}
+	}
+	return false
+}
+
 // ContainsAny returns true if any string in targets
 // is present in the list.
 func ContainsAny(list, targets []string) bool {

+ 122 - 0
psiphon/common/wildcard/wildcard.go

@@ -0,0 +1,122 @@
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+// Package wildcard implements a very simple wildcard matcher which supports
+// only the term '*', which matches any sequence of characters. The match
+// function, WildcardMatch, both parses the pattern and matches the target;
+// there is no compile stage and WildcardMatch can be a drop in replacement
+// anywhere a normal string comparison is done.
+//
+// This package is very similar to and inspired by github.com/ryanuber/go-
+// glob, but with performance optimizations; github.com/gobwas/glob offers a
+// much richer glob syntax and faster performance for cases where a compiled
+// glob state can be maintained.
+//
+// Performance comparison:
+//
+// wildcard:
+//
+// BenchmarkFixedMatch-4               100000000        14.0 ns/op
+// BenchmarkPrefixMatch-4               50000000        26.2 ns/op
+// BenchmarkSuffixMatch-4               50000000        25.8 ns/op
+// BenchmarkMultipleMatch-4             10000000       167 ns/op
+//
+// github.com/ryanuber/go-glob:
+//
+// BenchmarkFixedGoGlob-4              30000000         58.3 ns/op
+// BenchmarkPrefixGoGlob-4              20000000       106 ns/op
+// BenchmarkSuffixGoGlob-4              20000000       105 ns/op
+// BenchmarkMultipleGoGlob-4             5000000       270 ns/op
+//
+// github.com/gobwas/glob with precompile:
+//
+// BenchmarkFixedGlobPrecompile-4       100000000       14.1 ns/op
+// BenchmarkPrefixGlobPrecompile-4      200000000        6.66 ns/op
+// BenchmarkSuffixGlobPrecompile-4      200000000        7.31 ns/op
+// BenchmarkMultipleGlobPrecompile-4    10000000       151 ns/op
+//
+// github.com/gobwas/glob with compile-and-match:
+//
+// BenchmarkFixedGlob-4                   300000      4120 ns/op
+// BenchmarkPrefixGlob-4                 1000000      1502 ns/op
+// BenchmarkSuffixGlob-4                 1000000      1501 ns/op
+// BenchmarkMultipleGlob-4                300000      5203 ns/op
+//
+package wildcard
+
+import (
+	"strings"
+)
+
+func Match(pattern, target string) bool {
+
+	wildcard := "*"
+
+	for n := 0; ; n++ {
+
+		if pattern == wildcard {
+			return true
+		}
+
+		i := strings.Index(pattern, wildcard)
+
+		if n == 0 {
+
+			if i == -1 {
+
+				return pattern == target
+
+			} else if i == 0 {
+
+				pattern = pattern[i+1:]
+
+			} else if i > 0 {
+
+				if !strings.HasPrefix(target, pattern[:i]) {
+					return false
+				}
+				pattern = pattern[i+1:]
+				target = target[i:]
+
+			}
+
+		} else {
+
+			if i == -1 {
+
+				return strings.HasSuffix(target, pattern)
+
+			} else if i == 0 {
+
+				pattern = pattern[i+1:]
+
+			} else if i > 0 {
+
+				j := strings.Index(target, pattern[:i])
+				if j == -1 {
+					return false
+				}
+
+				pattern = pattern[i+1:]
+				target = target[j+i+1:]
+			}
+
+		}
+	}
+}

+ 100 - 0
psiphon/common/wildcard/wildcard_test.go

@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2018, Psiphon Inc.
+ * All rights reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+package wildcard
+
+import (
+	"fmt"
+	"testing"
+)
+
+const target = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
+
+func TestMatch(t *testing.T) {
+
+	testCases := []struct {
+		pattern string
+		target  string
+		match   bool
+	}{
+		{"*", target, true},
+		{target, target, true},
+		{"Lorem*", target, true},
+		{"*aliqua.", target, true},
+		{"*tempor*", target, true},
+		{"*dolor*eiusmod*magna*", target, true},
+		{"Lorem*dolor*eiusmod*magna*", target, true},
+		{"*ipsum*elit*aliqua.", target, true},
+		{"Lorem*dolor*eiusmod*dolore*aliqua.", target, true},
+
+		{"", target, false},
+		{"L-rem*", target, false},
+		{"L-rem**", target, false},
+		{"*aliqua-", target, false},
+		{"*temp-r*", target, false},
+		{"*dolor*ei-smod*magna*", target, false},
+		{"Lorem*dolor*eiu-mod*magna*", target, false},
+		{"*ipsum*eli-*aliqua.", target, false},
+		{"Lorem*dolor*eiusm-d*dolore*aliqua.", target, false},
+
+		{"Lorem**", target, true},
+		{"**aliqua.", target, true},
+		{"**tempor**", target, true},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(fmt.Sprintf("match: %+v", testCase), func(t *testing.T) {
+			if Match(testCase.pattern, testCase.target) != testCase.match {
+				t.Errorf("unexpected result")
+			}
+		})
+	}
+}
+
+func BenchmarkFixedMatch(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		if !Match(target, target) {
+			b.Fatalf("unexpected result")
+		}
+	}
+}
+
+func BenchmarkPrefixMatch(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		if !Match("Lorem*", target) {
+			b.Fatalf("unexpected result")
+		}
+	}
+}
+
+func BenchmarkSuffixMatch(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		if !Match("*aliqua.", target) {
+			b.Fatalf("unexpected result")
+		}
+	}
+}
+
+func BenchmarkMultipleMatch(b *testing.B) {
+	for i := 0; i < b.N; i++ {
+		if !Match("*dolor*eiusmod*magna*", target) {
+			b.Fatalf("unexpected result")
+		}
+	}
+}

+ 2 - 1
psiphon/server/trafficRules.go

@@ -123,6 +123,7 @@ type TrafficRulesFilter struct {
 	// HandshakeParameters specifies handshake API parameter names and
 	// a list of values, one of which must be specified to match this
 	// filter. Only scalar string API parameters may be filtered.
+	// Values may be patterns containing the '*' wildcard.
 	HandshakeParameters map[string][]string
 
 	// AuthorizedAccessTypes specifies a list of access types, at least
@@ -479,7 +480,7 @@ func (set *TrafficRulesSet) GetTrafficRules(
 			mismatch := false
 			for name, values := range filteredRules.Filter.HandshakeParameters {
 				clientValue, err := getStringRequestParam(state.apiParams, name)
-				if err != nil || !common.Contains(values, clientValue) {
+				if err != nil || !common.ContainsWildcard(values, clientValue) {
 					mismatch = true
 					break
 				}