Socket.php 13 KB

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