Ruleset.php
11 months ago
RulesetEndpoint.php
11 months ago
RulesetParameter.php
11 months ago
RulesetStandardLibrary.php
11 months ago
RulesetStandardLibrary.php
429 lines
| 1 | <?php |
| 2 | |
| 3 | namespace Aws\EndpointV2\Ruleset; |
| 4 | |
| 5 | use Aws\Exception\UnresolvedEndpointException; |
| 6 | |
| 7 | /** |
| 8 | * Provides functions and actions to be performed for endpoint evaluation. |
| 9 | * This is an internal only class and is not subject to backwards-compatibility guarantees. |
| 10 | * |
| 11 | * @internal |
| 12 | */ |
| 13 | class RulesetStandardLibrary |
| 14 | { |
| 15 | const IPV4_RE = '/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/'; |
| 16 | const IPV6_RE = '/([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:| |
| 17 | . ([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F] |
| 18 | . {1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:) |
| 19 | . {1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}| |
| 20 | . [0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80: |
| 21 | . (:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]| |
| 22 | . 1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F] |
| 23 | . {1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4] |
| 24 | . |1{0,1}[0-9]){0,1}[0-9])/'; |
| 25 | const TEMPLATE_ESCAPE_RE = '/{\{\s*(.*?)\s*\}\}/'; |
| 26 | const TEMPLATE_SEARCH_RE = '/\{[a-zA-Z#]+\}/'; |
| 27 | const TEMPLATE_PARSE_RE = '#\{((?>[^\{\}]+)|(?R))*\}#x'; |
| 28 | const HOST_LABEL_RE = '/^(?!-)[a-zA-Z\d-]{1,63}(?<!-)$/'; |
| 29 | |
| 30 | private $partitions; |
| 31 | |
| 32 | public function __construct($partitions) |
| 33 | { |
| 34 | $this->partitions = $partitions; |
| 35 | } |
| 36 | |
| 37 | /** |
| 38 | * Determines if a value is set. |
| 39 | * |
| 40 | * @return boolean |
| 41 | */ |
| 42 | public function is_set($value) |
| 43 | { |
| 44 | return isset($value); |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Function implementation of logical operator `not` |
| 49 | * |
| 50 | * @return boolean |
| 51 | */ |
| 52 | public function not($value) |
| 53 | { |
| 54 | return !$value; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Find an attribute within a value given a path string. |
| 59 | * |
| 60 | * @return mixed |
| 61 | */ |
| 62 | public function getAttr($from, $path) |
| 63 | { |
| 64 | $parts = explode('.', $path); |
| 65 | foreach ($parts as $part) { |
| 66 | $sliceIdx = strpos($part, '['); |
| 67 | if ($sliceIdx !== false) { |
| 68 | if (substr($part, -1) !== ']') { |
| 69 | return null; |
| 70 | } |
| 71 | $slice = intval(substr($part, $sliceIdx + 1, strlen($part) - 1)); |
| 72 | $from = isset($from[substr($part,0, $sliceIdx)][$slice]) |
| 73 | ? $from[substr($part,0, $sliceIdx)][$slice] |
| 74 | : null; |
| 75 | } else { |
| 76 | $from = $from[$part]; |
| 77 | } |
| 78 | } |
| 79 | return $from; |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Computes a substring given the start index and end index. If `reverse` is |
| 84 | * true, slice the string from the end instead. |
| 85 | * |
| 86 | * @return mixed |
| 87 | */ |
| 88 | public function substring($input, $start, $stop, $reverse) |
| 89 | { |
| 90 | if (!is_string($input)) { |
| 91 | throw new UnresolvedEndpointException( |
| 92 | 'Input passed to `substring` must be `string`.' |
| 93 | ); |
| 94 | } |
| 95 | |
| 96 | if (preg_match('/[^\x00-\x7F]/', $input)) { |
| 97 | return null; |
| 98 | } |
| 99 | if ($start >= $stop or strlen($input) < $stop) { |
| 100 | return null; |
| 101 | } |
| 102 | if (!$reverse) { |
| 103 | return substr($input, $start, $stop - $start); |
| 104 | } else { |
| 105 | $offset = strlen($input) - $stop; |
| 106 | $length = $stop - $start; |
| 107 | return substr($input, $offset, $length); |
| 108 | } |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Evaluates two strings for equality. |
| 113 | * |
| 114 | * @return boolean |
| 115 | */ |
| 116 | public function stringEquals($string1, $string2) |
| 117 | { |
| 118 | if (!is_string($string1) || !is_string($string2)) { |
| 119 | throw new UnresolvedEndpointException( |
| 120 | 'Values passed to StringEquals must be `string`.' |
| 121 | ); |
| 122 | } |
| 123 | return $string1 === $string2; |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * Evaluates two booleans for equality. |
| 128 | * |
| 129 | * @return boolean |
| 130 | */ |
| 131 | public function booleanEquals($boolean1, $boolean2) |
| 132 | { |
| 133 | return |
| 134 | filter_var($boolean1, FILTER_VALIDATE_BOOLEAN) |
| 135 | === filter_var($boolean2, FILTER_VALIDATE_BOOLEAN); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Percent-encodes an input string. |
| 140 | * |
| 141 | * @return mixed |
| 142 | */ |
| 143 | public function uriEncode($input) |
| 144 | { |
| 145 | if (is_null($input)) { |
| 146 | return null; |
| 147 | } |
| 148 | return str_replace('%7E', '~', rawurlencode($input)); |
| 149 | } |
| 150 | |
| 151 | /** |
| 152 | * Parses URL string into components. |
| 153 | * |
| 154 | * @return mixed |
| 155 | */ |
| 156 | public function parseUrl($url) |
| 157 | { |
| 158 | if (is_null($url)) { |
| 159 | return null; |
| 160 | } |
| 161 | |
| 162 | $parsed = parse_url($url); |
| 163 | |
| 164 | if ($parsed === false || !empty($parsed['query'])) { |
| 165 | return null; |
| 166 | } elseif (!isset($parsed['scheme'])) { |
| 167 | return null; |
| 168 | } |
| 169 | |
| 170 | if ($parsed['scheme'] !== 'http' |
| 171 | && $parsed['scheme'] !== 'https' |
| 172 | ) { |
| 173 | return null; |
| 174 | } |
| 175 | |
| 176 | $urlInfo = []; |
| 177 | $urlInfo['scheme'] = $parsed['scheme']; |
| 178 | $urlInfo['authority'] = isset($parsed['host']) ? $parsed['host'] : ''; |
| 179 | if (isset($parsed['port'])) { |
| 180 | $urlInfo['authority'] = $urlInfo['authority'] . ":" . $parsed['port']; |
| 181 | } |
| 182 | $urlInfo['path'] = isset($parsed['path']) ? $parsed['path'] : ''; |
| 183 | $urlInfo['normalizedPath'] = !empty($parsed['path']) |
| 184 | ? rtrim($urlInfo['path'] ?: '', '/' . "/") . '/' |
| 185 | : '/'; |
| 186 | $urlInfo['isIp'] = !isset($parsed['host']) ? |
| 187 | 'false' : $this->isValidIp($parsed['host']); |
| 188 | |
| 189 | return $urlInfo; |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Evaluates whether a value is a valid host label per |
| 194 | * RFC 1123. If allow_subdomains is true, split on `.` and validate |
| 195 | * each subdomain separately. |
| 196 | * |
| 197 | * @return boolean |
| 198 | */ |
| 199 | public function isValidHostLabel($hostLabel, $allowSubDomains) |
| 200 | { |
| 201 | if (!isset($hostLabel) |
| 202 | || (!$allowSubDomains && strpos($hostLabel, '.') != false) |
| 203 | ) { |
| 204 | return false; |
| 205 | } |
| 206 | |
| 207 | if ($allowSubDomains) { |
| 208 | foreach (explode('.', $hostLabel) as $subdomain) { |
| 209 | if (!$this->validateHostLabel($subdomain)) { |
| 210 | return false; |
| 211 | } |
| 212 | } |
| 213 | return true; |
| 214 | } else { |
| 215 | return $this->validateHostLabel($hostLabel); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | /** |
| 220 | * Parse and validate string for ARN components. |
| 221 | * |
| 222 | * @return array|null |
| 223 | */ |
| 224 | public function parseArn($arnString) |
| 225 | { |
| 226 | if (is_null($arnString) |
| 227 | || substr( $arnString, 0, 3 ) !== "arn" |
| 228 | ) { |
| 229 | return null; |
| 230 | } |
| 231 | |
| 232 | $arn = []; |
| 233 | $parts = explode(':', $arnString, 6); |
| 234 | if (sizeof($parts) < 6) { |
| 235 | return null; |
| 236 | } |
| 237 | |
| 238 | $arn['partition'] = isset($parts[1]) ? $parts[1] : null; |
| 239 | $arn['service'] = isset($parts[2]) ? $parts[2] : null; |
| 240 | $arn['region'] = isset($parts[3]) ? $parts[3] : null; |
| 241 | $arn['accountId'] = isset($parts[4]) ? $parts[4] : null; |
| 242 | $arn['resourceId'] = isset($parts[5]) ? $parts[5] : null; |
| 243 | |
| 244 | if (empty($arn['partition']) |
| 245 | || empty($arn['service']) |
| 246 | || empty($arn['resourceId']) |
| 247 | ) { |
| 248 | return null; |
| 249 | } |
| 250 | $resource = $arn['resourceId']; |
| 251 | $arn['resourceId'] = preg_split("/[:\/]/", $resource); |
| 252 | |
| 253 | return $arn; |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * Matches a region string to an AWS partition. |
| 258 | * |
| 259 | * @return mixed |
| 260 | */ |
| 261 | public function partition($region) |
| 262 | { |
| 263 | if (!is_string($region)) { |
| 264 | throw new UnresolvedEndpointException( |
| 265 | 'Value passed to `partition` must be `string`.' |
| 266 | ); |
| 267 | } |
| 268 | |
| 269 | $partitions = $this->partitions; |
| 270 | foreach ($partitions['partitions'] as $partition) { |
| 271 | if (array_key_exists($region, $partition['regions']) |
| 272 | || preg_match("/{$partition['regionRegex']}/", $region) |
| 273 | ) { |
| 274 | return $partition['outputs']; |
| 275 | } |
| 276 | } |
| 277 | //return `aws` partition if no match is found. |
| 278 | return $partitions['partitions'][0]['outputs']; |
| 279 | } |
| 280 | |
| 281 | /** |
| 282 | * Evaluates whether a value is a valid bucket name for virtual host |
| 283 | * style bucket URLs. |
| 284 | * |
| 285 | * @return boolean |
| 286 | */ |
| 287 | public function isVirtualHostableS3Bucket($bucketName, $allowSubdomains) |
| 288 | { |
| 289 | if ((is_null($bucketName) |
| 290 | || (strlen($bucketName) < 3 || strlen($bucketName) > 63)) |
| 291 | || preg_match(self::IPV4_RE, $bucketName) |
| 292 | || strtolower($bucketName) !== $bucketName |
| 293 | ) { |
| 294 | return false; |
| 295 | } |
| 296 | |
| 297 | if ($allowSubdomains) { |
| 298 | $labels = explode('.', $bucketName); |
| 299 | $results = []; |
| 300 | forEach($labels as $label) { |
| 301 | $results[] = $this->isVirtualHostableS3Bucket($label, false); |
| 302 | } |
| 303 | return !in_array(false, $results); |
| 304 | } |
| 305 | return $this->isValidHostLabel($bucketName, false); |
| 306 | } |
| 307 | |
| 308 | public function callFunction($funcCondition, &$inputParameters) |
| 309 | { |
| 310 | $funcArgs = []; |
| 311 | |
| 312 | forEach($funcCondition['argv'] as $arg) { |
| 313 | $funcArgs[] = $this->resolveValue($arg, $inputParameters); |
| 314 | } |
| 315 | |
| 316 | $funcName = str_replace('aws.', '', $funcCondition['fn']); |
| 317 | if ($funcName === 'isSet') { |
| 318 | $funcName = 'is_set'; |
| 319 | } |
| 320 | |
| 321 | $result = call_user_func_array( |
| 322 | [RulesetStandardLibrary::class, $funcName], |
| 323 | $funcArgs |
| 324 | ); |
| 325 | |
| 326 | if (isset($funcCondition['assign'])) { |
| 327 | $assign = $funcCondition['assign']; |
| 328 | if (isset($inputParameters[$assign])){ |
| 329 | throw new UnresolvedEndpointException( |
| 330 | "Assignment `{$assign}` already exists in input parameters" . |
| 331 | " or has already been assigned by an endpoint rule and cannot be overwritten." |
| 332 | ); |
| 333 | } |
| 334 | $inputParameters[$assign] = $result; |
| 335 | } |
| 336 | return $result; |
| 337 | } |
| 338 | |
| 339 | public function resolveValue($value, $inputParameters) |
| 340 | { |
| 341 | //Given a value, check if it's a function, reference or template. |
| 342 | //returns resolved value |
| 343 | if ($this->isFunc($value)) { |
| 344 | return $this->callFunction($value, $inputParameters); |
| 345 | } elseif ($this->isRef($value)) { |
| 346 | return isset($inputParameters[$value['ref']]) ? $inputParameters[$value['ref']] : null; |
| 347 | } elseif ($this->isTemplate($value)) { |
| 348 | return $this->resolveTemplateString($value, $inputParameters); |
| 349 | } |
| 350 | return $value; |
| 351 | } |
| 352 | |
| 353 | public function isFunc($arg) |
| 354 | { |
| 355 | return is_array($arg) && isset($arg['fn']); |
| 356 | } |
| 357 | |
| 358 | public function isRef($arg) |
| 359 | { |
| 360 | return is_array($arg) && isset($arg['ref']); |
| 361 | } |
| 362 | |
| 363 | public function isTemplate($arg) |
| 364 | { |
| 365 | return is_string($arg) && !empty(preg_match(self::TEMPLATE_SEARCH_RE, $arg)); |
| 366 | } |
| 367 | |
| 368 | public function resolveTemplateString($value, $inputParameters) |
| 369 | { |
| 370 | return preg_replace_callback( |
| 371 | self::TEMPLATE_PARSE_RE, |
| 372 | function ($match) use ($inputParameters) { |
| 373 | if (preg_match(self::TEMPLATE_ESCAPE_RE, $match[0])) { |
| 374 | return $match[1]; |
| 375 | } |
| 376 | |
| 377 | $notFoundMessage = 'Resolved value was null. Please check rules and ' . |
| 378 | 'input parameters and try again.'; |
| 379 | |
| 380 | $parts = explode("#", $match[1]); |
| 381 | if (count($parts) > 1) { |
| 382 | $resolvedValue = $inputParameters; |
| 383 | foreach($parts as $part) { |
| 384 | if (!isset($resolvedValue[$part])) { |
| 385 | throw new UnresolvedEndpointException($notFoundMessage); |
| 386 | } |
| 387 | $resolvedValue = $resolvedValue[$part]; |
| 388 | } |
| 389 | return $resolvedValue; |
| 390 | } else { |
| 391 | if (!isset($inputParameters[$parts[0]])) { |
| 392 | throw new UnresolvedEndpointException($notFoundMessage); |
| 393 | } |
| 394 | return $inputParameters[$parts[0]]; |
| 395 | } |
| 396 | }, |
| 397 | $value |
| 398 | ); |
| 399 | } |
| 400 | |
| 401 | private function validateHostLabel ($hostLabel) |
| 402 | { |
| 403 | if (empty($hostLabel) || strlen($hostLabel) > 63) { |
| 404 | return false; |
| 405 | } |
| 406 | if (preg_match(self::HOST_LABEL_RE, $hostLabel)) { |
| 407 | return true; |
| 408 | } |
| 409 | return false; |
| 410 | } |
| 411 | |
| 412 | private function isValidIp($hostName) |
| 413 | { |
| 414 | $isWrapped = strpos($hostName, '[') === 0 |
| 415 | && strrpos($hostName, ']') === strlen($hostName) - 1; |
| 416 | |
| 417 | return preg_match( |
| 418 | self::IPV4_RE, |
| 419 | $hostName |
| 420 | ) |
| 421 | //IPV6 enclosed in brackets |
| 422 | || ($isWrapped && preg_match( |
| 423 | self::IPV6_RE, |
| 424 | $hostName |
| 425 | )) |
| 426 | ? 'true' : 'false'; |
| 427 | } |
| 428 | } |
| 429 |