Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
httpclient.php
1<?php
2
10namespace Bitrix\Main\Web;
11
15use Psr\Log;
16use Psr\Http\Message\RequestInterface;
17use Psr\Http\Message\ResponseInterface;
18use Psr\Http\Client\ClientInterface;
19use Psr\Http\Client\ClientExceptionInterface;
20use Psr\Http\Client\NetworkExceptionInterface;
21use Http\Promise\Promise as PromiseInterface;
22
23class HttpClient implements Log\LoggerAwareInterface, ClientInterface, Http\DebugInterface
24{
25 use Log\LoggerAwareTrait;
27
28 const HTTP_1_0 = '1.0';
29 const HTTP_1_1 = '1.1';
30
31 const HTTP_GET = 'GET';
32 const HTTP_POST = 'POST';
33 const HTTP_PUT = 'PUT';
34 const HTTP_HEAD = 'HEAD';
35 const HTTP_PATCH = 'PATCH';
36 const HTTP_DELETE = 'DELETE';
37 const HTTP_OPTIONS = 'OPTIONS';
38
42
43 protected $proxyHost = '';
44 protected $proxyPort = 80;
45 protected $proxyUser = '';
46 protected $proxyPassword = '';
49 protected $waitResponse = true;
50 protected $redirect = true;
51 protected $redirectMax = 5;
52 protected $redirectCount = 0;
53 protected $compress = false;
55 protected $requestCharset = '';
56 protected $sslVerify = true;
57 protected $bodyLengthMax = 0;
58 protected $privateIp = true;
59 protected $contextOptions = [];
60 protected $outputStream = null;
61 protected $useCurl = false;
62 protected $curlLogFile = null;
63 protected $shouldFetchBody = null;
64
66 protected ?Http\Request $request = null;
67 protected ?Http\Response $response = null;
68 protected ?Http\Queue $queue = null;
69 protected ?IpAddress $effectiveIp = null;
70 protected $effectiveUrl;
71 protected $error = [];
72
97 public function __construct(array $options = null)
98 {
99 $this->headers = new HttpHeaders();
100
101 if ($options === null)
102 {
103 $options = [];
104 }
105
106 $defaultOptions = Configuration::getValue('http_client_options');
107 if ($defaultOptions !== null)
108 {
109 $options += $defaultOptions;
110 }
111
112 if (!empty($options))
113 {
114 if (isset($options['redirect']))
115 {
116 $this->setRedirect($options["redirect"], $options["redirectMax"] ?? null);
117 }
118 if (isset($options['waitResponse']))
119 {
120 $this->waitResponse($options['waitResponse']);
121 }
122 if (isset($options['socketTimeout']))
123 {
124 $this->setTimeout($options['socketTimeout']);
125 }
126 if (isset($options['streamTimeout']))
127 {
128 $this->setStreamTimeout($options['streamTimeout']);
129 }
130 if (isset($options['version']))
131 {
132 $this->setVersion($options['version']);
133 }
134 if (isset($options['proxyHost']))
135 {
136 $this->setProxy($options['proxyHost'], $options['proxyPort'] ?? null, $options['proxyUser'] ?? null, $options['proxyPassword'] ?? null);
137 }
138 if (isset($options['compress']))
139 {
140 $this->setCompress($options['compress']);
141 }
142 if (isset($options['charset']))
143 {
144 $this->setCharset($options['charset']);
145 }
146 if (isset($options['disableSslVerification']) && $options['disableSslVerification'] === true)
147 {
148 $this->disableSslVerification();
149 }
150 if (isset($options['bodyLengthMax']))
151 {
152 $this->setBodyLengthMax($options['bodyLengthMax']);
153 }
154 if (isset($options['privateIp']))
155 {
156 $this->setPrivateIp($options['privateIp']);
157 }
158 if (isset($options['debugLevel']))
159 {
160 $this->setDebugLevel((int)$options['debugLevel']);
161 }
162 if (isset($options['cookies']))
163 {
164 $this->setCookies($options['cookies']);
165 }
166 if (isset($options['headers']))
167 {
168 $this->setHeaders($options['headers']);
169 }
170 if (isset($options['useCurl']))
171 {
172 $this->useCurl = (bool)$options['useCurl'];
173 }
174 if (isset($options['curlLogFile']))
175 {
176 $this->curlLogFile = $options['curlLogFile'];
177 }
178 }
179
180 if ($this->useCurl && !function_exists('curl_init'))
181 {
182 $this->useCurl = false;
183 }
184 }
185
192 public function get($url)
193 {
194 if ($this->query(Http\Method::GET, $url))
195 {
196 return $this->getResult();
197 }
198 return false;
199 }
200
207 public function head($url)
208 {
209 if ($this->query(Http\Method::HEAD, $url))
210 {
211 return $this->getHeaders();
212 }
213 return false;
214 }
215
224 public function post($url, $postData = null, $multipart = false)
225 {
226 if ($multipart)
227 {
228 $postData = $this->prepareMultipart($postData);
229 if ($postData === false)
230 {
231 return false;
232 }
233 }
234
235 if ($this->query(Http\Method::POST, $url, $postData))
236 {
237 return $this->getResult();
238 }
239 return false;
240 }
241
249 protected function prepareMultipart($postData)
250 {
251 if (is_array($postData))
252 {
253 try
254 {
255 $data = new Http\MultipartStream($postData);
256 $this->setHeader('Content-type', 'multipart/form-data; boundary=' . $data->getBoundary());
257
258 return $data;
259 }
260 catch (ArgumentException $e)
261 {
262 $this->addError('MULTIPART', $e->getMessage(), true);
263 return false;
264 }
265 }
266
267 return $postData;
268 }
269
278 public function query($method, $url, $entityBody = null)
279 {
280 $this->effectiveUrl = $url;
281 $this->effectiveIp = null;
282 $this->error = [];
283
284 if (is_array($entityBody))
285 {
286 $entityBody = new Http\FormStream($entityBody);
287 }
288
289 if ($entityBody instanceof Http\Stream)
290 {
291 $body = $entityBody;
292 }
293 elseif (is_resource($entityBody))
294 {
295 $body = new Http\Stream($entityBody);
296 }
297 else
298 {
299 $body = new Http\Stream('php://temp', 'r+');
300 $body->write($entityBody ?? '');
301 }
302
303 $this->redirectCount = 0;
304
305 while (true)
306 {
307 //Only absoluteURI is accepted
308 //Location response-header field must be absoluteURI either
309 $uri = new Uri($this->effectiveUrl);
310
311 // make a PSR-7 request
312 $request = new Http\Request($method, $uri, $this->headers->getHeaders(), $body, $this->version);
313
314 try
315 {
316 // PSR-18 magic is here
317 $this->sendRequest($request);
318 }
319 catch (ClientExceptionInterface $e)
320 {
321 // compatibility mode
322 if ($e instanceof NetworkExceptionInterface)
323 {
324 $this->addError('NETWORK', $e->getMessage());
325 }
326 return false;
327 }
328
329 if (!$this->waitResponse)
330 {
331 return true;
332 }
333
334 if ($this->redirect && ($location = $this->getHeaders()->get('Location')) !== null && $location != '')
335 {
336 if ($this->redirectCount < $this->redirectMax)
337 {
338 // there can be different host in Location
339 $this->headers->delete('Host');
340 $this->effectiveUrl = $location;
341
342 $status = $this->getStatus();
343 if ($status == 302 || $status == 303)
344 {
345 $method = Http\Method::GET;
346 }
347
348 $this->redirectCount++;
349 }
350 else
351 {
352 $this->addError('REDIRECT', "Maximum number of redirects ({$this->redirectMax}) has been reached at URL {$url}", true);
353 return false;
354 }
355 }
356 else
357 {
358 return true;
359 }
360 }
361 }
362
371 public function setHeader($name, $value, $replace = true)
372 {
373 if ($replace || !$this->headers->has($name))
374 {
375 $this->headers->set($name, $value);
376 }
377 return $this;
378 }
379
386 public function setHeaders(array $headers)
387 {
388 foreach ($headers as $name => $value)
389 {
390 $this->setHeader($name, $value);
391 }
392 return $this;
393 }
394
401 {
402 if ($this->request)
403 {
404 return $this->request->getHeadersCollection();
405 }
406 return $this->headers;
407 }
408
412 public function clearHeaders()
413 {
414 $this->headers->clear();
415 }
416
423 public function setCookies(array $cookies)
424 {
425 if (!empty($cookies))
426 {
427 $this->setHeader('Cookie', (new HttpCookies($cookies))->implode());
428 }
429
430 return $this;
431 }
432
440 public function setAuthorization($user, $pass)
441 {
442 $this->setHeader('Authorization', 'Basic ' . base64_encode($user . ':' . $pass));
443 return $this;
444 }
445
453 public function setRedirect($value, $max = null)
454 {
455 $this->redirect = (bool)$value;
456 if ($max !== null)
457 {
458 $this->redirectMax = intval($max);
459 }
460 return $this;
461 }
462
469 public function waitResponse($value)
470 {
471 $this->waitResponse = (bool)$value;
472 if (!$this->waitResponse)
473 {
474 $this->setStreamTimeout(self::DEFAULT_STREAM_TIMEOUT_NO_WAIT);
475 }
476
477 return $this;
478 }
479
486 public function setTimeout($value)
487 {
488 $this->socketTimeout = intval($value);
489 return $this;
490 }
491
498 public function setStreamTimeout($value)
499 {
500 $this->streamTimeout = intval($value);
501 return $this;
502 }
503
510 public function setVersion($value)
511 {
512 $this->version = $value;
513 return $this;
514 }
515
524 public function setCompress($value)
525 {
526 $this->compress = (bool)$value;
527 return $this;
528 }
529
536 public function setCharset($value)
537 {
538 $this->requestCharset = $value;
539 return $this;
540 }
541
547 public function disableSslVerification()
548 {
549 $this->sslVerify = false;
550 return $this;
551 }
552
559 public function setPrivateIp($value)
560 {
561 $this->privateIp = (bool)$value;
562 return $this;
563 }
564
574 public function setProxy($proxyHost, $proxyPort = null, $proxyUser = null, $proxyPassword = null)
575 {
576 $this->proxyHost = $proxyHost;
577 $proxyPort = (int)$proxyPort;
578 if ($proxyPort > 0)
579 {
580 $this->proxyPort = $proxyPort;
581 }
582 $this->proxyUser = $proxyUser ?? '';
583 $this->proxyPassword = $proxyPassword ?? '';
584
585 return $this;
586 }
587
596 public function setOutputStream($handler)
597 {
598 $this->outputStream = $handler;
599 return $this;
600 }
601
609 {
610 $this->bodyLengthMax = intval($bodyLengthMax);
611 return $this;
612 }
613
621 public function download($url, $filePath)
622 {
623 $result = $this->query(Http\Method::GET, $url);
624
625 if ($result && ($status = $this->getStatus()) >= 200 && $status < 300)
626 {
627 $this->saveFile($filePath);
628
629 return true;
630 }
631
632 return false;
633 }
634
640 public function saveFile($filePath)
641 {
642 $dir = IO\Path::getDirectory($filePath);
643 IO\Directory::createDirectory($dir);
644
645 $file = new IO\File($filePath);
646 $handler = $file->open('w+');
647
648 $this->setOutputStream($handler);
649 $this->getResult();
650
651 $file->close();
652 }
653
659 public function getEffectiveUrl()
660 {
661 return $this->effectiveUrl;
662 }
663
670 public function setContextOptions(array $options)
671 {
672 $this->contextOptions = array_replace_recursive($this->contextOptions, $options);
673 return $this;
674 }
675
681 public function getHeaders(): HttpHeaders
682 {
683 if ($this->response)
684 {
685 return $this->response->getHeadersCollection();
686 }
687 return new HttpHeaders();
688 }
689
695 public function getCookies(): HttpCookies
696 {
697 return $this->getHeaders()->getCookies();
698 }
699
705 public function getStatus()
706 {
707 if ($this->response)
708 {
709 return $this->response->getStatusCode();
710 }
711 return 0;
712 }
713
719 public function getResult()
720 {
721 $result = '';
722 if ($this->response)
723 {
724 $body = $this->response->getBody();
725
726 if ($this->outputStream === null)
727 {
728 $result = (string)$body;
729 }
730 else
731 {
732 $body->copyTo($this->outputStream);
733 }
734 }
735 return $result;
736 }
737
743 public function getResponse()
744 {
745 return $this->response;
746 }
747
753 public function getError()
754 {
755 return $this->error;
756 }
757
763 public function getContentType()
764 {
765 return $this->getHeaders()->getContentType();
766 }
767
773 public function getCharset()
774 {
775 return $this->getHeaders()->getCharset();
776 }
777
783 public function getPeerAddress()
784 {
785 if ($this->effectiveIp)
786 {
787 return (string)$this->effectiveIp;
788 }
789 return false;
790 }
791
792 protected function addError($code, $message, $triggerWarning = false)
793 {
794 $this->error[$code] = $message;
795
796 if ($triggerWarning)
797 {
798 trigger_error($message, E_USER_WARNING);
799 }
800 }
801
802 protected function buildRequest(RequestInterface $request): RequestInterface
803 {
804 $method = $request->getMethod();
805 $uri = $request->getUri();
806 $body = $request->getBody();
807
808 $punyUri = new Uri('http://' . $uri->getHost());
809 if (($punyHost = $punyUri->convertToPunycode()) != $uri->getHost())
810 {
811 $uri = $uri->withHost($punyHost);
812 $request = $request->withUri($uri);
813 }
814
815 if (!$request->hasHeader('Host'))
816 {
817 $request = $request->withHeader('Host', $uri->getHost());
818 }
819 if (!$request->hasHeader('Connection'))
820 {
821 $request = $request->withHeader('Connection', 'close');
822 }
823 if (!$request->hasHeader('Accept'))
824 {
825 $request = $request->withHeader('Accept', '*/*');
826 }
827 if (!$request->hasHeader('Accept-Language'))
828 {
829 $request = $request->withHeader('Accept-Language', 'en');
830 }
831 if ($this->compress)
832 {
833 $request = $request->withHeader('Accept-Encoding', 'gzip');
834 }
835 if (($userInfo = $uri->getUserInfo()) != '')
836 {
837 $request = $request->withHeader('Authorization', 'Basic ' . base64_encode($userInfo));
838 }
839 if ($this->proxyHost != '' && $this->proxyUser != '')
840 {
841 $request = $request->withHeader('Proxy-Authorization', 'Basic ' . base64_encode($this->proxyUser . ':' . $this->proxyPassword));
842 }
843
844 // the client doesn't support "Expect-Continue", set empty value for cURL
845 if ($this->useCurl)
846 {
847 $request = $request->withHeader('Expect', '');
848 }
849
850 if ($method == Http\Method::POST)
851 {
852 //special processing for POST requests
853 if (!$request->hasHeader('Content-Type'))
854 {
855 $contentType = 'application/x-www-form-urlencoded';
856 if ($this->requestCharset != '')
857 {
858 $contentType .= '; charset=' . $this->requestCharset;
859 }
860 $request = $request->withHeader('Content-Type', $contentType);
861 }
862 }
863
864 $size = $body->getSize();
865
866 if ($size > 0 || $method == Http\Method::POST || $method == Http\Method::PUT)
867 {
868 // A valid Content-Length field value is required on all HTTP/1.0 request messages containing an entity body.
869 if (!$request->hasHeader('Content-Length'))
870 {
871 $request = $request->withHeader('Content-Length', $size ?? strlen((string)$body));
872 }
873 }
874
875 // Here's the chance to tune up the client and to rebuild the request.
876 $event = new Http\RequestEvent($this, $request, 'OnHttpClientBuildRequest');
877 $event->send();
878
879 foreach ($event->getResults() as $eventResult)
880 {
881 $request = $eventResult->getRequest();
882 }
883
884 return $request;
885 }
886
887 protected function checkRequest(RequestInterface $request): bool
888 {
889 $uri = $request->getUri();
890
891 $scheme = $uri->getScheme();
892 if ($scheme !== 'http' && $scheme !== 'https')
893 {
894 $this->addError('URI_SCHEME', 'Only http and https shemes are supported.');
895 return false;
896 }
897
898 if ($uri->getHost() == '')
899 {
900 $this->addError('URI_HOST', 'Incorrect host in URI.');
901 return false;
902 }
903
904 $punyUri = new Uri('http://' . $uri->getHost());
905 $error = $punyUri->convertToPunycode();
906 if ($error instanceof \Bitrix\Main\Error)
907 {
908 $this->addError('URI_PUNICODE', "Error converting hostname to punycode: {$error->getMessage()}");
909 return false;
910 }
911
912 if (!$this->privateIp)
913 {
914 $ip = IpAddress::createByUri($punyUri);
915 if ($ip->isPrivate())
916 {
917 $this->addError('PRIVATE_IP', "Resolved IP is incorrect or private: {$ip->get()}");
918 return false;
919 }
920 $this->effectiveIp = $ip;
921 }
922
923 return true;
924 }
925
929 public function sendRequest(RequestInterface $request): ResponseInterface
930 {
931 if (!$this->checkRequest($request))
932 {
933 throw new Http\RequestException($request, reset($this->error));
934 }
935
936 $this->request = $this->buildRequest($request);
937
938 $queue = $this->createQueue(false);
939
940 $handler = $this->createHandler($this->request);
941
942 $promise = $this->createPromise($handler, $queue);
943
944 $queue->add($promise);
945
946 $this->response = $promise->wait();
947
948 return $this->response;
949 }
950
956 public function sendAsyncRequest(RequestInterface $request): PromiseInterface
957 {
958 if (!$this->checkRequest($request))
959 {
960 throw new Http\RequestException($request, reset($this->error));
961 }
962
963 $this->request = $this->buildRequest($request);
964
965 if ($this->queue === null)
966 {
967 $this->queue = $this->createQueue();
968 }
969
970 $handler = $this->createHandler($this->request, true);
971
972 $promise = $this->createPromise($handler, $this->queue);
973
974 $this->queue->add($promise);
975
976 return $promise;
977 }
978
984 protected function createHandler(RequestInterface $request, bool $async = false)
985 {
986 if ($this->sslVerify === false)
987 {
988 $this->contextOptions['ssl']['verify_peer_name'] = false;
989 $this->contextOptions['ssl']['verify_peer'] = false;
990 $this->contextOptions['ssl']['allow_self_signed'] = true;
991 }
992
993 $handlerOptions = [
994 'waitResponse' => $this->waitResponse,
995 'bodyLengthMax' => $this->bodyLengthMax,
996 'proxyHost' => $this->proxyHost,
997 'proxyPort' => $this->proxyPort,
998 'effectiveIp' => $this->effectiveIp,
999 'contextOptions' => $this->contextOptions,
1000 'socketTimeout' => $this->socketTimeout,
1001 'streamTimeout' => $this->streamTimeout,
1002 'async' => $async,
1003 'curlLogFile' => $this->curlLogFile,
1004 ];
1005
1006 $responseBuilder = new Http\ResponseBuilder();
1007
1008 if ($this->useCurl)
1009 {
1010 $handler = new Http\Curl\Handler($request, $responseBuilder, $handlerOptions);
1011 }
1012 else
1013 {
1014 $handler = new Http\Socket\Handler($request, $responseBuilder, $handlerOptions);
1015 }
1016
1017 if ($this->logger !== null)
1018 {
1019 $handler->setLogger($this->logger);
1020 $handler->setDebugLevel($this->debugLevel);
1021 }
1022
1023 if ($this->shouldFetchBody !== null)
1024 {
1025 $handler->shouldFetchBody($this->shouldFetchBody);
1026 }
1027
1028 return $handler;
1029 }
1030
1036 protected function createPromise($handler, Http\Queue $queue)
1037 {
1038 if ($this->useCurl)
1039 {
1040 return new Http\Curl\Promise($handler, $queue);
1041 }
1042 return new Http\Socket\Promise($handler, $queue);
1043 }
1044
1049 protected function createQueue(bool $backgroundJob = true)
1050 {
1051 if ($this->useCurl)
1052 {
1053 return new Http\Curl\Queue($backgroundJob);
1054 }
1055 return new Http\Socket\Queue($backgroundJob);
1056 }
1057
1063 public function wait(): array
1064 {
1065 $responses = [];
1066
1067 if ($this->queue)
1068 {
1069 foreach ($this->queue->wait() as $promise)
1070 {
1071 $responses[$promise->getId()] = $promise->wait();
1072 }
1073 }
1074
1075 return $responses;
1076 }
1077
1084 public function shouldFetchBody(callable $callback)
1085 {
1086 $this->shouldFetchBody = $callback;
1087 return $this;
1088 }
1089}
setProxy($proxyHost, $proxyPort=null, $proxyUser=null, $proxyPassword=null)
post($url, $postData=null, $multipart=false)
shouldFetchBody(callable $callback)
setCookies(array $cookies)
__construct(array $options=null)
checkRequest(RequestInterface $request)
setContextOptions(array $options)
sendRequest(RequestInterface $request)
createHandler(RequestInterface $request, bool $async=false)
createQueue(bool $backgroundJob=true)
download($url, $filePath)
setHeader($name, $value, $replace=true)
setAuthorization($user, $pass)
createPromise($handler, Http\Queue $queue)
query($method, $url, $entityBody=null)
setRedirect($value, $max=null)
sendAsyncRequest(RequestInterface $request)
setBodyLengthMax($bodyLengthMax)
setHeaders(array $headers)
addError($code, $message, $triggerWarning=false)
buildRequest(RequestInterface $request)
static createByUri(UriInterface $uri)
Definition ipaddress.php:44