friendica-addons/s3_storage/vendor/akeeba/s3/src/Signature/V2.php

210 lines
6.0 KiB
PHP

<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\Engine\Postproc\Connector\S3v4\Signature;
// Protection against direct access
defined('AKEEBAENGINE') or die();
use Akeeba\Engine\Postproc\Connector\S3v4\Signature;
/**
* Implements the Amazon AWS v2 signatures
*
* @see http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
*/
class V2 extends Signature
{
/**
* Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
* add custom headers, e.g. x-amz-content-sha256
*
* @param array $headers The associative array of headers to process
* @param array $amzHeaders The associative array of amz-* headers to process
*
* @return void
*/
public function preProcessHeaders(array &$headers, array &$amzHeaders): void
{
// No pre-processing required for V2 signatures
}
/**
* Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
* pre-authorized URLs for downloading files from S3.
*
* @param integer|null $lifetime Lifetime in seconds. NULL for default lifetime.
* @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
*
* @return string The presigned URL
*/
public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
{
// Set the Expires header
if (is_null($lifetime))
{
$lifetime = 10;
}
$expires = time() + $lifetime;
$this->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;
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 (sizeof($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']))
{
$headers['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" .
(isset($headers['Content-MD5']) ? $headers['Content-MD5'] : '') . "\n" .
(isset($headers['Content-Type']) ? $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'];
}
$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);
}
}