PluginProbe
Media Cloud Sync / 1.4.2
Media Cloud Sync v1.4.2
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
media-cloud-sync / includes / sdk / s3 / Aws / Signature / SignatureV4.php

SignatureV4.php in Media Cloud Sync 1.4.2, at includes/sdk/s3/Aws/Signature/SignatureV4.php

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