PageRenderTime 25ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/upload/system/storage/vendor/aws/aws-sdk-php/src/Signature/SignatureV4.php

https://github.com/ocStore/ocStore
PHP | 424 lines | 309 code | 55 blank | 60 comment | 27 complexity | e7072f485f91d63a57fab0519e344e2b MD5 | raw file
  1. <?php
  2. namespace Aws\Signature;
  3. use Aws\Credentials\CredentialsInterface;
  4. use Aws\Exception\CouldNotCreateChecksumException;
  5. use GuzzleHttp\Psr7;
  6. use Psr\Http\Message\RequestInterface;
  7. /**
  8. * Signature Version 4
  9. * @link http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
  10. */
  11. class SignatureV4 implements SignatureInterface
  12. {
  13. use SignatureTrait;
  14. const ISO8601_BASIC = 'Ymd\THis\Z';
  15. const UNSIGNED_PAYLOAD = 'UNSIGNED-PAYLOAD';
  16. const AMZ_CONTENT_SHA256_HEADER = 'X-Amz-Content-Sha256';
  17. /** @var string */
  18. private $service;
  19. /** @var string */
  20. protected $region;
  21. /** @var bool */
  22. private $unsigned;
  23. /**
  24. * The following headers are not signed because signing these headers
  25. * would potentially cause a signature mismatch when sending a request
  26. * through a proxy or if modified at the HTTP client level.
  27. *
  28. * @return array
  29. */
  30. private function getHeaderBlacklist()
  31. {
  32. return [
  33. 'cache-control' => true,
  34. 'content-type' => true,
  35. 'content-length' => true,
  36. 'expect' => true,
  37. 'max-forwards' => true,
  38. 'pragma' => true,
  39. 'range' => true,
  40. 'te' => true,
  41. 'if-match' => true,
  42. 'if-none-match' => true,
  43. 'if-modified-since' => true,
  44. 'if-unmodified-since' => true,
  45. 'if-range' => true,
  46. 'accept' => true,
  47. 'authorization' => true,
  48. 'proxy-authorization' => true,
  49. 'from' => true,
  50. 'referer' => true,
  51. 'user-agent' => true,
  52. 'X-Amz-User-Agent' => true,
  53. 'x-amzn-trace-id' => true,
  54. 'aws-sdk-invocation-id' => true,
  55. 'aws-sdk-retry' => true,
  56. ];
  57. }
  58. /**
  59. * @param string $service Service name to use when signing
  60. * @param string $region Region name to use when signing
  61. * @param array $options Array of configuration options used when signing
  62. * - unsigned-body: Flag to make request have unsigned payload.
  63. * Unsigned body is used primarily for streaming requests.
  64. */
  65. public function __construct($service, $region, array $options = [])
  66. {
  67. $this->service = $service;
  68. $this->region = $region;
  69. $this->unsigned = isset($options['unsigned-body']) ? $options['unsigned-body'] : false;
  70. }
  71. /**
  72. * {@inheritdoc}
  73. */
  74. public function signRequest(
  75. RequestInterface $request,
  76. CredentialsInterface $credentials,
  77. $signingService = null
  78. ) {
  79. $ldt = gmdate(self::ISO8601_BASIC);
  80. $sdt = substr($ldt, 0, 8);
  81. $parsed = $this->parseRequest($request);
  82. $parsed['headers']['X-Amz-Date'] = [$ldt];
  83. if ($token = $credentials->getSecurityToken()) {
  84. $parsed['headers']['X-Amz-Security-Token'] = [$token];
  85. }
  86. $service = isset($signingService) ? $signingService : $this->service;
  87. $cs = $this->createScope($sdt, $this->region, $service);
  88. $payload = $this->getPayload($request);
  89. if ($payload == self::UNSIGNED_PAYLOAD) {
  90. $parsed['headers'][self::AMZ_CONTENT_SHA256_HEADER] = [$payload];
  91. }
  92. $context = $this->createContext($parsed, $payload);
  93. $toSign = $this->createStringToSign($ldt, $cs, $context['creq']);
  94. $signingKey = $this->getSigningKey(
  95. $sdt,
  96. $this->region,
  97. $service,
  98. $credentials->getSecretKey()
  99. );
  100. $signature = hash_hmac('sha256', $toSign, $signingKey);
  101. $parsed['headers']['Authorization'] = [
  102. "AWS4-HMAC-SHA256 "
  103. . "Credential={$credentials->getAccessKeyId()}/{$cs}, "
  104. . "SignedHeaders={$context['headers']}, Signature={$signature}"
  105. ];
  106. return $this->buildRequest($parsed);
  107. }
  108. /**
  109. * Get the headers that were used to pre-sign the request.
  110. * Used for the X-Amz-SignedHeaders header.
  111. *
  112. * @param array $headers
  113. * @return array
  114. */
  115. private function getPresignHeaders(array $headers)
  116. {
  117. $presignHeaders = [];
  118. $blacklist = $this->getHeaderBlacklist();
  119. foreach ($headers as $name => $value) {
  120. $lName = strtolower($name);
  121. if (!isset($blacklist[$lName])
  122. && $name !== self::AMZ_CONTENT_SHA256_HEADER
  123. ) {
  124. $presignHeaders[] = $lName;
  125. }
  126. }
  127. return $presignHeaders;
  128. }
  129. /**
  130. * {@inheritdoc}
  131. */
  132. public function presign(
  133. RequestInterface $request,
  134. CredentialsInterface $credentials,
  135. $expires,
  136. array $options = []
  137. ) {
  138. $startTimestamp = isset($options['start_time'])
  139. ? $this->convertToTimestamp($options['start_time'], null)
  140. : time();
  141. $expiresTimestamp = $this->convertToTimestamp($expires, $startTimestamp);
  142. $parsed = $this->createPresignedRequest($request, $credentials);
  143. $payload = $this->getPresignedPayload($request);
  144. $httpDate = gmdate(self::ISO8601_BASIC, $startTimestamp);
  145. $shortDate = substr($httpDate, 0, 8);
  146. $scope = $this->createScope($shortDate, $this->region, $this->service);
  147. $credential = $credentials->getAccessKeyId() . '/' . $scope;
  148. if ($credentials->getSecurityToken()) {
  149. unset($parsed['headers']['X-Amz-Security-Token']);
  150. }
  151. $parsed['query']['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256';
  152. $parsed['query']['X-Amz-Credential'] = $credential;
  153. $parsed['query']['X-Amz-Date'] = gmdate('Ymd\THis\Z', $startTimestamp);
  154. $parsed['query']['X-Amz-SignedHeaders'] = implode(';', $this->getPresignHeaders($parsed['headers']));
  155. $parsed['query']['X-Amz-Expires'] = $this->convertExpires($expiresTimestamp, $startTimestamp);
  156. $context = $this->createContext($parsed, $payload);
  157. $stringToSign = $this->createStringToSign($httpDate, $scope, $context['creq']);
  158. $key = $this->getSigningKey(
  159. $shortDate,
  160. $this->region,
  161. $this->service,
  162. $credentials->getSecretKey()
  163. );
  164. $parsed['query']['X-Amz-Signature'] = hash_hmac('sha256', $stringToSign, $key);
  165. return $this->buildRequest($parsed);
  166. }
  167. /**
  168. * Converts a POST request to a GET request by moving POST fields into the
  169. * query string.
  170. *
  171. * Useful for pre-signing query protocol requests.
  172. *
  173. * @param RequestInterface $request Request to clone
  174. *
  175. * @return RequestInterface
  176. * @throws \InvalidArgumentException if the method is not POST
  177. */
  178. public static function convertPostToGet(RequestInterface $request, $additionalQueryParams = "")
  179. {
  180. if ($request->getMethod() !== 'POST') {
  181. throw new \InvalidArgumentException('Expected a POST request but '
  182. . 'received a ' . $request->getMethod() . ' request.');
  183. }
  184. $sr = $request->withMethod('GET')
  185. ->withBody(Psr7\Utils::streamFor(''))
  186. ->withoutHeader('Content-Type')
  187. ->withoutHeader('Content-Length');
  188. // Move POST fields to the query if they are present
  189. if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') {
  190. $body = (string) $request->getBody() . $additionalQueryParams;
  191. $sr = $sr->withUri($sr->getUri()->withQuery($body));
  192. }
  193. return $sr;
  194. }
  195. protected function getPayload(RequestInterface $request)
  196. {
  197. if ($this->unsigned && $request->getUri()->getScheme() == 'https') {
  198. return self::UNSIGNED_PAYLOAD;
  199. }
  200. // Calculate the request signature payload
  201. if ($request->hasHeader(self::AMZ_CONTENT_SHA256_HEADER)) {
  202. // Handle streaming operations (e.g. Glacier.UploadArchive)
  203. return $request->getHeaderLine(self::AMZ_CONTENT_SHA256_HEADER);
  204. }
  205. if (!$request->getBody()->isSeekable()) {
  206. throw new CouldNotCreateChecksumException('sha256');
  207. }
  208. try {
  209. return Psr7\Utils::hash($request->getBody(), 'sha256');
  210. } catch (\Exception $e) {
  211. throw new CouldNotCreateChecksumException('sha256', $e);
  212. }
  213. }
  214. protected function getPresignedPayload(RequestInterface $request)
  215. {
  216. return $this->getPayload($request);
  217. }
  218. protected function createCanonicalizedPath($path)
  219. {
  220. $doubleEncoded = rawurlencode(ltrim($path, '/'));
  221. return '/' . str_replace('%2F', '/', $doubleEncoded);
  222. }
  223. private function createStringToSign($longDate, $credentialScope, $creq)
  224. {
  225. $hash = hash('sha256', $creq);
  226. return "AWS4-HMAC-SHA256\n{$longDate}\n{$credentialScope}\n{$hash}";
  227. }
  228. private function createPresignedRequest(
  229. RequestInterface $request,
  230. CredentialsInterface $credentials
  231. ) {
  232. $parsedRequest = $this->parseRequest($request);
  233. // Make sure to handle temporary credentials
  234. if ($token = $credentials->getSecurityToken()) {
  235. $parsedRequest['headers']['X-Amz-Security-Token'] = [$token];
  236. }
  237. return $this->moveHeadersToQuery($parsedRequest);
  238. }
  239. /**
  240. * @param array $parsedRequest
  241. * @param string $payload Hash of the request payload
  242. * @return array Returns an array of context information
  243. */
  244. private function createContext(array $parsedRequest, $payload)
  245. {
  246. $blacklist = $this->getHeaderBlacklist();
  247. // Normalize the path as required by SigV4
  248. $canon = $parsedRequest['method'] . "\n"
  249. . $this->createCanonicalizedPath($parsedRequest['path']) . "\n"
  250. . $this->getCanonicalizedQuery($parsedRequest['query']) . "\n";
  251. // Case-insensitively aggregate all of the headers.
  252. $aggregate = [];
  253. foreach ($parsedRequest['headers'] as $key => $values) {
  254. $key = strtolower($key);
  255. if (!isset($blacklist[$key])) {
  256. foreach ($values as $v) {
  257. $aggregate[$key][] = $v;
  258. }
  259. }
  260. }
  261. ksort($aggregate);
  262. $canonHeaders = [];
  263. foreach ($aggregate as $k => $v) {
  264. if (count($v) > 0) {
  265. sort($v);
  266. }
  267. $canonHeaders[] = $k . ':' . preg_replace('/\s+/', ' ', implode(',', $v));
  268. }
  269. $signedHeadersString = implode(';', array_keys($aggregate));
  270. $canon .= implode("\n", $canonHeaders) . "\n\n"
  271. . $signedHeadersString . "\n"
  272. . $payload;
  273. return ['creq' => $canon, 'headers' => $signedHeadersString];
  274. }
  275. private function getCanonicalizedQuery(array $query)
  276. {
  277. unset($query['X-Amz-Signature']);
  278. if (!$query) {
  279. return '';
  280. }
  281. $qs = '';
  282. ksort($query);
  283. foreach ($query as $k => $v) {
  284. if (!is_array($v)) {
  285. $qs .= rawurlencode($k) . '=' . rawurlencode($v !== null ? $v : '') . '&';
  286. } else {
  287. sort($v);
  288. foreach ($v as $value) {
  289. $qs .= rawurlencode($k) . '=' . rawurlencode($value !== null ? $value : '') . '&';
  290. }
  291. }
  292. }
  293. return substr($qs, 0, -1);
  294. }
  295. private function convertToTimestamp($dateValue, $relativeTimeBase = null)
  296. {
  297. if ($dateValue instanceof \DateTimeInterface) {
  298. $timestamp = $dateValue->getTimestamp();
  299. } elseif (!is_numeric($dateValue)) {
  300. $timestamp = strtotime($dateValue,
  301. $relativeTimeBase === null ? time() : $relativeTimeBase
  302. );
  303. } else {
  304. $timestamp = $dateValue;
  305. }
  306. return $timestamp;
  307. }
  308. private function convertExpires($expiresTimestamp, $startTimestamp)
  309. {
  310. $duration = $expiresTimestamp - $startTimestamp;
  311. // Ensure that the duration of the signature is not longer than a week
  312. if ($duration > 604800) {
  313. throw new \InvalidArgumentException('The expiration date of a '
  314. . 'signature version 4 presigned URL must be less than one '
  315. . 'week');
  316. }
  317. return $duration;
  318. }
  319. private function moveHeadersToQuery(array $parsedRequest)
  320. {
  321. //x-amz-user-agent shouldn't be put in a query param
  322. unset($parsedRequest['headers']['X-Amz-User-Agent']);
  323. foreach ($parsedRequest['headers'] as $name => $header) {
  324. $lname = strtolower($name);
  325. if (substr($lname, 0, 5) == 'x-amz') {
  326. $parsedRequest['query'][$name] = $header;
  327. }
  328. $blacklist = $this->getHeaderBlacklist();
  329. if (isset($blacklist[$lname])
  330. || $lname === strtolower(self::AMZ_CONTENT_SHA256_HEADER)
  331. ) {
  332. unset($parsedRequest['headers'][$name]);
  333. }
  334. }
  335. return $parsedRequest;
  336. }
  337. private function parseRequest(RequestInterface $request)
  338. {
  339. // Clean up any previously set headers.
  340. /** @var RequestInterface $request */
  341. $request = $request
  342. ->withoutHeader('X-Amz-Date')
  343. ->withoutHeader('Date')
  344. ->withoutHeader('Authorization');
  345. $uri = $request->getUri();
  346. return [
  347. 'method' => $request->getMethod(),
  348. 'path' => $uri->getPath(),
  349. 'query' => Psr7\Query::parse($uri->getQuery()),
  350. 'uri' => $uri,
  351. 'headers' => $request->getHeaders(),
  352. 'body' => $request->getBody(),
  353. 'version' => $request->getProtocolVersion()
  354. ];
  355. }
  356. private function buildRequest(array $req)
  357. {
  358. if ($req['query']) {
  359. $req['uri'] = $req['uri']->withQuery(Psr7\Query::build($req['query']));
  360. }
  361. return new Psr7\Request(
  362. $req['method'],
  363. $req['uri'],
  364. $req['headers'],
  365. $req['body'],
  366. $req['version']
  367. );
  368. }
  369. }