1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* fsockopen HTTP transport |
4
|
|
|
* |
5
|
|
|
* @package Requests |
6
|
|
|
* @subpackage Transport |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* fsockopen HTTP transport |
11
|
|
|
* |
12
|
|
|
* @package Requests |
13
|
|
|
* @subpackage Transport |
14
|
|
|
*/ |
15
|
|
|
class Requests_Transport_fsockopen implements Requests_Transport { |
16
|
|
|
/** |
17
|
|
|
* Second to microsecond conversion |
18
|
|
|
* |
19
|
|
|
* @var integer |
20
|
|
|
*/ |
21
|
|
|
const SECOND_IN_MICROSECONDS = 1000000; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* Raw HTTP data |
25
|
|
|
* |
26
|
|
|
* @var string |
27
|
|
|
*/ |
28
|
|
|
public $headers = ''; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Stream metadata |
32
|
|
|
* |
33
|
|
|
* @var array Associative array of properties, see {@see https://secure.php.net/stream_get_meta_data} |
34
|
|
|
*/ |
35
|
|
|
public $info; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* What's the maximum number of bytes we should keep? |
39
|
|
|
* |
40
|
|
|
* @var int|bool Byte count, or false if no limit. |
41
|
|
|
*/ |
42
|
|
|
protected $max_bytes = false; |
43
|
|
|
|
44
|
|
|
protected $connect_error = ''; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Perform a request |
48
|
|
|
* |
49
|
|
|
* @throws Requests_Exception On failure to connect to socket (`fsockopenerror`) |
50
|
|
|
* @throws Requests_Exception On socket timeout (`timeout`) |
51
|
|
|
* |
52
|
|
|
* @param string $url URL to request |
53
|
|
|
* @param array $headers Associative array of request headers |
54
|
|
|
* @param string|array $data Data to send either as the POST body, or as parameters in the URL for a GET/HEAD |
55
|
|
|
* @param array $options Request options, see {@see Requests::response()} for documentation |
56
|
|
|
* @return string Raw HTTP result |
57
|
|
|
*/ |
58
|
|
|
public function request($url, $headers = array(), $data = array(), $options = array()) { |
59
|
|
|
$options['hooks']->dispatch('fsockopen.before_request'); |
60
|
|
|
|
61
|
|
|
$url_parts = parse_url($url); |
62
|
|
|
if (empty($url_parts)) { |
63
|
|
|
throw new Requests_Exception('Invalid URL.', 'invalidurl', $url); |
64
|
|
|
} |
65
|
|
|
$host = $url_parts['host']; |
66
|
|
|
$context = stream_context_create(); |
67
|
|
|
$verifyname = false; |
68
|
|
|
$case_insensitive_headers = new Requests_Utility_CaseInsensitiveDictionary($headers); |
69
|
|
|
|
70
|
|
|
// HTTPS support |
71
|
|
|
if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { |
72
|
|
|
$remote_socket = 'ssl://' . $host; |
73
|
|
|
if (!isset($url_parts['port'])) { |
74
|
|
|
$url_parts['port'] = 443; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
$context_options = array( |
78
|
|
|
'verify_peer' => true, |
79
|
|
|
// 'CN_match' => $host, |
80
|
|
|
'capture_peer_cert' => true |
81
|
|
|
); |
82
|
|
|
$verifyname = true; |
83
|
|
|
|
84
|
|
|
// SNI, if enabled (OpenSSL >=0.9.8j) |
85
|
|
|
if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) { |
86
|
|
|
$context_options['SNI_enabled'] = true; |
87
|
|
View Code Duplication |
if (isset($options['verifyname']) && $options['verifyname'] === false) { |
88
|
|
|
$context_options['SNI_enabled'] = false; |
89
|
|
|
} |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
if (isset($options['verify'])) { |
93
|
|
|
if ($options['verify'] === false) { |
94
|
|
|
$context_options['verify_peer'] = false; |
95
|
|
|
} |
96
|
|
|
elseif (is_string($options['verify'])) { |
97
|
|
|
$context_options['cafile'] = $options['verify']; |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
|
101
|
|
View Code Duplication |
if (isset($options['verifyname']) && $options['verifyname'] === false) { |
102
|
|
|
$context_options['verify_peer_name'] = false; |
103
|
|
|
$verifyname = false; |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
stream_context_set_option($context, array('ssl' => $context_options)); |
107
|
|
|
} |
108
|
|
|
else { |
109
|
|
|
$remote_socket = 'tcp://' . $host; |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
$this->max_bytes = $options['max_bytes']; |
113
|
|
|
|
114
|
|
|
if (!isset($url_parts['port'])) { |
115
|
|
|
$url_parts['port'] = 80; |
116
|
|
|
} |
117
|
|
|
$remote_socket .= ':' . $url_parts['port']; |
118
|
|
|
|
119
|
|
|
set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE); |
120
|
|
|
|
121
|
|
|
$options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket)); |
122
|
|
|
|
123
|
|
|
$socket = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context); |
124
|
|
|
|
125
|
|
|
restore_error_handler(); |
126
|
|
|
|
127
|
|
|
if ($verifyname && !$this->verify_certificate_from_context($host, $context)) { |
128
|
|
|
throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match'); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
if (!$socket) { |
132
|
|
|
if ($errno === 0) { |
133
|
|
|
// Connection issue |
134
|
|
|
throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error'); |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
throw new Requests_Exception($errstr, 'fsockopenerror', null, $errno); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
$data_format = $options['data_format']; |
141
|
|
|
|
142
|
|
|
if ($data_format === 'query') { |
143
|
|
|
$path = self::format_get($url_parts, $data); |
|
|
|
|
144
|
|
|
$data = ''; |
145
|
|
|
} |
146
|
|
|
else { |
147
|
|
|
$path = self::format_get($url_parts, array()); |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
$options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url)); |
151
|
|
|
|
152
|
|
|
$request_body = ''; |
153
|
|
|
$out = sprintf("%s %s HTTP/%.1f\r\n", $options['type'], $path, $options['protocol_version']); |
154
|
|
|
|
155
|
|
|
if ($options['type'] !== Requests::TRACE) { |
156
|
|
|
if (is_array($data)) { |
157
|
|
|
$request_body = http_build_query($data, null, '&'); |
158
|
|
|
} |
159
|
|
|
else { |
160
|
|
|
$request_body = $data; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
if (!empty($data)) { |
164
|
|
|
if (!isset($case_insensitive_headers['Content-Length'])) { |
165
|
|
|
$headers['Content-Length'] = strlen($request_body); |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
if (!isset($case_insensitive_headers['Content-Type'])) { |
169
|
|
|
$headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
if (!isset($case_insensitive_headers['Host'])) { |
175
|
|
|
$out .= sprintf('Host: %s', $url_parts['host']); |
176
|
|
|
|
177
|
|
|
if (( 'http' === strtolower($url_parts['scheme']) && $url_parts['port'] !== 80 ) || ( 'https' === strtolower($url_parts['scheme']) && $url_parts['port'] !== 443 )) { |
178
|
|
|
$out .= ':' . $url_parts['port']; |
179
|
|
|
} |
180
|
|
|
$out .= "\r\n"; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
if (!isset($case_insensitive_headers['User-Agent'])) { |
184
|
|
|
$out .= sprintf("User-Agent: %s\r\n", $options['useragent']); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$accept_encoding = $this->accept_encoding(); |
188
|
|
|
if (!isset($case_insensitive_headers['Accept-Encoding']) && !empty($accept_encoding)) { |
189
|
|
|
$out .= sprintf("Accept-Encoding: %s\r\n", $accept_encoding); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
$headers = Requests::flatten($headers); |
193
|
|
|
|
194
|
|
|
if (!empty($headers)) { |
195
|
|
|
$out .= implode($headers, "\r\n") . "\r\n"; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$options['hooks']->dispatch('fsockopen.after_headers', array(&$out)); |
199
|
|
|
|
200
|
|
|
if (substr($out, -2) !== "\r\n") { |
201
|
|
|
$out .= "\r\n"; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
if (!isset($case_insensitive_headers['Connection'])) { |
205
|
|
|
$out .= "Connection: Close\r\n"; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$out .= "\r\n" . $request_body; |
209
|
|
|
|
210
|
|
|
$options['hooks']->dispatch('fsockopen.before_send', array(&$out)); |
211
|
|
|
|
212
|
|
|
fwrite($socket, $out); |
213
|
|
|
$options['hooks']->dispatch('fsockopen.after_send', array($out)); |
214
|
|
|
|
215
|
|
|
if (!$options['blocking']) { |
216
|
|
|
fclose($socket); |
217
|
|
|
$fake_headers = ''; |
218
|
|
|
$options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers)); |
219
|
|
|
return ''; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
$timeout_sec = (int) floor($options['timeout']); |
223
|
|
|
if ($timeout_sec == $options['timeout']) { |
224
|
|
|
$timeout_msec = 0; |
225
|
|
|
} |
226
|
|
|
else { |
227
|
|
|
$timeout_msec = self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS; |
228
|
|
|
} |
229
|
|
|
stream_set_timeout($socket, $timeout_sec, $timeout_msec); |
230
|
|
|
|
231
|
|
|
$response = $body = $headers = ''; |
232
|
|
|
$this->info = stream_get_meta_data($socket); |
233
|
|
|
$size = 0; |
234
|
|
|
$doingbody = false; |
235
|
|
|
$download = false; |
236
|
|
|
if ($options['filename']) { |
237
|
|
|
$download = fopen($options['filename'], 'wb'); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
while (!feof($socket)) { |
241
|
|
|
$this->info = stream_get_meta_data($socket); |
242
|
|
|
if ($this->info['timed_out']) { |
243
|
|
|
throw new Requests_Exception('fsocket timed out', 'timeout'); |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
$block = fread($socket, Requests::BUFFER_SIZE); |
247
|
|
|
if (!$doingbody) { |
248
|
|
|
$response .= $block; |
249
|
|
|
if (strpos($response, "\r\n\r\n")) { |
250
|
|
|
list($headers, $block) = explode("\r\n\r\n", $response, 2); |
251
|
|
|
$doingbody = true; |
252
|
|
|
} |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
// Are we in body mode now? |
256
|
|
|
if ($doingbody) { |
257
|
|
|
$options['hooks']->dispatch('request.progress', array($block, $size, $this->max_bytes)); |
258
|
|
|
$data_length = strlen($block); |
259
|
|
|
if ($this->max_bytes) { |
260
|
|
|
// Have we already hit a limit? |
261
|
|
|
if ($size === $this->max_bytes) { |
262
|
|
|
continue; |
263
|
|
|
} |
264
|
|
|
if (($size + $data_length) > $this->max_bytes) { |
265
|
|
|
// Limit the length |
266
|
|
|
$limited_length = ($this->max_bytes - $size); |
267
|
|
|
$block = substr($block, 0, $limited_length); |
268
|
|
|
} |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
$size += strlen($block); |
272
|
|
|
if ($download) { |
273
|
|
|
fwrite($download, $block); |
274
|
|
|
} |
275
|
|
|
else { |
276
|
|
|
$body .= $block; |
277
|
|
|
} |
278
|
|
|
} |
279
|
|
|
} |
280
|
|
|
$this->headers = $headers; |
281
|
|
|
|
282
|
|
|
if ($download) { |
283
|
|
|
fclose($download); |
284
|
|
|
} |
285
|
|
|
else { |
286
|
|
|
$this->headers .= "\r\n\r\n" . $body; |
287
|
|
|
} |
288
|
|
|
fclose($socket); |
289
|
|
|
|
290
|
|
|
$options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers, &$this->info)); |
291
|
|
|
return $this->headers; |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/** |
295
|
|
|
* Send multiple requests simultaneously |
296
|
|
|
* |
297
|
|
|
* @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request} |
298
|
|
|
* @param array $options Global options, see {@see Requests::response()} for documentation |
299
|
|
|
* @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well) |
300
|
|
|
*/ |
301
|
|
|
public function request_multiple($requests, $options) { |
302
|
|
|
$responses = array(); |
303
|
|
|
$class = get_class($this); |
304
|
|
|
foreach ($requests as $id => $request) { |
305
|
|
|
try { |
306
|
|
|
$handler = new $class(); |
307
|
|
|
$responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']); |
308
|
|
|
|
309
|
|
|
$request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request)); |
310
|
|
|
} |
311
|
|
|
catch (Requests_Exception $e) { |
312
|
|
|
$responses[$id] = $e; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
View Code Duplication |
if (!is_string($responses[$id])) { |
316
|
|
|
$request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id)); |
317
|
|
|
} |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
return $responses; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
/** |
324
|
|
|
* Retrieve the encodings we can accept |
325
|
|
|
* |
326
|
|
|
* @return string Accept-Encoding header value |
327
|
|
|
*/ |
328
|
|
|
protected static function accept_encoding() { |
329
|
|
|
$type = array(); |
330
|
|
|
if (function_exists('gzinflate')) { |
331
|
|
|
$type[] = 'deflate;q=1.0'; |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
if (function_exists('gzuncompress')) { |
335
|
|
|
$type[] = 'compress;q=0.5'; |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
$type[] = 'gzip;q=0.5'; |
339
|
|
|
|
340
|
|
|
return implode(', ', $type); |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Format a URL given GET data |
345
|
|
|
* |
346
|
|
|
* @param array $url_parts |
347
|
|
|
* @param array|object $data Data to build query using, see {@see https://secure.php.net/http_build_query} |
348
|
|
|
* @return string URL with data |
349
|
|
|
*/ |
350
|
|
|
protected static function format_get($url_parts, $data) { |
351
|
|
|
if (!empty($data)) { |
352
|
|
|
if (empty($url_parts['query'])) { |
353
|
|
|
$url_parts['query'] = ''; |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
$url_parts['query'] .= '&' . http_build_query($data, null, '&'); |
357
|
|
|
$url_parts['query'] = trim($url_parts['query'], '&'); |
358
|
|
|
} |
359
|
|
|
if (isset($url_parts['path'])) { |
360
|
|
|
if (isset($url_parts['query'])) { |
361
|
|
|
$get = $url_parts['path'] . '?' . $url_parts['query']; |
362
|
|
|
} |
363
|
|
|
else { |
364
|
|
|
$get = $url_parts['path']; |
365
|
|
|
} |
366
|
|
|
} |
367
|
|
|
else { |
368
|
|
|
$get = '/'; |
369
|
|
|
} |
370
|
|
|
return $get; |
371
|
|
|
} |
372
|
|
|
|
373
|
|
|
/** |
374
|
|
|
* Error handler for stream_socket_client() |
375
|
|
|
* |
376
|
|
|
* @param int $errno Error number (e.g. E_WARNING) |
377
|
|
|
* @param string $errstr Error message |
378
|
|
|
*/ |
379
|
|
|
public function connect_error_handler($errno, $errstr) { |
380
|
|
|
// Double-check we can handle it |
381
|
|
|
if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) { |
382
|
|
|
// Return false to indicate the default error handler should engage |
383
|
|
|
return false; |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
$this->connect_error .= $errstr . "\n"; |
387
|
|
|
return true; |
388
|
|
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* Verify the certificate against common name and subject alternative names |
392
|
|
|
* |
393
|
|
|
* Unfortunately, PHP doesn't check the certificate against the alternative |
394
|
|
|
* names, leading things like 'https://www.github.com/' to be invalid. |
395
|
|
|
* Instead |
396
|
|
|
* |
397
|
|
|
* @see https://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1 |
398
|
|
|
* |
399
|
|
|
* @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`) |
400
|
|
|
* @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`) |
401
|
|
|
* @param string $host Host name to verify against |
402
|
|
|
* @param resource $context Stream context |
403
|
|
|
* @return bool |
404
|
|
|
*/ |
405
|
|
|
public function verify_certificate_from_context($host, $context) { |
406
|
|
|
$meta = stream_context_get_options($context); |
407
|
|
|
|
408
|
|
|
// If we don't have SSL options, then we couldn't make the connection at |
409
|
|
|
// all |
410
|
|
|
if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) { |
411
|
|
|
throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error'); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
$cert = openssl_x509_parse($meta['ssl']['peer_certificate']); |
415
|
|
|
|
416
|
|
|
return Requests_SSL::verify_certificate($host, $cert); |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Whether this transport is valid |
421
|
|
|
* |
422
|
|
|
* @codeCoverageIgnore |
423
|
|
|
* @return boolean True if the transport is valid, false otherwise. |
424
|
|
|
*/ |
425
|
|
|
public static function test($capabilities = array()) { |
426
|
|
|
if (!function_exists('fsockopen')) { |
427
|
|
|
return false; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
// If needed, check that streams support SSL |
431
|
|
|
if (isset($capabilities['ssl']) && $capabilities['ssl']) { |
432
|
|
|
if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse')) { |
433
|
|
|
return false; |
434
|
|
|
} |
435
|
|
|
|
436
|
|
|
// Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156 |
437
|
|
|
if (defined('HHVM_VERSION')) { |
438
|
|
|
return false; |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return true; |
443
|
|
|
} |
444
|
|
|
} |
445
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.