Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
handler.php
1<?php
2
11
15use Psr\Http\Message\RequestInterface;
16
17class Handler extends Http\Handler
18{
19 protected const BUF_BODY_LEN = 131072;
20 protected const BUF_READ_LEN = 32768;
21
22 public const PENDING = 0;
23 public const CONNECTED = 1;
24 public const HEADERS_SENT = 2;
25 public const BODY_SENT = 3;
26 public const HEADERS_RECEIVED = 4;
27 public const BODY_RECEIVED = 5;
28 public const CONNECT_SENT = 6;
29 public const CONNECT_RECEIVED = 7;
30
31 protected Stream $socket;
32 protected bool $useProxy = false;
33 protected int $state = self::PENDING;
34 protected string $requestBodyPart = '';
35
41 public function __construct(RequestInterface $request, Http\ResponseBuilder $responseBuilder, array $options = [])
42 {
43 Http\Handler::__construct($request, $responseBuilder, $options);
44
45 if (isset($options['proxyHost']) && $options['proxyHost'] != '')
46 {
47 $this->useProxy = true;
48 }
49
50 $this->socket = $this->createSocket($options);
51 }
52
59 public function process(Http\Promise $promise)
60 {
62
63 try
64 {
65 switch ($this->state)
66 {
67 case self::PENDING:
68 // this is a new job - should connect asynchronously
69 try
70 {
71 $this->socket->connect();
72 }
73 catch (\RuntimeException $e)
74 {
75 throw new Http\NetworkException($request, $e->getMessage());
76 }
77
78 $this->state = self::CONNECTED;
79 break;
80
81 case self::CONNECTED:
82 case self::CONNECT_RECEIVED:
83 if ($this->state === self::CONNECTED && $this->useProxy && $request->getUri()->getScheme() === 'https')
84 {
85 // implement CONNECT method for https connections via proxy
86 $this->sendConnect();
87
88 $this->state = self::CONNECT_SENT;
89 }
90 else
91 {
92 // enable ssl before sending request headers
93 if ($request->getUri()->getScheme() === 'https')
94 {
95 $this->socket->setBlocking();
96
97 if ($this->socket->enableCrypto() === false)
98 {
99 throw new Http\NetworkException($request, 'Error establishing an SSL connection.');
100 }
101 }
102
103 // the socket is ready - can write headers
104 $this->sendHeaders();
105
106 // prepare the body for sending
107 $body = $request->getBody();
108 if ($body->isSeekable())
109 {
110 $body->rewind();
111 }
112
113 $this->state = self::HEADERS_SENT;
114 }
115 break;
116
117 case self::CONNECT_SENT:
118 if ($this->receiveHeaders())
119 {
120 $this->log("<<<CONNECT\n" . $this->responseHeaders . "\n", Web\HttpDebug::REQUEST_HEADERS);
121
122 // response to CONNECT from the proxy
123 $headers = Web\HttpHeaders::createFromString($this->responseHeaders);
124
125 if (($status = $headers->getStatus()) >= 200 && $status < 300)
126 {
127 $this->responseHeaders = '';
128
129 $this->state = self::CONNECT_RECEIVED;
130 }
131 else
132 {
133 throw new Http\NetworkException($request, 'Error receiving the CONNECT response from the proxy: ' . $headers->getStatus() . ' ' . $headers->getReasonPhrase());
134 }
135 }
136 break;
137
138 case self::HEADERS_SENT:
139 // it's time to send the request body asynchronously
140 if ($this->sendBody())
141 {
142 // sent all the body
143 $this->state = self::BODY_SENT;
144 }
145 break;
146
147 case self::BODY_SENT:
148 // request is sent now - switching to reading
149 if ($this->receiveHeaders())
150 {
151 // all headers received
152 $this->log("\n<<<RESPONSE\n" . $this->responseHeaders . "\n", Web\HttpDebug::RESPONSE_HEADERS);
153
154 // build the response for the next stage
155 $this->response = $this->responseBuilder->createFromString($this->responseHeaders);
156
157 $fetchBody = $this->waitResponse;
158
159 if ($this->shouldFetchBody !== null)
160 {
161 $fetchBody = call_user_func($this->shouldFetchBody, $this->response, $request);
162 }
163
164 if ($fetchBody)
165 {
166 $this->state = self::HEADERS_RECEIVED;
167 }
168 else
169 {
170 $this->socket->close();
171
172 // we don't want a body, just fulfil a promise with response headers
173 $promise->fulfill($this->response);
174
175 $this->state = self::BODY_RECEIVED;
176 }
177 }
178 break;
179
180 case self::HEADERS_RECEIVED:
181 // receiving a response body
182 if ($this->receiveBody())
183 {
184 // have read all the body
185 $this->socket->close();
186
187 if ($this->debugLevel & Web\HttpDebug::RESPONSE_BODY)
188 {
189 $this->log($this->response->getBody(), Web\HttpDebug::RESPONSE_BODY);
190 }
191
192 // need to ajust the response headers (PSR-18)
193 $this->response->adjustHeaders();
194
195 // we have a result!
196 $promise->fulfill($this->response);
197
198 $this->state = self::BODY_RECEIVED;
199 }
200 break;
201 }
202 }
203 catch (Http\ClientException $exception)
204 {
205 $this->socket->close();
206
207 $promise->reject($exception);
208
209 if ($logger = $this->getLogger())
210 {
211 $logger->error($exception->getMessage());
212 }
213 }
214 }
215
216 protected function write(string $data, string $error)
217 {
218 try
219 {
220 $result = $this->socket->write($data);
221 }
222 catch (\RuntimeException $e)
223 {
224 throw new Http\NetworkException($this->request, $error . ' ' . $e->getMessage());
225 }
226
227 return $result;
228 }
229
230 protected function sendConnect(): void
231 {
233 $uri = $request->getUri();
234 $host = $uri->getHost();
235
236 $requestHeaders = 'CONNECT ' . $host . ':' . $uri->getPort() . ' HTTP/1.1' . "\r\n"
237 . 'Host: ' . $host . "\r\n"
238 ;
239
240 if ($request->hasHeader('Proxy-Authorization'))
241 {
242 $requestHeaders .= 'Proxy-Authorization' . ': ' . $request->getHeaderLine('Proxy-Authorization') . "\r\n";
243 $this->request = $request->withoutHeader('Proxy-Authorization');
244 }
245
246 $requestHeaders .= "\r\n";
247
248 $this->log(">>>CONNECT\n" . $requestHeaders, Web\HttpDebug::REQUEST_HEADERS);
249
250 // blocking is critical for headers
251 $this->socket->setBlocking();
252
253 $this->write($requestHeaders, 'Error sending CONNECT to proxy.');
254
255 $this->socket->setBlocking(false);
256 }
257
258 protected function sendHeaders(): void
259 {
261 $uri = $request->getUri();
262
263 // Full URI for HTTP proxies
264 $target = ($this->useProxy && $uri->getScheme() === 'http' ? (string)$uri : $request->getRequestTarget());
265
266 $requestHeaders = $request->getMethod() . ' ' . $target . ' HTTP/' . $request->getProtocolVersion() . "\r\n";
267
268 foreach ($request->getHeaders() as $name => $values)
269 {
270 foreach ($values as $value)
271 {
272 $requestHeaders .= $name . ': ' . $value . "\r\n";
273 }
274 }
275
276 $requestHeaders .= "\r\n";
277
278 $this->log(">>>REQUEST\n" . $requestHeaders, Web\HttpDebug::REQUEST_HEADERS);
279
280 // blocking is critical for headers
281 $this->socket->setBlocking();
282
283 $this->write($requestHeaders, 'Error sending the message headers.');
284
285 $this->socket->setBlocking(false);
286 }
287
288 protected function sendBody(): bool
289 {
291 $body = $request->getBody();
292
293 if (!$body->eof() || $this->requestBodyPart !== '')
294 {
295 if (!$body->eof() && strlen($this->requestBodyPart) < self::BUF_BODY_LEN)
296 {
297 $part = $body->read(self::BUF_BODY_LEN);
298 $this->requestBodyPart .= $part;
299 $this->log($part, Web\HttpDebug::REQUEST_BODY);
300 }
301
302 $result = $this->write($this->requestBodyPart, 'Error sending the message body.');
303
304 $this->requestBodyPart = substr($this->requestBodyPart, $result);
305 }
306
307 return ($body->eof() && $this->requestBodyPart === '');
308 }
309
310 protected function receiveHeaders(): bool
311 {
312 while (!$this->socket->eof())
313 {
314 try
315 {
316 $line = $this->socket->gets();
317 }
318 catch (\RuntimeException $e)
319 {
320 throw new Http\NetworkException($this->request, $e->getMessage());
321 }
322
323 if ($line === false)
324 {
325 // no data in the socket or error(?)
326 return false;
327 }
328
329 if ($line === "\r\n")
330 {
331 // got all headers
332 return true;
333 }
334
335 $this->responseHeaders .= $line;
336 }
337
338 if ($this->responseHeaders === '')
339 {
340 throw new Http\NetworkException($this->request, 'Empty response from the server.');
341 }
342
343 return true;
344 }
345
346 protected function receiveBody(): bool
347 {
349 $headers = $this->response->getHeadersCollection();
350 $body = $this->response->getBody();
351
352 $length = $headers->get('Content-Length');
353
354 while (!$this->socket->eof())
355 {
356 try
357 {
358 $buf = $this->socket->read(self::BUF_READ_LEN);
359 }
360 catch (\RuntimeException)
361 {
362 throw new Http\NetworkException($request, 'Stream reading error.');
363 }
364
365 if ($buf === '')
366 {
367 // no data in the stream yet
368 return false;
369 }
370
371 try
372 {
373 $body->write($buf);
374 }
375 catch (\RuntimeException)
376 {
377 throw new Http\NetworkException($request, 'Error writing to response body stream.');
378 }
379
380 if ($this->bodyLengthMax > 0 && $body->getSize() > $this->bodyLengthMax)
381 {
382 throw new Http\NetworkException($request, 'Maximum content length has been reached. Breaking reading.');
383 }
384
385 if ($length !== null)
386 {
387 $length -= strlen($buf);
388 if ($length <= 0)
389 {
390 // have read all the body
391 return true;
392 }
393 }
394 }
395
396 return true;
397 }
398
399 protected function createSocket(array $options): Stream
400 {
401 $proxyHost = (string)($options['proxyHost'] ?? '');
402 $proxyPort = (int)($options['proxyPort'] ?? 80);
403 $contextOptions = $options['contextOptions'] ?? [];
404
405 $uri = $this->request->getUri();
406
407 if ($proxyHost != '')
408 {
409 $host = $proxyHost;
410 $port = $proxyPort;
411
412 // set original host to match a sertificate for proxy tunneling
413 $contextOptions['ssl']['peer_name'] = $uri->getHost();
414 }
415 else
416 {
417 $host = $uri->getHost();
418 $port = $uri->getPort();
419
420 if (isset($options['effectiveIp']) && $options['effectiveIp'] instanceof IpAddress)
421 {
422 // set original host to match a sertificate
423 $contextOptions['ssl']['peer_name'] = $host;
424
425 // resolved in HttpClient if private IPs were disabled
426 $host = $options['effectiveIp']->get();
427 }
428 }
429
430 $socket = new Stream(
431 'tcp://' . $host . ':' . $port,
432 [
433 'socketTimeout' => $options['socketTimeout'] ?? null,
434 'streamTimeout' => $options['streamTimeout'] ?? null,
435 'contextOptions' => $contextOptions,
436 'async' => $options['async'] ?? null,
437 ]
438 );
439
440 return $socket;
441 }
442
443 public function getState(): int
444 {
445 return $this->state;
446 }
447
453 public function getSocket(): Stream
454 {
455 return $this->socket;
456 }
457}
shouldFetchBody(callable $callback)
Definition handler.php:112
RequestInterface $request
Definition handler.php:24
ResponseBuilder $responseBuilder
Definition handler.php:25
log(string $logMessage, int $level)
Definition handler.php:87
process(Http\Promise $promise)
Definition handler.php:59
__construct(RequestInterface $request, Http\ResponseBuilder $responseBuilder, array $options=[])
Definition handler.php:41
write(string $data, string $error)
Definition handler.php:216