/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ using Amazon.Runtime.Endpoints; using Amazon.Util; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using ThirdParty.Json.LitJson; namespace Amazon.Runtime.Internal.Endpoints.StandardLibrary { /// /// Set of internal functions supported by ruleset conditions. /// public static class Fn { /// /// Evaluates whether a value (such as an endpoint parameter) is set /// public static bool IsSet(object value) { return value != null; } /// /// Extracts part of given object graph by path /// /// Example: Given the input object {"Thing1": "foo", "Thing2": ["index0", "index1"], "Thing3": {"SubThing": 42}} /// GetAttr(object, "Thing1") returns "foo" /// path "Thing2[0]" returns "index0" /// path "Thing3.SubThing" returns 42 /// Given the input IList list = {"foo", "bar"} /// GetAttr(list, "[0]") returns "foo" /// /// Every path segment must resolve to IPropertyBag /// Every path segment with indexer must resolve to IList /// Indexers must be at the very end of the path /// public static object GetAttr(object value, string path) { if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path"); var parts = path.Split('.'); var propertyValue = value; for (int i = 0; i < parts.Length; i++) { var part = parts[i]; // indexer is always at the end of path e.g. "Part1.Part2[3]" if (i == parts.Length - 1) { var indexerStart = part.LastIndexOf('['); var indexerEnd = part.Length - 1; // indexer detected if (indexerStart >= 0) { var propertyPath = part.Substring(0, indexerStart); var index = int.Parse(part.Substring(indexerStart + 1, indexerEnd - indexerStart - 1)); // indexer can be passed directly as a path e.g. "[1]" if (indexerStart > 0) { propertyValue = ((IPropertyBag)propertyValue)[propertyPath]; } if (!(propertyValue is IList)) throw new ArgumentException("Object addressing by pathing segment '{part}' with indexer must be IList"); var list = (IList)propertyValue; if (index < 0 || index > list.Count - 1) { return null; } return list[index]; } } if (!(propertyValue is IPropertyBag)) throw new ArgumentException("Object addressing by pathing segment '{part}' must be IPropertyBag"); propertyValue = ((IPropertyBag)propertyValue)[part]; } return propertyValue; } /// /// Returns partition data for a region /// public static Partition Partition(string region) { return StandardLibrary.Partition.GetPartitionByRegion(region); } /// /// Evaluates a string ARN value, and returns an object containing details about the parsed ARN. /// If the input was not a valid ARN, the function returns null. /// public static Arn ParseArn(string arn) { Arn parsedArn; if (!Arn.TryParse(arn, out parsedArn)) { return null; } return parsedArn; } /// /// Evaluates whether one or more string values are valid host labels per RFC 1123. /// https://www.rfc-editor.org/rfc/rfc1123#page-13 /// Each host label must be between [1, 63] characters, start with a number or letter, and only contain numbers, letters, or hyphens. /// Host label can't end with a hyphen. /// If allowSubDomains is true, then the provided value may be zero or more dotted subdomains which are each validated per RFC 1123. /// public static bool IsValidHostLabel(string hostLabel, bool allowSubDomains) { var hosts = new List(); if (allowSubDomains) { hosts.AddRange(hostLabel.Split('.')); } else { hosts.Add(hostLabel); } foreach (var host in hosts) { if (!IsVirtualHostableName(host)) { return false; } } return true; } private static bool IsVirtualHostableName(string name) { if (string.IsNullOrEmpty(name) || name.Length < 1 || name.Length > 63) { return false; } if (!char.IsLetterOrDigit(name[0]) || !char.IsLetterOrDigit(name.Last())) { return false; } for (int i = 1; i < name.Length - 1; i++) { if (!char.IsLetterOrDigit(name[i]) && name[i] != '-') { return false; } } return true; } /// /// In addition to the restrictions defined in RFC 1123 and isValidHostLabel, /// validates that the bucket name is between [3,63] characters, /// does not contain upper case characters, and is not formatted as an IP4 address. /// Host label can't end with a hyphen. /// public static bool IsVirtualHostableS3Bucket(string hostLabel, bool allowSubDomains) { if (IsIpV4Address(hostLabel)) { return false; } var hosts = new List(); if (allowSubDomains) { hosts.AddRange(hostLabel.Split('.')); } else { hosts.Add(hostLabel); } foreach (var host in hosts) { if (!IsVirtualHostableS3Name(host)) { return false; } } return true; } private static bool IsVirtualHostableS3Name(string name) { if (string.IsNullOrEmpty(name) || name.Length < 3 || name.Length > 63) { return false; } if (char.IsUpper(name[0]) || !char.IsLetterOrDigit(name[0]) || !char.IsLetterOrDigit(name.Last())) { return false; } for (int i = 1; i < name.Length - 1; i++) { if (char.IsUpper(name[i]) || (!char.IsLetterOrDigit(name[i]) && name[i] != '-')) { return false; } } return true; } private static bool IsIpV4Address(string name) { const byte IpV4AddressPartsCount = 4; var parts = name.Split('.'); if (parts.Length != IpV4AddressPartsCount) { return false; } foreach (var part in parts) { byte result; if (!byte.TryParse(part, out result)) { return false; } } return true; } #pragma warning disable CA1055 // Uri return values should not be strings /// /// Given a string the function will perform uri percent-encoding /// public static string UriEncode(string value) #pragma warning restore CA1055 // Uri return values should not be strings { return AWSSDKUtils.UrlEncode(value, false); } private static string[] SupportedSchemas = new string[] { "http", "https", "wss" }; /// /// Parses url string into URL object. /// Given a string the function will attempt to parse the string into it’s URL components. /// If the string can not be parsed into a valid URL then the function will return a null. /// If the URL given contains a query portion, the URL MUST be rejected and the function MUST return null. /// We only support "http" and "https" schemas at the moment. /// public static URL ParseURL(string url) { Uri uri; Uri.TryCreate(url, UriKind.Absolute, out uri); if (uri == null || uri.Query?.Length > 0 || !SupportedSchemas.Contains(uri.Scheme)) { return null; } var result = new URL { scheme = uri.Scheme, authority = uri.Authority, path = uri.GetComponents(UriComponents.Path, UriFormat.Unescaped) }; if (result.path.Length > 0) { result.path = "/" + result.path; } result.normalizedPath = uri.PathAndQuery; if (result.normalizedPath.Length > 1) { result.normalizedPath += "/"; } var hostNameType = Uri.CheckHostName(uri.Host); result.isIp = hostNameType == UriHostNameType.IPv4 || hostNameType == UriHostNameType.IPv6; return result; } /// /// Interpolate template placeholders with values from "refs" dictionary. /// /// e.g. Template "My url scheme is {url#scheme} for {region}", /// where "url" and "region" are keys in refs dictionary and "scheme" is property of object refs["url"]. /// Uses GetAttr() to resolve {} placeholders, i.e. {object#prop1.prop2[3]} -> GetAttr(refs["object"], "prop1.prop2[3]"). /// {{ and }} are considered as escape sequences to allow rule authors to output a literal { and } respectively. /// Throws ArgumentException if template is not well formed. /// public static string Interpolate(string template, Dictionary refs) { const char OpenBracket = '{'; const char CloseBracket = '}'; var result = new StringBuilder(); for (int i = 0; i < template.Length; i++) { var currentChar = template[i]; char nextChar = (i < template.Length - 1) ? template[i + 1] : default(char); // translate {{ -> { and }} -> } if (currentChar == OpenBracket && nextChar == OpenBracket) { result.Append(OpenBracket); i++; continue; } if (currentChar == CloseBracket && nextChar == CloseBracket) { result.Append(CloseBracket); i++; continue; } // translate {object#path} -> value if (currentChar == OpenBracket) { var placeholder = new StringBuilder(); while (i < template.Length - 1 && template[i + 1] != CloseBracket) { i++; placeholder.Append(template[i]); } if (i == template.Length - 1) { throw new ArgumentException("template is missing closing }"); } i++; var refParts = placeholder.ToString().Split('#'); var refName = refParts[0]; if (refParts.Length > 1) // has path after # { result.Append(GetAttr(refs[refName], refParts[1]).ToString()); } else { result.Append(refs[refName].ToString()); } } else if (currentChar == CloseBracket) { throw new ArgumentException("template has non-matching closing bracket, use }} to output }"); } else { result.Append(currentChar); } } return result.ToString(); } /// /// Interpolate all templates in all string nodes for given json /// public static string InterpolateJson(string json, Dictionary refs) { var jsonObject = JsonMapper.ToObject(json); InterpolateJson(jsonObject, refs); return jsonObject.ToJson(); } private static void InterpolateJson(JsonData json, Dictionary refs) { if (json.IsString) { var jsonWrapper = (IJsonWrapper)json; jsonWrapper.SetString(Interpolate(jsonWrapper.GetString(), refs)); } if (json.IsObject) { foreach (var key in json.PropertyNames) { InterpolateJson(json[key], refs); } } if (json.IsArray) { foreach (JsonData item in json) { InterpolateJson(item, refs); } } } /// /// Computes the substring of a given string, conditionally indexing from the end of the string. /// When the string is long enough to fully include the substring, return the substring. /// Otherwise, return null. The start index is inclusive and the stop index is exclusive. /// The length of the returned string will always be stop-start. /// Substring MUST return null when the input contains non-ascii (>127) characters. /// public static string Substring(string input, int start, int stop, bool reverse) { var str = (string)input; if (start >= stop || str.Length < stop) { return null; } if (str.Any(c => c > 127)) { return null; } if (!reverse) { return str.Substring(start, stop - start); } var r_start = str.Length - stop; var r_stop = str.Length - start; return str.Substring(r_start, r_stop - r_start); } } }