| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583 |
- <?php
-
- /**
- * Hoa
- *
- *
- * @license
- *
- * New BSD License
- *
- * Copyright © 2007-2017, Hoa community. All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright
- * notice, this list of conditions and the following disclaimer in the
- * documentation and/or other materials provided with the distribution.
- * * Neither the name of the Hoa nor the names of its contributors may be
- * used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-
- namespace Hoa\Websocket;
-
- use Hoa\Event;
- use Hoa\Exception as HoaException;
- use Hoa\Socket as HoaSocket;
-
- /**
- * Class \Hoa\Websocket\Connection.
- *
- * A cross-protocol Websocket connection.
- *
- * @copyright Copyright © 2007-2017 Hoa community
- * @license New BSD License
- */
- abstract class Connection
- extends HoaSocket\Connection\Handler
- implements Event\Listenable
- {
- use Event\Listens;
-
- /**
- * Opcode: continuation frame.
- *
- * @const int
- */
- const OPCODE_CONTINUATION_FRAME = 0x0;
-
- /**
- * Opcode: text frame.
- *
- * @const int
- */
- const OPCODE_TEXT_FRAME = 0x1;
-
- /**
- * Opcode: binary frame.
- *
- * @const int
- */
- const OPCODE_BINARY_FRAME = 0x2;
-
- /**
- * Opcode: connection close.
- *
- * @const int
- */
- const OPCODE_CONNECTION_CLOSE = 0x8;
-
- /**
- * Opcode: ping.
- *
- * @const int
- */
- const OPCODE_PING = 0x9;
-
- /**
- * Opcode: pong.
- *
- * @const int
- */
- const OPCODE_PONG = 0xa;
-
- /**
- * Close: normal.
- *
- * @const int
- */
- const CLOSE_NORMAL = 1000;
-
- /**
- * Close: going away.
- *
- * @const int
- */
- const CLOSE_GOING_AWAY = 1001;
-
- /**
- * Close: protocol error.
- *
- * @const int
- */
- const CLOSE_PROTOCOL_ERROR = 1002;
-
- /**
- * Close: data error.
- *
- * @const int
- */
- const CLOSE_DATA_ERROR = 1003;
-
- /**
- * Close: status error.
- *
- * @const int
- */
- const CLOSE_STATUS_ERROR = 1005;
-
- /**
- * Close: abnormal.
- *
- * @const int
- */
- const CLOSE_ABNORMAL = 1006;
-
- /**
- * Close: message error.
- *
- * @const int
- */
- const CLOSE_MESSAGE_ERROR = 1007;
-
- /**
- * Close: policy error.
- *
- * @const int
- */
- const CLOSE_POLICY_ERROR = 1008;
-
- /**
- * Close: message too big.
- *
- * @const int
- */
- const CLOSE_MESSAGE_TOO_BIG = 1009;
-
- /**
- * Close: extension missing.
- *
- * @const int
- */
- const CLOSE_EXTENSION_MISSING = 1010;
-
- /**
- * Close: server error.
- *
- * @const int
- */
- const CLOSE_SERVER_ERROR = 1011;
-
- /**
- * Close: TLS.
- *
- * @const int
- */
- const CLOSE_TLS = 1015;
-
-
-
- /**
- * Create a websocket connection.
- * 6 events can be listened: open, message, binary-message, ping, close and
- * error.
- *
- * @param \Hoa\Socket\Connection $connection Connection.
- * @throws \Hoa\Socket\Exception
- */
- public function __construct(HoaSocket\Connection $connection)
- {
- parent::__construct($connection);
- $this->getConnection()->setNodeName(Node::class);
- $this->setListener(
- new Event\Listener(
- $this,
- [
- 'open',
- 'message',
- 'binary-message',
- 'ping',
- 'close',
- 'error'
- ]
- )
- );
-
- return;
- }
-
- /**
- * Run a node.
- *
- * @param \Hoa\Socket\Node $node Node.
- * @return void
- */
- protected function _run(HoaSocket\Node $node)
- {
- try {
- if (FAILED === $node->getHandshake()) {
- $this->doHandshake();
- $this->getListener()->fire(
- 'open',
- new Event\Bucket()
- );
-
- return;
- }
-
- try {
- $frame = $node->getProtocolImplementation()->readFrame();
- } catch (Exception\CloseError $e) {
- $this->close($e->getErrorCode(), $e->getMessage());
-
- return;
- }
-
- if (false === $frame) {
- return;
- }
-
- if ($this instanceof Server &&
- isset($frame['mask']) &&
- 0x0 === $frame['mask']) {
- $this->close(
- self::CLOSE_MESSAGE_ERROR,
- 'All messages from the client must be masked.'
- );
-
- return;
- }
-
- $fromText = false;
- $fromBinary = false;
-
- switch ($frame['opcode']) {
- case self::OPCODE_BINARY_FRAME:
- $fromBinary = true;
-
- case self::OPCODE_TEXT_FRAME:
- if (0x1 === $frame['fin']) {
- if (0 < $node->getNumberOfFragments()) {
- $this->close(self::CLOSE_PROTOCOL_ERROR);
-
- break;
- }
-
- if (true === $fromBinary) {
- $fromBinary = false;
-
- try {
- $this->getListener()->fire(
- 'binary-message',
- new Event\Bucket([
- 'message' => $frame['message']
- ])
- );
- } catch (\Exception $e) {
- $this->getListener()->fire(
- 'error',
- new Event\Bucket([
- 'exception' => $e
- ])
- );
- }
-
- break;
- }
-
- if (false === (bool) preg_match('//u', $frame['message'])) {
- $this->close(self::CLOSE_MESSAGE_ERROR);
-
- break;
- }
-
- try {
- $this->getListener()->fire(
- 'message',
- new Event\Bucket([
- 'message' => $frame['message']
- ])
- );
- } catch (\Exception $e) {
- $this->getListener()->fire(
- 'error',
- new Event\Bucket([
- 'exception' => $e
- ])
- );
- }
-
- break;
- } else {
- $node->setComplete(false);
- }
-
- $fromText = true;
-
- case self::OPCODE_CONTINUATION_FRAME:
- if (false === $fromText) {
- if (0 === $node->getNumberOfFragments()) {
- $this->close(self::CLOSE_PROTOCOL_ERROR);
-
- break;
- }
- } else {
- $fromText = false;
-
- if (true === $fromBinary) {
- $node->setBinary(true);
- $fromBinary = false;
- }
- }
-
- $node->appendMessageFragment($frame['message']);
-
- if (0x1 === $frame['fin']) {
- $message = $node->getFragmentedMessage();
- $isBinary = $node->isBinary();
- $node->clearFragmentation();
-
- if (true === $isBinary) {
- try {
- $this->getListener()->fire(
- 'binary-message',
- new Event\Bucket([
- 'message' => $message
- ])
- );
- } catch (\Exception $e) {
- $this->getListener()->fire(
- 'error',
- new Event\Bucket([
- 'exception' => $e
- ])
- );
- }
-
- break;
- }
-
- if (false === (bool) preg_match('//u', $message)) {
- $this->close(self::CLOSE_MESSAGE_ERROR);
-
- break;
- }
-
- try {
- $this->getListener()->fire(
- 'message',
- new Event\Bucket([
- 'message' => $message
- ])
- );
- } catch (\Exception $e) {
- $this->getListener()->fire(
- 'error',
- new Event\Bucket([
- 'exception' => $e
- ])
- );
- }
- } else {
- $node->setComplete(false);
- }
-
- break;
-
- case self::OPCODE_PING:
- $message = &$frame['message'];
-
- if (0x0 === $frame['fin'] ||
- 0x7d < $frame['length']) {
- $this->close(self::CLOSE_PROTOCOL_ERROR);
-
- break;
- }
-
- $node
- ->getProtocolImplementation()
- ->writeFrame(
- $message,
- self::OPCODE_PONG,
- true
- );
-
- $this->getListener()->fire(
- 'ping',
- new Event\Bucket([
- 'message' => $message
- ])
- );
-
- break;
-
- case self::OPCODE_PONG:
- if (0x0 === $frame['fin']) {
- $this->close(self::CLOSE_PROTOCOL_ERROR);
-
- break;
- }
-
- break;
-
- case self::OPCODE_CONNECTION_CLOSE:
- $length = &$frame['length'];
-
- if (0x1 === $length ||
- 0x7d < $length) {
- $this->close(self::CLOSE_PROTOCOL_ERROR);
-
- break;
- }
-
- $code = self::CLOSE_NORMAL;
- $reason = null;
-
- if (0 < $length) {
- $message = &$frame['message'];
- $_code = unpack('nc', substr($message, 0, 2));
- $code = &$_code['c'];
-
- if (1000 > $code ||
- (1004 <= $code && $code <= 1006) ||
- (1012 <= $code && $code <= 1016) ||
- 5000 <= $code) {
- $this->close(self::CLOSE_PROTOCOL_ERROR);
-
- break;
- }
-
- if (2 < $length) {
- $reason = substr($message, 2);
-
- if (false === (bool) preg_match('//u', $reason)) {
- $this->close(self::CLOSE_MESSAGE_ERROR);
-
- break;
- }
- }
- }
-
- $this->close(self::CLOSE_NORMAL);
- $this->getListener()->fire(
- 'close',
- new Event\Bucket([
- 'code' => $code,
- 'reason' => $reason
- ])
- );
-
- break;
-
- default:
- $this->close(self::CLOSE_PROTOCOL_ERROR);
- }
- } catch (HoaException\Idle $e) {
- try {
- $this->close(self::CLOSE_SERVER_ERROR);
- $exception = $e;
- } catch (HoaException\Idle $ee) {
- $this->getConnection()->disconnect();
- $exception = new HoaException\Group(
- 'An exception has been thrown. We have tried to close ' .
- 'the connection but another exception has been thrown.',
- 42
- );
- $exception[] = $e;
- $exception[] = $ee;
- }
-
- $this->getListener()->fire(
- 'error',
- new Event\Bucket([
- 'exception' => $exception
- ])
- );
- }
-
- return;
- }
-
- /**
- * Try the handshake by trying different protocol implementation.
- *
- * @return void
- * @throws \Hoa\Websocket\Exception\BadProtocol
- */
- abstract protected function doHandshake();
-
- /**
- * Send a message.
- *
- * @param string $message Message.
- * @param \Hoa\Socket\Node $node Node.
- * @return \Closure
- */
- protected function _send($message, HoaSocket\Node $node)
- {
- $mustMask = $this instanceof Client;
-
- return function ($opcode, $end) use (&$message, $node, $mustMask) {
- if (false === $node->getHandshake()) {
- return;
- }
-
- return
- $node
- ->getProtocolImplementation()
- ->send($message, $opcode, $end, $mustMask);
- };
- }
-
- /**
- * Send a message to a specific node/connection.
- *
- * @param string $message Message.
- * @param \Hoa\Socket\Node $node Node (if null, current node).
- * @param int $opcode Opcode.
- * @param bool $end Whether it is the last frame of
- * the message.
- * @return void
- */
- public function send(
- $message,
- HoaSocket\Node $node = null,
- $opcode = self::OPCODE_TEXT_FRAME,
- $end = true
- ) {
- $send = parent::send($message, $node);
-
- if (null === $send) {
- return null;
- }
-
- return $send($opcode, $end);
- }
-
- /**
- * Close a specific node/connection.
- * It is just a “inline” method, a shortcut.
- *
- * @param int $code Code (please, see
- * self::CLOSE_* constants).
- * @param string $reason Reason.
- * @return void
- */
- public function close($code = self::CLOSE_NORMAL, $reason = null)
- {
- $connection = $this->getConnection();
- $protocol = $connection->getCurrentNode()->getProtocolImplementation();
-
- if (null !== $protocol) {
- $protocol->close($code, $reason);
- }
-
- return $connection->disconnect();
- }
- }
|