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