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 | }
|
---|