Geen omschrijving

StreamHandler.php 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. <?php
  2. namespace GuzzleHttp\Handler;
  3. use GuzzleHttp\Exception\RequestException;
  4. use GuzzleHttp\Exception\ConnectException;
  5. use GuzzleHttp\Promise\FulfilledPromise;
  6. use GuzzleHttp\Promise\RejectedPromise;
  7. use GuzzleHttp\Promise\PromiseInterface;
  8. use GuzzleHttp\Psr7;
  9. use GuzzleHttp\TransferStats;
  10. use Psr\Http\Message\RequestInterface;
  11. use Psr\Http\Message\ResponseInterface;
  12. use Psr\Http\Message\StreamInterface;
  13. /**
  14. * HTTP handler that uses PHP's HTTP stream wrapper.
  15. */
  16. class StreamHandler
  17. {
  18. private $lastHeaders = [];
  19. /**
  20. * Sends an HTTP request.
  21. *
  22. * @param RequestInterface $request Request to send.
  23. * @param array $options Request transfer options.
  24. *
  25. * @return PromiseInterface
  26. */
  27. public function __invoke(RequestInterface $request, array $options)
  28. {
  29. // Sleep if there is a delay specified.
  30. if (isset($options['delay'])) {
  31. usleep($options['delay'] * 1000);
  32. }
  33. $startTime = isset($options['on_stats']) ? microtime(true) : null;
  34. try {
  35. // Does not support the expect header.
  36. $request = $request->withoutHeader('Expect');
  37. // Append a content-length header if body size is zero to match
  38. // cURL's behavior.
  39. if (0 === $request->getBody()->getSize()) {
  40. $request = $request->withHeader('Content-Length', 0);
  41. }
  42. return $this->createResponse(
  43. $request,
  44. $options,
  45. $this->createStream($request, $options),
  46. $startTime
  47. );
  48. } catch (\InvalidArgumentException $e) {
  49. throw $e;
  50. } catch (\Exception $e) {
  51. // Determine if the error was a networking error.
  52. $message = $e->getMessage();
  53. // This list can probably get more comprehensive.
  54. if (strpos($message, 'getaddrinfo') // DNS lookup failed
  55. || strpos($message, 'Connection refused')
  56. || strpos($message, "couldn't connect to host") // error on HHVM
  57. ) {
  58. $e = new ConnectException($e->getMessage(), $request, $e);
  59. }
  60. $e = RequestException::wrapException($request, $e);
  61. $this->invokeStats($options, $request, $startTime, null, $e);
  62. return new RejectedPromise($e);
  63. }
  64. }
  65. private function invokeStats(
  66. array $options,
  67. RequestInterface $request,
  68. $startTime,
  69. ResponseInterface $response = null,
  70. $error = null
  71. ) {
  72. if (isset($options['on_stats'])) {
  73. $stats = new TransferStats(
  74. $request,
  75. $response,
  76. microtime(true) - $startTime,
  77. $error,
  78. []
  79. );
  80. call_user_func($options['on_stats'], $stats);
  81. }
  82. }
  83. private function createResponse(
  84. RequestInterface $request,
  85. array $options,
  86. $stream,
  87. $startTime
  88. ) {
  89. $hdrs = $this->lastHeaders;
  90. $this->lastHeaders = [];
  91. $parts = explode(' ', array_shift($hdrs), 3);
  92. $ver = explode('/', $parts[0])[1];
  93. $status = $parts[1];
  94. $reason = isset($parts[2]) ? $parts[2] : null;
  95. $headers = \GuzzleHttp\headers_from_lines($hdrs);
  96. list ($stream, $headers) = $this->checkDecode($options, $headers, $stream);
  97. $stream = Psr7\stream_for($stream);
  98. $sink = $this->createSink($stream, $options);
  99. $response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
  100. if (isset($options['on_headers'])) {
  101. try {
  102. $options['on_headers']($response);
  103. } catch (\Exception $e) {
  104. $msg = 'An error was encountered during the on_headers event';
  105. $ex = new RequestException($msg, $request, $response, $e);
  106. return new RejectedPromise($ex);
  107. }
  108. }
  109. if ($sink !== $stream) {
  110. $this->drain($stream, $sink);
  111. }
  112. $this->invokeStats($options, $request, $startTime, $response, null);
  113. return new FulfilledPromise($response);
  114. }
  115. private function createSink(StreamInterface $stream, array $options)
  116. {
  117. if (!empty($options['stream'])) {
  118. return $stream;
  119. }
  120. $sink = isset($options['sink'])
  121. ? $options['sink']
  122. : fopen('php://temp', 'r+');
  123. return is_string($sink)
  124. ? new Psr7\Stream(Psr7\try_fopen($sink, 'r+'))
  125. : Psr7\stream_for($sink);
  126. }
  127. private function checkDecode(array $options, array $headers, $stream)
  128. {
  129. // Automatically decode responses when instructed.
  130. if (!empty($options['decode_content'])) {
  131. $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers);
  132. if (isset($normalizedKeys['content-encoding'])) {
  133. $encoding = $headers[$normalizedKeys['content-encoding']];
  134. if ($encoding[0] == 'gzip' || $encoding[0] == 'deflate') {
  135. $stream = new Psr7\InflateStream(
  136. Psr7\stream_for($stream)
  137. );
  138. // Remove content-encoding header
  139. unset($headers[$normalizedKeys['content-encoding']]);
  140. // Fix content-length header
  141. if (isset($normalizedKeys['content-length'])) {
  142. $length = (int) $stream->getSize();
  143. if ($length == 0) {
  144. unset($headers[$normalizedKeys['content-length']]);
  145. } else {
  146. $headers[$normalizedKeys['content-length']] = [$length];
  147. }
  148. }
  149. }
  150. }
  151. }
  152. return [$stream, $headers];
  153. }
  154. /**
  155. * Drains the source stream into the "sink" client option.
  156. *
  157. * @param StreamInterface $source
  158. * @param StreamInterface $sink
  159. *
  160. * @return StreamInterface
  161. * @throws \RuntimeException when the sink option is invalid.
  162. */
  163. private function drain(StreamInterface $source, StreamInterface $sink)
  164. {
  165. Psr7\copy_to_stream($source, $sink);
  166. $sink->seek(0);
  167. $source->close();
  168. return $sink;
  169. }
  170. /**
  171. * Create a resource and check to ensure it was created successfully
  172. *
  173. * @param callable $callback Callable that returns stream resource
  174. *
  175. * @return resource
  176. * @throws \RuntimeException on error
  177. */
  178. private function createResource(callable $callback)
  179. {
  180. $errors = null;
  181. set_error_handler(function ($_, $msg, $file, $line) use (&$errors) {
  182. $errors[] = [
  183. 'message' => $msg,
  184. 'file' => $file,
  185. 'line' => $line
  186. ];
  187. return true;
  188. });
  189. $resource = $callback();
  190. restore_error_handler();
  191. if (!$resource) {
  192. $message = 'Error creating resource: ';
  193. foreach ($errors as $err) {
  194. foreach ($err as $key => $value) {
  195. $message .= "[$key] $value" . PHP_EOL;
  196. }
  197. }
  198. throw new \RuntimeException(trim($message));
  199. }
  200. return $resource;
  201. }
  202. private function createStream(RequestInterface $request, array $options)
  203. {
  204. static $methods;
  205. if (!$methods) {
  206. $methods = array_flip(get_class_methods(__CLASS__));
  207. }
  208. // HTTP/1.1 streams using the PHP stream wrapper require a
  209. // Connection: close header
  210. if ($request->getProtocolVersion() == '1.1'
  211. && !$request->hasHeader('Connection')
  212. ) {
  213. $request = $request->withHeader('Connection', 'close');
  214. }
  215. // Ensure SSL is verified by default
  216. if (!isset($options['verify'])) {
  217. $options['verify'] = true;
  218. }
  219. $params = [];
  220. $context = $this->getDefaultContext($request, $options);
  221. if (isset($options['on_headers']) && !is_callable($options['on_headers'])) {
  222. throw new \InvalidArgumentException('on_headers must be callable');
  223. }
  224. if (!empty($options)) {
  225. foreach ($options as $key => $value) {
  226. $method = "add_{$key}";
  227. if (isset($methods[$method])) {
  228. $this->{$method}($request, $context, $value, $params);
  229. }
  230. }
  231. }
  232. if (isset($options['stream_context'])) {
  233. if (!is_array($options['stream_context'])) {
  234. throw new \InvalidArgumentException('stream_context must be an array');
  235. }
  236. $context = array_replace_recursive(
  237. $context,
  238. $options['stream_context']
  239. );
  240. }
  241. $context = $this->createResource(
  242. function () use ($context, $params) {
  243. return stream_context_create($context, $params);
  244. }
  245. );
  246. return $this->createResource(
  247. function () use ($request, &$http_response_header, $context) {
  248. $resource = fopen($request->getUri(), 'r', null, $context);
  249. $this->lastHeaders = $http_response_header;
  250. return $resource;
  251. }
  252. );
  253. }
  254. private function getDefaultContext(RequestInterface $request)
  255. {
  256. $headers = '';
  257. foreach ($request->getHeaders() as $name => $value) {
  258. foreach ($value as $val) {
  259. $headers .= "$name: $val\r\n";
  260. }
  261. }
  262. $context = [
  263. 'http' => [
  264. 'method' => $request->getMethod(),
  265. 'header' => $headers,
  266. 'protocol_version' => $request->getProtocolVersion(),
  267. 'ignore_errors' => true,
  268. 'follow_location' => 0,
  269. ],
  270. ];
  271. $body = (string) $request->getBody();
  272. if (!empty($body)) {
  273. $context['http']['content'] = $body;
  274. // Prevent the HTTP handler from adding a Content-Type header.
  275. if (!$request->hasHeader('Content-Type')) {
  276. $context['http']['header'] .= "Content-Type:\r\n";
  277. }
  278. }
  279. $context['http']['header'] = rtrim($context['http']['header']);
  280. return $context;
  281. }
  282. private function add_proxy(RequestInterface $request, &$options, $value, &$params)
  283. {
  284. if (!is_array($value)) {
  285. $options['http']['proxy'] = $value;
  286. } else {
  287. $scheme = $request->getUri()->getScheme();
  288. if (isset($value[$scheme])) {
  289. if (!isset($value['no'])
  290. || !\GuzzleHttp\is_host_in_noproxy(
  291. $request->getUri()->getHost(),
  292. $value['no']
  293. )
  294. ) {
  295. $options['http']['proxy'] = $value[$scheme];
  296. }
  297. }
  298. }
  299. }
  300. private function add_timeout(RequestInterface $request, &$options, $value, &$params)
  301. {
  302. $options['http']['timeout'] = $value;
  303. }
  304. private function add_verify(RequestInterface $request, &$options, $value, &$params)
  305. {
  306. if ($value === true) {
  307. // PHP 5.6 or greater will find the system cert by default. When
  308. // < 5.6, use the Guzzle bundled cacert.
  309. if (PHP_VERSION_ID < 50600) {
  310. $options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle();
  311. }
  312. } elseif (is_string($value)) {
  313. $options['ssl']['cafile'] = $value;
  314. if (!file_exists($value)) {
  315. throw new \RuntimeException("SSL CA bundle not found: $value");
  316. }
  317. } elseif ($value === false) {
  318. $options['ssl']['verify_peer'] = false;
  319. return;
  320. } else {
  321. throw new \InvalidArgumentException('Invalid verify request option');
  322. }
  323. $options['ssl']['verify_peer'] = true;
  324. $options['ssl']['allow_self_signed'] = false;
  325. }
  326. private function add_cert(RequestInterface $request, &$options, $value, &$params)
  327. {
  328. if (is_array($value)) {
  329. $options['ssl']['passphrase'] = $value[1];
  330. $value = $value[0];
  331. }
  332. if (!file_exists($value)) {
  333. throw new \RuntimeException("SSL certificate not found: {$value}");
  334. }
  335. $options['ssl']['local_cert'] = $value;
  336. }
  337. private function add_progress(RequestInterface $request, &$options, $value, &$params)
  338. {
  339. $this->addNotification(
  340. $params,
  341. function ($code, $a, $b, $c, $transferred, $total) use ($value) {
  342. if ($code == STREAM_NOTIFY_PROGRESS) {
  343. $value($total, $transferred, null, null);
  344. }
  345. }
  346. );
  347. }
  348. private function add_debug(RequestInterface $request, &$options, $value, &$params)
  349. {
  350. if ($value === false) {
  351. return;
  352. }
  353. static $map = [
  354. STREAM_NOTIFY_CONNECT => 'CONNECT',
  355. STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',
  356. STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',
  357. STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',
  358. STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',
  359. STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',
  360. STREAM_NOTIFY_PROGRESS => 'PROGRESS',
  361. STREAM_NOTIFY_FAILURE => 'FAILURE',
  362. STREAM_NOTIFY_COMPLETED => 'COMPLETED',
  363. STREAM_NOTIFY_RESOLVE => 'RESOLVE',
  364. ];
  365. static $args = ['severity', 'message', 'message_code',
  366. 'bytes_transferred', 'bytes_max'];
  367. $value = \GuzzleHttp\debug_resource($value);
  368. $ident = $request->getMethod() . ' ' . $request->getUri();
  369. $this->addNotification(
  370. $params,
  371. function () use ($ident, $value, $map, $args) {
  372. $passed = func_get_args();
  373. $code = array_shift($passed);
  374. fprintf($value, '<%s> [%s] ', $ident, $map[$code]);
  375. foreach (array_filter($passed) as $i => $v) {
  376. fwrite($value, $args[$i] . ': "' . $v . '" ');
  377. }
  378. fwrite($value, "\n");
  379. }
  380. );
  381. }
  382. private function addNotification(array &$params, callable $notify)
  383. {
  384. // Wrap the existing function if needed.
  385. if (!isset($params['notification'])) {
  386. $params['notification'] = $notify;
  387. } else {
  388. $params['notification'] = $this->callArray([
  389. $params['notification'],
  390. $notify
  391. ]);
  392. }
  393. }
  394. private function callArray(array $functions)
  395. {
  396. return function () use ($functions) {
  397. $args = func_get_args();
  398. foreach ($functions as $fn) {
  399. call_user_func_array($fn, $args);
  400. }
  401. };
  402. }
  403. }