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(); } }