Rfc6455.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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\Protocol;
  36. use Hoa\Http;
  37. use Hoa\Websocket;
  38. /**
  39. * Class \Hoa\Websocket\Protocol\Rfc6455.
  40. *
  41. * Protocol implementation: RFC6455.
  42. *
  43. * @copyright Copyright © 2007-2017 Hoa community
  44. * @license New BSD License
  45. */
  46. class Rfc6455 extends Generic
  47. {
  48. /**
  49. * GUID.
  50. *
  51. * @const string
  52. */
  53. const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  54. /**
  55. * Do the handshake.
  56. *
  57. * @param \Hoa\Http\Request $request Request.
  58. * @return void
  59. * @throws \Hoa\Websocket\Exception\BadProtocol
  60. */
  61. public function doHandshake(Http\Request $request)
  62. {
  63. if (!isset($request['sec-websocket-key'])) {
  64. throw new Websocket\Exception\BadProtocol(
  65. 'Bad protocol implementation: it is not RFC6455.',
  66. 0
  67. );
  68. }
  69. $key = $request['sec-websocket-key'];
  70. if (0 === preg_match('#^[+/0-9A-Za-z]{21}[AQgw]==$#', $key) ||
  71. 16 !== strlen(base64_decode($key))) {
  72. throw new Websocket\Exception\BadProtocol(
  73. 'Header Sec-WebSocket-Key: %s is illegal.',
  74. 1,
  75. $key
  76. );
  77. }
  78. $response = base64_encode(sha1($key . static::GUID, true));
  79. /**
  80. * @TODO
  81. * • Origin;
  82. * • Sec-WebSocket-Protocol;
  83. * • Sec-WebSocket-Extensions.
  84. */
  85. $connection = $this->getConnection();
  86. $connection->writeAll(
  87. 'HTTP/1.1 101 Switching Protocols' . "\r\n" .
  88. 'Upgrade: websocket' . "\r\n" .
  89. 'Connection: Upgrade' . "\r\n" .
  90. 'Sec-WebSocket-Accept: ' . $response . "\r\n" .
  91. 'Sec-WebSocket-Version: 13' . "\r\n\r\n"
  92. );
  93. $connection->getCurrentNode()->setHandshake(SUCCEED);
  94. return;
  95. }
  96. /**
  97. * Read a frame.
  98. *
  99. * @return array
  100. * @throws \Hoa\Websocket\Exception\CloseError
  101. */
  102. public function readFrame()
  103. {
  104. $connection = $this->getConnection();
  105. $out = [];
  106. $read = $connection->read(1);
  107. if (empty($read)) {
  108. $out['opcode'] = Websocket\Connection::OPCODE_CONNECTION_CLOSE;
  109. return $out;
  110. }
  111. $handle = ord($read);
  112. $out['fin'] = ($handle >> 7) & 0x1;
  113. $out['rsv1'] = ($handle >> 6) & 0x1;
  114. $out['rsv2'] = ($handle >> 5) & 0x1;
  115. $out['rsv3'] = ($handle >> 4) & 0x1;
  116. $out['opcode'] = $handle & 0xf;
  117. $handle = ord($connection->read(1));
  118. $out['mask'] = ($handle >> 7) & 0x1;
  119. $out['length'] = $handle & 0x7f;
  120. $length = &$out['length'];
  121. if (0x0 !== $out['rsv1'] || 0x0 !== $out['rsv2'] || 0x0 !== $out['rsv3']) {
  122. $exception = new Websocket\Exception\CloseError(
  123. 'Get rsv1: %s, rsv2: %s, rsv3: %s, they all must be equal to 0.',
  124. 2,
  125. [$out['rsv1'], $out['rsv2'], $out['rsv3']]
  126. );
  127. $exception->setErrorCode(
  128. Websocket\Connection::CLOSE_PROTOCOL_ERROR
  129. );
  130. throw $exception;
  131. }
  132. if (0 === $length) {
  133. $out['message'] = '';
  134. // Consume the whole frame.
  135. if (0x1 === $out['mask']) {
  136. $connection->read(4);
  137. }
  138. return $out;
  139. } elseif (0x7e === $length) {
  140. $handle = unpack('nl', $connection->read(2));
  141. $length = $handle['l'];
  142. } elseif (0x7f === $length) {
  143. $handle = unpack('N*l', $connection->read(8));
  144. $length = $handle['l2'];
  145. if ($length > 0x7fffffffffffffff) {
  146. $exception = new Websocket\Exception\CloseError(
  147. 'Message is too long.',
  148. 3
  149. );
  150. $exception->setErrorCode(
  151. Websocket\Connection::CLOSE_MESSAGE_TOO_BIG
  152. );
  153. throw $exception;
  154. }
  155. }
  156. if (0x0 === $out['mask']) {
  157. $out['message'] = $connection->read($length);
  158. return $out;
  159. }
  160. $maskN = array_map('ord', str_split($connection->read(4)));
  161. $maskC = 0;
  162. if (4 !== count($maskN)) {
  163. $exception = new Websocket\Exception\CloseError(
  164. 'Mask is not well-formed (too short).',
  165. 4
  166. );
  167. $exception->setErrorCode(
  168. Websocket\Connection::CLOSE_PROTOCOL_ERROR
  169. );
  170. throw $exception;
  171. }
  172. $buffer = 0;
  173. $bufferLength = 3000;
  174. $message = null;
  175. for ($i = 0; $i < $length; $i += $bufferLength) {
  176. $buffer = min($bufferLength, $length - $i);
  177. $handle = $connection->read($buffer);
  178. for ($j = 0, $_length = strlen($handle); $j < $_length; ++$j) {
  179. $handle[$j] = chr(ord($handle[$j]) ^ $maskN[$maskC]);
  180. $maskC = ($maskC + 1) % 4;
  181. }
  182. $message .= $handle;
  183. }
  184. $out['message'] = $message;
  185. return $out;
  186. }
  187. /**
  188. * Write a frame.
  189. *
  190. * @param string $message Message.
  191. * @param int $opcode Opcode.
  192. * @param bool $end Whether it is the last frame of the message.
  193. * @param bool $mask Whether the message will be masked or not.
  194. * @return int
  195. */
  196. public function writeFrame(
  197. $message,
  198. $opcode = Websocket\Connection::OPCODE_TEXT_FRAME,
  199. $end = true,
  200. $mask = false
  201. ) {
  202. $fin = true === $end ? 0x1 : 0x0;
  203. $rsv1 = 0x0;
  204. $rsv2 = 0x0;
  205. $rsv3 = 0x0;
  206. $mask = true === $mask ? 0x1 : 0x0;
  207. $length = strlen($message);
  208. $out = chr(
  209. ($fin << 7)
  210. | ($rsv1 << 6)
  211. | ($rsv2 << 5)
  212. | ($rsv3 << 4)
  213. | $opcode
  214. );
  215. if (0xffff < $length) {
  216. $out .= chr(($mask << 7) | 0x7f) . pack('NN', 0, $length);
  217. } elseif (0x7d < $length) {
  218. $out .= chr(($mask << 7) | 0x7e) . pack('n', $length);
  219. } else {
  220. $out .= chr(($mask << 7) | $length);
  221. }
  222. if (0x0 === $mask) {
  223. $out .= $message;
  224. } else {
  225. $maskingKey = $this->getMaskingKey();
  226. for ($i = 0, $max = strlen($message); $i < $max; ++$i) {
  227. $message[$i] = chr(ord($message[$i]) ^ $maskingKey[$i % 4]);
  228. }
  229. $out .=
  230. implode('', array_map('chr', $maskingKey)) .
  231. $message;
  232. }
  233. return $this->getConnection()->writeAll($out);
  234. }
  235. /**
  236. * Get a random masking key.
  237. *
  238. * @return array
  239. */
  240. public function getMaskingKey()
  241. {
  242. if (true === function_exists('openssl_random_pseudo_bytes')) {
  243. $maskingKey = array_map(
  244. 'ord',
  245. str_split(
  246. openssl_random_pseudo_bytes(4)
  247. )
  248. );
  249. } else {
  250. $maskingKey = [];
  251. for ($i = 0; $i < 4; ++$i) {
  252. $maskingKey[] = mt_rand(1, 255);
  253. }
  254. }
  255. return $maskingKey;
  256. }
  257. /**
  258. * Send a message.
  259. *
  260. * @param string $message Message.
  261. * @param int $opcode Opcode.
  262. * @param bool $end Whether it is the last frame of
  263. * the message.
  264. * @param bool $mask Whether the message will be masked or not.
  265. * @return void
  266. * @throws \Hoa\Websocket\Exception\InvalidMessage
  267. */
  268. public function send(
  269. $message,
  270. $opcode = Websocket\Connection::OPCODE_TEXT_FRAME,
  271. $end = true,
  272. $mask = false
  273. ) {
  274. if (Websocket\Connection::OPCODE_TEXT_FRAME === $opcode &&
  275. true === $end &&
  276. false === (bool) preg_match('//u', $message)) {
  277. throw new Websocket\Exception\InvalidMessage(
  278. 'Message “%s” is not in UTF-8, cannot send it.',
  279. 5,
  280. 32 > strlen($message)
  281. ? substr($message, 0, 32) . '…'
  282. : $message
  283. );
  284. }
  285. $this->writeFrame($message, $opcode, $end, $mask);
  286. return;
  287. }
  288. /**
  289. * Close a connection.
  290. *
  291. * @param int $code Code (please, see
  292. * \Hoa\Websocket\Connection::CLOSE_*
  293. * constants).
  294. * @param string $reason Reason.
  295. * @param bool $mask Whether the message will be masked or not.
  296. * @return void
  297. */
  298. public function close(
  299. $code = Websocket\Connection::CLOSE_NORMAL,
  300. $reason = null,
  301. $mask = false
  302. ) {
  303. $this->writeFrame(
  304. pack('n', $code) . $reason,
  305. Websocket\Connection::OPCODE_CONNECTION_CLOSE,
  306. true,
  307. $mask
  308. );
  309. return;
  310. }
  311. }