Socket.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 1.2.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Network;
  16. use Cake\Core\InstanceConfigTrait;
  17. use Cake\Network\Exception\SocketException;
  18. use Cake\Validation\Validation;
  19. use Exception;
  20. use InvalidArgumentException;
  21. /**
  22. * CakePHP network socket connection class.
  23. *
  24. * Core base class for network communication.
  25. */
  26. class Socket
  27. {
  28. use InstanceConfigTrait;
  29. /**
  30. * Object description
  31. *
  32. * @var string
  33. */
  34. public $description = 'Remote DataSource Network Socket Interface';
  35. /**
  36. * Default configuration settings for the socket connection
  37. *
  38. * @var array
  39. */
  40. protected $_defaultConfig = [
  41. 'persistent' => false,
  42. 'host' => 'localhost',
  43. 'protocol' => 'tcp',
  44. 'port' => 80,
  45. 'timeout' => 30
  46. ];
  47. /**
  48. * Reference to socket connection resource
  49. *
  50. * @var resource|null
  51. */
  52. public $connection = null;
  53. /**
  54. * This boolean contains the current state of the Socket class
  55. *
  56. * @var bool
  57. */
  58. public $connected = false;
  59. /**
  60. * This variable contains an array with the last error number (num) and string (str)
  61. *
  62. * @var array
  63. */
  64. public $lastError = [];
  65. /**
  66. * True if the socket stream is encrypted after a Cake\Network\Socket::enableCrypto() call
  67. *
  68. * @var bool
  69. */
  70. public $encrypted = false;
  71. /**
  72. * Contains all the encryption methods available
  73. *
  74. * @var array
  75. */
  76. protected $_encryptMethods = [
  77. // @codingStandardsIgnoreStart
  78. 'sslv2_client' => STREAM_CRYPTO_METHOD_SSLv2_CLIENT,
  79. 'sslv3_client' => STREAM_CRYPTO_METHOD_SSLv3_CLIENT,
  80. 'sslv23_client' => STREAM_CRYPTO_METHOD_SSLv23_CLIENT,
  81. 'tls_client' => STREAM_CRYPTO_METHOD_TLS_CLIENT,
  82. 'sslv2_server' => STREAM_CRYPTO_METHOD_SSLv2_SERVER,
  83. 'sslv3_server' => STREAM_CRYPTO_METHOD_SSLv3_SERVER,
  84. 'sslv23_server' => STREAM_CRYPTO_METHOD_SSLv23_SERVER,
  85. 'tls_server' => STREAM_CRYPTO_METHOD_TLS_SERVER
  86. // @codingStandardsIgnoreEnd
  87. ];
  88. /**
  89. * Used to capture connection warnings which can happen when there are
  90. * SSL errors for example.
  91. *
  92. * @var array
  93. */
  94. protected $_connectionErrors = [];
  95. /**
  96. * Constructor.
  97. *
  98. * @param array $config Socket configuration, which will be merged with the base configuration
  99. * @see \Cake\Network\Socket::$_baseConfig
  100. */
  101. public function __construct(array $config = [])
  102. {
  103. $this->setConfig($config);
  104. }
  105. /**
  106. * Connect the socket to the given host and port.
  107. *
  108. * @return bool Success
  109. * @throws \Cake\Network\Exception\SocketException
  110. */
  111. public function connect()
  112. {
  113. if ($this->connection) {
  114. $this->disconnect();
  115. }
  116. $hasProtocol = strpos($this->_config['host'], '://') !== false;
  117. if ($hasProtocol) {
  118. list($this->_config['protocol'], $this->_config['host']) = explode('://', $this->_config['host']);
  119. }
  120. $scheme = null;
  121. if (!empty($this->_config['protocol'])) {
  122. $scheme = $this->_config['protocol'] . '://';
  123. }
  124. $this->_setSslContext($this->_config['host']);
  125. if (!empty($this->_config['context'])) {
  126. $context = stream_context_create($this->_config['context']);
  127. } else {
  128. $context = stream_context_create();
  129. }
  130. $connectAs = STREAM_CLIENT_CONNECT;
  131. if ($this->_config['persistent']) {
  132. $connectAs |= STREAM_CLIENT_PERSISTENT;
  133. }
  134. set_error_handler([$this, '_connectionErrorHandler']);
  135. $this->connection = stream_socket_client(
  136. $scheme . $this->_config['host'] . ':' . $this->_config['port'],
  137. $errNum,
  138. $errStr,
  139. $this->_config['timeout'],
  140. $connectAs,
  141. $context
  142. );
  143. restore_error_handler();
  144. if (!empty($errNum) || !empty($errStr)) {
  145. $this->setLastError($errNum, $errStr);
  146. throw new SocketException($errStr, $errNum);
  147. }
  148. if (!$this->connection && $this->_connectionErrors) {
  149. $message = implode("\n", $this->_connectionErrors);
  150. throw new SocketException($message, E_WARNING);
  151. }
  152. $this->connected = is_resource($this->connection);
  153. if ($this->connected) {
  154. stream_set_timeout($this->connection, $this->_config['timeout']);
  155. }
  156. return $this->connected;
  157. }
  158. /**
  159. * Configure the SSL context options.
  160. *
  161. * @param string $host The host name being connected to.
  162. * @return void
  163. */
  164. protected function _setSslContext($host)
  165. {
  166. foreach ($this->_config as $key => $value) {
  167. if (substr($key, 0, 4) !== 'ssl_') {
  168. continue;
  169. }
  170. $contextKey = substr($key, 4);
  171. if (empty($this->_config['context']['ssl'][$contextKey])) {
  172. $this->_config['context']['ssl'][$contextKey] = $value;
  173. }
  174. unset($this->_config[$key]);
  175. }
  176. if (!isset($this->_config['context']['ssl']['SNI_enabled'])) {
  177. $this->_config['context']['ssl']['SNI_enabled'] = true;
  178. }
  179. if (empty($this->_config['context']['ssl']['peer_name'])) {
  180. $this->_config['context']['ssl']['peer_name'] = $host;
  181. }
  182. if (empty($this->_config['context']['ssl']['cafile'])) {
  183. $dir = dirname(dirname(__DIR__));
  184. $this->_config['context']['ssl']['cafile'] = $dir . DIRECTORY_SEPARATOR .
  185. 'config' . DIRECTORY_SEPARATOR . 'cacert.pem';
  186. }
  187. if (!empty($this->_config['context']['ssl']['verify_host'])) {
  188. $this->_config['context']['ssl']['CN_match'] = $host;
  189. }
  190. unset($this->_config['context']['ssl']['verify_host']);
  191. }
  192. /**
  193. * socket_stream_client() does not populate errNum, or $errStr when there are
  194. * connection errors, as in the case of SSL verification failure.
  195. *
  196. * Instead we need to handle those errors manually.
  197. *
  198. * @param int $code Code number.
  199. * @param string $message Message.
  200. * @return void
  201. */
  202. protected function _connectionErrorHandler($code, $message)
  203. {
  204. $this->_connectionErrors[] = $message;
  205. }
  206. /**
  207. * Get the connection context.
  208. *
  209. * @return null|array Null when there is no connection, an array when there is.
  210. */
  211. public function context()
  212. {
  213. if (!$this->connection) {
  214. return null;
  215. }
  216. return stream_context_get_options($this->connection);
  217. }
  218. /**
  219. * Get the host name of the current connection.
  220. *
  221. * @return string Host name
  222. */
  223. public function host()
  224. {
  225. if (Validation::ip($this->_config['host'])) {
  226. return gethostbyaddr($this->_config['host']);
  227. }
  228. return gethostbyaddr($this->address());
  229. }
  230. /**
  231. * Get the IP address of the current connection.
  232. *
  233. * @return string IP address
  234. */
  235. public function address()
  236. {
  237. if (Validation::ip($this->_config['host'])) {
  238. return $this->_config['host'];
  239. }
  240. return gethostbyname($this->_config['host']);
  241. }
  242. /**
  243. * Get all IP addresses associated with the current connection.
  244. *
  245. * @return array IP addresses
  246. */
  247. public function addresses()
  248. {
  249. if (Validation::ip($this->_config['host'])) {
  250. return [$this->_config['host']];
  251. }
  252. return gethostbynamel($this->_config['host']);
  253. }
  254. /**
  255. * Get the last error as a string.
  256. *
  257. * @return string|null Last error
  258. */
  259. public function lastError()
  260. {
  261. if (!empty($this->lastError)) {
  262. return $this->lastError['num'] . ': ' . $this->lastError['str'];
  263. }
  264. return null;
  265. }
  266. /**
  267. * Set the last error.
  268. *
  269. * @param int $errNum Error code
  270. * @param string $errStr Error string
  271. * @return void
  272. */
  273. public function setLastError($errNum, $errStr)
  274. {
  275. $this->lastError = ['num' => $errNum, 'str' => $errStr];
  276. }
  277. /**
  278. * Write data to the socket.
  279. *
  280. * @param string $data The data to write to the socket.
  281. * @return int Bytes written.
  282. */
  283. public function write($data)
  284. {
  285. if (!$this->connected) {
  286. if (!$this->connect()) {
  287. return false;
  288. }
  289. }
  290. $totalBytes = strlen($data);
  291. $written = 0;
  292. while ($written < $totalBytes) {
  293. $rv = fwrite($this->connection, substr($data, $written));
  294. if ($rv === false || $rv === 0) {
  295. return $written;
  296. }
  297. $written += $rv;
  298. }
  299. return $written;
  300. }
  301. /**
  302. * Read data from the socket. Returns false if no data is available or no connection could be
  303. * established.
  304. *
  305. * @param int $length Optional buffer length to read; defaults to 1024
  306. * @return mixed Socket data
  307. */
  308. public function read($length = 1024)
  309. {
  310. if (!$this->connected) {
  311. if (!$this->connect()) {
  312. return false;
  313. }
  314. }
  315. if (!feof($this->connection)) {
  316. $buffer = fread($this->connection, $length);
  317. $info = stream_get_meta_data($this->connection);
  318. if ($info['timed_out']) {
  319. $this->setLastError(E_WARNING, 'Connection timed out');
  320. return false;
  321. }
  322. return $buffer;
  323. }
  324. return false;
  325. }
  326. /**
  327. * Disconnect the socket from the current connection.
  328. *
  329. * @return bool Success
  330. */
  331. public function disconnect()
  332. {
  333. if (!is_resource($this->connection)) {
  334. $this->connected = false;
  335. return true;
  336. }
  337. $this->connected = !fclose($this->connection);
  338. if (!$this->connected) {
  339. $this->connection = null;
  340. }
  341. return !$this->connected;
  342. }
  343. /**
  344. * Destructor, used to disconnect from current connection.
  345. */
  346. public function __destruct()
  347. {
  348. $this->disconnect();
  349. }
  350. /**
  351. * Resets the state of this Socket instance to it's initial state (before Object::__construct got executed)
  352. *
  353. * @param array|null $state Array with key and values to reset
  354. * @return bool True on success
  355. */
  356. public function reset($state = null)
  357. {
  358. if (empty($state)) {
  359. static $initalState = [];
  360. if (empty($initalState)) {
  361. $initalState = get_class_vars(__CLASS__);
  362. }
  363. $state = $initalState;
  364. }
  365. foreach ($state as $property => $value) {
  366. $this->{$property} = $value;
  367. }
  368. return true;
  369. }
  370. /**
  371. * Encrypts current stream socket, using one of the defined encryption methods
  372. *
  373. * @param string $type can be one of 'ssl2', 'ssl3', 'ssl23' or 'tls'
  374. * @param string $clientOrServer can be one of 'client', 'server'. Default is 'client'
  375. * @param bool $enable enable or disable encryption. Default is true (enable)
  376. * @return bool True on success
  377. * @throws \InvalidArgumentException When an invalid encryption scheme is chosen.
  378. * @throws \Cake\Network\Exception\SocketException When attempting to enable SSL/TLS fails
  379. * @see stream_socket_enable_crypto
  380. */
  381. public function enableCrypto($type, $clientOrServer = 'client', $enable = true)
  382. {
  383. if (!array_key_exists($type . '_' . $clientOrServer, $this->_encryptMethods)) {
  384. throw new InvalidArgumentException('Invalid encryption scheme chosen');
  385. }
  386. try {
  387. $enableCryptoResult = stream_socket_enable_crypto($this->connection, $enable, $this->_encryptMethods[$type . '_' . $clientOrServer]);
  388. } catch (Exception $e) {
  389. $this->setLastError(null, $e->getMessage());
  390. throw new SocketException($e->getMessage());
  391. }
  392. if ($enableCryptoResult === true) {
  393. $this->encrypted = $enable;
  394. return true;
  395. }
  396. $errorMessage = 'Unable to perform enableCrypto operation on the current socket';
  397. $this->setLastError(null, $errorMessage);
  398. throw new SocketException($errorMessage);
  399. }
  400. }