1// Copyright 2017 Istio Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package lang 16 17import ( 18 "errors" 19 "fmt" 20 "net" 21 "net/mail" 22 "net/url" 23 "regexp" 24 "strings" 25 "time" 26 27 "golang.org/x/net/idna" 28 29 config "istio.io/api/policy/v1beta1" 30 "istio.io/istio/mixer/pkg/il/interpreter" 31 "istio.io/istio/mixer/pkg/lang/ast" 32 "istio.io/pkg/attribute" 33) 34 35// Externs contains the list of standard external functions used during evaluation. 36var Externs = map[string]interpreter.Extern{ 37 "ip": interpreter.ExternFromFn("ip", ExternIP), 38 "ip_equal": interpreter.ExternFromFn("ip_equal", ExternIPEqual), 39 "timestamp": interpreter.ExternFromFn("timestamp", externTimestamp), 40 "timestamp_equal": interpreter.ExternFromFn("timestamp_equal", externTimestampEqual), 41 "timestamp_lt": interpreter.ExternFromFn("timestamp_lt", externTimestampLt), 42 "timestamp_le": interpreter.ExternFromFn("timestamp_le", externTimestampLe), 43 "timestamp_gt": interpreter.ExternFromFn("timestamp_gt", externTimestampGt), 44 "timestamp_ge": interpreter.ExternFromFn("timestamp_ge", externTimestampGe), 45 "dnsName": interpreter.ExternFromFn("dnsName", ExternDNSName), 46 "dnsName_equal": interpreter.ExternFromFn("dnsName_equal", ExternDNSNameEqual), 47 "email": interpreter.ExternFromFn("email", ExternEmail), 48 "email_equal": interpreter.ExternFromFn("email_equal", ExternEmailEqual), 49 "uri": interpreter.ExternFromFn("uri", ExternURI), 50 "uri_equal": interpreter.ExternFromFn("uri_equal", ExternURIEqual), 51 "match": interpreter.ExternFromFn("match", ExternMatch), 52 "matches": interpreter.ExternFromFn("matches", externMatches), 53 "startsWith": interpreter.ExternFromFn("startsWith", ExternStartsWith), 54 "endsWith": interpreter.ExternFromFn("endsWith", ExternEndsWith), 55 "emptyStringMap": interpreter.ExternFromFn("emptyStringMap", externEmptyStringMap), 56 "conditionalString": interpreter.ExternFromFn("conditionalString", externConditionalString), 57 "toLower": interpreter.ExternFromFn("toLower", ExternToLower), 58} 59 60// ExternFunctionMetadata is the type-metadata about externs. It gets used during compilations. 61var ExternFunctionMetadata = []ast.FunctionMetadata{ 62 { 63 Name: "ip", 64 ReturnType: config.IP_ADDRESS, 65 ArgumentTypes: []config.ValueType{config.STRING}, 66 }, 67 { 68 Name: "timestamp", 69 ReturnType: config.TIMESTAMP, 70 ArgumentTypes: []config.ValueType{config.STRING}, 71 }, 72 { 73 Name: "dnsName", 74 ReturnType: config.DNS_NAME, 75 ArgumentTypes: []config.ValueType{config.STRING}, 76 }, 77 { 78 Name: "email", 79 ReturnType: config.EMAIL_ADDRESS, 80 ArgumentTypes: []config.ValueType{config.STRING}, 81 }, 82 { 83 Name: "uri", 84 ReturnType: config.URI, 85 ArgumentTypes: []config.ValueType{config.STRING}, 86 }, 87 { 88 Name: "match", 89 ReturnType: config.BOOL, 90 ArgumentTypes: []config.ValueType{config.STRING, config.STRING}, 91 }, 92 { 93 Name: "matches", 94 Instance: true, 95 TargetType: config.STRING, 96 ReturnType: config.BOOL, 97 ArgumentTypes: []config.ValueType{config.STRING}, 98 }, 99 { 100 Name: "startsWith", 101 Instance: true, 102 TargetType: config.STRING, 103 ReturnType: config.BOOL, 104 ArgumentTypes: []config.ValueType{config.STRING}, 105 }, 106 { 107 Name: "endsWith", 108 Instance: true, 109 TargetType: config.STRING, 110 ReturnType: config.BOOL, 111 ArgumentTypes: []config.ValueType{config.STRING}, 112 }, 113 { 114 Name: "emptyStringMap", 115 ReturnType: config.STRING_MAP, 116 ArgumentTypes: []config.ValueType{}, 117 }, 118 { 119 Name: "conditionalString", 120 ReturnType: config.STRING, 121 ArgumentTypes: []config.ValueType{config.BOOL, config.STRING, config.STRING}, 122 }, 123 { 124 Name: "toLower", 125 ReturnType: config.STRING, 126 ArgumentTypes: []config.ValueType{config.STRING}, 127 }, 128} 129 130// ExternIP creates an IP address 131func ExternIP(in string) ([]byte, error) { 132 if ip := net.ParseIP(in); ip != nil { 133 return []byte(ip), nil 134 } 135 return []byte{}, fmt.Errorf("could not convert %s to IP_ADDRESS", in) 136} 137 138// ExternIPEqual compares two IP addresses for equality 139func ExternIPEqual(a []byte, b []byte) bool { 140 // net.IP is an alias for []byte, so these are safe to convert 141 ip1 := net.IP(a) 142 ip2 := net.IP(b) 143 return ip1.Equal(ip2) 144} 145 146func externTimestamp(in string) (time.Time, error) { 147 layout := time.RFC3339 148 t, err := time.Parse(layout, in) 149 if err != nil { 150 return time.Time{}, fmt.Errorf("could not convert '%s' to TIMESTAMP. expected format: '%s'", in, layout) 151 } 152 return t, nil 153} 154 155func externTimestampEqual(t1 time.Time, t2 time.Time) bool { 156 return t1.Equal(t2) 157} 158 159func externTimestampLt(t1 time.Time, t2 time.Time) bool { 160 return t1.Before(t2) 161} 162 163func externTimestampLe(t1 time.Time, t2 time.Time) bool { 164 return t1 == t2 || t1.Before(t2) 165} 166 167func externTimestampGt(t1 time.Time, t2 time.Time) bool { 168 return t2.Before(t1) 169} 170 171func externTimestampGe(t1 time.Time, t2 time.Time) bool { 172 return t1 == t2 || t2.Before(t1) 173} 174 175// This IDNA profile is for performing validations, but does not otherwise modify the string. 176var externDNSNameProfile = idna.New( 177 idna.StrictDomainName(true), 178 idna.ValidateLabels(true), 179 idna.VerifyDNSLength(true), 180 idna.BidiRule()) 181 182// ExternDNSName converts a string to a DNS name 183func ExternDNSName(in string) (string, error) { 184 s, err := externDNSNameProfile.ToUnicode(in) 185 if err != nil { 186 err = fmt.Errorf("error converting '%s' to dns name: '%v'", in, err) 187 } 188 return s, err 189} 190 191// This IDNA profile converts the string for lookup, which ends up canonicalizing the dns name, for the most 192// part. 193var externDNSNameEqualProfile = idna.New(idna.MapForLookup(), 194 idna.BidiRule()) 195 196// ExternDNSNameEqual compares two DNS names for equality 197func ExternDNSNameEqual(n1 string, n2 string) (bool, error) { 198 var err error 199 200 if n1, err = externDNSNameEqualProfile.ToUnicode(n1); err != nil { 201 return false, err 202 } 203 204 if n2, err = externDNSNameEqualProfile.ToUnicode(n2); err != nil { 205 return false, err 206 } 207 208 if len(n1) > 0 && len(n2) > 0 { 209 if n1[len(n1)-1] == '.' && n2[len(n2)-1] != '.' { 210 n1 = n1[:len(n1)-1] 211 } 212 if n2[len(n2)-1] == '.' && n1[len(n1)-1] != '.' { 213 n2 = n2[:len(n2)-1] 214 } 215 } 216 217 return n1 == n2, nil 218} 219 220// ExternEmail converts a string to an email address 221func ExternEmail(in string) (string, error) { 222 a, err := mail.ParseAddress(in) 223 if err != nil { 224 return "", fmt.Errorf("error converting '%s' to e-mail: '%v'", in, err) 225 } 226 227 if a.Name != "" { 228 return "", fmt.Errorf("error converting '%s' to e-mail: display names are not allowed", in) 229 } 230 231 // Also check through the dns name logic to ensure that this will not cause any breaks there, when used for 232 // comparison. 233 234 _, domain := getEmailParts(a.Address) 235 236 _, err = ExternDNSName(domain) 237 if err != nil { 238 return "", fmt.Errorf("error converting '%s' to e-mail: '%v'", in, err) 239 } 240 241 return in, nil 242} 243 244// ExternEmailEqual compares two email addresses for equality 245func ExternEmailEqual(e1 string, e2 string) (bool, error) { 246 a1, err := mail.ParseAddress(e1) 247 if err != nil { 248 return false, err 249 } 250 251 a2, err := mail.ParseAddress(e2) 252 if err != nil { 253 return false, err 254 } 255 256 local1, domain1 := getEmailParts(a1.Address) 257 local2, domain2 := getEmailParts(a2.Address) 258 259 domainEq, err := ExternDNSNameEqual(domain1, domain2) 260 if err != nil { 261 return false, fmt.Errorf("error comparing e-mails '%s' and '%s': %v", e1, e2, err) 262 } 263 264 if !domainEq { 265 return false, nil 266 } 267 268 return local1 == local2, nil 269} 270 271// ExternURI converts a string to a URI 272func ExternURI(in string) (string, error) { 273 if in == "" { 274 return "", errors.New("error converting string to uri: empty string") 275 } 276 277 if _, err := url.Parse(in); err != nil { 278 return "", fmt.Errorf("error converting string to uri '%s': '%v'", in, err) 279 } 280 return in, nil 281} 282 283// ExternURIEqual compares two URIs for equality 284func ExternURIEqual(u1 string, u2 string) (bool, error) { 285 url1, err := url.Parse(u1) 286 if err != nil { 287 return false, fmt.Errorf("error converting string to uri '%s': '%v'", u1, err) 288 } 289 290 url2, err := url.Parse(u2) 291 if err != nil { 292 return false, fmt.Errorf("error converting string to uri '%s': '%v'", u2, err) 293 } 294 295 // Try to apply as much normalization logic as possible. 296 scheme1 := strings.ToLower(url1.Scheme) 297 scheme2 := strings.ToLower(url2.Scheme) 298 if scheme1 != scheme2 { 299 return false, nil 300 } 301 302 // normalize schemes 303 url1.Scheme = scheme1 304 url2.Scheme = scheme1 305 306 if scheme1 == "http" || scheme1 == "https" { 307 // Special case http(s) URLs 308 309 dnsEq, err := ExternDNSNameEqual(url1.Hostname(), url2.Hostname()) 310 if err != nil { 311 return false, err 312 } 313 314 if !dnsEq { 315 return false, nil 316 } 317 318 if url1.Port() != url2.Port() { 319 return false, nil 320 } 321 322 // normalize host names 323 url1.Host = url2.Host 324 } 325 326 return url1.String() == url2.String(), nil 327} 328 329func getEmailParts(email string) (local string, domain string) { 330 idx := strings.IndexByte(email, '@') 331 if idx == -1 { 332 local = email 333 domain = "" 334 return 335 } 336 337 local = email[:idx] 338 domain = email[idx+1:] 339 return 340} 341 342// ExternMatch provides wildcard matching for strings 343func ExternMatch(str string, pattern string) bool { 344 if strings.HasSuffix(pattern, "*") { 345 return strings.HasPrefix(str, pattern[:len(pattern)-1]) 346 } 347 if strings.HasPrefix(pattern, "*") { 348 return strings.HasSuffix(str, pattern[1:]) 349 } 350 return str == pattern 351} 352 353func externMatches(pattern string, str string) (bool, error) { 354 return regexp.MatchString(pattern, str) 355} 356 357// ExternStartsWith checks for prefixes 358func ExternStartsWith(str string, prefix string) bool { 359 return strings.HasPrefix(str, prefix) 360} 361 362// ExternEndsWith checks for suffixes 363func ExternEndsWith(str string, suffix string) bool { 364 return strings.HasSuffix(str, suffix) 365} 366 367func externEmptyStringMap() attribute.StringMap { 368 return attribute.WrapStringMap(nil) 369} 370 371func externConditionalString(condition bool, trueStr, falseStr string) string { 372 if condition { 373 return trueStr 374 } 375 return falseStr 376} 377 378// ExternToLower changes the string case to lower 379func ExternToLower(str string) string { 380 return strings.ToLower(str) 381} 382