new appraoch

This commit is contained in:
Tim Bendt
2025-11-26 13:22:58 -05:00
parent de3d100844
commit c520b7df89
6760 changed files with 1009780 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
## Code of Conduct
This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
opensource-codeofconduct@amazon.com with any additional questions or comments.

View File

@@ -0,0 +1,4 @@
## Building and enabling the Common Run Time
1. **Follow instructions on crt repo** Clone and build the repo as shown [here][https://github.com/awslabs/aws-crt-php].
1. **Enable the CRT** add the following line to your php.ini file `extension=path/to/aws-crt-php/modules/awscrt.so`

View File

@@ -0,0 +1,141 @@
# Apache License
Version 2.0, January 2004
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
## 1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1
through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the
License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled
by, or are under common control with that entity. For the purposes of this definition, "control" means
(i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract
or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software
source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form,
including but not limited to compiled object code, generated documentation, and conversions to other media
types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License,
as indicated by a copyright notice that is included in or attached to the work (an example is provided in the
Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from)
the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent,
as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not
include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work
and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any
modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to
Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to
submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of
electronic, verbal, or written communication sent to the Licensor or its representatives, including but not
limited to communication on electronic mailing lists, source code control systems, and issue tracking systems
that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been
received by Licensor and subsequently incorporated within the Work.
## 2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare
Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
## 3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent
license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such
license applies only to those patent claims licensable by such Contributor that are necessarily infringed by
their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such
Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim
or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work
constitutes direct or contributory patent infringement, then any patent licenses granted to You under this
License for that Work shall terminate as of the date such litigation is filed.
## 4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You meet the following conditions:
1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
2. You must cause any modified files to carry prominent notices stating that You changed the files; and
3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent,
trademark, and attribution notices from the Source form of the Work, excluding those notices that do
not pertain to any part of the Derivative Works; and
4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that
You distribute must include a readable copy of the attribution notices contained within such NOTICE
file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed as part of the Derivative Works; within
the Source form or documentation, if provided along with the Derivative Works; or, within a display
generated by the Derivative Works, if and wherever such third-party notices normally appear. The
contents of the NOTICE file are for informational purposes only and do not modify the License. You may
add Your own attribution notices within Derivative Works that You distribute, alongside or as an
addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be
construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license
terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative
Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the
conditions stated in this License.
## 5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by
You to the Licensor shall be under the terms and conditions of this License, without any additional terms or
conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate
license agreement you may have executed with Licensor regarding such Contributions.
## 6. Trademarks.
This License does not grant permission to use the trade names, trademarks, service marks, or product names of
the Licensor, except as required for reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
## 7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor
provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT,
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
## 8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless
required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any
Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential
damages of any character arising as a result of this License or out of the use or inability to use the Work
(including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has been advised of the possibility
of such damages.
## 9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole
responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold
each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

17
pancake/system/vendor/aws/aws-sdk-php/NOTICE vendored Executable file
View File

@@ -0,0 +1,17 @@
# AWS SDK for PHP
<http://aws.amazon.com/php>
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License").
You may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,84 @@
The AWS SDK for PHP includes the following third-party software/licensing:
** Guzzle - https://github.com/guzzle/guzzle
Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
----------------
** jmespath.php - https://github.com/mtdowling/jmespath.php
Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
----------------
** phpunit-mock-objects -- https://github.com/sebastianbergmann/phpunit-mock-objects
Copyright (c) 2002-2018, Sebastian Bergmann <sebastian@phpunit.de>.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
* Neither the name of Sebastian Bergmann nor the names of his
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,74 @@
{
"name": "aws/aws-sdk-php",
"homepage": "http://aws.amazon.com/sdkforphp",
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"keywords": ["aws","amazon","sdk","s3","ec2","dynamodb","cloud","glacier"],
"type": "library",
"license": "Apache-2.0",
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues"
},
"require": {
"php": ">=5.5",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
"guzzlehttp/promises": "^1.4.0",
"mtdowling/jmespath.php": "^2.6",
"ext-pcre": "*",
"ext-json": "*",
"ext-simplexml": "*",
"aws/aws-crt-php": "^1.0.4",
"psr/http-message": "^1.0"
},
"require-dev": {
"composer/composer" : "^1.10.22",
"ext-openssl": "*",
"ext-dom": "*",
"ext-pcntl": "*",
"ext-sockets": "*",
"phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
"behat/behat": "~3.0",
"doctrine/cache": "~1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"nette/neon": "^2.3",
"andrewsville/php-token-reflection": "^1.4",
"psr/cache": "^1.0",
"psr/simple-cache": "^1.0",
"paragonie/random_compat": ">= 2",
"sebastian/comparator": "^1.2.3 || ^4.0",
"yoast/phpunit-polyfills": "^1.0",
"dms/phpunit-arraysubset-asserts": "^0.4.0"
},
"suggest": {
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-curl": "To send requests using cURL",
"ext-sockets": "To use client-side monitoring",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications"
},
"autoload": {
"psr-4": {
"Aws\\": "src/"
},
"files": ["src/functions.php"]
},
"autoload-dev": {
"psr-4": {
"Aws\\Test\\": "tests/"
},
"classmap": ["build/"]
},
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Aws\ARCZonalShift;
use Aws\AwsClient;
/**
* This client is used to interact with the **AWS ARC - Zonal Shift** service.
* @method \Aws\Result cancelZonalShift(array $args = [])
* @method \GuzzleHttp\Promise\Promise cancelZonalShiftAsync(array $args = [])
* @method \Aws\Result getManagedResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise getManagedResourceAsync(array $args = [])
* @method \Aws\Result listManagedResources(array $args = [])
* @method \GuzzleHttp\Promise\Promise listManagedResourcesAsync(array $args = [])
* @method \Aws\Result listZonalShifts(array $args = [])
* @method \GuzzleHttp\Promise\Promise listZonalShiftsAsync(array $args = [])
* @method \Aws\Result startZonalShift(array $args = [])
* @method \GuzzleHttp\Promise\Promise startZonalShiftAsync(array $args = [])
* @method \Aws\Result updateZonalShift(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateZonalShiftAsync(array $args = [])
*/
class ARCZonalShiftClient extends AwsClient {}

View File

@@ -0,0 +1,9 @@
<?php
namespace Aws\ARCZonalShift\Exception;
use Aws\Exception\AwsException;
/**
* Represents an error interacting with the **AWS ARC - Zonal Shift** service.
*/
class ARCZonalShiftException extends AwsException {}

View File

@@ -0,0 +1,157 @@
<?php
namespace Aws;
use GuzzleHttp\Promise;
/**
* A configuration provider is a function that returns a promise that is
* fulfilled with a configuration object. This class provides base functionality
* usable by specific configuration provider implementations
*/
abstract class AbstractConfigurationProvider
{
const ENV_PROFILE = 'AWS_PROFILE';
const ENV_CONFIG_FILE = 'AWS_CONFIG_FILE';
public static $cacheKey;
protected static $interfaceClass;
protected static $exceptionClass;
/**
* Wraps a config provider and saves provided configuration in an
* instance of Aws\CacheInterface. Forwards calls when no config found
* in cache and updates cache with the results.
*
* @param callable $provider Configuration provider function to wrap
* @param CacheInterface $cache Cache to store configuration
* @param string|null $cacheKey (optional) Cache key to use
*
* @return callable
*/
public static function cache(
callable $provider,
CacheInterface $cache,
$cacheKey = null
) {
$cacheKey = $cacheKey ?: static::$cacheKey;
return function () use ($provider, $cache, $cacheKey) {
$found = $cache->get($cacheKey);
if ($found instanceof static::$interfaceClass) {
return Promise\Create::promiseFor($found);
}
return $provider()
->then(function ($config) use (
$cache,
$cacheKey
) {
$cache->set($cacheKey, $config);
return $config;
});
};
}
/**
* Creates an aggregate configuration provider that invokes the provided
* variadic providers one after the other until a provider returns
* configuration.
*
* @return callable
*/
public static function chain()
{
$links = func_get_args();
if (empty($links)) {
throw new \InvalidArgumentException('No providers in chain');
}
return function () use ($links) {
/** @var callable $parent */
$parent = array_shift($links);
$promise = $parent();
while ($next = array_shift($links)) {
$promise = $promise->otherwise($next);
}
return $promise;
};
}
/**
* Gets the environment's HOME directory if available.
*
* @return null|string
*/
protected static function getHomeDir()
{
// On Linux/Unix-like systems, use the HOME environment variable
if ($homeDir = getenv('HOME')) {
return $homeDir;
}
// Get the HOMEDRIVE and HOMEPATH values for Windows hosts
$homeDrive = getenv('HOMEDRIVE');
$homePath = getenv('HOMEPATH');
return ($homeDrive && $homePath) ? $homeDrive . $homePath : null;
}
/**
* Gets default config file location from environment, falling back to aws
* default location
*
* @return string
*/
protected static function getDefaultConfigFilename()
{
if ($filename = getenv(self::ENV_CONFIG_FILE)) {
return $filename;
}
return self::getHomeDir() . '/.aws/config';
}
/**
* Wraps a config provider and caches previously provided configuration.
*
* @param callable $provider Config provider function to wrap.
*
* @return callable
*/
public static function memoize(callable $provider)
{
return function () use ($provider) {
static $result;
static $isConstant;
// Constant config will be returned constantly.
if ($isConstant) {
return $result;
}
// Create the initial promise that will be used as the cached value
if (null === $result) {
$result = $provider();
}
// Return config and set flag that provider is already set
return $result
->then(function ($config) use (&$isConstant) {
$isConstant = true;
return $config;
});
};
}
/**
* Reject promise with standardized exception.
*
* @param $msg
* @return Promise\RejectedPromise
*/
protected static function reject($msg)
{
$exceptionClass = static::$exceptionClass;
return new Promise\RejectedPromise(new $exceptionClass($msg));
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Aws\Api;
/**
* Base class that is used by most API shapes
*/
abstract class AbstractModel implements \ArrayAccess
{
/** @var array */
protected $definition;
/** @var ShapeMap */
protected $shapeMap;
/** @var array */
protected $contextParam;
/**
* @param array $definition Service description
* @param ShapeMap $shapeMap Shapemap used for creating shapes
*/
public function __construct(array $definition, ShapeMap $shapeMap)
{
$this->definition = $definition;
$this->shapeMap = $shapeMap;
if (isset($definition['contextParam'])) {
$this->contextParam = $definition['contextParam'];
}
}
public function toArray()
{
return $this->definition;
}
/**
* @return mixed|null
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return isset($this->definition[$offset])
? $this->definition[$offset] : null;
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
$this->definition[$offset] = $value;
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return isset($this->definition[$offset]);
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->definition[$offset]);
}
protected function shapeAt($key)
{
if (!isset($this->definition[$key])) {
throw new \InvalidArgumentException('Expected shape definition at '
. $key);
}
return $this->shapeFor($this->definition[$key]);
}
protected function shapeFor(array $definition)
{
return isset($definition['shape'])
? $this->shapeMap->resolve($definition)
: Shape::create($definition, $this->shapeMap);
}
}

View File

@@ -0,0 +1,244 @@
<?php
namespace Aws\Api;
use Aws\Exception\UnresolvedApiException;
/**
* API providers.
*
* An API provider is a function that accepts a type, service, and version and
* returns an array of API data on success or NULL if no API data can be created
* for the provided arguments.
*
* You can wrap your calls to an API provider with the
* {@see ApiProvider::resolve} method to ensure that API data is created. If the
* API data is not created, then the resolve() method will throw a
* {@see Aws\Exception\UnresolvedApiException}.
*
* use Aws\Api\ApiProvider;
* $provider = ApiProvider::defaultProvider();
* // Returns an array or NULL.
* $data = $provider('api', 's3', '2006-03-01');
* // Returns an array or throws.
* $data = ApiProvider::resolve($provider, 'api', 'elasticfood', '2020-01-01');
*
* You can compose multiple providers into a single provider using
* {@see Aws\or_chain}. This method accepts providers as arguments and
* returns a new function that will invoke each provider until a non-null value
* is returned.
*
* $a = ApiProvider::filesystem(sys_get_temp_dir() . '/aws-beta-models');
* $b = ApiProvider::manifest();
*
* $c = \Aws\or_chain($a, $b);
* $data = $c('api', 'betaservice', '2015-08-08'); // $a handles this.
* $data = $c('api', 's3', '2006-03-01'); // $b handles this.
* $data = $c('api', 'invalid', '2014-12-15'); // Neither handles this.
*/
class ApiProvider
{
/** @var array A map of public API type names to their file suffix. */
private static $typeMap = [
'api' => 'api-2',
'paginator' => 'paginators-1',
'waiter' => 'waiters-2',
'docs' => 'docs-2',
];
/** @var array API manifest */
private $manifest;
/** @var string The directory containing service models. */
private $modelsDir;
/**
* Resolves an API provider and ensures a non-null return value.
*
* @param callable $provider Provider function to invoke.
* @param string $type Type of data ('api', 'waiter', 'paginator').
* @param string $service Service name.
* @param string $version API version.
*
* @return array
* @throws UnresolvedApiException
*/
public static function resolve(callable $provider, $type, $service, $version)
{
// Execute the provider and return the result, if there is one.
$result = $provider($type, $service, $version);
if (is_array($result)) {
if (!isset($result['metadata']['serviceIdentifier'])) {
$result['metadata']['serviceIdentifier'] = $service;
}
return $result;
}
// Throw an exception with a message depending on the inputs.
if (!isset(self::$typeMap[$type])) {
$msg = "The type must be one of: " . implode(', ', self::$typeMap);
} elseif ($service) {
$msg = "The {$service} service does not have version: {$version}.";
} else {
$msg = "You must specify a service name to retrieve its API data.";
}
throw new UnresolvedApiException($msg);
}
/**
* Default SDK API provider.
*
* This provider loads pre-built manifest data from the `data` directory.
*
* @return self
*/
public static function defaultProvider()
{
return new self(__DIR__ . '/../data', \Aws\manifest());
}
/**
* Loads API data after resolving the version to the latest, compatible,
* available version based on the provided manifest data.
*
* Manifest data is essentially an associative array of service names to
* associative arrays of API version aliases.
*
* [
* ...
* 'ec2' => [
* 'latest' => '2014-10-01',
* '2014-10-01' => '2014-10-01',
* '2014-09-01' => '2014-10-01',
* '2014-06-15' => '2014-10-01',
* ...
* ],
* 'ecs' => [...],
* 'elasticache' => [...],
* ...
* ]
*
* @param string $dir Directory containing service models.
* @param array $manifest The API version manifest data.
*
* @return self
*/
public static function manifest($dir, array $manifest)
{
return new self($dir, $manifest);
}
/**
* Loads API data from the specified directory.
*
* If "latest" is specified as the version, this provider must glob the
* directory to find which is the latest available version.
*
* @param string $dir Directory containing service models.
*
* @return self
* @throws \InvalidArgumentException if the provided `$dir` is invalid.
*/
public static function filesystem($dir)
{
return new self($dir);
}
/**
* Retrieves a list of valid versions for the specified service.
*
* @param string $service Service name
*
* @return array
*/
public function getVersions($service)
{
if (!isset($this->manifest)) {
$this->buildVersionsList($service);
}
if (!isset($this->manifest[$service]['versions'])) {
return [];
}
return array_values(array_unique($this->manifest[$service]['versions']));
}
/**
* Execute the provider.
*
* @param string $type Type of data ('api', 'waiter', 'paginator').
* @param string $service Service name.
* @param string $version API version.
*
* @return array|null
*/
public function __invoke($type, $service, $version)
{
// Resolve the type or return null.
if (isset(self::$typeMap[$type])) {
$type = self::$typeMap[$type];
} else {
return null;
}
// Resolve the version or return null.
if (!isset($this->manifest)) {
$this->buildVersionsList($service);
}
if (!isset($this->manifest[$service]['versions'][$version])) {
return null;
}
$version = $this->manifest[$service]['versions'][$version];
$path = "{$this->modelsDir}/{$service}/{$version}/{$type}.json";
try {
return \Aws\load_compiled_json($path);
} catch (\InvalidArgumentException $e) {
return null;
}
}
/**
* @param string $modelsDir Directory containing service models.
* @param array $manifest The API version manifest data.
*/
private function __construct($modelsDir, array $manifest = null)
{
$this->manifest = $manifest;
$this->modelsDir = rtrim($modelsDir, '/');
if (!is_dir($this->modelsDir)) {
throw new \InvalidArgumentException(
"The specified models directory, {$modelsDir}, was not found."
);
}
}
/**
* Build the versions list for the specified service by globbing the dir.
*/
private function buildVersionsList($service)
{
$dir = "{$this->modelsDir}/{$service}/";
if (!is_dir($dir)) {
return;
}
// Get versions, remove . and .., and sort in descending order.
$results = array_diff(scandir($dir, SCANDIR_SORT_DESCENDING), ['..', '.']);
if (!$results) {
$this->manifest[$service] = ['versions' => []];
} else {
$this->manifest[$service] = [
'versions' => [
'latest' => $results[0]
]
];
$this->manifest[$service]['versions'] += array_combine($results, $results);
}
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Aws\Api;
use Aws\Api\Parser\Exception\ParserException;
use DateTime;
use DateTimeZone;
use Exception;
/**
* DateTime overrides that make DateTime work more seamlessly as a string,
* with JSON documents, and with JMESPath.
*/
class DateTimeResult extends \DateTime implements \JsonSerializable
{
/**
* Create a new DateTimeResult from a unix timestamp.
* The Unix epoch (or Unix time or POSIX time or Unix
* timestamp) is the number of seconds that have elapsed since
* January 1, 1970 (midnight UTC/GMT).
*
* @return DateTimeResult
* @throws Exception
*/
public static function fromEpoch($unixTimestamp)
{
if (!is_numeric($unixTimestamp)) {
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromEpoch');
}
// PHP 5.5 does not support sub-second precision
if (\PHP_VERSION_ID < 56000) {
return new self(gmdate('c', $unixTimestamp));
}
$decimalSeparator = isset(localeconv()['decimal_point']) ? localeconv()['decimal_point'] : ".";
$formatString = "U" . $decimalSeparator . "u";
$dateTime = DateTime::createFromFormat(
$formatString,
sprintf('%0.6f', $unixTimestamp),
new DateTimeZone('UTC')
);
if (false === $dateTime) {
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromEpoch');
}
return new self(
$dateTime->format('Y-m-d H:i:s.u'),
new DateTimeZone('UTC')
);
}
/**
* @return DateTimeResult
*/
public static function fromISO8601($iso8601Timestamp)
{
if (is_numeric($iso8601Timestamp) || !is_string($iso8601Timestamp)) {
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromISO8601');
}
return new DateTimeResult($iso8601Timestamp);
}
/**
* Create a new DateTimeResult from an unknown timestamp.
*
* @return DateTimeResult
* @throws Exception
*/
public static function fromTimestamp($timestamp, $expectedFormat = null)
{
if (empty($timestamp)) {
return self::fromEpoch(0);
}
if (!(is_string($timestamp) || is_numeric($timestamp))) {
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromTimestamp');
}
try {
if ($expectedFormat == 'iso8601') {
try {
return self::fromISO8601($timestamp);
} catch (Exception $exception) {
return self::fromEpoch($timestamp);
}
} else if ($expectedFormat == 'unixTimestamp') {
try {
return self::fromEpoch($timestamp);
} catch (Exception $exception) {
return self::fromISO8601($timestamp);
}
} else if (\Aws\is_valid_epoch($timestamp)) {
return self::fromEpoch($timestamp);
}
return self::fromISO8601($timestamp);
} catch (Exception $exception) {
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromTimestamp');
}
}
/**
* Serialize the DateTimeResult as an ISO 8601 date string.
*
* @return string
*/
public function __toString()
{
return $this->format('c');
}
/**
* Serialize the date as an ISO 8601 date when serializing as JSON.
*
* @return string
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return (string) $this;
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Aws\Api;
/**
* Encapsulates the documentation strings for a given service-version and
* provides methods for extracting the desired parts related to a service,
* operation, error, or shape (i.e., parameter).
*/
class DocModel
{
/** @var array */
private $docs;
/**
* @param array $docs
*
* @throws \RuntimeException
*/
public function __construct(array $docs)
{
if (!extension_loaded('tidy')) {
throw new \RuntimeException('The "tidy" PHP extension is required.');
}
$this->docs = $docs;
}
/**
* Convert the doc model to an array.
*
* @return array
*/
public function toArray()
{
return $this->docs;
}
/**
* Retrieves documentation about the service.
*
* @return null|string
*/
public function getServiceDocs()
{
return isset($this->docs['service']) ? $this->docs['service'] : null;
}
/**
* Retrieves documentation about an operation.
*
* @param string $operation Name of the operation
*
* @return null|string
*/
public function getOperationDocs($operation)
{
return isset($this->docs['operations'][$operation])
? $this->docs['operations'][$operation]
: null;
}
/**
* Retrieves documentation about an error.
*
* @param string $error Name of the error
*
* @return null|string
*/
public function getErrorDocs($error)
{
return isset($this->docs['shapes'][$error]['base'])
? $this->docs['shapes'][$error]['base']
: null;
}
/**
* Retrieves documentation about a shape, specific to the context.
*
* @param string $shapeName Name of the shape.
* @param string $parentName Name of the parent/context shape.
* @param string $ref Name used by the context to reference the shape.
*
* @return null|string
*/
public function getShapeDocs($shapeName, $parentName, $ref)
{
if (!isset($this->docs['shapes'][$shapeName])) {
return '';
}
$result = '';
$d = $this->docs['shapes'][$shapeName];
if (isset($d['refs']["{$parentName}\$${ref}"])) {
$result = $d['refs']["{$parentName}\$${ref}"];
} elseif (isset($d['base'])) {
$result = $d['base'];
}
if (isset($d['append'])) {
if (!isset($d['excludeAppend'])
|| !in_array($parentName, $d['excludeAppend'])
) {
$result .= $d['append'];
}
}
if (isset($d['appendOnly'])
&& in_array($parentName, $d['appendOnly']['shapes'])
) {
$result .= $d['appendOnly']['message'];
}
return $this->clean($result);
}
private function clean($content)
{
if (!$content) {
return '';
}
$tidy = new \tidy();
$tidy->parseString($content, [
'indent' => true,
'doctype' => 'omit',
'output-html' => true,
'show-body-only' => true,
'drop-empty-paras' => true,
'drop-font-tags' => true,
'drop-proprietary-attributes' => true,
'hide-comments' => true,
'logical-emphasis' => true
]);
$tidy->cleanRepair();
return (string) $content;
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Aws\Api\ErrorParser;
use Aws\Api\Parser\MetadataParserTrait;
use Aws\Api\Parser\PayloadParserTrait;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
abstract class AbstractErrorParser
{
use MetadataParserTrait;
use PayloadParserTrait;
/**
* @var Service
*/
protected $api;
/**
* @param Service $api
*/
public function __construct(Service $api = null)
{
$this->api = $api;
}
abstract protected function payload(
ResponseInterface $response,
StructureShape $member
);
protected function extractPayload(
StructureShape $member,
ResponseInterface $response
) {
if ($member instanceof StructureShape) {
// Structure members parse top-level data into a specific key.
return $this->payload($response, $member);
} else {
// Streaming data is just the stream from the response body.
return $response->getBody();
}
}
protected function populateShape(
array &$data,
ResponseInterface $response,
CommandInterface $command = null
) {
$data['body'] = [];
if (!empty($command) && !empty($this->api)) {
// If modeled error code is indicated, check for known error shape
if (!empty($data['code'])) {
$errors = $this->api->getOperation($command->getName())->getErrors();
foreach ($errors as $key => $error) {
// If error code matches a known error shape, populate the body
if ($data['code'] == $error['name']
&& $error instanceof StructureShape
) {
$modeledError = $error;
$data['body'] = $this->extractPayload(
$modeledError,
$response
);
$data['error_shape'] = $modeledError;
foreach ($error->getMembers() as $name => $member) {
switch ($member['location']) {
case 'header':
$this->extractHeader($name, $member, $response, $data['body']);
break;
case 'headers':
$this->extractHeaders($name, $member, $response, $data['body']);
break;
case 'statusCode':
$this->extractStatus($name, $response, $data['body']);
break;
}
}
break;
}
}
}
}
return $data;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Aws\Api\ErrorParser;
use Aws\Api\Parser\PayloadParserTrait;
use Aws\Api\StructureShape;
use Psr\Http\Message\ResponseInterface;
/**
* Provides basic JSON error parsing functionality.
*/
trait JsonParserTrait
{
use PayloadParserTrait;
private function genericHandler(ResponseInterface $response)
{
$code = (string) $response->getStatusCode();
if ($this->api
&& !is_null($this->api->getMetadata('awsQueryCompatible'))
&& $response->getHeaderLine('x-amzn-query-error')
) {
$queryError = $response->getHeaderLine('x-amzn-query-error');
$parts = explode(';', $queryError);
if (isset($parts) && count($parts) == 2 && $parts[0] && $parts[1]) {
$error_code = $parts[0];
$error_type = $parts[1];
}
}
if (!isset($error_type)) {
$error_type = $code[0] == '4' ? 'client' : 'server';
}
return [
'request_id' => (string) $response->getHeaderLine('x-amzn-requestid'),
'code' => isset($error_code) ? $error_code : null,
'message' => null,
'type' => $error_type,
'parsed' => $this->parseJson($response->getBody(), $response)
];
}
protected function payload(
ResponseInterface $response,
StructureShape $member
) {
$jsonBody = $this->parseJson($response->getBody(), $response);
if ($jsonBody) {
return $this->parser->parse($member, $jsonBody);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Aws\Api\ErrorParser;
use Aws\Api\Parser\JsonParser;
use Aws\Api\Service;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Parsers JSON-RPC errors.
*/
class JsonRpcErrorParser extends AbstractErrorParser
{
use JsonParserTrait;
private $parser;
public function __construct(Service $api = null, JsonParser $parser = null)
{
parent::__construct($api);
$this->parser = $parser ?: new JsonParser();
}
public function __invoke(
ResponseInterface $response,
CommandInterface $command = null
) {
$data = $this->genericHandler($response);
// Make the casing consistent across services.
if ($data['parsed']) {
$data['parsed'] = array_change_key_case($data['parsed']);
}
if (isset($data['parsed']['__type'])) {
if (!isset($data['code'])) {
$parts = explode('#', $data['parsed']['__type']);
$data['code'] = isset($parts[1]) ? $parts[1] : $parts[0];
}
$data['message'] = isset($data['parsed']['message'])
? $data['parsed']['message']
: null;
}
$this->populateShape($data, $response, $command);
return $data;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Aws\Api\ErrorParser;
use Aws\Api\Parser\JsonParser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Parses JSON-REST errors.
*/
class RestJsonErrorParser extends AbstractErrorParser
{
use JsonParserTrait;
private $parser;
public function __construct(Service $api = null, JsonParser $parser = null)
{
parent::__construct($api);
$this->parser = $parser ?: new JsonParser();
}
public function __invoke(
ResponseInterface $response,
CommandInterface $command = null
) {
$data = $this->genericHandler($response);
// Merge in error data from the JSON body
if ($json = $data['parsed']) {
$data = array_replace($data, $json);
}
// Correct error type from services like Amazon Glacier
if (!empty($data['type'])) {
$data['type'] = strtolower($data['type']);
}
// Retrieve the error code from services like Amazon Elastic Transcoder
if ($code = $response->getHeaderLine('x-amzn-errortype')) {
$colon = strpos($code, ':');
$data['code'] = $colon ? substr($code, 0, $colon) : $code;
}
// Retrieve error message directly
$data['message'] = isset($data['parsed']['message'])
? $data['parsed']['message']
: (isset($data['parsed']['Message'])
? $data['parsed']['Message']
: null);
$this->populateShape($data, $response, $command);
return $data;
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace Aws\Api\ErrorParser;
use Aws\Api\Parser\PayloadParserTrait;
use Aws\Api\Parser\XmlParser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Parses XML errors.
*/
class XmlErrorParser extends AbstractErrorParser
{
use PayloadParserTrait;
protected $parser;
public function __construct(Service $api = null, XmlParser $parser = null)
{
parent::__construct($api);
$this->parser = $parser ?: new XmlParser();
}
public function __invoke(
ResponseInterface $response,
CommandInterface $command = null
) {
$code = (string) $response->getStatusCode();
$data = [
'type' => $code[0] == '4' ? 'client' : 'server',
'request_id' => null,
'code' => null,
'message' => null,
'parsed' => null
];
$body = $response->getBody();
if ($body->getSize() > 0) {
$this->parseBody($this->parseXml($body, $response), $data);
} else {
$this->parseHeaders($response, $data);
}
$this->populateShape($data, $response, $command);
return $data;
}
private function parseHeaders(ResponseInterface $response, array &$data)
{
if ($response->getStatusCode() == '404') {
$data['code'] = 'NotFound';
}
$data['message'] = $response->getStatusCode() . ' '
. $response->getReasonPhrase();
if ($requestId = $response->getHeaderLine('x-amz-request-id')) {
$data['request_id'] = $requestId;
$data['message'] .= " (Request-ID: $requestId)";
}
}
private function parseBody(\SimpleXMLElement $body, array &$data)
{
$data['parsed'] = $body;
$prefix = $this->registerNamespacePrefix($body);
if ($tempXml = $body->xpath("//{$prefix}Code[1]")) {
$data['code'] = (string) $tempXml[0];
}
if ($tempXml = $body->xpath("//{$prefix}Message[1]")) {
$data['message'] = (string) $tempXml[0];
}
$tempXml = $body->xpath("//{$prefix}RequestId[1]");
if (isset($tempXml[0])) {
$data['request_id'] = (string)$tempXml[0];
}
}
protected function registerNamespacePrefix(\SimpleXMLElement $element)
{
$namespaces = $element->getDocNamespaces();
if (!isset($namespaces[''])) {
return '';
}
// Account for the default namespace being defined and PHP not
// being able to handle it :(.
$element->registerXPathNamespace('ns', $namespaces['']);
return 'ns:';
}
protected function payload(
ResponseInterface $response,
StructureShape $member
) {
$xmlBody = $this->parseXml($response->getBody(), $response);
$prefix = $this->registerNamespacePrefix($xmlBody);
$errorBody = $xmlBody->xpath("//{$prefix}Error");
if (is_array($errorBody) && !empty($errorBody[0])) {
return $this->parser->parse($member, $errorBody[0]);
}
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Aws\Api;
/**
* Represents a list shape.
*/
class ListShape extends Shape
{
private $member;
public function __construct(array $definition, ShapeMap $shapeMap)
{
$definition['type'] = 'list';
parent::__construct($definition, $shapeMap);
}
/**
* @return Shape
* @throws \RuntimeException if no member is specified
*/
public function getMember()
{
if (!$this->member) {
if (!isset($this->definition['member'])) {
throw new \RuntimeException('No member attribute specified');
}
$this->member = Shape::create(
$this->definition['member'],
$this->shapeMap
);
}
return $this->member;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Aws\Api;
/**
* Represents a map shape.
*/
class MapShape extends Shape
{
/** @var Shape */
private $value;
/** @var Shape */
private $key;
public function __construct(array $definition, ShapeMap $shapeMap)
{
$definition['type'] = 'map';
parent::__construct($definition, $shapeMap);
}
/**
* @return Shape
* @throws \RuntimeException if no value is specified
*/
public function getValue()
{
if (!$this->value) {
if (!isset($this->definition['value'])) {
throw new \RuntimeException('No value specified');
}
$this->value = Shape::create(
$this->definition['value'],
$this->shapeMap
);
}
return $this->value;
}
/**
* @return Shape
*/
public function getKey()
{
if (!$this->key) {
$this->key = isset($this->definition['key'])
? Shape::create($this->definition['key'], $this->shapeMap)
: new Shape(['type' => 'string'], $this->shapeMap);
}
return $this->key;
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Aws\Api;
/**
* Represents an API operation.
*/
class Operation extends AbstractModel
{
private $input;
private $output;
private $errors;
private $staticContextParams = [];
private $contextParams;
public function __construct(array $definition, ShapeMap $shapeMap)
{
$definition['type'] = 'structure';
if (!isset($definition['http']['method'])) {
$definition['http']['method'] = 'POST';
}
if (!isset($definition['http']['requestUri'])) {
$definition['http']['requestUri'] = '/';
}
if (isset($definition['staticContextParams'])) {
$this->staticContextParams = $definition['staticContextParams'];
}
parent::__construct($definition, $shapeMap);
$this->contextParams = $this->setContextParams();
}
/**
* Returns an associative array of the HTTP attribute of the operation:
*
* - method: HTTP method of the operation
* - requestUri: URI of the request (can include URI template placeholders)
*
* @return array
*/
public function getHttp()
{
return $this->definition['http'];
}
/**
* Get the input shape of the operation.
*
* @return StructureShape
*/
public function getInput()
{
if (!$this->input) {
if ($input = $this['input']) {
$this->input = $this->shapeFor($input);
} else {
$this->input = new StructureShape([], $this->shapeMap);
}
}
return $this->input;
}
/**
* Get the output shape of the operation.
*
* @return StructureShape
*/
public function getOutput()
{
if (!$this->output) {
if ($output = $this['output']) {
$this->output = $this->shapeFor($output);
} else {
$this->output = new StructureShape([], $this->shapeMap);
}
}
return $this->output;
}
/**
* Get an array of operation error shapes.
*
* @return Shape[]
*/
public function getErrors()
{
if ($this->errors === null) {
if ($errors = $this['errors']) {
foreach ($errors as $key => $error) {
$errors[$key] = $this->shapeFor($error);
}
$this->errors = $errors;
} else {
$this->errors = [];
}
}
return $this->errors;
}
/**
* Gets static modeled static values used for
* endpoint resolution.
*
* @return array
*/
public function getStaticContextParams()
{
return $this->staticContextParams;
}
/**
* Gets definition of modeled dynamic values used
* for endpoint resolution
*
* @return array
*/
public function getContextParams()
{
return $this->contextParams;
}
private function setContextParams()
{
$members = $this->getInput()->getMembers();
$contextParams = [];
foreach($members as $name => $shape) {
if (!empty($contextParam = $shape->getContextParam())) {
$contextParams[$contextParam['name']] = [
'shape' => $name,
'type' => $shape->getType()
];
}
}
return $contextParams;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Aws\ResultInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* @internal
*/
abstract class AbstractParser
{
/** @var \Aws\Api\Service Representation of the service API*/
protected $api;
/** @var callable */
protected $parser;
/**
* @param Service $api Service description.
*/
public function __construct(Service $api)
{
$this->api = $api;
}
/**
* @param CommandInterface $command Command that was executed.
* @param ResponseInterface $response Response that was received.
*
* @return ResultInterface
*/
abstract public function __invoke(
CommandInterface $command,
ResponseInterface $response
);
abstract public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
);
}

View File

@@ -0,0 +1,184 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\DateTimeResult;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
use Aws\Result;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @internal
*/
abstract class AbstractRestParser extends AbstractParser
{
use PayloadParserTrait;
/**
* Parses a payload from a response.
*
* @param ResponseInterface $response Response to parse.
* @param StructureShape $member Member to parse
* @param array $result Result value
*
* @return mixed
*/
abstract protected function payload(
ResponseInterface $response,
StructureShape $member,
array &$result
);
public function __invoke(
CommandInterface $command,
ResponseInterface $response
) {
$output = $this->api->getOperation($command->getName())->getOutput();
$result = [];
if ($payload = $output['payload']) {
$this->extractPayload($payload, $output, $response, $result);
}
foreach ($output->getMembers() as $name => $member) {
switch ($member['location']) {
case 'header':
$this->extractHeader($name, $member, $response, $result);
break;
case 'headers':
$this->extractHeaders($name, $member, $response, $result);
break;
case 'statusCode':
$this->extractStatus($name, $response, $result);
break;
}
}
if (!$payload
&& $response->getBody()->getSize() > 0
&& count($output->getMembers()) > 0
) {
// if no payload was found, then parse the contents of the body
$this->payload($response, $output, $result);
}
return new Result($result);
}
private function extractPayload(
$payload,
StructureShape $output,
ResponseInterface $response,
array &$result
) {
$member = $output->getMember($payload);
if (!empty($member['eventstream'])) {
$result[$payload] = new EventParsingIterator(
$response->getBody(),
$member,
$this
);
} else if ($member instanceof StructureShape) {
// Structure members parse top-level data into a specific key.
$result[$payload] = [];
$this->payload($response, $member, $result[$payload]);
} else {
// Streaming data is just the stream from the response body.
$result[$payload] = $response->getBody();
}
}
/**
* Extract a single header from the response into the result.
*/
private function extractHeader(
$name,
Shape $shape,
ResponseInterface $response,
&$result
) {
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
switch ($shape->getType()) {
case 'float':
case 'double':
$value = (float) $value;
break;
case 'long':
$value = (int) $value;
break;
case 'boolean':
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
break;
case 'blob':
$value = base64_decode($value);
break;
case 'timestamp':
try {
$value = DateTimeResult::fromTimestamp(
$value,
!empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null
);
break;
} catch (\Exception $e) {
// If the value cannot be parsed, then do not add it to the
// output structure.
return;
}
case 'string':
try {
if ($shape['jsonvalue']) {
$value = $this->parseJson(base64_decode($value), $response);
}
// If value is not set, do not add to output structure.
if (!isset($value)) {
return;
}
break;
} catch (\Exception $e) {
//If the value cannot be parsed, then do not add it to the
//output structure.
return;
}
}
$result[$name] = $value;
}
/**
* Extract a map of headers with an optional prefix from the response.
*/
private function extractHeaders(
$name,
Shape $shape,
ResponseInterface $response,
&$result
) {
// Check if the headers are prefixed by a location name
$result[$name] = [];
$prefix = $shape['locationName'];
$prefixLen = $prefix !== null ? strlen($prefix) : 0;
foreach ($response->getHeaders() as $k => $values) {
if (!$prefixLen) {
$result[$name][$k] = implode(', ', $values);
} elseif (stripos($k, $prefix) === 0) {
$result[$name][substr($k, $prefixLen)] = implode(', ', $values);
}
}
}
/**
* Places the status code of the response into the result array.
*/
private function extractStatus(
$name,
ResponseInterface $response,
array &$result
) {
$result[$name] = (int) $response->getStatusCode();
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Aws\Exception\AwsException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use GuzzleHttp\Psr7;
/**
* @internal Decorates a parser and validates the x-amz-crc32 header.
*/
class Crc32ValidatingParser extends AbstractParser
{
/**
* @param callable $parser Parser to wrap.
*/
public function __construct(callable $parser)
{
$this->parser = $parser;
}
public function __invoke(
CommandInterface $command,
ResponseInterface $response
) {
if ($expected = $response->getHeaderLine('x-amz-crc32')) {
$hash = hexdec(Psr7\Utils::hash($response->getBody(), 'crc32b'));
if ($expected != $hash) {
throw new AwsException(
"crc32 mismatch. Expected {$expected}, found {$hash}.",
$command,
[
'code' => 'ClientChecksumMismatch',
'connection_error' => true,
'response' => $response
]
);
}
}
$fn = $this->parser;
return $fn($command, $response);
}
public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
) {
return $this->parser->parseMemberFromStream($stream, $member, $response);
}
}

View File

@@ -0,0 +1,340 @@
<?php
namespace Aws\Api\Parser;
use \Iterator;
use Aws\Api\DateTimeResult;
use GuzzleHttp\Psr7;
use Psr\Http\Message\StreamInterface;
use Aws\Api\Parser\Exception\ParserException;
/**
* @internal Implements a decoder for a binary encoded event stream that will
* decode, validate, and provide individual events from the stream.
*/
class DecodingEventStreamIterator implements Iterator
{
const HEADERS = 'headers';
const PAYLOAD = 'payload';
const LENGTH_TOTAL = 'total_length';
const LENGTH_HEADERS = 'headers_length';
const CRC_PRELUDE = 'prelude_crc';
const BYTES_PRELUDE = 12;
const BYTES_TRAILING = 4;
private static $preludeFormat = [
self::LENGTH_TOTAL => 'decodeUint32',
self::LENGTH_HEADERS => 'decodeUint32',
self::CRC_PRELUDE => 'decodeUint32',
];
private static $lengthFormatMap = [
1 => 'decodeUint8',
2 => 'decodeUint16',
4 => 'decodeUint32',
8 => 'decodeUint64',
];
private static $headerTypeMap = [
0 => 'decodeBooleanTrue',
1 => 'decodeBooleanFalse',
2 => 'decodeInt8',
3 => 'decodeInt16',
4 => 'decodeInt32',
5 => 'decodeInt64',
6 => 'decodeBytes',
7 => 'decodeString',
8 => 'decodeTimestamp',
9 => 'decodeUuid',
];
/** @var StreamInterface Stream of eventstream shape to parse. */
private $stream;
/** @var array Currently parsed event. */
private $currentEvent;
/** @var int Current in-order event key. */
private $key;
/** @var resource|\HashContext CRC32 hash context for event validation */
private $hashContext;
/** @var int $currentPosition */
private $currentPosition;
/**
* DecodingEventStreamIterator constructor.
*
* @param StreamInterface $stream
*/
public function __construct(StreamInterface $stream)
{
$this->stream = $stream;
$this->rewind();
}
private function parseHeaders($headerBytes)
{
$headers = [];
$bytesRead = 0;
while ($bytesRead < $headerBytes) {
list($key, $numBytes) = $this->decodeString(1);
$bytesRead += $numBytes;
list($type, $numBytes) = $this->decodeUint8();
$bytesRead += $numBytes;
$f = self::$headerTypeMap[$type];
list($value, $numBytes) = $this->{$f}();
$bytesRead += $numBytes;
if (isset($headers[$key])) {
throw new ParserException('Duplicate key in event headers.');
}
$headers[$key] = $value;
}
return [$headers, $bytesRead];
}
private function parsePrelude()
{
$prelude = [];
$bytesRead = 0;
$calculatedCrc = null;
foreach (self::$preludeFormat as $key => $decodeFunction) {
if ($key === self::CRC_PRELUDE) {
$hashCopy = hash_copy($this->hashContext);
$calculatedCrc = hash_final($this->hashContext, true);
$this->hashContext = $hashCopy;
}
list($value, $numBytes) = $this->{$decodeFunction}();
$bytesRead += $numBytes;
$prelude[$key] = $value;
}
if (unpack('N', $calculatedCrc)[1] !== $prelude[self::CRC_PRELUDE]) {
throw new ParserException('Prelude checksum mismatch.');
}
return [$prelude, $bytesRead];
}
private function parseEvent()
{
$event = [];
if ($this->stream->tell() < $this->stream->getSize()) {
$this->hashContext = hash_init('crc32b');
$bytesLeft = $this->stream->getSize() - $this->stream->tell();
list($prelude, $numBytes) = $this->parsePrelude();
if ($prelude[self::LENGTH_TOTAL] > $bytesLeft) {
throw new ParserException('Message length too long.');
}
$bytesLeft -= $numBytes;
if ($prelude[self::LENGTH_HEADERS] > $bytesLeft) {
throw new ParserException('Headers length too long.');
}
list(
$event[self::HEADERS],
$numBytes
) = $this->parseHeaders($prelude[self::LENGTH_HEADERS]);
$event[self::PAYLOAD] = Psr7\Utils::streamFor(
$this->readAndHashBytes(
$prelude[self::LENGTH_TOTAL] - self::BYTES_PRELUDE
- $numBytes - self::BYTES_TRAILING
)
);
$calculatedCrc = hash_final($this->hashContext, true);
$messageCrc = $this->stream->read(4);
if ($calculatedCrc !== $messageCrc) {
throw new ParserException('Message checksum mismatch.');
}
}
return $event;
}
// Iterator Functionality
/**
* @return array
*/
#[\ReturnTypeWillChange]
public function current()
{
return $this->currentEvent;
}
/**
* @return int
*/
#[\ReturnTypeWillChange]
public function key()
{
return $this->key;
}
#[\ReturnTypeWillChange]
public function next()
{
$this->currentPosition = $this->stream->tell();
if ($this->valid()) {
$this->key++;
$this->currentEvent = $this->parseEvent();
}
}
#[\ReturnTypeWillChange]
public function rewind()
{
$this->stream->rewind();
$this->key = 0;
$this->currentPosition = 0;
$this->currentEvent = $this->parseEvent();
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid()
{
return $this->currentPosition < $this->stream->getSize();
}
// Decoding Utilities
private function readAndHashBytes($num)
{
$bytes = $this->stream->read($num);
hash_update($this->hashContext, $bytes);
return $bytes;
}
private function decodeBooleanTrue()
{
return [true, 0];
}
private function decodeBooleanFalse()
{
return [false, 0];
}
private function uintToInt($val, $size)
{
$signedCap = pow(2, $size - 1);
if ($val > $signedCap) {
$val -= (2 * $signedCap);
}
return $val;
}
private function decodeInt8()
{
$val = (int)unpack('C', $this->readAndHashBytes(1))[1];
return [$this->uintToInt($val, 8), 1];
}
private function decodeUint8()
{
return [unpack('C', $this->readAndHashBytes(1))[1], 1];
}
private function decodeInt16()
{
$val = (int)unpack('n', $this->readAndHashBytes(2))[1];
return [$this->uintToInt($val, 16), 2];
}
private function decodeUint16()
{
return [unpack('n', $this->readAndHashBytes(2))[1], 2];
}
private function decodeInt32()
{
$val = (int)unpack('N', $this->readAndHashBytes(4))[1];
return [$this->uintToInt($val, 32), 4];
}
private function decodeUint32()
{
return [unpack('N', $this->readAndHashBytes(4))[1], 4];
}
private function decodeInt64()
{
$val = $this->unpackInt64($this->readAndHashBytes(8))[1];
return [$this->uintToInt($val, 64), 8];
}
private function decodeUint64()
{
return [$this->unpackInt64($this->readAndHashBytes(8))[1], 8];
}
private function unpackInt64($bytes)
{
if (version_compare(PHP_VERSION, '5.6.3', '<')) {
$d = unpack('N2', $bytes);
return [1 => $d[1] << 32 | $d[2]];
}
return unpack('J', $bytes);
}
private function decodeBytes($lengthBytes=2)
{
if (!isset(self::$lengthFormatMap[$lengthBytes])) {
throw new ParserException('Undefined variable length format.');
}
$f = self::$lengthFormatMap[$lengthBytes];
list($len, $bytes) = $this->{$f}();
return [$this->readAndHashBytes($len), $len + $bytes];
}
private function decodeString($lengthBytes=2)
{
if (!isset(self::$lengthFormatMap[$lengthBytes])) {
throw new ParserException('Undefined variable length format.');
}
$f = self::$lengthFormatMap[$lengthBytes];
list($len, $bytes) = $this->{$f}();
return [$this->readAndHashBytes($len), $len + $bytes];
}
private function decodeTimestamp()
{
list($val, $bytes) = $this->decodeInt64();
return [
DateTimeResult::createFromFormat('U.u', $val / 1000),
$bytes
];
}
private function decodeUuid()
{
$val = unpack('H32', $this->readAndHashBytes(16))[1];
return [
substr($val, 0, 8) . '-'
. substr($val, 8, 4) . '-'
. substr($val, 12, 4) . '-'
. substr($val, 16, 4) . '-'
. substr($val, 20, 12),
16
];
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Aws\Api\Parser;
use \Iterator;
use Aws\Exception\EventStreamDataException;
use Aws\Api\Parser\Exception\ParserException;
use Aws\Api\StructureShape;
use Psr\Http\Message\StreamInterface;
/**
* @internal Implements a decoder for a binary encoded event stream that will
* decode, validate, and provide individual events from the stream.
*/
class EventParsingIterator implements Iterator
{
/** @var StreamInterface */
private $decodingIterator;
/** @var StructureShape */
private $shape;
/** @var AbstractParser */
private $parser;
public function __construct(
StreamInterface $stream,
StructureShape $shape,
AbstractParser $parser
) {
$this->decodingIterator = new DecodingEventStreamIterator($stream);
$this->shape = $shape;
$this->parser = $parser;
}
#[\ReturnTypeWillChange]
public function current()
{
return $this->parseEvent($this->decodingIterator->current());
}
#[\ReturnTypeWillChange]
public function key()
{
return $this->decodingIterator->key();
}
#[\ReturnTypeWillChange]
public function next()
{
$this->decodingIterator->next();
}
#[\ReturnTypeWillChange]
public function rewind()
{
$this->decodingIterator->rewind();
}
#[\ReturnTypeWillChange]
public function valid()
{
return $this->decodingIterator->valid();
}
private function parseEvent(array $event)
{
if (!empty($event['headers'][':message-type'])) {
if ($event['headers'][':message-type'] === 'error') {
return $this->parseError($event);
}
if ($event['headers'][':message-type'] !== 'event') {
throw new ParserException('Failed to parse unknown message type.');
}
}
if (empty($event['headers'][':event-type'])) {
throw new ParserException('Failed to parse without event type.');
}
$eventShape = $this->shape->getMember($event['headers'][':event-type']);
$parsedEvent = [];
foreach ($eventShape['members'] as $shape => $details) {
if (!empty($details['eventpayload'])) {
$payloadShape = $eventShape->getMember($shape);
if ($payloadShape['type'] === 'blob') {
$parsedEvent[$shape] = $event['payload'];
} else {
$parsedEvent[$shape] = $this->parser->parseMemberFromStream(
$event['payload'],
$payloadShape,
null
);
}
} else {
$parsedEvent[$shape] = $event['headers'][$shape];
}
}
return [
$event['headers'][':event-type'] => $parsedEvent
];
}
private function parseError(array $event)
{
throw new EventStreamDataException(
$event['headers'][':error-code'],
$event['headers'][':error-message']
);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Aws\Api\Parser\Exception;
use Aws\HasMonitoringEventsTrait;
use Aws\MonitoringEventsInterface;
use Aws\ResponseContainerInterface;
use Psr\Http\Message\ResponseInterface;
class ParserException extends \RuntimeException implements
MonitoringEventsInterface,
ResponseContainerInterface
{
use HasMonitoringEventsTrait;
private $errorCode;
private $requestId;
private $response;
public function __construct($message = '', $code = 0, $previous = null, array $context = [])
{
$this->errorCode = isset($context['error_code']) ? $context['error_code'] : null;
$this->requestId = isset($context['request_id']) ? $context['request_id'] : null;
$this->response = isset($context['response']) ? $context['response'] : null;
parent::__construct($message, $code, $previous);
}
/**
* Get the error code, if any.
*
* @return string|null
*/
public function getErrorCode()
{
return $this->errorCode;
}
/**
* Get the request ID, if any.
*
* @return string|null
*/
public function getRequestId()
{
return $this->requestId;
}
/**
* Get the received HTTP response if any.
*
* @return ResponseInterface|null
*/
public function getResponse()
{
return $this->response;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\DateTimeResult;
use Aws\Api\Shape;
/**
* @internal Implements standard JSON parsing.
*/
class JsonParser
{
public function parse(Shape $shape, $value)
{
if ($value === null) {
return $value;
}
switch ($shape['type']) {
case 'structure':
if (isset($shape['document']) && $shape['document']) {
return $value;
}
$target = [];
foreach ($shape->getMembers() as $name => $member) {
$locationName = $member['locationName'] ?: $name;
if (isset($value[$locationName])) {
$target[$name] = $this->parse($member, $value[$locationName]);
}
}
if (isset($shape['union'])
&& $shape['union']
&& is_array($value)
&& empty($target)
) {
foreach ($value as $key => $val) {
$target['Unknown'][$key] = $val;
}
}
return $target;
case 'list':
$member = $shape->getMember();
$target = [];
foreach ($value as $v) {
$target[] = $this->parse($member, $v);
}
return $target;
case 'map':
$values = $shape->getValue();
$target = [];
foreach ($value as $k => $v) {
$target[$k] = $this->parse($values, $v);
}
return $target;
case 'timestamp':
return DateTimeResult::fromTimestamp(
$value,
!empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null
);
case 'blob':
return base64_decode($value);
default:
return $value;
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\StructureShape;
use Aws\Api\Service;
use Aws\Result;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* @internal Implements JSON-RPC parsing (e.g., DynamoDB)
*/
class JsonRpcParser extends AbstractParser
{
use PayloadParserTrait;
/**
* @param Service $api Service description
* @param JsonParser $parser JSON body builder
*/
public function __construct(Service $api, JsonParser $parser = null)
{
parent::__construct($api);
$this->parser = $parser ?: new JsonParser();
}
public function __invoke(
CommandInterface $command,
ResponseInterface $response
) {
$operation = $this->api->getOperation($command->getName());
$result = null === $operation['output']
? null
: $this->parseMemberFromStream(
$response->getBody(),
$operation->getOutput(),
$response
);
return new Result($result ?: []);
}
public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
) {
return $this->parser->parse($member, $this->parseJson($stream, $response));
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\DateTimeResult;
use Aws\Api\Shape;
use Psr\Http\Message\ResponseInterface;
trait MetadataParserTrait
{
/**
* Extract a single header from the response into the result.
*/
protected function extractHeader(
$name,
Shape $shape,
ResponseInterface $response,
&$result
) {
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
switch ($shape->getType()) {
case 'float':
case 'double':
$value = (float) $value;
break;
case 'long':
$value = (int) $value;
break;
case 'boolean':
$value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
break;
case 'blob':
$value = base64_decode($value);
break;
case 'timestamp':
try {
$value = DateTimeResult::fromTimestamp(
$value,
!empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null
);
break;
} catch (\Exception $e) {
// If the value cannot be parsed, then do not add it to the
// output structure.
return;
}
case 'string':
if ($shape['jsonvalue']) {
$value = $this->parseJson(base64_decode($value), $response);
}
break;
}
$result[$name] = $value;
}
/**
* Extract a map of headers with an optional prefix from the response.
*/
protected function extractHeaders(
$name,
Shape $shape,
ResponseInterface $response,
&$result
) {
// Check if the headers are prefixed by a location name
$result[$name] = [];
$prefix = $shape['locationName'];
$prefixLen = strlen($prefix);
foreach ($response->getHeaders() as $k => $values) {
if (!$prefixLen) {
$result[$name][$k] = implode(', ', $values);
} elseif (stripos($k, $prefix) === 0) {
$result[$name][substr($k, $prefixLen)] = implode(', ', $values);
}
}
}
/**
* Places the status code of the response into the result array.
*/
protected function extractStatus(
$name,
ResponseInterface $response,
array &$result
) {
$result[$name] = (int) $response->getStatusCode();
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\Parser\Exception\ParserException;
use Psr\Http\Message\ResponseInterface;
trait PayloadParserTrait
{
/**
* @param string $json
*
* @throws ParserException
*
* @return array
*/
private function parseJson($json, $response)
{
$jsonPayload = json_decode($json, true);
if (JSON_ERROR_NONE !== json_last_error()) {
throw new ParserException(
'Error parsing JSON: ' . json_last_error_msg(),
0,
null,
['response' => $response]
);
}
return $jsonPayload;
}
/**
* @param string $xml
*
* @throws ParserException
*
* @return \SimpleXMLElement
*/
protected function parseXml($xml, $response)
{
$priorSetting = libxml_use_internal_errors(true);
try {
libxml_clear_errors();
$xmlPayload = new \SimpleXMLElement($xml);
if ($error = libxml_get_last_error()) {
throw new \RuntimeException($error->message);
}
} catch (\Exception $e) {
throw new ParserException(
"Error parsing XML: {$e->getMessage()}",
0,
$e,
['response' => $response]
);
} finally {
libxml_use_internal_errors($priorSetting);
}
return $xmlPayload;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\Result;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* @internal Parses query (XML) responses (e.g., EC2, SQS, and many others)
*/
class QueryParser extends AbstractParser
{
use PayloadParserTrait;
/** @var bool */
private $honorResultWrapper;
/**
* @param Service $api Service description
* @param XmlParser $xmlParser Optional XML parser
* @param bool $honorResultWrapper Set to false to disable the peeling
* back of result wrappers from the
* output structure.
*/
public function __construct(
Service $api,
XmlParser $xmlParser = null,
$honorResultWrapper = true
) {
parent::__construct($api);
$this->parser = $xmlParser ?: new XmlParser();
$this->honorResultWrapper = $honorResultWrapper;
}
public function __invoke(
CommandInterface $command,
ResponseInterface $response
) {
$output = $this->api->getOperation($command->getName())->getOutput();
$xml = $this->parseXml($response->getBody(), $response);
if ($this->honorResultWrapper && $output['resultWrapper']) {
$xml = $xml->{$output['resultWrapper']};
}
return new Result($this->parser->parse($output, $xml));
}
public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
) {
$xml = $this->parseXml($stream, $response);
return $this->parser->parse($member, $xml);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* @internal Implements REST-JSON parsing (e.g., Glacier, Elastic Transcoder)
*/
class RestJsonParser extends AbstractRestParser
{
use PayloadParserTrait;
/**
* @param Service $api Service description
* @param JsonParser $parser JSON body builder
*/
public function __construct(Service $api, JsonParser $parser = null)
{
parent::__construct($api);
$this->parser = $parser ?: new JsonParser();
}
protected function payload(
ResponseInterface $response,
StructureShape $member,
array &$result
) {
$jsonBody = $this->parseJson($response->getBody(), $response);
if ($jsonBody) {
$result += $this->parser->parse($member, $jsonBody);
}
}
public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
) {
$jsonBody = $this->parseJson($stream, $response);
if ($jsonBody) {
return $this->parser->parse($member, $jsonBody);
}
return [];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\StructureShape;
use Aws\Api\Service;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* @internal Implements REST-XML parsing (e.g., S3, CloudFront, etc...)
*/
class RestXmlParser extends AbstractRestParser
{
use PayloadParserTrait;
/**
* @param Service $api Service description
* @param XmlParser $parser XML body parser
*/
public function __construct(Service $api, XmlParser $parser = null)
{
parent::__construct($api);
$this->parser = $parser ?: new XmlParser();
}
protected function payload(
ResponseInterface $response,
StructureShape $member,
array &$result
) {
$result += $this->parseMemberFromStream($response->getBody(), $member, $response);
}
public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
) {
$xml = $this->parseXml($stream, $response);
return $this->parser->parse($member, $xml);
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace Aws\Api\Parser;
use Aws\Api\DateTimeResult;
use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Parser\Exception\ParserException;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
/**
* @internal Implements standard XML parsing for REST-XML and Query protocols.
*/
class XmlParser
{
public function parse(StructureShape $shape, \SimpleXMLElement $value)
{
return $this->dispatch($shape, $value);
}
private function dispatch($shape, \SimpleXMLElement $value)
{
static $methods = [
'structure' => 'parse_structure',
'list' => 'parse_list',
'map' => 'parse_map',
'blob' => 'parse_blob',
'boolean' => 'parse_boolean',
'integer' => 'parse_integer',
'float' => 'parse_float',
'double' => 'parse_float',
'timestamp' => 'parse_timestamp',
];
$type = $shape['type'];
if (isset($methods[$type])) {
return $this->{$methods[$type]}($shape, $value);
}
return (string) $value;
}
private function parse_structure(
StructureShape $shape,
\SimpleXMLElement $value
) {
$target = [];
foreach ($shape->getMembers() as $name => $member) {
// Extract the name of the XML node
$node = $this->memberKey($member, $name);
if (isset($value->{$node})) {
$target[$name] = $this->dispatch($member, $value->{$node});
} else {
$memberShape = $shape->getMember($name);
if (!empty($memberShape['xmlAttribute'])) {
$target[$name] = $this->parse_xml_attribute(
$shape,
$memberShape,
$value
);
}
}
}
if (isset($shape['union'])
&& $shape['union']
&& empty($target)
) {
foreach ($value as $key => $val) {
$name = $val->children()->getName();
$target['Unknown'][$name] = $val->$name;
}
}
return $target;
}
private function memberKey(Shape $shape, $name)
{
if (null !== $shape['locationName']) {
return $shape['locationName'];
}
if ($shape instanceof ListShape && $shape['flattened']) {
return $shape->getMember()['locationName'] ?: $name;
}
return $name;
}
private function parse_list(ListShape $shape, \SimpleXMLElement $value)
{
$target = [];
$member = $shape->getMember();
if (!$shape['flattened']) {
$value = $value->{$member['locationName'] ?: 'member'};
}
foreach ($value as $v) {
$target[] = $this->dispatch($member, $v);
}
return $target;
}
private function parse_map(MapShape $shape, \SimpleXMLElement $value)
{
$target = [];
if (!$shape['flattened']) {
$value = $value->entry;
}
$mapKey = $shape->getKey();
$mapValue = $shape->getValue();
$keyName = $shape->getKey()['locationName'] ?: 'key';
$valueName = $shape->getValue()['locationName'] ?: 'value';
foreach ($value as $node) {
$key = $this->dispatch($mapKey, $node->{$keyName});
$value = $this->dispatch($mapValue, $node->{$valueName});
$target[$key] = $value;
}
return $target;
}
private function parse_blob(Shape $shape, $value)
{
return base64_decode((string) $value);
}
private function parse_float(Shape $shape, $value)
{
return (float) (string) $value;
}
private function parse_integer(Shape $shape, $value)
{
return (int) (string) $value;
}
private function parse_boolean(Shape $shape, $value)
{
return $value == 'true';
}
private function parse_timestamp(Shape $shape, $value)
{
if (is_string($value)
|| is_int($value)
|| (is_object($value)
&& method_exists($value, '__toString'))
) {
return DateTimeResult::fromTimestamp(
(string) $value,
!empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null
);
}
throw new ParserException('Invalid timestamp value passed to XmlParser::parse_timestamp');
}
private function parse_xml_attribute(Shape $shape, Shape $memberShape, $value)
{
$namespace = $shape['xmlNamespace']['uri']
? $shape['xmlNamespace']['uri']
: '';
$prefix = $shape['xmlNamespace']['prefix']
? $shape['xmlNamespace']['prefix']
: '';
if (!empty($prefix)) {
$prefix .= ':';
}
$key = str_replace($prefix, '', $memberShape['locationName']);
$attributes = $value->attributes($namespace);
return isset($attributes[$key]) ? (string) $attributes[$key] : null;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\Shape;
use Aws\Api\ListShape;
/**
* @internal
*/
class Ec2ParamBuilder extends QueryParamBuilder
{
protected function queryName(Shape $shape, $default = null)
{
return ($shape['queryName']
?: ucfirst(@$shape['locationName'] ?: ""))
?: $default;
}
protected function isFlat(Shape $shape)
{
return false;
}
protected function format_list(
ListShape $shape,
array $value,
$prefix,
&$query
) {
// Handle empty list serialization
if (!$value) {
$query[$prefix] = false;
} else {
$items = $shape->getMember();
foreach ($value as $k => $v) {
$this->format($items, $v, $prefix . '.' . ($k + 1), $query);
}
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\Service;
use Aws\Api\Shape;
use Aws\Api\TimestampShape;
use Aws\Exception\InvalidJsonException;
/**
* Formats the JSON body of a JSON-REST or JSON-RPC operation.
* @internal
*/
class JsonBody
{
private $api;
public function __construct(Service $api)
{
$this->api = $api;
}
/**
* Gets the JSON Content-Type header for a service API
*
* @param Service $service
*
* @return string
*/
public static function getContentType(Service $service)
{
if ($service->getMetadata('protocol') === 'rest-json') {
return 'application/json';
}
$jsonVersion = $service->getMetadata('jsonVersion');
if (empty($jsonVersion)) {
throw new \InvalidArgumentException('invalid json');
} else {
return 'application/x-amz-json-'
. @number_format($service->getMetadata('jsonVersion'), 1);
}
}
/**
* Builds the JSON body based on an array of arguments.
*
* @param Shape $shape Operation being constructed
* @param array $args Associative array of arguments
*
* @return string
*/
public function build(Shape $shape, array $args)
{
$result = json_encode($this->format($shape, $args));
return $result == '[]' ? '{}' : $result;
}
private function format(Shape $shape, $value)
{
switch ($shape['type']) {
case 'structure':
$data = [];
if (isset($shape['document']) && $shape['document']) {
return $value;
}
foreach ($value as $k => $v) {
if ($v !== null && $shape->hasMember($k)) {
$valueShape = $shape->getMember($k);
$data[$valueShape['locationName'] ?: $k]
= $this->format($valueShape, $v);
}
}
if (empty($data)) {
return new \stdClass;
}
return $data;
case 'list':
$items = $shape->getMember();
foreach ($value as $k => $v) {
$value[$k] = $this->format($items, $v);
}
return $value;
case 'map':
if (empty($value)) {
return new \stdClass;
}
$values = $shape->getValue();
foreach ($value as $k => $v) {
$value[$k] = $this->format($values, $v);
}
return $value;
case 'blob':
return base64_encode($value);
case 'timestamp':
$timestampFormat = !empty($shape['timestampFormat'])
? $shape['timestampFormat']
: 'unixTimestamp';
return TimestampShape::format($value, $timestampFormat);
default:
return $value;
}
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\Service;
use Aws\CommandInterface;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\EndpointV2\EndpointV2SerializerTrait;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
/**
* Prepares a JSON-RPC request for transfer.
* @internal
*/
class JsonRpcSerializer
{
use EndpointV2SerializerTrait;
/** @var JsonBody */
private $jsonFormatter;
/** @var string */
private $endpoint;
/** @var Service */
private $api;
/** @var string */
private $contentType;
/**
* @param Service $api Service description
* @param string $endpoint Endpoint to connect to
* @param JsonBody $jsonFormatter Optional JSON formatter to use
*/
public function __construct(
Service $api,
$endpoint,
JsonBody $jsonFormatter = null
) {
$this->endpoint = $endpoint;
$this->api = $api;
$this->jsonFormatter = $jsonFormatter ?: new JsonBody($this->api);
$this->contentType = JsonBody::getContentType($api);
}
/**
* When invoked with an AWS command, returns a serialization array
* containing "method", "uri", "headers", and "body" key value pairs.
*
* @param CommandInterface $command Command to serialize into a request.
* @param $endpointProvider Provider used for dynamic endpoint resolution.
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
*
* @return RequestInterface
*/
public function __invoke(
CommandInterface $command,
$endpointProvider = null,
$clientArgs = null
)
{
$operationName = $command->getName();
$operation = $this->api->getOperation($operationName);
$commandArgs = $command->toArray();
$headers = [
'X-Amz-Target' => $this->api->getMetadata('targetPrefix') . '.' . $operationName,
'Content-Type' => $this->contentType
];
if ($endpointProvider instanceof EndpointProviderV2) {
$this->setRequestOptions(
$endpointProvider,
$command,
$operation,
$commandArgs,
$clientArgs,
$headers
);
}
return new Request(
$operation['http']['method'],
$this->endpoint,
$headers,
$this->jsonFormatter->build(
$operation->getInput(),
$commandArgs
)
);
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\StructureShape;
use Aws\Api\ListShape;
use Aws\Api\MapShape;
use Aws\Api\Shape;
use Aws\Api\TimestampShape;
/**
* @internal
*/
class QueryParamBuilder
{
private $methods;
protected function queryName(Shape $shape, $default = null)
{
if (null !== $shape['queryName']) {
return $shape['queryName'];
}
if (null !== $shape['locationName']) {
return $shape['locationName'];
}
if ($this->isFlat($shape) && !empty($shape['member']['locationName'])) {
return $shape['member']['locationName'];
}
return $default;
}
protected function isFlat(Shape $shape)
{
return $shape['flattened'] === true;
}
public function __invoke(StructureShape $shape, array $params)
{
if (!$this->methods) {
$this->methods = array_fill_keys(get_class_methods($this), true);
}
$query = [];
$this->format_structure($shape, $params, '', $query);
return $query;
}
protected function format(Shape $shape, $value, $prefix, array &$query)
{
$type = 'format_' . $shape['type'];
if (isset($this->methods[$type])) {
$this->{$type}($shape, $value, $prefix, $query);
} else {
$query[$prefix] = (string) $value;
}
}
protected function format_structure(
StructureShape $shape,
array $value,
$prefix,
&$query
) {
if ($prefix) {
$prefix .= '.';
}
foreach ($value as $k => $v) {
if ($shape->hasMember($k)) {
$member = $shape->getMember($k);
$this->format(
$member,
$v,
$prefix . $this->queryName($member, $k),
$query
);
}
}
}
protected function format_list(
ListShape $shape,
array $value,
$prefix,
&$query
) {
// Handle empty list serialization
if (!$value) {
$query[$prefix] = '';
return;
}
$items = $shape->getMember();
if (!$this->isFlat($shape)) {
$locationName = $shape->getMember()['locationName'] ?: 'member';
$prefix .= ".$locationName";
} elseif ($name = $this->queryName($items)) {
$parts = explode('.', $prefix);
$parts[count($parts) - 1] = $name;
$prefix = implode('.', $parts);
}
foreach ($value as $k => $v) {
$this->format($items, $v, $prefix . '.' . ($k + 1), $query);
}
}
protected function format_map(
MapShape $shape,
array $value,
$prefix,
array &$query
) {
$vals = $shape->getValue();
$keys = $shape->getKey();
if (!$this->isFlat($shape)) {
$prefix .= '.entry';
}
$i = 0;
$keyName = '%s.%d.' . $this->queryName($keys, 'key');
$valueName = '%s.%s.' . $this->queryName($vals, 'value');
foreach ($value as $k => $v) {
$i++;
$this->format($keys, $k, sprintf($keyName, $prefix, $i), $query);
$this->format($vals, $v, sprintf($valueName, $prefix, $i), $query);
}
}
protected function format_blob(Shape $shape, $value, $prefix, array &$query)
{
$query[$prefix] = base64_encode($value);
}
protected function format_timestamp(
TimestampShape $shape,
$value,
$prefix,
array &$query
) {
$timestampFormat = !empty($shape['timestampFormat'])
? $shape['timestampFormat']
: 'iso8601';
$query[$prefix] = TimestampShape::format($value, $timestampFormat);
}
protected function format_boolean(Shape $shape, $value, $prefix, array &$query)
{
$query[$prefix] = ($value) ? 'true' : 'false';
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\Service;
use Aws\CommandInterface;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\EndpointV2\EndpointV2SerializerTrait;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
/**
* Serializes a query protocol request.
* @internal
*/
class QuerySerializer
{
use EndpointV2SerializerTrait;
private $endpoint;
private $api;
private $paramBuilder;
public function __construct(
Service $api,
$endpoint,
callable $paramBuilder = null
) {
$this->api = $api;
$this->endpoint = $endpoint;
$this->paramBuilder = $paramBuilder ?: new QueryParamBuilder();
}
/**
* When invoked with an AWS command, returns a serialization array
* containing "method", "uri", "headers", and "body" key value pairs.
*
* @param CommandInterface $command Command to serialize into a request.
* @param $endpointProvider Provider used for dynamic endpoint resolution.
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
*
* @return RequestInterface
*/
public function __invoke(
CommandInterface $command,
$endpointProvider = null,
$clientArgs = null
)
{
$operation = $this->api->getOperation($command->getName());
$body = [
'Action' => $command->getName(),
'Version' => $this->api->getMetadata('apiVersion')
];
$commandArgs = $command->toArray();
// Only build up the parameters when there are parameters to build
if ($commandArgs) {
$body += call_user_func(
$this->paramBuilder,
$operation->getInput(),
$commandArgs
);
}
$body = http_build_query($body, '', '&', PHP_QUERY_RFC3986);
$headers = [
'Content-Length' => strlen($body),
'Content-Type' => 'application/x-www-form-urlencoded'
];
if ($endpointProvider instanceof EndpointProviderV2) {
$this->setRequestOptions(
$endpointProvider,
$command,
$operation,
$commandArgs,
$clientArgs,
$headers
);
}
return new Request(
'POST',
$this->endpoint,
$headers,
$body
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\Service;
use Aws\Api\StructureShape;
/**
* Serializes requests for the REST-JSON protocol.
* @internal
*/
class RestJsonSerializer extends RestSerializer
{
/** @var JsonBody */
private $jsonFormatter;
/** @var string */
private $contentType;
/**
* @param Service $api Service API description
* @param string $endpoint Endpoint to connect to
* @param JsonBody $jsonFormatter Optional JSON formatter to use
*/
public function __construct(
Service $api,
$endpoint,
JsonBody $jsonFormatter = null
) {
parent::__construct($api, $endpoint);
$this->contentType = JsonBody::getContentType($api);
$this->jsonFormatter = $jsonFormatter ?: new JsonBody($api);
}
protected function payload(StructureShape $member, array $value, array &$opts)
{
$body = isset($value) ?
((string) $this->jsonFormatter->build($member, $value))
: "{}";
$opts['headers']['Content-Type'] = $this->contentType;
$opts['body'] = $body;
}
}

View File

@@ -0,0 +1,307 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\MapShape;
use Aws\Api\Service;
use Aws\Api\Operation;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
use Aws\Api\TimestampShape;
use Aws\CommandInterface;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\EndpointV2\EndpointV2SerializerTrait;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Uri;
use GuzzleHttp\Psr7\UriResolver;
use Psr\Http\Message\RequestInterface;
/**
* Serializes HTTP locations like header, uri, payload, etc...
* @internal
*/
abstract class RestSerializer
{
use EndpointV2SerializerTrait;
/** @var Service */
private $api;
/** @var Uri */
private $endpoint;
/**
* @param Service $api Service API description
* @param string $endpoint Endpoint to connect to
*/
public function __construct(Service $api, $endpoint)
{
$this->api = $api;
$this->endpoint = Psr7\Utils::uriFor($endpoint);
}
/**
* @param CommandInterface $command Command to serialize into a request.
* @param $endpointProvider Provider used for dynamic endpoint resolution.
* @param $clientArgs Client arguments used for dynamic endpoint resolution.
*
* @return RequestInterface
*/
public function __invoke(
CommandInterface $command,
$endpointProvider = null,
$clientArgs = null
)
{
$operation = $this->api->getOperation($command->getName());
$commandArgs = $command->toArray();
$opts = $this->serialize($operation, $commandArgs);
$headers = isset($opts['headers']) ? $opts['headers'] : [];
if ($endpointProvider instanceof EndpointProviderV2) {
$this->setRequestOptions(
$endpointProvider,
$command,
$operation,
$commandArgs,
$clientArgs,
$headers
);
$this->endpoint = new Uri($this->endpoint);
}
$uri = $this->buildEndpoint($operation, $commandArgs, $opts);
return new Request(
$operation['http']['method'],
$uri,
$headers,
isset($opts['body']) ? $opts['body'] : null
);
}
/**
* Modifies a hash of request options for a payload body.
*
* @param StructureShape $member Member to serialize
* @param array $value Value to serialize
* @param array $opts Request options to modify.
*/
abstract protected function payload(
StructureShape $member,
array $value,
array &$opts
);
private function serialize(Operation $operation, array $args)
{
$opts = [];
$input = $operation->getInput();
// Apply the payload trait if present
if ($payload = $input['payload']) {
$this->applyPayload($input, $payload, $args, $opts);
}
foreach ($args as $name => $value) {
if ($input->hasMember($name)) {
$member = $input->getMember($name);
$location = $member['location'];
if (!$payload && !$location) {
$bodyMembers[$name] = $value;
} elseif ($location == 'header') {
$this->applyHeader($name, $member, $value, $opts);
} elseif ($location == 'querystring') {
$this->applyQuery($name, $member, $value, $opts);
} elseif ($location == 'headers') {
$this->applyHeaderMap($name, $member, $value, $opts);
}
}
}
if (isset($bodyMembers)) {
$this->payload($operation->getInput(), $bodyMembers, $opts);
} else if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) {
$this->payload($operation->getInput(), [], $opts);
}
return $opts;
}
private function applyPayload(StructureShape $input, $name, array $args, array &$opts)
{
if (!isset($args[$name])) {
return;
}
$m = $input->getMember($name);
if ($m['streaming'] ||
($m['type'] == 'string' || $m['type'] == 'blob')
) {
// Streaming bodies or payloads that are strings are
// always just a stream of data.
$opts['body'] = Psr7\Utils::streamFor($args[$name]);
return;
}
$this->payload($m, $args[$name], $opts);
}
private function applyHeader($name, Shape $member, $value, array &$opts)
{
if ($member->getType() === 'timestamp') {
$timestampFormat = !empty($member['timestampFormat'])
? $member['timestampFormat']
: 'rfc822';
$value = TimestampShape::format($value, $timestampFormat);
} elseif ($member->getType() === 'boolean') {
$value = $value ? 'true' : 'false';
}
if ($member['jsonvalue']) {
$value = json_encode($value);
if (empty($value) && JSON_ERROR_NONE !== json_last_error()) {
throw new \InvalidArgumentException('Unable to encode the provided value'
. ' with \'json_encode\'. ' . json_last_error_msg());
}
$value = base64_encode($value);
}
$opts['headers'][$member['locationName'] ?: $name] = $value;
}
/**
* Note: This is currently only present in the Amazon S3 model.
*/
private function applyHeaderMap($name, Shape $member, array $value, array &$opts)
{
$prefix = $member['locationName'];
foreach ($value as $k => $v) {
$opts['headers'][$prefix . $k] = $v;
}
}
private function applyQuery($name, Shape $member, $value, array &$opts)
{
if ($member instanceof MapShape) {
$opts['query'] = isset($opts['query']) && is_array($opts['query'])
? $opts['query'] + $value
: $value;
} elseif ($value !== null) {
$type = $member->getType();
if ($type === 'boolean') {
$value = $value ? 'true' : 'false';
} elseif ($type === 'timestamp') {
$timestampFormat = !empty($member['timestampFormat'])
? $member['timestampFormat']
: 'iso8601';
$value = TimestampShape::format($value, $timestampFormat);
}
$opts['query'][$member['locationName'] ?: $name] = $value;
}
}
private function buildEndpoint(Operation $operation, array $args, array $opts)
{
// Create an associative array of variable definitions used in expansions
$varDefinitions = $this->getVarDefinitions($operation, $args);
$relative = preg_replace_callback(
'/\{([^\}]+)\}/',
function (array $matches) use ($varDefinitions) {
$isGreedy = substr($matches[1], -1, 1) == '+';
$k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
if (!isset($varDefinitions[$k])) {
return '';
}
if ($isGreedy) {
return str_replace('%2F', '/', rawurlencode($varDefinitions[$k]));
}
return rawurlencode($varDefinitions[$k]);
},
$operation['http']['requestUri']
);
// Add the query string variables or appending to one if needed.
if (!empty($opts['query'])) {
$relative = $this->appendQuery($opts['query'], $relative);
}
$path = $this->endpoint->getPath();
//Accounts for trailing '/' in path when custom endpoint
//is provided to endpointProviderV2
if ($this->api->isModifiedModel()
&& $this->api->getServiceName() === 's3'
) {
if (substr($path, -1) === '/' && $relative[0] === '/') {
$path = rtrim($path, '/');
}
$relative = $path . $relative;
}
// If endpoint has path, remove leading '/' to preserve URI resolution.
if ($path && $relative[0] === '/') {
$relative = substr($relative, 1);
}
//Append path to endpoint when leading '//...' present
// as uri cannot be properly resolved
if ($this->api->isModifiedModel()
&& strpos($relative, '//') === 0
) {
return new Uri($this->endpoint . $relative);
}
// Expand path place holders using Amazon's slightly different URI
// template syntax.
return UriResolver::resolve($this->endpoint, new Uri($relative));
}
/**
* @param StructureShape $input
*/
private function hasPayloadParam(StructureShape $input, $payload)
{
if ($payload) {
$potentiallyEmptyTypes = ['blob','string'];
if ($this->api->getMetadata('protocol') == 'rest-xml') {
$potentiallyEmptyTypes[] = 'structure';
}
$payloadMember = $input->getMember($payload);
if (in_array($payloadMember['type'], $potentiallyEmptyTypes)) {
return false;
}
}
foreach ($input->getMembers() as $member) {
if (!isset($member['location'])) {
return true;
}
}
return false;
}
private function appendQuery($query, $endpoint)
{
$append = Psr7\Query::build($query);
return $endpoint .= strpos($endpoint, '?') !== false ? "&{$append}" : "?{$append}";
}
private function getVarDefinitions($command, $args)
{
$varDefinitions = [];
foreach ($command->getInput()->getMembers() as $name => $member) {
if ($member['location'] == 'uri') {
$varDefinitions[$member['locationName'] ?: $name] =
isset($args[$name])
? $args[$name]
: null;
}
}
return $varDefinitions;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\StructureShape;
use Aws\Api\Service;
/**
* @internal
*/
class RestXmlSerializer extends RestSerializer
{
/** @var XmlBody */
private $xmlBody;
/**
* @param Service $api Service API description
* @param string $endpoint Endpoint to connect to
* @param XmlBody $xmlBody Optional XML formatter to use
*/
public function __construct(
Service $api,
$endpoint,
XmlBody $xmlBody = null
) {
parent::__construct($api, $endpoint);
$this->xmlBody = $xmlBody ?: new XmlBody($api);
}
protected function payload(StructureShape $member, array $value, array &$opts)
{
$opts['headers']['Content-Type'] = 'application/xml';
$opts['body'] = $this->getXmlBody($member, $value);
}
/**
* @param StructureShape $member
* @param array $value
* @return string
*/
private function getXmlBody(StructureShape $member, array $value)
{
$xmlBody = (string)$this->xmlBody->build($member, $value);
$xmlBody = str_replace("'", "&apos;", $xmlBody);
$xmlBody = str_replace('\r', "&#13;", $xmlBody);
$xmlBody = str_replace('\n', "&#10;", $xmlBody);
return $xmlBody;
}
}

View File

@@ -0,0 +1,220 @@
<?php
namespace Aws\Api\Serializer;
use Aws\Api\MapShape;
use Aws\Api\Service;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
use Aws\Api\ListShape;
use Aws\Api\TimestampShape;
use XMLWriter;
/**
* @internal Formats the XML body of a REST-XML services.
*/
class XmlBody
{
/** @var \Aws\Api\Service */
private $api;
/**
* @param Service $api API being used to create the XML body.
*/
public function __construct(Service $api)
{
$this->api = $api;
}
/**
* Builds the XML body based on an array of arguments.
*
* @param Shape $shape Operation being constructed
* @param array $args Associative array of arguments
*
* @return string
*/
public function build(Shape $shape, array $args)
{
$xml = new XMLWriter();
$xml->openMemory();
$xml->startDocument('1.0', 'UTF-8');
$this->format($shape, $shape['locationName'] ?: $shape['name'], $args, $xml);
$xml->endDocument();
return $xml->outputMemory();
}
private function startElement(Shape $shape, $name, XMLWriter $xml)
{
$xml->startElement($name);
if ($ns = $shape['xmlNamespace']) {
$xml->writeAttribute(
isset($ns['prefix']) ? "xmlns:{$ns['prefix']}" : 'xmlns',
$shape['xmlNamespace']['uri']
);
}
}
private function format(Shape $shape, $name, $value, XMLWriter $xml)
{
// Any method mentioned here has a custom serialization handler.
static $methods = [
'add_structure' => true,
'add_list' => true,
'add_blob' => true,
'add_timestamp' => true,
'add_boolean' => true,
'add_map' => true,
'add_string' => true
];
$type = 'add_' . $shape['type'];
if (isset($methods[$type])) {
$this->{$type}($shape, $name, $value, $xml);
} else {
$this->defaultShape($shape, $name, $value, $xml);
}
}
private function defaultShape(Shape $shape, $name, $value, XMLWriter $xml)
{
$this->startElement($shape, $name, $xml);
$xml->text($value);
$xml->endElement();
}
private function add_structure(
StructureShape $shape,
$name,
array $value,
\XMLWriter $xml
) {
$this->startElement($shape, $name, $xml);
foreach ($this->getStructureMembers($shape, $value) as $k => $definition) {
$this->format(
$definition['member'],
$definition['member']['locationName'] ?: $k,
$definition['value'],
$xml
);
}
$xml->endElement();
}
private function getStructureMembers(StructureShape $shape, array $value)
{
$members = [];
foreach ($value as $k => $v) {
if ($v !== null && $shape->hasMember($k)) {
$definition = [
'member' => $shape->getMember($k),
'value' => $v,
];
if ($definition['member']['xmlAttribute']) {
// array_unshift_associative
$members = [$k => $definition] + $members;
} else {
$members[$k] = $definition;
}
}
}
return $members;
}
private function add_list(
ListShape $shape,
$name,
array $value,
XMLWriter $xml
) {
$items = $shape->getMember();
if ($shape['flattened']) {
$elementName = $name;
} else {
$this->startElement($shape, $name, $xml);
$elementName = $items['locationName'] ?: 'member';
}
foreach ($value as $v) {
$this->format($items, $elementName, $v, $xml);
}
if (!$shape['flattened']) {
$xml->endElement();
}
}
private function add_map(
MapShape $shape,
$name,
array $value,
XMLWriter $xml
) {
$xmlEntry = $shape['flattened'] ? $shape['locationName'] : 'entry';
$xmlKey = $shape->getKey()['locationName'] ?: 'key';
$xmlValue = $shape->getValue()['locationName'] ?: 'value';
$this->startElement($shape, $name, $xml);
foreach ($value as $key => $v) {
$this->startElement($shape, $xmlEntry, $xml);
$this->format($shape->getKey(), $xmlKey, $key, $xml);
$this->format($shape->getValue(), $xmlValue, $v, $xml);
$xml->endElement();
}
$xml->endElement();
}
private function add_blob(Shape $shape, $name, $value, XMLWriter $xml)
{
$this->startElement($shape, $name, $xml);
$xml->writeRaw(base64_encode($value));
$xml->endElement();
}
private function add_timestamp(
TimestampShape $shape,
$name,
$value,
XMLWriter $xml
) {
$this->startElement($shape, $name, $xml);
$timestampFormat = !empty($shape['timestampFormat'])
? $shape['timestampFormat']
: 'iso8601';
$xml->writeRaw(TimestampShape::format($value, $timestampFormat));
$xml->endElement();
}
private function add_boolean(
Shape $shape,
$name,
$value,
XMLWriter $xml
) {
$this->startElement($shape, $name, $xml);
$xml->writeRaw($value ? 'true' : 'false');
$xml->endElement();
}
private function add_string(
Shape $shape,
$name,
$value,
XMLWriter $xml
) {
if ($shape['xmlAttribute']) {
$xml->writeAttribute($shape['locationName'] ?: $name, $value);
} else {
$this->defaultShape($shape, $name, $value, $xml);
}
}
}

View File

@@ -0,0 +1,535 @@
<?php
namespace Aws\Api;
/**
* Represents a web service API model.
*/
class Service extends AbstractModel
{
/** @var callable */
private $apiProvider;
/** @var string */
private $serviceName;
/** @var string */
private $apiVersion;
/** @var array */
private $clientContextParams = [];
/** @var Operation[] */
private $operations = [];
/** @var array */
private $paginators = null;
/** @var array */
private $waiters = null;
/** @var boolean */
private $modifiedModel = false;
/**
* @param array $definition
* @param callable $provider
*
* @internal param array $definition Service description
*/
public function __construct(array $definition, callable $provider)
{
static $defaults = [
'operations' => [],
'shapes' => [],
'metadata' => [],
'clientContextParams' => []
], $defaultMeta = [
'apiVersion' => null,
'serviceFullName' => null,
'serviceId' => null,
'endpointPrefix' => null,
'signingName' => null,
'signatureVersion' => null,
'protocol' => null,
'uid' => null
];
$definition += $defaults;
$definition['metadata'] += $defaultMeta;
$this->definition = $definition;
$this->apiProvider = $provider;
parent::__construct($definition, new ShapeMap($definition['shapes']));
if (isset($definition['metadata']['serviceIdentifier'])) {
$this->serviceName = $this->getServiceName();
} else {
$this->serviceName = $this->getEndpointPrefix();
}
$this->apiVersion = $this->getApiVersion();
if (isset($definition['clientContextParams'])) {
$this->clientContextParams = $definition['clientContextParams'];
}
}
/**
* Creates a request serializer for the provided API object.
*
* @param Service $api API that contains a protocol.
* @param string $endpoint Endpoint to send requests to.
*
* @return callable
* @throws \UnexpectedValueException
*/
public static function createSerializer(Service $api, $endpoint)
{
static $mapping = [
'json' => Serializer\JsonRpcSerializer::class,
'query' => Serializer\QuerySerializer::class,
'rest-json' => Serializer\RestJsonSerializer::class,
'rest-xml' => Serializer\RestXmlSerializer::class
];
$proto = $api->getProtocol();
if (isset($mapping[$proto])) {
return new $mapping[$proto]($api, $endpoint);
}
if ($proto == 'ec2') {
return new Serializer\QuerySerializer($api, $endpoint, new Serializer\Ec2ParamBuilder());
}
throw new \UnexpectedValueException(
'Unknown protocol: ' . $api->getProtocol()
);
}
/**
* Creates an error parser for the given protocol.
*
* Redundant method signature to preserve backwards compatibility.
*
* @param string $protocol Protocol to parse (e.g., query, json, etc.)
*
* @return callable
* @throws \UnexpectedValueException
*/
public static function createErrorParser($protocol, Service $api = null)
{
static $mapping = [
'json' => ErrorParser\JsonRpcErrorParser::class,
'query' => ErrorParser\XmlErrorParser::class,
'rest-json' => ErrorParser\RestJsonErrorParser::class,
'rest-xml' => ErrorParser\XmlErrorParser::class,
'ec2' => ErrorParser\XmlErrorParser::class
];
if (isset($mapping[$protocol])) {
return new $mapping[$protocol]($api);
}
throw new \UnexpectedValueException("Unknown protocol: $protocol");
}
/**
* Applies the listeners needed to parse client models.
*
* @param Service $api API to create a parser for
* @return callable
* @throws \UnexpectedValueException
*/
public static function createParser(Service $api)
{
static $mapping = [
'json' => Parser\JsonRpcParser::class,
'query' => Parser\QueryParser::class,
'rest-json' => Parser\RestJsonParser::class,
'rest-xml' => Parser\RestXmlParser::class
];
$proto = $api->getProtocol();
if (isset($mapping[$proto])) {
return new $mapping[$proto]($api);
}
if ($proto == 'ec2') {
return new Parser\QueryParser($api, null, false);
}
throw new \UnexpectedValueException(
'Unknown protocol: ' . $api->getProtocol()
);
}
/**
* Get the full name of the service
*
* @return string
*/
public function getServiceFullName()
{
return $this->definition['metadata']['serviceFullName'];
}
/**
* Get the service id
*
* @return string
*/
public function getServiceId()
{
return $this->definition['metadata']['serviceId'];
}
/**
* Get the API version of the service
*
* @return string
*/
public function getApiVersion()
{
return $this->definition['metadata']['apiVersion'];
}
/**
* Get the API version of the service
*
* @return string
*/
public function getEndpointPrefix()
{
return $this->definition['metadata']['endpointPrefix'];
}
/**
* Get the signing name used by the service.
*
* @return string
*/
public function getSigningName()
{
return $this->definition['metadata']['signingName']
?: $this->definition['metadata']['endpointPrefix'];
}
/**
* Get the service name.
*
* @return string
*/
public function getServiceName()
{
return $this->definition['metadata']['serviceIdentifier'];
}
/**
* Get the default signature version of the service.
*
* Note: this method assumes "v4" when not specified in the model.
*
* @return string
*/
public function getSignatureVersion()
{
return $this->definition['metadata']['signatureVersion'] ?: 'v4';
}
/**
* Get the protocol used by the service.
*
* @return string
*/
public function getProtocol()
{
return $this->definition['metadata']['protocol'];
}
/**
* Get the uid string used by the service
*
* @return string
*/
public function getUid()
{
return $this->definition['metadata']['uid'];
}
/**
* Check if the description has a specific operation by name.
*
* @param string $name Operation to check by name
*
* @return bool
*/
public function hasOperation($name)
{
return isset($this['operations'][$name]);
}
/**
* Get an operation by name.
*
* @param string $name Operation to retrieve by name
*
* @return Operation
* @throws \InvalidArgumentException If the operation is not found
*/
public function getOperation($name)
{
if (!isset($this->operations[$name])) {
if (!isset($this->definition['operations'][$name])) {
throw new \InvalidArgumentException("Unknown operation: $name");
}
$this->operations[$name] = new Operation(
$this->definition['operations'][$name],
$this->shapeMap
);
} else if ($this->modifiedModel) {
$this->operations[$name] = new Operation(
$this->definition['operations'][$name],
$this->shapeMap
);
}
return $this->operations[$name];
}
/**
* Get all of the operations of the description.
*
* @return Operation[]
*/
public function getOperations()
{
$result = [];
foreach ($this->definition['operations'] as $name => $definition) {
$result[$name] = $this->getOperation($name);
}
return $result;
}
/**
* Get all of the error shapes of the service
*
* @return array
*/
public function getErrorShapes()
{
$result = [];
foreach ($this->definition['shapes'] as $name => $definition) {
if (!empty($definition['exception'])) {
$definition['name'] = $name;
$result[] = new StructureShape($definition, $this->getShapeMap());
}
}
return $result;
}
/**
* Get all of the service metadata or a specific metadata key value.
*
* @param string|null $key Key to retrieve or null to retrieve all metadata
*
* @return mixed Returns the result or null if the key is not found
*/
public function getMetadata($key = null)
{
if (!$key) {
return $this['metadata'];
}
if (isset($this->definition['metadata'][$key])) {
return $this->definition['metadata'][$key];
}
return null;
}
/**
* Gets an associative array of available paginator configurations where
* the key is the name of the paginator, and the value is the paginator
* configuration.
*
* @return array
* @unstable The configuration format of paginators may change in the future
*/
public function getPaginators()
{
if (!isset($this->paginators)) {
$res = call_user_func(
$this->apiProvider,
'paginator',
$this->serviceName,
$this->apiVersion
);
$this->paginators = isset($res['pagination'])
? $res['pagination']
: [];
}
return $this->paginators;
}
/**
* Determines if the service has a paginator by name.
*
* @param string $name Name of the paginator.
*
* @return bool
*/
public function hasPaginator($name)
{
return isset($this->getPaginators()[$name]);
}
/**
* Retrieve a paginator by name.
*
* @param string $name Paginator to retrieve by name. This argument is
* typically the operation name.
* @return array
* @throws \UnexpectedValueException if the paginator does not exist.
* @unstable The configuration format of paginators may change in the future
*/
public function getPaginatorConfig($name)
{
static $defaults = [
'input_token' => null,
'output_token' => null,
'limit_key' => null,
'result_key' => null,
'more_results' => null,
];
if ($this->hasPaginator($name)) {
return $this->paginators[$name] + $defaults;
}
throw new \UnexpectedValueException("There is no {$name} "
. "paginator defined for the {$this->serviceName} service.");
}
/**
* Gets an associative array of available waiter configurations where the
* key is the name of the waiter, and the value is the waiter
* configuration.
*
* @return array
*/
public function getWaiters()
{
if (!isset($this->waiters)) {
$res = call_user_func(
$this->apiProvider,
'waiter',
$this->serviceName,
$this->apiVersion
);
$this->waiters = isset($res['waiters'])
? $res['waiters']
: [];
}
return $this->waiters;
}
/**
* Determines if the service has a waiter by name.
*
* @param string $name Name of the waiter.
*
* @return bool
*/
public function hasWaiter($name)
{
return isset($this->getWaiters()[$name]);
}
/**
* Get a waiter configuration by name.
*
* @param string $name Name of the waiter by name.
*
* @return array
* @throws \UnexpectedValueException if the waiter does not exist.
*/
public function getWaiterConfig($name)
{
// Error if the waiter is not defined
if ($this->hasWaiter($name)) {
return $this->waiters[$name];
}
throw new \UnexpectedValueException("There is no {$name} waiter "
. "defined for the {$this->serviceName} service.");
}
/**
* Get the shape map used by the API.
*
* @return ShapeMap
*/
public function getShapeMap()
{
return $this->shapeMap;
}
/**
* Get all the context params of the description.
*
* @return array
*/
public function getClientContextParams()
{
return $this->clientContextParams;
}
/**
* Get the service's api provider.
*
* @return callable
*/
public function getProvider()
{
return $this->apiProvider;
}
/**
* Get the service's definition.
*
* @return callable
*/
public function getDefinition()
{
return $this->definition;
}
/**
* Sets the service's api definition.
* Intended for internal use only.
*
* @return void
*
* @internal
*/
public function setDefinition($definition)
{
$this->definition = $definition;
$this->modifiedModel = true;
}
/**
* Denotes whether or not a service's definition has
* been modified. Intended for internal use only.
*
* @return bool
*
* @internal
*/
public function isModifiedModel()
{
return $this->modifiedModel;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Aws\Api;
/**
* Base class representing a modeled shape.
*/
class Shape extends AbstractModel
{
/**
* Get a concrete shape for the given definition.
*
* @param array $definition
* @param ShapeMap $shapeMap
*
* @return mixed
* @throws \RuntimeException if the type is invalid
*/
public static function create(array $definition, ShapeMap $shapeMap)
{
static $map = [
'structure' => StructureShape::class,
'map' => MapShape::class,
'list' => ListShape::class,
'timestamp' => TimestampShape::class,
'integer' => Shape::class,
'double' => Shape::class,
'float' => Shape::class,
'long' => Shape::class,
'string' => Shape::class,
'byte' => Shape::class,
'character' => Shape::class,
'blob' => Shape::class,
'boolean' => Shape::class
];
if (isset($definition['shape'])) {
return $shapeMap->resolve($definition);
}
if (!isset($map[$definition['type']])) {
throw new \RuntimeException('Invalid type: '
. print_r($definition, true));
}
$type = $map[$definition['type']];
return new $type($definition, $shapeMap);
}
/**
* Get the type of the shape
*
* @return string
*/
public function getType()
{
return $this->definition['type'];
}
/**
* Get the name of the shape
*
* @return string
*/
public function getName()
{
return $this->definition['name'];
}
/**
* Get a context param definition.
*/
public function getContextParam()
{
return $this->contextParam;
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Aws\Api;
/**
* Builds shape based on shape references.
*/
class ShapeMap
{
/** @var array */
private $definitions;
/** @var Shape[] */
private $simple;
/**
* @param array $shapeModels Associative array of shape definitions.
*/
public function __construct(array $shapeModels)
{
$this->definitions = $shapeModels;
}
/**
* Get an array of shape names.
*
* @return array
*/
public function getShapeNames()
{
return array_keys($this->definitions);
}
/**
* Resolve a shape reference
*
* @param array $shapeRef Shape reference shape
*
* @return Shape
* @throws \InvalidArgumentException
*/
public function resolve(array $shapeRef)
{
$shape = $shapeRef['shape'];
if (!isset($this->definitions[$shape])) {
throw new \InvalidArgumentException('Shape not found: ' . $shape);
}
$isSimple = count($shapeRef) == 1;
if ($isSimple && isset($this->simple[$shape])) {
return $this->simple[$shape];
}
$definition = $shapeRef + $this->definitions[$shape];
$definition['name'] = $definition['shape'];
if (isset($definition['shape'])) {
unset($definition['shape']);
}
$result = Shape::create($definition, $this);
if ($isSimple) {
$this->simple[$shape] = $result;
}
return $result;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Aws\Api;
/**
* Represents a structure shape and resolve member shape references.
*/
class StructureShape extends Shape
{
/**
* @var Shape[]
*/
private $members;
public function __construct(array $definition, ShapeMap $shapeMap)
{
$definition['type'] = 'structure';
if (!isset($definition['members'])) {
$definition['members'] = [];
}
parent::__construct($definition, $shapeMap);
}
/**
* Gets a list of all members
*
* @return Shape[]
*/
public function getMembers()
{
if (empty($this->members)) {
$this->generateMembersHash();
}
return $this->members;
}
/**
* Check if a specific member exists by name.
*
* @param string $name Name of the member to check
*
* @return bool
*/
public function hasMember($name)
{
return isset($this->definition['members'][$name]);
}
/**
* Retrieve a member by name.
*
* @param string $name Name of the member to retrieve
*
* @return Shape
* @throws \InvalidArgumentException if the member is not found.
*/
public function getMember($name)
{
$members = $this->getMembers();
if (!isset($members[$name])) {
throw new \InvalidArgumentException('Unknown member ' . $name);
}
return $members[$name];
}
private function generateMembersHash()
{
$this->members = [];
foreach ($this->definition['members'] as $name => $definition) {
$this->members[$name] = $this->shapeFor($definition);
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Aws\Api;
/**
* Represents a timestamp shape.
*/
class TimestampShape extends Shape
{
public function __construct(array $definition, ShapeMap $shapeMap)
{
$definition['type'] = 'timestamp';
parent::__construct($definition, $shapeMap);
}
/**
* Formats a timestamp value for a service.
*
* @param mixed $value Value to format
* @param string $format Format used to serialize the value
*
* @return int|string
* @throws \UnexpectedValueException if the format is unknown.
* @throws \InvalidArgumentException if the value is an unsupported type.
*/
public static function format($value, $format)
{
if ($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
} elseif (is_string($value)) {
$value = strtotime($value);
} elseif (!is_int($value)) {
throw new \InvalidArgumentException('Unable to handle the provided'
. ' timestamp type: ' . gettype($value));
}
switch ($format) {
case 'iso8601':
return gmdate('Y-m-d\TH:i:s\Z', $value);
case 'rfc822':
return gmdate('D, d M Y H:i:s \G\M\T', $value);
case 'unixTimestamp':
return $value;
default:
throw new \UnexpectedValueException('Unknown timestamp format: '
. $format);
}
}
}

View File

@@ -0,0 +1,346 @@
<?php
namespace Aws\Api;
use Aws;
/**
* Validates a schema against a hash of input.
*/
class Validator
{
private $path = [];
private $errors = [];
private $constraints = [];
private static $defaultConstraints = [
'required' => true,
'min' => true,
'max' => false,
'pattern' => false
];
/**
* @param array $constraints Associative array of constraints to enforce.
* Accepts the following keys: "required", "min",
* "max", and "pattern". If a key is not
* provided, the constraint will assume false.
*/
public function __construct(array $constraints = null)
{
static $assumedFalseValues = [
'required' => false,
'min' => false,
'max' => false,
'pattern' => false
];
$this->constraints = empty($constraints)
? self::$defaultConstraints
: $constraints + $assumedFalseValues;
}
/**
* Validates the given input against the schema.
*
* @param string $name Operation name
* @param Shape $shape Shape to validate
* @param array $input Input to validate
*
* @throws \InvalidArgumentException if the input is invalid.
*/
public function validate($name, Shape $shape, array $input)
{
$this->dispatch($shape, $input);
if ($this->errors) {
$message = sprintf(
"Found %d error%s while validating the input provided for the "
. "%s operation:\n%s",
count($this->errors),
count($this->errors) > 1 ? 's' : '',
$name,
implode("\n", $this->errors)
);
$this->errors = [];
throw new \InvalidArgumentException($message);
}
}
private function dispatch(Shape $shape, $value)
{
static $methods = [
'structure' => 'check_structure',
'list' => 'check_list',
'map' => 'check_map',
'blob' => 'check_blob',
'boolean' => 'check_boolean',
'integer' => 'check_numeric',
'float' => 'check_numeric',
'long' => 'check_numeric',
'string' => 'check_string',
'byte' => 'check_string',
'char' => 'check_string'
];
$type = $shape->getType();
if (isset($methods[$type])) {
$this->{$methods[$type]}($shape, $value);
}
}
private function check_structure(StructureShape $shape, $value)
{
$isDocument = (isset($shape['document']) && $shape['document']);
$isUnion = (isset($shape['union']) && $shape['union']);
if ($isDocument) {
if (!$this->checkDocumentType($value)) {
$this->addError("is not a valid document type");
return;
}
} elseif ($isUnion) {
if (!$this->checkUnion($value)) {
$this->addError("is a union type and must have exactly one non null value");
return;
}
} elseif (!$this->checkAssociativeArray($value)) {
return;
}
if ($this->constraints['required'] && $shape['required']) {
foreach ($shape['required'] as $req) {
if (!isset($value[$req])) {
$this->path[] = $req;
$this->addError('is missing and is a required parameter');
array_pop($this->path);
}
}
}
if (!$isDocument) {
foreach ($value as $name => $v) {
if ($shape->hasMember($name)) {
$this->path[] = $name;
$this->dispatch(
$shape->getMember($name),
isset($value[$name]) ? $value[$name] : null
);
array_pop($this->path);
}
}
}
}
private function check_list(ListShape $shape, $value)
{
if (!is_array($value)) {
$this->addError('must be an array. Found '
. Aws\describe_type($value));
return;
}
$this->validateRange($shape, count($value), "list element count");
$items = $shape->getMember();
foreach ($value as $index => $v) {
$this->path[] = $index;
$this->dispatch($items, $v);
array_pop($this->path);
}
}
private function check_map(MapShape $shape, $value)
{
if (!$this->checkAssociativeArray($value)) {
return;
}
$values = $shape->getValue();
foreach ($value as $key => $v) {
$this->path[] = $key;
$this->dispatch($values, $v);
array_pop($this->path);
}
}
private function check_blob(Shape $shape, $value)
{
static $valid = [
'string' => true,
'integer' => true,
'double' => true,
'resource' => true
];
$type = gettype($value);
if (!isset($valid[$type])) {
if ($type != 'object' || !method_exists($value, '__toString')) {
$this->addError('must be an fopen resource, a '
. 'GuzzleHttp\Stream\StreamInterface object, or something '
. 'that can be cast to a string. Found '
. Aws\describe_type($value));
}
}
}
private function check_numeric(Shape $shape, $value)
{
if (!is_numeric($value)) {
$this->addError('must be numeric. Found '
. Aws\describe_type($value));
return;
}
$this->validateRange($shape, $value, "numeric value");
}
private function check_boolean(Shape $shape, $value)
{
if (!is_bool($value)) {
$this->addError('must be a boolean. Found '
. Aws\describe_type($value));
}
}
private function check_string(Shape $shape, $value)
{
if ($shape['jsonvalue']) {
if (!self::canJsonEncode($value)) {
$this->addError('must be a value encodable with \'json_encode\'.'
. ' Found ' . Aws\describe_type($value));
}
return;
}
if (!$this->checkCanString($value)) {
$this->addError('must be a string or an object that implements '
. '__toString(). Found ' . Aws\describe_type($value));
return;
}
$value = isset($value) ? $value : '';
$this->validateRange($shape, strlen($value), "string length");
if ($this->constraints['pattern']) {
$pattern = $shape['pattern'];
if ($pattern && !preg_match("/$pattern/", $value)) {
$this->addError("Pattern /$pattern/ failed to match '$value'");
}
}
}
private function validateRange(Shape $shape, $length, $descriptor)
{
if ($this->constraints['min']) {
$min = $shape['min'];
if ($min && $length < $min) {
$this->addError("expected $descriptor to be >= $min, but "
. "found $descriptor of $length");
}
}
if ($this->constraints['max']) {
$max = $shape['max'];
if ($max && $length > $max) {
$this->addError("expected $descriptor to be <= $max, but "
. "found $descriptor of $length");
}
}
}
private function checkArray($arr)
{
return $this->isIndexed($arr) || $this->isAssociative($arr);
}
private function isAssociative($arr)
{
return count(array_filter(array_keys($arr), "is_string")) == count($arr);
}
private function isIndexed(array $arr)
{
return $arr == array_values($arr);
}
private function checkCanString($value)
{
static $valid = [
'string' => true,
'integer' => true,
'double' => true,
'NULL' => true,
];
$type = gettype($value);
return isset($valid[$type]) ||
($type == 'object' && method_exists($value, '__toString'));
}
private function checkAssociativeArray($value)
{
$isAssociative = false;
if (is_array($value)) {
$expectedIndex = 0;
$key = key($value);
do {
$isAssociative = $key !== $expectedIndex++;
next($value);
$key = key($value);
} while (!$isAssociative && null !== $key);
}
if (!$isAssociative) {
$this->addError('must be an associative array. Found '
. Aws\describe_type($value));
return false;
}
return true;
}
private function checkDocumentType($value)
{
if (is_array($value)) {
$typeOfFirstKey = gettype(key($value));
foreach ($value as $key => $val) {
if (!$this->checkDocumentType($val) || gettype($key) != $typeOfFirstKey) {
return false;
}
}
return $this->checkArray($value);
}
return is_null($value)
|| is_numeric($value)
|| is_string($value)
|| is_bool($value);
}
private function checkUnion($value)
{
if (is_array($value)) {
$nonNullCount = 0;
foreach ($value as $key => $val) {
if (!is_null($val) && !(strpos($key, "@") === 0)) {
$nonNullCount++;
}
}
return $nonNullCount == 1;
}
return !is_null($value);
}
private function addError($message)
{
$this->errors[] =
implode('', array_map(function ($s) { return "[{$s}]"; }, $this->path))
. ' '
. $message;
}
private function canJsonEncode($data)
{
return !is_resource($data);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Aws\Arn;
use Aws\Arn\Exception\InvalidArnException;
/**
* @internal
*/
class AccessPointArn extends Arn implements AccessPointArnInterface
{
use ResourceTypeAndIdTrait;
/**
* AccessPointArn constructor
*
* @param $data
*/
public function __construct($data)
{
parent::__construct($data);
static::validate($this->data);
}
public static function parse($string)
{
$data = parent::parse($string);
$data = self::parseResourceTypeAndId($data);
$data['accesspoint_name'] = $data['resource_id'];
return $data;
}
public function getAccesspointName()
{
return $this->data['accesspoint_name'];
}
/**
* Validation specific to AccessPointArn
*
* @param array $data
*/
protected static function validate(array $data)
{
self::validateRegion($data, 'access point ARN');
self::validateAccountId($data, 'access point ARN');
if ($data['resource_type'] !== 'accesspoint') {
throw new InvalidArnException("The 6th component of an access point ARN"
. " represents the resource type and must be 'accesspoint'.");
}
if (empty($data['resource_id'])) {
throw new InvalidArnException("The 7th component of an access point ARN"
. " represents the resource ID and must not be empty.");
}
if (strpos($data['resource_id'], ':') !== false) {
throw new InvalidArnException("The resource ID component of an access"
. " point ARN must not contain additional components"
. " (delimited by ':').");
}
if (!self::isValidHostLabel($data['resource_id'])) {
throw new InvalidArnException("The resource ID in an access point ARN"
. " must be a valid host label value.");
}
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Aws\Arn;
/**
* @internal
*/
interface AccessPointArnInterface extends ArnInterface
{
public function getAccesspointName();
}

View File

@@ -0,0 +1,188 @@
<?php
namespace Aws\Arn;
use Aws\Arn\Exception\InvalidArnException;
/**
* Amazon Resource Names (ARNs) uniquely identify AWS resources. The Arn class
* parses and stores a generic ARN object representation that can apply to any
* service resource.
*
* @internal
*/
class Arn implements ArnInterface
{
protected $data;
protected $string;
public static function parse($string)
{
$data = [
'arn' => null,
'partition' => null,
'service' => null,
'region' => null,
'account_id' => null,
'resource' => null,
];
$length = strlen($string);
$lastDelim = 0;
$numComponents = 0;
for ($i = 0; $i < $length; $i++) {
if (($numComponents < 5 && $string[$i] === ':')) {
// Split components between delimiters
$data[key($data)] = substr($string, $lastDelim, $i - $lastDelim);
// Do not include delimiter character itself
$lastDelim = $i + 1;
next($data);
$numComponents++;
}
if ($i === $length - 1) {
// Put the remainder in the last component.
if (in_array($numComponents, [5])) {
$data['resource'] = substr($string, $lastDelim);
} else {
// If there are < 5 components, put remainder in current
// component.
$data[key($data)] = substr($string, $lastDelim);
}
}
}
return $data;
}
public function __construct($data)
{
if (is_array($data)) {
$this->data = $data;
} elseif (is_string($data)) {
$this->data = static::parse($data);
} else {
throw new InvalidArnException('Constructor accepts a string or an'
. ' array as an argument.');
}
static::validate($this->data);
}
public function __toString()
{
if (!isset($this->string)) {
$components = [
$this->getPrefix(),
$this->getPartition(),
$this->getService(),
$this->getRegion(),
$this->getAccountId(),
$this->getResource(),
];
$this->string = implode(':', $components);
}
return $this->string;
}
public function getPrefix()
{
return $this->data['arn'];
}
public function getPartition()
{
return $this->data['partition'];
}
public function getService()
{
return $this->data['service'];
}
public function getRegion()
{
return $this->data['region'];
}
public function getAccountId()
{
return $this->data['account_id'];
}
public function getResource()
{
return $this->data['resource'];
}
public function toArray()
{
return $this->data;
}
/**
* Minimally restrictive generic ARN validation
*
* @param array $data
*/
protected static function validate(array $data)
{
if ($data['arn'] !== 'arn') {
throw new InvalidArnException("The 1st component of an ARN must be"
. " 'arn'.");
}
if (empty($data['partition'])) {
throw new InvalidArnException("The 2nd component of an ARN"
. " represents the partition and must not be empty.");
}
if (empty($data['service'])) {
throw new InvalidArnException("The 3rd component of an ARN"
. " represents the service and must not be empty.");
}
if (empty($data['resource'])) {
throw new InvalidArnException("The 6th component of an ARN"
. " represents the resource information and must not be empty."
. " Individual service ARNs may include additional delimiters"
. " to further qualify resources.");
}
}
protected static function validateAccountId($data, $arnName)
{
if (!self::isValidHostLabel($data['account_id'])) {
throw new InvalidArnException("The 5th component of a {$arnName}"
. " is required, represents the account ID, and"
. " must be a valid host label.");
}
}
protected static function validateRegion($data, $arnName)
{
if (empty($data['region'])) {
throw new InvalidArnException("The 4th component of a {$arnName}"
. " represents the region and must not be empty.");
}
}
/**
* Validates whether a string component is a valid host label
*
* @param $string
* @return bool
*/
protected static function isValidHostLabel($string)
{
if (empty($string) || strlen($string) > 63) {
return false;
}
if ($value = preg_match("/^[a-zA-Z0-9-]+$/", $string)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Aws\Arn;
/**
* Amazon Resource Names (ARNs) uniquely identify AWS resources. Classes
* implementing ArnInterface parse and store an ARN object representation.
*
* Valid ARN formats include:
*
* arn:partition:service:region:account-id:resource-id
* arn:partition:service:region:account-id:resource-type/resource-id
* arn:partition:service:region:account-id:resource-type:resource-id
*
* Some components may be omitted, depending on the service and resource type.
*
* @internal
*/
interface ArnInterface
{
public static function parse($string);
public function __toString();
public function getPrefix();
public function getPartition();
public function getService();
public function getRegion();
public function getAccountId();
public function getResource();
public function toArray();
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Aws\Arn;
use Aws\Arn\S3\AccessPointArn as S3AccessPointArn;
use Aws\Arn\ObjectLambdaAccessPointArn;
use Aws\Arn\S3\MultiRegionAccessPointArn;
use Aws\Arn\S3\OutpostsBucketArn;
use Aws\Arn\S3\RegionalBucketArn;
use Aws\Arn\S3\OutpostsAccessPointArn;
/**
* This class provides functionality to parse ARN strings and return a
* corresponding ARN object. ARN-parsing logic may be subject to change in the
* future, so this should not be relied upon for external customer usage.
*
* @internal
*/
class ArnParser
{
/**
* @param $string
* @return bool
*/
public static function isArn($string)
{
return $string !== null && strpos($string, 'arn:') === 0;
}
/**
* Parses a string and returns an instance of ArnInterface. Returns a
* specific type of Arn object if it has a specific class representation
* or a generic Arn object if not.
*
* @param $string
* @return ArnInterface
*/
public static function parse($string)
{
$data = Arn::parse($string);
if ($data['service'] === 's3-object-lambda') {
return new ObjectLambdaAccessPointArn($string);
}
$resource = self::explodeResourceComponent($data['resource']);
if ($resource[0] === 'outpost') {
if (isset($resource[2]) && $resource[2] === 'bucket') {
return new OutpostsBucketArn($string);
}
if (isset($resource[2]) && $resource[2] === 'accesspoint') {
return new OutpostsAccessPointArn($string);
}
}
if (empty($data['region'])) {
return new MultiRegionAccessPointArn($string);
}
if ($resource[0] === 'accesspoint') {
if ($data['service'] === 's3') {
return new S3AccessPointArn($string);
}
return new AccessPointArn($string);
}
return new Arn($data);
}
private static function explodeResourceComponent($resource)
{
return preg_split("/[\/:]/", $resource);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Aws\Arn\Exception;
/**
* Represents a failed attempt to construct an Arn
*/
class InvalidArnException extends \RuntimeException {}

View File

@@ -0,0 +1,35 @@
<?php
namespace Aws\Arn;
/**
* This class represents an S3 Object bucket ARN, which is in the
* following format:
*
* @internal
*/
class ObjectLambdaAccessPointArn extends AccessPointArn
{
/**
* Parses a string into an associative array of components that represent
* a ObjectLambdaAccessPointArn
*
* @param $string
* @return array
*/
public static function parse($string)
{
$data = parent::parse($string);
return parent::parseResourceTypeAndId($data);
}
/**
*
* @param array $data
*/
protected static function validate(array $data)
{
parent::validate($data);
self::validateRegion($data, 'S3 Object Lambda ARN');
self::validateAccountId($data, 'S3 Object Lambda ARN');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Aws\Arn;
/**
* @internal
*/
trait ResourceTypeAndIdTrait
{
public function getResourceType()
{
return $this->data['resource_type'];
}
public function getResourceId()
{
return $this->data['resource_id'];
}
protected static function parseResourceTypeAndId(array $data)
{
$resourceData = preg_split("/[\/:]/", $data['resource'], 2);
$data['resource_type'] = isset($resourceData[0])
? $resourceData[0]
: null;
$data['resource_id'] = isset($resourceData[1])
? $resourceData[1]
: null;
return $data;
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Aws\Arn\S3;
use Aws\Arn\AccessPointArn as BaseAccessPointArn;
use Aws\Arn\AccessPointArnInterface;
use Aws\Arn\ArnInterface;
use Aws\Arn\Exception\InvalidArnException;
/**
* @internal
*/
class AccessPointArn extends BaseAccessPointArn implements AccessPointArnInterface
{
/**
* Validation specific to AccessPointArn
*
* @param array $data
*/
public static function validate(array $data)
{
parent::validate($data);
if ($data['service'] !== 's3') {
throw new InvalidArnException("The 3rd component of an S3 access"
. " point ARN represents the region and must be 's3'.");
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Aws\Arn\S3;
use Aws\Arn\ArnInterface;
/**
* @internal
*/
interface BucketArnInterface extends ArnInterface
{
public function getBucketName();
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Aws\Arn\S3;
use Aws\Arn\Arn;
use Aws\Arn\ResourceTypeAndIdTrait;
/**
* This class represents an S3 multi-region bucket ARN, which is in the
* following format:
*
* @internal
*/
class MultiRegionAccessPointArn extends AccessPointArn
{
use ResourceTypeAndIdTrait;
/**
* Parses a string into an associative array of components that represent
* a MultiRegionArn
*
* @param $string
* @return array
*/
public static function parse($string)
{
return parent::parse($string);
}
/**
*
* @param array $data
*/
public static function validate(array $data)
{
Arn::validate($data);
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Aws\Arn\S3;
use Aws\Arn\AccessPointArn as BaseAccessPointArn;
use Aws\Arn\AccessPointArnInterface;
use Aws\Arn\Arn;
use Aws\Arn\Exception\InvalidArnException;
/**
* This class represents an S3 Outposts access point ARN, which is in the
* following format:
*
* arn:{partition}:s3-outposts:{region}:{accountId}:outpost:{outpostId}:accesspoint:{accesspointName}
*
* ':' and '/' can be used interchangeably as delimiters for components after
* the account ID.
*
* @internal
*/
class OutpostsAccessPointArn extends BaseAccessPointArn implements
AccessPointArnInterface,
OutpostsArnInterface
{
public static function parse($string)
{
$data = parent::parse($string);
return self::parseOutpostData($data);
}
public function getOutpostId()
{
return $this->data['outpost_id'];
}
public function getAccesspointName()
{
return $this->data['accesspoint_name'];
}
private static function parseOutpostData(array $data)
{
$resourceData = preg_split("/[\/:]/", $data['resource_id']);
$data['outpost_id'] = isset($resourceData[0])
? $resourceData[0]
: null;
$data['accesspoint_type'] = isset($resourceData[1])
? $resourceData[1]
: null;
$data['accesspoint_name'] = isset($resourceData[2])
? $resourceData[2]
: null;
if (isset($resourceData[3])) {
$data['resource_extra'] = implode(':', array_slice($resourceData, 3));
}
return $data;
}
/**
* Validation specific to OutpostsAccessPointArn. Note this uses the base Arn
* class validation instead of the direct parent due to it having slightly
* differing requirements from its parent.
*
* @param array $data
*/
public static function validate(array $data)
{
Arn::validate($data);
if (($data['service'] !== 's3-outposts')) {
throw new InvalidArnException("The 3rd component of an S3 Outposts"
. " access point ARN represents the service and must be"
. " 's3-outposts'.");
}
self::validateRegion($data, 'S3 Outposts access point ARN');
self::validateAccountId($data, 'S3 Outposts access point ARN');
if (($data['resource_type'] !== 'outpost')) {
throw new InvalidArnException("The 6th component of an S3 Outposts"
. " access point ARN represents the resource type and must be"
. " 'outpost'.");
}
if (!self::isValidHostLabel($data['outpost_id'])) {
throw new InvalidArnException("The 7th component of an S3 Outposts"
. " access point ARN is required, represents the outpost ID, and"
. " must be a valid host label.");
}
if ($data['accesspoint_type'] !== 'accesspoint') {
throw new InvalidArnException("The 8th component of an S3 Outposts"
. " access point ARN must be 'accesspoint'");
}
if (!self::isValidHostLabel($data['accesspoint_name'])) {
throw new InvalidArnException("The 9th component of an S3 Outposts"
. " access point ARN is required, represents the accesspoint name,"
. " and must be a valid host label.");
}
if (!empty($data['resource_extra'])) {
throw new InvalidArnException("An S3 Outposts access point ARN"
. " should only have 9 components, delimited by the characters"
. " ':' and '/'. '{$data['resource_extra']}' was found after the"
. " 9th component.");
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Aws\Arn\S3;
use Aws\Arn\ArnInterface;
/**
* @internal
*/
interface OutpostsArnInterface extends ArnInterface
{
public function getOutpostId();
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Aws\Arn\S3;
use Aws\Arn\Arn;
use Aws\Arn\Exception\InvalidArnException;
use Aws\Arn\ResourceTypeAndIdTrait;
/**
* This class represents an S3 Outposts bucket ARN, which is in the
* following format:
*
* @internal
*/
class OutpostsBucketArn extends Arn implements
BucketArnInterface,
OutpostsArnInterface
{
use ResourceTypeAndIdTrait;
/**
* Parses a string into an associative array of components that represent
* a OutpostsBucketArn
*
* @param $string
* @return array
*/
public static function parse($string)
{
$data = parent::parse($string);
$data = self::parseResourceTypeAndId($data);
return self::parseOutpostData($data);
}
public function getBucketName()
{
return $this->data['bucket_name'];
}
public function getOutpostId()
{
return $this->data['outpost_id'];
}
private static function parseOutpostData(array $data)
{
$resourceData = preg_split("/[\/:]/", $data['resource_id'], 3);
$data['outpost_id'] = isset($resourceData[0])
? $resourceData[0]
: null;
$data['bucket_label'] = isset($resourceData[1])
? $resourceData[1]
: null;
$data['bucket_name'] = isset($resourceData[2])
? $resourceData[2]
: null;
return $data;
}
/**
*
* @param array $data
*/
public static function validate(array $data)
{
Arn::validate($data);
if (($data['service'] !== 's3-outposts')) {
throw new InvalidArnException("The 3rd component of an S3 Outposts"
. " bucket ARN represents the service and must be 's3-outposts'.");
}
self::validateRegion($data, 'S3 Outposts bucket ARN');
self::validateAccountId($data, 'S3 Outposts bucket ARN');
if (($data['resource_type'] !== 'outpost')) {
throw new InvalidArnException("The 6th component of an S3 Outposts"
. " bucket ARN represents the resource type and must be"
. " 'outpost'.");
}
if (!self::isValidHostLabel($data['outpost_id'])) {
throw new InvalidArnException("The 7th component of an S3 Outposts"
. " bucket ARN is required, represents the outpost ID, and"
. " must be a valid host label.");
}
if ($data['bucket_label'] !== 'bucket') {
throw new InvalidArnException("The 8th component of an S3 Outposts"
. " bucket ARN must be 'bucket'");
}
if (empty($data['bucket_name'])) {
throw new InvalidArnException("The 9th component of an S3 Outposts"
. " bucket ARN represents the bucket name and must not be empty.");
}
}
}

View File

@@ -0,0 +1,669 @@
<?php
namespace Aws;
use Aws\Api\ApiProvider;
use Aws\Api\DocModel;
use Aws\Api\Service;
use Aws\EndpointDiscovery\EndpointDiscoveryMiddleware;
use Aws\EndpointV2\EndpointProviderV2;
use Aws\Exception\AwsException;
use Aws\Signature\SignatureProvider;
use GuzzleHttp\Psr7\Uri;
/**
* Default AWS client implementation
*/
class AwsClient implements AwsClientInterface
{
use AwsClientTrait;
/** @var array */
private $aliases;
/** @var array */
private $config;
/** @var string */
private $region;
/** @var string */
private $endpoint;
/** @var Service */
private $api;
/** @var callable */
private $signatureProvider;
/** @var callable */
private $credentialProvider;
/** @var callable */
private $tokenProvider;
/** @var HandlerList */
private $handlerList;
/** @var array*/
private $defaultRequestOptions;
/** @var array*/
private $clientContextParams = [];
/** @var array*/
protected $clientBuiltIns = [];
/** @var EndpointProviderV2 | callable */
protected $endpointProvider;
/** @var callable */
protected $serializer;
/**
* Get an array of client constructor arguments used by the client.
*
* @return array
*/
public static function getArguments()
{
return ClientResolver::getDefaultArguments();
}
/**
* The client constructor accepts the following options:
*
* - api_provider: (callable) An optional PHP callable that accepts a
* type, service, and version argument, and returns an array of
* corresponding configuration data. The type value can be one of api,
* waiter, or paginator.
* - credentials:
* (Aws\Credentials\CredentialsInterface|array|bool|callable) Specifies
* the credentials used to sign requests. Provide an
* Aws\Credentials\CredentialsInterface object, an associative array of
* "key", "secret", and an optional "token" key, `false` to use null
* credentials, or a callable credentials provider used to create
* credentials or return null. See Aws\Credentials\CredentialProvider for
* a list of built-in credentials providers. If no credentials are
* provided, the SDK will attempt to load them from the environment.
* - token:
* (Aws\Token\TokenInterface|array|bool|callable) Specifies
* the token used to authorize requests. Provide an
* Aws\Token\TokenInterface object, an associative array of
* "token" and an optional "expires" key, `false` to use no
* token, or a callable token provider used to create a
* token or return null. See Aws\Token\TokenProvider for
* a list of built-in token providers. If no token is
* provided, the SDK will attempt to load one from the environment.
* - csm:
* (Aws\ClientSideMonitoring\ConfigurationInterface|array|callable) Specifies
* the credentials used to sign requests. Provide an
* Aws\ClientSideMonitoring\ConfigurationInterface object, a callable
* configuration provider used to create client-side monitoring configuration,
* `false` to disable csm, or an associative array with the following keys:
* enabled: (bool) Set to true to enable client-side monitoring, defaults
* to false; host: (string) the host location to send monitoring events to,
* defaults to 127.0.0.1; port: (int) The port used for the host connection,
* defaults to 31000; client_id: (string) An identifier for this project
* - debug: (bool|array) Set to true to display debug information when
* sending requests. Alternatively, you can provide an associative array
* with the following keys: logfn: (callable) Function that is invoked
* with log messages; stream_size: (int) When the size of a stream is
* greater than this number, the stream data will not be logged (set to
* "0" to not log any stream data); scrub_auth: (bool) Set to false to
* disable the scrubbing of auth data from the logged messages; http:
* (bool) Set to false to disable the "debug" feature of lower level HTTP
* adapters (e.g., verbose curl output).
* - stats: (bool|array) Set to true to gather transfer statistics on
* requests sent. Alternatively, you can provide an associative array with
* the following keys: retries: (bool) Set to false to disable reporting
* on retries attempted; http: (bool) Set to true to enable collecting
* statistics from lower level HTTP adapters (e.g., values returned in
* GuzzleHttp\TransferStats). HTTP handlers must support an
* `http_stats_receiver` option for this to have an effect; timer: (bool)
* Set to true to enable a command timer that reports the total wall clock
* time spent on an operation in seconds.
* - disable_host_prefix_injection: (bool) Set to true to disable host prefix
* injection logic for services that use it. This disables the entire
* prefix injection, including the portions supplied by user-defined
* parameters. Setting this flag will have no effect on services that do
* not use host prefix injection.
* - endpoint: (string) The full URI of the webservice. This is only
* required when connecting to a custom endpoint (e.g., a local version
* of S3).
* - endpoint_discovery: (Aws\EndpointDiscovery\ConfigurationInterface,
* Aws\CacheInterface, array, callable) Settings for endpoint discovery.
* Provide an instance of Aws\EndpointDiscovery\ConfigurationInterface,
* an instance Aws\CacheInterface, a callable that provides a promise for
* a Configuration object, or an associative array with the following
* keys: enabled: (bool) Set to true to enable endpoint discovery, false
* to explicitly disable it, defaults to false; cache_limit: (int) The
* maximum number of keys in the endpoints cache, defaults to 1000.
* - endpoint_provider: (callable) An optional PHP callable that
* accepts a hash of options including a "service" and "region" key and
* returns NULL or a hash of endpoint data, of which the "endpoint" key
* is required. See Aws\Endpoint\EndpointProvider for a list of built-in
* providers.
* - handler: (callable) A handler that accepts a command object,
* request object and returns a promise that is fulfilled with an
* Aws\ResultInterface object or rejected with an
* Aws\Exception\AwsException. A handler does not accept a next handler
* as it is terminal and expected to fulfill a command. If no handler is
* provided, a default Guzzle handler will be utilized.
* - http: (array, default=array(0)) Set to an array of SDK request
* options to apply to each request (e.g., proxy, verify, etc.).
* - http_handler: (callable) An HTTP handler is a function that
* accepts a PSR-7 request object and returns a promise that is fulfilled
* with a PSR-7 response object or rejected with an array of exception
* data. NOTE: This option supersedes any provided "handler" option.
* - idempotency_auto_fill: (bool|callable) Set to false to disable SDK to
* populate parameters that enabled 'idempotencyToken' trait with a random
* UUID v4 value on your behalf. Using default value 'true' still allows
* parameter value to be overwritten when provided. Note: auto-fill only
* works when cryptographically secure random bytes generator functions
* (random_bytes, openssl_random_pseudo_bytes or mcrypt_create_iv) can be
* found. You may also provide a callable source of random bytes.
* - profile: (string) Allows you to specify which profile to use when
* credentials are created from the AWS credentials file in your HOME
* directory. This setting overrides the AWS_PROFILE environment
* variable. Note: Specifying "profile" will cause the "credentials" key
* to be ignored.
* - region: (string, required) Region to connect to. See
* http://docs.aws.amazon.com/general/latest/gr/rande.html for a list of
* available regions.
* - retries: (int, Aws\Retry\ConfigurationInterface, Aws\CacheInterface,
* array, callable) Configures the retry mode and maximum number of
* allowed retries for a client (pass 0 to disable retries). Provide an
* integer for 'legacy' mode with the specified number of retries.
* Otherwise provide an instance of Aws\Retry\ConfigurationInterface, an
* instance of Aws\CacheInterface, a callable function, or an array with
* the following keys: mode: (string) Set to 'legacy', 'standard' (uses
* retry quota management), or 'adapative' (an experimental mode that adds
* client-side rate limiting to standard mode); max_attempts (int) The
* maximum number of attempts for a given request.
* - scheme: (string, default=string(5) "https") URI scheme to use when
* connecting connect. The SDK will utilize "https" endpoints (i.e.,
* utilize SSL/TLS connections) by default. You can attempt to connect to
* a service over an unencrypted "http" endpoint by setting ``scheme`` to
* "http".
* - signature_provider: (callable) A callable that accepts a signature
* version name (e.g., "v4"), a service name, and region, and
* returns a SignatureInterface object or null. This provider is used to
* create signers utilized by the client. See
* Aws\Signature\SignatureProvider for a list of built-in providers
* - signature_version: (string) A string representing a custom
* signature version to use with a service (e.g., v4). Note that
* per/operation signature version MAY override this requested signature
* version.
* - use_aws_shared_config_files: (bool, default=bool(true)) Set to false to
* disable checking for shared config file in '~/.aws/config' and
* '~/.aws/credentials'. This will override the AWS_CONFIG_FILE
* environment variable.
* - validate: (bool, default=bool(true)) Set to false to disable
* client-side parameter validation.
* - version: (string, required) The version of the webservice to
* utilize (e.g., 2006-03-01).
*
* @param array $args Client configuration arguments.
*
* @throws \InvalidArgumentException if any required options are missing or
* the service is not supported.
*/
public function __construct(array $args)
{
list($service, $exceptionClass) = $this->parseClass();
if (!isset($args['service'])) {
$args['service'] = manifest($service)['endpoint'];
}
if (!isset($args['exception_class'])) {
$args['exception_class'] = $exceptionClass;
}
$this->handlerList = new HandlerList();
$resolver = new ClientResolver(static::getArguments());
$config = $resolver->resolve($args, $this->handlerList);
$this->api = $config['api'];
$this->signatureProvider = $config['signature_provider'];
$this->endpoint = new Uri($config['endpoint']);
$this->credentialProvider = $config['credentials'];
$this->tokenProvider = $config['token'];
$this->region = isset($config['region']) ? $config['region'] : null;
$this->config = $config['config'];
$this->setClientBuiltIns($args);
$this->clientContextParams = $this->setClientContextParams($args);
$this->defaultRequestOptions = $config['http'];
$this->endpointProvider = $config['endpoint_provider'];
$this->serializer = $config['serializer'];
$this->addSignatureMiddleware();
$this->addInvocationId();
$this->addEndpointParameterMiddleware($args);
$this->addEndpointDiscoveryMiddleware($config, $args);
$this->addRequestCompressionMiddleware($config);
$this->loadAliases();
$this->addStreamRequestPayload();
$this->addRecursionDetection();
$this->addRequestBuilder();
if (!$config['suppress_php_deprecation_warning']) {
$this->emitDeprecationWarning();
}
if (isset($args['with_resolved'])) {
$args['with_resolved']($config);
}
}
public function getHandlerList()
{
return $this->handlerList;
}
public function getConfig($option = null)
{
return $option === null
? $this->config
: (isset($this->config[$option])
? $this->config[$option]
: null);
}
public function getCredentials()
{
$fn = $this->credentialProvider;
return $fn();
}
public function getEndpoint()
{
return $this->endpoint;
}
public function getRegion()
{
return $this->region;
}
public function getApi()
{
return $this->api;
}
public function getCommand($name, array $args = [])
{
// Fail fast if the command cannot be found in the description.
if (!isset($this->getApi()['operations'][$name])) {
$name = ucfirst($name);
if (!isset($this->getApi()['operations'][$name])) {
throw new \InvalidArgumentException("Operation not found: $name");
}
}
if (!isset($args['@http'])) {
$args['@http'] = $this->defaultRequestOptions;
} else {
$args['@http'] += $this->defaultRequestOptions;
}
return new Command($name, $args, clone $this->getHandlerList());
}
public function getEndpointProvider()
{
return $this->endpointProvider;
}
/**
* Provides the set of service context parameter
* key-value pairs used for endpoint resolution.
*
* @return array
*/
public function getClientContextParams()
{
return $this->clientContextParams;
}
/**
* Provides the set of built-in keys and values
* used for endpoint resolution
*
* @return array
*/
public function getClientBuiltIns()
{
return $this->clientBuiltIns;
}
public function __sleep()
{
throw new \RuntimeException('Instances of ' . static::class
. ' cannot be serialized');
}
/**
* Get the signature_provider function of the client.
*
* @return callable
*/
final public function getSignatureProvider()
{
return $this->signatureProvider;
}
/**
* Parse the class name and setup the custom exception class of the client
* and return the "service" name of the client and "exception_class".
*
* @return array
*/
private function parseClass()
{
$klass = get_class($this);
if ($klass === __CLASS__) {
return ['', AwsException::class];
}
$service = substr($klass, strrpos($klass, '\\') + 1, -6);
return [
strtolower($service),
"Aws\\{$service}\\Exception\\{$service}Exception"
];
}
private function addEndpointParameterMiddleware($args)
{
if (empty($args['disable_host_prefix_injection'])) {
$list = $this->getHandlerList();
$list->appendBuild(
EndpointParameterMiddleware::wrap(
$this->api
),
'endpoint_parameter'
);
}
}
private function addEndpointDiscoveryMiddleware($config, $args)
{
$list = $this->getHandlerList();
if (!isset($args['endpoint'])) {
$list->appendBuild(
EndpointDiscoveryMiddleware::wrap(
$this,
$args,
$config['endpoint_discovery']
),
'EndpointDiscoveryMiddleware'
);
}
}
private function addSignatureMiddleware()
{
$api = $this->getApi();
$provider = $this->signatureProvider;
$version = $this->config['signature_version'];
$name = $this->config['signing_name'];
$region = $this->config['signing_region'];
$resolver = static function (
CommandInterface $c
) use ($api, $provider, $name, $region, $version) {
if (!empty($c['@context']['signing_region'])) {
$region = $c['@context']['signing_region'];
}
if (!empty($c['@context']['signing_service'])) {
$name = $c['@context']['signing_service'];
}
$authType = $api->getOperation($c->getName())['authtype'];
switch ($authType){
case 'none':
$version = 'anonymous';
break;
case 'v4-unsigned-body':
$version = 'v4-unsigned-body';
break;
case 'bearer':
$version = 'bearer';
break;
}
if (isset($c['@context']['signature_version'])) {
if ($c['@context']['signature_version'] == 'v4a') {
$version = 'v4a';
}
}
if (!empty($endpointAuthSchemes = $c->getAuthSchemes())) {
$version = $endpointAuthSchemes['version'];
$name = isset($endpointAuthSchemes['name']) ?
$endpointAuthSchemes['name'] : $name;
$region = isset($endpointAuthSchemes['region']) ?
$endpointAuthSchemes['region'] : $region;
}
return SignatureProvider::resolve($provider, $version, $name, $region);
};
$this->handlerList->appendSign(
Middleware::signer($this->credentialProvider, $resolver, $this->tokenProvider),
'signer'
);
}
private function addRequestCompressionMiddleware($config)
{
if (empty($config['disable_request_compression'])) {
$list = $this->getHandlerList();
$list->appendBuild(
RequestCompressionMiddleware::wrap($config),
'request-compression'
);
}
}
private function addInvocationId()
{
// Add invocation id to each request
$this->handlerList->prependSign(Middleware::invocationId(), 'invocation-id');
}
private function loadAliases($file = null)
{
if (!isset($this->aliases)) {
if (is_null($file)) {
$file = __DIR__ . '/data/aliases.json';
}
$aliases = \Aws\load_compiled_json($file);
$serviceId = $this->api->getServiceId();
$version = $this->getApi()->getApiVersion();
if (!empty($aliases['operations'][$serviceId][$version])) {
$this->aliases = array_flip($aliases['operations'][$serviceId][$version]);
}
}
}
private function addStreamRequestPayload()
{
$streamRequestPayloadMiddleware = StreamRequestPayloadMiddleware::wrap(
$this->api
);
$this->handlerList->prependSign(
$streamRequestPayloadMiddleware,
'StreamRequestPayloadMiddleware'
);
}
private function addRecursionDetection()
{
// Add recursion detection header to requests
// originating in supported Lambda runtimes
$this->handlerList->appendBuild(
Middleware::recursionDetection(), 'recursion-detection'
);
}
/**
* Adds the `builder` middleware such that a client's endpoint
* provider and endpoint resolution arguments can be passed.
*/
private function addRequestBuilder()
{
$handlerList = $this->getHandlerList();
$serializer = $this->serializer;
$endpointProvider = $this->endpointProvider;
$endpointArgs = $this->getEndpointProviderArgs();
$handlerList->prependBuild(
Middleware::requestBuilder(
$serializer,
$endpointProvider,
$endpointArgs
),
'builderV2'
);
}
/**
* Retrieves client context param definition from service model,
* creates mapping of client context param names with client-provided
* values.
*
* @return array
*/
private function setClientContextParams($args)
{
$api = $this->getApi();
$resolvedParams = [];
if (!empty($paramDefinitions = $api->getClientContextParams())) {
foreach($paramDefinitions as $paramName => $paramValue) {
if (isset($args[$paramName])) {
$result[$paramName] = $args[$paramName];
}
}
}
return $resolvedParams;
}
/**
* Retrieves and sets default values used for endpoint resolution.
*/
private function setClientBuiltIns($args)
{
$builtIns = [];
$config = $this->getConfig();
$service = $args['service'];
$builtIns['SDK::Endpoint'] = isset($args['endpoint']) ? $args['endpoint'] : null;
$builtIns['AWS::Region'] = $this->getRegion();
$builtIns['AWS::UseFIPS'] = $config['use_fips_endpoint']->isUseFipsEndpoint();
$builtIns['AWS::UseDualStack'] = $config['use_dual_stack_endpoint']->isUseDualstackEndpoint();
if ($service === 's3' || $service === 's3control'){
$builtIns['AWS::S3::UseArnRegion'] = $config['use_arn_region']->isUseArnRegion();
}
if ($service === 's3') {
$builtIns['AWS::S3::UseArnRegion'] = $config['use_arn_region']->isUseArnRegion();
$builtIns['AWS::S3::Accelerate'] = $config['use_accelerate_endpoint'];
$builtIns['AWS::S3::ForcePathStyle'] = $config['use_path_style_endpoint'];
$builtIns['AWS::S3::DisableMultiRegionAccessPoints'] = $config['disable_multiregion_access_points'];
}
$this->clientBuiltIns += $builtIns;
}
/**
* Retrieves arguments to be used in endpoint resolution.
*
* @return array
*/
public function getEndpointProviderArgs()
{
return $this->normalizeEndpointProviderArgs();
}
/**
* Combines built-in and client context parameter values in
* order of specificity. Client context parameter values supersede
* built-in values.
*
* @return array
*/
private function normalizeEndpointProviderArgs()
{
$normalizedBuiltIns = [];
foreach($this->clientBuiltIns as $name => $value) {
$normalizedName = explode('::', $name);
$normalizedName = $normalizedName[count($normalizedName) - 1];
$normalizedBuiltIns[$normalizedName] = $value;
}
return array_merge($normalizedBuiltIns, $this->getClientContextParams());
}
protected function isUseEndpointV2()
{
return $this->endpointProvider instanceof EndpointProviderV2;
}
public static function emitDeprecationWarning() {
$phpVersion = PHP_VERSION_ID;
if ($phpVersion < 70205) {
$phpVersionString = phpversion();
@trigger_error(
"This installation of the SDK is using PHP version"
. " {$phpVersionString}, which will be deprecated on August"
. " 15th, 2023. Please upgrade your PHP version to a minimum of"
. " 7.2.5 before then to continue receiving updates to the AWS"
. " SDK for PHP. To disable this warning, set"
. " suppress_php_deprecation_warning to true on the client constructor"
. " or set the environment variable AWS_SUPPRESS_PHP_DEPRECATION_WARNING"
. " to true.",
E_USER_DEPRECATED
);
}
}
/**
* Returns a service model and doc model with any necessary changes
* applied.
*
* @param array $api Array of service data being documented.
* @param array $docs Array of doc model data.
*
* @return array Tuple containing a [Service, DocModel]
*
* @internal This should only used to document the service API.
* @codeCoverageIgnore
*/
public static function applyDocFilters(array $api, array $docs)
{
$aliases = \Aws\load_compiled_json(__DIR__ . '/data/aliases.json');
$serviceId = $api['metadata']['serviceId'];
$version = $api['metadata']['apiVersion'];
// Replace names for any operations with SDK aliases
if (!empty($aliases['operations'][$serviceId][$version])) {
foreach ($aliases['operations'][$serviceId][$version] as $op => $alias) {
$api['operations'][$alias] = $api['operations'][$op];
$docs['operations'][$alias] = $docs['operations'][$op];
unset($api['operations'][$op], $docs['operations'][$op]);
}
}
ksort($api['operations']);
return [
new Service($api, ApiProvider::defaultProvider()),
new DocModel($docs)
];
}
/**
* @deprecated
* @return static
*/
public static function factory(array $config = [])
{
return new static($config);
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Aws;
use Psr\Http\Message\UriInterface;
use GuzzleHttp\Promise\PromiseInterface;
/**
* Represents an AWS client.
*/
interface AwsClientInterface
{
/**
* Creates and executes a command for an operation by name.
*
* Suffixing an operation name with "Async" will return a
* promise that can be used to execute commands asynchronously.
*
* @param string $name Name of the command to execute.
* @param array $arguments Arguments to pass to the getCommand method.
*
* @return ResultInterface
* @throws \Exception
*/
public function __call($name, array $arguments);
/**
* Create a command for an operation name.
*
* Special keys may be set on the command to control how it behaves,
* including:
*
* - @http: Associative array of transfer specific options to apply to the
* request that is serialized for this command. Available keys include
* "proxy", "verify", "timeout", "connect_timeout", "debug", "delay", and
* "headers".
*
* @param string $name Name of the operation to use in the command
* @param array $args Arguments to pass to the command
*
* @return CommandInterface
* @throws \InvalidArgumentException if no command can be found by name
*/
public function getCommand($name, array $args = []);
/**
* Execute a single command.
*
* @param CommandInterface $command Command to execute
*
* @return ResultInterface
* @throws \Exception
*/
public function execute(CommandInterface $command);
/**
* Execute a command asynchronously.
*
* @param CommandInterface $command Command to execute
*
* @return \GuzzleHttp\Promise\PromiseInterface
*/
public function executeAsync(CommandInterface $command);
/**
* Returns a promise that is fulfilled with an
* {@see \Aws\Credentials\CredentialsInterface} object.
*
* If you need the credentials synchronously, then call the wait() method
* on the returned promise.
*
* @return PromiseInterface
*/
public function getCredentials();
/**
* Get the region to which the client is configured to send requests.
*
* @return string
*/
public function getRegion();
/**
* Gets the default endpoint, or base URL, used by the client.
*
* @return UriInterface
*/
public function getEndpoint();
/**
* Get the service description associated with the client.
*
* @return \Aws\Api\Service
*/
public function getApi();
/**
* Get a client configuration value.
*
* @param string|null $option The option to retrieve. Pass null to retrieve
* all options.
* @return mixed|null
*/
public function getConfig($option = null);
/**
* Get the handler list used to transfer commands.
*
* This list can be modified to add middleware or to change the underlying
* handler used to send HTTP requests.
*
* @return HandlerList
*/
public function getHandlerList();
/**
* Get a resource iterator for the specified operation.
*
* @param string $name Name of the iterator to retrieve.
* @param array $args Command arguments to use with each command.
*
* @return \Iterator
* @throws \UnexpectedValueException if the iterator config is invalid.
*/
public function getIterator($name, array $args = []);
/**
* Get a result paginator for the specified operation.
*
* @param string $name Name of the operation used for iterator
* @param array $args Command args to be used with each command
*
* @return \Aws\ResultPaginator
* @throws \UnexpectedValueException if the iterator config is invalid.
*/
public function getPaginator($name, array $args = []);
/**
* Wait until a resource is in a particular state.
*
* @param string|callable $name Name of the waiter that defines the wait
* configuration and conditions.
* @param array $args Args to be used with each command executed
* by the waiter. Waiter configuration options
* can be provided in an associative array in
* the @waiter key.
* @return void
* @throws \UnexpectedValueException if the waiter is invalid.
*/
public function waitUntil($name, array $args = []);
/**
* Get a waiter that waits until a resource is in a particular state.
*
* Retrieving a waiter can be useful when you wish to wait asynchronously:
*
* $waiter = $client->getWaiter('foo', ['bar' => 'baz']);
* $waiter->promise()->then(function () { echo 'Done!'; });
*
* @param string|callable $name Name of the waiter that defines the wait
* configuration and conditions.
* @param array $args Args to be used with each command executed
* by the waiter. Waiter configuration options
* can be provided in an associative array in
* the @waiter key.
* @return \Aws\Waiter
* @throws \UnexpectedValueException if the waiter is invalid.
*/
public function getWaiter($name, array $args = []);
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Aws;
use Aws\Api\Service;
/**
* A trait providing generic functionality for interacting with Amazon Web
* Services. This is meant to be used in classes implementing
* \Aws\AwsClientInterface
*/
trait AwsClientTrait
{
public function getPaginator($name, array $args = [])
{
$config = $this->getApi()->getPaginatorConfig($name);
return new ResultPaginator($this, $name, $args, $config);
}
public function getIterator($name, array $args = [])
{
$config = $this->getApi()->getPaginatorConfig($name);
if (!$config['result_key']) {
throw new \UnexpectedValueException(sprintf(
'There are no resources to iterate for the %s operation of %s',
$name, $this->getApi()['serviceFullName']
));
}
$key = is_array($config['result_key'])
? $config['result_key'][0]
: $config['result_key'];
if ($config['output_token'] && $config['input_token']) {
return $this->getPaginator($name, $args)->search($key);
}
$result = $this->execute($this->getCommand($name, $args))->search($key);
return new \ArrayIterator((array) $result);
}
public function waitUntil($name, array $args = [])
{
return $this->getWaiter($name, $args)->promise()->wait();
}
public function getWaiter($name, array $args = [])
{
$config = isset($args['@waiter']) ? $args['@waiter'] : [];
$config += $this->getApi()->getWaiterConfig($name);
return new Waiter($this, $name, $args, $config);
}
public function execute(CommandInterface $command)
{
return $this->executeAsync($command)->wait();
}
public function executeAsync(CommandInterface $command)
{
$handler = $command->getHandlerList()->resolve();
return $handler($command);
}
public function __call($name, array $args)
{
if (substr($name, -5) === 'Async') {
$name = substr($name, 0, -5);
$isAsync = true;
}
if (!empty($this->aliases[ucfirst($name)])) {
$name = $this->aliases[ucfirst($name)];
}
$params = isset($args[0]) ? $args[0] : [];
if (!empty($isAsync)) {
return $this->executeAsync(
$this->getCommand($name, $params)
);
}
return $this->execute($this->getCommand($name, $params));
}
/**
* @param string $name
* @param array $args
*
* @return CommandInterface
*/
abstract public function getCommand($name, array $args = []);
/**
* @return Service
*/
abstract public function getApi();
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Aws;
/**
* Represents a simple cache interface.
*/
interface CacheInterface
{
/**
* Get a cache item by key.
*
* @param string $key Key to retrieve.
*
* @return mixed|null Returns the value or null if not found.
*/
public function get($key);
/**
* Set a cache key value.
*
* @param string $key Key to set
* @param mixed $value Value to set.
* @param int $ttl Number of seconds the item is allowed to live. Set
* to 0 to allow an unlimited lifetime.
*/
public function set($key, $value, $ttl = 0);
/**
* Remove a cache key.
*
* @param string $key Key to remove.
*/
public function remove($key);
}

View File

@@ -0,0 +1,83 @@
<?php
namespace Aws\CleanRooms;
use Aws\AwsClient;
/**
* This client is used to interact with the **AWS Clean Rooms Service** service.
* @method \Aws\Result batchGetSchema(array $args = [])
* @method \GuzzleHttp\Promise\Promise batchGetSchemaAsync(array $args = [])
* @method \Aws\Result createCollaboration(array $args = [])
* @method \GuzzleHttp\Promise\Promise createCollaborationAsync(array $args = [])
* @method \Aws\Result createConfiguredTable(array $args = [])
* @method \GuzzleHttp\Promise\Promise createConfiguredTableAsync(array $args = [])
* @method \Aws\Result createConfiguredTableAnalysisRule(array $args = [])
* @method \GuzzleHttp\Promise\Promise createConfiguredTableAnalysisRuleAsync(array $args = [])
* @method \Aws\Result createConfiguredTableAssociation(array $args = [])
* @method \GuzzleHttp\Promise\Promise createConfiguredTableAssociationAsync(array $args = [])
* @method \Aws\Result createMembership(array $args = [])
* @method \GuzzleHttp\Promise\Promise createMembershipAsync(array $args = [])
* @method \Aws\Result deleteCollaboration(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteCollaborationAsync(array $args = [])
* @method \Aws\Result deleteConfiguredTable(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteConfiguredTableAsync(array $args = [])
* @method \Aws\Result deleteConfiguredTableAnalysisRule(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteConfiguredTableAnalysisRuleAsync(array $args = [])
* @method \Aws\Result deleteConfiguredTableAssociation(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteConfiguredTableAssociationAsync(array $args = [])
* @method \Aws\Result deleteMember(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteMemberAsync(array $args = [])
* @method \Aws\Result deleteMembership(array $args = [])
* @method \GuzzleHttp\Promise\Promise deleteMembershipAsync(array $args = [])
* @method \Aws\Result getCollaboration(array $args = [])
* @method \GuzzleHttp\Promise\Promise getCollaborationAsync(array $args = [])
* @method \Aws\Result getConfiguredTable(array $args = [])
* @method \GuzzleHttp\Promise\Promise getConfiguredTableAsync(array $args = [])
* @method \Aws\Result getConfiguredTableAnalysisRule(array $args = [])
* @method \GuzzleHttp\Promise\Promise getConfiguredTableAnalysisRuleAsync(array $args = [])
* @method \Aws\Result getConfiguredTableAssociation(array $args = [])
* @method \GuzzleHttp\Promise\Promise getConfiguredTableAssociationAsync(array $args = [])
* @method \Aws\Result getMembership(array $args = [])
* @method \GuzzleHttp\Promise\Promise getMembershipAsync(array $args = [])
* @method \Aws\Result getProtectedQuery(array $args = [])
* @method \GuzzleHttp\Promise\Promise getProtectedQueryAsync(array $args = [])
* @method \Aws\Result getSchema(array $args = [])
* @method \GuzzleHttp\Promise\Promise getSchemaAsync(array $args = [])
* @method \Aws\Result getSchemaAnalysisRule(array $args = [])
* @method \GuzzleHttp\Promise\Promise getSchemaAnalysisRuleAsync(array $args = [])
* @method \Aws\Result listCollaborations(array $args = [])
* @method \GuzzleHttp\Promise\Promise listCollaborationsAsync(array $args = [])
* @method \Aws\Result listConfiguredTableAssociations(array $args = [])
* @method \GuzzleHttp\Promise\Promise listConfiguredTableAssociationsAsync(array $args = [])
* @method \Aws\Result listConfiguredTables(array $args = [])
* @method \GuzzleHttp\Promise\Promise listConfiguredTablesAsync(array $args = [])
* @method \Aws\Result listMembers(array $args = [])
* @method \GuzzleHttp\Promise\Promise listMembersAsync(array $args = [])
* @method \Aws\Result listMemberships(array $args = [])
* @method \GuzzleHttp\Promise\Promise listMembershipsAsync(array $args = [])
* @method \Aws\Result listProtectedQueries(array $args = [])
* @method \GuzzleHttp\Promise\Promise listProtectedQueriesAsync(array $args = [])
* @method \Aws\Result listSchemas(array $args = [])
* @method \GuzzleHttp\Promise\Promise listSchemasAsync(array $args = [])
* @method \Aws\Result listTagsForResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise listTagsForResourceAsync(array $args = [])
* @method \Aws\Result startProtectedQuery(array $args = [])
* @method \GuzzleHttp\Promise\Promise startProtectedQueryAsync(array $args = [])
* @method \Aws\Result tagResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise tagResourceAsync(array $args = [])
* @method \Aws\Result untagResource(array $args = [])
* @method \GuzzleHttp\Promise\Promise untagResourceAsync(array $args = [])
* @method \Aws\Result updateCollaboration(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateCollaborationAsync(array $args = [])
* @method \Aws\Result updateConfiguredTable(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateConfiguredTableAsync(array $args = [])
* @method \Aws\Result updateConfiguredTableAnalysisRule(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateConfiguredTableAnalysisRuleAsync(array $args = [])
* @method \Aws\Result updateConfiguredTableAssociation(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateConfiguredTableAssociationAsync(array $args = [])
* @method \Aws\Result updateMembership(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateMembershipAsync(array $args = [])
* @method \Aws\Result updateProtectedQuery(array $args = [])
* @method \GuzzleHttp\Promise\Promise updateProtectedQueryAsync(array $args = [])
*/
class CleanRoomsClient extends AwsClient {}

View File

@@ -0,0 +1,9 @@
<?php
namespace Aws\CleanRooms\Exception;
use Aws\Exception\AwsException;
/**
* Represents an error interacting with the **AWS Clean Rooms Service** service.
*/
class CleanRoomsException extends AwsException {}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,289 @@
<?php
namespace Aws\ClientSideMonitoring;
use Aws\CommandInterface;
use Aws\Exception\AwsException;
use Aws\MonitoringEventsInterface;
use Aws\ResponseContainerInterface;
use Aws\ResultInterface;
use GuzzleHttp\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @internal
*/
abstract class AbstractMonitoringMiddleware
implements MonitoringMiddlewareInterface
{
private static $socket;
private $nextHandler;
private $options;
protected $credentialProvider;
protected $region;
protected $service;
protected static function getAwsExceptionHeader(AwsException $e, $headerName)
{
$response = $e->getResponse();
if ($response !== null) {
$header = $response->getHeader($headerName);
if (!empty($header[0])) {
return $header[0];
}
}
return null;
}
protected static function getResultHeader(ResultInterface $result, $headerName)
{
if (isset($result['@metadata']['headers'][$headerName])) {
return $result['@metadata']['headers'][$headerName];
}
return null;
}
protected static function getExceptionHeader(\Exception $e, $headerName)
{
if ($e instanceof ResponseContainerInterface) {
$response = $e->getResponse();
if ($response instanceof ResponseInterface) {
$header = $response->getHeader($headerName);
if (!empty($header[0])) {
return $header[0];
}
}
}
return null;
}
/**
* Constructor stores the passed in handler and options.
*
* @param callable $handler
* @param callable $credentialProvider
* @param $options
* @param $region
* @param $service
*/
public function __construct(
callable $handler,
callable $credentialProvider,
$options,
$region,
$service
) {
$this->nextHandler = $handler;
$this->credentialProvider = $credentialProvider;
$this->options = $options;
$this->region = $region;
$this->service = $service;
}
/**
* Standard invoke pattern for middleware execution to be implemented by
* child classes.
*
* @param CommandInterface $cmd
* @param RequestInterface $request
* @return Promise\PromiseInterface
*/
public function __invoke(CommandInterface $cmd, RequestInterface $request)
{
$handler = $this->nextHandler;
$eventData = null;
$enabled = $this->isEnabled();
if ($enabled) {
$cmd['@http']['collect_stats'] = true;
$eventData = $this->populateRequestEventData(
$cmd,
$request,
$this->getNewEvent($cmd, $request)
);
}
$g = function ($value) use ($eventData, $enabled) {
if ($enabled) {
$eventData = $this->populateResultEventData(
$value,
$eventData
);
$this->sendEventData($eventData);
if ($value instanceof MonitoringEventsInterface) {
$value->appendMonitoringEvent($eventData);
}
}
if ($value instanceof \Exception || $value instanceof \Throwable) {
return Promise\Create::rejectionFor($value);
}
return $value;
};
return Promise\Create::promiseFor($handler($cmd, $request))->then($g, $g);
}
private function getClientId()
{
return $this->unwrappedOptions()->getClientId();
}
private function getNewEvent(
CommandInterface $cmd,
RequestInterface $request
) {
$event = [
'Api' => $cmd->getName(),
'ClientId' => $this->getClientId(),
'Region' => $this->getRegion(),
'Service' => $this->getService(),
'Timestamp' => (int) floor(microtime(true) * 1000),
'UserAgent' => substr(
$request->getHeaderLine('User-Agent') . ' ' . \Aws\default_user_agent(),
0,
256
),
'Version' => 1
];
return $event;
}
private function getHost()
{
return $this->unwrappedOptions()->getHost();
}
private function getPort()
{
return $this->unwrappedOptions()->getPort();
}
private function getRegion()
{
return $this->region;
}
private function getService()
{
return $this->service;
}
/**
* Returns enabled flag from options, unwrapping options if necessary.
*
* @return bool
*/
private function isEnabled()
{
return $this->unwrappedOptions()->isEnabled();
}
/**
* Returns $eventData array with information from the request and command.
*
* @param CommandInterface $cmd
* @param RequestInterface $request
* @param array $event
* @return array
*/
protected function populateRequestEventData(
CommandInterface $cmd,
RequestInterface $request,
array $event
) {
$dataFormat = static::getRequestData($request);
foreach ($dataFormat as $eventKey => $value) {
if ($value !== null) {
$event[$eventKey] = $value;
}
}
return $event;
}
/**
* Returns $eventData array with information from the response, including
* the calculation for attempt latency.
*
* @param ResultInterface|\Exception $result
* @param array $event
* @return array
*/
protected function populateResultEventData(
$result,
array $event
) {
$dataFormat = static::getResponseData($result);
foreach ($dataFormat as $eventKey => $value) {
if ($value !== null) {
$event[$eventKey] = $value;
}
}
return $event;
}
/**
* Creates a UDP socket resource and stores it with the class, or retrieves
* it if already instantiated and connected. Handles error-checking and
* re-connecting if necessary. If $forceNewConnection is set to true, a new
* socket will be created.
*
* @param bool $forceNewConnection
* @return Resource
*/
private function prepareSocket($forceNewConnection = false)
{
if (!is_resource(self::$socket)
|| $forceNewConnection
|| socket_last_error(self::$socket)
) {
self::$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
socket_clear_error(self::$socket);
socket_connect(self::$socket, $this->getHost(), $this->getPort());
}
return self::$socket;
}
/**
* Sends formatted monitoring event data via the UDP socket connection to
* the CSM agent endpoint.
*
* @param array $eventData
* @return int
*/
private function sendEventData(array $eventData)
{
$socket = $this->prepareSocket();
$datagram = json_encode($eventData);
$result = socket_write($socket, $datagram, strlen($datagram));
if ($result === false) {
$this->prepareSocket(true);
}
return $result;
}
/**
* Unwraps options, if needed, and returns them.
*
* @return ConfigurationInterface
*/
private function unwrappedOptions()
{
if (!($this->options instanceof ConfigurationInterface)) {
try {
$this->options = ConfigurationProvider::unwrap($this->options);
} catch (\Exception $e) {
// Errors unwrapping CSM config defaults to disabling it
$this->options = new Configuration(
false,
ConfigurationProvider::DEFAULT_HOST,
ConfigurationProvider::DEFAULT_PORT
);
}
}
return $this->options;
}
}

View File

@@ -0,0 +1,262 @@
<?php
namespace Aws\ClientSideMonitoring;
use Aws\CommandInterface;
use Aws\Credentials\CredentialsInterface;
use Aws\Exception\AwsException;
use Aws\ResponseContainerInterface;
use Aws\ResultInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* @internal
*/
class ApiCallAttemptMonitoringMiddleware extends AbstractMonitoringMiddleware
{
/**
* Standard middleware wrapper function with CSM options passed in.
*
* @param callable $credentialProvider
* @param mixed $options
* @param string $region
* @param string $service
* @return callable
*/
public static function wrap(
callable $credentialProvider,
$options,
$region,
$service
) {
return function (callable $handler) use (
$credentialProvider,
$options,
$region,
$service
) {
return new static(
$handler,
$credentialProvider,
$options,
$region,
$service
);
};
}
/**
* {@inheritdoc}
*/
public static function getRequestData(RequestInterface $request)
{
return [
'Fqdn' => $request->getUri()->getHost(),
];
}
/**
* {@inheritdoc}
*/
public static function getResponseData($klass)
{
if ($klass instanceof ResultInterface) {
return [
'AttemptLatency' => self::getResultAttemptLatency($klass),
'DestinationIp' => self::getResultDestinationIp($klass),
'DnsLatency' => self::getResultDnsLatency($klass),
'HttpStatusCode' => self::getResultHttpStatusCode($klass),
'XAmzId2' => self::getResultHeader($klass, 'x-amz-id-2'),
'XAmzRequestId' => self::getResultHeader($klass, 'x-amz-request-id'),
'XAmznRequestId' => self::getResultHeader($klass, 'x-amzn-RequestId'),
];
}
if ($klass instanceof AwsException) {
return [
'AttemptLatency' => self::getAwsExceptionAttemptLatency($klass),
'AwsException' => substr(
self::getAwsExceptionErrorCode($klass),
0,
128
),
'AwsExceptionMessage' => substr(
self::getAwsExceptionMessage($klass),
0,
512
),
'DestinationIp' => self::getAwsExceptionDestinationIp($klass),
'DnsLatency' => self::getAwsExceptionDnsLatency($klass),
'HttpStatusCode' => self::getAwsExceptionHttpStatusCode($klass),
'XAmzId2' => self::getAwsExceptionHeader($klass, 'x-amz-id-2'),
'XAmzRequestId' => self::getAwsExceptionHeader(
$klass,
'x-amz-request-id'
),
'XAmznRequestId' => self::getAwsExceptionHeader(
$klass,
'x-amzn-RequestId'
),
];
}
if ($klass instanceof \Exception) {
return [
'HttpStatusCode' => self::getExceptionHttpStatusCode($klass),
'SdkException' => substr(
self::getExceptionCode($klass),
0,
128
),
'SdkExceptionMessage' => substr(
self::getExceptionMessage($klass),
0,
512
),
'XAmzId2' => self::getExceptionHeader($klass, 'x-amz-id-2'),
'XAmzRequestId' => self::getExceptionHeader($klass, 'x-amz-request-id'),
'XAmznRequestId' => self::getExceptionHeader($klass, 'x-amzn-RequestId'),
];
}
throw new \InvalidArgumentException('Parameter must be an instance of ResultInterface, AwsException or Exception.');
}
private static function getResultAttemptLatency(ResultInterface $result)
{
if (isset($result['@metadata']['transferStats']['http'])) {
$attempt = end($result['@metadata']['transferStats']['http']);
if (isset($attempt['total_time'])) {
return (int) floor($attempt['total_time'] * 1000);
}
}
return null;
}
private static function getResultDestinationIp(ResultInterface $result)
{
if (isset($result['@metadata']['transferStats']['http'])) {
$attempt = end($result['@metadata']['transferStats']['http']);
if (isset($attempt['primary_ip'])) {
return $attempt['primary_ip'];
}
}
return null;
}
private static function getResultDnsLatency(ResultInterface $result)
{
if (isset($result['@metadata']['transferStats']['http'])) {
$attempt = end($result['@metadata']['transferStats']['http']);
if (isset($attempt['namelookup_time'])) {
return (int) floor($attempt['namelookup_time'] * 1000);
}
}
return null;
}
private static function getResultHttpStatusCode(ResultInterface $result)
{
return $result['@metadata']['statusCode'];
}
private static function getAwsExceptionAttemptLatency(AwsException $e) {
$attempt = $e->getTransferInfo();
if (isset($attempt['total_time'])) {
return (int) floor($attempt['total_time'] * 1000);
}
return null;
}
private static function getAwsExceptionErrorCode(AwsException $e) {
return $e->getAwsErrorCode();
}
private static function getAwsExceptionMessage(AwsException $e) {
return $e->getAwsErrorMessage();
}
private static function getAwsExceptionDestinationIp(AwsException $e) {
$attempt = $e->getTransferInfo();
if (isset($attempt['primary_ip'])) {
return $attempt['primary_ip'];
}
return null;
}
private static function getAwsExceptionDnsLatency(AwsException $e) {
$attempt = $e->getTransferInfo();
if (isset($attempt['namelookup_time'])) {
return (int) floor($attempt['namelookup_time'] * 1000);
}
return null;
}
private static function getAwsExceptionHttpStatusCode(AwsException $e) {
$response = $e->getResponse();
if ($response !== null) {
return $response->getStatusCode();
}
return null;
}
private static function getExceptionHttpStatusCode(\Exception $e) {
if ($e instanceof ResponseContainerInterface) {
$response = $e->getResponse();
if ($response instanceof ResponseInterface) {
return $response->getStatusCode();
}
}
return null;
}
private static function getExceptionCode(\Exception $e) {
if (!($e instanceof AwsException)) {
return get_class($e);
}
return null;
}
private static function getExceptionMessage(\Exception $e) {
if (!($e instanceof AwsException)) {
return $e->getMessage();
}
return null;
}
/**
* {@inheritdoc}
*/
protected function populateRequestEventData(
CommandInterface $cmd,
RequestInterface $request,
array $event
) {
$event = parent::populateRequestEventData($cmd, $request, $event);
$event['Type'] = 'ApiCallAttempt';
return $event;
}
/**
* {@inheritdoc}
*/
protected function populateResultEventData(
$result,
array $event
) {
$event = parent::populateResultEventData($result, $event);
$provider = $this->credentialProvider;
/** @var CredentialsInterface $credentials */
$credentials = $provider()->wait();
$event['AccessKey'] = $credentials->getAccessKeyId();
$sessionToken = $credentials->getSecurityToken();
if ($sessionToken !== null) {
$event['SessionToken'] = $sessionToken;
}
if (empty($event['AttemptLatency'])) {
$event['AttemptLatency'] = (int) (floor(microtime(true) * 1000) - $event['Timestamp']);
}
return $event;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Aws\ClientSideMonitoring;
use Aws\CommandInterface;
use Aws\Exception\AwsException;
use Aws\MonitoringEventsInterface;
use Aws\ResultInterface;
use Psr\Http\Message\RequestInterface;
/**
* @internal
*/
class ApiCallMonitoringMiddleware extends AbstractMonitoringMiddleware
{
/**
* Api Call Attempt event keys for each Api Call event key
*
* @var array
*/
private static $eventKeys = [
'FinalAwsException' => 'AwsException',
'FinalAwsExceptionMessage' => 'AwsExceptionMessage',
'FinalSdkException' => 'SdkException',
'FinalSdkExceptionMessage' => 'SdkExceptionMessage',
'FinalHttpStatusCode' => 'HttpStatusCode',
];
/**
* Standard middleware wrapper function with CSM options passed in.
*
* @param callable $credentialProvider
* @param mixed $options
* @param string $region
* @param string $service
* @return callable
*/
public static function wrap(
callable $credentialProvider,
$options,
$region,
$service
) {
return function (callable $handler) use (
$credentialProvider,
$options,
$region,
$service
) {
return new static(
$handler,
$credentialProvider,
$options,
$region,
$service
);
};
}
/**
* {@inheritdoc}
*/
public static function getRequestData(RequestInterface $request)
{
return [];
}
/**
* {@inheritdoc}
*/
public static function getResponseData($klass)
{
if ($klass instanceof ResultInterface) {
$data = [
'AttemptCount' => self::getResultAttemptCount($klass),
'MaxRetriesExceeded' => 0,
];
} elseif ($klass instanceof \Exception) {
$data = [
'AttemptCount' => self::getExceptionAttemptCount($klass),
'MaxRetriesExceeded' => self::getMaxRetriesExceeded($klass),
];
} else {
throw new \InvalidArgumentException('Parameter must be an instance of ResultInterface or Exception.');
}
return $data + self::getFinalAttemptData($klass);
}
private static function getResultAttemptCount(ResultInterface $result) {
if (isset($result['@metadata']['transferStats']['http'])) {
return count($result['@metadata']['transferStats']['http']);
}
return 1;
}
private static function getExceptionAttemptCount(\Exception $e) {
$attemptCount = 0;
if ($e instanceof MonitoringEventsInterface) {
foreach ($e->getMonitoringEvents() as $event) {
if (isset($event['Type']) &&
$event['Type'] === 'ApiCallAttempt') {
$attemptCount++;
}
}
}
return $attemptCount;
}
private static function getFinalAttemptData($klass)
{
$data = [];
if ($klass instanceof MonitoringEventsInterface) {
$finalAttempt = self::getFinalAttempt($klass->getMonitoringEvents());
if (!empty($finalAttempt)) {
foreach (self::$eventKeys as $callKey => $attemptKey) {
if (isset($finalAttempt[$attemptKey])) {
$data[$callKey] = $finalAttempt[$attemptKey];
}
}
}
}
return $data;
}
private static function getFinalAttempt(array $events)
{
for (end($events); key($events) !== null; prev($events)) {
$current = current($events);
if (isset($current['Type'])
&& $current['Type'] === 'ApiCallAttempt'
) {
return $current;
}
}
return null;
}
private static function getMaxRetriesExceeded($klass)
{
if ($klass instanceof AwsException && $klass->isMaxRetriesExceeded()) {
return 1;
}
return 0;
}
/**
* {@inheritdoc}
*/
protected function populateRequestEventData(
CommandInterface $cmd,
RequestInterface $request,
array $event
) {
$event = parent::populateRequestEventData($cmd, $request, $event);
$event['Type'] = 'ApiCall';
return $event;
}
/**
* {@inheritdoc}
*/
protected function populateResultEventData(
$result,
array $event
) {
$event = parent::populateResultEventData($result, $event);
$event['Latency'] = (int) (floor(microtime(true) * 1000) - $event['Timestamp']);
return $event;
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Aws\ClientSideMonitoring;
class Configuration implements ConfigurationInterface
{
private $clientId;
private $enabled;
private $host;
private $port;
/**
* Constructs a new Configuration object with the specified CSM options set.
*
* @param mixed $enabled
* @param string $host
* @param string|int $port
* @param string $clientId
*/
public function __construct($enabled, $host, $port, $clientId = '')
{
$this->host = $host;
$this->port = filter_var($port, FILTER_VALIDATE_INT);
if ($this->port === false) {
throw new \InvalidArgumentException(
"CSM 'port' value must be an integer!");
}
// Unparsable $enabled flag errors on the side of disabling CSM
$this->enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN);
$this->clientId = trim($clientId);
}
/**
* {@inheritdoc}
*/
public function isEnabled()
{
return $this->enabled;
}
/**
* {@inheritdoc}
*/
public function getClientId()
{
return $this->clientId;
}
/**
* /{@inheritdoc}
*/
public function getHost()
{
return $this->host;
}
/**
* {@inheritdoc}
*/
public function getPort()
{
return $this->port;
}
/**
* {@inheritdoc}
*/
public function toArray()
{
return [
'client_id' => $this->getClientId(),
'enabled' => $this->isEnabled(),
'host' => $this->getHost(),
'port' => $this->getPort()
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Aws\ClientSideMonitoring;
/**
* Provides access to client-side monitoring configuration options:
* 'client_id', 'enabled', 'host', 'port'
*/
interface ConfigurationInterface
{
/**
* Checks whether or not client-side monitoring is enabled.
*
* @return bool
*/
public function isEnabled();
/**
* Returns the Client ID, if available.
*
* @return string|null
*/
public function getClientId();
/**
* Returns the configured host.
*
* @return string|null
*/
public function getHost();
/**
* Returns the configured port.
*
* @return int|null
*/
public function getPort();
/**
* Returns the configuration as an associative array.
*
* @return array
*/
public function toArray();
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Aws\ClientSideMonitoring;
use Aws\AbstractConfigurationProvider;
use Aws\CacheInterface;
use Aws\ClientSideMonitoring\Exception\ConfigurationException;
use Aws\ConfigurationProviderInterface;
use GuzzleHttp\Promise;
use GuzzleHttp\Promise\PromiseInterface;
/**
* A configuration provider is a function that accepts no arguments and returns
* a promise that is fulfilled with a {@see \Aws\ClientSideMonitoring\ConfigurationInterface}
* or rejected with an {@see \Aws\ClientSideMonitoring\Exception\ConfigurationException}.
*
* <code>
* use Aws\ClientSideMonitoring\ConfigurationProvider;
* $provider = ConfigurationProvider::defaultProvider();
* // Returns a ConfigurationInterface or throws.
* $config = $provider()->wait();
* </code>
*
* Configuration providers can be composed to create configuration using
* conditional logic that can create different configurations in different
* environments. You can compose multiple providers into a single provider using
* {@see Aws\ClientSideMonitoring\ConfigurationProvider::chain}. This function
* accepts providers as variadic arguments and returns a new function that will
* invoke each provider until a successful configuration is returned.
*
* <code>
* // First try an INI file at this location.
* $a = ConfigurationProvider::ini(null, '/path/to/file.ini');
* // Then try an INI file at this location.
* $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini');
* // Then try loading from environment variables.
* $c = ConfigurationProvider::env();
* // Combine the three providers together.
* $composed = ConfigurationProvider::chain($a, $b, $c);
* // Returns a promise that is fulfilled with a configuration or throws.
* $promise = $composed();
* // Wait on the configuration to resolve.
* $config = $promise->wait();
* </code>
*/
class ConfigurationProvider extends AbstractConfigurationProvider
implements ConfigurationProviderInterface
{
const DEFAULT_CLIENT_ID = '';
const DEFAULT_ENABLED = false;
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 31000;
const ENV_CLIENT_ID = 'AWS_CSM_CLIENT_ID';
const ENV_ENABLED = 'AWS_CSM_ENABLED';
const ENV_HOST = 'AWS_CSM_HOST';
const ENV_PORT = 'AWS_CSM_PORT';
const ENV_PROFILE = 'AWS_PROFILE';
public static $cacheKey = 'aws_cached_csm_config';
protected static $interfaceClass = ConfigurationInterface::class;
protected static $exceptionClass = ConfigurationException::class;
/**
* Create a default config provider that first checks for environment
* variables, then checks for a specified profile in the environment-defined
* config file location (env variable is 'AWS_CONFIG_FILE', file location
* defaults to ~/.aws/config), then checks for the "default" profile in the
* environment-defined config file location, and failing those uses a default
* fallback set of configuration options.
*
* This provider is automatically wrapped in a memoize function that caches
* previously provided config options.
*
* @param array $config
*
* @return callable
*/
public static function defaultProvider(array $config = [])
{
$configProviders = [self::env()];
if (
!isset($config['use_aws_shared_config_files'])
|| $config['use_aws_shared_config_files'] != false
) {
$configProviders[] = self::ini();
}
$configProviders[] = self::fallback();
$memo = self::memoize(
call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders)
);
if (isset($config['csm']) && $config['csm'] instanceof CacheInterface) {
return self::cache($memo, $config['csm'], self::$cacheKey);
}
return $memo;
}
/**
* Provider that creates CSM config from environment variables.
*
* @return callable
*/
public static function env()
{
return function () {
// Use credentials from environment variables, if available
$enabled = getenv(self::ENV_ENABLED);
if ($enabled !== false) {
return Promise\Create::promiseFor(
new Configuration(
$enabled,
getenv(self::ENV_HOST) ?: self::DEFAULT_HOST,
getenv(self::ENV_PORT) ?: self::DEFAULT_PORT,
getenv(self:: ENV_CLIENT_ID) ?: self::DEFAULT_CLIENT_ID
)
);
}
return self::reject('Could not find environment variable CSM config'
. ' in ' . self::ENV_ENABLED. '/' . self::ENV_HOST . '/'
. self::ENV_PORT . '/' . self::ENV_CLIENT_ID);
};
}
/**
* Fallback config options when other sources are not set.
*
* @return callable
*/
public static function fallback()
{
return function() {
return Promise\Create::promiseFor(
new Configuration(
self::DEFAULT_ENABLED,
self::DEFAULT_HOST,
self::DEFAULT_PORT,
self::DEFAULT_CLIENT_ID
)
);
};
}
/**
* Config provider that creates config using a config file whose location
* is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to
* ~/.aws/config if not specified
*
* @param string|null $profile Profile to use. If not specified will use
* the "default" profile.
* @param string|null $filename If provided, uses a custom filename rather
* than looking in the default directory.
*
* @return callable
*/
public static function ini($profile = null, $filename = null)
{
$filename = $filename ?: (self::getDefaultConfigFilename());
$profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'aws_csm');
return function () use ($profile, $filename) {
if (!@is_readable($filename)) {
return self::reject("Cannot read CSM config from $filename");
}
$data = \Aws\parse_ini_file($filename, true);
if ($data === false) {
return self::reject("Invalid config file: $filename");
}
if (!isset($data[$profile])) {
return self::reject("'$profile' not found in config file");
}
if (!isset($data[$profile]['csm_enabled'])) {
return self::reject("Required CSM config values not present in
INI profile '{$profile}' ({$filename})");
}
// host is optional
if (empty($data[$profile]['csm_host'])) {
$data[$profile]['csm_host'] = self::DEFAULT_HOST;
}
// port is optional
if (empty($data[$profile]['csm_port'])) {
$data[$profile]['csm_port'] = self::DEFAULT_PORT;
}
// client_id is optional
if (empty($data[$profile]['csm_client_id'])) {
$data[$profile]['csm_client_id'] = self::DEFAULT_CLIENT_ID;
}
return Promise\Create::promiseFor(
new Configuration(
$data[$profile]['csm_enabled'],
$data[$profile]['csm_host'],
$data[$profile]['csm_port'],
$data[$profile]['csm_client_id']
)
);
};
}
/**
* Unwraps a configuration object in whatever valid form it is in,
* always returning a ConfigurationInterface object.
*
* @param mixed $config
* @return ConfigurationInterface
* @throws \InvalidArgumentException
*/
public static function unwrap($config)
{
if (is_callable($config)) {
$config = $config();
}
if ($config instanceof PromiseInterface) {
$config = $config->wait();
}
if ($config instanceof ConfigurationInterface) {
return $config;
} elseif (is_array($config) && isset($config['enabled'])) {
$client_id = isset($config['client_id']) ? $config['client_id']
: self::DEFAULT_CLIENT_ID;
$host = isset($config['host']) ? $config['host']
: self::DEFAULT_HOST;
$port = isset($config['port']) ? $config['port']
: self::DEFAULT_PORT;
return new Configuration($config['enabled'], $host, $port, $client_id);
}
throw new \InvalidArgumentException('Not a valid CSM configuration '
. 'argument.');
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Aws\ClientSideMonitoring\Exception;
use Aws\HasMonitoringEventsTrait;
use Aws\MonitoringEventsInterface;
/**
* Represents an error interacting with configuration for client-side monitoring.
*/
class ConfigurationException extends \RuntimeException implements
MonitoringEventsInterface
{
use HasMonitoringEventsTrait;
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Aws\ClientSideMonitoring;
use Aws\CommandInterface;
use Aws\Exception\AwsException;
use Aws\ResultInterface;
use GuzzleHttp\Psr7\Request;
use Psr\Http\Message\RequestInterface;
/**
* @internal
*/
interface MonitoringMiddlewareInterface
{
/**
* Data for event properties to be sent to the monitoring agent.
*
* @param RequestInterface $request
* @return array
*/
public static function getRequestData(RequestInterface $request);
/**
* Data for event properties to be sent to the monitoring agent.
*
* @param ResultInterface|AwsException|\Exception $klass
* @return array
*/
public static function getResponseData($klass);
public function __invoke(CommandInterface $cmd, RequestInterface $request);
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Aws;
/**
* AWS command object.
*/
class Command implements CommandInterface
{
use HasDataTrait;
/** @var string */
private $name;
/** @var HandlerList */
private $handlerList;
/** @var Array */
private $authSchemes;
/**
* Accepts an associative array of command options, including:
*
* - @http: (array) Associative array of transfer options.
*
* @param string $name Name of the command
* @param array $args Arguments to pass to the command
* @param HandlerList $list Handler list
*/
public function __construct($name, array $args = [], HandlerList $list = null)
{
$this->name = $name;
$this->data = $args;
$this->handlerList = $list ?: new HandlerList();
if (!isset($this->data['@http'])) {
$this->data['@http'] = [];
}
if (!isset($this->data['@context'])) {
$this->data['@context'] = [];
}
}
public function __clone()
{
$this->handlerList = clone $this->handlerList;
}
public function getName()
{
return $this->name;
}
public function hasParam($name)
{
return array_key_exists($name, $this->data);
}
public function getHandlerList()
{
return $this->handlerList;
}
/**
* For overriding auth schemes on a per endpoint basis when using
* EndpointV2 provider. Intended for internal use only.
*
* @param array $authSchemes
*
* @internal
*/
public function setAuthSchemes(array $authSchemes)
{
$this->authSchemes = $authSchemes;
}
/**
* Get auth schemes added to command as required
* for endpoint resolution
*
* @returns array | null
*/
public function getAuthSchemes()
{
return $this->authSchemes;
}
/** @deprecated */
public function get($name)
{
return $this[$name];
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Aws;
/**
* A command object encapsulates the input parameters used to control the
* creation of a HTTP request and processing of a HTTP response.
*
* Using the toArray() method will return the input parameters of the command
* as an associative array.
*/
interface CommandInterface extends \ArrayAccess, \Countable, \IteratorAggregate
{
/**
* Converts the command parameters to an array
*
* @return array
*/
public function toArray();
/**
* Get the name of the command
*
* @return string
*/
public function getName();
/**
* Check if the command has a parameter by name.
*
* @param string $name Name of the parameter to check
*
* @return bool
*/
public function hasParam($name);
/**
* Get the handler list used to transfer the command.
*
* @return HandlerList
*/
public function getHandlerList();
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Aws;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\PromisorInterface;
use GuzzleHttp\Promise\EachPromise;
/**
* Sends and iterator of commands concurrently using a capped pool size.
*
* The pool will read command objects from an iterator until it is cancelled or
* until the iterator is consumed.
*/
class CommandPool implements PromisorInterface
{
/** @var EachPromise */
private $each;
/**
* The CommandPool constructor accepts a hash of configuration options:
*
* - concurrency: (callable|int) Maximum number of commands to execute
* concurrently. Provide a function to resize the pool dynamically. The
* function will be provided the current number of pending requests and
* is expected to return an integer representing the new pool size limit.
* - before: (callable) function to invoke before sending each command. The
* before function accepts the command and the key of the iterator of the
* command. You can mutate the command as needed in the before function
* before sending the command.
* - fulfilled: (callable) Function to invoke when a promise is fulfilled.
* The function is provided the result object, id of the iterator that the
* result came from, and the aggregate promise that can be resolved/rejected
* if you need to short-circuit the pool.
* - rejected: (callable) Function to invoke when a promise is rejected.
* The function is provided an AwsException object, id of the iterator that
* the exception came from, and the aggregate promise that can be
* resolved/rejected if you need to short-circuit the pool.
* - preserve_iterator_keys: (bool) Retain the iterator key when generating
* the commands.
*
* @param AwsClientInterface $client Client used to execute commands.
* @param array|\Iterator $commands Iterable that yields commands.
* @param array $config Associative array of options.
*/
public function __construct(
AwsClientInterface $client,
$commands,
array $config = []
) {
if (!isset($config['concurrency'])) {
$config['concurrency'] = 25;
}
$before = $this->getBefore($config);
$mapFn = function ($commands) use ($client, $before, $config) {
foreach ($commands as $key => $command) {
if (!($command instanceof CommandInterface)) {
throw new \InvalidArgumentException('Each value yielded by '
. 'the iterator must be an Aws\CommandInterface.');
}
if ($before) {
$before($command, $key);
}
if (!empty($config['preserve_iterator_keys'])) {
yield $key => $client->executeAsync($command);
} else {
yield $client->executeAsync($command);
}
}
};
$this->each = new EachPromise($mapFn($commands), $config);
}
/**
* @return PromiseInterface
*/
public function promise()
{
return $this->each->promise();
}
/**
* Executes a pool synchronously and aggregates the results of the pool
* into an indexed array in the same order as the passed in array.
*
* @param AwsClientInterface $client Client used to execute commands.
* @param mixed $commands Iterable that yields commands.
* @param array $config Configuration options.
*
* @return array
* @see \Aws\CommandPool::__construct for available configuration options.
*/
public static function batch(
AwsClientInterface $client,
$commands,
array $config = []
) {
$results = [];
self::cmpCallback($config, 'fulfilled', $results);
self::cmpCallback($config, 'rejected', $results);
return (new self($client, $commands, $config))
->promise()
->then(static function () use (&$results) {
ksort($results);
return $results;
})
->wait();
}
/**
* @return callable
*/
private function getBefore(array $config)
{
if (!isset($config['before'])) {
return null;
}
if (is_callable($config['before'])) {
return $config['before'];
}
throw new \InvalidArgumentException('before must be callable');
}
/**
* Adds an onFulfilled or onRejected callback that aggregates results into
* an array. If a callback is already present, it is replaced with the
* composed function.
*
* @param array $config
* @param $name
* @param array $results
*/
private static function cmpCallback(array &$config, $name, array &$results)
{
if (!isset($config[$name])) {
$config[$name] = function ($v, $k) use (&$results) {
$results[$k] = $v;
};
} else {
$currentFn = $config[$name];
$config[$name] = function ($v, $k) use (&$results, $currentFn) {
$currentFn($v, $k);
$results[$k] = $v;
};
}
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Aws;
interface ConfigurationProviderInterface
{
/**
* Create a default config provider
*
* @param array $config
* @return callable
*/
public static function defaultProvider(array $config = []);
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Aws\Credentials;
use Aws\Exception\CredentialsException;
use Aws\Result;
use Aws\Sts\StsClient;
use GuzzleHttp\Promise\PromiseInterface;
/**
* Credential provider that provides credentials via assuming a role
* More Information, see: http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sts-2011-06-15.html#assumerole
*/
class AssumeRoleCredentialProvider
{
const ERROR_MSG = "Missing required 'AssumeRoleCredentialProvider' configuration option: ";
/** @var StsClient */
private $client;
/** @var array */
private $assumeRoleParams;
/**
* The constructor requires following configure parameters:
* - client: a StsClient
* - assume_role_params: Parameters used to make assumeRole call
*
* @param array $config Configuration options
* @throws \InvalidArgumentException
*/
public function __construct(array $config = [])
{
if (!isset($config['assume_role_params'])) {
throw new \InvalidArgumentException(self::ERROR_MSG . "'assume_role_params'.");
}
if (!isset($config['client'])) {
throw new \InvalidArgumentException(self::ERROR_MSG . "'client'.");
}
$this->client = $config['client'];
$this->assumeRoleParams = $config['assume_role_params'];
}
/**
* Loads assume role credentials.
*
* @return PromiseInterface
*/
public function __invoke()
{
$client = $this->client;
return $client->assumeRoleAsync($this->assumeRoleParams)
->then(function (Result $result) {
return $this->client->createCredentials($result);
})->otherwise(function (\RuntimeException $exception) {
throw new CredentialsException(
"Error in retrieving assume role credentials.",
0,
$exception
);
});
}
}

View File

@@ -0,0 +1,166 @@
<?php
namespace Aws\Credentials;
use Aws\Exception\AwsException;
use Aws\Exception\CredentialsException;
use Aws\Result;
use Aws\Sts\StsClient;
use GuzzleHttp\Promise;
/**
* Credential provider that provides credentials via assuming a role with a web identity
* More Information, see: https://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sts-2011-06-15.html#assumerolewithwebidentity
*/
class AssumeRoleWithWebIdentityCredentialProvider
{
const ERROR_MSG = "Missing required 'AssumeRoleWithWebIdentityCredentialProvider' configuration option: ";
const ENV_RETRIES = 'AWS_METADATA_SERVICE_NUM_ATTEMPTS';
/** @var string */
private $tokenFile;
/** @var string */
private $arn;
/** @var string */
private $session;
/** @var StsClient */
private $client;
/** @var integer */
private $retries;
/** @var integer */
private $authenticationAttempts;
/** @var integer */
private $tokenFileReadAttempts;
/**
* The constructor attempts to load config from environment variables.
* If not set, the following config options are used:
* - WebIdentityTokenFile: full path of token filename
* - RoleArn: arn of role to be assumed
* - SessionName: (optional) set by SDK if not provided
*
* @param array $config Configuration options
* @throws \InvalidArgumentException
*/
public function __construct(array $config = [])
{
if (!isset($config['RoleArn'])) {
throw new \InvalidArgumentException(self::ERROR_MSG . "'RoleArn'.");
}
$this->arn = $config['RoleArn'];
if (!isset($config['WebIdentityTokenFile'])) {
throw new \InvalidArgumentException(self::ERROR_MSG . "'WebIdentityTokenFile'.");
}
$this->tokenFile = $config['WebIdentityTokenFile'];
if (!preg_match("/^\w\:|^\/|^\\\/", $this->tokenFile)) {
throw new \InvalidArgumentException("'WebIdentityTokenFile' must be an absolute path.");
}
$this->retries = (int) getenv(self::ENV_RETRIES) ?: (isset($config['retries']) ? $config['retries'] : 3);
$this->authenticationAttempts = 0;
$this->tokenFileReadAttempts = 0;
$this->session = isset($config['SessionName'])
? $config['SessionName']
: 'aws-sdk-php-' . round(microtime(true) * 1000);
$region = isset($config['region'])
? $config['region']
: 'us-east-1';
if (isset($config['client'])) {
$this->client = $config['client'];
} else {
$this->client = new StsClient([
'credentials' => false,
'region' => $region,
'version' => 'latest'
]);
}
}
/**
* Loads assume role with web identity credentials.
*
* @return Promise\PromiseInterface
*/
public function __invoke()
{
return Promise\Coroutine::of(function () {
$client = $this->client;
$result = null;
while ($result == null) {
try {
$token = @file_get_contents($this->tokenFile);
if (false === $token) {
clearstatcache(true, dirname($this->tokenFile) . "/" . readlink($this->tokenFile));
clearstatcache(true, dirname($this->tokenFile) . "/" . dirname(readlink($this->tokenFile)));
clearstatcache(true, $this->tokenFile);
if (!@is_readable($this->tokenFile)) {
throw new CredentialsException(
"Unreadable tokenfile at location {$this->tokenFile}"
);
}
$token = @file_get_contents($this->tokenFile);
}
if (empty($token)) {
if ($this->tokenFileReadAttempts < $this->retries) {
sleep((int) pow(1.2, $this->tokenFileReadAttempts));
$this->tokenFileReadAttempts++;
continue;
}
throw new CredentialsException("InvalidIdentityToken from file: {$this->tokenFile}");
}
} catch (\Exception $exception) {
throw new CredentialsException(
"Error reading WebIdentityTokenFile from " . $this->tokenFile,
0,
$exception
);
}
$assumeParams = [
'RoleArn' => $this->arn,
'RoleSessionName' => $this->session,
'WebIdentityToken' => $token
];
try {
$result = $client->assumeRoleWithWebIdentity($assumeParams);
} catch (AwsException $e) {
if ($e->getAwsErrorCode() == 'InvalidIdentityToken') {
if ($this->authenticationAttempts < $this->retries) {
sleep((int) pow(1.2, $this->authenticationAttempts));
} else {
throw new CredentialsException(
"InvalidIdentityToken, retries exhausted"
);
}
} else {
throw new CredentialsException(
"Error assuming role from web identity credentials",
0,
$e
);
}
} catch (\Exception $e) {
throw new CredentialsException(
"Error retrieving web identity credentials: " . $e->getMessage()
. " (" . $e->getCode() . ")"
);
}
$this->authenticationAttempts++;
}
yield $this->client->createCredentials($result);
});
}
}

View File

@@ -0,0 +1,991 @@
<?php
namespace Aws\Credentials;
use Aws;
use Aws\Api\DateTimeResult;
use Aws\CacheInterface;
use Aws\Exception\CredentialsException;
use Aws\Sts\StsClient;
use GuzzleHttp\Promise;
/**
* Credential providers are functions that accept no arguments and return a
* promise that is fulfilled with an {@see \Aws\Credentials\CredentialsInterface}
* or rejected with an {@see \Aws\Exception\CredentialsException}.
*
* <code>
* use Aws\Credentials\CredentialProvider;
* $provider = CredentialProvider::defaultProvider();
* // Returns a CredentialsInterface or throws.
* $creds = $provider()->wait();
* </code>
*
* Credential providers can be composed to create credentials using conditional
* logic that can create different credentials in different environments. You
* can compose multiple providers into a single provider using
* {@see Aws\Credentials\CredentialProvider::chain}. This function accepts
* providers as variadic arguments and returns a new function that will invoke
* each provider until a successful set of credentials is returned.
*
* <code>
* // First try an INI file at this location.
* $a = CredentialProvider::ini(null, '/path/to/file.ini');
* // Then try an INI file at this location.
* $b = CredentialProvider::ini(null, '/path/to/other-file.ini');
* // Then try loading from environment variables.
* $c = CredentialProvider::env();
* // Combine the three providers together.
* $composed = CredentialProvider::chain($a, $b, $c);
* // Returns a promise that is fulfilled with credentials or throws.
* $promise = $composed();
* // Wait on the credentials to resolve.
* $creds = $promise->wait();
* </code>
*/
class CredentialProvider
{
const ENV_ARN = 'AWS_ROLE_ARN';
const ENV_KEY = 'AWS_ACCESS_KEY_ID';
const ENV_PROFILE = 'AWS_PROFILE';
const ENV_ROLE_SESSION_NAME = 'AWS_ROLE_SESSION_NAME';
const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY';
const ENV_SESSION = 'AWS_SESSION_TOKEN';
const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE';
const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE';
/**
* Create a default credential provider that
* first checks for environment variables,
* then checks for assumed role via web identity,
* then checks for cached SSO credentials from the CLI,
* then check for credential_process in the "default" profile in ~/.aws/credentials,
* then checks for the "default" profile in ~/.aws/credentials,
* then for credential_process in the "default profile" profile in ~/.aws/config,
* then checks for "profile default" profile in ~/.aws/config (which is
* the default profile of AWS CLI),
* then tries to make a GET Request to fetch credentials if ECS environment variable is presented,
* finally checks for EC2 instance profile credentials.
*
* This provider is automatically wrapped in a memoize function that caches
* previously provided credentials.
*
* @param array $config Optional array of ecs/instance profile credentials
* provider options.
*
* @return callable
*/
public static function defaultProvider(array $config = [])
{
$cacheable = [
'web_identity',
'sso',
'process_credentials',
'process_config',
'ecs',
'instance'
];
$profileName = getenv(self::ENV_PROFILE) ?: 'default';
$defaultChain = [
'env' => self::env(),
'web_identity' => self::assumeRoleWithWebIdentityCredentialProvider($config),
];
if (
!isset($config['use_aws_shared_config_files'])
|| $config['use_aws_shared_config_files'] !== false
) {
$defaultChain['sso'] = self::sso(
$profileName,
self::getHomeDir() . '/.aws/config',
$config
);
$defaultChain['process_credentials'] = self::process();
$defaultChain['ini'] = self::ini();
$defaultChain['process_config'] = self::process(
'profile ' . $profileName,
self::getHomeDir() . '/.aws/config'
);
$defaultChain['ini_config'] = self::ini(
'profile '. $profileName,
self::getHomeDir() . '/.aws/config'
);
}
if (self::shouldUseEcs()) {
$defaultChain['ecs'] = self::ecsCredentials($config);
} else {
$defaultChain['instance'] = self::instanceProfile($config);
}
if (isset($config['credentials'])
&& $config['credentials'] instanceof CacheInterface
) {
foreach ($cacheable as $provider) {
if (isset($defaultChain[$provider])) {
$defaultChain[$provider] = self::cache(
$defaultChain[$provider],
$config['credentials'],
'aws_cached_' . $provider . '_credentials'
);
}
}
}
return self::memoize(
call_user_func_array(
[CredentialProvider::class, 'chain'],
array_values($defaultChain)
)
);
}
/**
* Create a credential provider function from a set of static credentials.
*
* @param CredentialsInterface $creds
*
* @return callable
*/
public static function fromCredentials(CredentialsInterface $creds)
{
$promise = Promise\Create::promiseFor($creds);
return function () use ($promise) {
return $promise;
};
}
/**
* Creates an aggregate credentials provider that invokes the provided
* variadic providers one after the other until a provider returns
* credentials.
*
* @return callable
*/
public static function chain()
{
$links = func_get_args();
if (empty($links)) {
throw new \InvalidArgumentException('No providers in chain');
}
return function ($previousCreds = null) use ($links) {
/** @var callable $parent */
$parent = array_shift($links);
$promise = $parent();
while ($next = array_shift($links)) {
if ($next instanceof InstanceProfileProvider
&& $previousCreds instanceof Credentials
) {
$promise = $promise->otherwise(
function () use ($next, $previousCreds) {return $next($previousCreds);}
);
} else {
$promise = $promise->otherwise($next);
}
}
return $promise;
};
}
/**
* Wraps a credential provider and caches previously provided credentials.
*
* Ensures that cached credentials are refreshed when they expire.
*
* @param callable $provider Credentials provider function to wrap.
*
* @return callable
*/
public static function memoize(callable $provider)
{
return function () use ($provider) {
static $result;
static $isConstant;
// Constant credentials will be returned constantly.
if ($isConstant) {
return $result;
}
// Create the initial promise that will be used as the cached value
// until it expires.
if (null === $result) {
$result = $provider();
}
// Return credentials that could expire and refresh when needed.
return $result
->then(function (CredentialsInterface $creds) use ($provider, &$isConstant, &$result) {
// Determine if these are constant credentials.
if (!$creds->getExpiration()) {
$isConstant = true;
return $creds;
}
// Refresh expired credentials.
if (!$creds->isExpired()) {
return $creds;
}
// Refresh the result and forward the promise.
return $result = $provider($creds);
})
->otherwise(function($reason) use (&$result) {
// Cleanup rejected promise.
$result = null;
return new Promise\RejectedPromise($reason);
});
};
}
/**
* Wraps a credential provider and saves provided credentials in an
* instance of Aws\CacheInterface. Forwards calls when no credentials found
* in cache and updates cache with the results.
*
* @param callable $provider Credentials provider function to wrap
* @param CacheInterface $cache Cache to store credentials
* @param string|null $cacheKey (optional) Cache key to use
*
* @return callable
*/
public static function cache(
callable $provider,
CacheInterface $cache,
$cacheKey = null
) {
$cacheKey = $cacheKey ?: 'aws_cached_credentials';
return function () use ($provider, $cache, $cacheKey) {
$found = $cache->get($cacheKey);
if ($found instanceof CredentialsInterface && !$found->isExpired()) {
return Promise\Create::promiseFor($found);
}
return $provider()
->then(function (CredentialsInterface $creds) use (
$cache,
$cacheKey
) {
$cache->set(
$cacheKey,
$creds,
null === $creds->getExpiration() ?
0 : $creds->getExpiration() - time()
);
return $creds;
});
};
}
/**
* Provider that creates credentials from environment variables
* AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN.
*
* @return callable
*/
public static function env()
{
return function () {
// Use credentials from environment variables, if available
$key = getenv(self::ENV_KEY);
$secret = getenv(self::ENV_SECRET);
if ($key && $secret) {
return Promise\Create::promiseFor(
new Credentials($key, $secret, getenv(self::ENV_SESSION) ?: NULL)
);
}
return self::reject('Could not find environment variable '
. 'credentials in ' . self::ENV_KEY . '/' . self::ENV_SECRET);
};
}
/**
* Credential provider that creates credentials using instance profile
* credentials.
*
* @param array $config Array of configuration data.
*
* @return InstanceProfileProvider
* @see Aws\Credentials\InstanceProfileProvider for $config details.
*/
public static function instanceProfile(array $config = [])
{
return new InstanceProfileProvider($config);
}
/**
* Credential provider that retrieves cached SSO credentials from the CLI
*
* @return callable
*/
public static function sso($ssoProfileName = 'default',
$filename = null,
$config = []
) {
$filename = $filename ?: (self::getHomeDir() . '/.aws/config');
return function () use ($ssoProfileName, $filename, $config) {
if (!@is_readable($filename)) {
return self::reject("Cannot read credentials from $filename");
}
$profiles = self::loadProfiles($filename);
if (isset($profiles[$ssoProfileName])) {
$ssoProfile = $profiles[$ssoProfileName];
} elseif (isset($profiles['profile ' . $ssoProfileName])) {
$ssoProfileName = 'profile ' . $ssoProfileName;
$ssoProfile = $profiles[$ssoProfileName];
} else {
return self::reject("Profile {$ssoProfileName} does not exist in {$filename}.");
}
if (!empty($ssoProfile['sso_session'])) {
return CredentialProvider::getSsoCredentials($profiles, $ssoProfileName, $filename, $config);
} else {
return CredentialProvider::getSsoCredentialsLegacy($profiles, $ssoProfileName, $filename, $config);
}
};
}
/**
* Credential provider that creates credentials using
* ecs credentials by a GET request, whose uri is specified
* by environment variable
*
* @param array $config Array of configuration data.
*
* @return EcsCredentialProvider
* @see Aws\Credentials\EcsCredentialProvider for $config details.
*/
public static function ecsCredentials(array $config = [])
{
return new EcsCredentialProvider($config);
}
/**
* Credential provider that creates credentials using assume role
*
* @param array $config Array of configuration data
* @return callable
* @see Aws\Credentials\AssumeRoleCredentialProvider for $config details.
*/
public static function assumeRole(array $config=[])
{
return new AssumeRoleCredentialProvider($config);
}
/**
* Credential provider that creates credentials by assuming role from a
* Web Identity Token
*
* @param array $config Array of configuration data
* @return callable
* @see Aws\Credentials\AssumeRoleWithWebIdentityCredentialProvider for
* $config details.
*/
public static function assumeRoleWithWebIdentityCredentialProvider(array $config = [])
{
return function () use ($config) {
$arnFromEnv = getenv(self::ENV_ARN);
$tokenFromEnv = getenv(self::ENV_TOKEN_FILE);
$stsClient = isset($config['stsClient'])
? $config['stsClient']
: null;
$region = isset($config['region'])
? $config['region']
: null;
if ($tokenFromEnv && $arnFromEnv) {
$sessionName = getenv(self::ENV_ROLE_SESSION_NAME)
? getenv(self::ENV_ROLE_SESSION_NAME)
: null;
$provider = new AssumeRoleWithWebIdentityCredentialProvider([
'RoleArn' => $arnFromEnv,
'WebIdentityTokenFile' => $tokenFromEnv,
'SessionName' => $sessionName,
'client' => $stsClient,
'region' => $region
]);
return $provider();
}
$profileName = getenv(self::ENV_PROFILE) ?: 'default';
if (isset($config['filename'])) {
$profiles = self::loadProfiles($config['filename']);
} else {
$profiles = self::loadDefaultProfiles();
}
if (isset($profiles[$profileName])) {
$profile = $profiles[$profileName];
if (isset($profile['region'])) {
$region = $profile['region'];
}
if (isset($profile['web_identity_token_file'])
&& isset($profile['role_arn'])
) {
$sessionName = isset($profile['role_session_name'])
? $profile['role_session_name']
: null;
$provider = new AssumeRoleWithWebIdentityCredentialProvider([
'RoleArn' => $profile['role_arn'],
'WebIdentityTokenFile' => $profile['web_identity_token_file'],
'SessionName' => $sessionName,
'client' => $stsClient,
'region' => $region
]);
return $provider();
}
} else {
return self::reject("Unknown profile: $profileName");
}
return self::reject("No RoleArn or WebIdentityTokenFile specified");
};
}
/**
* Credentials provider that creates credentials using an ini file stored
* in the current user's home directory. A source can be provided
* in this file for assuming a role using the credential_source config option.
*
* @param string|null $profile Profile to use. If not specified will use
* the "default" profile in "~/.aws/credentials".
* @param string|null $filename If provided, uses a custom filename rather
* than looking in the home directory.
* @param array|null $config If provided, may contain the following:
* preferStaticCredentials: If true, prefer static
* credentials to role_arn if both are present
* disableAssumeRole: If true, disable support for
* roles that assume an IAM role. If true and role profile
* is selected, an error is raised.
* stsClient: StsClient used to assume role specified in profile
*
* @return callable
*/
public static function ini($profile = null, $filename = null, array $config = [])
{
$filename = self::getFileName($filename);
$profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default');
return function () use ($profile, $filename, $config) {
$preferStaticCredentials = isset($config['preferStaticCredentials'])
? $config['preferStaticCredentials']
: false;
$disableAssumeRole = isset($config['disableAssumeRole'])
? $config['disableAssumeRole']
: false;
$stsClient = isset($config['stsClient']) ? $config['stsClient'] : null;
if (!@is_readable($filename)) {
return self::reject("Cannot read credentials from $filename");
}
$data = self::loadProfiles($filename);
if ($data === false) {
return self::reject("Invalid credentials file: $filename");
}
if (!isset($data[$profile])) {
return self::reject("'$profile' not found in credentials file");
}
/*
In the CLI, the presence of both a role_arn and static credentials have
different meanings depending on how many profiles have been visited. For
the first profile processed, role_arn takes precedence over any static
credentials, but for all subsequent profiles, static credentials are
used if present, and only in their absence will the profile's
source_profile and role_arn keys be used to load another set of
credentials. This bool is intended to yield compatible behaviour in this
sdk.
*/
$preferStaticCredentialsToRoleArn = ($preferStaticCredentials
&& isset($data[$profile]['aws_access_key_id'])
&& isset($data[$profile]['aws_secret_access_key']));
if (isset($data[$profile]['role_arn'])
&& !$preferStaticCredentialsToRoleArn
) {
if ($disableAssumeRole) {
return self::reject(
"Role assumption profiles are disabled. "
. "Failed to load profile " . $profile);
}
return self::loadRoleProfile(
$data,
$profile,
$filename,
$stsClient,
$config
);
}
if (!isset($data[$profile]['aws_access_key_id'])
|| !isset($data[$profile]['aws_secret_access_key'])
) {
return self::reject("No credentials present in INI profile "
. "'$profile' ($filename)");
}
if (empty($data[$profile]['aws_session_token'])) {
$data[$profile]['aws_session_token']
= isset($data[$profile]['aws_security_token'])
? $data[$profile]['aws_security_token']
: null;
}
return Promise\Create::promiseFor(
new Credentials(
$data[$profile]['aws_access_key_id'],
$data[$profile]['aws_secret_access_key'],
$data[$profile]['aws_session_token']
)
);
};
}
/**
* Credentials provider that creates credentials using a process configured in
* ini file stored in the current user's home directory.
*
* @param string|null $profile Profile to use. If not specified will use
* the "default" profile in "~/.aws/credentials".
* @param string|null $filename If provided, uses a custom filename rather
* than looking in the home directory.
*
* @return callable
*/
public static function process($profile = null, $filename = null)
{
$filename = self::getFileName($filename);
$profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default');
return function () use ($profile, $filename) {
if (!@is_readable($filename)) {
return self::reject("Cannot read process credentials from $filename");
}
$data = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW);
if ($data === false) {
return self::reject("Invalid credentials file: $filename");
}
if (!isset($data[$profile])) {
return self::reject("'$profile' not found in credentials file");
}
if (!isset($data[$profile]['credential_process'])) {
return self::reject("No credential_process present in INI profile "
. "'$profile' ($filename)");
}
$credentialProcess = $data[$profile]['credential_process'];
$json = shell_exec($credentialProcess);
$processData = json_decode($json, true);
// Only support version 1
if (isset($processData['Version'])) {
if ($processData['Version'] !== 1) {
return self::reject("credential_process does not return Version == 1");
}
}
if (!isset($processData['AccessKeyId'])
|| !isset($processData['SecretAccessKey']))
{
return self::reject("credential_process does not return valid credentials");
}
if (isset($processData['Expiration'])) {
try {
$expiration = new DateTimeResult($processData['Expiration']);
} catch (\Exception $e) {
return self::reject("credential_process returned invalid expiration");
}
$now = new DateTimeResult();
if ($expiration < $now) {
return self::reject("credential_process returned expired credentials");
}
$expires = $expiration->getTimestamp();
} else {
$expires = null;
}
if (empty($processData['SessionToken'])) {
$processData['SessionToken'] = null;
}
return Promise\Create::promiseFor(
new Credentials(
$processData['AccessKeyId'],
$processData['SecretAccessKey'],
$processData['SessionToken'],
$expires
)
);
};
}
/**
* Assumes role for profile that includes role_arn
*
* @return callable
*/
private static function loadRoleProfile(
$profiles,
$profileName,
$filename,
$stsClient,
$config = []
) {
$roleProfile = $profiles[$profileName];
$roleArn = isset($roleProfile['role_arn']) ? $roleProfile['role_arn'] : '';
$roleSessionName = isset($roleProfile['role_session_name'])
? $roleProfile['role_session_name']
: 'aws-sdk-php-' . round(microtime(true) * 1000);
if (
empty($roleProfile['source_profile'])
== empty($roleProfile['credential_source'])
) {
return self::reject("Either source_profile or credential_source must be set " .
"using profile " . $profileName . ", but not both."
);
}
$sourceProfileName = "";
if (!empty($roleProfile['source_profile'])) {
$sourceProfileName = $roleProfile['source_profile'];
if (!isset($profiles[$sourceProfileName])) {
return self::reject("source_profile " . $sourceProfileName
. " using profile " . $profileName . " does not exist"
);
}
if (isset($config['visited_profiles']) &&
in_array($roleProfile['source_profile'], $config['visited_profiles'])
) {
return self::reject("Circular source_profile reference found.");
}
$config['visited_profiles'] [] = $roleProfile['source_profile'];
} else {
if (empty($roleArn)) {
return self::reject(
"A role_arn must be provided with credential_source in " .
"file {$filename} under profile {$profileName} "
);
}
}
if (empty($stsClient)) {
$sourceRegion = isset($profiles[$sourceProfileName]['region'])
? $profiles[$sourceProfileName]['region']
: 'us-east-1';
$config['preferStaticCredentials'] = true;
$sourceCredentials = null;
if (!empty($roleProfile['source_profile'])){
$sourceCredentials = call_user_func(
CredentialProvider::ini($sourceProfileName, $filename, $config)
)->wait();
} else {
$sourceCredentials = self::getCredentialsFromSource(
$profileName,
$filename
);
}
$stsClient = new StsClient([
'credentials' => $sourceCredentials,
'region' => $sourceRegion,
'version' => '2011-06-15',
]);
}
$result = $stsClient->assumeRole([
'RoleArn' => $roleArn,
'RoleSessionName' => $roleSessionName
]);
$credentials = $stsClient->createCredentials($result);
return Promise\Create::promiseFor($credentials);
}
/**
* Gets the environment's HOME directory if available.
*
* @return null|string
*/
private static function getHomeDir()
{
// On Linux/Unix-like systems, use the HOME environment variable
if ($homeDir = getenv('HOME')) {
return $homeDir;
}
// Get the HOMEDRIVE and HOMEPATH values for Windows hosts
$homeDrive = getenv('HOMEDRIVE');
$homePath = getenv('HOMEPATH');
return ($homeDrive && $homePath) ? $homeDrive . $homePath : null;
}
/**
* Gets profiles from specified $filename, or default ini files.
*/
private static function loadProfiles($filename)
{
$profileData = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW);
// If loading .aws/credentials, also load .aws/config when AWS_SDK_LOAD_NONDEFAULT_CONFIG is set
if ($filename === self::getHomeDir() . '/.aws/credentials'
&& getenv('AWS_SDK_LOAD_NONDEFAULT_CONFIG')
) {
$configFilename = self::getHomeDir() . '/.aws/config';
$configProfileData = \Aws\parse_ini_file($configFilename, true, INI_SCANNER_RAW);
foreach ($configProfileData as $name => $profile) {
// standardize config profile names
$name = str_replace('profile ', '', $name);
if (!isset($profileData[$name])) {
$profileData[$name] = $profile;
}
}
}
return $profileData;
}
/**
* Gets profiles from ~/.aws/credentials and ~/.aws/config ini files
*/
private static function loadDefaultProfiles() {
$profiles = [];
$credFile = self::getHomeDir() . '/.aws/credentials';
$configFile = self::getHomeDir() . '/.aws/config';
if (file_exists($credFile)) {
$profiles = \Aws\parse_ini_file($credFile, true, INI_SCANNER_RAW);
}
if (file_exists($configFile)) {
$configProfileData = \Aws\parse_ini_file($configFile, true, INI_SCANNER_RAW);
foreach ($configProfileData as $name => $profile) {
// standardize config profile names
$name = str_replace('profile ', '', $name);
if (!isset($profiles[$name])) {
$profiles[$name] = $profile;
}
}
}
return $profiles;
}
public static function getCredentialsFromSource(
$profileName = '',
$filename = '',
$config = []
) {
$data = self::loadProfiles($filename);
$credentialSource = !empty($data[$profileName]['credential_source'])
? $data[$profileName]['credential_source']
: null;
$credentialsPromise = null;
switch ($credentialSource) {
case 'Environment':
$credentialsPromise = self::env();
break;
case 'Ec2InstanceMetadata':
$credentialsPromise = self::instanceProfile($config);
break;
case 'EcsContainer':
$credentialsPromise = self::ecsCredentials($config);
break;
default:
throw new CredentialsException(
"Invalid credential_source found in config file: {$credentialSource}. Valid inputs "
. "include Environment, Ec2InstanceMetadata, and EcsContainer."
);
}
$credentialsResult = null;
try {
$credentialsResult = $credentialsPromise()->wait();
} catch (\Exception $reason) {
return self::reject(
"Unable to successfully retrieve credentials from the source specified in the"
. " credentials file: {$credentialSource}; failure message was: "
. $reason->getMessage()
);
}
return function () use ($credentialsResult) {
return Promise\Create::promiseFor($credentialsResult);
};
}
private static function reject($msg)
{
return new Promise\RejectedPromise(new CredentialsException($msg));
}
/**
* @param $filename
* @return string
*/
private static function getFileName($filename)
{
if (!isset($filename)) {
$filename = getenv(self::ENV_SHARED_CREDENTIALS_FILE) ?:
(self::getHomeDir() . '/.aws/credentials');
}
return $filename;
}
/**
* @return boolean
*/
public static function shouldUseEcs()
{
//Check for relative uri. if not, then full uri.
//fall back to server for each as getenv is not thread-safe.
return !empty(getenv(EcsCredentialProvider::ENV_URI))
|| !empty($_SERVER[EcsCredentialProvider::ENV_URI])
|| !empty(getenv(EcsCredentialProvider::ENV_FULL_URI))
|| !empty($_SERVER[EcsCredentialProvider::ENV_FULL_URI]);
}
/**
* @param $profiles
* @param $ssoProfileName
* @param $filename
* @param $config
* @return Promise\PromiseInterface
*/
private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $config)
{
if (empty($config['ssoOidcClient'])) {
$ssoProfile = $profiles[$ssoProfileName];
$sessionName = $ssoProfile['sso_session'];
if (empty($profiles['sso-session ' . $sessionName])) {
return self::reject(
"Could not find sso-session {$sessionName} in {$filename}"
);
}
$ssoSession = $profiles['sso-session ' . $ssoProfile['sso_session']];
$ssoOidcClient = new Aws\SSOOIDC\SSOOIDCClient([
'region' => $ssoSession['sso_region'],
'version' => '2019-06-10',
'credentials' => false
]);
} else {
$ssoOidcClient = $config['ssoClient'];
}
$tokenPromise = new Aws\Token\SsoTokenProvider(
$ssoProfileName,
$filename,
$ssoOidcClient
);
$token = $tokenPromise()->wait();
$ssoCredentials = CredentialProvider::getCredentialsFromSsoService(
$ssoProfile,
$ssoSession['sso_region'],
$token->getToken(),
$config
);
$expiration = $ssoCredentials['expiration'];
return Promise\Create::promiseFor(
new Credentials(
$ssoCredentials['accessKeyId'],
$ssoCredentials['secretAccessKey'],
$ssoCredentials['sessionToken'],
$expiration
)
);
}
/**
* @param $profiles
* @param $ssoProfileName
* @param $filename
* @param $config
* @return Promise\PromiseInterface
*/
private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $filename, $config)
{
$ssoProfile = $profiles[$ssoProfileName];
if (empty($ssoProfile['sso_start_url'])
|| empty($ssoProfile['sso_region'])
|| empty($ssoProfile['sso_account_id'])
|| empty($ssoProfile['sso_role_name'])
) {
return self::reject(
"Profile {$ssoProfileName} in {$filename} must contain the following keys: "
. "sso_start_url, sso_region, sso_account_id, and sso_role_name."
);
}
$tokenLocation = self::getHomeDir()
. '/.aws/sso/cache/'
. sha1($ssoProfile['sso_start_url'])
. ".json";
if (!@is_readable($tokenLocation)) {
return self::reject("Unable to read token file at $tokenLocation");
}
$tokenData = json_decode(file_get_contents($tokenLocation), true);
if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) {
return self::reject(
"Token file at {$tokenLocation} must contain an access token and an expiration"
);
}
try {
$expiration = (new DateTimeResult($tokenData['expiresAt']))->getTimestamp();
} catch (\Exception $e) {
return self::reject("Cached SSO credentials returned an invalid expiration");
}
$now = time();
if ($expiration < $now) {
return self::reject("Cached SSO credentials returned expired credentials");
}
$ssoCredentials = CredentialProvider::getCredentialsFromSsoService(
$ssoProfile,
$ssoProfile['sso_region'],
$tokenData['accessToken'],
$config
);
return Promise\Create::promiseFor(
new Credentials(
$ssoCredentials['accessKeyId'],
$ssoCredentials['secretAccessKey'],
$ssoCredentials['sessionToken'],
$expiration
)
);
}
/**
* @param array $ssoProfile
* @param string $clientRegion
* @param string $accessToken
* @param array $config
* @return array|null
*/
private static function getCredentialsFromSsoService($ssoProfile, $clientRegion, $accessToken, $config)
{
if (empty($config['ssoClient'])) {
$ssoClient = new Aws\SSO\SSOClient([
'region' => $clientRegion,
'version' => '2019-06-10',
'credentials' => false
]);
} else {
$ssoClient = $config['ssoClient'];
}
$ssoResponse = $ssoClient->getRoleCredentials([
'accessToken' => $accessToken,
'accountId' => $ssoProfile['sso_account_id'],
'roleName' => $ssoProfile['sso_role_name']
]);
$ssoCredentials = $ssoResponse['roleCredentials'];
return $ssoCredentials;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Aws\Credentials;
/**
* Basic implementation of the AWS Credentials interface that allows callers to
* pass in the AWS Access Key and AWS Secret Access Key in the constructor.
*/
class Credentials implements CredentialsInterface, \Serializable
{
private $key;
private $secret;
private $token;
private $expires;
/**
* Constructs a new BasicAWSCredentials object, with the specified AWS
* access key and AWS secret key
*
* @param string $key AWS access key ID
* @param string $secret AWS secret access key
* @param string $token Security token to use
* @param int $expires UNIX timestamp for when credentials expire
*/
public function __construct($key, $secret, $token = null, $expires = null)
{
$this->key = trim($key);
$this->secret = trim($secret);
$this->token = $token;
$this->expires = $expires;
}
public static function __set_state(array $state)
{
return new self(
$state['key'],
$state['secret'],
$state['token'],
$state['expires']
);
}
public function getAccessKeyId()
{
return $this->key;
}
public function getSecretKey()
{
return $this->secret;
}
public function getSecurityToken()
{
return $this->token;
}
public function getExpiration()
{
return $this->expires;
}
public function isExpired()
{
return $this->expires !== null && time() >= $this->expires;
}
public function toArray()
{
return [
'key' => $this->key,
'secret' => $this->secret,
'token' => $this->token,
'expires' => $this->expires
];
}
public function serialize()
{
return json_encode($this->__serialize());
}
public function unserialize($serialized)
{
$data = json_decode($serialized, true);
$this->__unserialize($data);
}
public function __serialize()
{
return $this->toArray();
}
public function __unserialize($data)
{
$this->key = $data['key'];
$this->secret = $data['secret'];
$this->token = $data['token'];
$this->expires = $data['expires'];
}
/**
* Internal-only. Used when IMDS is unreachable
* or returns expires credentials.
*
* @internal
*/
public function extendExpiration() {
$extension = mt_rand(5, 10);
$this->expires = time() + $extension * 60;
$message = <<<EOT
Attempting credential expiration extension due to a credential service
availability issue. A refresh of these credentials will be attempted again
after {$extension} minutes.\n
EOT;
trigger_error($message, E_USER_WARNING);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Aws\Credentials;
/**
* Provides access to the AWS credentials used for accessing AWS services: AWS
* access key ID, secret access key, and security token. These credentials are
* used to securely sign requests to AWS services.
*/
interface CredentialsInterface
{
/**
* Returns the AWS access key ID for this credentials object.
*
* @return string
*/
public function getAccessKeyId();
/**
* Returns the AWS secret access key for this credentials object.
*
* @return string
*/
public function getSecretKey();
/**
* Get the associated security token if available
*
* @return string|null
*/
public function getSecurityToken();
/**
* Get the UNIX timestamp in which the credentials will expire
*
* @return int|null
*/
public function getExpiration();
/**
* Check if the credentials are expired
*
* @return bool
*/
public function isExpired();
/**
* Converts the credentials to an associative array.
*
* @return array
*/
public function toArray();
}

View File

@@ -0,0 +1,134 @@
<?php
namespace Aws\Credentials;
use Aws\Exception\CredentialsException;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Credential provider that fetches credentials with GET request.
* ECS environment variable is used in constructing request URI.
*/
class EcsCredentialProvider
{
const SERVER_URI = 'http://169.254.170.2';
const ENV_URI = "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI";
const ENV_FULL_URI = "AWS_CONTAINER_CREDENTIALS_FULL_URI";
const ENV_AUTH_TOKEN = "AWS_CONTAINER_AUTHORIZATION_TOKEN";
const ENV_TIMEOUT = 'AWS_METADATA_SERVICE_TIMEOUT';
/** @var callable */
private $client;
/** @var float|mixed */
private $timeout;
/**
* The constructor accepts following options:
* - timeout: (optional) Connection timeout, in seconds, default 1.0
* - client: An EcsClient to make request from
*
* @param array $config Configuration options
*/
public function __construct(array $config = [])
{
$timeout = getenv(self::ENV_TIMEOUT);
if (!$timeout) {
$timeout = isset($_SERVER[self::ENV_TIMEOUT])
? $_SERVER[self::ENV_TIMEOUT]
: (isset($config['timeout']) ? $config['timeout'] : 1.0);
}
$this->timeout = (float) $timeout;
$this->client = isset($config['client'])
? $config['client']
: \Aws\default_http_handler();
}
/**
* Load ECS credentials
*
* @return PromiseInterface
*/
public function __invoke()
{
$client = $this->client;
$request = new Request('GET', self::getEcsUri());
$headers = $this->setHeaderForAuthToken();
return $client(
$request,
[
'timeout' => $this->timeout,
'proxy' => '',
'headers' => $headers
]
)->then(function (ResponseInterface $response) {
$result = $this->decodeResult((string) $response->getBody());
return new Credentials(
$result['AccessKeyId'],
$result['SecretAccessKey'],
$result['Token'],
strtotime($result['Expiration'])
);
})->otherwise(function ($reason) {
$reason = is_array($reason) ? $reason['exception'] : $reason;
$msg = $reason->getMessage();
throw new CredentialsException(
"Error retrieving credential from ECS ($msg)"
);
});
}
private function getEcsAuthToken()
{
return getenv(self::ENV_AUTH_TOKEN);
}
public function setHeaderForAuthToken(){
$authToken = self::getEcsAuthToken();
$headers = [];
if(!empty($authToken))
$headers = ['Authorization' => $authToken];
return $headers;
}
/**
* Fetch credential URI from ECS environment variable
*
* @return string Returns ECS URI
*/
private function getEcsUri()
{
$credsUri = getenv(self::ENV_URI);
if ($credsUri === false) {
$credsUri = isset($_SERVER[self::ENV_URI]) ? $_SERVER[self::ENV_URI] : '';
}
if(empty($credsUri)){
$credFullUri = getenv(self::ENV_FULL_URI);
if($credFullUri === false){
$credFullUri = isset($_SERVER[self::ENV_FULL_URI]) ? $_SERVER[self::ENV_FULL_URI] : '';
}
if(!empty($credFullUri))
return $credFullUri;
}
return self::SERVER_URI . $credsUri;
}
private function decodeResult($response)
{
$result = json_decode($response, true);
if (!isset($result['AccessKeyId'])) {
throw new CredentialsException('Unexpected ECS credential value');
}
return $result;
}
}

View File

@@ -0,0 +1,299 @@
<?php
namespace Aws\Credentials;
use Aws\Exception\CredentialsException;
use Aws\Exception\InvalidJsonException;
use Aws\Sdk;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Promise;
use GuzzleHttp\Exception\RequestException;
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 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'] // internal use only
: \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 .= ' ' . \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;
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use Aws\Crypto\Cipher\Cbc;
use GuzzleHttp\Psr7\Stream;
/**
* Legacy abstract encryption client. New workflows should use
* AbstractCryptoClientV2.
*
* @deprecated
* @internal
*/
abstract class AbstractCryptoClient
{
public static $supportedCiphers = ['cbc', 'gcm'];
public static $supportedKeyWraps = [
KmsMaterialsProvider::WRAP_ALGORITHM_NAME
];
/**
* Returns if the passed cipher name is supported for encryption by the SDK.
*
* @param string $cipherName The name of a cipher to verify is registered.
*
* @return bool If the cipher passed is in our supported list.
*/
public static function isSupportedCipher($cipherName)
{
return in_array($cipherName, self::$supportedCiphers);
}
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @param string $cipherName Name of the cipher being used for encrypting
* or decrypting.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return string
*/
abstract protected function getCipherOpenSslName($cipherName, $keySize);
/**
* Constructs a CipherMethod for the given name, initialized with the other
* data passed for use in encrypting or decrypting.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return CipherMethod
*
* @internal
*/
abstract protected function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
abstract protected function getCipherFromAesName($aesName);
/**
* Dependency to provide an interface for building an encryption stream for
* data given cipher details, metadata, and materials to do so.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param array $cipherOptions Options for use in determining the cipher to
* be used for encrypting data.
* @param MaterialsProvider $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be added to.
*
* @return AesStreamInterface
*
* @internal
*/
abstract public function encrypt(
Stream $plaintext,
array $cipherOptions,
MaterialsProvider $provider,
MetadataEnvelope $envelope
);
/**
* Dependency to provide an interface for building a decryption stream for
* cipher text given metadata and materials to do so.
*
* @param string $cipherText Plain-text data to be decrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterface $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
* @param array $cipherOptions Additional verification options.
*
* @return AesStreamInterface
*
* @internal
*/
abstract public function decrypt(
$cipherText,
MaterialsProviderInterface $provider,
MetadataEnvelope $envelope,
array $cipherOptions = []
);
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Cipher\CipherMethod;
use GuzzleHttp\Psr7\Stream;
/**
* @internal
*/
abstract class AbstractCryptoClientV2
{
public static $supportedCiphers = ['gcm'];
public static $supportedKeyWraps = [
KmsMaterialsProviderV2::WRAP_ALGORITHM_NAME
];
public static $supportedSecurityProfiles = ['V2', 'V2_AND_LEGACY'];
public static $legacySecurityProfiles = ['V2_AND_LEGACY'];
/**
* Returns if the passed cipher name is supported for encryption by the SDK.
*
* @param string $cipherName The name of a cipher to verify is registered.
*
* @return bool If the cipher passed is in our supported list.
*/
public static function isSupportedCipher($cipherName)
{
return in_array($cipherName, self::$supportedCiphers, true);
}
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-gcm`
*
* @param string $cipherName Name of the cipher being used for encrypting
* or decrypting.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return string
*/
abstract protected function getCipherOpenSslName($cipherName, $keySize);
/**
* Constructs a CipherMethod for the given name, initialized with the other
* data passed for use in encrypting or decrypting.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return CipherMethod
*
* @internal
*/
abstract protected function buildCipherMethod($cipherName, $iv, $keySize);
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
abstract protected function getCipherFromAesName($aesName);
/**
* Dependency to provide an interface for building an encryption stream for
* data given cipher details, metadata, and materials to do so.
*
* @param Stream $plaintext Plain-text data to be encrypted using the
* materials, algorithm, and data provided.
* @param array $options Options for use in encryption.
* @param MaterialsProviderV2 $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be added to.
*
* @return AesStreamInterface
*
* @internal
*/
abstract public function encrypt(
Stream $plaintext,
array $options,
MaterialsProviderV2 $provider,
MetadataEnvelope $envelope
);
/**
* Dependency to provide an interface for building a decryption stream for
* cipher text given metadata and materials to do so.
*
* @param string $cipherText Plain-text data to be decrypted using the
* materials, algorithm, and data provided.
* @param MaterialsProviderInterface $provider A provider to supply and encrypt
* materials used in encryption.
* @param MetadataEnvelope $envelope A storage envelope for encryption
* metadata to be read from.
* @param array $options Options used for decryption.
*
* @return AesStreamInterface
*
* @internal
*/
abstract public function decrypt(
$cipherText,
MaterialsProviderInterfaceV2 $provider,
MetadataEnvelope $envelope,
array $options = []
);
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use \LogicException;
use Psr\Http\Message\StreamInterface;
use Aws\Crypto\Cipher\CipherMethod;
/**
* @internal Represents a stream of data to be decrypted with passed cipher.
*/
class AesDecryptingStream implements AesStreamInterface
{
const BLOCK_SIZE = 16; // 128 bits
use StreamDecoratorTrait;
/**
* @var string
*/
private $buffer = '';
/**
* @var CipherMethod
*/
private $cipherMethod;
/**
* @var string
*/
private $key;
/**
* @var StreamInterface
*/
private $stream;
/**
* @param StreamInterface $cipherText
* @param string $key
* @param CipherMethod $cipherMethod
*/
public function __construct(
StreamInterface $cipherText,
$key,
CipherMethod $cipherMethod
) {
$this->stream = $cipherText;
$this->key = $key;
$this->cipherMethod = clone $cipherMethod;
}
public function getOpenSslName()
{
return $this->cipherMethod->getOpenSslName();
}
public function getAesName()
{
return $this->cipherMethod->getAesName();
}
public function getCurrentIv()
{
return $this->cipherMethod->getCurrentIv();
}
public function getSize()
{
$plainTextSize = $this->stream->getSize();
if ($this->cipherMethod->requiresPadding()) {
// PKCS7 padding requires that between 1 and self::BLOCK_SIZE be
// added to the plaintext to make it an even number of blocks. The
// plaintext is between strlen($cipherText) - self::BLOCK_SIZE and
// strlen($cipherText) - 1
return null;
}
return $plainTextSize;
}
public function isWritable()
{
return false;
}
public function read($length)
{
if ($length > strlen($this->buffer)) {
$this->buffer .= $this->decryptBlock(
(int) (
self::BLOCK_SIZE * ceil(($length - strlen($this->buffer)) / self::BLOCK_SIZE)
)
);
}
$data = substr($this->buffer, 0, $length);
$this->buffer = substr($this->buffer, $length);
return $data ? $data : '';
}
public function seek($offset, $whence = SEEK_SET)
{
if ($offset === 0 && $whence === SEEK_SET) {
$this->buffer = '';
$this->cipherMethod->seek(0, SEEK_SET);
$this->stream->seek(0, SEEK_SET);
} else {
throw new LogicException('AES encryption streams only support being'
. ' rewound, not arbitrary seeking.');
}
}
private function decryptBlock($length)
{
if ($this->stream->eof()) {
return '';
}
$cipherText = '';
do {
$cipherText .= $this->stream->read((int) ($length - strlen($cipherText)));
} while (strlen($cipherText) < $length && !$this->stream->eof());
$options = OPENSSL_RAW_DATA;
if (!$this->stream->eof()
&& $this->stream->getSize() !== $this->stream->tell()
) {
$options |= OPENSSL_ZERO_PADDING;
}
$plaintext = openssl_decrypt(
$cipherText,
$this->cipherMethod->getOpenSslName(),
$this->key,
$options,
$this->cipherMethod->getCurrentIv()
);
$this->cipherMethod->update($cipherText);
return $plaintext;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Aws\Crypto;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use \LogicException;
use Psr\Http\Message\StreamInterface;
use Aws\Crypto\Cipher\CipherMethod;
/**
* @internal Represents a stream of data to be encrypted with a passed cipher.
*/
class AesEncryptingStream implements AesStreamInterface
{
const BLOCK_SIZE = 16; // 128 bits
use StreamDecoratorTrait;
/**
* @var string
*/
private $buffer = '';
/**
* @var CipherMethod
*/
private $cipherMethod;
/**
* @var string
*/
private $key;
/**
* @var StreamInterface
*/
private $stream;
/**
* @param StreamInterface $plainText
* @param string $key
* @param CipherMethod $cipherMethod
*/
public function __construct(
StreamInterface $plainText,
$key,
CipherMethod $cipherMethod
) {
$this->stream = $plainText;
$this->key = $key;
$this->cipherMethod = clone $cipherMethod;
}
public function getOpenSslName()
{
return $this->cipherMethod->getOpenSslName();
}
public function getAesName()
{
return $this->cipherMethod->getAesName();
}
public function getCurrentIv()
{
return $this->cipherMethod->getCurrentIv();
}
public function getSize()
{
$plainTextSize = $this->stream->getSize();
if ($this->cipherMethod->requiresPadding() && $plainTextSize !== null) {
// PKCS7 padding requires that between 1 and self::BLOCK_SIZE be
// added to the plaintext to make it an even number of blocks.
$padding = self::BLOCK_SIZE - $plainTextSize % self::BLOCK_SIZE;
return $plainTextSize + $padding;
}
return $plainTextSize;
}
public function isWritable()
{
return false;
}
public function read($length)
{
if ($length > strlen($this->buffer)) {
$this->buffer .= $this->encryptBlock(
(int)
self::BLOCK_SIZE * ceil(($length - strlen($this->buffer)) / self::BLOCK_SIZE)
);
}
$data = substr($this->buffer, 0, $length);
$this->buffer = substr($this->buffer, $length);
return $data ? $data : '';
}
public function seek($offset, $whence = SEEK_SET)
{
if ($whence === SEEK_CUR) {
$offset = $this->tell() + $offset;
$whence = SEEK_SET;
}
if ($whence === SEEK_SET) {
$this->buffer = '';
$wholeBlockOffset
= (int) ($offset / self::BLOCK_SIZE) * self::BLOCK_SIZE;
$this->stream->seek($wholeBlockOffset);
$this->cipherMethod->seek($wholeBlockOffset);
$this->read($offset - $wholeBlockOffset);
} else {
throw new LogicException('Unrecognized whence.');
}
}
private function encryptBlock($length)
{
if ($this->stream->eof()) {
return '';
}
$plainText = '';
do {
$plainText .= $this->stream->read((int) ($length - strlen($plainText)));
} while (strlen($plainText) < $length && !$this->stream->eof());
$options = OPENSSL_RAW_DATA;
if (!$this->stream->eof()
|| $this->stream->getSize() !== $this->stream->tell()
) {
$options |= OPENSSL_ZERO_PADDING;
}
$cipherText = openssl_encrypt(
$plainText,
$this->cipherMethod->getOpenSslName(),
$this->key,
$options,
$this->cipherMethod->getCurrentIv()
);
$this->cipherMethod->update($cipherText);
return $cipherText;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Aws\Crypto;
use Aws\Exception\CryptoException;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use Aws\Crypto\Polyfill\AesGcm;
use Aws\Crypto\Polyfill\Key;
/**
* @internal Represents a stream of data to be gcm decrypted.
*/
class AesGcmDecryptingStream implements AesStreamInterface
{
use StreamDecoratorTrait;
private $aad;
private $initializationVector;
private $key;
private $keySize;
private $cipherText;
private $tag;
private $tagLength;
/**
* @param StreamInterface $cipherText
* @param string $key
* @param string $initializationVector
* @param string $tag
* @param string $aad
* @param int $tagLength
* @param int $keySize
*/
public function __construct(
StreamInterface $cipherText,
$key,
$initializationVector,
$tag,
$aad = '',
$tagLength = 128,
$keySize = 256
) {
$this->cipherText = $cipherText;
$this->key = $key;
$this->initializationVector = $initializationVector;
$this->tag = $tag;
$this->aad = $aad;
$this->tagLength = $tagLength;
$this->keySize = $keySize;
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-gcm";
}
public function getAesName()
{
return 'AES/GCM/NoPadding';
}
public function getCurrentIv()
{
return $this->initializationVector;
}
public function createStream()
{
if (version_compare(PHP_VERSION, '7.1', '<')) {
return Psr7\Utils::streamFor(AesGcm::decrypt(
(string) $this->cipherText,
$this->initializationVector,
new Key($this->key),
$this->aad,
$this->tag,
$this->keySize
));
} else {
$result = \openssl_decrypt(
(string)$this->cipherText,
$this->getOpenSslName(),
$this->key,
OPENSSL_RAW_DATA,
$this->initializationVector,
$this->tag,
$this->aad
);
if ($result === false) {
throw new CryptoException('The requested object could not be'
. ' decrypted due to an invalid authentication tag.');
}
return Psr7\Utils::streamFor($result);
}
}
public function isWritable()
{
return false;
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Aws\Crypto;
use Aws\Crypto\Polyfill\AesGcm;
use Aws\Crypto\Polyfill\Key;
use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\StreamDecoratorTrait;
use Psr\Http\Message\StreamInterface;
use \RuntimeException;
/**
* @internal Represents a stream of data to be gcm encrypted.
*/
class AesGcmEncryptingStream implements AesStreamInterface, AesStreamInterfaceV2
{
use StreamDecoratorTrait;
private $aad;
private $initializationVector;
private $key;
private $keySize;
private $plaintext;
private $tag = '';
private $tagLength;
/**
* Same as non-static 'getAesName' method, allowing calls in a static
* context.
*
* @return string
*/
public static function getStaticAesName()
{
return 'AES/GCM/NoPadding';
}
/**
* @param StreamInterface $plaintext
* @param string $key
* @param string $initializationVector
* @param string $aad
* @param int $tagLength
* @param int $keySize
*/
public function __construct(
StreamInterface $plaintext,
$key,
$initializationVector,
$aad = '',
$tagLength = 16,
$keySize = 256
) {
$this->plaintext = $plaintext;
$this->key = $key;
$this->initializationVector = $initializationVector;
$this->aad = $aad;
$this->tagLength = $tagLength;
$this->keySize = $keySize;
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-gcm";
}
/**
* Same as static method and retained for backwards compatibility
*
* @return string
*/
public function getAesName()
{
return self::getStaticAesName();
}
public function getCurrentIv()
{
return $this->initializationVector;
}
public function createStream()
{
if (version_compare(PHP_VERSION, '7.1', '<')) {
return Psr7\Utils::streamFor(AesGcm::encrypt(
(string) $this->plaintext,
$this->initializationVector,
new Key($this->key),
$this->aad,
$this->tag,
$this->keySize
));
} else {
return Psr7\Utils::streamFor(\openssl_encrypt(
(string)$this->plaintext,
$this->getOpenSslName(),
$this->key,
OPENSSL_RAW_DATA,
$this->initializationVector,
$this->tag,
$this->aad,
$this->tagLength
));
}
}
/**
* @return string
*/
public function getTag()
{
return $this->tag;
}
public function isWritable()
{
return false;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Aws\Crypto;
use Psr\Http\Message\StreamInterface;
interface AesStreamInterface extends StreamInterface
{
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @return string
*/
public function getOpenSslName();
/**
* Returns an AES recognizable name, such as 'AES/GCM/NoPadding'.
*
* @return string
*/
public function getAesName();
/**
* Returns the IV that should be used to initialize the next block in
* encrypt or decrypt.
*
* @return string
*/
public function getCurrentIv();
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Aws\Crypto;
use Psr\Http\Message\StreamInterface;
interface AesStreamInterfaceV2 extends StreamInterface
{
/**
* Returns an AES recognizable name, such as 'AES/GCM/NoPadding'. V2
* interface is accessible from a static context.
*
* @return string
*/
public static function getStaticAesName();
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @return string
*/
public function getOpenSslName();
/**
* Returns the IV that should be used to initialize the next block in
* encrypt or decrypt.
*
* @return string
*/
public function getCurrentIv();
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Aws\Crypto\Cipher;
use \InvalidArgumentException;
use \LogicException;
/**
* An implementation of the CBC cipher for use with an AesEncryptingStream or
* AesDecrypting stream.
*
* This cipher method is deprecated and in maintenance mode - no new updates will be
* released. Please see https://docs.aws.amazon.com/general/latest/gr/aws_sdk_cryptography.html
* for more information.
*
* @deprecated
*/
class Cbc implements CipherMethod
{
const BLOCK_SIZE = 16;
/**
* @var string
*/
private $baseIv;
/**
* @var string
*/
private $iv;
/**
* @var int
*/
private $keySize;
/**
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @throws InvalidArgumentException Thrown if the passed iv does not match
* the iv length required by the cipher.
*/
public function __construct($iv, $keySize = 256)
{
$this->baseIv = $this->iv = $iv;
$this->keySize = $keySize;
if (strlen($iv) !== openssl_cipher_iv_length($this->getOpenSslName())) {
throw new InvalidArgumentException('Invalid initialization vector');
}
}
public function getOpenSslName()
{
return "aes-{$this->keySize}-cbc";
}
public function getAesName()
{
return 'AES/CBC/PKCS5Padding';
}
public function getCurrentIv()
{
return $this->iv;
}
public function requiresPadding()
{
return true;
}
public function seek($offset, $whence = SEEK_SET)
{
if ($offset === 0 && $whence === SEEK_SET) {
$this->iv = $this->baseIv;
} else {
throw new LogicException('CBC initialization only support being'
. ' rewound, not arbitrary seeking.');
}
}
public function update($cipherTextBlock)
{
$this->iv = substr($cipherTextBlock, self::BLOCK_SIZE * -1);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace Aws\Crypto\Cipher;
use Aws\Exception\CryptoException;
trait CipherBuilderTrait
{
/**
* Returns an identifier recognizable by `openssl_*` functions, such as
* `aes-256-cbc` or `aes-128-ctr`.
*
* @param string $cipherName Name of the cipher being used for encrypting
* or decrypting.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return string
*/
protected function getCipherOpenSslName($cipherName, $keySize)
{
return "aes-{$keySize}-{$cipherName}";
}
/**
* Constructs a CipherMethod for the given name, initialized with the other
* data passed for use in encrypting or decrypting.
*
* @param string $cipherName Name of the cipher to generate for encrypting.
* @param string $iv Base Initialization Vector for the cipher.
* @param int $keySize Size of the encryption key, in bits, that will be
* used.
*
* @return CipherMethod
*
* @internal
*/
protected function buildCipherMethod($cipherName, $iv, $keySize)
{
switch ($cipherName) {
case 'cbc':
return new Cbc(
$iv,
$keySize
);
default:
return null;
}
}
/**
* Performs a reverse lookup to get the openssl_* cipher name from the
* AESName passed in from the MetadataEnvelope.
*
* @param $aesName
*
* @return string
*
* @internal
*/
protected function getCipherFromAesName($aesName)
{
switch ($aesName) {
case 'AES/GCM/NoPadding':
return 'gcm';
case 'AES/CBC/PKCS5Padding':
return 'cbc';
default:
throw new CryptoException('Unrecognized or unsupported'
. ' AESName for reverse lookup.');
}
}
}

Some files were not shown because too many files have changed in this diff Show More