9c5d536bb9d81275eaf00458dc7b79b7b8af5352
[tine20] / tine20 / library / Zend / Http / Client / Adapter / Socket.php
1 <?php
2
3 /**
4  * Zend Framework
5  *
6  * LICENSE
7  *
8  * This source file is subject to the new BSD license that is bundled
9  * with this package in the file LICENSE.txt.
10  * It is also available through the world-wide-web at this URL:
11  * http://framework.zend.com/license/new-bsd
12  * If you did not receive a copy of the license and are unable to
13  * obtain it through the world-wide-web, please send an email
14  * to license@zend.com so we can send you a copy immediately.
15  *
16  * @category   Zend
17  * @package    Zend_Http
18  * @subpackage Client_Adapter
19  * @version    $Id: Socket.php 10020 2009-08-18 14:34:09Z j.fischer@metaways.de $
20  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
21  * @license    http://framework.zend.com/license/new-bsd     New BSD License
22  */
23
24 /**
25  * @see Zend_Uri_Http
26  */
27 require_once 'Zend/Uri/Http.php';
28 /**
29  * @see Zend_Http_Client_Adapter_Interface
30  */
31 require_once 'Zend/Http/Client/Adapter/Interface.php';
32
33 /**
34  * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used
35  * on almost every PHP environment, and does not require any special extensions.
36  *
37  * @category   Zend
38  * @package    Zend_Http
39  * @subpackage Client_Adapter
40  * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
41  * @license    http://framework.zend.com/license/new-bsd     New BSD License
42  */
43 class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface
44 {
45     /**
46      * The socket for server connection
47      *
48      * @var resource|null
49      */
50     protected $socket = null;
51
52     /**
53      * What host/port are we connected to?
54      *
55      * @var array
56      */
57     protected $connected_to = array(null, null);
58
59     /**
60      * Parameters array
61      *
62      * @var array
63      */
64     protected $config = array(
65         'persistent'    => false,
66         'ssltransport'  => 'ssl',
67         'sslcert'       => null,
68         'sslpassphrase' => null
69     );
70
71     /**
72      * Request method - will be set by write() and might be used by read()
73      *
74      * @var string
75      */
76     protected $method = null;
77
78     /**
79      * Stream context
80      *
81      * @var resource
82      */
83     protected $_context = null;
84
85     /**
86      * Adapter constructor, currently empty. Config is set using setConfig()
87      *
88      */
89     public function __construct()
90     {
91     }
92
93     /**
94      * Set the configuration array for the adapter
95      *
96      * @param Zend_Config | array $config
97      */
98     public function setConfig($config = array())
99     {
100         if ($config instanceof Zend_Config) {
101             $config = $config->toArray();
102
103         } elseif (! is_array($config)) {
104             require_once 'Zend/Http/Client/Adapter/Exception.php';
105             throw new Zend_Http_Client_Adapter_Exception(
106                 'Array or Zend_Config object expected, got ' . gettype($config)
107             );
108         }
109
110         foreach ($config as $k => $v) {
111             $this->config[strtolower($k)] = $v;
112         }
113     }
114
115     /**
116      * Set the stream context for the TCP connection to the server
117      *
118      * Can accept either a pre-existing stream context resource, or an array
119      * of stream options, similar to the options array passed to the
120      * stream_context_create() PHP function. In such case a new stream context
121      * will be created using the passed options.
122      *
123      * @since  Zend Framework 1.9
124      *
125      * @param  mixed $context Stream context or array of context options
126      * @return Zend_Http_Client_Adapter_Socket
127      */
128     public function setStreamContext($context)
129     {
130         if (is_resource($context) && get_resource_type($context) == 'stream-context') {
131             $this->_context = $context;
132
133         } elseif (is_array($context)) {
134             $this->_context = stream_context_create($context);
135
136         } else {
137             // Invalid parameter
138             require_once 'Zend/Http/Client/Adapter/Exception.php';
139             throw new Zend_Http_Client_Adapter_Exception(
140                 "Expecting either a stream context resource or array, got " . gettype($context)
141             );
142         }
143
144         return $this;
145     }
146
147     /**
148      * Get the stream context for the TCP connection to the server.
149      *
150      * If no stream context is set, will create a default one.
151      *
152      * @return resource
153      */
154     public function getStreamContext()
155     {
156         if (! $this->_context) {
157             $this->_context = stream_context_create();
158         }
159
160         return $this->_context;
161     }
162
163     /**
164      * Connect to the remote server
165      *
166      * @param string  $host
167      * @param int     $port
168      * @param boolean $secure
169      */
170     public function connect($host, $port = 80, $secure = false)
171     {
172         // If the URI should be accessed via SSL, prepend the Hostname with ssl://
173         $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
174
175         // If we are connected to the wrong host, disconnect first
176         if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) {
177             if (is_resource($this->socket)) $this->close();
178         }
179
180         // Now, if we are not connected, connect
181         if (! is_resource($this->socket) || ! $this->config['keepalive']) {
182             $context = $this->getStreamContext();
183             if ($secure) {
184                 if ($this->config['sslcert'] !== null) {
185                     if (! stream_context_set_option($context, 'ssl', 'local_cert',
186                                                     $this->config['sslcert'])) {
187                         require_once 'Zend/Http/Client/Adapter/Exception.php';
188                         throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option');
189                     }
190                 }
191                 if ($this->config['sslpassphrase'] !== null) {
192                     if (! stream_context_set_option($context, 'ssl', 'passphrase',
193                                                     $this->config['sslpassphrase'])) {
194                         require_once 'Zend/Http/Client/Adapter/Exception.php';
195                         throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option');
196                     }
197                 }
198             }
199
200             $flags = STREAM_CLIENT_CONNECT;
201             if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT;
202
203             $this->socket = @stream_socket_client($host . ':' . $port,
204                                                   $errno,
205                                                   $errstr,
206                                                   (int) $this->config['timeout'],
207                                                   $flags,
208                                                   $context);
209
210             if (! $this->socket) {
211                 $this->close();
212                 require_once 'Zend/Http/Client/Adapter/Exception.php';
213                 throw new Zend_Http_Client_Adapter_Exception(
214                     'Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr);
215             }
216
217             // Set the stream timeout
218             if (! stream_set_timeout($this->socket, (int) $this->config['timeout'])) {
219                 require_once 'Zend/Http/Client/Adapter/Exception.php';
220                 throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout');
221             }
222
223             // Update connected_to
224             $this->connected_to = array($host, $port);
225         }
226     }
227
228     /**
229      * Send request to the remote server
230      *
231      * @param string        $method
232      * @param Zend_Uri_Http $uri
233      * @param string        $http_ver
234      * @param array         $headers
235      * @param string        $body
236      * @return string Request as string
237      */
238     public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '')
239     {
240         // Make sure we're properly connected
241         if (! $this->socket) {
242             require_once 'Zend/Http/Client/Adapter/Exception.php';
243             throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected');
244         }
245
246         $host = $uri->getHost();
247         $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
248         if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) {
249             require_once 'Zend/Http/Client/Adapter/Exception.php';
250             throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host');
251         }
252
253         // Save request method for later
254         $this->method = $method;
255
256         // Build request headers
257         $path = $uri->getPath();
258         if ($uri->getQuery()) $path .= '?' . $uri->getQuery();
259         $request = "{$method} {$path} HTTP/{$http_ver}\r\n";
260         foreach ($headers as $k => $v) {
261             if (is_string($k)) $v = ucfirst($k) . ": $v";
262             $request .= "$v\r\n";
263         }
264
265         // Add the request body
266         $request .= "\r\n" . $body;
267
268         // Send the request
269         if (! @fwrite($this->socket, $request)) {
270             require_once 'Zend/Http/Client/Adapter/Exception.php';
271             throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
272         }
273
274         return $request;
275     }
276
277     /**
278      * Read response from server
279      *
280      * @return string
281      */
282     public function read()
283     {
284         // First, read headers only
285         $response = '';
286         $gotStatus = false;
287
288         while (($line = @fgets($this->socket)) !== false) {
289             $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
290             if ($gotStatus) {
291                 $response .= $line;
292                 if (rtrim($line) === '') break;
293             }
294         }
295
296         $this->_checkSocketReadTimeout();
297
298         $statusCode = Zend_Http_Response::extractCode($response);
299
300         // Handle 100 and 101 responses internally by restarting the read again
301         if ($statusCode == 100 || $statusCode == 101) return $this->read();
302
303         // Check headers to see what kind of connection / transfer encoding we have
304         $headers = Zend_Http_Response::extractHeaders($response);
305
306         /**
307          * Responses to HEAD requests and 204 or 304 responses are not expected
308          * to have a body - stop reading here
309          */
310         if ($statusCode == 304 || $statusCode == 204 ||
311             $this->method == Zend_Http_Client::HEAD) {
312
313             // Close the connection if requested to do so by the server
314             if (isset($headers['connection']) && $headers['connection'] == 'close') {
315                 $this->close();
316             }
317             return $response;
318         }
319
320         // If we got a 'transfer-encoding: chunked' header
321         if (isset($headers['transfer-encoding'])) {
322
323             if (strtolower($headers['transfer-encoding']) == 'chunked') {
324
325                 do {
326                     $line  = @fgets($this->socket);
327                     $this->_checkSocketReadTimeout();
328
329                     $chunk = $line;
330
331                     // Figure out the next chunk size
332                     $chunksize = trim($line);
333                     if (! ctype_xdigit($chunksize)) {
334                         $this->close();
335                         require_once 'Zend/Http/Client/Adapter/Exception.php';
336                         throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' .
337                             $chunksize . '" unable to read chunked body');
338                     }
339
340                     // Convert the hexadecimal value to plain integer
341                     $chunksize = hexdec($chunksize);
342
343                     // Read next chunk
344                     $read_to = ftell($this->socket) + $chunksize;
345
346                     do {
347                         $current_pos = ftell($this->socket);
348                         if ($current_pos >= $read_to) break;
349
350                         $line = @fread($this->socket, $read_to - $current_pos);
351                         if ($line === false || strlen($line) === 0) {
352                             $this->_checkSocketReadTimeout();
353                             break;
354                         } else {
355                             $chunk .= $line;
356                         }
357
358                     } while (! feof($this->socket));
359
360                     $chunk .= @fgets($this->socket);
361                     $this->_checkSocketReadTimeout();
362
363                     $response .= $chunk;
364                 } while ($chunksize > 0);
365
366             } else {
367                 $this->close();
368                 throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' .
369                     $headers['transfer-encoding'] . '" transfer encoding');
370             }
371
372         // Else, if we got the content-length header, read this number of bytes
373         } elseif (isset($headers['content-length'])) {
374
375             $current_pos = ftell($this->socket);
376             $chunk = '';
377
378             for ($read_to = $current_pos + $headers['content-length'];
379                  $read_to > $current_pos;
380                  $current_pos = ftell($this->socket)) {
381
382                 $chunk = @fread($this->socket, $read_to - $current_pos);
383                 if ($chunk === false || strlen($chunk) === 0) {
384                     $this->_checkSocketReadTimeout();
385                     break;
386                 }
387
388                 $response .= $chunk;
389
390                 // Break if the connection ended prematurely
391                 if (feof($this->socket)) break;
392             }
393
394         // Fallback: just read the response until EOF
395         } else {
396
397             do {
398                 $buff = @fread($this->socket, 8192);
399                 if ($buff === false || strlen($buff) === 0) {
400                     $this->_checkSocketReadTimeout();
401                     break;
402                 } else {
403                     $response .= $buff;
404                 }
405
406             } while (feof($this->socket) === false);
407
408             $this->close();
409         }
410
411         // Close the connection if requested to do so by the server
412         if (isset($headers['connection']) && $headers['connection'] == 'close') {
413             $this->close();
414         }
415
416         return $response;
417     }
418
419     /**
420      * Close the connection to the server
421      *
422      */
423     public function close()
424     {
425         if (is_resource($this->socket)) @fclose($this->socket);
426         $this->socket = null;
427         $this->connected_to = array(null, null);
428     }
429
430     /**
431      * Check if the socket has timed out - if so close connection and throw
432      * an exception
433      *
434      * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code
435      */
436     protected function _checkSocketReadTimeout()
437     {
438         if ($this->socket) {
439             $info = stream_get_meta_data($this->socket);
440             $timedout = $info['timed_out'];
441             if ($timedout) {
442                 $this->close();
443                 require_once 'Zend/Http/Client/Adapter/Exception.php';
444                 throw new Zend_Http_Client_Adapter_Exception(
445                     "Read timed out after {$this->config['timeout']} seconds",
446                     Zend_Http_Client_Adapter_Exception::READ_TIMEOUT
447                 );
448             }
449         }
450     }
451
452     /**
453      * Destructor: make sure the socket is disconnected
454      *
455      * If we are in persistent TCP mode, will not close the connection
456      *
457      */
458     public function __destruct()
459     {
460         if (! $this->config['persistent']) {
461             if ($this->socket) $this->close();
462         }
463     }
464 }