1/* 2 * 3 * Copyright 2021 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19// Package matcher contains types that need to be shared between code under 20// google.golang.org/grpc/xds/... and the rest of gRPC. 21package matcher 22 23import ( 24 "errors" 25 "fmt" 26 "regexp" 27 "strings" 28 29 v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" 30) 31 32// StringMatcher contains match criteria for matching a string, and is an 33// internal representation of the `StringMatcher` proto defined at 34// https://github.com/envoyproxy/envoy/blob/main/api/envoy/type/matcher/v3/string.proto. 35type StringMatcher struct { 36 // Since these match fields are part of a `oneof` in the corresponding xDS 37 // proto, only one of them is expected to be set. 38 exactMatch *string 39 prefixMatch *string 40 suffixMatch *string 41 regexMatch *regexp.Regexp 42 containsMatch *string 43 // If true, indicates the exact/prefix/suffix/contains matching should be 44 // case insensitive. This has no effect on the regex match. 45 ignoreCase bool 46} 47 48// Match returns true if input matches the criteria in the given StringMatcher. 49func (sm StringMatcher) Match(input string) bool { 50 if sm.ignoreCase { 51 input = strings.ToLower(input) 52 } 53 switch { 54 case sm.exactMatch != nil: 55 return input == *sm.exactMatch 56 case sm.prefixMatch != nil: 57 return strings.HasPrefix(input, *sm.prefixMatch) 58 case sm.suffixMatch != nil: 59 return strings.HasSuffix(input, *sm.suffixMatch) 60 case sm.regexMatch != nil: 61 return sm.regexMatch.MatchString(input) 62 case sm.containsMatch != nil: 63 return strings.Contains(input, *sm.containsMatch) 64 } 65 return false 66} 67 68// StringMatcherFromProto is a helper function to create a StringMatcher from 69// the corresponding StringMatcher proto. 70// 71// Returns a non-nil error if matcherProto is invalid. 72func StringMatcherFromProto(matcherProto *v3matcherpb.StringMatcher) (StringMatcher, error) { 73 if matcherProto == nil { 74 return StringMatcher{}, errors.New("input StringMatcher proto is nil") 75 } 76 77 matcher := StringMatcher{ignoreCase: matcherProto.GetIgnoreCase()} 78 switch mt := matcherProto.GetMatchPattern().(type) { 79 case *v3matcherpb.StringMatcher_Exact: 80 matcher.exactMatch = &mt.Exact 81 if matcher.ignoreCase { 82 *matcher.exactMatch = strings.ToLower(*matcher.exactMatch) 83 } 84 case *v3matcherpb.StringMatcher_Prefix: 85 if matcherProto.GetPrefix() == "" { 86 return StringMatcher{}, errors.New("empty prefix is not allowed in StringMatcher") 87 } 88 matcher.prefixMatch = &mt.Prefix 89 if matcher.ignoreCase { 90 *matcher.prefixMatch = strings.ToLower(*matcher.prefixMatch) 91 } 92 case *v3matcherpb.StringMatcher_Suffix: 93 if matcherProto.GetSuffix() == "" { 94 return StringMatcher{}, errors.New("empty suffix is not allowed in StringMatcher") 95 } 96 matcher.suffixMatch = &mt.Suffix 97 if matcher.ignoreCase { 98 *matcher.suffixMatch = strings.ToLower(*matcher.suffixMatch) 99 } 100 case *v3matcherpb.StringMatcher_SafeRegex: 101 regex := matcherProto.GetSafeRegex().GetRegex() 102 re, err := regexp.Compile(regex) 103 if err != nil { 104 return StringMatcher{}, fmt.Errorf("safe_regex matcher %q is invalid", regex) 105 } 106 matcher.regexMatch = re 107 case *v3matcherpb.StringMatcher_Contains: 108 if matcherProto.GetContains() == "" { 109 return StringMatcher{}, errors.New("empty contains is not allowed in StringMatcher") 110 } 111 matcher.containsMatch = &mt.Contains 112 if matcher.ignoreCase { 113 *matcher.containsMatch = strings.ToLower(*matcher.containsMatch) 114 } 115 default: 116 return StringMatcher{}, fmt.Errorf("unrecognized string matcher: %+v", matcherProto) 117 } 118 return matcher, nil 119} 120 121// StringMatcherForTesting is a helper function to create a StringMatcher based 122// on the given arguments. Intended only for testing purposes. 123func StringMatcherForTesting(exact, prefix, suffix, contains *string, regex *regexp.Regexp, ignoreCase bool) StringMatcher { 124 sm := StringMatcher{ 125 exactMatch: exact, 126 prefixMatch: prefix, 127 suffixMatch: suffix, 128 regexMatch: regex, 129 containsMatch: contains, 130 ignoreCase: ignoreCase, 131 } 132 if ignoreCase { 133 switch { 134 case sm.exactMatch != nil: 135 *sm.exactMatch = strings.ToLower(*exact) 136 case sm.prefixMatch != nil: 137 *sm.prefixMatch = strings.ToLower(*prefix) 138 case sm.suffixMatch != nil: 139 *sm.suffixMatch = strings.ToLower(*suffix) 140 case sm.containsMatch != nil: 141 *sm.containsMatch = strings.ToLower(*contains) 142 } 143 } 144 return sm 145} 146 147// ExactMatch returns the value of the configured exact match or an empty string 148// if exact match criteria was not specified. 149func (sm StringMatcher) ExactMatch() string { 150 if sm.exactMatch != nil { 151 return *sm.exactMatch 152 } 153 return "" 154} 155 156// Equal returns true if other and sm are equivalent to each other. 157func (sm StringMatcher) Equal(other StringMatcher) bool { 158 if sm.ignoreCase != other.ignoreCase { 159 return false 160 } 161 162 if (sm.exactMatch != nil) != (other.exactMatch != nil) || 163 (sm.prefixMatch != nil) != (other.prefixMatch != nil) || 164 (sm.suffixMatch != nil) != (other.suffixMatch != nil) || 165 (sm.regexMatch != nil) != (other.regexMatch != nil) || 166 (sm.containsMatch != nil) != (other.containsMatch != nil) { 167 return false 168 } 169 170 switch { 171 case sm.exactMatch != nil: 172 return *sm.exactMatch == *other.exactMatch 173 case sm.prefixMatch != nil: 174 return *sm.prefixMatch == *other.prefixMatch 175 case sm.suffixMatch != nil: 176 return *sm.suffixMatch == *other.suffixMatch 177 case sm.regexMatch != nil: 178 return sm.regexMatch.String() == other.regexMatch.String() 179 case sm.containsMatch != nil: 180 return *sm.containsMatch == *other.containsMatch 181 } 182 return true 183} 184