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