| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 | 
							- <?php
 
- namespace Aws\Credentials;
 
- use Aws\Configuration\ConfigurationResolver;
 
- use Aws\Exception\CredentialsException;
 
- use Aws\Exception\InvalidJsonException;
 
- use Aws\Sdk;
 
- use GuzzleHttp\Exception\TransferException;
 
- use GuzzleHttp\Promise;
 
- use GuzzleHttp\Psr7\Request;
 
- use GuzzleHttp\Promise\PromiseInterface;
 
- use Psr\Http\Message\ResponseInterface;
 
- /**
 
-  * Credential provider that provides credentials from the EC2 metadata service.
 
-  */
 
- class InstanceProfileProvider
 
- {
 
-     const CRED_PATH = 'meta-data/iam/security-credentials/';
 
-     const TOKEN_PATH = 'api/token';
 
-     const ENV_DISABLE = 'AWS_EC2_METADATA_DISABLED';
 
-     const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
 
-     const ENV_RETRIES = 'AWS_METADATA_SERVICE_NUM_ATTEMPTS';
 
-     const CFG_EC2_METADATA_V1_DISABLED = 'ec2_metadata_v1_disabled';
 
-     const CFG_EC2_METADATA_SERVICE_ENDPOINT = 'ec2_metadata_service_endpoint';
 
-     const CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE = 'ec2_metadata_service_endpoint_mode';
 
-     const DEFAULT_TIMEOUT = 1.0;
 
-     const DEFAULT_RETRIES = 3;
 
-     const DEFAULT_TOKEN_TTL_SECONDS = 21600;
 
-     const DEFAULT_AWS_EC2_METADATA_V1_DISABLED = false;
 
-     const ENDPOINT_MODE_IPv4 = 'IPv4';
 
-     const ENDPOINT_MODE_IPv6 = 'IPv6';
 
-     const DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT = 'http://169.254.169.254';
 
-     const DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT = 'http://[fd00:ec2::254]';
 
-     /** @var string */
 
-     private $profile;
 
-     /** @var callable */
 
-     private $client;
 
-     /** @var int */
 
-     private $retries;
 
-     /** @var int */
 
-     private $attempts;
 
-     /** @var float|mixed */
 
-     private $timeout;
 
-     /** @var bool */
 
-     private $secureMode = true;
 
-     /** @var bool|null */
 
-     private $ec2MetadataV1Disabled;
 
-     /** @var string */
 
-     private $endpoint;
 
-     /** @var string */
 
-     private $endpointMode;
 
-     /** @var array */
 
-     private $config;
 
-     /**
 
-      * The constructor accepts the following options:
 
-      *
 
-      * - timeout: Connection timeout, in seconds.
 
-      * - profile: Optional EC2 profile name, if known.
 
-      * - retries: Optional number of retries to be attempted.
 
-      * - ec2_metadata_v1_disabled: Optional for disabling the fallback to IMDSv1.
 
-      * - endpoint: Optional for overriding the default endpoint to be used for fetching credentials.
 
-      *   The value must contain a valid URI scheme. If the URI scheme is not https, it must
 
-      *   resolve to a loopback address.
 
-      * - endpoint_mode: Optional for overriding the default endpoint mode (IPv4|IPv6) to be used for
 
-      *   resolving the default endpoint.
 
-      * - use_aws_shared_config_files: Decides whether the shared config file should be considered when
 
-      *   using the ConfigurationResolver::resolve method.
 
-      *
 
-      * @param array $config Configuration options.
 
-      */
 
-     public function __construct(array $config = [])
 
-     {
 
-         $this->timeout = (float) getenv(self::ENV_TIMEOUT) ?: ($config['timeout'] ?? self::DEFAULT_TIMEOUT);
 
-         $this->profile = $config['profile'] ?? null;
 
-         $this->retries = (int) getenv(self::ENV_RETRIES) ?: ($config['retries'] ?? self::DEFAULT_RETRIES);
 
-         $this->client = $config['client'] ?? \Aws\default_http_handler();
 
-         $this->ec2MetadataV1Disabled = $config[self::CFG_EC2_METADATA_V1_DISABLED] ?? null;
 
-         $this->endpoint = $config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT] ?? null;
 
-         if (!empty($this->endpoint) && !$this->isValidEndpoint($this->endpoint)) {
 
-             throw new \InvalidArgumentException('The provided URI "' . $this->endpoint . '" is invalid, or contains an unsupported host');
 
-         }
 
-         $this->endpointMode = $config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE] ?? null;
 
-         $this->config = $config;
 
-     }
 
-     /**
 
-      * Loads instance profile credentials.
 
-      *
 
-      * @return PromiseInterface
 
-      */
 
-     public function __invoke($previousCredentials = null)
 
-     {
 
-         $this->attempts = 0;
 
-         return Promise\Coroutine::of(function () use ($previousCredentials) {
 
-             // Retrieve token or switch out of secure mode
 
-             $token = null;
 
-             while ($this->secureMode && is_null($token)) {
 
-                 try {
 
-                     $token = (yield $this->request(
 
-                         self::TOKEN_PATH,
 
-                         'PUT',
 
-                         [
 
-                             'x-aws-ec2-metadata-token-ttl-seconds' => self::DEFAULT_TOKEN_TTL_SECONDS
 
-                         ]
 
-                     ));
 
-                 } catch (TransferException $e) {
 
-                     if ($this->getExceptionStatusCode($e) === 500
 
-                         && $previousCredentials instanceof Credentials
 
-                     ) {
 
-                         goto generateCredentials;
 
-                     } elseif ($this->shouldFallbackToIMDSv1()
 
-                         && (!method_exists($e, 'getResponse')
 
-                         || empty($e->getResponse())
 
-                         || !in_array(
 
-                             $e->getResponse()->getStatusCode(),
 
-                             [400, 500, 502, 503, 504]
 
-                         ))
 
-                     ) {
 
-                         $this->secureMode = false;
 
-                     } else {
 
-                         $this->handleRetryableException(
 
-                             $e,
 
-                             [],
 
-                             $this->createErrorMessage(
 
-                                 'Error retrieving metadata token'
 
-                             )
 
-                         );
 
-                     }
 
-                 }
 
-                 $this->attempts++;
 
-             }
 
-             // Set token header only for secure mode
 
-             $headers = [];
 
-             if ($this->secureMode) {
 
-                 $headers = [
 
-                     'x-aws-ec2-metadata-token' => $token
 
-                 ];
 
-             }
 
-             // Retrieve profile
 
-             while (!$this->profile) {
 
-                 try {
 
-                     $this->profile = (yield $this->request(
 
-                         self::CRED_PATH,
 
-                         'GET',
 
-                         $headers
 
-                     ));
 
-                 } catch (TransferException $e) {
 
-                     // 401 indicates insecure flow not supported, switch to
 
-                     // attempting secure mode for subsequent calls
 
-                     if (!empty($this->getExceptionStatusCode($e))
 
-                         && $this->getExceptionStatusCode($e) === 401
 
-                     ) {
 
-                         $this->secureMode = true;
 
-                     }
 
-                     $this->handleRetryableException(
 
-                         $e,
 
-                         [ 'blacklist' => [401, 403] ],
 
-                         $this->createErrorMessage($e->getMessage())
 
-                     );
 
-                 }
 
-                 $this->attempts++;
 
-             }
 
-             // Retrieve credentials
 
-             $result = null;
 
-             while ($result == null) {
 
-                 try {
 
-                     $json = (yield $this->request(
 
-                         self::CRED_PATH . $this->profile,
 
-                         'GET',
 
-                         $headers
 
-                     ));
 
-                     $result = $this->decodeResult($json);
 
-                 } catch (InvalidJsonException $e) {
 
-                     $this->handleRetryableException(
 
-                         $e,
 
-                         [ 'blacklist' => [401, 403] ],
 
-                         $this->createErrorMessage(
 
-                             'Invalid JSON response, retries exhausted'
 
-                         )
 
-                     );
 
-                 } catch (TransferException $e) {
 
-                     // 401 indicates insecure flow not supported, switch to
 
-                     // attempting secure mode for subsequent calls
 
-                     if (($this->getExceptionStatusCode($e) === 500
 
-                             || strpos($e->getMessage(), "cURL error 28") !== false)
 
-                         && $previousCredentials instanceof Credentials
 
-                     ) {
 
-                         goto generateCredentials;
 
-                     } elseif (!empty($this->getExceptionStatusCode($e))
 
-                         && $this->getExceptionStatusCode($e) === 401
 
-                     ) {
 
-                         $this->secureMode = true;
 
-                     }
 
-                     $this->handleRetryableException(
 
-                         $e,
 
-                         [ 'blacklist' => [401, 403] ],
 
-                         $this->createErrorMessage($e->getMessage())
 
-                     );
 
-                 }
 
-                 $this->attempts++;
 
-             }
 
-             generateCredentials:
 
-             if (!isset($result)) {
 
-                 $credentials = $previousCredentials;
 
-             } else {
 
-                 $credentials = new Credentials(
 
-                     $result['AccessKeyId'],
 
-                     $result['SecretAccessKey'],
 
-                     $result['Token'],
 
-                     strtotime($result['Expiration'])
 
-                 );
 
-             }
 
-             if ($credentials->isExpired()) {
 
-                 $credentials->extendExpiration();
 
-             }
 
-             yield $credentials;
 
-         });
 
-     }
 
-     /**
 
-      * @param string $url
 
-      * @param string $method
 
-      * @param array $headers
 
-      * @return PromiseInterface Returns a promise that is fulfilled with the
 
-      *                          body of the response as a string.
 
-      */
 
-     private function request($url, $method = 'GET', $headers = [])
 
-     {
 
-         $disabled = getenv(self::ENV_DISABLE) ?: false;
 
-         if (strcasecmp($disabled, 'true') === 0) {
 
-             throw new CredentialsException(
 
-                 $this->createErrorMessage('EC2 metadata service access disabled')
 
-             );
 
-         }
 
-         $fn = $this->client;
 
-         $request = new Request($method, $this->resolveEndpoint() . $url);
 
-         $userAgent = 'aws-sdk-php/' . Sdk::VERSION;
 
-         if (defined('HHVM_VERSION')) {
 
-             $userAgent .= ' HHVM/' . HHVM_VERSION;
 
-         }
 
-         $userAgent .= ' ' . \Aws\default_user_agent();
 
-         $request = $request->withHeader('User-Agent', $userAgent);
 
-         foreach ($headers as $key => $value) {
 
-             $request = $request->withHeader($key, $value);
 
-         }
 
-         return $fn($request, ['timeout' => $this->timeout])
 
-             ->then(function (ResponseInterface $response) {
 
-                 return (string) $response->getBody();
 
-             })->otherwise(function (array $reason) {
 
-                 $reason = $reason['exception'];
 
-                 if ($reason instanceof TransferException) {
 
-                     throw $reason;
 
-                 }
 
-                 $msg = $reason->getMessage();
 
-                 throw new CredentialsException(
 
-                     $this->createErrorMessage($msg)
 
-                 );
 
-             });
 
-     }
 
-     private function handleRetryableException(
 
-         \Exception $e,
 
-         $retryOptions,
 
-         $message
 
-     ) {
 
-         $isRetryable = true;
 
-         if (!empty($status = $this->getExceptionStatusCode($e))
 
-             && isset($retryOptions['blacklist'])
 
-             && in_array($status, $retryOptions['blacklist'])
 
-         ) {
 
-             $isRetryable = false;
 
-         }
 
-         if ($isRetryable && $this->attempts < $this->retries) {
 
-             sleep((int) pow(1.2, $this->attempts));
 
-         } else {
 
-             throw new CredentialsException($message);
 
-         }
 
-     }
 
-     private function getExceptionStatusCode(\Exception $e)
 
-     {
 
-         if (method_exists($e, 'getResponse')
 
-             && !empty($e->getResponse())
 
-         ) {
 
-             return $e->getResponse()->getStatusCode();
 
-         }
 
-         return null;
 
-     }
 
-     private function createErrorMessage($previous)
 
-     {
 
-         return "Error retrieving credentials from the instance profile "
 
-             . "metadata service. ({$previous})";
 
-     }
 
-     private function decodeResult($response)
 
-     {
 
-         $result = json_decode($response, true);
 
-         if (json_last_error() > 0) {
 
-             throw new InvalidJsonException();
 
-         }
 
-         if ($result['Code'] !== 'Success') {
 
-             throw new CredentialsException('Unexpected instance profile '
 
-                 .  'response code: ' . $result['Code']);
 
-         }
 
-         return $result;
 
-     }
 
-     /**
 
-      * This functions checks for whether we should fall back to IMDSv1 or not.
 
-      * If $ec2MetadataV1Disabled is null then we will try to resolve this value from
 
-      * the following sources:
 
-      * - From environment: "AWS_EC2_METADATA_V1_DISABLED".
 
-      * - From config file: aws_ec2_metadata_v1_disabled
 
-      * - Defaulted to false
 
-      *
 
-      * @return bool
 
-      */
 
-     private function shouldFallbackToIMDSv1(): bool
 
-     {
 
-         $isImdsV1Disabled = \Aws\boolean_value($this->ec2MetadataV1Disabled)
 
-             ?? \Aws\boolean_value(
 
-                 ConfigurationResolver::resolve(
 
-                     self::CFG_EC2_METADATA_V1_DISABLED,
 
-                     self::DEFAULT_AWS_EC2_METADATA_V1_DISABLED,
 
-                     'bool',
 
-                     $this->config
 
-                 )
 
-             )
 
-             ?? self::DEFAULT_AWS_EC2_METADATA_V1_DISABLED;
 
-         return !$isImdsV1Disabled;
 
-     }
 
-     /**
 
-      * Resolves the metadata service endpoint. If the endpoint is not provided
 
-      * or configured then, the default endpoint, based on the endpoint mode resolved,
 
-      * will be used.
 
-      * Example: if endpoint_mode is resolved to be IPv4 and the endpoint is not provided
 
-      * then, the endpoint to be used will be http://169.254.169.254.
 
-      *
 
-      * @return string
 
-      */
 
-     private function resolveEndpoint(): string
 
-     {
 
-         $endpoint = $this->endpoint;
 
-         if (is_null($endpoint)) {
 
-             $endpoint = ConfigurationResolver::resolve(
 
-                 self::CFG_EC2_METADATA_SERVICE_ENDPOINT,
 
-                 $this->getDefaultEndpoint(),
 
-                 'string',
 
-                 $this->config
 
-             );
 
-         }
 
-         if (!$this->isValidEndpoint($endpoint)) {
 
-             throw new CredentialsException('The provided URI "' . $endpoint . '" is invalid, or contains an unsupported host');
 
-         }
 
-         if (substr($endpoint, strlen($endpoint) - 1) !== '/') {
 
-             $endpoint = $endpoint . '/';
 
-         }
 
-         return $endpoint . 'latest/';
 
-     }
 
-     /**
 
-      * Resolves the default metadata service endpoint.
 
-      * If endpoint_mode is resolved as IPv4 then:
 
-      * - endpoint = http://169.254.169.254
 
-      * If endpoint_mode is resolved as IPv6 then:
 
-      * - endpoint = http://[fd00:ec2::254]
 
-      *
 
-      * @return string
 
-      */
 
-     private function getDefaultEndpoint(): string
 
-     {
 
-         $endpointMode = $this->resolveEndpointMode();
 
-         switch ($endpointMode) {
 
-             case self::ENDPOINT_MODE_IPv4:
 
-                 return self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT;
 
-             case self::ENDPOINT_MODE_IPv6:
 
-                 return self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT;
 
-         }
 
-         throw new CredentialsException("Invalid endpoint mode '$endpointMode' resolved");
 
-     }
 
-     /**
 
-      * Resolves the endpoint mode to be considered when resolving the default
 
-      * metadata service endpoint.
 
-      *
 
-      * @return string
 
-      */
 
-     private function resolveEndpointMode(): string
 
-     {
 
-         $endpointMode = $this->endpointMode;
 
-         if (is_null($endpointMode)) {
 
-             $endpointMode = ConfigurationResolver::resolve(
 
-                 self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE,
 
-                     self::ENDPOINT_MODE_IPv4,
 
-                 'string',
 
-                 $this->config
 
-             );
 
-         }
 
-         return $endpointMode;
 
-     }
 
-     /**
 
-      * This method checks for whether a provide URI is valid.
 
-      * @param string $uri this parameter is the uri to do the validation against to.
 
-      *
 
-      * @return string|null
 
-      */
 
-     private function isValidEndpoint(
 
-         $uri
 
-     ): bool
 
-     {
 
-         // We make sure first the provided uri is a valid URL
 
-         $isValidURL = filter_var($uri, FILTER_VALIDATE_URL) !== false;
 
-         if (!$isValidURL) {
 
-             return false;
 
-         }
 
-         // We make sure that if is a no secure host then it must be a loop back address.
 
-         $parsedUri = parse_url($uri);
 
-         if ($parsedUri['scheme'] !== 'https') {
 
-             $host = trim($parsedUri['host'], '[]');
 
-             return CredentialsUtils::isLoopBackAddress(gethostbyname($host))
 
-                 || in_array(
 
-                     $uri,
 
-                     [self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT, self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT]
 
-                 );
 
-         }
 
-         return true;
 
-     }
 
- }
 
 
  |