[f9c482b] | 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 | }
|
---|