CurlClientState.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpClient\Internal;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\HttpClient\Response\CurlResponse;
  13. /**
  14. * Internal representation of the cURL client's state.
  15. *
  16. * @author Alexander M. Turek <me@derrabus.de>
  17. *
  18. * @internal
  19. */
  20. final class CurlClientState extends ClientState
  21. {
  22. /**
  23. * @var \CurlMultiHandle|null
  24. */
  25. public $handle;
  26. /**
  27. * @var \CurlShareHandle|null
  28. */
  29. public $share;
  30. /**
  31. * @var bool
  32. */
  33. public $performing = false;
  34. /** @var PushedResponse[] */
  35. public $pushedResponses = [];
  36. /**
  37. * @var \Symfony\Component\HttpClient\Internal\DnsCache
  38. */
  39. public $dnsCache;
  40. /** @var float[] */
  41. public $pauseExpiries = [];
  42. /**
  43. * @var int
  44. */
  45. public $execCounter = \PHP_INT_MIN;
  46. /**
  47. * @var \Psr\Log\LoggerInterface|null
  48. */
  49. public $logger;
  50. /**
  51. * @var mixed[]
  52. */
  53. public static $curlVersion;
  54. /**
  55. * @param int $maxHostConnections
  56. * @param int $maxPendingPushes
  57. */
  58. public function __construct($maxHostConnections, $maxPendingPushes)
  59. {
  60. self::$curlVersion = self::$curlVersion ?? curl_version();
  61. $this->handle = curl_multi_init();
  62. $this->dnsCache = new DnsCache();
  63. $this->reset();
  64. // Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
  65. if (\defined('CURLPIPE_MULTIPLEX')) {
  66. curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
  67. }
  68. if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
  69. $maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
  70. }
  71. if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
  72. curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
  73. }
  74. // Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
  75. if (0 >= $maxPendingPushes) {
  76. return;
  77. }
  78. // HTTP/2 push crashes before curl 7.61
  79. if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
  80. return;
  81. }
  82. // Clone to prevent a circular reference
  83. $multi = clone $this;
  84. $multi->handle = null;
  85. $multi->share = null;
  86. $multi->pushedResponses = &$this->pushedResponses;
  87. $multi->logger = &$this->logger;
  88. $multi->handlesActivity = &$this->handlesActivity;
  89. $multi->openHandles = &$this->openHandles;
  90. curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes) {
  91. return $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
  92. });
  93. }
  94. public function reset()
  95. {
  96. foreach ($this->pushedResponses as $url => $response) {
  97. ($logger = $this->logger) ? $logger->debug(sprintf('Unused pushed response: "%s"', $url)) : null;
  98. curl_multi_remove_handle($this->handle, $response->handle);
  99. curl_close($response->handle);
  100. }
  101. $this->pushedResponses = [];
  102. $this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
  103. $this->dnsCache->removals = $this->dnsCache->hostnames = [];
  104. $this->share = curl_share_init();
  105. curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
  106. curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
  107. if (\defined('CURL_LOCK_DATA_CONNECT') && \PHP_VERSION_ID >= 80000) {
  108. curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
  109. }
  110. }
  111. /**
  112. * @param mixed[] $requestHeaders
  113. * @param int $maxPendingPushes
  114. */
  115. private function handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes)
  116. {
  117. $headers = [];
  118. $origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
  119. foreach ($requestHeaders as $h) {
  120. if (false !== $i = strpos($h, ':', 1)) {
  121. $headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
  122. }
  123. }
  124. if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
  125. ($logger = $this->logger) ? $logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin)) : null;
  126. return \CURL_PUSH_DENY;
  127. }
  128. $url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
  129. // curl before 7.65 doesn't validate the pushed ":authority" header,
  130. // but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
  131. // ignoring domains mentioned as alt-name in the certificate for now (same as curl).
  132. if (strncmp($origin, $url.'/', strlen($url.'/')) !== 0) {
  133. ($logger = $this->logger) ? $logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url)) : null;
  134. return \CURL_PUSH_DENY;
  135. }
  136. if ($maxPendingPushes <= \count($this->pushedResponses)) {
  137. $fifoUrl = key($this->pushedResponses);
  138. unset($this->pushedResponses[$fifoUrl]);
  139. ($logger = $this->logger) ? $logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl)) : null;
  140. }
  141. $url .= $headers[':path'][0];
  142. ($logger = $this->logger) ? $logger->debug(sprintf('Queueing pushed response: "%s"', $url)) : null;
  143. $this->pushedResponses[$url] = new PushedResponse(new CurlResponse($this, $pushed), $headers, $this->openHandles[(int) $parent][1] ?? [], $pushed);
  144. return \CURL_PUSH_OK;
  145. }
  146. }