sigs.k8s.io/external-dns@v0.14.1/endpoint/domain_filter.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package endpoint 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "regexp" 24 "sort" 25 "strings" 26 ) 27 28 type MatchAllDomainFilters []*DomainFilter 29 30 func (f MatchAllDomainFilters) Match(domain string) bool { 31 for _, filter := range f { 32 if filter == nil { 33 continue 34 } 35 if !filter.Match(domain) { 36 return false 37 } 38 } 39 return true 40 } 41 42 // DomainFilter holds a lists of valid domain names 43 type DomainFilter struct { 44 // Filters define what domains to match 45 Filters []string 46 // exclude define what domains not to match 47 exclude []string 48 // regex defines a regular expression to match the domains 49 regex *regexp.Regexp 50 // regexExclusion defines a regular expression to exclude the domains matched 51 regexExclusion *regexp.Regexp 52 } 53 54 // domainFilterSerde is a helper type for serializing and deserializing DomainFilter. 55 type domainFilterSerde struct { 56 Include []string `json:"include,omitempty"` 57 Exclude []string `json:"exclude,omitempty"` 58 RegexInclude string `json:"regexInclude,omitempty"` 59 RegexExclude string `json:"regexExclude,omitempty"` 60 } 61 62 // prepareFilters provides consistent trimming for filters/exclude params 63 func prepareFilters(filters []string) []string { 64 var fs []string 65 for _, filter := range filters { 66 if domain := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(filter), ".")); domain != "" { 67 fs = append(fs, domain) 68 } 69 } 70 return fs 71 } 72 73 // NewDomainFilterWithExclusions returns a new DomainFilter, given a list of matches and exclusions 74 func NewDomainFilterWithExclusions(domainFilters []string, excludeDomains []string) DomainFilter { 75 return DomainFilter{Filters: prepareFilters(domainFilters), exclude: prepareFilters(excludeDomains)} 76 } 77 78 // NewDomainFilter returns a new DomainFilter given a comma separated list of domains 79 func NewDomainFilter(domainFilters []string) DomainFilter { 80 return DomainFilter{Filters: prepareFilters(domainFilters)} 81 } 82 83 // NewRegexDomainFilter returns a new DomainFilter given a regular expression 84 func NewRegexDomainFilter(regexDomainFilter *regexp.Regexp, regexDomainExclusion *regexp.Regexp) DomainFilter { 85 return DomainFilter{regex: regexDomainFilter, regexExclusion: regexDomainExclusion} 86 } 87 88 // Match checks whether a domain can be found in the DomainFilter. 89 // RegexFilter takes precedence over Filters 90 func (df DomainFilter) Match(domain string) bool { 91 if df.regex != nil && df.regex.String() != "" || df.regexExclusion != nil && df.regexExclusion.String() != "" { 92 return matchRegex(df.regex, df.regexExclusion, domain) 93 } 94 95 return matchFilter(df.Filters, domain, true) && !matchFilter(df.exclude, domain, false) 96 } 97 98 // matchFilter determines if any `filters` match `domain`. 99 // If no `filters` are provided, behavior depends on `emptyval` 100 // (empty `df.filters` matches everything, while empty `df.exclude` excludes nothing) 101 func matchFilter(filters []string, domain string, emptyval bool) bool { 102 if len(filters) == 0 { 103 return emptyval 104 } 105 106 strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) 107 for _, filter := range filters { 108 if filter == "" { 109 continue 110 } 111 112 if strings.HasPrefix(filter, ".") && strings.HasSuffix(strippedDomain, filter) { 113 return true 114 } else if strings.Count(strippedDomain, ".") == strings.Count(filter, ".") { 115 if strippedDomain == filter { 116 return true 117 } 118 } else if strings.HasSuffix(strippedDomain, "."+filter) { 119 return true 120 } 121 } 122 return false 123 } 124 125 // matchRegex determines if a domain matches the configured regular expressions in DomainFilter. 126 // negativeRegex, if set, takes precedence over regex. Therefore, matchRegex returns true when 127 // only regex regular expression matches the domain 128 // Otherwise, if either negativeRegex matches or regex does not match the domain, it returns false 129 func matchRegex(regex *regexp.Regexp, negativeRegex *regexp.Regexp, domain string) bool { 130 strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) 131 132 if negativeRegex != nil && negativeRegex.String() != "" { 133 return !negativeRegex.MatchString(strippedDomain) 134 } 135 return regex.MatchString(strippedDomain) 136 } 137 138 // IsConfigured returns true if any inclusion or exclusion rules have been specified. 139 func (df DomainFilter) IsConfigured() bool { 140 if df.regex != nil && df.regex.String() != "" { 141 return true 142 } else if df.regexExclusion != nil && df.regexExclusion.String() != "" { 143 return true 144 } 145 return len(df.Filters) > 0 || len(df.exclude) > 0 146 } 147 148 func (df DomainFilter) MarshalJSON() ([]byte, error) { 149 if df.regex != nil || df.regexExclusion != nil { 150 var include, exclude string 151 if df.regex != nil { 152 include = df.regex.String() 153 } 154 if df.regexExclusion != nil { 155 exclude = df.regexExclusion.String() 156 } 157 return json.Marshal(domainFilterSerde{ 158 RegexInclude: include, 159 RegexExclude: exclude, 160 }) 161 } 162 sort.Strings(df.Filters) 163 sort.Strings(df.exclude) 164 return json.Marshal(domainFilterSerde{ 165 Include: df.Filters, 166 Exclude: df.exclude, 167 }) 168 } 169 170 func (df *DomainFilter) UnmarshalJSON(b []byte) error { 171 var deserialized domainFilterSerde 172 err := json.Unmarshal(b, &deserialized) 173 if err != nil { 174 return err 175 } 176 177 if deserialized.RegexInclude == "" && deserialized.RegexExclude == "" { 178 *df = NewDomainFilterWithExclusions(deserialized.Include, deserialized.Exclude) 179 return nil 180 } 181 182 if len(deserialized.Include) > 0 || len(deserialized.Exclude) > 0 { 183 return errors.New("cannot have both domain list and regex") 184 } 185 186 var include, exclude *regexp.Regexp 187 if deserialized.RegexInclude != "" { 188 include, err = regexp.Compile(deserialized.RegexInclude) 189 if err != nil { 190 return fmt.Errorf("invalid regexInclude: %w", err) 191 } 192 } 193 if deserialized.RegexExclude != "" { 194 exclude, err = regexp.Compile(deserialized.RegexExclude) 195 if err != nil { 196 return fmt.Errorf("invalid regexExclude: %w", err) 197 } 198 } 199 *df = NewRegexDomainFilter(include, exclude) 200 return nil 201 } 202 203 func (df DomainFilter) MatchParent(domain string) bool { 204 if matchFilter(df.exclude, domain, false) { 205 return false 206 } 207 if len(df.Filters) == 0 { 208 return true 209 } 210 211 strippedDomain := strings.ToLower(strings.TrimSuffix(domain, ".")) 212 for _, filter := range df.Filters { 213 if filter == "" || strings.HasPrefix(filter, ".") { 214 // We don't check parents if the filter is prefixed with "." 215 continue 216 } 217 if strings.HasSuffix(filter, "."+strippedDomain) { 218 return true 219 } 220 } 221 return false 222 }