| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 | <?phpnamespace Aws\EndpointDiscovery;use Aws\AwsClient;use Aws\CacheInterface;use Aws\CommandInterface;use Aws\Credentials\CredentialsInterface;use Aws\Exception\AwsException;use Aws\Exception\UnresolvedEndpointException;use Aws\LruArrayCache;use Aws\Middleware;use Psr\Http\Message\RequestInterface;use Psr\Http\Message\UriInterface;class EndpointDiscoveryMiddleware{    /**     * @var CacheInterface     */    private static $cache;    private static $discoveryCooldown = 60;    private $args;    private $client;    private $config;    private $discoveryTimes = [];    private $nextHandler;    private $service;    public static function wrap(        $client,        $args,        $config    ) {        return function (callable $handler) use (            $client,            $args,            $config        ) {            return new static(                $handler,                $client,                $args,                $config            );        };    }    public function __construct(        callable $handler,        AwsClient $client,        array $args,        $config    ) {        $this->nextHandler = $handler;        $this->client = $client;        $this->args = $args;        $this->service = $client->getApi();        $this->config = $config;    }    public function __invoke(CommandInterface $cmd, RequestInterface $request)    {        $nextHandler = $this->nextHandler;        $op = $this->service->getOperation($cmd->getName())->toArray();        // Continue only if endpointdiscovery trait is set        if (isset($op['endpointdiscovery'])) {            $config = ConfigurationProvider::unwrap($this->config);            $isRequired = !empty($op['endpointdiscovery']['required']);            if ($isRequired && !($config->isEnabled())) {                throw new UnresolvedEndpointException('This operation '                    . 'requires the use of endpoint discovery, but this has '                    . 'been disabled in the configuration. Enable endpoint '                    . 'discovery or use a different operation.');            }            // Continue only if enabled by config            if ($config->isEnabled()) {                if (isset($op['endpointoperation'])) {                    throw new UnresolvedEndpointException('This operation is '                        . 'contradictorily marked both as using endpoint discovery '                        . 'and being the endpoint discovery operation. Please '                        . 'verify the accuracy of your model files.');                }                // Original endpoint may be used if discovery optional                $originalUri = $request->getUri();                $identifiers = $this->getIdentifiers($op);                $cacheKey = $this->getCacheKey(                    $this->client->getCredentials()->wait(),                    $cmd,                    $identifiers                );                // Check/create cache                if (!isset(self::$cache)) {                    self::$cache = new LruArrayCache($config->getCacheLimit());                }                if (empty($endpointList = self::$cache->get($cacheKey))) {                    $endpointList = new EndpointList([]);                }                $endpoint = $endpointList->getActive();                // Retrieve endpoints if there is no active endpoint                if (empty($endpoint)) {                    try {                        $endpoint = $this->discoverEndpoint(                            $cacheKey,                            $cmd,                            $identifiers                        );                    } catch (\Exception $e) {                        // Use cached endpoint, expired or active, if any remain                        $endpoint = $endpointList->getEndpoint();                        if (empty($endpoint)) {                            return $this->handleDiscoveryException(                                $isRequired,                                $originalUri,                                $e,                                $cmd,                                $request                            );                        }                    }                }                $request = $this->modifyRequest($request, $endpoint);                $g = function ($value) use (                    $cacheKey,                    $cmd,                    $identifiers,                    $isRequired,                    $originalUri,                    $request,                    &$endpoint,                    &$g                ) {                    if ($value instanceof AwsException                        && (                            $value->getAwsErrorCode() == 'InvalidEndpointException'                            || $value->getStatusCode() == 421                        )                    ) {                        return $this->handleInvalidEndpoint(                            $cacheKey,                            $cmd,                            $identifiers,                            $isRequired,                            $originalUri,                            $request,                            $value,                            $endpoint,                            $g                        );                    }                                        return $value;                };                return $nextHandler($cmd, $request)->otherwise($g);            }        }        return $nextHandler($cmd, $request);    }    private function discoverEndpoint(        $cacheKey,        CommandInterface $cmd,        array $identifiers    ) {        $discCmd = $this->getDiscoveryCommand($cmd, $identifiers);        $this->discoveryTimes[$cacheKey] = time();        $result = $this->client->execute($discCmd);        if (isset($result['Endpoints'])) {            $endpointData = [];            foreach ($result['Endpoints'] as $datum) {                $endpointData[$datum['Address']] = time()                    + ($datum['CachePeriodInMinutes'] * 60);            }            $endpointList = new EndpointList($endpointData);            self::$cache->set($cacheKey, $endpointList);            return $endpointList->getEndpoint();        }        throw new UnresolvedEndpointException('The endpoint discovery operation '            . 'yielded a response that did not contain properly formatted '            . 'endpoint data.');    }    private function getCacheKey(        CredentialsInterface $creds,        CommandInterface $cmd,        array $identifiers    ) {        $key = $this->service->getServiceName() . '_' . $creds->getAccessKeyId();        if (!empty($identifiers)) {            $key .= '_' . $cmd->getName();            foreach ($identifiers as $identifier) {                $key .= "_{$cmd[$identifier]}";            }        }        return $key;    }    private function getDiscoveryCommand(        CommandInterface $cmd,        array $identifiers    ) {        foreach ($this->service->getOperations() as $op) {            if (isset($op['endpointoperation'])) {                $endpointOperation = $op->toArray()['name'];                break;            }        }        if (!isset($endpointOperation)) {            throw new UnresolvedEndpointException('This command is set to use '                . 'endpoint discovery, but no endpoint discovery operation was '                . 'found. Please verify the accuracy of your model files.');        }        $params = [];        if (!empty($identifiers)) {            $params['Operation'] = $cmd->getName();            $params['Identifiers'] = [];            foreach ($identifiers as $identifier) {                $params['Identifiers'][$identifier] = $cmd[$identifier];            }        }        $command = $this->client->getCommand($endpointOperation, $params);        $command->getHandlerList()->appendBuild(            Middleware::mapRequest(function (RequestInterface $r) {                return $r->withHeader(                    'x-amz-api-version',                    $this->service->getApiVersion()                );            }),            'x-amz-api-version-header'        );        return $command;    }    private function getIdentifiers(array $operation)    {        $inputShape = $this->service->getShapeMap()            ->resolve($operation['input'])            ->toArray();        $identifiers = [];        foreach ($inputShape['members'] as $key => $member) {            if (!empty($member['endpointdiscoveryid'])) {                $identifiers[] = $key;            }        }        return $identifiers;    }    private function handleDiscoveryException(        $isRequired,        $originalUri,        \Exception $e,        CommandInterface $cmd,        RequestInterface $request    ) {        // If no cached endpoints and discovery required,        // throw exception        if ($isRequired) {            $message = 'The endpoint required for this service is currently '                . 'unable to be retrieved, and your request can not be fulfilled '                . 'unless you manually specify an endpoint.';            throw new AwsException(                $message,                $cmd,                [                    'code' => 'EndpointDiscoveryException',                    'message' => $message                ],                $e            );        }        // If discovery isn't required, use original endpoint        return $this->useOriginalUri(            $originalUri,            $cmd,            $request        );    }    private function handleInvalidEndpoint(        $cacheKey,        $cmd,        $identifiers,        $isRequired,        $originalUri,        $request,        $value,        &$endpoint,        &$g    ) {        $nextHandler = $this->nextHandler;        $endpointList = self::$cache->get($cacheKey);        if ($endpointList instanceof EndpointList) {            // Remove invalid endpoint from cached list            $endpointList->remove($endpoint);            // If possible, get another cached endpoint            $newEndpoint = $endpointList->getEndpoint();        }        if (empty($newEndpoint)) {            // If no more cached endpoints, make discovery call            // if none made within cooldown for given key            if (time() - $this->discoveryTimes[$cacheKey]                < self::$discoveryCooldown            ) {                // If no more cached endpoints and it's required,                // fail with original exception                if ($isRequired) {                    return $value;                }                // Use original endpoint if not required                return $this->useOriginalUri(                    $originalUri,                    $cmd,                    $request                );            }            $newEndpoint = $this->discoverEndpoint(                $cacheKey,                $cmd,                $identifiers            );        }        $endpoint = $newEndpoint;        $request = $this->modifyRequest($request, $endpoint);        return $nextHandler($cmd, $request)->otherwise($g);    }    private function modifyRequest(RequestInterface $request, $endpoint)    {        $parsed = $this->parseEndpoint($endpoint);        if (!empty($request->getHeader('User-Agent'))) {            $userAgent = $request->getHeader('User-Agent')[0];            if (strpos($userAgent, 'endpoint-discovery') === false) {                $userAgent = $userAgent . ' endpoint-discovery';            }        } else {            $userAgent = 'endpoint-discovery';        }        return $request            ->withUri(                $request->getUri()                    ->withHost($parsed['host'])                    ->withPath($parsed['path'])            )            ->withHeader('User-Agent', $userAgent);    }    /**     * Parses an endpoint returned from the discovery API into an array with     * 'host' and 'path' keys.     *     * @param $endpoint     * @return array     */    private function parseEndpoint($endpoint)    {        $parsed = parse_url($endpoint);        // parse_url() will correctly parse full URIs with schemes        if (isset($parsed['host'])) {            return $parsed;        }        // parse_url() will put host & path in 'path' if scheme is not provided        if (isset($parsed['path'])) {            $split = explode('/', $parsed['path'], 2);            $parsed['host'] = $split[0];            if (isset($split[1])) {                if (substr($split[1], 0 , 1) !== '/') {                    $split[1] = '/' . $split[1];                }                $parsed['path'] = $split[1];            } else {                $parsed['path'] = '';            }            return $parsed;        }        throw new UnresolvedEndpointException("The supplied endpoint '"            . "{$endpoint}' is invalid.");    }    private function useOriginalUri(        UriInterface $uri,        CommandInterface $cmd,        RequestInterface $request    ) {        $nextHandler = $this->nextHandler;        $endpoint = $uri->getHost() . $uri->getPath();        $request = $this->modifyRequest(            $request,            $endpoint        );        return $nextHandler($cmd, $request);    }}
 |