]> CyberLeo.Net >> Repos - Github/YOURLS.git/blob - includes/Requests/Requests/Transport/fsockopen.php
Merge branch 'master' of https://github.com/YOURLS/YOURLS
[Github/YOURLS.git] / includes / Requests / Requests / Transport / fsockopen.php
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 http://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                 $host = $url_parts['host'];
63                 $context = stream_context_create();
64                 $verifyname = false;
65
66                 // HTTPS support
67                 if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
68                         $remote_socket = 'ssl://' . $host;
69                         $url_parts['port'] = 443;
70
71                         $context_options = array(
72                                 'verify_peer' => true,
73                                 // 'CN_match' => $host,
74                                 'capture_peer_cert' => true
75                         );
76                         $verifyname = true;
77
78                         // SNI, if enabled (OpenSSL >=0.9.8j)
79                         if (defined('OPENSSL_TLSEXT_SERVER_NAME') && OPENSSL_TLSEXT_SERVER_NAME) {
80                                 $context_options['SNI_enabled'] = true;
81                                 if (isset($options['verifyname']) && $options['verifyname'] === false) {
82                                         $context_options['SNI_enabled'] = false;
83                                 }
84                         }
85
86                         if (isset($options['verify'])) {
87                                 if ($options['verify'] === false) {
88                                         $context_options['verify_peer'] = false;
89                                 } elseif (is_string($options['verify'])) {
90                                         $context_options['cafile'] = $options['verify'];
91                                 }
92                         }
93
94                         if (isset($options['verifyname']) && $options['verifyname'] === false) {
95                                 $verifyname = false;
96                         }
97
98                         stream_context_set_option($context, array('ssl' => $context_options));
99                 }
100                 else {
101                         $remote_socket = 'tcp://' . $host;
102                 }
103
104                 $this->max_bytes = $options['max_bytes'];
105
106                 $proxy = isset( $options['proxy'] );
107                 $proxy_auth = $proxy && isset( $options['proxy_username'] ) && isset( $options['proxy_password'] );
108
109                 if (!isset($url_parts['port'])) {
110                         $url_parts['port'] = 80;
111                 }
112                 $remote_socket .= ':' . $url_parts['port'];
113
114                 set_error_handler(array($this, 'connect_error_handler'), E_WARNING | E_NOTICE);
115
116                 $options['hooks']->dispatch('fsockopen.remote_socket', array(&$remote_socket));
117
118                 $fp = stream_socket_client($remote_socket, $errno, $errstr, ceil($options['connect_timeout']), STREAM_CLIENT_CONNECT, $context);
119
120                 restore_error_handler();
121
122                 if ($verifyname) {
123                         if (!$this->verify_certificate_from_context($host, $context)) {
124                                 throw new Requests_Exception('SSL certificate did not match the requested domain name', 'ssl.no_match');
125                         }
126                 }
127
128                 if (!$fp) {
129                         if ($errno === 0) {
130                                 // Connection issue
131                                 throw new Requests_Exception(rtrim($this->connect_error), 'fsockopen.connect_error');
132                         }
133                         else {
134                                 throw new Requests_Exception($errstr, 'fsockopenerror');
135                                 return;
136                         }
137                 }
138
139                 $request_body = '';
140                 $out = '';
141                 switch ($options['type']) {
142                         case Requests::POST:
143                         case Requests::PUT:
144                         case Requests::PATCH:
145                                 if (isset($url_parts['path'])) {
146                                         $path = $url_parts['path'];
147                                         if (isset($url_parts['query'])) {
148                                                 $path .= '?' . $url_parts['query'];
149                                         }
150                                 }
151                                 else {
152                                         $path = '/';
153                                 }
154
155                                 $options['hooks']->dispatch( 'fsockopen.remote_host_path', array( &$path, $url ) );
156                                 $out = $options['type'] . " $path HTTP/1.0\r\n";
157
158                                 if (is_array($data)) {
159                                         $request_body = http_build_query($data, null, '&');
160                                 }
161                                 else {
162                                         $request_body = $data;
163                                 }
164                                 if (empty($headers['Content-Length'])) {
165                                         $headers['Content-Length'] = strlen($request_body);
166                                 }
167                                 if (empty($headers['Content-Type'])) {
168                                         $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
169                                 }
170                                 break;
171                         case Requests::HEAD:
172                         case Requests::GET:
173                         case Requests::DELETE:
174                                 $path = self::format_get($url_parts, $data);
175                                 $options['hooks']->dispatch('fsockopen.remote_host_path', array(&$path, $url));
176                                 $out = $options['type'] . " $path HTTP/1.0\r\n";
177                                 break;
178                 }
179                 $out .= "Host: {$url_parts['host']}";
180
181                 if ($url_parts['port'] !== 80) {
182                         $out .= ":{$url_parts['port']}";
183                 }
184                 $out .= "\r\n";
185
186                 $out .= "User-Agent: {$options['useragent']}\r\n";
187                 $accept_encoding = $this->accept_encoding();
188                 if (!empty($accept_encoding)) {
189                         $out .= "Accept-Encoding: $accept_encoding\r\n";
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                 $out .= "Connection: Close\r\n\r\n" . $request_body;
205
206                 $options['hooks']->dispatch('fsockopen.before_send', array(&$out));
207
208                 fwrite($fp, $out);
209                 $options['hooks']->dispatch('fsockopen.after_send', array(&$fake_headers));
210
211                 if (!$options['blocking']) {
212                         fclose($fp);
213                         $fake_headers = '';
214                         $options['hooks']->dispatch('fsockopen.after_request', array(&$fake_headers));
215                         return '';
216                 }
217
218                 $timeout_sec = (int) floor($options['timeout']);
219                 $timeout_msec = $timeout_sec == $options['timeout'] ? 0 : self::SECOND_IN_MICROSECONDS * $options['timeout'] % self::SECOND_IN_MICROSECONDS;
220                 stream_set_timeout($fp, $timeout_sec, $timeout_msec);
221
222                 $this->info = stream_get_meta_data($fp);
223
224                 $response = $body = $headers = '';
225                 $this->info = stream_get_meta_data($fp);
226                 $size = 0;
227                 $doingbody = false;
228                 $download = false;
229                 if ($options['filename']) {
230                         $download = fopen($options['filename'], 'wb');
231                 }
232
233                 while (!feof($fp)) {
234                         $this->info = stream_get_meta_data($fp);
235                         if ($this->info['timed_out']) {
236                                 throw new Requests_Exception('fsocket timed out', 'timeout');
237                         }
238
239                         $block = fread($fp, Requests::BUFFER_SIZE);
240                         if (!$doingbody) {
241                                 $response .= $block;
242                                 if (strpos($response, "\r\n\r\n")) {
243                                         list($headers, $block) = explode("\r\n\r\n", $response, 2);
244                                         $doingbody = true;
245                                 }
246                         }
247
248                         // Are we in body mode now?
249                         if ($doingbody) {
250                                 $data_length = strlen($block);
251                                 if ($this->max_bytes) {
252                                         // Have we already hit a limit?
253                                         if ($size === $this->max_bytes) {
254                                                 continue;
255                                         }
256                                         if (($size + $data_length) > $this->max_bytes) {
257                                                 // Limit the length
258                                                 $limited_length = ($this->max_bytes - $size);
259                                                 $block = substr($block, 0, $limited_length);
260                                         }
261                                 }
262
263                                 $size += strlen($block);
264                                 if ($download) {
265                                         fwrite($download, $block);
266                                 }
267                                 else {
268                                         $body .= $block;
269                                 }
270                         }
271                 }
272                 $this->headers = $headers;
273
274                 if ($download) {
275                         fclose($download);
276                 }
277                 else {
278                         $this->headers .= "\r\n\r\n" . $body;
279                 }
280                 fclose($fp);
281
282                 $options['hooks']->dispatch('fsockopen.after_request', array(&$this->headers));
283                 return $this->headers;
284         }
285
286         /**
287          * Send multiple requests simultaneously
288          *
289          * @param array $requests Request data (array of 'url', 'headers', 'data', 'options') as per {@see Requests_Transport::request}
290          * @param array $options Global options, see {@see Requests::response()} for documentation
291          * @return array Array of Requests_Response objects (may contain Requests_Exception or string responses as well)
292          */
293         public function request_multiple($requests, $options) {
294                 $responses = array();
295                 $class = get_class($this);
296                 foreach ($requests as $id => $request) {
297                         try {
298                                 $handler = new $class();
299                                 $responses[$id] = $handler->request($request['url'], $request['headers'], $request['data'], $request['options']);
300
301                                 $request['options']['hooks']->dispatch('transport.internal.parse_response', array(&$responses[$id], $request));
302                         }
303                         catch (Requests_Exception $e) {
304                                 $responses[$id] = $e;
305                         }
306
307                         if (!is_string($responses[$id])) {
308                                 $request['options']['hooks']->dispatch('multiple.request.complete', array(&$responses[$id], $id));
309                         }
310                 }
311
312                 return $responses;
313         }
314
315         /**
316          * Retrieve the encodings we can accept
317          *
318          * @return string Accept-Encoding header value
319          */
320         protected static function accept_encoding() {
321                 $type = array();
322                 if (function_exists('gzinflate')) {
323                         $type[] = 'deflate;q=1.0';
324                 }
325
326                 if (function_exists('gzuncompress')) {
327                         $type[] = 'compress;q=0.5';
328                 }
329
330                 $type[] = 'gzip;q=0.5';
331
332                 return implode(', ', $type);
333         }
334
335         /**
336          * Format a URL given GET data
337          *
338          * @param array $url_parts
339          * @param array|object $data Data to build query using, see {@see http://php.net/http_build_query}
340          * @return string URL with data
341          */
342         protected static function format_get($url_parts, $data) {
343                 if (!empty($data)) {
344                         if (empty($url_parts['query']))
345                                 $url_parts['query'] = '';
346
347                         $url_parts['query'] .= '&' . http_build_query($data, null, '&');
348                         $url_parts['query'] = trim($url_parts['query'], '&');
349                 }
350                 if (isset($url_parts['path'])) {
351                         if (isset($url_parts['query'])) {
352                                 $get = $url_parts['path'] . '?' . $url_parts['query'];
353                         }
354                         else {
355                                 $get = $url_parts['path'];
356                         }
357                 }
358                 else {
359                         $get = '/';
360                 }
361                 return $get;
362         }
363
364         /**
365          * Error handler for stream_socket_client()
366          *
367          * @param int $errno Error number (e.g. E_WARNING)
368          * @param string $errstr Error message
369          */
370         public function connect_error_handler($errno, $errstr) {
371                 // Double-check we can handle it
372                 if (($errno & E_WARNING) === 0 && ($errno & E_NOTICE) === 0) {
373                         // Return false to indicate the default error handler should engage
374                         return false;
375                 }
376
377                 $this->connect_error .= $errstr . "\n";
378                 return true;
379         }
380
381         /**
382          * Verify the certificate against common name and subject alternative names
383          *
384          * Unfortunately, PHP doesn't check the certificate against the alternative
385          * names, leading things like 'https://www.github.com/' to be invalid.
386          * Instead
387          *
388          * @see http://tools.ietf.org/html/rfc2818#section-3.1 RFC2818, Section 3.1
389          *
390          * @throws Requests_Exception On failure to connect via TLS (`fsockopen.ssl.connect_error`)
391          * @throws Requests_Exception On not obtaining a match for the host (`fsockopen.ssl.no_match`)
392          * @param string $host Host name to verify against
393          * @param resource $context Stream context
394          * @return bool
395          */
396         public function verify_certificate_from_context($host, $context) {
397                 $meta = stream_context_get_options($context);
398
399                 // If we don't have SSL options, then we couldn't make the connection at
400                 // all
401                 if (empty($meta) || empty($meta['ssl']) || empty($meta['ssl']['peer_certificate'])) {
402                         throw new Requests_Exception(rtrim($this->connect_error), 'ssl.connect_error');
403                 }
404
405                 $cert = openssl_x509_parse($meta['ssl']['peer_certificate']);
406
407                 return Requests_SSL::verify_certificate($host, $cert);
408         }
409
410         /**
411          * Whether this transport is valid
412          *
413          * @codeCoverageIgnore
414          * @return boolean True if the transport is valid, false otherwise.
415          */
416         public static function test($capabilities = array()) {
417                 if (!function_exists('fsockopen'))
418                         return false;
419
420                 // If needed, check that streams support SSL
421                 if (isset( $capabilities['ssl'] ) && $capabilities['ssl']) {
422                         if (!extension_loaded('openssl') || !function_exists('openssl_x509_parse'))
423                                 return false;
424
425                         // Currently broken, thanks to https://github.com/facebook/hhvm/issues/2156
426                         if (defined('HHVM_VERSION'))
427                                 return false;
428                 }
429
430                 return true;
431         }
432 }