web/lib/Zend/Http/Client/Adapter/Socket.php
changeset 64 162c1de6545a
parent 19 1c2f13fd785c
child 68 ecaf28ffe26e
equal deleted inserted replaced
63:5b37998e522e 64:162c1de6545a
       
     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 22576 2010-07-16 15:49:24Z dragonbe $
       
    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
       
    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  * @see Zend_Http_Client_Adapter_Stream
       
    34  */
       
    35 require_once 'Zend/Http/Client/Adapter/Stream.php';
       
    36 
       
    37 /**
       
    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.
       
    40  *
       
    41  * @category   Zend
       
    42  * @package    Zend_Http
       
    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
       
    46  */
       
    47 class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream
       
    48 {
       
    49     /**
       
    50      * The socket for server connection
       
    51      *
       
    52      * @var resource|null
       
    53      */
       
    54     protected $socket = null;
       
    55 
       
    56     /**
       
    57      * What host/port are we connected to?
       
    58      *
       
    59      * @var array
       
    60      */
       
    61     protected $connected_to = array(null, null);
       
    62 
       
    63     /**
       
    64      * Stream for storing output
       
    65      * 
       
    66      * @var resource
       
    67      */
       
    68     protected $out_stream = null;
       
    69     
       
    70     /**
       
    71      * Parameters array
       
    72      *
       
    73      * @var array
       
    74      */
       
    75     protected $config = array(
       
    76         'persistent'    => false,
       
    77         'ssltransport'  => 'ssl',
       
    78         'sslcert'       => null,
       
    79         'sslpassphrase' => null,
       
    80         'sslusecontext' => false
       
    81     );
       
    82 
       
    83     /**
       
    84      * Request method - will be set by write() and might be used by read()
       
    85      *
       
    86      * @var string
       
    87      */
       
    88     protected $method = null;
       
    89 
       
    90     /**
       
    91      * Stream context
       
    92      *
       
    93      * @var resource
       
    94      */
       
    95     protected $_context = null;
       
    96 
       
    97     /**
       
    98      * Adapter constructor, currently empty. Config is set using setConfig()
       
    99      *
       
   100      */
       
   101     public function __construct()
       
   102     {
       
   103     }
       
   104 
       
   105     /**
       
   106      * Set the configuration array for the adapter
       
   107      *
       
   108      * @param Zend_Config | array $config
       
   109      */
       
   110     public function setConfig($config = array())
       
   111     {
       
   112         if ($config instanceof Zend_Config) {
       
   113             $config = $config->toArray();
       
   114 
       
   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)
       
   119             );
       
   120         }
       
   121 
       
   122         foreach ($config as $k => $v) {
       
   123             $this->config[strtolower($k)] = $v;
       
   124         }
       
   125     }
       
   126 
       
   127 	/**
       
   128  	 * Retrieve the array of all configuration options
       
   129  	 *
       
   130  	 * @return array
       
   131  	 */
       
   132  	public function getConfig()
       
   133  	{
       
   134  	    return $this->config;
       
   135  	}
       
   136 
       
   137  	/**
       
   138      * Set the stream context for the TCP connection to the server
       
   139      *
       
   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.
       
   144      *
       
   145      * @since  Zend Framework 1.9
       
   146      *
       
   147      * @param  mixed $context Stream context or array of context options
       
   148      * @return Zend_Http_Client_Adapter_Socket
       
   149      */
       
   150     public function setStreamContext($context)
       
   151     {
       
   152         if (is_resource($context) && get_resource_type($context) == 'stream-context') {
       
   153             $this->_context = $context;
       
   154 
       
   155         } elseif (is_array($context)) {
       
   156             $this->_context = stream_context_create($context);
       
   157 
       
   158         } else {
       
   159             // Invalid parameter
       
   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)
       
   163             );
       
   164         }
       
   165 
       
   166         return $this;
       
   167     }
       
   168 
       
   169     /**
       
   170      * Get the stream context for the TCP connection to the server.
       
   171      *
       
   172      * If no stream context is set, will create a default one.
       
   173      *
       
   174      * @return resource
       
   175      */
       
   176     public function getStreamContext()
       
   177     {
       
   178         if (! $this->_context) {
       
   179             $this->_context = stream_context_create();
       
   180         }
       
   181 
       
   182         return $this->_context;
       
   183     }
       
   184 
       
   185     /**
       
   186      * Connect to the remote server
       
   187      *
       
   188      * @param string  $host
       
   189      * @param int     $port
       
   190      * @param boolean $secure
       
   191      */
       
   192     public function connect($host, $port = 80, $secure = false)
       
   193     {
       
   194         // If the URI should be accessed via SSL, prepend the Hostname with ssl://
       
   195         $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host;
       
   196 
       
   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();
       
   200         }
       
   201 
       
   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');
       
   211                     }
       
   212                 }
       
   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');
       
   218                     }
       
   219                 }
       
   220             }
       
   221 
       
   222             $flags = STREAM_CLIENT_CONNECT;
       
   223             if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT;
       
   224 
       
   225             $this->socket = @stream_socket_client($host . ':' . $port,
       
   226                                                   $errno,
       
   227                                                   $errstr,
       
   228                                                   (int) $this->config['timeout'],
       
   229                                                   $flags,
       
   230                                                   $context);
       
   231 
       
   232             if (! $this->socket) {
       
   233                 $this->close();
       
   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);
       
   237             }
       
   238 
       
   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');
       
   243             }
       
   244 
       
   245             // Update connected_to
       
   246             $this->connected_to = array($host, $port);
       
   247         }
       
   248     }
       
   249 
       
   250     /**
       
   251      * Send request to the remote server
       
   252      *
       
   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
       
   259      */
       
   260     public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '')
       
   261     {
       
   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');
       
   266         }
       
   267 
       
   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');
       
   273         }
       
   274 
       
   275         // Save request method for later
       
   276         $this->method = $method;
       
   277 
       
   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";
       
   285         }
       
   286 
       
   287         if(is_resource($body)) {
       
   288             $request .= "\r\n";
       
   289         } else {
       
   290             // Add the request body
       
   291             $request .= "\r\n" . $body;
       
   292         }
       
   293         
       
   294         // Send the request
       
   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');
       
   298         }
       
   299         
       
   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');
       
   304             }
       
   305         }
       
   306 
       
   307         return $request;
       
   308     }
       
   309 
       
   310     /**
       
   311      * Read response from server
       
   312      *
       
   313      * @return string
       
   314      */
       
   315     public function read()
       
   316     {
       
   317         // First, read headers only
       
   318         $response = '';
       
   319         $gotStatus = false;
       
   320         $stream = !empty($this->config['stream']);
       
   321 
       
   322         while (($line = @fgets($this->socket)) !== false) {
       
   323             $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false);
       
   324             if ($gotStatus) {
       
   325                 $response .= $line;
       
   326                 if (rtrim($line) === '') break;
       
   327             }
       
   328         }
       
   329         
       
   330         $this->_checkSocketReadTimeout();
       
   331 
       
   332         $statusCode = Zend_Http_Response::extractCode($response);
       
   333 
       
   334         // Handle 100 and 101 responses internally by restarting the read again
       
   335         if ($statusCode == 100 || $statusCode == 101) return $this->read();
       
   336 
       
   337         // Check headers to see what kind of connection / transfer encoding we have
       
   338         $headers = Zend_Http_Response::extractHeaders($response);
       
   339 
       
   340         /**
       
   341          * Responses to HEAD requests and 204 or 304 responses are not expected
       
   342          * to have a body - stop reading here
       
   343          */
       
   344         if ($statusCode == 304 || $statusCode == 204 ||
       
   345             $this->method == Zend_Http_Client::HEAD) {
       
   346 
       
   347             // Close the connection if requested to do so by the server
       
   348             if (isset($headers['connection']) && $headers['connection'] == 'close') {
       
   349                 $this->close();
       
   350             }
       
   351             return $response;
       
   352         }
       
   353 
       
   354         // If we got a 'transfer-encoding: chunked' header
       
   355         if (isset($headers['transfer-encoding'])) {
       
   356             
       
   357             if (strtolower($headers['transfer-encoding']) == 'chunked') {
       
   358 
       
   359                 do {
       
   360                     $line  = @fgets($this->socket);
       
   361                     $this->_checkSocketReadTimeout();
       
   362 
       
   363                     $chunk = $line;
       
   364 
       
   365                     // Figure out the next chunk size
       
   366                     $chunksize = trim($line);
       
   367                     if (! ctype_xdigit($chunksize)) {
       
   368                         $this->close();
       
   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');
       
   372                     }
       
   373 
       
   374                     // Convert the hexadecimal value to plain integer
       
   375                     $chunksize = hexdec($chunksize);
       
   376 
       
   377                     // Read next chunk
       
   378                     $read_to = ftell($this->socket) + $chunksize;
       
   379 
       
   380                     do {
       
   381                         $current_pos = ftell($this->socket);
       
   382                         if ($current_pos >= $read_to) break;
       
   383 
       
   384                         if($this->out_stream) {
       
   385                             if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
       
   386                               $this->_checkSocketReadTimeout();
       
   387                               break;   
       
   388                              }
       
   389                         } else {
       
   390                             $line = @fread($this->socket, $read_to - $current_pos);
       
   391                             if ($line === false || strlen($line) === 0) {
       
   392                                 $this->_checkSocketReadTimeout();
       
   393                                 break;
       
   394                             }
       
   395                                     $chunk .= $line;
       
   396                         }
       
   397                     } while (! feof($this->socket));
       
   398 
       
   399                     $chunk .= @fgets($this->socket);
       
   400                     $this->_checkSocketReadTimeout();
       
   401 
       
   402                     if(!$this->out_stream) {
       
   403                         $response .= $chunk;
       
   404                     }
       
   405                 } while ($chunksize > 0);
       
   406             } else {
       
   407                 $this->close();
       
   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');
       
   411             }
       
   412             
       
   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);
       
   417             }
       
   418         // Else, if we got the content-length header, read this number of bytes
       
   419         } elseif (isset($headers['content-length'])) {
       
   420 
       
   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]; 
       
   425             } else {
       
   426                 $contentLength = $headers['content-length'];
       
   427             }
       
   428             
       
   429             $current_pos = ftell($this->socket);
       
   430             $chunk = '';
       
   431 
       
   432             for ($read_to = $current_pos + $contentLength;
       
   433                  $read_to > $current_pos;
       
   434                  $current_pos = ftell($this->socket)) {
       
   435 
       
   436                  if($this->out_stream) {
       
   437                      if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) {
       
   438                           $this->_checkSocketReadTimeout();
       
   439                           break;   
       
   440                      }
       
   441                  } else {
       
   442                     $chunk = @fread($this->socket, $read_to - $current_pos);
       
   443                     if ($chunk === false || strlen($chunk) === 0) {
       
   444                         $this->_checkSocketReadTimeout();
       
   445                         break;
       
   446                     }
       
   447 
       
   448                     $response .= $chunk;
       
   449                 }
       
   450 
       
   451                 // Break if the connection ended prematurely
       
   452                 if (feof($this->socket)) break;
       
   453             }
       
   454 
       
   455         // Fallback: just read the response until EOF
       
   456         } else {
       
   457 
       
   458             do {
       
   459                 if($this->out_stream) {
       
   460                     if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) {
       
   461                           $this->_checkSocketReadTimeout();
       
   462                           break;   
       
   463                      }
       
   464                 }  else {
       
   465                     $buff = @fread($this->socket, 8192);
       
   466                     if ($buff === false || strlen($buff) === 0) {
       
   467                         $this->_checkSocketReadTimeout();
       
   468                         break;
       
   469                     } else {
       
   470                         $response .= $buff;
       
   471                     }
       
   472                 }
       
   473 
       
   474             } while (feof($this->socket) === false);
       
   475 
       
   476             $this->close();
       
   477         }
       
   478 
       
   479         // Close the connection if requested to do so by the server
       
   480         if (isset($headers['connection']) && $headers['connection'] == 'close') {
       
   481             $this->close();
       
   482         }
       
   483 
       
   484         return $response;
       
   485     }
       
   486 
       
   487     /**
       
   488      * Close the connection to the server
       
   489      *
       
   490      */
       
   491     public function close()
       
   492     {
       
   493         if (is_resource($this->socket)) @fclose($this->socket);
       
   494         $this->socket = null;
       
   495         $this->connected_to = array(null, null);
       
   496     }
       
   497 
       
   498     /**
       
   499      * Check if the socket has timed out - if so close connection and throw
       
   500      * an exception
       
   501      *
       
   502      * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code
       
   503      */
       
   504     protected function _checkSocketReadTimeout()
       
   505     {
       
   506         if ($this->socket) {
       
   507             $info = stream_get_meta_data($this->socket);
       
   508             $timedout = $info['timed_out'];
       
   509             if ($timedout) {
       
   510                 $this->close();
       
   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
       
   515                 );
       
   516             }
       
   517         }
       
   518     }
       
   519     
       
   520     /**
       
   521      * Set output stream for the response
       
   522      * 
       
   523      * @param resource $stream
       
   524      * @return Zend_Http_Client_Adapter_Socket
       
   525      */
       
   526     public function setOutputStream($stream) 
       
   527     {
       
   528         $this->out_stream = $stream;
       
   529         return $this;
       
   530     }
       
   531     
       
   532     /**
       
   533      * Destructor: make sure the socket is disconnected
       
   534      *
       
   535      * If we are in persistent TCP mode, will not close the connection
       
   536      *
       
   537      */
       
   538     public function __destruct()
       
   539     {
       
   540         if (! $this->config['persistent']) {
       
   541             if ($this->socket) $this->close();
       
   542         }
       
   543     }
       
   544 }