timeout = (double) getenv(self::ENV_TIMEOUT) ?: (isset($config['timeout']) ? $config['timeout'] : 1.0); $this->profile = isset($config['profile']) ? $config['profile'] : null; $this->retries = (int) getenv(self::ENV_RETRIES) ?: (isset($config['retries']) ? $config['retries'] : 3); $this->attempts = 0; $this->client = isset($config['client']) ? $config['client'] : \UglyRobot\Infinite_Uploads\Aws\default_http_handler(); } /** * Loads instance profile credentials. * * @return PromiseInterface */ public function __invoke() { return \UglyRobot\Infinite_Uploads\GuzzleHttp\Promise\coroutine(function () { // 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' => 21600])); } catch (TransferException $e) { if (!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 (!empty($this->getExceptionStatusCode($e)) && $this->getExceptionStatusCode($e) === 401) { $this->secureMode = true; } $this->handleRetryableException($e, ['blacklist' => [401, 403]], $this->createErrorMessage($e->getMessage())); } $this->attempts++; } (yield new \UglyRobot\Infinite_Uploads\Aws\Credentials\Credentials($result['AccessKeyId'], $result['SecretAccessKey'], $result['Token'], strtotime($result['Expiration']))); }); } /** * @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 \UglyRobot\Infinite_Uploads\Aws\Exception\CredentialsException($this->createErrorMessage('EC2 metadata service access disabled')); } $fn = $this->client; $request = new \UglyRobot\Infinite_Uploads\GuzzleHttp\Psr7\Request($method, self::SERVER_URI . $url); $userAgent = 'aws-sdk-php/' . \UglyRobot\Infinite_Uploads\Aws\Sdk::VERSION; if (defined('HHVM_VERSION')) { $userAgent .= ' HHVM/' . HHVM_VERSION; } $userAgent .= ' ' . \UglyRobot\Infinite_Uploads\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 (\UglyRobot\Infinite_Uploads\Psr\Http\Message\ResponseInterface $response) { return (string) $response->getBody(); })->otherwise(function (array $reason) { $reason = $reason['exception']; if ($reason instanceof TransferException) { throw $reason; } $msg = $reason->getMessage(); throw new \UglyRobot\Infinite_Uploads\Aws\Exception\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(pow(1.2, $this->attempts)); } else { throw new \UglyRobot\Infinite_Uploads\Aws\Exception\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 \UglyRobot\Infinite_Uploads\Aws\Exception\InvalidJsonException(); } if ($result['Code'] !== 'Success') { throw new \UglyRobot\Infinite_Uploads\Aws\Exception\CredentialsException('Unexpected instance profile ' . 'response code: ' . $result['Code']); } return $result; } }