11
11
12
12
namespace Symfony \Component \HttpClient ;
13
13
14
+ use Psr \Log \LoggerAwareInterface ;
15
+ use Psr \Log \LoggerAwareTrait ;
14
16
use Psr \Log \LoggerInterface ;
15
- use Psr \Log \NullLogger ;
16
17
use Symfony \Component \HttpClient \Exception \TransportException ;
17
18
use Symfony \Component \HttpClient \Response \CurlResponse ;
18
19
use Symfony \Component \HttpClient \Response \ResponseStream ;
30
31
*
31
32
* @experimental in 4.3
32
33
*/
33
- final class CurlHttpClient implements HttpClientInterface
34
+ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface
34
35
{
35
36
use HttpClientTrait;
37
+ use LoggerAwareTrait;
36
38
37
39
private $ defaultOptions = self ::OPTIONS_DEFAULTS ;
38
40
private $ multi ;
39
- private $ logger ;
40
41
41
42
/**
42
43
* @param array $defaultOptions Default requests' options
43
44
* @param int $maxHostConnections The maximum number of connections to a single host
45
+ * @param int $maxPendingPushes The maximum number of pushed responses to accept in the queue
44
46
*
45
47
* @see HttpClientInterface::OPTIONS_DEFAULTS for available options
46
48
*/
47
- public function __construct (array $ defaultOptions = [], LoggerInterface $ logger = null , int $ maxHostConnections = 6 )
49
+ public function __construct (array $ defaultOptions = [], int $ maxHostConnections = 6 , int $ maxPendingPushes = 50 )
48
50
{
49
- $ this ->logger = $ logger ?? new NullLogger ();
50
-
51
51
if ($ defaultOptions ) {
52
52
[, $ this ->defaultOptions ] = self ::prepareRequest (null , null , $ defaultOptions , self ::OPTIONS_DEFAULTS );
53
53
}
@@ -70,7 +70,7 @@ public function __construct(array $defaultOptions = [], LoggerInterface $logger
70
70
];
71
71
72
72
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/bug.php?id=77535
73
- if (\PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304 )) {
73
+ if (0 >= $ maxPendingPushes || \PHP_VERSION_ID < 70217 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70304 )) {
74
74
return ;
75
75
}
76
76
@@ -79,8 +79,10 @@ public function __construct(array $defaultOptions = [], LoggerInterface $logger
79
79
return ;
80
80
}
81
81
82
- curl_multi_setopt ($ mh , CURLMOPT_PUSHFUNCTION , static function ($ parent , $ pushed , array $ requestHeaders ) use ($ multi ) {
83
- return self ::handlePush ($ parent , $ pushed , $ requestHeaders , $ multi );
82
+ $ logger = &$ this ->logger ;
83
+
84
+ curl_multi_setopt ($ mh , CURLMOPT_PUSHFUNCTION , static function ($ parent , $ pushed , array $ requestHeaders ) use ($ multi , $ maxPendingPushes , &$ logger ) {
85
+ return self ::handlePush ($ parent , $ pushed , $ requestHeaders , $ multi , $ maxPendingPushes , $ logger );
84
86
});
85
87
}
86
88
@@ -91,7 +93,6 @@ public function __construct(array $defaultOptions = [], LoggerInterface $logger
91
93
*/
92
94
public function request (string $ method , string $ url , array $ options = []): ResponseInterface
93
95
{
94
- $ this ->logger ->notice ('Making a request ' , ['url ' => $ url , 'method ' => $ method , 'client ' => static ::class]);
95
96
[$ url , $ options ] = self ::prepareRequest ($ method , $ url , $ options , $ this ->defaultOptions );
96
97
$ scheme = $ url ['scheme ' ];
97
98
$ authority = $ url ['authority ' ];
@@ -109,14 +110,19 @@ public function request(string $method, string $url, array $options = []): Respo
109
110
];
110
111
111
112
if ('GET ' === $ method && !$ options ['body ' ] && $ expectedHeaders === $ pushedHeaders ) {
112
- $ this ->logger ->debug ('Creating pushed response ' );
113
+ $ this ->logger && $ this ->logger ->info (sprintf ('Connecting request to pushed response: %s %s ' , $ method , $ url ));
114
+
113
115
// Reinitialize the pushed response with request's options
114
- $ pushedResponse ->__construct ($ this ->multi , $ url , $ options );
116
+ $ pushedResponse ->__construct ($ this ->multi , $ url , $ options, $ this -> logger );
115
117
116
118
return $ pushedResponse ;
117
119
}
120
+
121
+ $ this ->logger && $ this ->logger ->info (sprintf ('Rejecting pushed response for "%s": authorization headers don \'t match the request ' , $ url ));
118
122
}
119
123
124
+ $ this ->logger && $ this ->logger ->info (sprintf ('Request: %s %s ' , $ method , $ url ));
125
+
120
126
$ curlopts = [
121
127
CURLOPT_URL => $ url ,
122
128
CURLOPT_USERAGENT => 'Symfony HttpClient/Curl ' ,
@@ -163,7 +169,7 @@ public function request(string $method, string $url, array $options = []): Respo
163
169
// DNS cache removals require curl 7.42 or higher
164
170
// On lower versions, we have to create a new multi handle
165
171
curl_multi_close ($ this ->multi ->handle );
166
- $ this ->multi ->handle = (new self ([], $ this -> logger ))->multi ->handle ;
172
+ $ this ->multi ->handle = (new self ())->multi ->handle ;
167
173
}
168
174
169
175
foreach ($ options ['resolve ' ] as $ host => $ ip ) {
@@ -262,7 +268,7 @@ public function request(string $method, string $url, array $options = []): Respo
262
268
}
263
269
}
264
270
265
- return new CurlResponse ($ this ->multi , $ ch , $ options , $ method , self ::createRedirectResolver ($ options , $ host ));
271
+ return new CurlResponse ($ this ->multi , $ ch , $ options , $ this -> logger , $ method , self ::createRedirectResolver ($ options , $ host ));
266
272
}
267
273
268
274
/**
@@ -289,30 +295,44 @@ public function __destruct()
289
295
}
290
296
}
291
297
292
- private static function handlePush ($ parent , $ pushed , array $ requestHeaders , \stdClass $ multi ): int
298
+ private static function handlePush ($ parent , $ pushed , array $ requestHeaders , \stdClass $ multi, int $ maxPendingPushes , ? LoggerInterface $ logger ): int
293
299
{
294
300
$ headers = [];
301
+ $ origin = curl_getinfo ($ parent , CURLINFO_EFFECTIVE_URL );
295
302
296
303
foreach ($ requestHeaders as $ h ) {
297
304
if (false !== $ i = strpos ($ h , ': ' , 1 )) {
298
305
$ headers [substr ($ h , 0 , $ i )] = substr ($ h , 1 + $ i );
299
306
}
300
307
}
301
308
302
- if ('GET ' !== $ headers [':method ' ] || isset ($ headers ['range ' ])) {
309
+ if (!isset ($ headers [':method ' ]) || !isset ($ headers [':scheme ' ]) || !isset ($ headers [':authority ' ]) || !isset ($ headers [':path ' ]) || 'GET ' !== $ headers [':method ' ] || isset ($ headers ['range ' ])) {
310
+ $ logger && $ logger ->info (sprintf ('Rejecting pushed response from "%s": pushed headers are invalid ' , $ origin ));
311
+
303
312
return CURL_PUSH_DENY ;
304
313
}
305
314
306
315
$ url = $ headers [':scheme ' ].':// ' .$ headers [':authority ' ];
307
316
317
+ if ($ maxPendingPushes <= \count ($ multi ->pushedResponses )) {
318
+ $ logger && $ logger ->info (sprintf ('Rejecting pushed response from "%s" for "%s": the queue is full ' , $ origin , $ url ));
319
+
320
+ return CURL_PUSH_DENY ;
321
+ }
322
+
308
323
// curl before 7.65 doesn't validate the pushed ":authority" header,
309
324
// but this is a MUST in the HTTP/2 RFC; let's restrict pushes to the original host,
310
325
// ignoring domains mentioned as alt-name in the certificate for now (same as curl).
311
- if (0 !== strpos (curl_getinfo ($ parent , CURLINFO_EFFECTIVE_URL ), $ url .'/ ' )) {
326
+ if (0 !== strpos ($ origin , $ url .'/ ' )) {
327
+ $ logger && $ logger ->info (sprintf ('Rejecting pushed response from "%s": server is not authoritative for "%s" ' , $ origin , $ url ));
328
+
312
329
return CURL_PUSH_DENY ;
313
330
}
314
331
315
- $ multi ->pushedResponses [$ url .$ headers [':path ' ]] = [
332
+ $ url .= $ headers [':path ' ];
333
+ $ logger && $ logger ->info (sprintf ('Queueing pushed response: %s ' , $ url ));
334
+
335
+ $ multi ->pushedResponses [$ url ] = [
316
336
new CurlResponse ($ multi , $ pushed ),
317
337
[
318
338
$ headers ['authorization ' ] ?? null ,
0 commit comments