diff --git a/internal/grpcutil/regex.go b/internal/grpcutil/regex.go new file mode 100644 index 00000000000..2810a8ba2fd --- /dev/null +++ b/internal/grpcutil/regex.go @@ -0,0 +1,28 @@ +/* + * + * Copyright 2021 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package grpcutil + +import "regexp" + +// FullMatchWithRegex returns whether the full string matches the regex provided. +func FullMatchWithRegex(re *regexp.Regexp, string string) bool { + re.Longest() + rem := re.FindString(string) + return len(rem) == len(string) +} diff --git a/internal/grpcutil/regex_test.go b/internal/grpcutil/regex_test.go new file mode 100644 index 00000000000..1b2299858da --- /dev/null +++ b/internal/grpcutil/regex_test.go @@ -0,0 +1,60 @@ +/* + * + * Copyright 2021 gRPC authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package grpcutil + +import ( + "regexp" + "testing" +) + +func TestFullMatchWithRegex(t *testing.T) { + tests := []struct { + name string + regexStr string + string string + want bool + }{ + { + name: "not match because only partial", + regexStr: "^a+$", + string: "ab", + want: false, + }, + { + name: "match because fully match", + regexStr: "^a+$", + string: "aa", + want: true, + }, + { + name: "longest", + regexStr: "a(|b)", + string: "ab", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hrm := regexp.MustCompile(tt.regexStr) + if got := FullMatchWithRegex(hrm, tt.string); got != tt.want { + t.Errorf("match() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/xds/matcher/matcher_header.go b/internal/xds/matcher/matcher_header.go index 35a22adadcf..c3944373cd7 100644 --- a/internal/xds/matcher/matcher_header.go +++ b/internal/xds/matcher/matcher_header.go @@ -24,6 +24,7 @@ import ( "strconv" "strings" + "google.golang.org/grpc/internal/grpcutil" "google.golang.org/grpc/metadata" ) @@ -91,7 +92,7 @@ func (hrm *HeaderRegexMatcher) Match(md metadata.MD) bool { if !ok { return false } - return hrm.re.MatchString(v) + return grpcutil.FullMatchWithRegex(hrm.re, v) } func (hrm *HeaderRegexMatcher) String() string { diff --git a/internal/xds/matcher/matcher_header_test.go b/internal/xds/matcher/matcher_header_test.go index 9a0d51300d0..7e78065212c 100644 --- a/internal/xds/matcher/matcher_header_test.go +++ b/internal/xds/matcher/matcher_header_test.go @@ -107,6 +107,20 @@ func TestHeaderRegexMatcherMatch(t *testing.T) { md: metadata.Pairs("th", "abc"), want: false, }, + { + name: "no match because only part of value matches with regex", + key: "header", + regexStr: "^a+$", + md: metadata.Pairs("header", "ab"), + want: false, + }, + { + name: "match because full value matches with regex", + key: "header", + regexStr: "^a+$", + md: metadata.Pairs("header", "aa"), + want: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/xds/matcher/string_matcher.go b/internal/xds/matcher/string_matcher.go index d7df6a1e2b4..c138f78735b 100644 --- a/internal/xds/matcher/string_matcher.go +++ b/internal/xds/matcher/string_matcher.go @@ -27,6 +27,7 @@ import ( "strings" v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + "google.golang.org/grpc/internal/grpcutil" ) // StringMatcher contains match criteria for matching a string, and is an @@ -58,7 +59,7 @@ func (sm StringMatcher) Match(input string) bool { case sm.suffixMatch != nil: return strings.HasSuffix(input, *sm.suffixMatch) case sm.regexMatch != nil: - return sm.regexMatch.MatchString(input) + return grpcutil.FullMatchWithRegex(sm.regexMatch, input) case sm.containsMatch != nil: return strings.Contains(input, *sm.containsMatch) } diff --git a/internal/xds/matcher/string_matcher_test.go b/internal/xds/matcher/string_matcher_test.go index 389963b94e9..9528b57e44a 100644 --- a/internal/xds/matcher/string_matcher_test.go +++ b/internal/xds/matcher/string_matcher_test.go @@ -266,6 +266,12 @@ func TestMatch(t *testing.T) { input: "goodregex", wantMatch: true, }, + { + desc: "regex match failure because only part match", + matcher: regexMatcher, + input: "goodregexa", + wantMatch: false, + }, { desc: "regex match failure", matcher: regexMatcher, diff --git a/xds/internal/xdsclient/matcher_path.go b/xds/internal/xdsclient/matcher_path.go index a00c6954ef5..2ca0e4bbcc4 100644 --- a/xds/internal/xdsclient/matcher_path.go +++ b/xds/internal/xdsclient/matcher_path.go @@ -21,6 +21,8 @@ package xdsclient import ( "regexp" "strings" + + "google.golang.org/grpc/internal/grpcutil" ) type pathMatcher interface { @@ -93,7 +95,7 @@ func newPathRegexMatcher(re *regexp.Regexp) *pathRegexMatcher { } func (prm *pathRegexMatcher) match(path string) bool { - return prm.re.MatchString(path) + return grpcutil.FullMatchWithRegex(prm.re, path) } func (prm *pathRegexMatcher) String() string { diff --git a/xds/internal/xdsclient/matcher_path_test.go b/xds/internal/xdsclient/matcher_path_test.go index a211034a60d..003d6db72e2 100644 --- a/xds/internal/xdsclient/matcher_path_test.go +++ b/xds/internal/xdsclient/matcher_path_test.go @@ -80,6 +80,8 @@ func TestPathRegexMatcherMatch(t *testing.T) { }{ {name: "match", regexPath: "^/s+/m.*$", path: "/sss/me", want: true}, {name: "not match", regexPath: "^/s+/m*$", path: "/sss/b", want: false}, + {name: "no match because only part of path matches with regex", regexPath: "^a+$", path: "ab", want: false}, + {name: "match because full path matches with regex", regexPath: "^a+$", path: "aa", want: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {