<?php

namespace VendorDuplicator\Aws\Credentials;

use VendorDuplicator\Aws\Exception\CredentialsException;
use VendorDuplicator\Aws\Exception\InvalidJsonException;
use VendorDuplicator\Aws\Sdk;
use VendorDuplicator\GuzzleHttp\Exception\TransferException;
use VendorDuplicator\GuzzleHttp\Promise;
use VendorDuplicator\GuzzleHttp\Exception\RequestException;
use VendorDuplicator\GuzzleHttp\Psr7\Request;
use VendorDuplicator\GuzzleHttp\Promise\PromiseInterface;
use VendorDuplicator\Psr\Http\Message\ResponseInterface;
/**
 * Credential provider that provides credentials from the EC2 metadata service.
 */
class InstanceProfileProvider
{
    const SERVER_URI = 'http://169.254.169.254/latest/';
    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';
    /** @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;
    /**
     * 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.
     *
     * @param array $config Configuration options.
     */
    public function __construct(array $config = [])
    {
        $this->timeout = (float) \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->client = isset($config['client']) ? $config['client'] : \VendorDuplicator\Aws\default_http_handler();
    }
    /**
     * 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' => 21600]));
                } catch (TransferException $e) {
                    if ($this->getExceptionStatusCode($e) === 500 && $previousCredentials instanceof Credentials) {
                        goto generateCredentials;
                    } else {
                        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 (($this->getExceptionStatusCode($e) === 500 || \strpos($e->getMessage(), "cURL error 28") !== \false) && $previousCredentials instanceof Credentials) {
                        goto generateCredentials;
                    } else {
                        if (!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, self::SERVER_URI . $url);
        $userAgent = 'aws-sdk-php/' . Sdk::VERSION;
        if (\defined('HHVM_VERSION')) {
            $userAgent .= ' HHVM/' . HHVM_VERSION;
        }
        $userAgent .= ' ' . \VendorDuplicator\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;
    }
}
