This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | /** |
||
3 | * HTTP service client |
||
4 | * |
||
5 | * This program is free software; you can redistribute it and/or modify |
||
6 | * it under the terms of the GNU General Public License as published by |
||
7 | * the Free Software Foundation; either version 2 of the License, or |
||
8 | * (at your option) any later version. |
||
9 | * |
||
10 | * This program is distributed in the hope that it will be useful, |
||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | * GNU General Public License for more details. |
||
14 | * |
||
15 | * You should have received a copy of the GNU General Public License along |
||
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
||
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||
18 | * http://www.gnu.org/copyleft/gpl.html |
||
19 | * |
||
20 | * @file |
||
21 | */ |
||
22 | |||
23 | /** |
||
24 | * Class to handle concurrent HTTP requests |
||
25 | * |
||
26 | * HTTP request maps are arrays that use the following format: |
||
27 | * - method : GET/HEAD/PUT/POST/DELETE |
||
28 | * - url : HTTP/HTTPS URL |
||
29 | * - query : <query parameter field/value associative array> (uses RFC 3986) |
||
30 | * - headers : <header name/value associative array> |
||
31 | * - body : source to get the HTTP request body from; |
||
32 | * this can simply be a string (always), a resource for |
||
33 | * PUT requests, and a field/value array for POST request; |
||
34 | * array bodies are encoded as multipart/form-data and strings |
||
35 | * use application/x-www-form-urlencoded (headers sent automatically) |
||
36 | * - stream : resource to stream the HTTP response body to |
||
37 | * - proxy : HTTP proxy to use |
||
38 | * - flags : map of boolean flags which supports: |
||
39 | * - relayResponseHeaders : write out header via header() |
||
40 | * Request maps can use integer index 0 instead of 'method' and 1 instead of 'url'. |
||
41 | * |
||
42 | * @author Aaron Schulz |
||
43 | * @since 1.23 |
||
44 | */ |
||
45 | class MultiHttpClient { |
||
46 | /** @var resource */ |
||
47 | protected $multiHandle = null; // curl_multi handle |
||
48 | /** @var string|null SSL certificates path */ |
||
49 | protected $caBundlePath; |
||
50 | /** @var integer */ |
||
51 | protected $connTimeout = 10; |
||
52 | /** @var integer */ |
||
53 | protected $reqTimeout = 300; |
||
54 | /** @var bool */ |
||
55 | protected $usePipelining = false; |
||
56 | /** @var integer */ |
||
57 | protected $maxConnsPerHost = 50; |
||
58 | /** @var string|null proxy */ |
||
59 | protected $proxy; |
||
60 | /** @var string */ |
||
61 | protected $userAgent = 'wikimedia/multi-http-client v1.0'; |
||
62 | |||
63 | /** |
||
64 | * @param array $options |
||
65 | * - connTimeout : default connection timeout (seconds) |
||
66 | * - reqTimeout : default request timeout (seconds) |
||
67 | * - proxy : HTTP proxy to use |
||
68 | * - usePipelining : whether to use HTTP pipelining if possible (for all hosts) |
||
69 | * - maxConnsPerHost : maximum number of concurrent connections (per host) |
||
70 | * - userAgent : The User-Agent header value to send |
||
71 | * @throws Exception |
||
72 | */ |
||
73 | public function __construct( array $options ) { |
||
74 | if ( isset( $options['caBundlePath'] ) ) { |
||
75 | $this->caBundlePath = $options['caBundlePath']; |
||
76 | if ( !file_exists( $this->caBundlePath ) ) { |
||
77 | throw new Exception( "Cannot find CA bundle: " . $this->caBundlePath ); |
||
78 | } |
||
79 | } |
||
80 | static $opts = [ |
||
81 | 'connTimeout', 'reqTimeout', 'usePipelining', 'maxConnsPerHost', 'proxy', 'userAgent' |
||
82 | ]; |
||
83 | foreach ( $opts as $key ) { |
||
84 | if ( isset( $options[$key] ) ) { |
||
85 | $this->$key = $options[$key]; |
||
86 | } |
||
87 | } |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Execute an HTTP(S) request |
||
92 | * |
||
93 | * This method returns a response map of: |
||
94 | * - code : HTTP response code or 0 if there was a serious cURL error |
||
95 | * - reason : HTTP response reason (empty if there was a serious cURL error) |
||
96 | * - headers : <header name/value associative array> |
||
97 | * - body : HTTP response body or resource (if "stream" was set) |
||
98 | * - error : Any cURL error string |
||
99 | * The map also stores integer-indexed copies of these values. This lets callers do: |
||
100 | * @code |
||
101 | * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $http->run( $req ); |
||
102 | * @endcode |
||
103 | * @param array $req HTTP request array |
||
104 | * @param array $opts |
||
105 | * - connTimeout : connection timeout per request (seconds) |
||
106 | * - reqTimeout : post-connection timeout per request (seconds) |
||
107 | * @return array Response array for request |
||
108 | */ |
||
109 | public function run( array $req, array $opts = [] ) { |
||
110 | return $this->runMulti( [ $req ], $opts )[0]['response']; |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * Execute a set of HTTP(S) requests concurrently |
||
115 | * |
||
116 | * The maps are returned by this method with the 'response' field set to a map of: |
||
117 | * - code : HTTP response code or 0 if there was a serious cURL error |
||
118 | * - reason : HTTP response reason (empty if there was a serious cURL error) |
||
119 | * - headers : <header name/value associative array> |
||
120 | * - body : HTTP response body or resource (if "stream" was set) |
||
121 | * - error : Any cURL error string |
||
122 | * The map also stores integer-indexed copies of these values. This lets callers do: |
||
123 | * @code |
||
124 | * list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $req['response']; |
||
125 | * @endcode |
||
126 | * All headers in the 'headers' field are normalized to use lower case names. |
||
127 | * This is true for the request headers and the response headers. Integer-indexed |
||
128 | * method/URL entries will also be changed to use the corresponding string keys. |
||
129 | * |
||
130 | * @param array $reqs Map of HTTP request arrays |
||
131 | * @param array $opts |
||
132 | * - connTimeout : connection timeout per request (seconds) |
||
133 | * - reqTimeout : post-connection timeout per request (seconds) |
||
134 | * - usePipelining : whether to use HTTP pipelining if possible |
||
135 | * - maxConnsPerHost : maximum number of concurrent connections (per host) |
||
136 | * @return array $reqs With response array populated for each |
||
137 | * @throws Exception |
||
138 | */ |
||
139 | public function runMulti( array $reqs, array $opts = [] ) { |
||
140 | $chm = $this->getCurlMulti(); |
||
141 | |||
142 | // Normalize $reqs and add all of the required cURL handles... |
||
143 | $handles = []; |
||
144 | foreach ( $reqs as $index => &$req ) { |
||
145 | $req['response'] = [ |
||
146 | 'code' => 0, |
||
147 | 'reason' => '', |
||
148 | 'headers' => [], |
||
149 | 'body' => '', |
||
150 | 'error' => '' |
||
151 | ]; |
||
152 | if ( isset( $req[0] ) ) { |
||
153 | $req['method'] = $req[0]; // short-form |
||
154 | unset( $req[0] ); |
||
155 | } |
||
156 | if ( isset( $req[1] ) ) { |
||
157 | $req['url'] = $req[1]; // short-form |
||
158 | unset( $req[1] ); |
||
159 | } |
||
160 | if ( !isset( $req['method'] ) ) { |
||
161 | throw new Exception( "Request has no 'method' field set." ); |
||
162 | } elseif ( !isset( $req['url'] ) ) { |
||
163 | throw new Exception( "Request has no 'url' field set." ); |
||
164 | } |
||
165 | $req['query'] = isset( $req['query'] ) ? $req['query'] : []; |
||
166 | $headers = []; // normalized headers |
||
167 | if ( isset( $req['headers'] ) ) { |
||
168 | foreach ( $req['headers'] as $name => $value ) { |
||
169 | $headers[strtolower( $name )] = $value; |
||
170 | } |
||
171 | } |
||
172 | $req['headers'] = $headers; |
||
173 | if ( !isset( $req['body'] ) ) { |
||
174 | $req['body'] = ''; |
||
175 | $req['headers']['content-length'] = 0; |
||
176 | } |
||
177 | $req['flags'] = isset( $req['flags'] ) ? $req['flags'] : []; |
||
178 | $handles[$index] = $this->getCurlHandle( $req, $opts ); |
||
179 | if ( count( $reqs ) > 1 ) { |
||
180 | // https://github.com/guzzle/guzzle/issues/349 |
||
181 | curl_setopt( $handles[$index], CURLOPT_FORBID_REUSE, true ); |
||
182 | } |
||
183 | } |
||
184 | unset( $req ); // don't assign over this by accident |
||
185 | |||
186 | $indexes = array_keys( $reqs ); |
||
187 | if ( isset( $opts['usePipelining'] ) ) { |
||
188 | curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$opts['usePipelining'] ); |
||
189 | } |
||
190 | if ( isset( $opts['maxConnsPerHost'] ) ) { |
||
191 | // Keep these sockets around as they may be needed later in the request |
||
192 | curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$opts['maxConnsPerHost'] ); |
||
193 | } |
||
194 | |||
195 | // @TODO: use a per-host rolling handle window (e.g. CURLMOPT_MAX_HOST_CONNECTIONS) |
||
196 | $batches = array_chunk( $indexes, $this->maxConnsPerHost ); |
||
197 | $infos = []; |
||
198 | |||
199 | foreach ( $batches as $batch ) { |
||
200 | // Attach all cURL handles for this batch |
||
201 | foreach ( $batch as $index ) { |
||
202 | curl_multi_add_handle( $chm, $handles[$index] ); |
||
203 | } |
||
204 | // Execute the cURL handles concurrently... |
||
205 | $active = null; // handles still being processed |
||
206 | do { |
||
207 | // Do any available work... |
||
208 | do { |
||
209 | $mrc = curl_multi_exec( $chm, $active ); |
||
210 | $info = curl_multi_info_read( $chm ); |
||
211 | if ( $info !== false ) { |
||
212 | $infos[(int)$info['handle']] = $info; |
||
213 | } |
||
214 | } while ( $mrc == CURLM_CALL_MULTI_PERFORM ); |
||
215 | // Wait (if possible) for available work... |
||
216 | if ( $active > 0 && $mrc == CURLM_OK ) { |
||
217 | if ( curl_multi_select( $chm, 10 ) == -1 ) { |
||
218 | // PHP bug 63411; https://curl.haxx.se/libcurl/c/curl_multi_fdset.html |
||
219 | usleep( 5000 ); // 5ms |
||
220 | } |
||
221 | } |
||
222 | } while ( $active > 0 && $mrc == CURLM_OK ); |
||
223 | } |
||
224 | |||
225 | // Remove all of the added cURL handles and check for errors... |
||
226 | foreach ( $reqs as $index => &$req ) { |
||
227 | $ch = $handles[$index]; |
||
228 | curl_multi_remove_handle( $chm, $ch ); |
||
229 | |||
230 | if ( isset( $infos[(int)$ch] ) ) { |
||
231 | $info = $infos[(int)$ch]; |
||
232 | $errno = $info['result']; |
||
233 | if ( $errno !== 0 ) { |
||
234 | $req['response']['error'] = "(curl error: $errno)"; |
||
235 | if ( function_exists( 'curl_strerror' ) ) { |
||
236 | $req['response']['error'] .= " " . curl_strerror( $errno ); |
||
237 | } |
||
238 | } |
||
239 | } else { |
||
240 | $req['response']['error'] = "(curl error: no status set)"; |
||
241 | } |
||
242 | |||
243 | // For convenience with the list() operator |
||
244 | $req['response'][0] = $req['response']['code']; |
||
245 | $req['response'][1] = $req['response']['reason']; |
||
246 | $req['response'][2] = $req['response']['headers']; |
||
247 | $req['response'][3] = $req['response']['body']; |
||
248 | $req['response'][4] = $req['response']['error']; |
||
249 | curl_close( $ch ); |
||
250 | // Close any string wrapper file handles |
||
251 | if ( isset( $req['_closeHandle'] ) ) { |
||
252 | fclose( $req['_closeHandle'] ); |
||
253 | unset( $req['_closeHandle'] ); |
||
254 | } |
||
255 | } |
||
256 | unset( $req ); // don't assign over this by accident |
||
257 | |||
258 | // Restore the default settings |
||
259 | curl_multi_setopt( $chm, CURLMOPT_PIPELINING, (int)$this->usePipelining ); |
||
260 | curl_multi_setopt( $chm, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost ); |
||
261 | |||
262 | return $reqs; |
||
263 | } |
||
264 | |||
265 | /** |
||
266 | * @param array $req HTTP request map |
||
267 | * @param array $opts |
||
268 | * - connTimeout : default connection timeout |
||
269 | * - reqTimeout : default request timeout |
||
270 | * @return resource |
||
271 | * @throws Exception |
||
272 | */ |
||
273 | protected function getCurlHandle( array &$req, array $opts = [] ) { |
||
274 | $ch = curl_init(); |
||
275 | |||
276 | curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, |
||
277 | isset( $opts['connTimeout'] ) ? $opts['connTimeout'] : $this->connTimeout ); |
||
278 | curl_setopt( $ch, CURLOPT_PROXY, isset( $req['proxy'] ) ? $req['proxy'] : $this->proxy ); |
||
279 | curl_setopt( $ch, CURLOPT_TIMEOUT, |
||
280 | isset( $opts['reqTimeout'] ) ? $opts['reqTimeout'] : $this->reqTimeout ); |
||
281 | curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1 ); |
||
282 | curl_setopt( $ch, CURLOPT_MAXREDIRS, 4 ); |
||
283 | curl_setopt( $ch, CURLOPT_HEADER, 0 ); |
||
284 | if ( !is_null( $this->caBundlePath ) ) { |
||
285 | curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, true ); |
||
286 | curl_setopt( $ch, CURLOPT_CAINFO, $this->caBundlePath ); |
||
287 | } |
||
288 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); |
||
289 | |||
290 | $url = $req['url']; |
||
291 | $query = http_build_query( $req['query'], '', '&', PHP_QUERY_RFC3986 ); |
||
292 | if ( $query != '' ) { |
||
293 | $url .= strpos( $req['url'], '?' ) === false ? "?$query" : "&$query"; |
||
294 | } |
||
295 | curl_setopt( $ch, CURLOPT_URL, $url ); |
||
296 | |||
297 | curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, $req['method'] ); |
||
298 | if ( $req['method'] === 'HEAD' ) { |
||
299 | curl_setopt( $ch, CURLOPT_NOBODY, 1 ); |
||
300 | } |
||
301 | |||
302 | if ( $req['method'] === 'PUT' ) { |
||
303 | curl_setopt( $ch, CURLOPT_PUT, 1 ); |
||
304 | if ( is_resource( $req['body'] ) ) { |
||
305 | curl_setopt( $ch, CURLOPT_INFILE, $req['body'] ); |
||
306 | if ( isset( $req['headers']['content-length'] ) ) { |
||
307 | curl_setopt( $ch, CURLOPT_INFILESIZE, $req['headers']['content-length'] ); |
||
308 | } elseif ( isset( $req['headers']['transfer-encoding'] ) && |
||
309 | $req['headers']['transfer-encoding'] === 'chunks' |
||
310 | ) { |
||
311 | curl_setopt( $ch, CURLOPT_UPLOAD, true ); |
||
312 | } else { |
||
313 | throw new Exception( "Missing 'Content-Length' or 'Transfer-Encoding' header." ); |
||
314 | } |
||
315 | } elseif ( $req['body'] !== '' ) { |
||
316 | $fp = fopen( "php://temp", "wb+" ); |
||
317 | fwrite( $fp, $req['body'], strlen( $req['body'] ) ); |
||
318 | rewind( $fp ); |
||
319 | curl_setopt( $ch, CURLOPT_INFILE, $fp ); |
||
320 | curl_setopt( $ch, CURLOPT_INFILESIZE, strlen( $req['body'] ) ); |
||
321 | $req['_closeHandle'] = $fp; // remember to close this later |
||
322 | } else { |
||
323 | curl_setopt( $ch, CURLOPT_INFILESIZE, 0 ); |
||
324 | } |
||
325 | curl_setopt( $ch, CURLOPT_READFUNCTION, |
||
326 | function ( $ch, $fd, $length ) { |
||
327 | $data = fread( $fd, $length ); |
||
328 | $len = strlen( $data ); |
||
0 ignored issues
–
show
|
|||
329 | return $data; |
||
330 | } |
||
331 | ); |
||
332 | } elseif ( $req['method'] === 'POST' ) { |
||
333 | curl_setopt( $ch, CURLOPT_POST, 1 ); |
||
334 | // Don't interpret POST parameters starting with '@' as file uploads, because this |
||
335 | // makes it impossible to POST plain values starting with '@' (and causes security |
||
336 | // issues potentially exposing the contents of local files). |
||
337 | // The PHP manual says this option was introduced in PHP 5.5 defaults to true in PHP 5.6, |
||
338 | // but we support lower versions, and the option doesn't exist in HHVM 5.6.99. |
||
339 | if ( defined( 'CURLOPT_SAFE_UPLOAD' ) ) { |
||
340 | curl_setopt( $ch, CURLOPT_SAFE_UPLOAD, true ); |
||
341 | } elseif ( is_array( $req['body'] ) ) { |
||
342 | // In PHP 5.2 and later, '@' is interpreted as a file upload if POSTFIELDS |
||
343 | // is an array, but not if it's a string. So convert $req['body'] to a string |
||
344 | // for safety. |
||
345 | $req['body'] = http_build_query( $req['body'] ); |
||
346 | } |
||
347 | curl_setopt( $ch, CURLOPT_POSTFIELDS, $req['body'] ); |
||
348 | } else { |
||
349 | if ( is_resource( $req['body'] ) || $req['body'] !== '' ) { |
||
350 | throw new Exception( "HTTP body specified for a non PUT/POST request." ); |
||
351 | } |
||
352 | $req['headers']['content-length'] = 0; |
||
353 | } |
||
354 | |||
355 | if ( !isset( $req['headers']['user-agent'] ) ) { |
||
356 | $req['headers']['user-agent'] = $this->userAgent; |
||
357 | } |
||
358 | |||
359 | $headers = []; |
||
360 | foreach ( $req['headers'] as $name => $value ) { |
||
361 | if ( strpos( $name, ': ' ) ) { |
||
362 | throw new Exception( "Headers cannot have ':' in the name." ); |
||
363 | } |
||
364 | $headers[] = $name . ': ' . trim( $value ); |
||
365 | } |
||
366 | curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); |
||
367 | |||
368 | curl_setopt( $ch, CURLOPT_HEADERFUNCTION, |
||
369 | function ( $ch, $header ) use ( &$req ) { |
||
370 | if ( !empty( $req['flags']['relayResponseHeaders'] ) ) { |
||
371 | header( $header ); |
||
372 | } |
||
373 | $length = strlen( $header ); |
||
374 | $matches = []; |
||
375 | if ( preg_match( "/^(HTTP\/1\.[01]) (\d{3}) (.*)/", $header, $matches ) ) { |
||
376 | $req['response']['code'] = (int)$matches[2]; |
||
377 | $req['response']['reason'] = trim( $matches[3] ); |
||
378 | return $length; |
||
379 | } |
||
380 | if ( strpos( $header, ":" ) === false ) { |
||
381 | return $length; |
||
382 | } |
||
383 | list( $name, $value ) = explode( ":", $header, 2 ); |
||
384 | $req['response']['headers'][strtolower( $name )] = trim( $value ); |
||
385 | return $length; |
||
386 | } |
||
387 | ); |
||
388 | |||
389 | if ( isset( $req['stream'] ) ) { |
||
390 | // Don't just use CURLOPT_FILE as that might give: |
||
391 | // curl_setopt(): cannot represent a stream of type Output as a STDIO FILE* |
||
392 | // The callback here handles both normal files and php://temp handles. |
||
393 | curl_setopt( $ch, CURLOPT_WRITEFUNCTION, |
||
394 | function ( $ch, $data ) use ( &$req ) { |
||
395 | return fwrite( $req['stream'], $data ); |
||
396 | } |
||
397 | ); |
||
398 | } else { |
||
399 | curl_setopt( $ch, CURLOPT_WRITEFUNCTION, |
||
400 | function ( $ch, $data ) use ( &$req ) { |
||
401 | $req['response']['body'] .= $data; |
||
402 | return strlen( $data ); |
||
403 | } |
||
404 | ); |
||
405 | } |
||
406 | |||
407 | return $ch; |
||
408 | } |
||
409 | |||
410 | /** |
||
411 | * @return resource |
||
412 | */ |
||
413 | protected function getCurlMulti() { |
||
414 | if ( !$this->multiHandle ) { |
||
415 | $cmh = curl_multi_init(); |
||
416 | curl_multi_setopt( $cmh, CURLMOPT_PIPELINING, (int)$this->usePipelining ); |
||
417 | curl_multi_setopt( $cmh, CURLMOPT_MAXCONNECTS, (int)$this->maxConnsPerHost ); |
||
418 | $this->multiHandle = $cmh; |
||
419 | } |
||
420 | return $this->multiHandle; |
||
421 | } |
||
422 | |||
423 | function __destruct() { |
||
424 | if ( $this->multiHandle ) { |
||
425 | curl_multi_close( $this->multiHandle ); |
||
426 | } |
||
427 | } |
||
428 | } |
||
429 |
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
Both the
$myVar
assignment in line 1 and the$higher
assignment in line 2 are dead. The first because$myVar
is never used and the second because$higher
is always overwritten for every possible time line.