Connection.php 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. <?php
  2. /**
  3. * Hoa
  4. *
  5. *
  6. * @license
  7. *
  8. * New BSD License
  9. *
  10. * Copyright © 2007-2017, Hoa community. All rights reserved.
  11. *
  12. * Redistribution and use in source and binary forms, with or without
  13. * modification, are permitted provided that the following conditions are met:
  14. * * Redistributions of source code must retain the above copyright
  15. * notice, this list of conditions and the following disclaimer.
  16. * * Redistributions in binary form must reproduce the above copyright
  17. * notice, this list of conditions and the following disclaimer in the
  18. * documentation and/or other materials provided with the distribution.
  19. * * Neither the name of the Hoa nor the names of its contributors may be
  20. * used to endorse or promote products derived from this software without
  21. * specific prior written permission.
  22. *
  23. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  24. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  25. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  26. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
  27. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  28. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  29. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  30. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  31. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  32. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  33. * POSSIBILITY OF SUCH DAMAGE.
  34. */
  35. namespace Hoa\Websocket;
  36. use Hoa\Event;
  37. use Hoa\Exception as HoaException;
  38. use Hoa\Socket as HoaSocket;
  39. /**
  40. * Class \Hoa\Websocket\Connection.
  41. *
  42. * A cross-protocol Websocket connection.
  43. *
  44. * @copyright Copyright © 2007-2017 Hoa community
  45. * @license New BSD License
  46. */
  47. abstract class Connection
  48. extends HoaSocket\Connection\Handler
  49. implements Event\Listenable
  50. {
  51. use Event\Listens;
  52. /**
  53. * Opcode: continuation frame.
  54. *
  55. * @const int
  56. */
  57. const OPCODE_CONTINUATION_FRAME = 0x0;
  58. /**
  59. * Opcode: text frame.
  60. *
  61. * @const int
  62. */
  63. const OPCODE_TEXT_FRAME = 0x1;
  64. /**
  65. * Opcode: binary frame.
  66. *
  67. * @const int
  68. */
  69. const OPCODE_BINARY_FRAME = 0x2;
  70. /**
  71. * Opcode: connection close.
  72. *
  73. * @const int
  74. */
  75. const OPCODE_CONNECTION_CLOSE = 0x8;
  76. /**
  77. * Opcode: ping.
  78. *
  79. * @const int
  80. */
  81. const OPCODE_PING = 0x9;
  82. /**
  83. * Opcode: pong.
  84. *
  85. * @const int
  86. */
  87. const OPCODE_PONG = 0xa;
  88. /**
  89. * Close: normal.
  90. *
  91. * @const int
  92. */
  93. const CLOSE_NORMAL = 1000;
  94. /**
  95. * Close: going away.
  96. *
  97. * @const int
  98. */
  99. const CLOSE_GOING_AWAY = 1001;
  100. /**
  101. * Close: protocol error.
  102. *
  103. * @const int
  104. */
  105. const CLOSE_PROTOCOL_ERROR = 1002;
  106. /**
  107. * Close: data error.
  108. *
  109. * @const int
  110. */
  111. const CLOSE_DATA_ERROR = 1003;
  112. /**
  113. * Close: status error.
  114. *
  115. * @const int
  116. */
  117. const CLOSE_STATUS_ERROR = 1005;
  118. /**
  119. * Close: abnormal.
  120. *
  121. * @const int
  122. */
  123. const CLOSE_ABNORMAL = 1006;
  124. /**
  125. * Close: message error.
  126. *
  127. * @const int
  128. */
  129. const CLOSE_MESSAGE_ERROR = 1007;
  130. /**
  131. * Close: policy error.
  132. *
  133. * @const int
  134. */
  135. const CLOSE_POLICY_ERROR = 1008;
  136. /**
  137. * Close: message too big.
  138. *
  139. * @const int
  140. */
  141. const CLOSE_MESSAGE_TOO_BIG = 1009;
  142. /**
  143. * Close: extension missing.
  144. *
  145. * @const int
  146. */
  147. const CLOSE_EXTENSION_MISSING = 1010;
  148. /**
  149. * Close: server error.
  150. *
  151. * @const int
  152. */
  153. const CLOSE_SERVER_ERROR = 1011;
  154. /**
  155. * Close: TLS.
  156. *
  157. * @const int
  158. */
  159. const CLOSE_TLS = 1015;
  160. /**
  161. * Create a websocket connection.
  162. * 6 events can be listened: open, message, binary-message, ping, close and
  163. * error.
  164. *
  165. * @param \Hoa\Socket\Connection $connection Connection.
  166. * @throws \Hoa\Socket\Exception
  167. */
  168. public function __construct(HoaSocket\Connection $connection)
  169. {
  170. parent::__construct($connection);
  171. $this->getConnection()->setNodeName(Node::class);
  172. $this->setListener(
  173. new Event\Listener(
  174. $this,
  175. [
  176. 'open',
  177. 'message',
  178. 'binary-message',
  179. 'ping',
  180. 'close',
  181. 'error'
  182. ]
  183. )
  184. );
  185. return;
  186. }
  187. /**
  188. * Run a node.
  189. *
  190. * @param \Hoa\Socket\Node $node Node.
  191. * @return void
  192. */
  193. protected function _run(HoaSocket\Node $node)
  194. {
  195. try {
  196. if (FAILED === $node->getHandshake()) {
  197. $this->doHandshake();
  198. $this->getListener()->fire(
  199. 'open',
  200. new Event\Bucket()
  201. );
  202. return;
  203. }
  204. try {
  205. $frame = $node->getProtocolImplementation()->readFrame();
  206. } catch (Exception\CloseError $e) {
  207. $this->close($e->getErrorCode(), $e->getMessage());
  208. return;
  209. }
  210. if (false === $frame) {
  211. return;
  212. }
  213. if ($this instanceof Server &&
  214. isset($frame['mask']) &&
  215. 0x0 === $frame['mask']) {
  216. $this->close(
  217. self::CLOSE_MESSAGE_ERROR,
  218. 'All messages from the client must be masked.'
  219. );
  220. return;
  221. }
  222. $fromText = false;
  223. $fromBinary = false;
  224. switch ($frame['opcode']) {
  225. case self::OPCODE_BINARY_FRAME:
  226. $fromBinary = true;
  227. case self::OPCODE_TEXT_FRAME:
  228. if (0x1 === $frame['fin']) {
  229. if (0 < $node->getNumberOfFragments()) {
  230. $this->close(self::CLOSE_PROTOCOL_ERROR);
  231. break;
  232. }
  233. if (true === $fromBinary) {
  234. $fromBinary = false;
  235. try {
  236. $this->getListener()->fire(
  237. 'binary-message',
  238. new Event\Bucket([
  239. 'message' => $frame['message']
  240. ])
  241. );
  242. } catch (\Exception $e) {
  243. $this->getListener()->fire(
  244. 'error',
  245. new Event\Bucket([
  246. 'exception' => $e
  247. ])
  248. );
  249. }
  250. break;
  251. }
  252. if (false === (bool) preg_match('//u', $frame['message'])) {
  253. $this->close(self::CLOSE_MESSAGE_ERROR);
  254. break;
  255. }
  256. try {
  257. $this->getListener()->fire(
  258. 'message',
  259. new Event\Bucket([
  260. 'message' => $frame['message']
  261. ])
  262. );
  263. } catch (\Exception $e) {
  264. $this->getListener()->fire(
  265. 'error',
  266. new Event\Bucket([
  267. 'exception' => $e
  268. ])
  269. );
  270. }
  271. break;
  272. } else {
  273. $node->setComplete(false);
  274. }
  275. $fromText = true;
  276. case self::OPCODE_CONTINUATION_FRAME:
  277. if (false === $fromText) {
  278. if (0 === $node->getNumberOfFragments()) {
  279. $this->close(self::CLOSE_PROTOCOL_ERROR);
  280. break;
  281. }
  282. } else {
  283. $fromText = false;
  284. if (true === $fromBinary) {
  285. $node->setBinary(true);
  286. $fromBinary = false;
  287. }
  288. }
  289. $node->appendMessageFragment($frame['message']);
  290. if (0x1 === $frame['fin']) {
  291. $message = $node->getFragmentedMessage();
  292. $isBinary = $node->isBinary();
  293. $node->clearFragmentation();
  294. if (true === $isBinary) {
  295. try {
  296. $this->getListener()->fire(
  297. 'binary-message',
  298. new Event\Bucket([
  299. 'message' => $message
  300. ])
  301. );
  302. } catch (\Exception $e) {
  303. $this->getListener()->fire(
  304. 'error',
  305. new Event\Bucket([
  306. 'exception' => $e
  307. ])
  308. );
  309. }
  310. break;
  311. }
  312. if (false === (bool) preg_match('//u', $message)) {
  313. $this->close(self::CLOSE_MESSAGE_ERROR);
  314. break;
  315. }
  316. try {
  317. $this->getListener()->fire(
  318. 'message',
  319. new Event\Bucket([
  320. 'message' => $message
  321. ])
  322. );
  323. } catch (\Exception $e) {
  324. $this->getListener()->fire(
  325. 'error',
  326. new Event\Bucket([
  327. 'exception' => $e
  328. ])
  329. );
  330. }
  331. } else {
  332. $node->setComplete(false);
  333. }
  334. break;
  335. case self::OPCODE_PING:
  336. $message = &$frame['message'];
  337. if (0x0 === $frame['fin'] ||
  338. 0x7d < $frame['length']) {
  339. $this->close(self::CLOSE_PROTOCOL_ERROR);
  340. break;
  341. }
  342. $node
  343. ->getProtocolImplementation()
  344. ->writeFrame(
  345. $message,
  346. self::OPCODE_PONG,
  347. true
  348. );
  349. $this->getListener()->fire(
  350. 'ping',
  351. new Event\Bucket([
  352. 'message' => $message
  353. ])
  354. );
  355. break;
  356. case self::OPCODE_PONG:
  357. if (0x0 === $frame['fin']) {
  358. $this->close(self::CLOSE_PROTOCOL_ERROR);
  359. break;
  360. }
  361. break;
  362. case self::OPCODE_CONNECTION_CLOSE:
  363. $length = &$frame['length'];
  364. if (0x1 === $length ||
  365. 0x7d < $length) {
  366. $this->close(self::CLOSE_PROTOCOL_ERROR);
  367. break;
  368. }
  369. $code = self::CLOSE_NORMAL;
  370. $reason = null;
  371. if (0 < $length) {
  372. $message = &$frame['message'];
  373. $_code = unpack('nc', substr($message, 0, 2));
  374. $code = &$_code['c'];
  375. if (1000 > $code ||
  376. (1004 <= $code && $code <= 1006) ||
  377. (1012 <= $code && $code <= 1016) ||
  378. 5000 <= $code) {
  379. $this->close(self::CLOSE_PROTOCOL_ERROR);
  380. break;
  381. }
  382. if (2 < $length) {
  383. $reason = substr($message, 2);
  384. if (false === (bool) preg_match('//u', $reason)) {
  385. $this->close(self::CLOSE_MESSAGE_ERROR);
  386. break;
  387. }
  388. }
  389. }
  390. $this->close(self::CLOSE_NORMAL);
  391. $this->getListener()->fire(
  392. 'close',
  393. new Event\Bucket([
  394. 'code' => $code,
  395. 'reason' => $reason
  396. ])
  397. );
  398. break;
  399. default:
  400. $this->close(self::CLOSE_PROTOCOL_ERROR);
  401. }
  402. } catch (HoaException\Idle $e) {
  403. try {
  404. $this->close(self::CLOSE_SERVER_ERROR);
  405. $exception = $e;
  406. } catch (HoaException\Idle $ee) {
  407. $this->getConnection()->disconnect();
  408. $exception = new HoaException\Group(
  409. 'An exception has been thrown. We have tried to close ' .
  410. 'the connection but another exception has been thrown.',
  411. 42
  412. );
  413. $exception[] = $e;
  414. $exception[] = $ee;
  415. }
  416. $this->getListener()->fire(
  417. 'error',
  418. new Event\Bucket([
  419. 'exception' => $exception
  420. ])
  421. );
  422. }
  423. return;
  424. }
  425. /**
  426. * Try the handshake by trying different protocol implementation.
  427. *
  428. * @return void
  429. * @throws \Hoa\Websocket\Exception\BadProtocol
  430. */
  431. abstract protected function doHandshake();
  432. /**
  433. * Send a message.
  434. *
  435. * @param string $message Message.
  436. * @param \Hoa\Socket\Node $node Node.
  437. * @return \Closure
  438. */
  439. protected function _send($message, HoaSocket\Node $node)
  440. {
  441. $mustMask = $this instanceof Client;
  442. return function ($opcode, $end) use (&$message, $node, $mustMask) {
  443. if (false === $node->getHandshake()) {
  444. return;
  445. }
  446. return
  447. $node
  448. ->getProtocolImplementation()
  449. ->send($message, $opcode, $end, $mustMask);
  450. };
  451. }
  452. /**
  453. * Send a message to a specific node/connection.
  454. *
  455. * @param string $message Message.
  456. * @param \Hoa\Socket\Node $node Node (if null, current node).
  457. * @param int $opcode Opcode.
  458. * @param bool $end Whether it is the last frame of
  459. * the message.
  460. * @return void
  461. */
  462. public function send(
  463. $message,
  464. HoaSocket\Node $node = null,
  465. $opcode = self::OPCODE_TEXT_FRAME,
  466. $end = true
  467. ) {
  468. $send = parent::send($message, $node);
  469. if (null === $send) {
  470. return null;
  471. }
  472. return $send($opcode, $end);
  473. }
  474. /**
  475. * Close a specific node/connection.
  476. * It is just a “inline” method, a shortcut.
  477. *
  478. * @param int $code Code (please, see
  479. * self::CLOSE_* constants).
  480. * @param string $reason Reason.
  481. * @return void
  482. */
  483. public function close($code = self::CLOSE_NORMAL, $reason = null)
  484. {
  485. $connection = $this->getConnection();
  486. $protocol = $connection->getCurrentNode()->getProtocolImplementation();
  487. if (null !== $protocol) {
  488. $protocol->close($code, $reason);
  489. }
  490. return $connection->disconnect();
  491. }
  492. }