Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c0602fd

Browse files
[HttpClient] Fix closing curl-multi handle too early on destruct
1 parent 9e3696f commit c0602fd

File tree

2 files changed

+99
-113
lines changed

2 files changed

+99
-113
lines changed

src/Symfony/Component/HttpClient/CurlHttpClient.php

Lines changed: 17 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
namespace Symfony\Component\HttpClient;
1313

1414
use Psr\Log\LoggerAwareInterface;
15-
use Psr\Log\LoggerAwareTrait;
15+
use Psr\Log\LoggerInterface;
1616
use Symfony\Component\HttpClient\Exception\InvalidArgumentException;
1717
use Symfony\Component\HttpClient\Exception\TransportException;
1818
use Symfony\Component\HttpClient\Internal\CurlClientState;
@@ -35,22 +35,24 @@
3535
final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, ResetInterface
3636
{
3737
use HttpClientTrait;
38-
use LoggerAwareTrait;
3938

4039
private $defaultOptions = self::OPTIONS_DEFAULTS + [
4140
'auth_ntlm' => null, // array|string - an array containing the username as first value, and optionally the
4241
// password as the second one; or string like username:password - enabling NTLM auth
4342
];
4443

44+
/**
45+
* @var LoggerInterface|null
46+
*/
47+
private $logger;
48+
4549
/**
4650
* An internal object to share state between the client and its responses.
4751
*
4852
* @var CurlClientState
4953
*/
5054
private $multi;
5155

52-
private static $curlVersion;
53-
5456
/**
5557
* @param array $defaultOptions Default request's options
5658
* @param int $maxHostConnections The maximum number of connections to a single host
@@ -70,33 +72,12 @@ public function __construct(array $defaultOptions = [], int $maxHostConnections
7072
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
7173
}
7274

73-
$this->multi = new CurlClientState();
74-
self::$curlVersion = self::$curlVersion ?? curl_version();
75-
76-
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
77-
if (\defined('CURLPIPE_MULTIPLEX')) {
78-
curl_multi_setopt($this->multi->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
79-
}
80-
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
81-
$maxHostConnections = curl_multi_setopt($this->multi->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
82-
}
83-
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
84-
curl_multi_setopt($this->multi->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
85-
}
86-
87-
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
88-
if (0 >= $maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304)) {
89-
return;
90-
}
91-
92-
// HTTP/2 push crashes before curl 7.61
93-
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
94-
return;
95-
}
75+
$this->multi = new CurlClientState($maxHostConnections, $maxPendingPushes);
76+
}
9677

97-
curl_multi_setopt($this->multi->handle, \CURLMOPT_PUSHFUNCTION, function ($parent, $pushed, array $requestHeaders) use ($maxPendingPushes) {
98-
return $this->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
99-
});
78+
public function setLogger(LoggerInterface $logger): void
79+
{
80+
$this->logger = $this->multi->logger = $logger;
10081
}
10182

10283
/**
@@ -142,7 +123,7 @@ public function request(string $method, string $url, array $options = []): Respo
142123
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0;
143124
} elseif (1.1 === (float) $options['http_version']) {
144125
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1;
145-
} elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & self::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
126+
} elseif (\defined('CURL_VERSION_HTTP2') && (\CURL_VERSION_HTTP2 & CurlClientState::$curlVersion['features']) && ('https:' === $scheme || 2.0 === (float) $options['http_version'])) {
146127
$curlopts[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0;
147128
}
148129

@@ -185,11 +166,10 @@ public function request(string $method, string $url, array $options = []): Respo
185166
$this->multi->dnsCache->evictions = [];
186167
$port = parse_url($authority, \PHP_URL_PORT) ?: ('http:' === $scheme ? 80 : 443);
187168

188-
if ($resolve && 0x072A00 > self::$curlVersion['version_number']) {
169+
if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
189170
// DNS cache removals require curl 7.42 or higher
190171
// On lower versions, we have to create a new multi handle
191-
curl_multi_close($this->multi->handle);
192-
$this->multi->handle = (new self())->multi->handle;
172+
$this->multi->reset();
193173
}
194174

195175
foreach ($options['resolve'] as $host => $ip) {
@@ -312,7 +292,7 @@ public function request(string $method, string $url, array $options = []): Respo
312292
}
313293
}
314294

315-
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), self::$curlVersion['version_number']);
295+
return $pushedResponse ?? new CurlResponse($this->multi, $ch, $options, $this->logger, $method, self::createRedirectResolver($options, $host), CurlClientState::$curlVersion['version_number']);
316296
}
317297

318298
/**
@@ -328,78 +308,18 @@ public function stream($responses, float $timeout = null): ResponseStreamInterfa
328308

329309
if (\is_resource($this->multi->handle) || $this->multi->handle instanceof \CurlMultiHandle) {
330310
$active = 0;
331-
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active));
311+
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) {
312+
}
332313
}
333314

334315
return new ResponseStream(CurlResponse::stream($responses, $timeout));
335316
}
336317

337318
public function reset()
338319
{
339-
$this->multi->logger = $this->logger;
340320
$this->multi->reset();
341321
}
342322

343-
/**
344-
* @return array
345-
*/
346-
public function __sleep()
347-
{
348-
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
349-
}
350-
351-
public function __wakeup()
352-
{
353-
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
354-
}
355-
356-
public function __destruct()
357-
{
358-
$this->multi->logger = $this->logger;
359-
}
360-
361-
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
362-
{
363-
$headers = [];
364-
$origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
365-
366-
foreach ($requestHeaders as $h) {
367-
if (false !== $i = strpos($h, ':', 1)) {
368-
$headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
369-
}
370-
}
371-
372-
if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
373-
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
374-
375-
return \CURL_PUSH_DENY;
376-
}
377-
378-
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
379-
380-
// curl before 7.65 doesn't validate the pushed ":authority" header,
381-
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
382-
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
383-
if (!str_starts_with($origin, $url.'/')) {
384-
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));
385-
386-
return \CURL_PUSH_DENY;
387-
}
388-
389-
if ($maxPendingPushes <= \count($this->multi->pushedResponses)) {
390-
$fifoUrl = key($this->multi->pushedResponses);
391-
unset($this->multi->pushedResponses[$fifoUrl]);
392-
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
393-
}
394-
395-
$url .= $headers[':path'][0];
396-
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
397-
398-
$this->multi->pushedResponses[$url] = new PushedResponse(new CurlResponse($this->multi, $pushed), $headers, $this->multi->openHandles[(int) $parent][1] ?? [], $pushed);
399-
400-
return \CURL_PUSH_OK;
401-
}
402-
403323
/**
404324
* Accepts pushed responses only if their headers related to authentication match the request.
405325
*/

src/Symfony/Component/HttpClient/Internal/CurlClientState.php

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\HttpClient\Internal;
1313

1414
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\HttpClient\Response\CurlResponse;
1516

1617
/**
1718
* Internal representation of the cURL client's state.
@@ -31,10 +32,44 @@ final class CurlClientState extends ClientState
3132
/** @var LoggerInterface|null */
3233
public $logger;
3334

34-
public function __construct()
35+
public static $curlVersion;
36+
37+
private $maxHostConnections;
38+
private $maxPendingPushes;
39+
40+
public function __construct(int $maxHostConnections, int $maxPendingPushes)
3541
{
42+
self::$curlVersion = self::$curlVersion ?? curl_version();
43+
3644
$this->handle = curl_multi_init();
3745
$this->dnsCache = new DnsCache();
46+
$this->maxHostConnections = $maxHostConnections;
47+
$this->maxPendingPushes = $maxPendingPushes;
48+
49+
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
50+
if (\defined('CURLPIPE_MULTIPLEX')) {
51+
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
52+
}
53+
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
54+
$maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
55+
}
56+
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
57+
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
58+
}
59+
60+
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
61+
if (0 >= $maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304)) {
62+
return;
63+
}
64+
65+
// HTTP/2 push crashes before curl 7.61
66+
if (!\defined('CURLMOPT_PUSHFUNCTION') || 0x073D00 > self::$curlVersion['version_number'] || !(\CURL_VERSION_HTTP2 & self::$curlVersion['features'])) {
67+
return;
68+
}
69+
70+
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, function ($parent, $pushed, array $requestHeaders) use ($maxPendingPushes) {
71+
return $this->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
72+
});
3873
}
3974

4075
public function reset()
@@ -54,32 +89,63 @@ public function reset()
5489
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, null);
5590
}
5691

57-
$active = 0;
58-
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->handle, $active));
92+
$this->__construct($this->maxHostConnections, $this->maxPendingPushes);
5993
}
94+
}
95+
96+
public function __wakeup()
97+
{
98+
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
99+
}
60100

101+
public function __destruct()
102+
{
61103
foreach ($this->openHandles as [$ch]) {
62104
if (\is_resource($ch) || $ch instanceof \CurlHandle) {
63105
curl_setopt($ch, \CURLOPT_VERBOSE, false);
64106
}
65107
}
66-
67-
curl_multi_close($this->handle);
68-
$this->handle = curl_multi_init();
69108
}
70109

71-
public function __sleep(): array
110+
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int
72111
{
73-
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
74-
}
112+
$headers = [];
113+
$origin = curl_getinfo($parent, \CURLINFO_EFFECTIVE_URL);
75114

76-
public function __wakeup()
77-
{
78-
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
79-
}
115+
foreach ($requestHeaders as $h) {
116+
if (false !== $i = strpos($h, ':', 1)) {
117+
$headers[substr($h, 0, $i)][] = substr($h, 1 + $i);
118+
}
119+
}
80120

81-
public function __destruct()
82-
{
83-
$this->reset();
121+
if (!isset($headers[':method']) || !isset($headers[':scheme']) || !isset($headers[':authority']) || !isset($headers[':path'])) {
122+
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": pushed headers are invalid', $origin));
123+
124+
return \CURL_PUSH_DENY;
125+
}
126+
127+
$url = $headers[':scheme'][0].'://'.$headers[':authority'][0];
128+
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 (!str_starts_with($origin, $url.'/')) {
133+
$this->logger && $this->logger->debug(sprintf('Rejecting pushed response from "%s": server is not authoritative for "%s"', $origin, $url));
134+
135+
return \CURL_PUSH_DENY;
136+
}
137+
138+
if ($maxPendingPushes <= \count($this->pushedResponses)) {
139+
$fifoUrl = key($this->pushedResponses);
140+
unset($this->pushedResponses[$fifoUrl]);
141+
$this->logger && $this->logger->debug(sprintf('Evicting oldest pushed response: "%s"', $fifoUrl));
142+
}
143+
144+
$url .= $headers[':path'][0];
145+
$this->logger && $this->logger->debug(sprintf('Queueing pushed response: "%s"', $url));
146+
147+
$this->pushedResponses[$url] = new PushedResponse(new CurlResponse($this, $pushed), $headers, $this->openHandles[(int) $parent][1] ?? [], $pushed);
148+
149+
return \CURL_PUSH_OK;
84150
}
85151
}

0 commit comments

Comments
 (0)