diff options
Diffstat (limited to 'vendor/aws/aws-sdk-php/src/Signature/SignatureV4.php')
-rw-r--r-- | vendor/aws/aws-sdk-php/src/Signature/SignatureV4.php | 502 |
1 files changed, 502 insertions, 0 deletions
diff --git a/vendor/aws/aws-sdk-php/src/Signature/SignatureV4.php b/vendor/aws/aws-sdk-php/src/Signature/SignatureV4.php new file mode 100644 index 0000000..4f93713 --- /dev/null +++ b/vendor/aws/aws-sdk-php/src/Signature/SignatureV4.php @@ -0,0 +1,502 @@ +<?php +namespace Aws\Signature; + +use Aws\Credentials\CredentialsInterface; +use AWS\CRT\Auth\Signable; +use AWS\CRT\Auth\SignatureType; +use AWS\CRT\Auth\Signing; +use AWS\CRT\Auth\SigningAlgorithm; +use AWS\CRT\Auth\SigningConfigAWS; +use AWS\CRT\Auth\StaticCredentialsProvider; +use AWS\CRT\HTTP\Request; +use Aws\Exception\CommonRuntimeException; +use Aws\Exception\CouldNotCreateChecksumException; +use GuzzleHttp\Psr7; +use Psr\Http\Message\RequestInterface; + +/** + * Signature Version 4 + * @link http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + */ +class SignatureV4 implements SignatureInterface +{ + use SignatureTrait; + const ISO8601_BASIC = 'Ymd\THis\Z'; + const UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD'; + const AMZ_CONTENT_SHA256_HEADER = 'X-Amz-Content-Sha256'; + + /** @var string */ + private $service; + + /** @var string */ + protected $region; + + /** @var bool */ + private $unsigned; + + /** @var bool */ + private $useV4a; + + /** + * The following headers are not signed because signing these headers + * would potentially cause a signature mismatch when sending a request + * through a proxy or if modified at the HTTP client level. + * + * @return array + */ + protected function getHeaderBlacklist() + { + return [ + 'cache-control' => true, + 'content-type' => true, + 'content-length' => true, + 'expect' => true, + 'max-forwards' => true, + 'pragma' => true, + 'range' => true, + 'te' => true, + 'if-match' => true, + 'if-none-match' => true, + 'if-modified-since' => true, + 'if-unmodified-since' => true, + 'if-range' => true, + 'accept' => true, + 'authorization' => true, + 'proxy-authorization' => true, + 'from' => true, + 'referer' => true, + 'user-agent' => true, + 'X-Amz-User-Agent' => true, + 'x-amzn-trace-id' => true, + 'aws-sdk-invocation-id' => true, + 'aws-sdk-retry' => true, + ]; + } + + /** + * @param string $service Service name to use when signing + * @param string $region Region name to use when signing + * @param array $options Array of configuration options used when signing + * - unsigned-body: Flag to make request have unsigned payload. + * Unsigned body is used primarily for streaming requests. + */ + public function __construct($service, $region, array $options = []) + { + $this->service = $service; + $this->region = $region; + $this->unsigned = isset($options['unsigned-body']) ? $options['unsigned-body'] : false; + $this->useV4a = isset($options['use_v4a']) && $options['use_v4a'] === true; + } + + /** + * {@inheritdoc} + */ + public function signRequest( + RequestInterface $request, + CredentialsInterface $credentials, + $signingService = null + ) { + $ldt = gmdate(self::ISO8601_BASIC); + $sdt = substr($ldt, 0, 8); + $parsed = $this->parseRequest($request); + $parsed['headers']['X-Amz-Date'] = [$ldt]; + + if ($token = $credentials->getSecurityToken()) { + $parsed['headers']['X-Amz-Security-Token'] = [$token]; + } + $service = isset($signingService) ? $signingService : $this->service; + + if ($this->useV4a) { + return $this->signWithV4a($credentials, $request, $service); + } + + $cs = $this->createScope($sdt, $this->region, $service); + $payload = $this->getPayload($request); + + if ($payload == self::UNSIGNED_PAYLOAD) { + $parsed['headers'][self::AMZ_CONTENT_SHA256_HEADER] = [$payload]; + } + + $context = $this->createContext($parsed, $payload); + $toSign = $this->createStringToSign($ldt, $cs, $context['creq']); + $signingKey = $this->getSigningKey( + $sdt, + $this->region, + $service, + $credentials->getSecretKey() + ); + $signature = hash_hmac('sha256', $toSign, $signingKey); + $parsed['headers']['Authorization'] = [ + "AWS4-HMAC-SHA256 " + . "Credential={$credentials->getAccessKeyId()}/{$cs}, " + . "SignedHeaders={$context['headers']}, Signature={$signature}" + ]; + + return $this->buildRequest($parsed); + } + + /** + * Get the headers that were used to pre-sign the request. + * Used for the X-Amz-SignedHeaders header. + * + * @param array $headers + * @return array + */ + private function getPresignHeaders(array $headers) + { + $presignHeaders = []; + $blacklist = $this->getHeaderBlacklist(); + foreach ($headers as $name => $value) { + $lName = strtolower($name); + if (!isset($blacklist[$lName]) + && $name !== self::AMZ_CONTENT_SHA256_HEADER + ) { + $presignHeaders[] = $lName; + } + } + return $presignHeaders; + } + + /** + * {@inheritdoc} + */ + public function presign( + RequestInterface $request, + CredentialsInterface $credentials, + $expires, + array $options = [] + ) { + + $startTimestamp = isset($options['start_time']) + ? $this->convertToTimestamp($options['start_time'], null) + : time(); + + $expiresTimestamp = $this->convertToTimestamp($expires, $startTimestamp); + + $parsed = $this->createPresignedRequest($request, $credentials); + $payload = $this->getPresignedPayload($request); + $httpDate = gmdate(self::ISO8601_BASIC, $startTimestamp); + $shortDate = substr($httpDate, 0, 8); + $scope = $this->createScope($shortDate, $this->region, $this->service); + $credential = $credentials->getAccessKeyId() . '/' . $scope; + if ($credentials->getSecurityToken()) { + unset($parsed['headers']['X-Amz-Security-Token']); + } + $parsed['query']['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'; + $parsed['query']['X-Amz-Credential'] = $credential; + $parsed['query']['X-Amz-Date'] = gmdate('Ymd\THis\Z', $startTimestamp); + $parsed['query']['X-Amz-SignedHeaders'] = implode(';', $this->getPresignHeaders($parsed['headers'])); + $parsed['query']['X-Amz-Expires'] = $this->convertExpires($expiresTimestamp, $startTimestamp); + $context = $this->createContext($parsed, $payload); + $stringToSign = $this->createStringToSign($httpDate, $scope, $context['creq']); + $key = $this->getSigningKey( + $shortDate, + $this->region, + $this->service, + $credentials->getSecretKey() + ); + $parsed['query']['X-Amz-Signature'] = hash_hmac('sha256', $stringToSign, $key); + + return $this->buildRequest($parsed); + } + + /** + * Converts a POST request to a GET request by moving POST fields into the + * query string. + * + * Useful for pre-signing query protocol requests. + * + * @param RequestInterface $request Request to clone + * + * @return RequestInterface + * @throws \InvalidArgumentException if the method is not POST + */ + public static function convertPostToGet(RequestInterface $request, $additionalQueryParams = "") + { + if ($request->getMethod() !== 'POST') { + throw new \InvalidArgumentException('Expected a POST request but ' + . 'received a ' . $request->getMethod() . ' request.'); + } + + $sr = $request->withMethod('GET') + ->withBody(Psr7\Utils::streamFor('')) + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + + // Move POST fields to the query if they are present + if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') { + $body = (string) $request->getBody() . $additionalQueryParams; + $sr = $sr->withUri($sr->getUri()->withQuery($body)); + } + + return $sr; + } + + protected function getPayload(RequestInterface $request) + { + if ($this->unsigned && $request->getUri()->getScheme() == 'https') { + return self::UNSIGNED_PAYLOAD; + } + // Calculate the request signature payload + if ($request->hasHeader(self::AMZ_CONTENT_SHA256_HEADER)) { + // Handle streaming operations (e.g. Glacier.UploadArchive) + return $request->getHeaderLine(self::AMZ_CONTENT_SHA256_HEADER); + } + + if (!$request->getBody()->isSeekable()) { + throw new CouldNotCreateChecksumException('sha256'); + } + + try { + return Psr7\Utils::hash($request->getBody(), 'sha256'); + } catch (\Exception $e) { + throw new CouldNotCreateChecksumException('sha256', $e); + } + } + + protected function getPresignedPayload(RequestInterface $request) + { + return $this->getPayload($request); + } + + protected function createCanonicalizedPath($path) + { + $doubleEncoded = rawurlencode(ltrim($path, '/')); + + return '/' . str_replace('%2F', '/', $doubleEncoded); + } + + private function createStringToSign($longDate, $credentialScope, $creq) + { + $hash = hash('sha256', $creq); + + return "AWS4-HMAC-SHA256\n{$longDate}\n{$credentialScope}\n{$hash}"; + } + + private function createPresignedRequest( + RequestInterface $request, + CredentialsInterface $credentials + ) { + $parsedRequest = $this->parseRequest($request); + + // Make sure to handle temporary credentials + if ($token = $credentials->getSecurityToken()) { + $parsedRequest['headers']['X-Amz-Security-Token'] = [$token]; + } + + return $this->moveHeadersToQuery($parsedRequest); + } + + /** + * @param array $parsedRequest + * @param string $payload Hash of the request payload + * @return array Returns an array of context information + */ + private function createContext(array $parsedRequest, $payload) + { + $blacklist = $this->getHeaderBlacklist(); + + // Normalize the path as required by SigV4 + $canon = $parsedRequest['method'] . "\n" + . $this->createCanonicalizedPath($parsedRequest['path']) . "\n" + . $this->getCanonicalizedQuery($parsedRequest['query']) . "\n"; + + // Case-insensitively aggregate all of the headers. + $aggregate = []; + foreach ($parsedRequest['headers'] as $key => $values) { + $key = strtolower($key); + if (!isset($blacklist[$key])) { + foreach ($values as $v) { + $aggregate[$key][] = $v; + } + } + } + + ksort($aggregate); + $canonHeaders = []; + foreach ($aggregate as $k => $v) { + if (count($v) > 0) { + sort($v); + } + $canonHeaders[] = $k . ':' . preg_replace('/\s+/', ' ', implode(',', $v)); + } + + $signedHeadersString = implode(';', array_keys($aggregate)); + $canon .= implode("\n", $canonHeaders) . "\n\n" + . $signedHeadersString . "\n" + . $payload; + + return ['creq' => $canon, 'headers' => $signedHeadersString]; + } + + private function getCanonicalizedQuery(array $query) + { + unset($query['X-Amz-Signature']); + + if (!$query) { + return ''; + } + + $qs = ''; + ksort($query); + foreach ($query as $k => $v) { + if (!is_array($v)) { + $qs .= rawurlencode($k) . '=' . rawurlencode($v !== null ? $v : '') . '&'; + } else { + sort($v); + foreach ($v as $value) { + $qs .= rawurlencode($k) . '=' . rawurlencode($value !== null ? $value : '') . '&'; + } + } + } + + return substr($qs, 0, -1); + } + + private function convertToTimestamp($dateValue, $relativeTimeBase = null) + { + if ($dateValue instanceof \DateTimeInterface) { + $timestamp = $dateValue->getTimestamp(); + } elseif (!is_numeric($dateValue)) { + $timestamp = strtotime($dateValue, + $relativeTimeBase === null ? time() : $relativeTimeBase + ); + } else { + $timestamp = $dateValue; + } + + return $timestamp; + } + + private function convertExpires($expiresTimestamp, $startTimestamp) + { + $duration = $expiresTimestamp - $startTimestamp; + + // Ensure that the duration of the signature is not longer than a week + if ($duration > 604800) { + throw new \InvalidArgumentException('The expiration date of a ' + . 'signature version 4 presigned URL must be less than one ' + . 'week'); + } + + return $duration; + } + + private function moveHeadersToQuery(array $parsedRequest) + { + //x-amz-user-agent shouldn't be put in a query param + unset($parsedRequest['headers']['X-Amz-User-Agent']); + + foreach ($parsedRequest['headers'] as $name => $header) { + $lname = strtolower($name); + if (substr($lname, 0, 5) == 'x-amz') { + $parsedRequest['query'][$name] = $header; + } + $blacklist = $this->getHeaderBlacklist(); + if (isset($blacklist[$lname]) + || $lname === strtolower(self::AMZ_CONTENT_SHA256_HEADER) + ) { + unset($parsedRequest['headers'][$name]); + } + } + + return $parsedRequest; + } + + private function parseRequest(RequestInterface $request) + { + // Clean up any previously set headers. + /** @var RequestInterface $request */ + $request = $request + ->withoutHeader('X-Amz-Date') + ->withoutHeader('Date') + ->withoutHeader('Authorization'); + $uri = $request->getUri(); + + return [ + 'method' => $request->getMethod(), + 'path' => $uri->getPath(), + 'query' => Psr7\Query::parse($uri->getQuery()), + 'uri' => $uri, + 'headers' => $request->getHeaders(), + 'body' => $request->getBody(), + 'version' => $request->getProtocolVersion() + ]; + } + + private function buildRequest(array $req) + { + if ($req['query']) { + $req['uri'] = $req['uri']->withQuery(Psr7\Query::build($req['query'])); + } + + return new Psr7\Request( + $req['method'], + $req['uri'], + $req['headers'], + $req['body'], + $req['version'] + ); + } + + /** + * @param CredentialsInterface $credentials + * @param RequestInterface $request + * @param $signingService + * @return RequestInterface + */ + protected function signWithV4a(CredentialsInterface $credentials, RequestInterface $request, $signingService) + { + if (!extension_loaded('awscrt')) { + throw new CommonRuntimeException( + "AWS Common Runtime for PHP is required to use Signature V4A" + . ". Please install it using the instructions found at" + . " https://github.com/aws/aws-sdk-php/blob/master/CRT_INSTRUCTIONS.md" + ); + } + $credentials_provider = new StaticCredentialsProvider([ + 'access_key_id' => $credentials->getAccessKeyId(), + 'secret_access_key' => $credentials->getSecretKey(), + 'session_token' => $credentials->getSecurityToken(), + ]); + $sha = $this->getPayload($request); + $signingConfig = new SigningConfigAWS([ + 'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC, + 'signature_type' => SignatureType::HTTP_REQUEST_HEADERS, + 'credentials_provider' => $credentials_provider, + 'signed_body_value' => $sha, + 'region' => "*", + 'service' => $signingService, + 'date' => time(), + ]); + $sha = $this->getPayload($request); + $invocationId = $request->getHeader("aws-sdk-invocation-id"); + $retry = $request->getHeader("aws-sdk-retry"); + $request = $request->withoutHeader("aws-sdk-invocation-id"); + $request = $request->withoutHeader("aws-sdk-retry"); + $http_request = new Request( + $request->getMethod(), + (string) $request->getUri(), + [], + array_map(function ($header) { + return $header[0]; + }, $request->getHeaders()) + ); + + Signing::signRequestAws( + Signable::fromHttpRequest($http_request), + $signingConfig, function ($signing_result, $error_code) use (&$http_request) { + $signing_result->applyToHttpRequest($http_request); + }); + $sigV4AHeaders = $http_request->headers(); + foreach ($sigV4AHeaders->toArray() as $h => $v) { + $request = $request->withHeader($h, $v); + } + $request = $request->withHeader("aws-sdk-invocation-id", $invocationId); + $request = $request->withHeader("x-amz-content-sha256", $sha); + $request = $request->withHeader("aws-sdk-retry", $retry); + $request = $request->withHeader("x-amz-region-set", "*"); + + return $request; + } +} |