ef7e7daad0a81c0979124705626422b1ece2ae6e
[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-2024 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                 // This does not look right... The bucket name must be included in the URL.
68 //               if (strpos($uri, $search) === 0)
69 //               {
70 //                      $uri = substr($uri, strlen($search));
71 //               }
72
73                 $queryParameters = array_merge($this->request->getParameters(), [
74                         'AWSAccessKeyId' => $accessKey,
75                         'Expires'        => sprintf('%u', $expires),
76                         'Signature'      => $signature,
77                 ]);
78
79                 $query = http_build_query($queryParameters);
80
81                 // fix authenticated url for Google Cloud Storage - https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
82                 if ($this->request->getConfiguration()->getEndpoint() === "storage.googleapis.com")
83                 {
84                         // replace host with endpoint
85                         $headers['Host'] = 'storage.googleapis.com';
86                         // replace "AWSAccessKeyId" with "GoogleAccessId"
87                         $query = str_replace('AWSAccessKeyId', 'GoogleAccessId', $query);
88                         // add bucket to url
89                         $uri = '/' . $bucket . $uri;
90                 }
91
92                 $url = $protocol . '://' . $headers['Host'] . $uri;
93                 $url .= (strpos($uri, '?') !== false) ? '&' : '?';
94                 $url .= $query;
95
96                 return $url;
97         }
98
99         /**
100          * Returns the authorization header for the request
101          *
102          * @return  string
103          */
104         public function getAuthorizationHeader(): string
105         {
106                 $verb           = strtoupper($this->request->getVerb());
107                 $resourcePath   = $this->request->getResource();
108                 $headers        = $this->request->getHeaders();
109                 $amzHeaders     = $this->request->getAmzHeaders();
110                 $parameters     = $this->request->getParameters();
111                 $bucket         = $this->request->getBucket();
112                 $isPresignedURL = false;
113
114                 $amz       = [];
115                 $amzString = '';
116
117                 // Collect AMZ headers for signature
118                 foreach ($amzHeaders as $header => $value)
119                 {
120                         if (strlen($value) > 0)
121                         {
122                                 $amz[] = strtolower($header) . ':' . $value;
123                         }
124                 }
125
126                 // AMZ headers must be sorted and sent as separate lines
127                 if (count($amz) > 0)
128                 {
129                         sort($amz);
130                         $amzString = "\n" . implode("\n", $amz);
131                 }
132
133                 // If the Expires query string parameter is set up we're pre-signing a download URL. The string to sign is a bit
134                 // different in this case; it does not include the Date, it includes the Expires.
135                 // See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
136                 if (isset($headers['Expires']))
137                 {
138                         if (isset($headers['Date']))
139                         {
140                                 $headers['Date'] = $headers['Expires'];
141                         }
142                         else
143                         {
144                                 $amzHeaders['x-amz-date'] = $headers['Expires'];
145                         }
146
147                         unset ($headers['Expires']);
148
149                         $isPresignedURL = true;
150                 }
151
152                 /**
153                  * The resource path in S3 V2 signatures must ALWAYS contain the bucket name if a bucket is defined, even if we
154                  * are not using path-style access to the resource
155                  */
156                 if (!empty($bucket) && !$this->request->getConfiguration()->getUseLegacyPathStyle())
157                 {
158                         $resourcePath = '/' . $bucket . $resourcePath;
159                 }
160
161                 $stringToSign = $verb . "\n" .
162                         ($headers['Content-MD5'] ?? '') . "\n" .
163                         ($headers['Content-Type'] ?? '') . "\n" .
164                         ($headers['Date'] ?? '') .
165                         $amzString . "\n" .
166                         $resourcePath;
167
168                 // CloudFront only requires a date to be signed
169                 if ($headers['Host'] == 'cloudfront.amazonaws.com')
170                 {
171                         $stringToSign = $headers['Date'] ?? $amzHeaders['x-amz-date'] ?? '';
172                 }
173
174                 $amazonV2Hash = $this->amazonV2Hash($stringToSign);
175
176                 // For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
177                 // public access key.
178                 if ($isPresignedURL)
179                 {
180                         return $amazonV2Hash;
181                 }
182
183                 return 'AWS ' .
184                         $this->request->getConfiguration()->getAccess() . ':' .
185                         $amazonV2Hash;
186         }
187
188         /**
189          * Creates a HMAC-SHA1 hash. Uses the hash extension if present, otherwise falls back to slower, manual calculation.
190          *
191          * @param   string  $stringToSign  String to sign
192          *
193          * @return  string
194          */
195         private function amazonV2Hash(string $stringToSign): string
196         {
197                 $secret = $this->request->getConfiguration()->getSecret();
198
199                 if (extension_loaded('hash'))
200                 {
201                         $raw = hash_hmac('sha1', $stringToSign, $secret, true);
202
203                         return base64_encode($raw);
204                 }
205
206                 $raw = pack('H*', sha1(
207                                 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
208                                 pack('H*', sha1(
209                                                 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $stringToSign
210                                         )
211                                 )
212                         )
213                 );
214
215                 return base64_encode($raw);
216         }
217
218 }