PluginProbe ʕ •ᴥ•ʔ
JetBackup – Backup, Restore & Migrate / trunk
JetBackup – Backup, Restore & Migrate vtrunk
3.1.22.4 3.1.22.3 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.8.1 1.4.9 1.5.0 1.5.1 1.5.1.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 1.6.10 1.6.11 1.6.12 1.6.13 1.6.15 1.6.5.1 1.6.8.8 1.6.9 1.6.9.1 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7.5 2.0.8.7 2.0.9.11 2.0.9.14 2.0.9.15 2.0.9.6 2.0.9.7 2.0.9.9 3.1.10.7 3.1.11.1 3.1.12.3 3.1.13.4 3.1.14.17 3.1.15.4 3.1.16.1 3.1.17.5 3.1.18.10 3.1.18.8 3.1.18.9 3.1.19.8 3.1.20.3 3.1.21.3 3.1.7.9 3.1.9.2 trunk 1.1.90 1.1.91 1.2.0 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2
backup / src / JetBackup / Destination / Vendors / S3 / Client / Client.php
backup / src / JetBackup / Destination / Vendors / S3 / Client Last commit date
Exception 1 year ago .htaccess 1 year ago Client.php 1 year ago ClientManager.php 2 months ago ClientRetry.php 1 year ago ListObjects.php 1 year ago ObjectData.php 1 year ago index.html 1 year ago web.config 1 year ago
Client.php
412 lines
1 <?php
2 /*
3 *
4 * JetBackup @ package
5 * Created By Idan Ben-Ezra
6 *
7 * Copyrights @ JetApps
8 * https://www.jetapps.com
9 *
10 **/
11 namespace JetBackup\Destination\Vendors\S3\Client;
12
13 use JetBackup\Destination\Vendors\S3\Client\Exception\ClientException;
14 use JetBackup\Exception\HttpRequestException;
15 use JetBackup\Web\File\FileChunk;
16 use JetBackup\Web\File\FileDownload;
17 use JetBackup\Web\JetHttp;
18 use JetBackup\Wordpress\Wordpress;
19 use stdClass;
20
21 defined( '__JETBACKUP__' ) or die( 'Restricted access' );
22
23 class Client {
24
25 const METHOD_GET = JetHttp::METHOD_GET;
26 const METHOD_POST = JetHttp::METHOD_POST;
27 const METHOD_PUT = JetHttp::METHOD_PUT;
28 const METHOD_DELETE = JetHttp::METHOD_DELETE;
29 const METHOD_HEAD = JetHttp::METHOD_HEAD;
30
31 const METHOD_NAMES = [
32 self::METHOD_GET => 'GET',
33 self::METHOD_POST => 'POST',
34 self::METHOD_PUT => 'PUT',
35 self::METHOD_DELETE => 'DELETE',
36 self::METHOD_HEAD => 'HEAD',
37 ];
38
39 const ALGO = 'sha256';
40 const ALGORITHM = 'AWS4-HMAC-SHA256';
41 const DATE_FORMAT = "Ymd";
42 const DATE_FORMAT_LONG = "Ymd\THis\Z";
43
44 private ?JetHttp $_http=null;
45 private string $_key;
46 private string $_secret;
47 private string $_region;
48 private string $_bucket;
49 private bool $_verifyssl;
50 private int $_date;
51 private string $_host;
52
53 private string $_uri = '';
54 private int $_method;
55 private array $_params;
56 private $_body;
57 private string $_destination;
58 private ?FileChunk $_chunk = null;
59 private array $_headers;
60 private array $_headers_signature;
61 private int $_keepalive_timeout;
62 private int $_keepalive_requests;
63
64 public function __construct(string $key, string $secret, string $region, string $bucket, string $endpoint, bool $verifyssl=true, int $keepalive_timeout=0, int $keepalive_queries=0) {
65 $this->_key = $key;
66 $this->_secret = $secret;
67 $this->_region = $region;
68 $this->_bucket = $bucket;
69 $this->_verifyssl = !!$verifyssl;
70 $this->_keepalive_timeout = $keepalive_timeout;
71 $this->_keepalive_requests = $keepalive_queries;
72
73 //if($this->_region && strpos($endpoint, '{region}') === false) $endpoint = "{region}.{$endpoint}";
74 $this->_host = str_replace(['{region}','{bucket}'], [$this->_region, $this->_bucket], $endpoint);
75 if(Wordpress::strContains($endpoint, '{bucket}')) $this->_bucket = '';
76 }
77
78 private function _reset():void {
79 $this->_chunk = null;
80 $this->_body = false;
81 $this->_date = time();
82
83 $this->_uri = $this->_destination = '';
84 $this->_method = self::METHOD_GET;
85 $this->_headers = $this->_headers_signature = $this->_params = [];
86
87 $this->addHeader("host", $this->_host, true);
88 }
89
90 public function getHeaders():array { return $this->_headers; }
91 public function getHeadersSignature():array { return $this->_headers_signature; }
92 public function addHeader(string $key, string $value, bool $signature=false):void {
93 $this->_headers[$key] = $value;
94 if($signature) $this->_headers_signature[$key] = $value;
95 }
96 public function getHeader(string $key, bool $signature=false):string {
97 if($signature) return $this->_headers_signature[$key];
98 return $this->_headers[$key];
99 }
100
101 public function getMethod():int { return $this->_method; }
102 public function setMethod(int $method):void { $this->_method = $method; }
103
104 public function getURI():string {
105 $uri = $this->_uri;
106 if($this->_bucket) $uri = '/' . $this->_bucket . '/' . $uri;
107 $uri = preg_replace("#/+#", "/", $uri);
108 return implode("/", array_map('rawurlencode', explode("/", $uri)));
109 }
110 public function setURI(string $uri):void { $this->_uri = $uri; }
111
112 public function getBody() { return $this->_body; }
113 public function setBody($body):void { $this->_body = $body; }
114
115 public function getDestination():string { return $this->_destination; }
116 public function setDestination(string $destination):void { $this->_destination = $destination; }
117
118 public function getParams():array { return $this->_params; }
119 public function setParams(array $params):void { $this->_params = $params; }
120 public function addParams(string $key, $value):void { $this->_params[$key] = $value; }
121
122 public function getFileChunk():?FileChunk { return $this->_chunk; }
123 public function setFileChunk(FileChunk $chunk):void { $this->_chunk = $chunk; }
124
125 private static function hmac(string $data, string $key, bool $raw=true):string {
126 return hash_hmac( self::ALGO, $data, $key, $raw);
127 }
128
129 public function getAmzCredential():string { return gmdate(self::DATE_FORMAT, $this->_date) . '/' . $this->_region . '/s3/aws4_request'; }
130
131 private function buildSignatureCanonical():string {
132
133 $fields = [];
134 $fields[] = self::METHOD_NAMES[$this->getMethod()];
135 $fields[] = $this->getURI();
136 $fields[] = $this->getParams() ? self::http_build_query($this->getParams()) : '';
137 foreach($this->getHeadersSignature() as $key => $value) $fields[] = "$key:$value";
138 $fields[] = "";
139 $fields[] = implode(";", array_keys($this->getHeadersSignature()));
140 $fields[] = $this->getHeader('x-amz-content-sha256');
141
142 return hash(self::ALGO, implode("\n", $fields));
143 }
144
145 private function buildSignatureSigningString():string {
146 return implode("\n", [
147 self::ALGORITHM,
148 gmdate(self::DATE_FORMAT_LONG, $this->_date),
149 $this->getAmzCredential(),
150 $this->buildSignatureCanonical()
151 ]);
152 }
153
154 private function buildSignature():string {
155 $key = 'AWS4' . $this->_secret;
156 foreach(explode("/", $this->getAmzCredential()) as $data) $key = self::hmac($data, $key);
157 return self::hmac($this->buildSignatureSigningString(), $key, false);
158 }
159
160 /**
161 * @param FileChunk $fileChunk
162 * @param string $destination
163 * @param string|null $uploadId
164 * @param string|null $partNumber
165 *
166 * @return stdClass
167 * @throws ClientException
168 * @throws HttpRequestException
169 */
170 public function putChunk(FileChunk $fileChunk, string $destination, ?string $uploadId=null, ?string $partNumber=null):stdClass {
171 $this->_reset();
172
173 $this->setMethod(self::METHOD_PUT);
174 $this->setURI($destination);
175 if($uploadId) $this->addParams('uploadId', $uploadId);
176 if($partNumber) $this->addParams('partNumber', $partNumber);
177 $this->setFileChunk($fileChunk);
178
179 $this->addHeader("content-type", "multipart/form-data");
180 $this->addHeader("content-length", $fileChunk->getSize());
181
182 return $this->_execute();
183 }
184
185 /**
186 * @param string|false $body
187 * @param string $uri
188 * @param array $params
189 *
190 * @return object
191 * @throws HttpRequestException
192 * @throws ClientException
193 */
194 public function putString($body, string $uri='/', array $params=[]):object {
195 $this->_reset();
196 $this->setMethod(self::METHOD_PUT);
197 $this->setURI($uri);
198 $this->setParams($params);
199 $this->setBody($body);
200
201 if($body) $this->addHeader("Content-Type", "multipart/form-data");
202 $this->addHeader("Content-Length", strlen($body));
203
204 return $this->_execute();
205 }
206
207 /**
208 * @param string $uri
209 * @param array $params
210 *
211 * @return object
212 * @throws HttpRequestException
213 * @throws ClientException
214 */
215 private function _request(string $uri, array $params):object {
216 $this->setURI($uri);
217 $this->setParams($params);
218
219 return $this->_execute();
220 }
221
222 /**
223 * @param string $uri
224 * @param string $destination
225 *
226 * @return object
227 * @throws ClientException
228 * @throws HttpRequestException
229 */
230 public function getObject(string $uri, string $destination):object {
231 $this->_reset();
232 $this->setMethod(self::METHOD_GET);
233 $this->setURI($uri);
234 $this->setDestination($destination);
235
236 return $this->_execute();
237 }
238
239 public function getObjectRange(string $uri, string $destination, int $start, int $end):object {
240 $this->_reset();
241 $this->setMethod(self::METHOD_GET);
242 $this->setURI($uri);
243 $this->setDestination($destination);
244 $this->addHeader('Range', 'bytes=' . $start . '-' . $end);
245
246 return $this->_execute();
247 }
248
249 /**
250 * @param string $uri
251 * @param array $params
252 *
253 * @return object
254 * @throws ClientException
255 * @throws HttpRequestException
256 */
257 public function get(string $uri='/', array $params=[]):object {
258 $this->_reset();
259 $this->setMethod(self::METHOD_GET);
260 return $this->_request($uri, $params);
261 }
262
263 /**
264 * @param string $uri
265 * @param array $params
266 * @param string|false $body
267 * @param string|null $contentType
268 *
269 * @return object
270 * @throws HttpRequestException
271 * @throws ClientException
272 */
273 public function post(string $uri='/', array $params=[], $body=false, ?string $contentType=null):object {
274 $this->_reset();
275
276 $this->setMethod(self::METHOD_POST);
277 if($body !== false) $this->setBody($body);
278 if($contentType) $this->addHeader('Content-Type', $contentType);
279 return $this->_request($uri, $params);
280 }
281
282 /**
283 * @param string $uri
284 * @param array $params
285 *
286 * @return object
287 * @throws ClientException
288 * @throws HttpRequestException
289 */
290 public function delete(string $uri='/', array $params=[]):object {
291 $this->_reset();
292 $this->setMethod(self::METHOD_DELETE);
293 return $this->_request($uri, $params);
294 }
295
296 /**
297 * @param string $uri
298 * @param array $params
299 *
300 * @return object
301 * @throws ClientException
302 * @throws HttpRequestException
303 */
304 public function head(string $uri='/', array $params=[]):object {
305 $this->_reset();
306 $this->setMethod(self::METHOD_HEAD);
307 return $this->_request($uri, $params);
308 }
309
310 private function _getAuthorization (): string {
311 $authorization = [];
312 $authorization[] = "Credential=" . $this->_key . '/' . $this->getAmzCredential();
313 $authorization[] = "SignedHeaders=" . implode(";", array_keys($this->getHeadersSignature()));
314 $authorization[] = "Signature=" . $this->buildSignature();
315 return self::ALGORITHM . " " . implode(",", $authorization);
316 }
317
318 /**
319 * @return object
320 * @throws HttpRequestException
321 * @throws ClientException
322 */
323 private function _execute():object {
324
325 $url = 'https://' . $this->_host;
326 $url .= $this->getURI();
327 if($this->getParams()) $url .= "?" . http_build_query($this->getParams());
328
329 if(!$this->_keepalive_timeout || !$this->_http) $this->_http = new JetHttp();
330 $this->_http->reset();
331
332 $this->_http
333 ->setSSLVerify($this->_verifyssl ? 1 : 0, $this->_verifyssl ? 2 : 0)
334 ->setReturnTransfer()
335 ->setConnectionTimeout(30)
336 ->setTimeout(43200)
337 ->setLowSpeed(1, 120);
338
339 if(($chunk = $this->getFileChunk())) {
340 $hashedContent = $chunk->getHash(self::ALGO);
341 } else {
342 if($this->getMethod() != self::METHOD_GET) $this->_http->setMethod($this->getMethod());
343 $this->_http->setBody($this->getBody());
344 $hashedContent = hash(self::ALGO, $this->getBody() !== false ? $this->getBody() : '');
345 }
346
347 if ($this->_keepalive_timeout) {
348 if ($this->_keepalive_requests) $keepalive_val = sprintf('timeout=%d,max=%d',$this->_keepalive_timeout, $this->_keepalive_requests);
349 else $keepalive_val = sprintf('timeout=%d',$this->_keepalive_timeout);
350
351 $this->addHeader('Connection', 'Keep-Alive');
352 $this->addHeader('Keep-Alive', $keepalive_val);
353 }
354
355 $this->addHeader("x-amz-content-sha256", $hashedContent, true);
356 $this->addHeader("x-amz-date", gmdate(self::DATE_FORMAT_LONG, $this->_date), true);
357 $this->addHeader("Authorization", $this->_getAuthorization());
358
359 $this->_http->setHeaders($this->getHeaders());
360
361 if(($destination = $this->getDestination())) {
362
363 if(!file_exists(dirname($destination)) || !is_dir(dirname($destination)))
364 throw new ClientException("Destination provided not exists (" . dirname($destination) . ")");
365
366 $fileDownload = new FileDownload($destination);
367 $response = $this->_http->download($url, $fileDownload);
368
369 $output = new stdClass();
370 $output->Headers = $response->getHeaders()->getHeaders();
371 $output->Body = $response->getBody() ? @simplexml_load_string(trim($response->getBody())) : new stdClass();
372
373 if($output->Body === false) $output->Body = $response->getBody();
374 elseif(isset($output->Body->Code) && $output->Body->Code) throw new ClientException($output->Body->Message ? ($output->Body->Message . " (" . $output->Body->Code . ")") : $output->Body->Code, $response->getHeaders()->getCode());
375
376 if($response->getHeaders()->getCode() < 200 || $response->getHeaders()->getCode() > 299)
377 throw new ClientException($response->getHeaders()->getMessage(), $response->getHeaders()->getCode());
378
379 return $output;
380 }
381
382 $response = $chunk ? $this->_http->uploadChunk($url, $chunk) : $this->_http->exec($url);
383
384 $output = new stdClass();
385 $output->Headers = $response->getHeaders()->getHeaders();
386
387 $output->Body = @simplexml_load_string(trim($response->getBody()));
388
389 if($output->Body === false) $output->Body = $response->getBody();
390 else {
391 $code = $output->Body->Code?? ($output->Body->code?? false);
392 $message = $output->Body->Message?? ($output->Body->message?? "");
393 if($code) throw new ClientException($message? ($message . " (" . $code . ")") : $code, $response->getHeaders()->getCode());
394 }
395
396 if($response->getHeaders()->getCode() < 200 || $response->getHeaders()->getCode() > 299)
397 throw new ClientException($response->getHeaders()->getMessage(), $response->getHeaders()->getCode());
398
399 return $output;
400 }
401
402 private static function http_build_query(array $params):string {
403 $url = [];
404 foreach($params as $key => $value) $url[] = rawurlencode($key) . "=" . rawurlencode($value);
405 sort($url);
406 return implode("&", $url);
407 }
408
409 public function close():void {
410 unset($this->_http);
411 }
412 }