File
11 months ago
.htaccess
11 months ago
JetHttp.php
11 months ago
JetHttpResponse.php
11 months ago
JetHttpResponseHeaders.php
11 months ago
index.html
11 months ago
web.config
11 months ago
JetHttp.php
501 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\Web; |
| 12 | |
| 13 | use JetBackup\Exception\HttpRequestException; |
| 14 | use JetBackup\Web\File\FileStream; |
| 15 | use JetBackup\Web\File\FileChunk; |
| 16 | use JetBackup\Web\File\FileDownload; |
| 17 | |
| 18 | defined("__JETBACKUP__") or die("Restricted Access."); |
| 19 | |
| 20 | class JetHttp { |
| 21 | |
| 22 | const METHOD_HEAD = 1; |
| 23 | const METHOD_PUT = 2; |
| 24 | const METHOD_DELETE = 3; |
| 25 | const METHOD_GET = 4; |
| 26 | const METHOD_POST = 5; |
| 27 | |
| 28 | const LINE_BREAK = "\r\n"; |
| 29 | |
| 30 | private $_curl; |
| 31 | private JetHttpResponse $_response; |
| 32 | private bool $_debug; |
| 33 | private array $_headers; |
| 34 | private array $_options; |
| 35 | private int $_method; |
| 36 | private $_body; |
| 37 | private bool $_is_ftp=false; |
| 38 | |
| 39 | /** |
| 40 | * @param bool $debug |
| 41 | */ |
| 42 | public function __construct(bool $debug=false) { |
| 43 | $this->reset(); |
| 44 | |
| 45 | $this->_debug = !!$debug; |
| 46 | if($this->_debug) $this->setVerbose(); |
| 47 | |
| 48 | $this->renew(); |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * @param bool $debug |
| 53 | * |
| 54 | * @return JetHttp |
| 55 | */ |
| 56 | public static function request(bool $debug=false):JetHttp { |
| 57 | return new JetHttp($debug); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * @return void |
| 62 | */ |
| 63 | public function reset():void { |
| 64 | $this->_headers = []; |
| 65 | $this->_options = []; |
| 66 | $this->_method = 0; |
| 67 | $this->_body = false; |
| 68 | $this->_response = new JetHttpResponse(); |
| 69 | |
| 70 | if($this->_curl) curl_reset($this->_curl); |
| 71 | } |
| 72 | |
| 73 | /** |
| 74 | * @param string $key |
| 75 | * @param string $value |
| 76 | * |
| 77 | * @return JetHttp |
| 78 | */ |
| 79 | public function addHeader(string $key, string $value):JetHttp { |
| 80 | $headers = $this->getHeaders(); |
| 81 | $headers[$key] = $value; |
| 82 | $this->setHeaders($headers); |
| 83 | return $this; |
| 84 | } |
| 85 | |
| 86 | /** |
| 87 | * @return array |
| 88 | */ |
| 89 | public function getHeaders(): array { |
| 90 | return $this->_headers; |
| 91 | } |
| 92 | |
| 93 | /** |
| 94 | * @param array $headers |
| 95 | * |
| 96 | * @return JetHttp |
| 97 | */ |
| 98 | public function setHeaders(array $headers):JetHttp { |
| 99 | $this->_headers = $headers; |
| 100 | return $this; |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * @param int $option |
| 105 | * @param mixed $value |
| 106 | * |
| 107 | * @return JetHttp |
| 108 | */ |
| 109 | public function addOption(int $option, $value):JetHttp { |
| 110 | $this->_options[$option] = $value; |
| 111 | return $this; |
| 112 | } |
| 113 | |
| 114 | /** |
| 115 | * @return array |
| 116 | */ |
| 117 | public function getOption(int $option) { |
| 118 | return $this->_options[$option] ?? null; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * @return array |
| 123 | */ |
| 124 | public function getOptions(): array { |
| 125 | return $this->_options; |
| 126 | } |
| 127 | |
| 128 | /** |
| 129 | * @param array $options |
| 130 | * |
| 131 | * @return JetHttp |
| 132 | */ |
| 133 | public function setOptions(array $options):JetHttp { |
| 134 | $this->_options = $options; |
| 135 | return $this; |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * @param int $option |
| 140 | * |
| 141 | * @return bool |
| 142 | */ |
| 143 | public function optionExists(int $option):bool { |
| 144 | $options = $this->getOptions(); |
| 145 | return isset($options[$option]); |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * @return int |
| 150 | */ |
| 151 | public function getMethod(): int { |
| 152 | return $this->_method; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * @param int $method |
| 157 | * |
| 158 | * @return JetHttp |
| 159 | */ |
| 160 | public function setMethod(int $method):JetHttp { |
| 161 | $this->_method = $method; |
| 162 | return $this; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * @return string|array|false |
| 167 | */ |
| 168 | public function getBody() { |
| 169 | return $this->_body; |
| 170 | } |
| 171 | |
| 172 | /** |
| 173 | * @param string|array|false $body |
| 174 | * |
| 175 | * @return JetHttp |
| 176 | */ |
| 177 | public function setBody($body):JetHttp { |
| 178 | $this->_body = $body; |
| 179 | return $this; |
| 180 | } |
| 181 | |
| 182 | /** |
| 183 | * @param null $stderr |
| 184 | * |
| 185 | * @return JetHttp |
| 186 | */ |
| 187 | public function setVerbose($stderr=null):JetHttp { |
| 188 | $this->addOption(CURLOPT_VERBOSE, true); |
| 189 | if($stderr !== null) $this->addOption(CURLOPT_STDERR, $stderr); |
| 190 | return $this; |
| 191 | } |
| 192 | |
| 193 | /** |
| 194 | * @param int|null $limit |
| 195 | * @param int|null $time |
| 196 | * |
| 197 | * @return JetHttp |
| 198 | */ |
| 199 | public function setLowSpeed(?int $limit=null, ?int $time=null):JetHttp { |
| 200 | if($limit !== null) $this->addOption(CURLOPT_LOW_SPEED_LIMIT, $limit); |
| 201 | if($time !== null) $this->addOption(CURLOPT_LOW_SPEED_TIME, $time); |
| 202 | return $this; |
| 203 | } |
| 204 | |
| 205 | /** |
| 206 | * @param int|null $peer |
| 207 | * @param int|null $host |
| 208 | * |
| 209 | * @return JetHttp |
| 210 | */ |
| 211 | public function setSSLVerify(?int $peer=null, ?int $host=null):JetHttp { |
| 212 | if($peer !== null) $this->addOption(CURLOPT_SSL_VERIFYPEER, $peer); |
| 213 | if($host !== null) $this->addOption(CURLOPT_SSL_VERIFYHOST, $host); |
| 214 | return $this; |
| 215 | } |
| 216 | |
| 217 | public function setAuth(?string $username=null, ?string $password=null, ?int $type=null):JetHttp { |
| 218 | if($username !== null) $this->addOption(CURLOPT_USERNAME, $username); |
| 219 | if($password !== null) $this->addOption(CURLOPT_USERPWD, $password); |
| 220 | if($type !== null) $this->addOption(CURLOPT_HTTPAUTH, $type); |
| 221 | return $this; |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * @param int $port |
| 226 | * |
| 227 | * @return $this |
| 228 | */ |
| 229 | public function setPort(int $port):JetHttp { |
| 230 | $this->addOption(CURLOPT_PORT, $port); |
| 231 | return $this; |
| 232 | } |
| 233 | |
| 234 | /** |
| 235 | * @param int $timeout |
| 236 | * |
| 237 | * @return JetHttp |
| 238 | */ |
| 239 | public function setTimeout(int $timeout):JetHttp { |
| 240 | $this->addOption(CURLOPT_TIMEOUT, $timeout); |
| 241 | return $this; |
| 242 | } |
| 243 | |
| 244 | /** |
| 245 | * @param int $timeout |
| 246 | * |
| 247 | * @return JetHttp |
| 248 | */ |
| 249 | public function setConnectionTimeout(int $timeout):JetHttp { |
| 250 | $this->addOption(CURLOPT_CONNECTTIMEOUT, $timeout); |
| 251 | return $this; |
| 252 | } |
| 253 | |
| 254 | /** |
| 255 | * @return JetHttp |
| 256 | */ |
| 257 | public function setFollowLocation():JetHttp { |
| 258 | $this->addOption(CURLOPT_FOLLOWLOCATION, 1); |
| 259 | return $this; |
| 260 | } |
| 261 | |
| 262 | public function setReturnTransfer():JetHttp { |
| 263 | $this->addOption(CURLOPT_RETURNTRANSFER, 1); |
| 264 | return $this; |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * @param string $url |
| 269 | * @param FileChunk $chunk |
| 270 | * @param bool $self_method |
| 271 | * |
| 272 | * @return JetHttpResponse |
| 273 | * @throws HttpRequestException |
| 274 | */ |
| 275 | public function uploadChunk(string $url, FileChunk $chunk, $self_method=false):JetHttpResponse { |
| 276 | |
| 277 | if(!$self_method) { |
| 278 | $this->setMethod(0); |
| 279 | $this->addOption(CURLOPT_PUT, 1); |
| 280 | } |
| 281 | |
| 282 | $this->addOption(CURLOPT_INFILE, $chunk->getFile()->getDescriptor()); |
| 283 | $this->addOption(CURLOPT_INFILESIZE, $chunk->getSize()); |
| 284 | $this->addOption(CURLOPT_READFUNCTION, function ($ch, $fd, $length) use ($chunk) { |
| 285 | return $chunk->readPiece($length); |
| 286 | }); |
| 287 | |
| 288 | return $this->exec($url); |
| 289 | } |
| 290 | |
| 291 | /** |
| 292 | * @param string $url |
| 293 | * @param FileStream $file |
| 294 | * |
| 295 | * @return JetHttpResponse |
| 296 | * @throws HttpRequestException |
| 297 | */ |
| 298 | public function upload(string $url, FileStream $file):JetHttpResponse { |
| 299 | $this->setMethod(0); |
| 300 | $this->addOption(CURLOPT_PUT, 1); |
| 301 | $this->addOption(CURLOPT_INFILE, $file->getDescriptor()); |
| 302 | $this->addOption(CURLOPT_INFILESIZE, $file->getSize()); |
| 303 | $this->addOption(CURLOPT_READFUNCTION, function ($ch, $fd, $length) use ($file) { |
| 304 | return $file->read($length); |
| 305 | }); |
| 306 | |
| 307 | return $this->exec($url); |
| 308 | } |
| 309 | |
| 310 | /** |
| 311 | * @param string $url |
| 312 | * @param FileStream $stream |
| 313 | * @param string $details |
| 314 | * |
| 315 | * @return JetHttpResponse |
| 316 | * @throws HttpRequestException |
| 317 | */ |
| 318 | public function uploadString(string $url, FileStream $stream, string $details):JetHttpResponse { |
| 319 | |
| 320 | $boundary = uniqid(); |
| 321 | $delimiter = '-------------' . $boundary; |
| 322 | |
| 323 | $body = "--$delimiter" . self::LINE_BREAK; |
| 324 | $body .= "Content-Type: application/json" . self::LINE_BREAK . self::LINE_BREAK; |
| 325 | $body .= $details . self::LINE_BREAK; |
| 326 | $body .= "--$delimiter" . self::LINE_BREAK; |
| 327 | $body .= "Content-Type: " . $stream->getMimeType() . self::LINE_BREAK . self::LINE_BREAK; |
| 328 | $body .= ($stream->getSize() > 0 ? $stream->read() : '') . self::LINE_BREAK; |
| 329 | $body .= "--$delimiter--" . self::LINE_BREAK; |
| 330 | |
| 331 | $this->addHeader('Content-Type', 'multipart/related; boundary=' . $delimiter); |
| 332 | $this->addHeader('Content-Length', strlen($body)); |
| 333 | $this->setMethod(self::METHOD_POST); |
| 334 | $this->setBody($body); |
| 335 | |
| 336 | return $this->exec($url); |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * @param string $url |
| 341 | * @param FileDownload $fileDownload |
| 342 | * |
| 343 | * @return JetHttpResponse |
| 344 | * @throws HttpRequestException |
| 345 | */ |
| 346 | public function download(string $url, FileDownload $fileDownload):JetHttpResponse { |
| 347 | |
| 348 | $this->addOption(CURLOPT_WRITEFUNCTION, function($ch, $str) use ($fileDownload) { |
| 349 | |
| 350 | $response = $this->_response->getHeaders(); |
| 351 | |
| 352 | if(!$this->_is_ftp && (!$response || $response->getCode() < 200 || $response->getCode() > 299)) { |
| 353 | $fileDownload->deleteFile(); |
| 354 | $this->_response->appendBody($str); |
| 355 | } else { |
| 356 | $fileDownload->writeFile($str); |
| 357 | } |
| 358 | |
| 359 | return strlen($str); |
| 360 | |
| 361 | }); |
| 362 | |
| 363 | $this->_prepare($url); |
| 364 | curl_exec($this->_curl); |
| 365 | return $this->_finalize(); |
| 366 | } |
| 367 | |
| 368 | /** |
| 369 | * @param string $url |
| 370 | * |
| 371 | * @return JetHttpResponse |
| 372 | * @throws HttpRequestException |
| 373 | */ |
| 374 | public function exec(string $url):JetHttpResponse { |
| 375 | $this->_prepare($url); |
| 376 | $this->_response->setBody(curl_exec($this->_curl)); |
| 377 | return $this->_finalize(); |
| 378 | } |
| 379 | |
| 380 | /** |
| 381 | * @return JetHttpResponse |
| 382 | * @throws HttpRequestException |
| 383 | */ |
| 384 | private function _finalize(): JetHttpResponse { |
| 385 | $error_code = curl_errno($this->_curl); |
| 386 | $error_message = curl_error($this->_curl); |
| 387 | |
| 388 | /** |
| 389 | * Detect known transient SSL/network errors (case-insensitive) |
| 390 | * By default the error code will be 0, so we return custom error |
| 391 | * code 499 so our wrapper clients will trigger retry |
| 392 | */ |
| 393 | $retryableSSL = ( |
| 394 | stripos($error_message, 'SSL_ERROR_SYSCALL') !== false || |
| 395 | stripos($error_message, 'Connection reset') !== false || |
| 396 | stripos($error_message, 'Connection aborted') !== false || |
| 397 | stripos($error_message, 'timeout') !== false || |
| 398 | stripos($error_message, 'timed out') !== false || |
| 399 | stripos($error_message, 'could not resolve host') !== false || // DNS issue |
| 400 | stripos($error_message, 'Failed to connect to') !== false || // TCP connection issue |
| 401 | stripos($error_message, 'Transfer closed') !== false // abrupt remote closure |
| 402 | ); |
| 403 | |
| 404 | if ($error_code || $retryableSSL) { |
| 405 | $code = $retryableSSL ? 499 : $error_code; |
| 406 | throw new HttpRequestException($error_message, $code); |
| 407 | } |
| 408 | |
| 409 | return $this->_response; |
| 410 | } |
| 411 | |
| 412 | |
| 413 | /** |
| 414 | * @param string $url |
| 415 | * |
| 416 | * @return void |
| 417 | */ |
| 418 | private function _prepare(string $url):void { |
| 419 | |
| 420 | $this->_is_ftp = str_starts_with($url, 'ftp:') || str_starts_with($url, 'ftps:'); |
| 421 | $this->_response->isFTP($this->_is_ftp); |
| 422 | |
| 423 | $this->addOption(CURLOPT_URL, $url); |
| 424 | $this->addOption(CURLOPT_HEADER, 0); |
| 425 | $this->addOption(CURLOPT_HEADERFUNCTION, function($ch, $str) { |
| 426 | $this->_response->appendHeadersBuffer($str); |
| 427 | return strlen($str); |
| 428 | }); |
| 429 | |
| 430 | switch($this->getMethod()) { |
| 431 | case self::METHOD_HEAD: $this->addOption(CURLOPT_NOBODY, 1); break; |
| 432 | case self::METHOD_PUT: $this->addOption(CURLOPT_CUSTOMREQUEST, 'PUT'); break; |
| 433 | case self::METHOD_DELETE: $this->addOption(CURLOPT_CUSTOMREQUEST, 'DELETE'); break; |
| 434 | case self::METHOD_GET: $this->addOption(CURLOPT_CUSTOMREQUEST, 'GET'); break; |
| 435 | case self::METHOD_POST: $this->addOption(CURLOPT_POST, 1); break; |
| 436 | } |
| 437 | |
| 438 | if($this->getBody() !== false) $this->addOption(CURLOPT_POSTFIELDS, $this->getBody()); |
| 439 | |
| 440 | $headers = []; |
| 441 | foreach($this->getHeaders() as $key => $value) $headers[] = "$key:$value"; |
| 442 | if($headers) $this->addOption(CURLOPT_HTTPHEADER, $headers); |
| 443 | |
| 444 | curl_setopt_array($this->_curl, $this->getOptions()); |
| 445 | //$this->_logRequestDetails($url, $headers); |
| 446 | } |
| 447 | |
| 448 | // Use for heavy debugs |
| 449 | private function _logRequestDetails(string $url, array $headers): void { |
| 450 | $options = $this->getOptions(); |
| 451 | $method = $this->getMethod(); |
| 452 | $body = $this->getBody(); |
| 453 | |
| 454 | $methodMap = [ |
| 455 | self::METHOD_HEAD => 'HEAD', |
| 456 | self::METHOD_PUT => 'PUT', |
| 457 | self::METHOD_DELETE => 'DELETE', |
| 458 | self::METHOD_GET => 'GET', |
| 459 | self::METHOD_POST => 'POST', |
| 460 | ]; |
| 461 | |
| 462 | $logMessage = "[JetHttp Request]\n"; |
| 463 | $logMessage .= "URL: $url\n"; |
| 464 | $logMessage .= "Method: " . $methodMap[$method] ?? 'UNKNOWN' . "\n"; |
| 465 | $logMessage .= "Headers: " . print_r($headers, true) . "\n"; |
| 466 | $logMessage .= "Options: " . print_r($options, true) . "\n"; |
| 467 | if ($body) { |
| 468 | $logMessage .= "Body: " . (is_string($body) ? $body : json_encode($body, JSON_PRETTY_PRINT)) . "\n"; |
| 469 | } |
| 470 | |
| 471 | // Choose logging option |
| 472 | //error_log($logMessage); |
| 473 | //echo $logMessage . "\n"; |
| 474 | // file_put_contents('/path/to/log.txt', $logMessage, FILE_APPEND); |
| 475 | |
| 476 | //foreach($this->getOptions() as $option => $value) echo "OPTION: $option -> " . (is_callable($value) ? "FUNC" : print_r($value, true)) . "\n"; |
| 477 | //echo "\n\n"; |
| 478 | } |
| 479 | |
| 480 | |
| 481 | /** |
| 482 | * @return void |
| 483 | */ |
| 484 | public function close():void { |
| 485 | $this->_curl = false; |
| 486 | } |
| 487 | |
| 488 | /** |
| 489 | * @return void |
| 490 | */ |
| 491 | public function renew():void { |
| 492 | $this->_curl = curl_init(); |
| 493 | } |
| 494 | |
| 495 | /** |
| 496 | * |
| 497 | */ |
| 498 | public function __destruct() { |
| 499 | $this->close(); |
| 500 | } |
| 501 | } |