snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
/*
*
* Copyright 2020 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 matcher
import (
"fmt"
"regexp"
"strconv"
"strings"
"google.golang.org/grpc/metadata"
)
// HeaderMatcher is an interface for header matchers. These are
// documented in (EnvoyProxy link here?). These matchers will match on different
// aspects of HTTP header name/value pairs.
type HeaderMatcher interface {
Match(metadata.MD) bool
String() string
}
// valueFromMD retrieves metadata from context. If there are
// multiple values, the values are concatenated with "," (comma and no space).
//
// All header matchers only match against the comma-concatenated string.
func valueFromMD(md metadata.MD, key string) (string, bool) {
vs, ok := md[key]
if !ok {
return "", false
}
return strings.Join(vs, ","), true
}
// HeaderExactMatcher matches on an exact match of the value of the header.
type HeaderExactMatcher struct {
key string
exact string
invert bool
}
// NewHeaderExactMatcher returns a new HeaderExactMatcher.
func NewHeaderExactMatcher(key, exact string, invert bool) *HeaderExactMatcher {
return &HeaderExactMatcher{key: key, exact: exact, invert: invert}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderExactMatcher.
func (hem *HeaderExactMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hem.key)
if !ok {
return false
}
return (v == hem.exact) != hem.invert
}
func (hem *HeaderExactMatcher) String() string {
return fmt.Sprintf("headerExact:%v:%v", hem.key, hem.exact)
}
// HeaderRegexMatcher matches on whether the entire request header value matches
// the regex.
type HeaderRegexMatcher struct {
key string
re *regexp.Regexp
invert bool
}
// NewHeaderRegexMatcher returns a new HeaderRegexMatcher.
func NewHeaderRegexMatcher(key string, re *regexp.Regexp, invert bool) *HeaderRegexMatcher {
return &HeaderRegexMatcher{key: key, re: re, invert: invert}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderRegexMatcher.
func (hrm *HeaderRegexMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hrm.key)
if !ok {
return false
}
return hrm.re.MatchString(v) != hrm.invert
}
func (hrm *HeaderRegexMatcher) String() string {
return fmt.Sprintf("headerRegex:%v:%v", hrm.key, hrm.re.String())
}
// HeaderRangeMatcher matches on whether the request header value is within the
// range. The header value must be an integer in base 10 notation.
type HeaderRangeMatcher struct {
key string
start, end int64 // represents [start, end).
invert bool
}
// NewHeaderRangeMatcher returns a new HeaderRangeMatcher.
func NewHeaderRangeMatcher(key string, start, end int64, invert bool) *HeaderRangeMatcher {
return &HeaderRangeMatcher{key: key, start: start, end: end, invert: invert}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderRangeMatcher.
func (hrm *HeaderRangeMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hrm.key)
if !ok {
return false
}
if i, err := strconv.ParseInt(v, 10, 64); err == nil && i >= hrm.start && i < hrm.end {
return !hrm.invert
}
return hrm.invert
}
func (hrm *HeaderRangeMatcher) String() string {
return fmt.Sprintf("headerRange:%v:[%d,%d)", hrm.key, hrm.start, hrm.end)
}
// HeaderPresentMatcher will match based on whether the header is present in the
// whole request.
type HeaderPresentMatcher struct {
key string
present bool
}
// NewHeaderPresentMatcher returns a new HeaderPresentMatcher.
func NewHeaderPresentMatcher(key string, present bool, invert bool) *HeaderPresentMatcher {
if invert {
present = !present
}
return &HeaderPresentMatcher{key: key, present: present}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderPresentMatcher.
func (hpm *HeaderPresentMatcher) Match(md metadata.MD) bool {
vs, ok := valueFromMD(md, hpm.key)
present := ok && len(vs) > 0 // TODO: Are we sure we need this len(vs) > 0?
return present == hpm.present
}
func (hpm *HeaderPresentMatcher) String() string {
return fmt.Sprintf("headerPresent:%v:%v", hpm.key, hpm.present)
}
// HeaderPrefixMatcher matches on whether the prefix of the header value matches
// the prefix passed into this struct.
type HeaderPrefixMatcher struct {
key string
prefix string
invert bool
}
// NewHeaderPrefixMatcher returns a new HeaderPrefixMatcher.
func NewHeaderPrefixMatcher(key string, prefix string, invert bool) *HeaderPrefixMatcher {
return &HeaderPrefixMatcher{key: key, prefix: prefix, invert: invert}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderPrefixMatcher.
func (hpm *HeaderPrefixMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hpm.key)
if !ok {
return false
}
return strings.HasPrefix(v, hpm.prefix) != hpm.invert
}
func (hpm *HeaderPrefixMatcher) String() string {
return fmt.Sprintf("headerPrefix:%v:%v", hpm.key, hpm.prefix)
}
// HeaderSuffixMatcher matches on whether the suffix of the header value matches
// the suffix passed into this struct.
type HeaderSuffixMatcher struct {
key string
suffix string
invert bool
}
// NewHeaderSuffixMatcher returns a new HeaderSuffixMatcher.
func NewHeaderSuffixMatcher(key string, suffix string, invert bool) *HeaderSuffixMatcher {
return &HeaderSuffixMatcher{key: key, suffix: suffix, invert: invert}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderSuffixMatcher.
func (hsm *HeaderSuffixMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hsm.key)
if !ok {
return false
}
return strings.HasSuffix(v, hsm.suffix) != hsm.invert
}
func (hsm *HeaderSuffixMatcher) String() string {
return fmt.Sprintf("headerSuffix:%v:%v", hsm.key, hsm.suffix)
}
// HeaderContainsMatcher matches on whether the header value contains the
// value passed into this struct.
type HeaderContainsMatcher struct {
key string
contains string
invert bool
}
// NewHeaderContainsMatcher returns a new HeaderContainsMatcher. key is the HTTP
// Header key to match on, and contains is the value that the header should
// contain for a successful match. An empty contains string does not
// work, use HeaderPresentMatcher in that case.
func NewHeaderContainsMatcher(key string, contains string, invert bool) *HeaderContainsMatcher {
return &HeaderContainsMatcher{key: key, contains: contains, invert: invert}
}
// Match returns whether the passed in HTTP Headers match according to the
// HeaderContainsMatcher.
func (hcm *HeaderContainsMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hcm.key)
if !ok {
return false
}
return strings.Contains(v, hcm.contains) != hcm.invert
}
func (hcm *HeaderContainsMatcher) String() string {
return fmt.Sprintf("headerContains:%v%v", hcm.key, hcm.contains)
}
// HeaderStringMatcher matches on whether the header value matches against the
// StringMatcher specified.
type HeaderStringMatcher struct {
key string
stringMatcher StringMatcher
invert bool
}
// NewHeaderStringMatcher returns a new HeaderStringMatcher.
func NewHeaderStringMatcher(key string, sm StringMatcher, invert bool) *HeaderStringMatcher {
return &HeaderStringMatcher{
key: key,
stringMatcher: sm,
invert: invert,
}
}
// Match returns whether the passed in HTTP Headers match according to the
// specified StringMatcher.
func (hsm *HeaderStringMatcher) Match(md metadata.MD) bool {
v, ok := valueFromMD(md, hsm.key)
if !ok {
return false
}
return hsm.stringMatcher.Match(v) != hsm.invert
}
func (hsm *HeaderStringMatcher) String() string {
return fmt.Sprintf("headerString:%v:%v", hsm.key, hsm.stringMatcher)
}

View File

@@ -0,0 +1,549 @@
/*
*
* Copyright 2020 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 matcher
import (
"regexp"
"testing"
"google.golang.org/grpc/metadata"
)
func TestHeaderExactMatcherMatch(t *testing.T) {
tests := []struct {
name string
key, exact string
md metadata.MD
want bool
invert bool
}{
{
name: "one value one match",
key: "th",
exact: "tv",
md: metadata.Pairs("th", "tv"),
want: true,
},
{
name: "two value one match",
key: "th",
exact: "tv",
md: metadata.Pairs("th", "abc", "th", "tv"),
// Doesn't match comma-concatenated string.
want: false,
},
{
name: "two value match concatenated",
key: "th",
exact: "abc,tv",
md: metadata.Pairs("th", "abc", "th", "tv"),
want: true,
},
{
name: "not match",
key: "th",
exact: "tv",
md: metadata.Pairs("th", "abc"),
want: false,
},
{
name: "invert header not present",
key: "th",
exact: "tv",
md: metadata.Pairs(":method", "GET"),
want: false,
invert: true,
},
{
name: "invert header match",
key: "th",
exact: "tv",
md: metadata.Pairs("th", "tv"),
want: false,
invert: true,
},
{
name: "invert header not match",
key: "th",
exact: "tv",
md: metadata.Pairs("th", "tvv"),
want: true,
invert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hem := NewHeaderExactMatcher(tt.key, tt.exact, tt.invert)
if got := hem.Match(tt.md); got != tt.want {
t.Errorf("match() = %v, want %v", got, tt.want)
}
})
}
}
func TestHeaderRegexMatcherMatch(t *testing.T) {
tests := []struct {
name string
key, regexStr string
md metadata.MD
want bool
invert bool
}{
{
name: "one value one match",
key: "th",
regexStr: "^t+v*$",
md: metadata.Pairs("th", "tttvv"),
want: true,
},
{
name: "two value one match",
key: "th",
regexStr: "^t+v*$",
md: metadata.Pairs("th", "abc", "th", "tttvv"),
want: false,
},
{
name: "two value match concatenated",
key: "th",
regexStr: "^[abc]*,t+v*$",
md: metadata.Pairs("th", "abc", "th", "tttvv"),
want: true,
},
{
name: "no match",
key: "th",
regexStr: "^t+v*$",
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,
},
{
name: "invert header not present",
key: "th",
regexStr: "^t+v*$",
md: metadata.Pairs(":method", "GET"),
want: false,
invert: true,
},
{
name: "invert header match",
key: "th",
regexStr: "^t+v*$",
md: metadata.Pairs("th", "tttvv"),
want: false,
invert: true,
},
{
name: "invert header not match",
key: "th",
regexStr: "^t+v*$",
md: metadata.Pairs("th", "abc"),
want: true,
invert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hrm := NewHeaderRegexMatcher(tt.key, regexp.MustCompile(tt.regexStr), tt.invert)
if got := hrm.Match(tt.md); got != tt.want {
t.Errorf("match() = %v, want %v", got, tt.want)
}
})
}
}
func TestHeaderRangeMatcherMatch(t *testing.T) {
tests := []struct {
name string
key string
start, end int64
md metadata.MD
want bool
invert bool
}{
{
name: "match",
key: "th",
start: 1, end: 10,
md: metadata.Pairs("th", "5"),
want: true,
},
{
name: "equal to start",
key: "th",
start: 1, end: 10,
md: metadata.Pairs("th", "1"),
want: true,
},
{
name: "equal to end",
key: "th",
start: 1, end: 10,
md: metadata.Pairs("th", "10"),
want: false,
},
{
name: "negative",
key: "th",
start: -10, end: 10,
md: metadata.Pairs("th", "-5"),
want: true,
},
{
name: "invert header not present",
key: "th",
start: 1, end: 10,
md: metadata.Pairs(":method", "GET"),
want: false,
invert: true,
},
{
name: "invert header match",
key: "th",
start: 1, end: 10,
md: metadata.Pairs("th", "5"),
want: false,
invert: true,
},
{
name: "invert header not match",
key: "th",
start: 1, end: 9,
md: metadata.Pairs("th", "10"),
want: true,
invert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hrm := NewHeaderRangeMatcher(tt.key, tt.start, tt.end, tt.invert)
if got := hrm.Match(tt.md); got != tt.want {
t.Errorf("match() = %v, want %v", got, tt.want)
}
})
}
}
func TestHeaderPresentMatcherMatch(t *testing.T) {
tests := []struct {
name string
key string
present bool
md metadata.MD
want bool
invert bool
}{
{
name: "want present is present",
key: "th",
present: true,
md: metadata.Pairs("th", "tv"),
want: true,
},
{
name: "want present not present",
key: "th",
present: true,
md: metadata.Pairs("abc", "tv"),
want: false,
},
{
name: "want not present is present",
key: "th",
present: false,
md: metadata.Pairs("th", "tv"),
want: false,
},
{
name: "want not present is not present",
key: "th",
present: false,
md: metadata.Pairs("abc", "tv"),
want: true,
},
{
name: "invert header not present",
key: "th",
present: true,
md: metadata.Pairs(":method", "GET"),
want: true,
invert: true,
},
{
name: "invert header match",
key: "th",
present: true,
md: metadata.Pairs("th", "tv"),
want: false,
invert: true,
},
{
name: "invert header not match",
key: "th",
present: true,
md: metadata.Pairs(":method", "GET"),
want: true,
invert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hpm := NewHeaderPresentMatcher(tt.key, tt.present, tt.invert)
if got := hpm.Match(tt.md); got != tt.want {
t.Errorf("match() = %v, want %v", got, tt.want)
}
})
}
}
func TestHeaderPrefixMatcherMatch(t *testing.T) {
tests := []struct {
name string
key, prefix string
md metadata.MD
want bool
invert bool
}{
{
name: "one value one match",
key: "th",
prefix: "tv",
md: metadata.Pairs("th", "tv123"),
want: true,
},
{
name: "two value one match",
key: "th",
prefix: "tv",
md: metadata.Pairs("th", "abc", "th", "tv123"),
want: false,
},
{
name: "two value match concatenated",
key: "th",
prefix: "tv",
md: metadata.Pairs("th", "tv123", "th", "abc"),
want: true,
},
{
name: "not match",
key: "th",
prefix: "tv",
md: metadata.Pairs("th", "abc"),
want: false,
},
{
name: "invert header not present",
key: "th",
prefix: "tv",
md: metadata.Pairs(":method", "GET"),
want: false,
invert: true,
},
{
name: "invert header match",
key: "th",
prefix: "tv",
md: metadata.Pairs("th", "tv123"),
want: false,
invert: true,
},
{
name: "invert header not match",
key: "th",
prefix: "tv",
md: metadata.Pairs("th", "abc"),
want: true,
invert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hpm := NewHeaderPrefixMatcher(tt.key, tt.prefix, tt.invert)
if got := hpm.Match(tt.md); got != tt.want {
t.Errorf("match() = %v, want %v", got, tt.want)
}
})
}
}
func TestHeaderSuffixMatcherMatch(t *testing.T) {
tests := []struct {
name string
key, suffix string
md metadata.MD
want bool
invert bool
}{
{
name: "one value one match",
key: "th",
suffix: "tv",
md: metadata.Pairs("th", "123tv"),
want: true,
},
{
name: "two value one match",
key: "th",
suffix: "tv",
md: metadata.Pairs("th", "123tv", "th", "abc"),
want: false,
},
{
name: "two value match concatenated",
key: "th",
suffix: "tv",
md: metadata.Pairs("th", "abc", "th", "123tv"),
want: true,
},
{
name: "not match",
key: "th",
suffix: "tv",
md: metadata.Pairs("th", "abc"),
want: false,
},
{
name: "invert header not present",
key: "th",
suffix: "tv",
md: metadata.Pairs(":method", "GET"),
want: false,
invert: true,
},
{
name: "invert header match",
key: "th",
suffix: "tv",
md: metadata.Pairs("th", "123tv"),
want: false,
invert: true,
},
{
name: "invert header not match",
key: "th",
suffix: "tv",
md: metadata.Pairs("th", "abc"),
want: true,
invert: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hsm := NewHeaderSuffixMatcher(tt.key, tt.suffix, tt.invert)
if got := hsm.Match(tt.md); got != tt.want {
t.Errorf("match() = %v, want %v", got, tt.want)
}
})
}
}
func TestHeaderStringMatch(t *testing.T) {
tests := []struct {
name string
key string
sm StringMatcher
invert bool
md metadata.MD
want bool
}{
{
name: "should-match",
key: "th",
sm: StringMatcher{
exactMatch: newStringP("tv"),
},
invert: false,
md: metadata.Pairs("th", "tv"),
want: true,
},
{
name: "not match",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: false,
md: metadata.Pairs("th", "not-match"),
want: false,
},
{
name: "invert string match",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: true,
md: metadata.Pairs("th", "not-match"),
want: true,
},
{
name: "header missing",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: false,
md: metadata.Pairs("not-specified-key", "not-match"),
want: false,
},
{
name: "header missing invert true",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: true,
md: metadata.Pairs("not-specified-key", "not-match"),
want: false,
},
{
name: "header empty string invert",
key: "th",
sm: StringMatcher{
containsMatch: newStringP("tv"),
},
invert: true,
md: metadata.Pairs("th", ""),
want: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
hsm := NewHeaderStringMatcher(test.key, test.sm, test.invert)
if got := hsm.Match(test.md); got != test.want {
t.Errorf("match() = %v, want %v", got, test.want)
}
})
}
}

View File

@@ -0,0 +1,229 @@
/*
*
* 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 matcher contains types that need to be shared between code under
// google.golang.org/grpc/xds/... and the rest of gRPC.
package matcher
import (
"errors"
"fmt"
"regexp"
"strings"
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
)
// StringMatcher contains match criteria for matching a string, and is an
// internal representation of the `StringMatcher` proto defined at
// https://github.com/envoyproxy/envoy/blob/main/api/envoy/type/matcher/v3/string.proto.
type StringMatcher struct {
// Since these match fields are part of a `oneof` in the corresponding xDS
// proto, only one of them is expected to be set.
exactMatch *string
prefixMatch *string
suffixMatch *string
regexMatch *regexp.Regexp
containsMatch *string
// If true, indicates the exact/prefix/suffix/contains matching should be
// case insensitive. This has no effect on the regex match.
ignoreCase bool
}
// Match returns true if input matches the criteria in the given StringMatcher.
func (sm StringMatcher) Match(input string) bool {
switch {
case sm.exactMatch != nil:
if sm.ignoreCase {
input = strings.ToLower(input)
}
return input == *sm.exactMatch
case sm.prefixMatch != nil:
if sm.ignoreCase {
input = strings.ToLower(input)
}
return strings.HasPrefix(input, *sm.prefixMatch)
case sm.suffixMatch != nil:
if sm.ignoreCase {
input = strings.ToLower(input)
}
return strings.HasSuffix(input, *sm.suffixMatch)
case sm.containsMatch != nil:
if sm.ignoreCase {
input = strings.ToLower(input)
}
return strings.Contains(input, *sm.containsMatch)
case sm.regexMatch != nil:
return sm.regexMatch.MatchString(input)
}
return false
}
// newStrPtr allocates a new string that holds the value of input and returns a
// pointer to it. ignoreCase controls if a lower case version of input is used.
func newStrPtr(input *string, ignoreCase bool) *string {
if input == nil {
return nil
}
s := new(string)
if ignoreCase {
*s = strings.ToLower(*input)
} else {
*s = *input
}
return s
}
// StringMatcherFromProto is a helper function to create a StringMatcher from
// the corresponding StringMatcher proto.
//
// Returns a non-nil error if matcherProto is invalid.
func StringMatcherFromProto(matcherProto *v3matcherpb.StringMatcher) (StringMatcher, error) {
if matcherProto == nil {
return StringMatcher{}, errors.New("input StringMatcher proto is nil")
}
matcher := StringMatcher{ignoreCase: matcherProto.GetIgnoreCase()}
switch mt := matcherProto.GetMatchPattern().(type) {
case *v3matcherpb.StringMatcher_Exact:
matcher.exactMatch = newStrPtr(&mt.Exact, matcher.ignoreCase)
case *v3matcherpb.StringMatcher_Prefix:
if matcherProto.GetPrefix() == "" {
return StringMatcher{}, errors.New("empty prefix is not allowed in StringMatcher")
}
matcher.prefixMatch = newStrPtr(&mt.Prefix, matcher.ignoreCase)
case *v3matcherpb.StringMatcher_Suffix:
if matcherProto.GetSuffix() == "" {
return StringMatcher{}, errors.New("empty suffix is not allowed in StringMatcher")
}
matcher.suffixMatch = newStrPtr(&mt.Suffix, matcher.ignoreCase)
case *v3matcherpb.StringMatcher_SafeRegex:
regex := matcherProto.GetSafeRegex().GetRegex()
re, err := CompileSafeRegex(regex)
if err != nil {
return StringMatcher{}, fmt.Errorf("safe_regex matcher %q is invalid", regex)
}
matcher = NewRegexStringMatcher(re)
case *v3matcherpb.StringMatcher_Contains:
if matcherProto.GetContains() == "" {
return StringMatcher{}, errors.New("empty contains is not allowed in StringMatcher")
}
matcher.containsMatch = newStrPtr(&mt.Contains, matcher.ignoreCase)
default:
return StringMatcher{}, fmt.Errorf("unrecognized string matcher: %+v", matcherProto)
}
return matcher, nil
}
// NewExactStringMatcher creates a string matcher that requires the input string
// to exactly match the pattern specified here. The match will be case
// insensitive if ignore_case is true.
func NewExactStringMatcher(pattern string, ignoreCase bool) StringMatcher {
return StringMatcher{
exactMatch: newStrPtr(&pattern, ignoreCase),
ignoreCase: ignoreCase,
}
}
// NewPrefixStringMatcher creates a string matcher that requires the input
// string to contain the prefix specified here. The match will be case
// insensitive if ignore_case is true.
func NewPrefixStringMatcher(prefix string, ignoreCase bool) StringMatcher {
return StringMatcher{
prefixMatch: newStrPtr(&prefix, ignoreCase),
ignoreCase: ignoreCase,
}
}
// NewSuffixStringMatcher creates a string matcher that requires the input
// string to contain the suffix specified here. The match will be case
// insensitive if ignore_case is true.
func NewSuffixStringMatcher(suffix string, ignoreCase bool) StringMatcher {
return StringMatcher{
suffixMatch: newStrPtr(&suffix, ignoreCase),
ignoreCase: ignoreCase,
}
}
// NewContainsStringMatcher creates a string matcher that requires the input
// string to contain the pattern specified here. The match will be case
// insensitive if ignore_case is true.
func NewContainsStringMatcher(pattern string, ignoreCase bool) StringMatcher {
return StringMatcher{
containsMatch: newStrPtr(&pattern, ignoreCase),
ignoreCase: ignoreCase,
}
}
// NewRegexStringMatcher creates a string matcher that requires the input string
// to match the regular expression specified here.
func NewRegexStringMatcher(regex *regexp.Regexp) StringMatcher {
return StringMatcher{
regexMatch: regex,
}
}
// ExactMatch returns the value of the configured exact match or an empty string
// if exact match criteria was not specified.
func (sm StringMatcher) ExactMatch() string {
if sm.exactMatch != nil {
return *sm.exactMatch
}
return ""
}
// Equal returns true if other and sm are equivalent to each other.
func (sm StringMatcher) Equal(other StringMatcher) bool {
if sm.ignoreCase != other.ignoreCase {
return false
}
if (sm.exactMatch != nil) != (other.exactMatch != nil) ||
(sm.prefixMatch != nil) != (other.prefixMatch != nil) ||
(sm.suffixMatch != nil) != (other.suffixMatch != nil) ||
(sm.regexMatch != nil) != (other.regexMatch != nil) ||
(sm.containsMatch != nil) != (other.containsMatch != nil) {
return false
}
switch {
case sm.exactMatch != nil:
return *sm.exactMatch == *other.exactMatch
case sm.prefixMatch != nil:
return *sm.prefixMatch == *other.prefixMatch
case sm.suffixMatch != nil:
return *sm.suffixMatch == *other.suffixMatch
case sm.regexMatch != nil:
return sm.regexMatch.String() == other.regexMatch.String()
case sm.containsMatch != nil:
return *sm.containsMatch == *other.containsMatch
}
return true
}
// CompileSafeRegex attempts to compile the provided pattern as a regular
// expression. It first compiles the unanchored pattern to catch syntax errors
// and then compiles and returns the explicitly anchored pattern to guarantee
// full-string matching.
func CompileSafeRegex(pattern string) (*regexp.Regexp, error) {
if _, err := regexp.Compile(pattern); err != nil {
return nil, err
}
return regexp.Compile(fmt.Sprintf("^(?:%s)$", pattern))
}

View File

@@ -0,0 +1,349 @@
/*
*
* 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 matcher
import (
"regexp"
"testing"
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"github.com/google/go-cmp/cmp"
)
func TestStringMatcherFromProto(t *testing.T) {
tests := []struct {
desc string
inputProto *v3matcherpb.StringMatcher
wantMatcher StringMatcher
wantErr bool
}{
{
desc: "nil proto",
wantErr: true,
},
{
desc: "empty prefix",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: ""},
},
wantErr: true,
},
{
desc: "empty suffix",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: ""},
},
wantErr: true,
},
{
desc: "empty contains",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: ""},
},
wantErr: true,
},
{
desc: "invalid regex",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
SafeRegex: &v3matcherpb.RegexMatcher{Regex: "??"},
},
},
wantErr: true,
},
{
desc: "happy case exact",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "exact"},
},
wantMatcher: StringMatcher{exactMatch: newStringP("exact")},
},
{
desc: "happy case exact ignore case",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "EXACT"},
IgnoreCase: true,
},
wantMatcher: StringMatcher{
exactMatch: newStringP("exact"),
ignoreCase: true,
},
},
{
desc: "happy case prefix",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "prefix"},
},
wantMatcher: StringMatcher{prefixMatch: newStringP("prefix")},
},
{
desc: "happy case prefix ignore case",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "PREFIX"},
IgnoreCase: true,
},
wantMatcher: StringMatcher{
prefixMatch: newStringP("prefix"),
ignoreCase: true,
},
},
{
desc: "happy case suffix",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "suffix"},
},
wantMatcher: StringMatcher{suffixMatch: newStringP("suffix")},
},
{
desc: "happy case suffix ignore case",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "SUFFIX"},
IgnoreCase: true,
},
wantMatcher: StringMatcher{
suffixMatch: newStringP("suffix"),
ignoreCase: true,
},
},
{
desc: "happy case regex",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
SafeRegex: &v3matcherpb.RegexMatcher{Regex: "good?regex?"},
},
},
wantMatcher: StringMatcher{regexMatch: regexp.MustCompile("^(?:good?regex?)$")},
},
{
desc: "regex with ignore case",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
SafeRegex: &v3matcherpb.RegexMatcher{Regex: "good?regex?"},
},
IgnoreCase: true,
},
wantMatcher: StringMatcher{
regexMatch: regexp.MustCompile("^(?:good?regex?)$"),
},
},
{
desc: "happy case contains",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: "contains"},
},
wantMatcher: StringMatcher{containsMatch: newStringP("contains")},
},
{
desc: "happy case contains ignore case",
inputProto: &v3matcherpb.StringMatcher{
MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: "CONTAINS"},
IgnoreCase: true,
},
wantMatcher: StringMatcher{
containsMatch: newStringP("contains"),
ignoreCase: true,
},
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
gotMatcher, err := StringMatcherFromProto(test.inputProto)
if (err != nil) != test.wantErr {
t.Fatalf("StringMatcherFromProto(%+v) returned err: %v, wantErr: %v", test.inputProto, err, test.wantErr)
}
if diff := cmp.Diff(gotMatcher, test.wantMatcher, cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
t.Fatalf("StringMatcherFromProto(%+v) returned unexpected diff (-got, +want):\n%s", test.inputProto, diff)
}
})
}
}
func TestMatch(t *testing.T) {
var (
exactMatcher = NewExactStringMatcher("exact", false)
exactMatcherIgnoreCase = NewExactStringMatcher("exact", true)
prefixMatcher = NewPrefixStringMatcher("prefix", false)
prefixMatcherIgnoreCase = NewPrefixStringMatcher("prefix", true)
suffixMatcher = NewSuffixStringMatcher("suffix", false)
suffixMatcherIgnoreCase = NewSuffixStringMatcher("suffix", true)
containsMatcher = NewContainsStringMatcher("contains", false)
containsMatcherIgnoreCase = NewContainsStringMatcher("contains", true)
regexMatcher = NewRegexStringMatcher(regexp.MustCompile("^(?:good?regex?)$"))
)
tests := []struct {
desc string
matcher StringMatcher
input string
wantMatch bool
}{
{
desc: "exact match success",
matcher: exactMatcher,
input: "exact",
wantMatch: true,
},
{
desc: "exact match failure",
matcher: exactMatcher,
input: "not-exact",
},
{
desc: "exact match success with ignore case",
matcher: exactMatcherIgnoreCase,
input: "EXACT",
wantMatch: true,
},
{
desc: "exact match failure with ignore case",
matcher: exactMatcherIgnoreCase,
input: "not-exact",
},
{
desc: "prefix match success",
matcher: prefixMatcher,
input: "prefixIsHere",
wantMatch: true,
},
{
desc: "prefix match failure",
matcher: prefixMatcher,
input: "not-prefix",
},
{
desc: "prefix match success with ignore case",
matcher: prefixMatcherIgnoreCase,
input: "PREFIXisHere",
wantMatch: true,
},
{
desc: "prefix match failure with ignore case",
matcher: prefixMatcherIgnoreCase,
input: "not-PREFIX",
},
{
desc: "suffix match success",
matcher: suffixMatcher,
input: "hereIsThesuffix",
wantMatch: true,
},
{
desc: "suffix match failure",
matcher: suffixMatcher,
input: "suffix-is-not-here",
},
{
desc: "suffix match success with ignore case",
matcher: suffixMatcherIgnoreCase,
input: "hereIsTheSuFFix",
wantMatch: true,
},
{
desc: "suffix match failure with ignore case",
matcher: suffixMatcherIgnoreCase,
input: "SUFFIX-is-not-here",
},
{
desc: "regex match success",
matcher: regexMatcher,
input: "goodregex",
wantMatch: true,
},
{
desc: "regex match failure because only part match",
matcher: regexMatcher,
input: "goodregexa",
wantMatch: false,
},
{
desc: "regex match failure",
matcher: regexMatcher,
input: "regex-is-not-here",
},
{
desc: "contains match success",
matcher: containsMatcher,
input: "IScontainsHERE",
wantMatch: true,
},
{
desc: "contains match failure",
matcher: containsMatcher,
input: "con-tains-is-not-here",
},
{
desc: "contains match success with ignore case",
matcher: containsMatcherIgnoreCase,
input: "isCONTAINShere",
wantMatch: true,
},
{
desc: "contains match failure with ignore case",
matcher: containsMatcherIgnoreCase,
input: "CON-TAINS-is-not-here",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
if gotMatch := test.matcher.Match(test.input); gotMatch != test.wantMatch {
t.Errorf("StringMatcher.Match(%s) returned %v, want %v", test.input, gotMatch, test.wantMatch)
}
})
}
}
func newStringP(s string) *string {
return &s
}
func TestCompileSafeRegex(t *testing.T) {
tests := []struct {
desc string
pattern string
wantRegex string
wantErr bool
}{
{
desc: "ValidPattern",
pattern: "abc",
wantRegex: "^(?:abc)$",
},
{
desc: "UnmatchedParenthesis",
pattern: "abc(",
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
re, err := CompileSafeRegex(test.pattern)
if (err != nil) != test.wantErr {
t.Fatalf("CompileSafeRegex(%q) returned error %q, wantErr=%v", test.pattern, err, test.wantErr)
}
if err == nil {
if re.String() != test.wantRegex {
t.Fatalf("CompileSafeRegex(%q) = %q, want %q", test.pattern, re.String(), test.wantRegex)
}
}
})
}
}