annotate vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php @ 19:fa3358dc1485 tip

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