File indexing completed on 2025-01-12 05:22:07
0001 <?php 0002 0003 /** 0004 * Zend Framework 0005 * 0006 * LICENSE 0007 * 0008 * This source file is subject to the new BSD license that is bundled 0009 * with this package in the file LICENSE.txt. 0010 * It is also available through the world-wide-web at this URL: 0011 * http://framework.zend.com/license/new-bsd 0012 * If you did not receive a copy of the license and are unable to 0013 * obtain it through the world-wide-web, please send an email 0014 * to license@zend.com so we can send you a copy immediately. 0015 * 0016 * @category Zend 0017 * @package Zend_Http 0018 * @subpackage Client_Adapter 0019 * @version $Id$ 0020 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0021 * @license http://framework.zend.com/license/new-bsd New BSD License 0022 */ 0023 0024 /** 0025 * @see Zend_Uri_Http 0026 */ 0027 // require_once 'Zend/Uri/Http.php'; 0028 /** 0029 * @see Zend_Http_Client_Adapter_Interface 0030 */ 0031 // require_once 'Zend/Http/Client/Adapter/Interface.php'; 0032 /** 0033 * @see Zend_Http_Client_Adapter_Stream 0034 */ 0035 // require_once 'Zend/Http/Client/Adapter/Stream.php'; 0036 0037 /** 0038 * A sockets based (stream_socket_client) adapter class for Zend_Http_Client. Can be used 0039 * on almost every PHP environment, and does not require any special extensions. 0040 * 0041 * @category Zend 0042 * @package Zend_Http 0043 * @subpackage Client_Adapter 0044 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 0045 * @license http://framework.zend.com/license/new-bsd New BSD License 0046 */ 0047 class Zend_Http_Client_Adapter_Socket implements Zend_Http_Client_Adapter_Interface, Zend_Http_Client_Adapter_Stream 0048 { 0049 /** 0050 * The socket for server connection 0051 * 0052 * @var resource|null 0053 */ 0054 protected $socket = null; 0055 0056 /** 0057 * What host/port are we connected to? 0058 * 0059 * @var array 0060 */ 0061 protected $connected_to = array(null, null); 0062 0063 /** 0064 * Stream for storing output 0065 * 0066 * @var resource 0067 */ 0068 protected $out_stream = null; 0069 0070 /** 0071 * Parameters array 0072 * 0073 * @var array 0074 */ 0075 protected $config = array( 0076 'persistent' => false, 0077 'ssltransport' => 'ssl', 0078 'sslcert' => null, 0079 'sslpassphrase' => null, 0080 'sslusecontext' => false 0081 ); 0082 0083 /** 0084 * Request method - will be set by write() and might be used by read() 0085 * 0086 * @var string 0087 */ 0088 protected $method = null; 0089 0090 /** 0091 * Stream context 0092 * 0093 * @var resource 0094 */ 0095 protected $_context = null; 0096 0097 /** 0098 * Adapter constructor, currently empty. Config is set using setConfig() 0099 * 0100 */ 0101 public function __construct() 0102 { 0103 } 0104 0105 /** 0106 * Set the configuration array for the adapter 0107 * 0108 * @param Zend_Config | array $config 0109 */ 0110 public function setConfig($config = array()) 0111 { 0112 if ($config instanceof Zend_Config) { 0113 $config = $config->toArray(); 0114 0115 } elseif (! is_array($config)) { 0116 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0117 throw new Zend_Http_Client_Adapter_Exception( 0118 'Array or Zend_Config object expected, got ' . gettype($config) 0119 ); 0120 } 0121 0122 foreach ($config as $k => $v) { 0123 $this->config[strtolower($k)] = $v; 0124 } 0125 } 0126 0127 /** 0128 * Retrieve the array of all configuration options 0129 * 0130 * @return array 0131 */ 0132 public function getConfig() 0133 { 0134 return $this->config; 0135 } 0136 0137 /** 0138 * Set the stream context for the TCP connection to the server 0139 * 0140 * Can accept either a pre-existing stream context resource, or an array 0141 * of stream options, similar to the options array passed to the 0142 * stream_context_create() PHP function. In such case a new stream context 0143 * will be created using the passed options. 0144 * 0145 * @since Zend Framework 1.9 0146 * 0147 * @param mixed $context Stream context or array of context options 0148 * @return Zend_Http_Client_Adapter_Socket 0149 */ 0150 public function setStreamContext($context) 0151 { 0152 if (is_resource($context) && get_resource_type($context) == 'stream-context') { 0153 $this->_context = $context; 0154 0155 } elseif (is_array($context)) { 0156 $this->_context = stream_context_create($context); 0157 0158 } else { 0159 // Invalid parameter 0160 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0161 throw new Zend_Http_Client_Adapter_Exception( 0162 "Expecting either a stream context resource or array, got " . gettype($context) 0163 ); 0164 } 0165 0166 return $this; 0167 } 0168 0169 /** 0170 * Get the stream context for the TCP connection to the server. 0171 * 0172 * If no stream context is set, will create a default one. 0173 * 0174 * @return resource 0175 */ 0176 public function getStreamContext() 0177 { 0178 if (! $this->_context) { 0179 $this->_context = stream_context_create(); 0180 } 0181 0182 return $this->_context; 0183 } 0184 0185 /** 0186 * Connect to the remote server 0187 * 0188 * @param string $host 0189 * @param int $port 0190 * @param boolean $secure 0191 */ 0192 public function connect($host, $port = 80, $secure = false) 0193 { 0194 // If the URI should be accessed via SSL, prepend the Hostname with ssl:// 0195 $host = ($secure ? $this->config['ssltransport'] : 'tcp') . '://' . $host; 0196 0197 // If we are connected to the wrong host, disconnect first 0198 if (($this->connected_to[0] != $host || $this->connected_to[1] != $port)) { 0199 if (is_resource($this->socket)) $this->close(); 0200 } 0201 0202 // Now, if we are not connected, connect 0203 if (! is_resource($this->socket) || ! $this->config['keepalive']) { 0204 $context = $this->getStreamContext(); 0205 if ($secure || $this->config['sslusecontext']) { 0206 if ($this->config['sslcert'] !== null) { 0207 if (! stream_context_set_option($context, 'ssl', 'local_cert', 0208 $this->config['sslcert'])) { 0209 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0210 throw new Zend_Http_Client_Adapter_Exception('Unable to set sslcert option'); 0211 } 0212 } 0213 if ($this->config['sslpassphrase'] !== null) { 0214 if (! stream_context_set_option($context, 'ssl', 'passphrase', 0215 $this->config['sslpassphrase'])) { 0216 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0217 throw new Zend_Http_Client_Adapter_Exception('Unable to set sslpassphrase option'); 0218 } 0219 } 0220 } 0221 0222 $flags = STREAM_CLIENT_CONNECT; 0223 if ($this->config['persistent']) $flags |= STREAM_CLIENT_PERSISTENT; 0224 0225 $this->socket = @stream_socket_client($host . ':' . $port, 0226 $errno, 0227 $errstr, 0228 (int) $this->config['timeout'], 0229 $flags, 0230 $context); 0231 0232 if (! $this->socket) { 0233 $this->close(); 0234 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0235 throw new Zend_Http_Client_Adapter_Exception( 0236 'Unable to Connect to ' . $host . ':' . $port . '. Error #' . $errno . ': ' . $errstr); 0237 } 0238 0239 // Set the stream timeout 0240 if (! stream_set_timeout($this->socket, (int) $this->config['timeout'])) { 0241 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0242 throw new Zend_Http_Client_Adapter_Exception('Unable to set the connection timeout'); 0243 } 0244 0245 // Update connected_to 0246 $this->connected_to = array($host, $port); 0247 } 0248 } 0249 0250 /** 0251 * Send request to the remote server 0252 * 0253 * @param string $method 0254 * @param Zend_Uri_Http $uri 0255 * @param string $http_ver 0256 * @param array $headers 0257 * @param string $body 0258 * @return string Request as string 0259 */ 0260 public function write($method, $uri, $http_ver = '1.1', $headers = array(), $body = '') 0261 { 0262 // Make sure we're properly connected 0263 if (! $this->socket) { 0264 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0265 throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are not connected'); 0266 } 0267 0268 $host = $uri->getHost(); 0269 $host = (strtolower($uri->getScheme()) == 'https' ? $this->config['ssltransport'] : 'tcp') . '://' . $host; 0270 if ($this->connected_to[0] != $host || $this->connected_to[1] != $uri->getPort()) { 0271 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0272 throw new Zend_Http_Client_Adapter_Exception('Trying to write but we are connected to the wrong host'); 0273 } 0274 0275 // Save request method for later 0276 $this->method = $method; 0277 0278 // Build request headers 0279 $path = $uri->getPath(); 0280 if ($uri->getQuery()) $path .= '?' . $uri->getQuery(); 0281 $request = "{$method} {$path} HTTP/{$http_ver}\r\n"; 0282 foreach ($headers as $k => $v) { 0283 if (is_string($k)) $v = ucfirst($k) . ": $v"; 0284 $request .= "$v\r\n"; 0285 } 0286 0287 if(is_resource($body)) { 0288 $request .= "\r\n"; 0289 } else { 0290 // Add the request body 0291 $request .= "\r\n" . $body; 0292 } 0293 0294 // Send the request 0295 if (! @fwrite($this->socket, $request)) { 0296 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0297 throw new Zend_Http_Client_Adapter_Exception('Error writing request to server'); 0298 } 0299 0300 if(is_resource($body)) { 0301 if(stream_copy_to_stream($body, $this->socket) == 0) { 0302 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0303 throw new Zend_Http_Client_Adapter_Exception('Error writing request to server'); 0304 } 0305 } 0306 0307 return $request; 0308 } 0309 0310 /** 0311 * Read response from server 0312 * 0313 * @return string 0314 */ 0315 public function read() 0316 { 0317 // First, read headers only 0318 $response = ''; 0319 $gotStatus = false; 0320 0321 while (($line = @fgets($this->socket)) !== false) { 0322 $gotStatus = $gotStatus || (strpos($line, 'HTTP') !== false); 0323 if ($gotStatus) { 0324 $response .= $line; 0325 if (rtrim($line) === '') break; 0326 } 0327 } 0328 0329 $this->_checkSocketReadTimeout(); 0330 0331 $statusCode = Zend_Http_Response::extractCode($response); 0332 0333 // Handle 100 and 101 responses internally by restarting the read again 0334 if ($statusCode == 100 || $statusCode == 101) return $this->read(); 0335 0336 // Check headers to see what kind of connection / transfer encoding we have 0337 $headers = Zend_Http_Response::extractHeaders($response); 0338 0339 /** 0340 * Responses to HEAD requests and 204 or 304 responses are not expected 0341 * to have a body - stop reading here 0342 */ 0343 if ($statusCode == 304 || $statusCode == 204 || 0344 $this->method == Zend_Http_Client::HEAD) { 0345 0346 // Close the connection if requested to do so by the server 0347 if (isset($headers['connection']) && $headers['connection'] == 'close') { 0348 $this->close(); 0349 } 0350 return $response; 0351 } 0352 0353 // If we got a 'transfer-encoding: chunked' header 0354 if (isset($headers['transfer-encoding'])) { 0355 0356 if (strtolower($headers['transfer-encoding']) == 'chunked') { 0357 0358 do { 0359 $line = @fgets($this->socket); 0360 $this->_checkSocketReadTimeout(); 0361 0362 $chunk = $line; 0363 0364 // Figure out the next chunk size 0365 $chunksize = trim($line); 0366 if (! ctype_xdigit($chunksize)) { 0367 $this->close(); 0368 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0369 throw new Zend_Http_Client_Adapter_Exception('Invalid chunk size "' . 0370 $chunksize . '" unable to read chunked body'); 0371 } 0372 0373 // Convert the hexadecimal value to plain integer 0374 $chunksize = hexdec($chunksize); 0375 0376 // Read next chunk 0377 $read_to = ftell($this->socket) + $chunksize; 0378 0379 do { 0380 $current_pos = ftell($this->socket); 0381 if ($current_pos >= $read_to) break; 0382 0383 if($this->out_stream) { 0384 if(stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) { 0385 $this->_checkSocketReadTimeout(); 0386 break; 0387 } 0388 } else { 0389 $line = @fread($this->socket, $read_to - $current_pos); 0390 if ($line === false || strlen($line) === 0) { 0391 $this->_checkSocketReadTimeout(); 0392 break; 0393 } 0394 $chunk .= $line; 0395 } 0396 } while (! feof($this->socket)); 0397 0398 $chunk .= @fgets($this->socket); 0399 $this->_checkSocketReadTimeout(); 0400 0401 if(!$this->out_stream) { 0402 $response .= $chunk; 0403 } 0404 } while ($chunksize > 0); 0405 } else { 0406 $this->close(); 0407 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0408 throw new Zend_Http_Client_Adapter_Exception('Cannot handle "' . 0409 $headers['transfer-encoding'] . '" transfer encoding'); 0410 } 0411 0412 // We automatically decode chunked-messages when writing to a stream 0413 // this means we have to disallow the Zend_Http_Response to do it again 0414 if ($this->out_stream) { 0415 $response = str_ireplace("Transfer-Encoding: chunked\r\n", '', $response); 0416 } 0417 // Else, if we got the content-length header, read this number of bytes 0418 } elseif (isset($headers['content-length'])) { 0419 0420 // If we got more than one Content-Length header (see ZF-9404) use 0421 // the last value sent 0422 if (is_array($headers['content-length'])) { 0423 $contentLength = $headers['content-length'][count($headers['content-length']) - 1]; 0424 } else { 0425 $contentLength = $headers['content-length']; 0426 } 0427 0428 $current_pos = ftell($this->socket); 0429 $chunk = ''; 0430 0431 for ($read_to = $current_pos + $contentLength; 0432 $read_to > $current_pos; 0433 $current_pos = ftell($this->socket)) { 0434 0435 if($this->out_stream) { 0436 if(@stream_copy_to_stream($this->socket, $this->out_stream, $read_to - $current_pos) == 0) { 0437 $this->_checkSocketReadTimeout(); 0438 break; 0439 } 0440 } else { 0441 $chunk = @fread($this->socket, $read_to - $current_pos); 0442 if ($chunk === false || strlen($chunk) === 0) { 0443 $this->_checkSocketReadTimeout(); 0444 break; 0445 } 0446 0447 $response .= $chunk; 0448 } 0449 0450 // Break if the connection ended prematurely 0451 if (feof($this->socket)) break; 0452 } 0453 0454 // Fallback: just read the response until EOF 0455 } else { 0456 0457 do { 0458 if($this->out_stream) { 0459 if(@stream_copy_to_stream($this->socket, $this->out_stream) == 0) { 0460 $this->_checkSocketReadTimeout(); 0461 break; 0462 } 0463 } else { 0464 $buff = @fread($this->socket, 8192); 0465 if ($buff === false || strlen($buff) === 0) { 0466 $this->_checkSocketReadTimeout(); 0467 break; 0468 } else { 0469 $response .= $buff; 0470 } 0471 } 0472 0473 } while (feof($this->socket) === false); 0474 0475 $this->close(); 0476 } 0477 0478 // Close the connection if requested to do so by the server 0479 if (isset($headers['connection']) && $headers['connection'] == 'close') { 0480 $this->close(); 0481 } 0482 0483 return $response; 0484 } 0485 0486 /** 0487 * Close the connection to the server 0488 * 0489 */ 0490 public function close() 0491 { 0492 if (is_resource($this->socket)) @fclose($this->socket); 0493 $this->socket = null; 0494 $this->connected_to = array(null, null); 0495 } 0496 0497 /** 0498 * Check if the socket has timed out - if so close connection and throw 0499 * an exception 0500 * 0501 * @throws Zend_Http_Client_Adapter_Exception with READ_TIMEOUT code 0502 */ 0503 protected function _checkSocketReadTimeout() 0504 { 0505 if ($this->socket) { 0506 $info = stream_get_meta_data($this->socket); 0507 $timedout = $info['timed_out']; 0508 if ($timedout) { 0509 $this->close(); 0510 // require_once 'Zend/Http/Client/Adapter/Exception.php'; 0511 throw new Zend_Http_Client_Adapter_Exception( 0512 "Read timed out after {$this->config['timeout']} seconds", 0513 Zend_Http_Client_Adapter_Exception::READ_TIMEOUT 0514 ); 0515 } 0516 } 0517 } 0518 0519 /** 0520 * Set output stream for the response 0521 * 0522 * @param resource $stream 0523 * @return Zend_Http_Client_Adapter_Socket 0524 */ 0525 public function setOutputStream($stream) 0526 { 0527 $this->out_stream = $stream; 0528 return $this; 0529 } 0530 0531 /** 0532 * Destructor: make sure the socket is disconnected 0533 * 0534 * If we are in persistent TCP mode, will not close the connection 0535 * 0536 */ 0537 public function __destruct() 0538 { 0539 if (! $this->config['persistent']) { 0540 if ($this->socket) $this->close(); 0541 } 0542 } 0543 }