PluginProbe
Media Cloud Sync / 1.2.13
Media Cloud Sync v1.2.13
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
media-cloud-sync / includes / sdk / s3 / GuzzleHttp / Handler / StreamHandler.php

StreamHandler.php in Media Cloud Sync 1.2.13, at includes/sdk/s3/GuzzleHttp/Handler/StreamHandler.php

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