request->setHeader('Expires', $expires); $bucket = $this->request->getBucket(); $uri = $this->request->getResource(); $headers = $this->request->getHeaders(); $accessKey = $this->request->getConfiguration()->getAccess(); $protocol = $https ? 'https' : 'http'; $signature = $this->getAuthorizationHeader(); $search = '/' . $bucket; // This does not look right... The bucket name must be included in the URL. // if (strpos($uri, $search) === 0) // { // $uri = substr($uri, strlen($search)); // } $queryParameters = array_merge($this->request->getParameters(), [ 'AWSAccessKeyId' => $accessKey, 'Expires' => sprintf('%u', $expires), 'Signature' => $signature, ]); $query = http_build_query($queryParameters); // fix authenticated url for Google Cloud Storage - https://cloud.google.com/storage/docs/access-control/create-signed-urls-program if ($this->request->getConfiguration()->getEndpoint() === "storage.googleapis.com") { // replace host with endpoint $headers['Host'] = 'storage.googleapis.com'; // replace "AWSAccessKeyId" with "GoogleAccessId" $query = str_replace('AWSAccessKeyId', 'GoogleAccessId', $query); // add bucket to url $uri = '/' . $bucket . $uri; } $url = $protocol . '://' . $headers['Host'] . $uri; $url .= (strpos($uri, '?') !== false) ? '&' : '?'; $url .= $query; return $url; } /** * Returns the authorization header for the request * * @return string */ public function getAuthorizationHeader(): string { $verb = strtoupper($this->request->getVerb()); $resourcePath = $this->request->getResource(); $headers = $this->request->getHeaders(); $amzHeaders = $this->request->getAmzHeaders(); $parameters = $this->request->getParameters(); $bucket = $this->request->getBucket(); $isPresignedURL = false; $amz = []; $amzString = ''; // Collect AMZ headers for signature foreach ($amzHeaders as $header => $value) { if (strlen($value) > 0) { $amz[] = strtolower($header) . ':' . $value; } } // AMZ headers must be sorted and sent as separate lines if (count($amz) > 0) { sort($amz); $amzString = "\n" . implode("\n", $amz); } // If the Expires query string parameter is set up we're pre-signing a download URL. The string to sign is a bit // different in this case; it does not include the Date, it includes the Expires. // See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth if (isset($headers['Expires'])) { if (isset($headers['Date'])) { $headers['Date'] = $headers['Expires']; } else { $amzHeaders['x-amz-date'] = $headers['Expires']; } unset ($headers['Expires']); $isPresignedURL = true; } /** * The resource path in S3 V2 signatures must ALWAYS contain the bucket name if a bucket is defined, even if we * are not using path-style access to the resource */ if (!empty($bucket) && !$this->request->getConfiguration()->getUseLegacyPathStyle()) { $resourcePath = '/' . $bucket . $resourcePath; } $stringToSign = $verb . "\n" . ($headers['Content-MD5'] ?? '') . "\n" . ($headers['Content-Type'] ?? '') . "\n" . ($headers['Date'] ?? '') . $amzString . "\n" . $resourcePath; // CloudFront only requires a date to be signed if ($headers['Host'] == 'cloudfront.amazonaws.com') { $stringToSign = $headers['Date'] ?? $amzHeaders['x-amz-date'] ?? ''; } $amazonV2Hash = $this->amazonV2Hash($stringToSign); // For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the // public access key. if ($isPresignedURL) { return $amazonV2Hash; } return 'AWS ' . $this->request->getConfiguration()->getAccess() . ':' . $amazonV2Hash; } /** * Creates a HMAC-SHA1 hash. Uses the hash extension if present, otherwise falls back to slower, manual calculation. * * @param string $stringToSign String to sign * * @return string */ private function amazonV2Hash(string $stringToSign): string { $secret = $this->request->getConfiguration()->getSecret(); if (extension_loaded('hash')) { $raw = hash_hmac('sha1', $stringToSign, $secret, true); return base64_encode($raw); } $raw = pack('H*', sha1( (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) . pack('H*', sha1( (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $stringToSign ) ) ) ); return base64_encode($raw); } }