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