From 7f4ea447edd94960ee702a62466bf2141c406cb5 Mon Sep 17 00:00:00 2001 From: Jeremy Lindblom Date: Mon, 18 May 2015 14:30:58 -0700 Subject: [PATCH] Removing S3's old Signature in favor of using Signature Version 4 exclusively and making S3 more regionalized --- docs/guide/configuration.rst | 8 +- src/Api/Parser/AbstractParser.php | 16 +- src/Api/Parser/XmlParser.php | 2 +- src/AwsClient.php | 4 +- src/ClientResolver.php | 4 +- src/DynamoDb/DynamoDbClient.php | 1 + src/S3/ApplyMd5Middleware.php | 47 +-- ...eware.php => BucketEndpointMiddleware.php} | 33 +- src/S3/GetBucketLocationParser.php | 42 +++ src/S3/MultipartUploader.php | 41 +-- src/S3/S3Client.php | 138 ++++---- src/S3/SSECMiddleware.php | 1 + src/Signature/S3Signature.php | 203 ----------- src/Signature/SignatureProvider.php | 3 - tests/ClientResolverTest.php | 8 +- tests/FunctionsTest.php | 4 +- tests/Integ/ClientSmokeTest.php | 5 +- tests/Integ/S3SignatureTest.php | 42 --- tests/S3/ApplyMd5MiddlewareTest.php | 55 +-- tests/S3/BucketEndpointMiddlewareTest.php | 61 ++++ tests/S3/BucketStyleMiddlewareTest.php | 113 ------- tests/S3/GetBucketLocationParserTest.php | 44 +++ tests/S3/MultipartUploaderTest.php | 9 +- tests/S3/S3ClientTest.php | 82 +++-- tests/S3/StreamWrapperTest.php | 26 +- tests/Signature/S3SignatureTest.php | 316 ------------------ tests/Signature/SignatureProviderTest.php | 1 - 27 files changed, 326 insertions(+), 983 deletions(-) rename src/S3/{BucketStyleMiddleware.php => BucketEndpointMiddleware.php} (54%) create mode 100644 src/S3/GetBucketLocationParser.php delete mode 100644 src/Signature/S3Signature.php delete mode 100644 tests/Integ/S3SignatureTest.php create mode 100644 tests/S3/BucketEndpointMiddlewareTest.php delete mode 100644 tests/S3/BucketStyleMiddlewareTest.php create mode 100644 tests/S3/GetBucketLocationParserTest.php delete mode 100644 tests/Signature/S3SignatureTest.php diff --git a/docs/guide/configuration.rst b/docs/guide/configuration.rst index 306024e0b3..f1d6a09b66 100644 --- a/docs/guide/configuration.rst +++ b/docs/guide/configuration.rst @@ -681,7 +681,7 @@ signature_provider :Type: ``callable`` -A callable that accepts a signature version name (e.g., ``v4``, ``s3``), a +A callable that accepts a signature version name (e.g., ``v4``), a service name, and region, and returns a ``Aws\Signature\SignatureInterface`` object or ``NULL`` if the provider is able to create a signer for the given parameters. This provider is used to create signers utilized by the client. @@ -697,8 +697,8 @@ signature_version :Type: ``string`` A string representing a custom signature version to use with a service -(e.g., ``v4``, ``s3``, etc.). Per/operation signature version MAY -override this requested signature version if needed. +(e.g., ``v4``, etc.). Per/operation signature version MAY override this +requested signature version if needed. The following examples show how to configure an Amazon S3 client to use `signature version 4 `_: @@ -716,7 +716,7 @@ The following examples show how to configure an Amazon S3 client to use The ``signature_provider`` used by your client MUST be able to create the ``signature_version`` option you provide. The default ``signature_provider`` - used by the SDK can create signature objects for "v4" and "s3" + used by the SDK can create signature objects for "v4" and "anonymous" signature versions. diff --git a/src/Api/Parser/AbstractParser.php b/src/Api/Parser/AbstractParser.php index a5e42e830f..59190231a5 100644 --- a/src/Api/Parser/AbstractParser.php +++ b/src/Api/Parser/AbstractParser.php @@ -2,6 +2,9 @@ namespace Aws\Api\Parser; use Aws\Api\Service; +use Aws\CommandInterface; +use Aws\ResultInterface; +use Psr\Http\Message\ResponseInterface; /** * @internal @@ -12,10 +15,21 @@ abstract class AbstractParser protected $api; /** - * @param Service $api Service description + * @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 + ); } diff --git a/src/Api/Parser/XmlParser.php b/src/Api/Parser/XmlParser.php index 8b21074c94..c9590e7b91 100644 --- a/src/Api/Parser/XmlParser.php +++ b/src/Api/Parser/XmlParser.php @@ -39,7 +39,7 @@ private function dispatch($shape, \SimpleXMLElement $value) private function parse_structure( StructureShape $shape, - \SimpleXMLElement $value + \SimpleXMLElement $value ) { $target = []; diff --git a/src/AwsClient.php b/src/AwsClient.php index 6971393451..09ae5f82a4 100644 --- a/src/AwsClient.php +++ b/src/AwsClient.php @@ -104,12 +104,12 @@ public static function getArguments() * 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", "s3"), a service name, and region, and + * 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, s3). Note that + * signature version to use with a service (e.g., v4). Note that * per/operation signature version MAY override this requested signature * version. * - validate: (bool, default=bool(true)) Set to false to disable diff --git a/src/ClientResolver.php b/src/ClientResolver.php index 82af5674e2..055ce6313d 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -72,7 +72,7 @@ class ClientResolver 'signature_provider' => [ 'type' => 'value', 'valid' => ['callable'], - 'doc' => 'A callable that accepts a signature version name (e.g., "v4", "s3"), 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', + 'doc' => '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', 'default' => [__CLASS__, '_default_signature_provider'], ], 'endpoint_provider' => [ @@ -92,7 +92,7 @@ class ClientResolver 'signature_version' => [ 'type' => 'config', 'valid' => ['string'], - 'doc' => 'A string representing a custom signature version to use with a service (e.g., v4, s3). Note that per/operation signature version MAY override this requested signature version.', + 'doc' => '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.', 'default' => [__CLASS__, '_default_signature_version'], ], 'profile' => [ diff --git a/src/DynamoDb/DynamoDbClient.php b/src/DynamoDb/DynamoDbClient.php index 8afc28f052..0b09a0966e 100644 --- a/src/DynamoDb/DynamoDbClient.php +++ b/src/DynamoDb/DynamoDbClient.php @@ -59,6 +59,7 @@ function ($retries) { ); } + /** @internal */ public static function _applyApiProvider($value, array &$args, HandlerList $list) { ClientResolver::_apply_api_provider($value, $args, $list); diff --git a/src/S3/ApplyMd5Middleware.php b/src/S3/ApplyMd5Middleware.php index 19e36a4788..843cdeb81b 100644 --- a/src/S3/ApplyMd5Middleware.php +++ b/src/S3/ApplyMd5Middleware.php @@ -19,34 +19,27 @@ class ApplyMd5Middleware 'DeleteObjects', 'PutBucketCors', 'PutBucketLifecycle', - ]; - - private static $canMd5 = [ - 'PutObject', - 'UploadPart' + 'PutBucketPolicy', + 'PutBucketTagging', ]; private $nextHandler; - private $byDefault; /** * Create a middleware wrapper function. * - * @param bool $calculateMd5 Set to true to calculate optional MD5 hashes. - * * @return callable */ - public static function wrap($calculateMd5) + public static function wrap() { - return function (callable $handler) use ($calculateMd5) { - return new self($calculateMd5, $handler); + return function (callable $handler) { + return new self($handler); }; } - public function __construct($calculateMd5, callable $nextHandler) + public function __construct(callable $nextHandler) { $this->nextHandler = $nextHandler; - $this->byDefault = $calculateMd5; } public function __invoke( @@ -54,34 +47,18 @@ public function __invoke( RequestInterface $request ) { $name = $command->getName(); - + $body = $request->getBody(); if (!$request->hasHeader('Content-MD5') - && $request->getBody()->getSize() + && $body->getSize() + && in_array($name, self::$requireMd5) ) { - $request = $this->addMd5($name, $request); - } - - $next = $this->nextHandler; - return $next($command, $request); - } - - private function addMd5($name, RequestInterface $request) - { - // If and MD5 is required or enabled, add one. - $optional = $this->byDefault && in_array($name, self::$canMd5); - - if (in_array($name, self::$requireMd5) || $optional) { - $body = $request->getBody(); - // Throw exception is calculating and MD5 would result in an error. - if (!$body->isSeekable()) { - throw new CouldNotCreateChecksumException('md5'); - } - return $request->withHeader( + $request = $request->withHeader( 'Content-MD5', base64_encode(Psr7\hash($body, 'md5', true)) ); } - return $request; + $next = $this->nextHandler; + return $next($command, $request); } } diff --git a/src/S3/BucketStyleMiddleware.php b/src/S3/BucketEndpointMiddleware.php similarity index 54% rename from src/S3/BucketStyleMiddleware.php rename to src/S3/BucketEndpointMiddleware.php index 374bdf1321..2e74b5f40e 100644 --- a/src/S3/BucketStyleMiddleware.php +++ b/src/S3/BucketEndpointMiddleware.php @@ -5,38 +5,32 @@ use Psr\Http\Message\RequestInterface; /** - * Used to change the style in which buckets are inserted in to the URL - * (path or virtual style) based on the context. + * Used to update the host used for S3 requests in the case of using a + * "bucket endpoint" or CNAME bucket. * * IMPORTANT: this middleware must be added after the "build" step. * * @internal */ -class BucketStyleMiddleware +class BucketEndpointMiddleware { private static $exclusions = ['GetBucketLocation' => true]; - private $bucketEndpoint; private $nextHandler; /** * Create a middleware wrapper function. * - * @param bool $bucketEndpoint Set to true to send requests to a bucket - * specific endpoint and not inject a bucket - * in the request host or path. - * * @return callable */ - public static function wrap($bucketEndpoint = false) + public static function wrap() { - return function (callable $handler) use ($bucketEndpoint) { - return new self($bucketEndpoint, $handler); + return function (callable $handler) { + return new self($handler); }; } - public function __construct($bucketEndpoint, callable $nextHandler) + public function __construct(callable $nextHandler) { - $this->bucketEndpoint = $bucketEndpoint; $this->nextHandler = $nextHandler; } @@ -69,18 +63,7 @@ private function modifyRequest( $uri = $request->getUri(); $path = $uri->getPath(); $bucket = $command['Bucket']; - - if ($this->bucketEndpoint) { - $path = $this->removeBucketFromPath($path, $bucket); - } elseif (S3Client::isBucketDnsCompatible($bucket) - && !($uri->getScheme() == 'https' && strpos($bucket, '.')) - ) { - // Switch to virtual if not a DNS compatible bucket name, or the - // scheme is https and there are no dots in the host header - // (avoids SSL issues). - $uri = $uri->withHost($bucket . '.' . $uri->getHost()); - $path = $this->removeBucketFromPath($path, $bucket); - } + $path = $this->removeBucketFromPath($path, $bucket); // Modify the Key to make sure the key is encoded, but slashes are not. if ($command['Key']) { diff --git a/src/S3/GetBucketLocationParser.php b/src/S3/GetBucketLocationParser.php new file mode 100644 index 0000000000..1f739d03f5 --- /dev/null +++ b/src/S3/GetBucketLocationParser.php @@ -0,0 +1,42 @@ +parser = $parser; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + $fn = $this->parser; + $result = $fn($command, $response); + + if ($command->getName() === 'GetBucketLocation') { + $location = 'us-east-1'; + if (preg_match('/>(.+?)<\/LocationConstraint>/', $response->getBody(), $matches)) { + $location = $matches[1] === 'EU' ? 'eu-west-1' : $matches[1]; + } + $result['LocationConstraint'] = $location; + } + + return $result; + } +} diff --git a/src/S3/MultipartUploader.php b/src/S3/MultipartUploader.php index e1f8a1877c..fae020d5b7 100644 --- a/src/S3/MultipartUploader.php +++ b/src/S3/MultipartUploader.php @@ -154,11 +154,7 @@ protected function createPart($seekable, $number) } else { // Case 2: Stream is not seekable; must store in temp stream. $source = $this->limitPartStream($this->source); - $source = $this->decorateWithHashes($source, - function ($result, $type) use (&$data) { - $data['Content' . strtoupper($type)] = $result; - } - ); + $source = $this->decorateWithHashes($source, $data); $body = Psr7\stream_for(); Psr7\copy_to_stream($source, $body); $data['ContentLength'] = $body->getSize(); @@ -208,38 +204,19 @@ protected function getCompleteParams() } /** - * Decorates a stream with a md5/sha256 linear hashing stream if needed. - * - * S3 does not typically require content hashes (unless using Signature V4), - * but they can be used to ensure the message integrity of the upload. - * When using non-seekable/remote streams, we must do the work of reading - * through the body to calculate parts. In this case, we can wrap the parts' - * body streams with a hashing stream decorator to calculate the hashes at - * the same time, instead of having to buffer the stream to disk and re-read - * the stream later. + * Decorates a stream with a sha256 linear hashing stream. * - * @param Stream $stream Stream to decorate. - * @param callable $complete Callback to execute for the hash result. + * @param Stream $stream Stream to decorate. + * @param array $data Part data to augment with the hash result. * * @return Stream */ - private function decorateWithHashes(Stream $stream, callable $complete) + private function decorateWithHashes(Stream $stream, array &$data) { - // Determine if the checksum needs to be calculated. - if ($this->client->getConfig('signature_version') == 'v4') { - $type = 'sha256'; - } elseif ($this->client->getConfig('calculate_md5')) { - $type = 'md5'; - } else { - return $stream; - } - // Decorate source with a hashing stream - $hash = new PhpHash($type, ['base64' => true]); - return new HashingStream($stream, $hash, - function ($result) use ($type, $complete) { - return $complete($result, $type); - } - ); + $hash = new PhpHash('sha256', ['base64' => true]); + return new HashingStream($stream, $hash, function ($result) use (&$data) { + $data['ContentSHA256'] = $result; + }); } } diff --git a/src/S3/S3Client.php b/src/S3/S3Client.php index fc6a6b7832..d20daa9857 100644 --- a/src/S3/S3Client.php +++ b/src/S3/S3Client.php @@ -25,32 +25,11 @@ class S3Client extends AwsClient public static function getArguments() { $args = parent::getArguments(); - // Apply custom retry strategy. $args['retries']['fn'] = [__CLASS__, '_applyRetryConfig']; - - // Show information about the "us-standard" region in the error message - $args['region']['required'] = function (array $args) { - $base = ClientResolver::_missing_region($args); - return "{$base}\nUse the 'us-standard' or 'us-east-1' region to " - . "send requests to the global\nAmazon S3 endpoint, which is " - . "generally able to send requests to any region\n(though you " - . "may need to use a region specific endpoint when sending " - . "requests\nover https to buckets with dots in them or " - . "contacting signature version\n4 only regions."; - }; + $args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider']; + $args['signature_version']['default'] = 'v4'; return $args + [ - 'calculate_md5' => [ - 'type' => 'config', - 'valid' => ['bool'], - 'doc' => 'Set to false to disable calculating an MD5 for ' - . 'all Amazon S3 signed uploads.', - 'default' => function (array &$args) { - // S3Client should calculate MD5 checksums for uploads - // unless explicitly disabled or using a v4 signer. - return $args['config']['signature_version'] != 'v4'; - }, - ], 'bucket_endpoint' => [ 'type' => 'config', 'valid' => ['bool'], @@ -59,18 +38,6 @@ public static function getArguments() . 'result of injecting the bucket into the URL. This ' . 'option is useful for interacting with CNAME endpoints.', ], - 'force_path_style' => [ - 'type' => 'config', - 'valid' => ['bool'], - 'doc' => 'Set to true to send requests using path style ' - . 'bucket addressing (e.g., ' - . 'https://s3.amazonaws.com/bucket/key). All requests sent ' - . 'to region specific endpoints will use path style by ' - . 'default. This option is only relevant when you wish to ' - . 'use the us-standard or us-east-1 region AND force a ' - . 'path style request. This option has no effect when ' - . 'using a "bucket_endpoint".' - ], ]; } @@ -87,64 +54,29 @@ public static function getArguments() * interacting with CNAME endpoints. * - calculate_md5: (bool) Set to false to disable calculating an MD5 * for all Amazon S3 signed uploads. - * - force_path_style: (bool) Set to true to send requests using path - * style bucket addressing (e.g., https://s3.amazonaws.com/bucket/key). - * All requests sent to region specific endpoints will use path style by - * default. This option is only relevant when you wish to use the - * us-standard or us-east-1 region AND force a path style request. This - * option has no effect when using a "bucket_endpoint". - * - * The S3Client requires that a region is provided. You can provide the - * "us-standard" or "us-east-1" region to use a custom region that connects - * to the global endpoint (s3.amazonaws.com). This region should be used - * when you do not know the actual region of the bucket or wish to attempt - * to use the same client to connect to multiple buckets. Using the - * "us-standard" region may encounter errors in specific cases. For example, - * we will attempt to place the bucket in the host header so that the - * us-standard region works with buckets created for a specific region. - * However, if the bucket contains dots ".", then the bucket must remain in - * the path of the URI to ensure that SSL certificate verification does not - * fail. In this case, you may receive a 301 redirect which will require - * you to use a client with a specific region setting. You may also be - * required to provide a region other than us-standard or us-east-1 when - * the bucket is in a signature version 4 only region. * * @param array $args */ public function __construct(array $args) { - $region = isset($args['region']) ? $args['region'] : null; - $standard = ($region == 'us-standard' || $region == 'us-east-1'); - // Rewrite us-standard to us-east-1 so that we can sign correctly. - if ($standard) { - $args['region'] = 'us-east-1'; - } - parent::__construct($args); $stack = $this->getHandlerList(); $stack->appendInit(SSECMiddleware::wrap($this->getEndpoint()->getScheme()), 's3.ssec'); - $stack->appendBuild(ApplyMd5Middleware::wrap($this->getConfig('calculate_md5')), 's3.md5'); + $stack->appendBuild(ApplyMd5Middleware::wrap(), 's3.md5'); $stack->appendBuild( Middleware::contentType(['PutObject', 'UploadPart']), 's3.content_type' ); - // Use the bucket style middleware when using a "bucket_endpoint" or - // to move the bucket to the host if using us-standard and - // force_path_style is false. - $bucketEndpoint = $this->getConfig('bucket_endpoint'); - if ($bucketEndpoint - || ($standard && !$this->getConfig('force_path_style')) - ) { - $stack->appendBuild( - BucketStyleMiddleware::wrap($bucketEndpoint), - 's3.bucket_style' - ); + // Use the bucket style middleware when using a "bucket_endpoint" (for cnames) + if ($this->getConfig('bucket_endpoint')) { + $stack->appendBuild(BucketEndpointMiddleware::wrap(), 's3.bucket_endpoint'); } $stack->appendSign(PutObjectUrlMiddleware::wrap(), 's3.put_object_url'); $stack->appendSign(PermanentRedirectMiddleware::wrap(), 's3.permanent_redirect'); $stack->appendInit(Middleware::sourceFile($this->getApi()), 's3.source_file'); + $stack->appendInit($this->getLocationConstraintMiddleware(), 's3.location'); } /** @@ -490,6 +422,29 @@ private function checkExistenceWithCommand(CommandInterface $command) } } + /** + * Provides a middleware that removes the need to specify LocationConstraint on CreateBucket. + * + * @return \Closure + */ + private function getLocationConstraintMiddleware() + { + return function (callable $handler) { + return function ($command, $request = null) use ($handler) { + if ($command->getName() === 'CreateBucket') { + $region = $this->getRegion(); + if ($region === 'us-east-1') { + unset($command['CreateBucketConfiguration']); + } else { + $command['CreateBucketConfiguration'] = ['LocationConstraint' => $region]; + } + } + + return $handler($command, $request); + }; + }; + } + /** @internal */ public static function _applyRetryConfig($value, $_, HandlerList $list) { @@ -515,6 +470,13 @@ public static function _applyRetryConfig($value, $_, HandlerList $list) $list->appendSign(Middleware::retry($decider, $delay), 'retry'); } + /** @internal */ + public static function _applyApiProvider($value, array &$args, HandlerList $list) + { + ClientResolver::_apply_api_provider($value, $args, $list); + $args['parser'] = new GetBucketLocationParser($args['parser']); + } + /** * @internal * @codeCoverageIgnore @@ -533,9 +495,8 @@ public static function applyDocFilters(array $api, array $docs) // Several SSECustomerKey documentation updates. $docs['shapes']['SSECustomerKey']['append'] = $b64; $docs['shapes']['CopySourceSSECustomerKey']['append'] = $b64; - $docs['shapes']['SSECustomerKeyMd5']['append'] = - '
The value will be computed on your ' - . 'behalf if it is not supplied.
'; + $docs['shapes']['SSECustomerKeyMd5']['append'] = '
The value will be computed on ' + . 'your behalf if it is not supplied.
'; // Add the ObjectURL to various output shapes and documentation. $objectUrl = 'The URI of the created object.'; @@ -545,11 +506,24 @@ public static function applyDocFilters(array $api, array $docs) $api['shapes']['CompleteMultipartUploadOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL']; $docs['shapes']['ObjectURL']['base'] = $objectUrl; + // Fix references to Location Constraint. + unset($api['shapes']['CreateBucketRequest']['payload']); + $api['shapes']['BucketLocationConstraint']['enum'] = [ + "ap-northeast-1", + "ap-southeast-2", + "ap-southeast-1", + "cn-north-1", + "eu-central-1", + "eu-west-1", + "us-east-1", + "us-west-1", + "us-west-2", + "sa-east-1", + ]; + // Add a note that the ContentMD5 is optional. - $docs['shapes']['ContentMD5']['append'] = - '
The SDK will compute this value ' - . 'for you on your behalf if it is required or if you are using ' - . 'the "s3" signature version.
'; + $docs['shapes']['ContentMD5']['append'] = '
The value will be computed on ' + . 'your behalf.
'; return [ new Service($api, ApiProvider::defaultProvider()), diff --git a/src/S3/SSECMiddleware.php b/src/S3/SSECMiddleware.php index 94ece59323..9435a209dd 100644 --- a/src/S3/SSECMiddleware.php +++ b/src/S3/SSECMiddleware.php @@ -6,6 +6,7 @@ /** * Simplifies the SSE-C process by encoding and hashing the key. + * @internal */ class SSECMiddleware { diff --git a/src/Signature/S3Signature.php b/src/Signature/S3Signature.php deleted file mode 100644 index 3a75901a94..0000000000 --- a/src/Signature/S3Signature.php +++ /dev/null @@ -1,203 +0,0 @@ -parser = new S3UriParser(); - // Ensure that the signable query string parameters are sorted - sort($this->signableQueryString); - } - - public function signRequest( - RequestInterface $request, - CredentialsInterface $credentials - ) { - $request = $this->prepareRequest($request, $credentials); - $stringToSign = $this->createCanonicalizedString($request); - $auth = 'AWS ' - . $credentials->getAccessKeyId() . ':' - . $this->signString($stringToSign, $credentials); - - return $request->withHeader('Authorization', $auth); - } - - public function presign( - RequestInterface $request, - CredentialsInterface $credentials, - $expires - ) { - $query = []; - // URL encoding already occurs in the URI template expansion. Undo that - // and encode using the same encoding as GET object, PUT object, etc. - $uri = $request->getUri(); - $path = S3Client::encodeKey(rawurldecode($uri->getPath())); - $request = $request->withUri($uri->withPath($path)); - - // Make sure to handle temporary credentials - if ($token = $credentials->getSecurityToken()) { - $request = $request->withHeader('X-Amz-Security-Token', $token); - $query['X-Amz-Security-Token'] = $token; - } - - if ($expires instanceof \DateTime) { - $expires = $expires->getTimestamp(); - } elseif (!is_numeric($expires)) { - $expires = strtotime($expires); - } - - // Set query params required for pre-signed URLs - $query['AWSAccessKeyId'] = $credentials->getAccessKeyId(); - $query['Expires'] = $expires; - $query['Signature'] = $this->signString( - $this->createCanonicalizedString($request, $expires), - $credentials - ); - - // Move X-Amz-* headers to the query string - foreach ($request->getHeaders() as $name => $header) { - $name = strtolower($name); - if (strpos($name, 'x-amz-') === 0) { - $query[$name] = implode(',', $header); - } - } - - $queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986); - - return $request->withUri($request->getUri()->withQuery($queryString)); - } - - /** - * @param RequestInterface $request - * @param CredentialsInterface $creds - * - * @return RequestInterface - */ - private function prepareRequest( - RequestInterface $request, - CredentialsInterface $creds - ) { - $modify = [ - 'remove_headers' => ['X-Amz-Date'], - 'set_headers' => ['Date' => gmdate(\DateTime::RFC2822)] - ]; - - // Add the security token header if one is being used by the credentials - if ($token = $creds->getSecurityToken()) { - $modify['set_headers']['X-Amz-Security-Token'] = $token; - } - - return Psr7\modify_request($request, $modify); - } - - private function signString($string, CredentialsInterface $credentials) - { - return base64_encode( - hash_hmac('sha1', $string, $credentials->getSecretKey(), true) - ); - } - - private function createCanonicalizedString( - RequestInterface $request, - $expires = null - ) { - $buffer = $request->getMethod() . "\n"; - - // Add the interesting headers - foreach ($this->signableHeaders as $header) { - $buffer .= $request->getHeaderLine($header) . "\n"; - } - - $date = $expires ?: $request->getHeaderLine('date'); - $buffer .= "{$date}\n" - . $this->createCanonicalizedAmzHeaders($request) - . $this->createCanonicalizedResource($request); - - return $buffer; - } - - private function createCanonicalizedAmzHeaders(RequestInterface $request) - { - $headers = []; - foreach ($request->getHeaders() as $name => $header) { - $name = strtolower($name); - if (strpos($name, 'x-amz-') === 0) { - $value = implode(',', $header); - if (strlen($value) > 0) { - $headers[$name] = $name . ':' . $value; - } - } - } - - if (!$headers) { - return ''; - } - - ksort($headers); - - return implode("\n", $headers) . "\n"; - } - - private function createCanonicalizedResource(RequestInterface $request) - { - $data = $this->parser->parse($request->getUri()); - $buffer = '/'; - - if ($data['bucket']) { - $buffer .= $data['bucket']; - if (!empty($data['key']) || !$data['path_style']) { - $buffer .= '/' . $data['key']; - } - } - - // Add sub resource parameters if present. - $query = $request->getUri()->getQuery(); - - if ($query) { - $params = Psr7\parse_query($query); - $first = true; - foreach ($this->signableQueryString as $key) { - if (array_key_exists($key, $params)) { - $value = $params[$key]; - $buffer .= $first ? '?' : '&'; - $first = false; - $buffer .= $key; - // Don't add values for empty sub-resources - if (strlen($value)) { - $buffer .= "={$value}"; - } - } - } - } - - return $buffer; - } -} diff --git a/src/Signature/SignatureProvider.php b/src/Signature/SignatureProvider.php index 2e4bf0719e..ed4143eb45 100644 --- a/src/Signature/SignatureProvider.php +++ b/src/Signature/SignatureProvider.php @@ -101,7 +101,6 @@ public static function memoize(callable $provider) * This provider currently recognizes the following signature versions: * * - v4: Signature version 4. - * - s3: Amazon S3 specific signature. * - anonymous: Does not sign requests. * * @return callable @@ -114,8 +113,6 @@ public static function version() return $service === 's3' ? new S3SignatureV4($service, $region) : new SignatureV4($service, $region); - case 's3': - return new S3Signature(); case 'anonymous': return new AnonymousSignature(); default: diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index 0cfcacb268..8c9f84fe53 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -328,11 +328,11 @@ public function testCanAddHttpClientDefaultOptions() public function testCanAddConfigOptions() { $c = new S3Client([ - 'version' => 'latest', - 'region' => 'us-west-2', - 'calculate_md5' => true + 'version' => 'latest', + 'region' => 'us-west-2', + 'bucket_endpoint' => true, ]); - $this->assertTrue($c->getConfig('calculate_md5')); + $this->assertTrue($c->getConfig('bucket_endpoint')); } public function testSkipsNonRequiredKeys() diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index f19ded5f42..b5c180e1ce 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -108,9 +108,9 @@ public function testSerializesHttpRequests() 'Body' => '123' ]); $request = \Aws\serialize($command); - $this->assertEquals('/bar', $request->getRequestTarget()); + $this->assertEquals('/foo/bar', $request->getRequestTarget()); $this->assertEquals('PUT', $request->getMethod()); - $this->assertEquals('foo.s3.amazonaws.com', $request->getHeaderLine('Host')); + $this->assertEquals('s3.amazonaws.com', $request->getHeaderLine('Host')); $this->assertTrue($request->hasHeader('Authorization')); $this->assertTrue($request->hasHeader('X-Amz-Content-Sha256')); $this->assertTrue($request->hasHeader('X-Amz-Date')); diff --git a/tests/Integ/ClientSmokeTest.php b/tests/Integ/ClientSmokeTest.php index ea5dfc52c3..7aa70a8436 100644 --- a/tests/Integ/ClientSmokeTest.php +++ b/tests/Integ/ClientSmokeTest.php @@ -37,8 +37,7 @@ public function testBasicOperationWorks($service, $class, $options, // Setup event to get the request's host value. $host = null; - $client->getHandlerList()->append( - 'sign:integ', + $client->getHandlerList()->appendSign( Middleware::tap(function ($command, RequestInterface $request) use (&$host) { $host = $request->getUri()->getHost(); } @@ -468,7 +467,7 @@ class (expected class name of instantiated client) 's3', 'Aws\\S3\\S3Client', [], - 't0tally-1nval1d-8uck3t-nam3.s3.amazonaws.com', + 's3.amazonaws.com', 'ListObjects', ['Bucket' => 't0tally-1nval1d-8uck3t-nam3'], false, diff --git a/tests/Integ/S3SignatureTest.php b/tests/Integ/S3SignatureTest.php deleted file mode 100644 index 9a822f9c61..0000000000 --- a/tests/Integ/S3SignatureTest.php +++ /dev/null @@ -1,42 +0,0 @@ - uniqid('notthere'), 'PathStyle' => true]], - [['Version' => 'foo', 'Bucket' => uniqid('notthere'), 'PathStyle' => true]], - [['Bucket' => uniqid('notthere'), 'Key' => 'foo', 'PathStyle' => true]], - [['Bucket' => uniqid('notthere')]], - [['Version' => 'foo', 'Bucket' => uniqid('notthere')]], - [['Bucket' => uniqid('notthere'), 'Key' => 'foo', 'PathStyle' => true]], - ]; - } - - /** - * @dataProvider signProvider - */ - public function testSignsS3Requests($args) - { - $s3 = $this->getSdk()->createClient('s3', ['region' => 'us-east-1']); - $command = $s3->getCommand('HeadBucket', $args); - $this->ensureNot403($command, $s3); - } - - private function ensureNot403(CommandInterface $command, S3Client $client) - { - try { - $client->execute($command); - } catch (AwsException $e) { - $this->assertNotEquals(403, $e->getResponse()->getStatusCode()); - } - } -} diff --git a/tests/S3/ApplyMd5MiddlewareTest.php b/tests/S3/ApplyMd5MiddlewareTest.php index dd0193c0d7..d975e92a64 100644 --- a/tests/S3/ApplyMd5MiddlewareTest.php +++ b/tests/S3/ApplyMd5MiddlewareTest.php @@ -13,20 +13,6 @@ class ApplyMd5MiddlewareTest extends \PHPUnit_Framework_TestCase { use UsesServiceTrait; - /** - * @expectedException \Aws\Exception\CouldNotCreateChecksumException - */ - public function testThrowsExceptionIfBodyIsNotSeekable() - { - $s3 = $this->getTestClient('s3'); - $command = $s3->getCommand('PutObject', [ - 'Bucket' => 'foo', - 'Key' => 'bar', - 'Body' => new Psr7\NoSeekStream(Psr7\stream_for('foo')), - ]); - $s3->execute($command); - } - /** * @dataProvider getContentMd5UseCases */ @@ -38,9 +24,7 @@ public function testAddsContentMd5AsAppropriate($options, $operation, $args, $md $command->getHandlerList()->appendBuild( Middleware::tap(function ($cmd, RequestInterface $request) use ($md5Added, $md5Value) { $this->assertSame($md5Added, $request->hasHeader('Content-MD5')); - if ($md5Value !== 'SKIP') { - $this->assertEquals($md5Value, $request->getHeaderLine('Content-MD5')); - } + $this->assertEquals($md5Value, $request->getHeaderLine('Content-MD5')); }) ); $s3->execute($command); @@ -48,15 +32,14 @@ public function testAddsContentMd5AsAppropriate($options, $operation, $args, $md public function getContentMd5UseCases() { - $args = ['Bucket' => 'foo', 'Key' => 'bar']; - $md5 = base64_encode(md5('baz', true)); + $md5 = '/12roh/ATpPMcGD9Rj4ZlQ=='; return [ - // Do nothing if Content MD% was explicitly provided. + // Do nothing if Content MD5 was explicitly provided. [ [], - 'PutObject', - $args + ['ContentMD5' => $md5], + 'DeleteObjects', + ['Bucket' => 'foo', 'Delete' => ['Objects' => [['Key' => 'bar']]]], true, $md5 ], @@ -66,37 +49,13 @@ public function getContentMd5UseCases() 'DeleteObjects', ['Bucket' => 'foo', 'Delete' => ['Objects' => [['Key' => 'bar']]]], true, - 'SKIP' - ], - // Gets added for upload operations by default - [ - [], - 'PutObject', - $args + ['Body' => 'baz'], - true, - $md5, - ], - // Not added for upload operations when turned off at client level - [ - ['calculate_md5' => false], - 'PutObject', - $args + ['Body' => 'baz'], - false, - null, + $md5 ], // Not added for operations that does not require it [ [], 'GetObject', - $args, - false, - null, - ], - // Not added to upload operations when using SigV4 - [ - ['signature_version' => 'v4'], - 'PutObject', - $args + ['Body' => 'baz'], + ['Bucket' => 'foo', 'Key' => 'bar'], false, null, ], diff --git a/tests/S3/BucketEndpointMiddlewareTest.php b/tests/S3/BucketEndpointMiddlewareTest.php new file mode 100644 index 0000000000..56007a7456 --- /dev/null +++ b/tests/S3/BucketEndpointMiddlewareTest.php @@ -0,0 +1,61 @@ +getTestClient('s3'); + $this->addMockResults($s3, [[]]); + $command = $s3->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'Bar/Baz']); + $command->getHandlerList()->appendSign( + Middleware::tap(function ($cmd, $req) { + $this->assertEquals('s3.amazonaws.com', $req->getUri()->getHost()); + $this->assertEquals('/foo/Bar/Baz', $req->getRequestTarget()); + }) + ); + $s3->execute($command); + } + + public function testIgnoresExcludedCommands() + { + $s3 = $this->getTestClient('s3'); + $this->addMockResults($s3, [['bucket_endpoint' => true]]); + $command = $s3->getCommand('GetBucketLocation', ['Bucket' => 'foo']); + $command->getHandlerList()->appendSign( + Middleware::tap(function ($cmd, $req) { + $this->assertEquals('s3.amazonaws.com', $req->getUri()->getHost()); + $this->assertEquals('/foo?location', $req->getRequestTarget()); + }) + ); + $s3->execute($command); + } + + public function testRemovesBucketWhenBucketEndpoint() + { + $s3 = $this->getTestClient('s3', [ + 'endpoint' => 'http://test.domain.com', + 'bucket_endpoint' => true + ]); + $this->addMockResults($s3, [[]]); + $command = $s3->getCommand('GetObject', [ + 'Bucket' => 'test', + 'Key' => 'key' + ]); + $command->getHandlerList()->appendSign( + Middleware::tap(function ($cmd, $req) { + $this->assertEquals('test.domain.com', $req->getUri()->getHost()); + $this->assertEquals('/key', $req->getRequestTarget()); + }) + ); + $s3->execute($command); + } +} diff --git a/tests/S3/BucketStyleMiddlewareTest.php b/tests/S3/BucketStyleMiddlewareTest.php deleted file mode 100644 index 0f6b8f97d1..0000000000 --- a/tests/S3/BucketStyleMiddlewareTest.php +++ /dev/null @@ -1,113 +0,0 @@ -getTestClient('s3'); - $this->addMockResults($s3, [[]]); - $command = $s3->getCommand('GetObject', [ - 'Bucket' => 'test.123', - 'Key' => 'Bar' - ]); - $command->getHandlerList()->appendSign( - Middleware::tap(function ($cmd, RequestInterface $req) { - $this->assertEquals('s3.amazonaws.com', $req->getUri()->getHost()); - $this->assertEquals('/test.123/Bar', $req->getRequestTarget()); - }) - ); - $s3->execute($command); - } - - public function testUsesPathStyleWhenNotDnsCompatible() - { - $s3 = $this->getTestClient('s3'); - $this->addMockResults($s3, [[]]); - $command = $s3->getCommand('GetObject', [ - 'Bucket' => '_baz_!', - 'Key' => 'Bar' - ]); - $command->getHandlerList()->appendSign( - Middleware::tap(function ($cmd, $req) { - $this->assertEquals('s3.amazonaws.com', $req->getUri()->getHost()); - $this->assertEquals('/_baz_%21/Bar', $req->getRequestTarget()); - }) - ); - $s3->execute($command); - } - - public function testUsesPathStyleWhenForced() - { - $s3 = $this->getTestClient('s3', ['force_path_style' => true]); - $this->addMockResults($s3, [[]]); - $command = $s3->getCommand('GetObject', [ - 'Bucket' => 'foo', - 'Key' => 'Bar' - ]); - $command->getHandlerList()->appendSign( - Middleware::tap(function ($cmd, $req) { - $this->assertEquals('s3.amazonaws.com', $req->getUri()->getHost()); - $this->assertEquals('/foo/Bar', $req->getRequestTarget()); - }) - ); - $s3->execute($command); - } - - public function testUsesVirtualHostedWhenPossible() - { - $s3 = $this->getTestClient('s3'); - $this->addMockResults($s3, [[]]); - $command = $s3->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'Bar/Baz']); - $command->getHandlerList()->appendSign( - Middleware::tap(function ($cmd, $req) { - $this->assertEquals('foo.s3.amazonaws.com', $req->getUri()->getHost()); - $this->assertEquals('/Bar/Baz', $req->getRequestTarget()); - }) - ); - $s3->execute($command); - } - - public function testIgnoresExcludedCommands() - { - $s3 = $this->getTestClient('s3'); - $this->addMockResults($s3, [[]]); - $command = $s3->getCommand('GetBucketLocation', ['Bucket' => 'foo']); - $command->getHandlerList()->appendSign( - Middleware::tap(function ($cmd, $req) { - $this->assertEquals('s3.amazonaws.com', $req->getUri()->getHost()); - $this->assertEquals('/foo?location', $req->getRequestTarget()); - }) - ); - $s3->execute($command); - } - - public function testRemovesBucketWhenBucketEndpoint() - { - $s3 = $this->getTestClient('s3', [ - 'endpoint' => 'http://test.domain.com', - 'bucket_endpoint' => true - ]); - $this->addMockResults($s3, [[]]); - $command = $s3->getCommand('GetObject', [ - 'Bucket' => 'test', - 'Key' => 'key' - ]); - $command->getHandlerList()->appendSign( - Middleware::tap(function ($cmd, $req) { - $this->assertEquals('test.domain.com', $req->getUri()->getHost()); - $this->assertEquals('/key', $req->getRequestTarget()); - }) - ); - $s3->execute($command); - } -} diff --git a/tests/S3/GetBucketLocationParserTest.php b/tests/S3/GetBucketLocationParserTest.php new file mode 100644 index 0000000000..aefdaa5b25 --- /dev/null +++ b/tests/S3/GetBucketLocationParserTest.php @@ -0,0 +1,44 @@ +assertEquals($expectedValue, $result['LocationConstraint']); + } + + public function getTestCases() + { + return [ + ['GetBucketLocation', 'us-west-2', 'us-west-2'], + ['GetBucketLocation', 'EU', 'eu-west-1'], + ['GetBucketLocation', '', 'us-east-1'], + ['GetBucket', 'us-west-2', null], + ]; + } +} diff --git a/tests/S3/MultipartUploaderTest.php b/tests/S3/MultipartUploaderTest.php index f1cf76e2f5..c119bbfe06 100644 --- a/tests/S3/MultipartUploaderTest.php +++ b/tests/S3/MultipartUploaderTest.php @@ -69,13 +69,8 @@ public function getTestCases() ['acl' => 'private'] + $defaults, Psr7\stream_for(fopen($filename, 'r')) ], - [ // Non-seekable stream, with SigV4 - ['signature_version' => 'v4'], - $defaults, - Psr7\stream_for($data) - ], - [ // Non-seekable stream, no MD5 hash - ['calculate_md5' => false], + [ // Non-seekable stream + [], $defaults, Psr7\stream_for($data) ], diff --git a/tests/S3/S3ClientTest.php b/tests/S3/S3ClientTest.php index c92f67574b..28474f9f40 100644 --- a/tests/S3/S3ClientTest.php +++ b/tests/S3/S3ClientTest.php @@ -17,26 +17,6 @@ class S3ClientTest extends \PHPUnit_Framework_TestCase { use UsesServiceTrait; - public function testCanForcePathStyleOnAllOperations() - { - $mock = new MockHandler([new Result()]); - $c = new S3Client([ - 'region' => 'us-standard', - 'version' => 'latest', - 'force_path_style' => true, - 'handler' => $mock - ]); - $command = $c->getCommand('GetObject', [ - 'Bucket' => 'foo', - 'Key' => 'baz' - ]); - $c->execute($command); - $this->assertEquals( - 'https://s3.amazonaws.com/foo/baz', - (string) $mock->getLastRequest()->getUri() - ); - } - public function testCanUseBucketEndpoint() { $c = new S3Client([ @@ -51,16 +31,6 @@ public function testCanUseBucketEndpoint() ); } - public function testAddsMd5ToConfig() - { - $c = new S3Client([ - 'region' => 'us-standard', - 'version' => 'latest', - 'calculate_md5' => true - ]); - $this->assertTrue($c->getConfig('calculate_md5')); - } - public function bucketNameProvider() { return [ @@ -98,12 +68,10 @@ public function testCreatesPresignedRequests() ]); $command = $client->getCommand('GetObject', ['Bucket' => 'foo', 'Key' => 'bar']); $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); - $this->assertContains( - 'https://foo.s3.amazonaws.com/bar?AWSAccessKeyId=', - $url - ); - $this->assertContains('Expires=', $url); - $this->assertContains('Signature=', $url); + $this->assertStringStartsWith('https://s3.amazonaws.com/foo/bar?', $url); + $this->assertContains('X-Amz-Expires=', $url); + $this->assertContains('X-Amz-Credential=', $url); + $this->assertContains('X-Amz-Signature=', $url); } public function testCreatesPresignedUrlsWithSpecialCharacters() @@ -117,11 +85,11 @@ public function testCreatesPresignedUrlsWithSpecialCharacters() 'Bucket' => 'foobar test: abc', 'Key' => '+%.a' ]); - $url = (string) $client->createPresignedRequest($command, 1342138769)->getUri(); - $this->assertContains( - 'https://s3.amazonaws.com/foobar%20test%3A%20abc/%2B%25.a?AWSAccessKeyId=', - $url - ); + $url = $client->createPresignedRequest($command, 1342138769)->getUri(); + $this->assertEquals('/foobar%20test%3A%20abc/%2B%25.a', $url->getPath()); + $query = Psr7\parse_query($url->getQuery()); + $this->assertArrayHasKey('X-Amz-Credential', $query); + $this->assertArrayHasKey('X-Amz-Signature', $query); } public function testRegistersStreamWrapper() @@ -187,7 +155,7 @@ public function testReturnsObjectUrl() 'region' => 'us-east-1', 'credentials' => false ]); - $this->assertEquals('https://foo.s3.amazonaws.com/bar', $s3->getObjectUrl('foo', 'bar')); + $this->assertEquals('https://s3.amazonaws.com/foo/bar', $s3->getObjectUrl('foo', 'bar')); } public function testReturnsObjectUrlViaPath() @@ -215,7 +183,7 @@ public function testUploadHelperDoesCorrectOperation( $client = $this->getTestClient('S3'); $this->addMockResults($client, $mockedResults); $result = $client->upload('bucket', 'key', $body, 'private', $options); - $this->assertEquals('https://bucket.s3.amazonaws.com/key', $result['ObjectURL']); + $this->assertEquals('https://s3.amazonaws.com/bucket/key', $result['ObjectURL']); } /** @@ -265,7 +233,7 @@ public function getUploadTestCases() $putObject = new Result(); $initiate = new Result(['UploadId' => 'foo']); $putPart = new Result(['ETag' => 'bar']); - $complete = new Result(['Location' => 'https://bucket.s3.amazonaws.com/key']); + $complete = new Result(['Location' => 'https://s3.amazonaws.com/bucket/key']); return [ [ @@ -338,4 +306,30 @@ public function testProxiesToTransferObjectGet() $client = $this->getTestClient('S3'); $client->downloadBucket(__DIR__, 'test'); } + + /** + * @dataProvider getTestCasesForLocationConstraints + */ + public function testAddsLocationConstraintAutomatically($region, $command, $contains) + { + $client = $this->getTestClient('S3', ['region' => $region]); + $command = $client->getCommand($command, ['Bucket' => 'foo']); + + $text = "{$region}"; + $body = (string) \Aws\serialize($command)->getBody(); + if ($contains) { + $this->assertContains($text, $body); + } else { + $this->assertNotContains($text, $body); + } + } + + public function getTestCasesForLocationConstraints() + { + return [ + ['us-west-2', 'CreateBucket', true], + ['us-east-1', 'CreateBucket', false], + ['us-west-2', 'HeadBucket', false], + ]; + } } diff --git a/tests/S3/StreamWrapperTest.php b/tests/S3/StreamWrapperTest.php index dea8e13ec1..0b143617c9 100644 --- a/tests/S3/StreamWrapperTest.php +++ b/tests/S3/StreamWrapperTest.php @@ -236,8 +236,8 @@ public function testCanUnlinkFiles() $this->assertEquals(1, count($history)); $entries = $history->toArray(); $this->assertEquals('DELETE', $entries[0]['request']->getMethod()); - $this->assertEquals('/key', $entries[0]['request']->getUri()->getPath()); - $this->assertEquals('bucket.s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); + $this->assertEquals('/bucket/key', $entries[0]['request']->getUri()->getPath()); + $this->assertEquals('s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); } /** @@ -303,8 +303,8 @@ function ($cmd, $r) { return new S3Exception('404', $cmd); }, $this->assertEquals('HEAD', $entries[4]['request']->getMethod()); $this->assertEquals('PUT', $entries[1]['request']->getMethod()); - $this->assertEquals('/', $entries[1]['request']->getUri()->getPath()); - $this->assertEquals('bucket.s3.amazonaws.com', $entries[1]['request']->getUri()->getHost()); + $this->assertEquals('/bucket', $entries[1]['request']->getUri()->getPath()); + $this->assertEquals('s3.amazonaws.com', $entries[1]['request']->getUri()->getHost()); $this->assertEquals('public-read', (string) $entries[1]['request']->getHeaderLine('x-amz-acl')); $this->assertEquals('authenticated-read', (string) $entries[3]['request']->getHeaderLine('x-amz-acl')); $this->assertEquals('private', (string) $entries[5]['request']->getHeaderLine('x-amz-acl')); @@ -358,8 +358,8 @@ public function testCanDeleteBucketWithRmDir() $this->assertEquals(1, count($history)); $entries = $history->toArray(); $this->assertEquals('DELETE', $entries[0]['request']->getMethod()); - $this->assertEquals('/', $entries[0]['request']->getUri()->getPath()); - $this->assertEquals('bucket.s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); + $this->assertEquals('/bucket', $entries[0]['request']->getUri()->getPath()); + $this->assertEquals('s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); } public function rmdirProvider() @@ -404,7 +404,7 @@ public function testCanDeleteNestedFolderWithRmDir() $this->assertEquals('GET', $entries[0]['request']->getMethod()); $this->assertContains('prefix=bar%2F', $entries[0]['request']->getUri()->getQuery()); $this->assertEquals('DELETE', $entries[1]['request']->getMethod()); - $this->assertEquals('/bar/', $entries[1]['request']->getUri()->getPath()); + $this->assertEquals('/foo/bar/', $entries[1]['request']->getUri()->getPath()); } /** @@ -440,8 +440,8 @@ public function testCanRenameObjects() $entries = $history->toArray(); $this->assertEquals(2, count($entries)); $this->assertEquals('PUT', $entries[0]['request']->getMethod()); - $this->assertEquals('/new_key', $entries[0]['request']->getUri()->getPath()); - $this->assertEquals('other.s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); + $this->assertEquals('/other/new_key', $entries[0]['request']->getUri()->getPath()); + $this->assertEquals('s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); $this->assertEquals( '/bucket/key', $entries[0]['request']->getHeaderLine('x-amz-copy-source') @@ -451,8 +451,8 @@ public function testCanRenameObjects() $entries[0]['request']->getHeaderLine('x-amz-metadata-directive') ); $this->assertEquals('DELETE', $entries[1]['request']->getMethod()); - $this->assertEquals('/key', $entries[1]['request']->getUri()->getPath()); - $this->assertEquals('bucket.s3.amazonaws.com', $entries[1]['request']->getUri()->getHost()); + $this->assertEquals('/bucket/key', $entries[1]['request']->getUri()->getPath()); + $this->assertEquals('s3.amazonaws.com', $entries[1]['request']->getUri()->getHost()); } public function testCanRenameObjectsWithCustomSettings() @@ -471,8 +471,8 @@ public function testCanRenameObjectsWithCustomSettings() $entries = $history->toArray(); $this->assertEquals(2, count($entries)); $this->assertEquals('PUT', $entries[0]['request']->getMethod()); - $this->assertEquals('/new_key', $entries[0]['request']->getUri()->getPath()); - $this->assertEquals('other.s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); + $this->assertEquals('/other/new_key', $entries[0]['request']->getUri()->getPath()); + $this->assertEquals('s3.amazonaws.com', $entries[0]['request']->getUri()->getHost()); $this->assertEquals( '/bucket/key', $entries[0]['request']->getHeaderLine('x-amz-copy-source') diff --git a/tests/Signature/S3SignatureTest.php b/tests/Signature/S3SignatureTest.php deleted file mode 100644 index 03715fa6b4..0000000000 --- a/tests/Signature/S3SignatureTest.php +++ /dev/null @@ -1,316 +0,0 @@ - 'Baz', 'X-Amz-Meta-Boo' => 'bam'], - 'body' - ); - $result = $signature->signRequest($request, $creds); - $this->assertEquals('baz', $result->getHeaderLine('X-Amz-Security-Token')); - $this->assertTrue($result->hasHeader('Date')); - $this->assertFalse($result->hasHeader('X-Amz-Date')); - $this->assertTrue($result->hasHeader('Authorization')); - $this->assertContains('AWS foo:', $result->getHeaderLine('Authorization')); - } - - public function presignedUrlProvider() - { - return [ - [ - "GET /t1234/test\r\nHost: s3.amazonaws.com\r\n\r\n", - 'http://s3.amazonaws.com/t1234/test?AWSAccessKeyId=foo&Expires=1397952000&Signature=hIZ2sBC96XAf1hiqE%2BuCC8VNKt8%3D' - ], - [ - "PUT /t1234/put\r\nHost: s3.amazonaws.com\r\n\r\n", - 'http://s3.amazonaws.com/t1234/put?AWSAccessKeyId=foo&Expires=1397952000&Signature=X5F%2FUBPes8Fc6vr%2Bl%2FQ5ltmKxc0%3D' - ], - [ - "PUT /t1234/put\r\nContent-Type: foo\r\nHost: s3.amazonaws.com\r\n\r\n", - 'http://s3.amazonaws.com/t1234/put?AWSAccessKeyId=foo&Expires=1397952000&Signature=cAUmoCjwcyKXjY2ilsGX7ghlHUI%3D' - ], - [ - "HEAD /test\r\nHost: test.s3.amazonaws.com\r\n\r\n", - 'http://test.s3.amazonaws.com/test?AWSAccessKeyId=foo&Expires=1397952000&Signature=1DQjgb9HhOH91oLFbwX8wze1tGs%3D' - ], - ]; - } - - /** - * @dataProvider presignedUrlProvider - */ - public function testCreatesPresignedUrls($message, $url) - { - $dt = 'April 20, 2014'; - $creds = new Credentials('foo', 'bar'); - $signature = new S3Signature(); - $req = Psr7\parse_request($message); - // Try with string - $res = $signature->presign($req, $creds, $dt); - $this->assertSame($url, (string) $res->getUri()); - // Try with timestamp - $res = $signature->presign($req, $creds, new \DateTime($dt)); - $this->assertSame($url, (string) $res->getUri()); - } - - public function signatureDataProvider() - { - return [ - // Use two subresources to set the ACL of a specific version and - // make sure subresources are sorted correctly - [ - [ - 'verb' => 'PUT', - 'path' => '/key?versionId=1234&acl=', - 'headers' => [ - 'Host' => 'test.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 21:06:08 +0000', - 'Content-Length' => '15' - ] - ], - "PUT\n\n\nTue, 27 Mar 2007 21:06:08 +0000\n/test/key?acl&versionId=1234" - ], - // DELETE a path hosted object with a folder prefix and custom headers - [ - [ - 'verb' => 'DELETE', - 'path' => '/johnsmith/photos/puppy.jpg', - 'headers' => [ - 'User-Agent' => 'dotnet', - 'Host' => 's3.amazonaws.com', - 'x-amz-date' => 'Tue, 27 Mar 2007 21:20:26 +0000' - ] - ], "DELETE\n\n\n\nx-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n/johnsmith/photos/puppy.jpg" - ], - // List buckets - [ - [ - 'verb' => 'GET', - 'path' => '/', - 'headers' => [ - 'Host' => 's3.amazonaws.com', - 'Date' => 'Wed, 28 Mar 2007 01:29:59 +0000' - ] - ], "GET\n\n\nWed, 28 Mar 2007 01:29:59 +0000\n/" - ], - // GET the ACL of a virtual hosted bucket - [ - [ - 'verb' => 'GET', - 'path' => '/?acl=', - 'headers' => [ - 'Host' => 'johnsmith.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:44:46 +0000' - ] - ], "GET\n\n\nTue, 27 Mar 2007 19:44:46 +0000\n/johnsmith/?acl" - ], - // GET the contents of a bucket using parameters - [ - [ - 'verb' => 'GET', - 'path' => '/?prefix=photos&max-keys=50&marker=puppy', - 'headers' => [ - 'User-Agent' => 'Mozilla/5.0', - 'Host' => 'johnsmith.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:42:41 +0000' - ] - ], "GET\n\n\nTue, 27 Mar 2007 19:42:41 +0000\n/johnsmith/" - ], - // PUT an object with a folder prefix from a virtual hosted bucket - [ - [ - 'verb' => 'PUT', - 'path' => '/photos/puppy.jpg', - 'headers' => [ - 'Content-Type' => 'image/jpeg', - 'Content-Length' => '94328', - 'Host' => 'johnsmith.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 21:15:45 +0000' - ] - ], "PUT\n\nimage/jpeg\nTue, 27 Mar 2007 21:15:45 +0000\n/johnsmith/photos/puppy.jpg" - ], - // GET an object with a folder prefix from a virtual hosted bucket - [ - [ - 'verb' => 'GET', - 'path' => '/photos/puppy.jpg', - 'headers' => [ - 'Host' => 'johnsmith.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ] - ], "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg" - ], - // Set the ACL of an object - [ - [ - 'verb' => 'PUT', - 'path' => '/photos/puppy.jpg?acl=', - 'headers' => [ - 'Host' => 'johnsmith.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ] - ], "PUT\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg?acl" - ], - // Set the ACL of an object with no prefix - [ - [ - 'verb' => 'PUT', - 'path' => '/photos/puppy?acl=', - 'headers' => [ - 'Host' => 'johnsmith.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ] - ], "PUT\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy?acl" - ], - // Set the ACL of an object with no prefix in a path hosted bucket - [ - [ - 'verb' => 'PUT', - 'path' => '/johnsmith/photos/puppy?acl=', - 'headers' => [ - 'Host' => 's3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ] - ], "PUT\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy?acl" - ], - // Set the ACL of a path hosted bucket - [ - [ - 'verb' => 'PUT', - 'path' => '/johnsmith?acl=', - 'headers' => [ - 'Host' => 's3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ] - ], "PUT\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith?acl" - ], - // Set the ACL of a path hosted bucket with an erroneous path value - [ - [ - 'verb' => 'PUT', - 'path' => '/johnsmith?acl=', - 'headers' => [ - 'Host' => 's3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ], - ], "PUT\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith?acl" - ], - // Send a request to the EU region - [ - [ - 'verb' => 'GET', - 'path' => '/johnsmith', - 'headers' => [ - 'Host' => 'test.s3-eu-west-1.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ], - ], "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/test/johnsmith" - ], - // Use a bucket with hyphens and a region - [ - [ - 'verb' => 'GET', - 'path' => '/bar', - 'headers' => [ - 'Host' => 'foo-s3-test-bucket.s3-eu-west-1.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ], - ], "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/foo-s3-test-bucket/bar" - ], - // Use a bucket with hyphens and the default region - [ - [ - 'verb' => 'GET', - 'path' => '/bar', - 'headers' => [ - 'Host' => 'foo-s3-test-bucket.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ], - ], "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/foo-s3-test-bucket/bar" - ], - [ - [ - 'verb' => 'GET', - 'path' => '/', - 'headers' => [ - 'Host' => 'foo.s3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ], - ], "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/foo/" - ], - [ - [ - 'verb' => 'GET', - 'path' => '/foo', - 'headers' => [ - 'Host' => 's3.amazonaws.com', - 'Date' => 'Tue, 27 Mar 2007 19:36:42 +0000' - ], - ], "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/foo" - ], - ]; - } - - /** - * @dataProvider signatureDataProvider - */ - public function testCreatesCanonicalizedString( - $input, - $result, - $expires = null - ) { - $signature = new S3Signature(); - $meth = new \ReflectionMethod($signature, 'createCanonicalizedString'); - $meth->setAccessible(true); - $request = new Request( - $input['verb'], - 'http://' . $input['headers']['Host'] . $input['path'], - $input['headers'] - ); - $this->assertEquals( - $result, - $meth->invoke($signature, $request, $expires) - ); - } - - public function testCreatesPreSignedUrlWithXAmzHeaders() - { - $signature = new S3Signature(); - $meth = new \ReflectionMethod($signature, 'createCanonicalizedString'); - $meth->setAccessible(true); - - $request = new Request('GET', 'https://s3.amazonaws.com', [ - 'X-Amz-Acl' => 'public-read', - 'X-Amz-Foo' => 'bar' - ]); - - $this->assertContains( - 'x-amz-acl:public-read', - $meth->invoke($signature, $request, time()) - ); - - $result = (string) $signature->presign( - $request, - new Credentials('foo', 'bar', 'baz'), - time() - )->getUri(); - - $this->assertContains('&x-amz-acl=public-read', $result); - $this->assertContains('x-amz-foo=bar', $result); - } -} diff --git a/tests/Signature/SignatureProviderTest.php b/tests/Signature/SignatureProviderTest.php index 650f367bef..f8e35bc8f3 100644 --- a/tests/Signature/SignatureProviderTest.php +++ b/tests/Signature/SignatureProviderTest.php @@ -12,7 +12,6 @@ public function versionProvider() { return [ ['v4', 'Aws\Signature\SignatureV4', 'foo'], - ['s3', 'Aws\Signature\S3Signature', 'foo'], ['v4', 'Aws\Signature\S3SignatureV4', 's3'], ['anonymous', 'Aws\Signature\AnonymousSignature', 's3'], ];