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