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 | } |