PluginProbe
Authorizer / 3.9.0
Authorizer v3.9.0
3.16.0 3.15.3 3.15.2 3.15.1 3.15.0 3.14.3 3.14.4 3.14.2 3.14.1 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.9.0 2.9.1 2.9.10 2.9.11 2.9.12 2.9.13 2.9.2 2.9.3 All 127 releases
authorizer / vendor / guzzlehttp / guzzle / src / Handler / StreamHandler.php

StreamHandler.php in Authorizer 3.9.0, at vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php

622 lines 21.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace GuzzleHttp\Handler;
4
5 use GuzzleHttp\Exception\ConnectException;
6 use GuzzleHttp\Exception\RequestException;
7 use GuzzleHttp\Promise as P;
8 use GuzzleHttp\Promise\FulfilledPromise;
9 use GuzzleHttp\Promise\PromiseInterface;
10 use GuzzleHttp\Psr7;
11 use GuzzleHttp\TransferStats;
12 use GuzzleHttp\Utils;
13 use Psr\Http\Message\RequestInterface;
14 use Psr\Http\Message\ResponseInterface;
15 use Psr\Http\Message\StreamInterface;
16 use Psr\Http\Message\UriInterface;
17
18 /**
19 * HTTP handler that uses PHP's HTTP stream wrapper.
20 *
21 * @final
22 */
23 class StreamHandler
24 {
25 /**
26 * @var array
27 */
28 private $lastHeaders = [];
29
30 /**
31 * Sends an HTTP request.
32 *
33 * @param RequestInterface $request Request to send.
34 * @param array $options Request transfer options.
35 */
36 public function __invoke(RequestInterface $request, array $options): PromiseInterface
37 {
38 // Sleep if there is a delay specified.
39 if (isset($options['delay'])) {
40 \usleep($options['delay'] * 1000);
41 }
42
43 $protocolVersion = $request->getProtocolVersion();
44
45 if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
46 throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
47 }
48
49 $startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
50
51 try {
52 // Does not support the expect header.
53 $request = $request->withoutHeader('Expect');
54
55 // Append a content-length header if body size is zero to match
56 // cURL's behavior.
57 if (0 === $request->getBody()->getSize()) {
58 $request = $request->withHeader('Content-Length', '0');
59 }
60
61 return $this->createResponse(
62 $request,
63 $options,
64 $this->createStream($request, $options),
65 $startTime
66 );
67 } catch (\InvalidArgumentException $e) {
68 throw $e;
69 } catch (\Exception $e) {
70 // Determine if the error was a networking error.
71 $message = $e->getMessage();
72 // This list can probably get more comprehensive.
73 if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed
74 || false !== \strpos($message, 'Connection refused')
75 || false !== \strpos($message, "couldn't connect to host") // error on HHVM
76 || false !== \strpos($message, 'connection attempt failed')
77 ) {
78 $e = new ConnectException($e->getMessage(), $request, $e);
79 } else {
80 $e = RequestException::wrapException($request, $e);
81 }
82 $this->invokeStats($options, $request, $startTime, null, $e);
83
84 return P\Create::rejectionFor($e);
85 }
86 }
87
88 private function invokeStats(
89 array $options,
90 RequestInterface $request,
91 ?float $startTime,
92 ?ResponseInterface $response = null,
93 ?\Throwable $error = null
94 ): void {
95 if (isset($options['on_stats'])) {
96 $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []);
97 ($options['on_stats'])($stats);
98 }
99 }
100
101 /**
102 * @param resource $stream
103 */
104 private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime): PromiseInterface
105 {
106 $hdrs = $this->lastHeaders;
107 $this->lastHeaders = [];
108
109 try {
110 [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);
111 } catch (\Exception $e) {
112 return P\Create::rejectionFor(
113 new RequestException('An error was encountered while creating the response', $request, null, $e)
114 );
115 }
116
117 [$stream, $headers] = $this->checkDecode($options, $headers, $stream);
118 $stream = Psr7\Utils::streamFor($stream);
119 $sink = $stream;
120
121 if (\strcasecmp('HEAD', $request->getMethod())) {
122 $sink = $this->createSink($stream, $options);
123 }
124
125 try {
126 $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
127 } catch (\Exception $e) {
128 return P\Create::rejectionFor(
129 new RequestException('An error was encountered while creating the response', $request, null, $e)
130 );
131 }
132
133 if (isset($options['on_headers'])) {
134 try {
135 $options['on_headers']($response);
136 } catch (\Exception $e) {
137 return P\Create::rejectionFor(
138 new RequestException('An error was encountered during the on_headers event', $request, $response, $e)
139 );
140 }
141 }
142
143 // Do not drain when the request is a HEAD request because they have
144 // no body.
145 if ($sink !== $stream) {
146 $this->drain($stream, $sink, $response->getHeaderLine('Content-Length'));
147 }
148
149 $this->invokeStats($options, $request, $startTime, $response, null);
150
151 return new FulfilledPromise($response);
152 }
153
154 private function createSink(StreamInterface $stream, array $options): StreamInterface
155 {
156 if (!empty($options['stream'])) {
157 return $stream;
158 }
159
160 $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+');
161
162 return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink);
163 }
164
165 /**
166 * @param resource $stream
167 */
168 private function checkDecode(array $options, array $headers, $stream): array
169 {
170 // Automatically decode responses when instructed.
171 if (!empty($options['decode_content'])) {
172 $normalizedKeys = Utils::normalizeHeaderKeys($headers);
173 if (isset($normalizedKeys['content-encoding'])) {
174 $encoding = $headers[$normalizedKeys['content-encoding']];
175 if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') {
176 $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream));
177 $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']];
178
179 // Remove content-encoding header
180 unset($headers[$normalizedKeys['content-encoding']]);
181
182 // Fix content-length header
183 if (isset($normalizedKeys['content-length'])) {
184 $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
185 $length = (int) $stream->getSize();
186 if ($length === 0) {
187 unset($headers[$normalizedKeys['content-length']]);
188 } else {
189 $headers[$normalizedKeys['content-length']] = [$length];
190 }
191 }
192 }
193 }
194 }
195
196 return [$stream, $headers];
197 }
198
199 /**
200 * Drains the source stream into the "sink" client option.
201 *
202 * @param string $contentLength Header specifying the amount of
203 * data to read.
204 *
205 * @throws \RuntimeException when the sink option is invalid.
206 */
207 private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface
208 {
209 // If a content-length header is provided, then stop reading once
210 // that number of bytes has been read. This can prevent infinitely
211 // reading from a stream when dealing with servers that do not honor
212 // Connection: Close headers.
213 Psr7\Utils::copyToStream(
214 $source,
215 $sink,
216 (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1
217 );
218
219 $sink->seek(0);
220 $source->close();
221
222 return $sink;
223 }
224
225 /**
226 * Create a resource and check to ensure it was created successfully
227 *
228 * @param callable $callback Callable that returns stream resource
229 *
230 * @return resource
231 *
232 * @throws \RuntimeException on error
233 */
234 private function createResource(callable $callback)
235 {
236 $errors = [];
237 \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool {
238 $errors[] = [
239 'message' => $msg,
240 'file' => $file,
241 'line' => $line,
242 ];
243
244 return true;
245 });
246
247 try {
248 $resource = $callback();
249 } finally {
250 \restore_error_handler();
251 }
252
253 if (!$resource) {
254 $message = 'Error creating resource: ';
255 foreach ($errors as $err) {
256 foreach ($err as $key => $value) {
257 $message .= "[$key] $value".\PHP_EOL;
258 }
259 }
260 throw new \RuntimeException(\trim($message));
261 }
262
263 return $resource;
264 }
265
266 /**
267 * @return resource
268 */
269 private function createStream(RequestInterface $request, array $options)
270 {
271 static $methods;
272 if (!$methods) {
273 $methods = \array_flip(\get_class_methods(__CLASS__));
274 }
275
276 if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
277 throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
278 }
279
280 // HTTP/1.1 streams using the PHP stream wrapper require a
281 // Connection: close header
282 if ($request->getProtocolVersion() === '1.1'
283 && !$request->hasHeader('Connection')
284 ) {
285 $request = $request->withHeader('Connection', 'close');
286 }
287
288 // Ensure SSL is verified by default
289 if (!isset($options['verify'])) {
290 $options['verify'] = true;
291 }
292
293 $params = [];
294 $context = $this->getDefaultContext($request);
295
296 if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) {
297 throw new \InvalidArgumentException('on_headers must be callable');
298 }
299
300 if (!empty($options)) {
301 foreach ($options as $key => $value) {
302 $method = "add_{$key}";
303 if (isset($methods[$method])) {
304 $this->{$method}($request, $context, $value, $params);
305 }
306 }
307 }
308
309 if (isset($options['stream_context'])) {
310 if (!\is_array($options['stream_context'])) {
311 throw new \InvalidArgumentException('stream_context must be an array');
312 }
313 $context = \array_replace_recursive($context, $options['stream_context']);
314 }
315
316 // Microsoft NTLM authentication only supported with curl handler
317 if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) {
318 throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
319 }
320
321 $uri = $this->resolveHost($request, $options);
322
323 $contextResource = $this->createResource(
324 static function () use ($context, $params) {
325 return \stream_context_create($context, $params);
326 }
327 );
328
329 return $this->createResource(
330 function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) {
331 $resource = @\fopen((string) $uri, 'r', false, $contextResource);
332 $this->lastHeaders = $http_response_header ?? [];
333
334 if (false === $resource) {
335 throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context);
336 }
337
338 if (isset($options['read_timeout'])) {
339 $readTimeout = $options['read_timeout'];
340 $sec = (int) $readTimeout;
341 $usec = ($readTimeout - $sec) * 100000;
342 \stream_set_timeout($resource, $sec, $usec);
343 }
344
345 return $resource;
346 }
347 );
348 }
349
350 private function resolveHost(RequestInterface $request, array $options): UriInterface
351 {
352 $uri = $request->getUri();
353
354 if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) {
355 if ('v4' === $options['force_ip_resolve']) {
356 $records = \dns_get_record($uri->getHost(), \DNS_A);
357 if (false === $records || !isset($records[0]['ip'])) {
358 throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request);
359 }
360
361 return $uri->withHost($records[0]['ip']);
362 }
363 if ('v6' === $options['force_ip_resolve']) {
364 $records = \dns_get_record($uri->getHost(), \DNS_AAAA);
365 if (false === $records || !isset($records[0]['ipv6'])) {
366 throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request);
367 }
368
369 return $uri->withHost('['.$records[0]['ipv6'].']');
370 }
371 }
372
373 return $uri;
374 }
375
376 private function getDefaultContext(RequestInterface $request): array
377 {
378 $headers = '';
379 foreach ($request->getHeaders() as $name => $value) {
380 foreach ($value as $val) {
381 $headers .= "$name: $val\r\n";
382 }
383 }
384
385 $context = [
386 'http' => [
387 'method' => $request->getMethod(),
388 'header' => $headers,
389 'protocol_version' => $request->getProtocolVersion(),
390 'ignore_errors' => true,
391 'follow_location' => 0,
392 ],
393 'ssl' => [
394 'peer_name' => $request->getUri()->getHost(),
395 ],
396 ];
397
398 $body = (string) $request->getBody();
399
400 if ('' !== $body) {
401 $context['http']['content'] = $body;
402 // Prevent the HTTP handler from adding a Content-Type header.
403 if (!$request->hasHeader('Content-Type')) {
404 $context['http']['header'] .= "Content-Type:\r\n";
405 }
406 }
407
408 $context['http']['header'] = \rtrim($context['http']['header']);
409
410 return $context;
411 }
412
413 /**
414 * @param mixed $value as passed via Request transfer options.
415 */
416 private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void
417 {
418 $uri = null;
419
420 if (!\is_array($value)) {
421 $uri = $value;
422 } else {
423 $scheme = $request->getUri()->getScheme();
424 if (isset($value[$scheme])) {
425 if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
426 $uri = $value[$scheme];
427 }
428 }
429 }
430
431 if (!$uri) {
432 return;
433 }
434
435 $parsed = $this->parse_proxy($uri);
436 $options['http']['proxy'] = $parsed['proxy'];
437
438 if ($parsed['auth']) {
439 if (!isset($options['http']['header'])) {
440 $options['http']['header'] = [];
441 }
442 $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}";
443 }
444 }
445
446 /**
447 * Parses the given proxy URL to make it compatible with the format PHP's stream context expects.
448 */
449 private function parse_proxy(string $url): array
450 {
451 $parsed = \parse_url($url);
452
453 if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') {
454 if (isset($parsed['host']) && isset($parsed['port'])) {
455 $auth = null;
456 if (isset($parsed['user']) && isset($parsed['pass'])) {
457 $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}");
458 }
459
460 return [
461 'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}",
462 'auth' => $auth ? "Basic {$auth}" : null,
463 ];
464 }
465 }
466
467 // Return proxy as-is.
468 return [
469 'proxy' => $url,
470 'auth' => null,
471 ];
472 }
473
474 /**
475 * @param mixed $value as passed via Request transfer options.
476 */
477 private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void
478 {
479 if ($value > 0) {
480 $options['http']['timeout'] = $value;
481 }
482 }
483
484 /**
485 * @param mixed $value as passed via Request transfer options.
486 */
487 private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void
488 {
489 if (
490 $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
491 || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
492 || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
493 || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT)
494 ) {
495 $options['http']['crypto_method'] = $value;
496
497 return;
498 }
499
500 throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided');
501 }
502
503 /**
504 * @param mixed $value as passed via Request transfer options.
505 */
506 private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void
507 {
508 if ($value === false) {
509 $options['ssl']['verify_peer'] = false;
510 $options['ssl']['verify_peer_name'] = false;
511
512 return;
513 }
514
515 if (\is_string($value)) {
516 $options['ssl']['cafile'] = $value;
517 if (!\file_exists($value)) {
518 throw new \RuntimeException("SSL CA bundle not found: $value");
519 }
520 } elseif ($value !== true) {
521 throw new \InvalidArgumentException('Invalid verify request option');
522 }
523
524 $options['ssl']['verify_peer'] = true;
525 $options['ssl']['verify_peer_name'] = true;
526 $options['ssl']['allow_self_signed'] = false;
527 }
528
529 /**
530 * @param mixed $value as passed via Request transfer options.
531 */
532 private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
533 {
534 if (\is_array($value)) {
535 $options['ssl']['passphrase'] = $value[1];
536 $value = $value[0];
537 }
538
539 if (!\file_exists($value)) {
540 throw new \RuntimeException("SSL certificate not found: {$value}");
541 }
542
543 $options['ssl']['local_cert'] = $value;
544 }
545
546 /**
547 * @param mixed $value as passed via Request transfer options.
548 */
549 private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
550 {
551 self::addNotification(
552 $params,
553 static function ($code, $a, $b, $c, $transferred, $total) use ($value) {
554 if ($code == \STREAM_NOTIFY_PROGRESS) {
555 // The upload progress cannot be determined. Use 0 for cURL compatibility:
556 // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
557 $value($total, $transferred, 0, 0);
558 }
559 }
560 );
561 }
562
563 /**
564 * @param mixed $value as passed via Request transfer options.
565 */
566 private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void
567 {
568 if ($value === false) {
569 return;
570 }
571
572 static $map = [
573 \STREAM_NOTIFY_CONNECT => 'CONNECT',
574 \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
575 \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
576 \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
577 \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
578 \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
579 \STREAM_NOTIFY_PROGRESS => 'PROGRESS',
580 \STREAM_NOTIFY_FAILURE => 'FAILURE',
581 \STREAM_NOTIFY_COMPLETED => 'COMPLETED',
582 \STREAM_NOTIFY_RESOLVE => 'RESOLVE',
583 ];
584 static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max'];
585
586 $value = Utils::debugResource($value);
587 $ident = $request->getMethod().' '.$request->getUri()->withFragment('');
588 self::addNotification(
589 $params,
590 static function (int $code, ...$passed) use ($ident, $value, $map, $args): void {
591 \fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
592 foreach (\array_filter($passed) as $i => $v) {
593 \fwrite($value, $args[$i].': "'.$v.'" ');
594 }
595 \fwrite($value, "\n");
596 }
597 );
598 }
599
600 private static function addNotification(array &$params, callable $notify): void
601 {
602 // Wrap the existing function if needed.
603 if (!isset($params['notification'])) {
604 $params['notification'] = $notify;
605 } else {
606 $params['notification'] = self::callArray([
607 $params['notification'],
608 $notify,
609 ]);
610 }
611 }
612
613 private static function callArray(array $functions): callable
614 {
615 return static function (...$args) use ($functions) {
616 foreach ($functions as $fn) {
617 $fn(...$args);
618 }
619 };
620 }
621 }
622