Signer.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <?php
  2. namespace AsyncAws\S3\Signer;
  3. use AsyncAws\Core\Configuration;
  4. use AsyncAws\Core\Credentials\Credentials;
  5. use AsyncAws\Core\Exception\InvalidArgument;
  6. use AsyncAws\Core\Request;
  7. use AsyncAws\Core\RequestContext;
  8. use AsyncAws\Core\Signer\SignerV4;
  9. use AsyncAws\Core\Signer\SigningContext;
  10. use AsyncAws\Core\Stream\FixedSizeStream;
  11. use AsyncAws\Core\Stream\IterableStream;
  12. use AsyncAws\Core\Stream\ReadOnceResultStream;
  13. use AsyncAws\Core\Stream\RequestStream;
  14. use AsyncAws\Core\Stream\RewindableStream;
  15. /**
  16. * Version4 of signer dedicated for service S3.
  17. *
  18. * @author Jérémy Derussé <jeremy@derusse.com>
  19. */
  20. class Signer extends SignerV4
  21. {
  22. private const ALGORITHM_CHUNK = 'AWS4-HMAC-SHA256-PAYLOAD';
  23. private const CHUNK_SIZE = 64 * 1024;
  24. private const MD5_OPERATIONS = [
  25. 'DeleteObjects' => true,
  26. 'PutBucketCors' => true,
  27. 'PutBucketLifecycle' => true,
  28. 'PutBucketLifecycleConfiguration' => true,
  29. 'PutBucketPolicy' => true,
  30. 'PutBucketTagging' => true,
  31. 'PutBucketReplication' => true,
  32. 'PutObjectLegalHold' => true,
  33. 'PutObjectRetention' => true,
  34. 'PutObjectLockConfiguration' => true,
  35. ];
  36. private $sendChunkedBody;
  37. /**
  38. * @param array{
  39. * sendChunkedBody?: bool,
  40. * } $s3SignerOptions
  41. */
  42. public function __construct(string $scopeName, string $region, array $s3SignerOptions = [])
  43. {
  44. parent::__construct($scopeName, $region);
  45. $this->sendChunkedBody = $s3SignerOptions[Configuration::OPTION_SEND_CHUNKED_BODY] ?? false;
  46. unset($s3SignerOptions[Configuration::OPTION_SEND_CHUNKED_BODY]);
  47. if (!empty($s3SignerOptions)) {
  48. throw new InvalidArgument(sprintf('Invalid option(s) "%s" passed to "%s::%s". ', implode('", "', array_keys($s3SignerOptions)), __CLASS__, __METHOD__));
  49. }
  50. }
  51. public function sign(Request $request, Credentials $credentials, RequestContext $context): void
  52. {
  53. if ((null === ($operation = $context->getOperation()) || isset(self::MD5_OPERATIONS[$operation])) && !$request->hasHeader('content-md5')) {
  54. $request->setHeader('content-md5', base64_encode($request->getBody()->hash('md5', true)));
  55. }
  56. if (!$request->hasHeader('x-amz-content-sha256')) {
  57. $request->setHeader('x-amz-content-sha256', $request->getBody()->hash());
  58. }
  59. parent::sign($request, $credentials, $context);
  60. }
  61. protected function buildBodyDigest(Request $request, bool $isPresign): string
  62. {
  63. if ($isPresign) {
  64. $request->setHeader('x-amz-content-sha256', 'UNSIGNED-PAYLOAD');
  65. return 'UNSIGNED-PAYLOAD';
  66. }
  67. return parent::buildBodyDigest($request, $isPresign);
  68. }
  69. /**
  70. * Amazon S3 does not double-encode the path component in the canonical request.
  71. */
  72. protected function buildCanonicalPath(Request $request): string
  73. {
  74. return '/' . ltrim($request->getUri(), '/');
  75. }
  76. protected function convertBodyToStream(SigningContext $context): void
  77. {
  78. $request = $context->getRequest();
  79. $body = $request->getBody();
  80. if ($request->hasHeader('content-length')) {
  81. $contentLength = (int) $request->getHeader('content-length');
  82. } else {
  83. $contentLength = $body->length();
  84. }
  85. // If content length is unknown, use the rewindable stream to read it once locally in order to get the length
  86. if (null === $contentLength) {
  87. $request->setBody($body = RewindableStream::create($body));
  88. $body->read();
  89. $contentLength = $body->length();
  90. }
  91. // no need to stream small body. It's simple to convert it to string directly
  92. if ($contentLength < self::CHUNK_SIZE || !$this->sendChunkedBody) {
  93. if ($body instanceof ReadOnceResultStream) {
  94. $request->setBody(RewindableStream::create($body));
  95. }
  96. return;
  97. }
  98. // Add content-encoding for chunked stream if available
  99. $customEncoding = $request->getHeader('content-encoding');
  100. // Convert the body into a chunked stream
  101. $request->setHeader('content-encoding', $customEncoding ? "aws-chunked, $customEncoding" : 'aws-chunked');
  102. $request->setHeader('x-amz-decoded-content-length', (string) $contentLength);
  103. $request->setHeader('x-amz-content-sha256', 'STREAMING-' . self::ALGORITHM_CHUNK);
  104. // Compute size of content + metadata used sign each Chunk
  105. $chunkCount = (int) ceil($contentLength / self::CHUNK_SIZE);
  106. $fullChunkCount = $chunkCount * self::CHUNK_SIZE === $contentLength ? $chunkCount : ($chunkCount - 1);
  107. $metaLength = \strlen(";chunk-signature=\r\n\r\n") + 64;
  108. $request->setHeader('content-length', (string) ($contentLength + $fullChunkCount * ($metaLength + \strlen(dechex(self::CHUNK_SIZE))) + ($chunkCount - $fullChunkCount) * ($metaLength + \strlen(dechex($contentLength % self::CHUNK_SIZE))) + $metaLength + 1));
  109. $body = RewindableStream::create(IterableStream::create((function (RequestStream $body) use ($context): iterable {
  110. $now = $context->getNow();
  111. $credentialString = $context->getCredentialString();
  112. $signingKey = $context->getSigningKey();
  113. $signature = $context->getSignature();
  114. foreach (FixedSizeStream::create($body, self::CHUNK_SIZE) as $chunk) {
  115. $stringToSign = $this->buildChunkStringToSign($now, $credentialString, $signature, $chunk);
  116. $context->setSignature($signature = $this->buildSignature($stringToSign, $signingKey));
  117. yield sprintf("%s;chunk-signature=%s\r\n", dechex(\strlen($chunk)), $signature) . "$chunk\r\n";
  118. }
  119. $stringToSign = $this->buildChunkStringToSign($now, $credentialString, $signature, '');
  120. $context->setSignature($signature = $this->buildSignature($stringToSign, $signingKey));
  121. yield sprintf("%s;chunk-signature=%s\r\n\r\n", dechex(0), $signature);
  122. })($body)));
  123. $request->setBody($body);
  124. }
  125. private function buildChunkStringToSign(\DateTimeImmutable $now, string $credentialString, string $signature, string $chunk): string
  126. {
  127. static $emptyHash;
  128. $emptyHash = $emptyHash ?? hash('sha256', '');
  129. return implode("\n", [
  130. self::ALGORITHM_CHUNK,
  131. $now->format('Ymd\THis\Z'),
  132. $credentialString,
  133. $signature,
  134. $emptyHash,
  135. hash('sha256', $chunk),
  136. ]);
  137. }
  138. private function buildSignature(string $stringToSign, string $signingKey): string
  139. {
  140. return hash_hmac('sha256', $stringToSign, $signingKey);
  141. }
  142. }