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