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.
18 * @subpackage Client_Adapter
20 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
21 * @license http://framework.zend.com/license/new-bsd New BSD License
27 require_once 'Zend/Uri/Http.php';
29 * @see Zend_Http_Client_Adapter_Interface
31 require_once 'Zend/Http/Client/Adapter/Interface.php';
33 * @see Zend_Http_Client_Adapter_Stream
35 require_once 'Zend/Http/Client/Adapter/Stream.php';
38 * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used
39 * on almost every PHP environment, and does not require any special extensions.
43 * @subpackage Client_Adapter
44 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
45 * @license http://framework.zend.com/license/new-bsd New BSD License
47 class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream
50 * The socket for server connection
54 protected $socket = null;
57 * What host/port are we connected to?
61 protected $connected_to = array(null, null);
64 * Stream for storing output
68 protected $out_stream = null;
75 protected $config = array(
76 'persistent' => false,
77 'ssltransport' => 'ssl',
79 'sslpassphrase' => null,
80 'sslusecontext' => false
84 * Request method - will be set by write() and might be used by read()
88 protected $method = null;
95 protected $_context = null;
98 * Adapter constructor, currently empty. Config is set using setConfig()
101 public function __construct()
106 * Set the configuration array for the adapter
108 * @param Zend_Config | array $config
110 public function setConfig($config = array())
112 if ($config instanceof Zend_Config) {
113 $config = $config->toArray();
115 } elseif (! is_array($config)) {
116 require_once 'Zend/Http/Client/Adapter/Exception.php';
117 throw new Zend_Http_Client_Adapter_Exception(
118 'Array or Zend_Config object expected, got ' . gettype($config)
122 foreach ($config as $k => $v) {
123 $this->config[strtolower($k)] = $v;
128 * Retrieve the array of all configuration options
132 public function getConfig()
134 return $this->config;
138 * Set the stream context for the TCP connection to the server
140 * Can accept either a pre-existing stream context resource, or an array
141 * of stream options, similar to the options array passed to the
142 * stream_context_create() PHP function. In such case a new stream context
143 * will be created using the passed options.
145 * @since Zend Framework 1.9
147 * @param mixed $context Stream context or array of context options
148 * @return Zend_Http_Client_Adapter_Socket
150 public function setStreamContext($context)
152 if (is_resource($context) && get_resource_type($context) == 'stream-context') {
153 $this->_context = $context;
155 } elseif (is_array($context)) {
156 $this->_context = stream_context_create($context);
160 require_once 'Zend/Http/Client/Adapter/Exception.php';
161 throw new Zend_Http_Client_Adapter_Exception(
162 "Expecting either a stream context resource or array, got " . gettype($context)
170 * Get the stream context for the TCP connection to the server.
172 * If no stream context is set, will create a default one.
176 public function getStreamContext()
178 if (! $this->_context) {
179 $this->_context = stream_context_create();
182 return $this->_context;
186 * Connect to the remote server
188 * @param string $host
190 * @param boolean $secure
192 public function connect($host, $port = 80, $secure = false)
194 // If the URI should be accessed via SSL, prepend the Hostname with ssl://
195 $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
197 // If we are connected to the wrong host, disconnect first
198 if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) {
199 if (is_resource($this->socket)) $this->close();
202 // Now, if we are not connected, connect
203 if (! is_resource($this->socket) || ! $this->config['keepalive']) {
204 $context = $this->getStreamContext();
205 if ($secure || $this->config['sslusecontext']) {
206 if ($this->config['sslcert'] !== null) {
207 if (! stream_context_set_option($context, 'ssl', 'local_cert',
208 $this->config['sslcert'])) {
209 require_once 'Zend/Http/Client/Adapter/Exception.php';
210 throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option');
213 if ($this->config['sslpassphrase'] !== null) {
214 if (! stream_context_set_option($context, 'ssl', 'passphrase',
215 $this->config['sslpassphrase'])) {
216 require_once 'Zend/Http/Client/Adapter/Exception.php';
217 throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option');
222 $flags = STREAM_CLIENT_CONNECT;
223 if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT;
225 $this->socket = @stream_socket_client($host . ':' . $port,
228 (int) $this->config['timeout'],
232 if (! $this->socket) {
234 require_once 'Zend/Http/Client/Adapter/Exception.php';
235 throw new Zend_Http_Client_Adapter_Exception(
236 'Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr);
239 // Set the stream timeout
240 if (! stream_set_timeout($this->socket, (int) $this->config['timeout'])) {
241 require_once 'Zend/Http/Client/Adapter/Exception.php';
242 throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout');
245 // Update connected_to
246 $this->connected_to = array($host, $port);
251 * Send request to the remote server
253 * @param string $method
254 * @param Zend_Uri_Http $uri
255 * @param string $http_ver
256 * @param array $headers
257 * @param string $body
258 * @return string Request as string
260 public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '')
262 // Make sure we're properly connected
263 if (! $this->socket) {
264 require_once 'Zend/Http/Client/Adapter/Exception.php';
265 throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected');
268 $host = $uri->getHost();
269 $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
270 if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) {
271 require_once 'Zend/Http/Client/Adapter/Exception.php';
272 throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host');
275 // Save request method for later
276 $this->method = $method;
278 // Build request headers
279 $path = $uri->getPath();
280 if ($uri->getQuery()) $path .= '?' . $uri->getQuery();
281 $request = "{$method} {$path} HTTP/{$http_ver}\r\n";
282 foreach ($headers as $k => $v) {
283 if (is_string($k)) $v = ucfirst($k) . ": $v";
284 $request .= "$v\r\n";
287 if(is_resource($body)) {
290 // Add the request body
291 $request .= "\r\n" . $body;
295 if (! @fwrite($this->socket, $request)) {
296 require_once 'Zend/Http/Client/Adapter/Exception.php';
297 throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
300 if(is_resource($body)) {
301 if(stream_copy_to_stream($body, $this->socket) == 0) {
302 require_once 'Zend/Http/Client/Adapter/Exception.php';
303 throw new Zend_Http_Client_Adapter_Exception('Error writing request to server');
311 * Read response from server
315 public function read()
317 // First, read headers only
320 $stream = !empty($this->config['stream']);
322 while (($line = @fgets($this->socket)) !== false) {
323 $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
326 if (rtrim($line) === '') break;
330 $this->_checkSocketReadTimeout();
332 $statusCode = Zend_Http_Response::extractCode($response);
334 // Handle 100 and 101 responses internally by restarting the read again
335 if ($statusCode == 100 || $statusCode == 101) return $this->read();
337 // Check headers to see what kind of connection / transfer encoding we have
338 $headers = Zend_Http_Response::extractHeaders($response);
341 * Responses to HEAD requests and 204 or 304 responses are not expected
342 * to have a body - stop reading here
344 if ($statusCode == 304 || $statusCode == 204 ||
345 $this->method == Zend_Http_Client::HEAD) {
347 // Close the connection if requested to do so by the server
348 if (isset($headers['connection']) && $headers['connection'] == 'close') {
354 // If we got a 'transfer-encoding: chunked' header
355 if (isset($headers['transfer-encoding'])) {
357 if (strtolower($headers['transfer-encoding']) == 'chunked') {
360 $line = @fgets($this->socket);
361 $this->_checkSocketReadTimeout();
365 // Figure out the next chunk size
366 $chunksize = trim($line);
367 if (! ctype_xdigit($chunksize)) {
369 require_once 'Zend/Http/Client/Adapter/Exception.php';
370 throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' .
371 $chunksize . '" unable to read chunked body');
374 // Convert the hexadecimal value to plain integer
375 $chunksize = hexdec($chunksize);
378 $read_to = ftell($this->socket) + $chunksize;
381 $current_pos = ftell($this->socket);
382 if ($current_pos >= $read_to) break;
384 if($this->out_stream) {
385 if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
386 $this->_checkSocketReadTimeout();
390 $line = @fread($this->socket, $read_to - $current_pos);
391 if ($line === false || strlen($line) === 0) {
392 $this->_checkSocketReadTimeout();
397 } while (! feof($this->socket));
399 $chunk .= @fgets($this->socket);
400 $this->_checkSocketReadTimeout();
402 if(!$this->out_stream) {
405 } while ($chunksize > 0);
408 require_once 'Zend/Http/Client/Adapter/Exception.php';
409 throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' .
410 $headers['transfer-encoding'] . '" transfer encoding');
413 // We automatically decode chunked-messages when writing to a stream
414 // this means we have to disallow the Zend_Http_Response to do it again
415 if ($this->out_stream) {
416 $response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $response);
418 // Else, if we got the content-length header, read this number of bytes
419 } elseif (isset($headers['content-length'])) {
421 // If we got more than one Content-Length header (see ZF-9404) use
422 // the last value sent
423 if (is_array($headers['content-length'])) {
424 $contentLength = $headers['content-length'][count($headers['content-length']) - 1];
426 $contentLength = $headers['content-length'];
429 $current_pos = ftell($this->socket);
432 for ($read_to = $current_pos + $contentLength;
433 $read_to > $current_pos;
434 $current_pos = ftell($this->socket)) {
436 if($this->out_stream) {
437 if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
438 $this->_checkSocketReadTimeout();
442 $chunk = @fread($this->socket, $read_to - $current_pos);
443 if ($chunk === false || strlen($chunk) === 0) {
444 $this->_checkSocketReadTimeout();
451 // Break if the connection ended prematurely
452 if (feof($this->socket)) break;
455 // Fallback: just read the response until EOF
459 if($this->out_stream) {
460 if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) {
461 $this->_checkSocketReadTimeout();
465 $buff = @fread($this->socket, 8192);
466 if ($buff === false || strlen($buff) === 0) {
467 $this->_checkSocketReadTimeout();
474 } while (feof($this->socket) === false);
479 // Close the connection if requested to do so by the server
480 if (isset($headers['connection']) && $headers['connection'] == 'close') {
488 * Close the connection to the server
491 public function close()
493 if (is_resource($this->socket)) @fclose($this->socket);
494 $this->socket = null;
495 $this->connected_to = array(null, null);
499 * Check if the socket has timed out - if so close connection and throw
502 * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code
504 protected function _checkSocketReadTimeout()
507 $info = stream_get_meta_data($this->socket);
508 $timedout = $info['timed_out'];
511 require_once 'Zend/Http/Client/Adapter/Exception.php';
512 throw new Zend_Http_Client_Adapter_Exception(
513 "Read timed out after {$this->config['timeout']} seconds",
514 Zend_Http_Client_Adapter_Exception::READ_TIMEOUT
521 * Set output stream for the response
523 * @param resource $stream
524 * @return Zend_Http_Client_Adapter_Socket
526 public function setOutputStream($stream)
528 $this->out_stream = $stream;
533 * Destructor: make sure the socket is disconnected
535 * If we are in persistent TCP mode, will not close the connection
538 public function __destruct()
540 if (! $this->config['persistent']) {
541 if ($this->socket) $this->close();