Skip to content

Commit

Permalink
Removing S3's old Signature in favor of using Signature Version 4 exc…
Browse files Browse the repository at this point in the history
…lusively and making S3 more regionalized
  • Loading branch information
jeremeamia committed May 20, 2015
1 parent 643fb16 commit 7f4ea44
Show file tree
Hide file tree
Showing 27 changed files with 326 additions and 983 deletions.
8 changes: 4 additions & 4 deletions docs/guide/configuration.rst
Expand Up @@ -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.
Expand All @@ -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 <http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html>`_:
Expand All @@ -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.


Expand Down
16 changes: 15 additions & 1 deletion src/Api/Parser/AbstractParser.php
Expand Up @@ -2,6 +2,9 @@
namespace Aws\Api\Parser;

use Aws\Api\Service;
use Aws\CommandInterface;
use Aws\ResultInterface;
use Psr\Http\Message\ResponseInterface;

/**
* @internal
Expand All @@ -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
);
}
2 changes: 1 addition & 1 deletion src/Api/Parser/XmlParser.php
Expand Up @@ -39,7 +39,7 @@ private function dispatch($shape, \SimpleXMLElement $value)

private function parse_structure(
StructureShape $shape,
\SimpleXMLElement $value
\SimpleXMLElement $value
) {
$target = [];

Expand Down
4 changes: 2 additions & 2 deletions src/AwsClient.php
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/ClientResolver.php
Expand Up @@ -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' => [
Expand All @@ -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' => [
Expand Down
1 change: 1 addition & 0 deletions src/DynamoDb/DynamoDbClient.php
Expand Up @@ -59,6 +59,7 @@ function ($retries) {
);
}

/** @internal */
public static function _applyApiProvider($value, array &$args, HandlerList $list)
{
ClientResolver::_apply_api_provider($value, $args, $list);
Expand Down
47 changes: 12 additions & 35 deletions src/S3/ApplyMd5Middleware.php
Expand Up @@ -19,69 +19,46 @@ 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(
CommandInterface $command,
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);
}
}
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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']) {
Expand Down
42 changes: 42 additions & 0 deletions src/S3/GetBucketLocationParser.php
@@ -0,0 +1,42 @@
<?php
namespace Aws\S3;

use Aws\Api\Parser\AbstractParser;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;

/**
* @internal Decorates a parser for the S3 service to correctly handle the
* GetBucketLocation operation.
*/
class GetBucketLocationParser extends AbstractParser
{
/** @var callable */
private $parser;

/**
* @param callable $parser Parser to wrap.
*/
public function __construct(callable $parser)
{
$this->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;
}
}
41 changes: 9 additions & 32 deletions src/S3/MultipartUploader.php
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
});
}
}

0 comments on commit 7f4ea44

Please sign in to comment.