1 <?php
  2 /**
  3  * @package     Joomla.Platform
  4  * @subpackage  HTTP
  5  *
  6  * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
  7  * @license     GNU General Public License version 2 or later; see LICENSE
  8  */
  9 
 10 defined('JPATH_PLATFORM') or die;
 11 
 12 use Joomla\Registry\Registry;
 13 
 14 /**
 15  * HTTP transport class for using sockets directly.
 16  *
 17  * @since  11.3
 18  */
 19 class JHttpTransportSocket implements JHttpTransport
 20 {
 21     /**
 22      * @var    array  Reusable socket connections.
 23      * @since  11.3
 24      */
 25     protected $connections;
 26 
 27     /**
 28      * @var    Registry  The client options.
 29      * @since  11.3
 30      */
 31     protected $options;
 32 
 33     /**
 34      * Constructor.
 35      *
 36      * @param   Registry  $options  Client options object.
 37      *
 38      * @since   11.3
 39      * @throws  RuntimeException
 40      */
 41     public function __construct(Registry $options)
 42     {
 43         if (!self::isSupported())
 44         {
 45             throw new RuntimeException('Cannot use a socket transport when fsockopen() is not available.');
 46         }
 47 
 48         $this->options = $options;
 49     }
 50 
 51     /**
 52      * Send a request to the server and return a JHttpResponse object with the response.
 53      *
 54      * @param   string   $method     The HTTP method for sending the request.
 55      * @param   JUri     $uri        The URI to the resource to request.
 56      * @param   mixed    $data       Either an associative array or a string to be sent with the request.
 57      * @param   array    $headers    An array of request headers to send with the request.
 58      * @param   integer  $timeout    Read timeout in seconds.
 59      * @param   string   $userAgent  The optional user agent string to send with the request.
 60      *
 61      * @return  JHttpResponse
 62      *
 63      * @since   11.3
 64      * @throws  RuntimeException
 65      */
 66     public function request($method, JUri $uri, $data = null, array $headers = null, $timeout = null, $userAgent = null)
 67     {
 68         $connection = $this->connect($uri, $timeout);
 69 
 70         // Make sure the connection is alive and valid.
 71         if (is_resource($connection))
 72         {
 73             // Make sure the connection has not timed out.
 74             $meta = stream_get_meta_data($connection);
 75 
 76             if ($meta['timed_out'])
 77             {
 78                 throw new RuntimeException('Server connection timed out.');
 79             }
 80         }
 81         else
 82         {
 83             throw new RuntimeException('Not connected to server.');
 84         }
 85 
 86         // Get the request path from the URI object.
 87         $path = $uri->toString(array('path', 'query'));
 88 
 89         // If we have data to send make sure our request is setup for it.
 90         if (!empty($data))
 91         {
 92             // If the data is not a scalar value encode it to be sent with the request.
 93             if (!is_scalar($data))
 94             {
 95                 $data = http_build_query($data);
 96             }
 97 
 98             if (!isset($headers['Content-Type']))
 99             {
100                 $headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8';
101             }
102 
103             // Add the relevant headers.
104             $headers['Content-Length'] = strlen($data);
105         }
106 
107         // Build the request payload.
108         $request = array();
109         $request[] = strtoupper($method) . ' ' . ((empty($path)) ? '/' : $path) . ' HTTP/1.0';
110         $request[] = 'Host: ' . $uri->getHost();
111 
112         // If an explicit user agent is given use it.
113         if (isset($userAgent))
114         {
115             $headers['User-Agent'] = $userAgent;
116         }
117 
118         // If there are custom headers to send add them to the request payload.
119         if (is_array($headers))
120         {
121             foreach ($headers as $k => $v)
122             {
123                 $request[] = $k . ': ' . $v;
124             }
125         }
126 
127         // Set any custom transport options
128         foreach ($this->options->get('transport.socket', array()) as $value)
129         {
130             $request[] = $value;
131         }
132 
133         // If we have data to send add it to the request payload.
134         if (!empty($data))
135         {
136             $request[] = null;
137             $request[] = $data;
138         }
139 
140         // Authentification, if needed
141         if ($this->options->get('userauth') && $this->options->get('passwordauth'))
142         {
143             $request[] = 'Authorization: Basic ' . base64_encode($this->options->get('userauth') . ':' . $this->options->get('passwordauth'));
144         }
145 
146         // Send the request to the server.
147         fwrite($connection, implode("\r\n", $request) . "\r\n\r\n");
148 
149         // Get the response data from the server.
150         $content = '';
151 
152         while (!feof($connection))
153         {
154             $content .= fgets($connection, 4096);
155         }
156 
157         $content = $this->getResponse($content);
158 
159         // Follow Http redirects
160         if ($content->code >= 301 && $content->code < 400 && isset($content->headers['Location']))
161         {
162             return $this->request($method, new JUri($content->headers['Location']), $data, $headers, $timeout, $userAgent);
163         }
164 
165         return $content;
166     }
167 
168     /**
169      * Method to get a response object from a server response.
170      *
171      * @param   string  $content  The complete server response, including headers.
172      *
173      * @return  JHttpResponse
174      *
175      * @since   11.3
176      * @throws  UnexpectedValueException
177      */
178     protected function getResponse($content)
179     {
180         // Create the response object.
181         $return = new JHttpResponse;
182 
183         if (empty($content))
184         {
185             throw new UnexpectedValueException('No content in response.');
186         }
187 
188         // Split the response into headers and body.
189         $response = explode("\r\n\r\n", $content, 2);
190 
191         // Get the response headers as an array.
192         $headers = explode("\r\n", $response[0]);
193 
194         // Set the body for the response.
195         $return->body = empty($response[1]) ? '' : $response[1];
196 
197         // Get the response code from the first offset of the response headers.
198         preg_match('/[0-9]{3}/', array_shift($headers), $matches);
199         $code = $matches[0];
200 
201         if (is_numeric($code))
202         {
203             $return->code = (int) $code;
204         }
205 
206         // No valid response code was detected.
207         else
208         {
209             throw new UnexpectedValueException('No HTTP response code found.');
210         }
211 
212         // Add the response headers to the response object.
213         foreach ($headers as $header)
214         {
215             $pos = strpos($header, ':');
216             $return->headers[trim(substr($header, 0, $pos))] = trim(substr($header, ($pos + 1)));
217         }
218 
219         return $return;
220     }
221 
222     /**
223      * Method to connect to a server and get the resource.
224      *
225      * @param   JUri     $uri      The URI to connect with.
226      * @param   integer  $timeout  Read timeout in seconds.
227      *
228      * @return  resource  Socket connection resource.
229      *
230      * @since   11.3
231      * @throws  RuntimeException
232      */
233     protected function connect(JUri $uri, $timeout = null)
234     {
235         $errno = null;
236         $err = null;
237 
238         // Get the host from the uri.
239         $host = ($uri->isSsl()) ? 'ssl://' . $uri->getHost() : $uri->getHost();
240 
241         // If the port is not explicitly set in the URI detect it.
242         if (!$uri->getPort())
243         {
244             $port = ($uri->getScheme() == 'https') ? 443 : 80;
245         }
246 
247         // Use the set port.
248         else
249         {
250             $port = $uri->getPort();
251         }
252 
253         // Build the connection key for resource memory caching.
254         $key = md5($host . $port);
255 
256         // If the connection already exists, use it.
257         if (!empty($this->connections[$key]) && is_resource($this->connections[$key]))
258         {
259             // Connection reached EOF, cannot be used anymore
260             $meta = stream_get_meta_data($this->connections[$key]);
261 
262             if ($meta['eof'])
263             {
264                 if (!fclose($this->connections[$key]))
265                 {
266                     throw new RuntimeException('Cannot close connection');
267                 }
268             }
269 
270             // Make sure the connection has not timed out.
271             elseif (!$meta['timed_out'])
272             {
273                 return $this->connections[$key];
274             }
275         }
276 
277         if (!is_numeric($timeout))
278         {
279             $timeout = ini_get('default_socket_timeout');
280         }
281 
282         // Capture PHP errors
283         $php_errormsg = '';
284         $track_errors = ini_get('track_errors');
285         ini_set('track_errors', true);
286 
287         // PHP sends a warning if the uri does not exists; we silence it and throw an exception instead.
288         // Attempt to connect to the server
289         $connection = @fsockopen($host, $port, $errno, $err, $timeout);
290 
291         if (!$connection)
292         {
293             if (!$php_errormsg)
294             {
295                 // Error but nothing from php? Create our own
296                 $php_errormsg = sprintf('Could not connect to resource: %s', $uri, $err, $errno);
297             }
298 
299             // Restore error tracking to give control to the exception handler
300             ini_set('track_errors', $track_errors);
301 
302             throw new RuntimeException($php_errormsg);
303         }
304 
305         // Restore error tracking to what it was before.
306         ini_set('track_errors', $track_errors);
307 
308         // Since the connection was successful let's store it in case we need to use it later.
309         $this->connections[$key] = $connection;
310 
311         // If an explicit timeout is set, set it.
312         if (isset($timeout))
313         {
314             stream_set_timeout($this->connections[$key], (int) $timeout);
315         }
316 
317         return $this->connections[$key];
318     }
319 
320     /**
321      * Method to check if http transport socket available for use
322      *
323      * @return  boolean   True if available else false
324      *
325      * @since   12.1
326      */
327     public static function isSupported()
328     {
329         return function_exists('fsockopen') && is_callable('fsockopen') && !JFactory::getConfig()->get('proxy_enable');
330     }
331 }
332