PluginProbe
Media Cloud Sync / 1.1.1
Media Cloud Sync v1.1.1
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 1.2.9 All 35 releases
media-cloud-sync / includes / sdk / s3 / Aws / Signature / SignatureV4.php

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

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