6864846fa88ce91e88d41c716141ea001e9ba9c5
[friendica-addons.git/.git] / s3_storage / vendor / akeeba / s3 / src / Signature / V2.php
1 <?php
2 /**
3  * Akeeba Engine
4  *
5  * @package   akeebaengine
6  * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
7  * @license   GNU General Public License version 3, or later
8  */
9
10 namespace Akeeba\S3\Signature;
11
12 // Protection against direct access
13 defined('AKEEBAENGINE') || die();
14
15 use Akeeba\S3\Signature;
16
17 /**
18  * Implements the Amazon AWS v2 signatures
19  *
20  * @see http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
21  */
22 class V2 extends Signature
23 {
24         /**
25          * Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
26          * add custom headers, e.g. x-amz-content-sha256
27          *
28          * @param   array  $headers     The associative array of headers to process
29          * @param   array  $amzHeaders  The associative array of amz-* headers to process
30          *
31          * @return  void
32          */
33         public function preProcessHeaders(array &$headers, array &$amzHeaders): void
34         {
35                 // No pre-processing required for V2 signatures
36         }
37
38         /**
39          * Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
40          * pre-authorized URLs for downloading files from S3.
41          *
42          * @param   integer|null  $lifetime  Lifetime in seconds. NULL for default lifetime.
43          * @param   bool          $https     Use HTTPS ($hostBucket should be false for SSL verification)?
44          *
45          * @return  string  The presigned URL
46          */
47         public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
48         {
49                 // Set the Expires header
50                 if (is_null($lifetime))
51                 {
52                         $lifetime = 10;
53                 }
54
55                 $expires = time() + $lifetime;
56                 $this->request->setHeader('Expires', $expires);
57
58                 $bucket    = $this->request->getBucket();
59                 $uri       = $this->request->getResource();
60                 $headers   = $this->request->getHeaders();
61                 $accessKey = $this->request->getConfiguration()->getAccess();
62                 $protocol  = $https ? 'https' : 'http';
63                 $signature = $this->getAuthorizationHeader();
64
65                 $search = '/' . $bucket;
66
67                 if (strpos($uri, $search) === 0)
68                 {
69                         $uri = substr($uri, strlen($search));
70                 }
71
72                 $queryParameters = array_merge($this->request->getParameters(), [
73                         'AWSAccessKeyId' => $accessKey,
74                         'Expires'        => sprintf('%u', $expires),
75                         'Signature'      => $signature,
76                 ]);
77
78                 $query = http_build_query($queryParameters);
79
80                 // fix authenticated url for Google Cloud Storage - https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
81                 if ($this->request->getConfiguration()->getEndpoint() === "storage.googleapis.com")
82                 {
83                         // replace host with endpoint
84                         $headers['Host'] = 'storage.googleapis.com';
85                         // replace "AWSAccessKeyId" with "GoogleAccessId"
86                         $query = str_replace('AWSAccessKeyId', 'GoogleAccessId', $query);
87                         // add bucket to url
88                         $uri = '/' . $bucket . $uri;
89                 }
90
91                 $url = $protocol . '://' . $headers['Host'] . $uri;
92                 $url .= (strpos($uri, '?') !== false) ? '&' : '?';
93                 $url .= $query;
94
95                 return $url;
96         }
97
98         /**
99          * Returns the authorization header for the request
100          *
101          * @return  string
102          */
103         public function getAuthorizationHeader(): string
104         {
105                 $verb           = strtoupper($this->request->getVerb());
106                 $resourcePath   = $this->request->getResource();
107                 $headers        = $this->request->getHeaders();
108                 $amzHeaders     = $this->request->getAmzHeaders();
109                 $parameters     = $this->request->getParameters();
110                 $bucket         = $this->request->getBucket();
111                 $isPresignedURL = false;
112
113                 $amz       = [];
114                 $amzString = '';
115
116                 // Collect AMZ headers for signature
117                 foreach ($amzHeaders as $header => $value)
118                 {
119                         if (strlen($value) > 0)
120                         {
121                                 $amz[] = strtolower($header) . ':' . $value;
122                         }
123                 }
124
125                 // AMZ headers must be sorted and sent as separate lines
126                 if (count($amz) > 0)
127                 {
128                         sort($amz);
129                         $amzString = "\n" . implode("\n", $amz);
130                 }
131
132                 // If the Expires query string parameter is set up we're pre-signing a download URL. The string to sign is a bit
133                 // different in this case; it does not include the Date, it includes the Expires.
134                 // See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
135                 if (isset($headers['Expires']))
136                 {
137                         $headers['Date'] = $headers['Expires'];
138                         unset ($headers['Expires']);
139
140                         $isPresignedURL = true;
141                 }
142
143                 /**
144                  * The resource path in S3 V2 signatures must ALWAYS contain the bucket name if a bucket is defined, even if we
145                  * are not using path-style access to the resource
146                  */
147                 if (!empty($bucket) && !$this->request->getConfiguration()->getUseLegacyPathStyle())
148                 {
149                         $resourcePath = '/' . $bucket . $resourcePath;
150                 }
151
152                 $stringToSign = $verb . "\n" .
153                         ($headers['Content-MD5'] ?? '') . "\n" .
154                         ($headers['Content-Type'] ?? '') . "\n" .
155                         $headers['Date'] .
156                         $amzString . "\n" .
157                         $resourcePath;
158
159                 // CloudFront only requires a date to be signed
160                 if ($headers['Host'] == 'cloudfront.amazonaws.com')
161                 {
162                         $stringToSign = $headers['Date'];
163                 }
164
165                 $amazonV2Hash = $this->amazonV2Hash($stringToSign);
166
167                 // For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
168                 // public access key.
169                 if ($isPresignedURL)
170                 {
171                         return $amazonV2Hash;
172                 }
173
174                 return 'AWS ' .
175                         $this->request->getConfiguration()->getAccess() . ':' .
176                         $amazonV2Hash;
177         }
178
179         /**
180          * Creates a HMAC-SHA1 hash. Uses the hash extension if present, otherwise falls back to slower, manual calculation.
181          *
182          * @param   string  $stringToSign  String to sign
183          *
184          * @return  string
185          */
186         private function amazonV2Hash(string $stringToSign): string
187         {
188                 $secret = $this->request->getConfiguration()->getSecret();
189
190                 if (extension_loaded('hash'))
191                 {
192                         $raw = hash_hmac('sha1', $stringToSign, $secret, true);
193
194                         return base64_encode($raw);
195                 }
196
197                 $raw = pack('H*', sha1(
198                                 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
199                                 pack('H*', sha1(
200                                                 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $stringToSign
201                                         )
202                                 )
203                         )
204                 );
205
206                 return base64_encode($raw);
207         }
208
209 }