null , 'Hixie76' => null , 'RFC6455' => null ); protected $_mask_payload = false; public function __construct(ApplicationInterface $app) { $this->_app = $app; $this->_factory = new Factory; } /** * Return the desired socket configuration if hosting a WebSocket server * This method may be removed * @return array */ public static function getDefaultConfig() { return array( 'domain' => AF_INET , 'type' => SOCK_STREAM , 'protocol' => SOL_TCP , 'options' => array( SOL_SOCKET => array(SO_REUSEADDR => 1) ) ); } public function onOpen(Connection $conn) { $conn->WebSocket = new \stdClass; $conn->WebSocket->handshake = false; $conn->WebSocket->headers = ''; } /** * Do handshake, frame/unframe messages coming/going in stack * @todo This needs some major refactoring * @todo "Once the client's opening handshake has been sent, the client MUST wait for a response from the server before sending any further data." * @todo Change Header to be a class, not array|string - will make things SO much easier...right now can't do WAMP on Hixie */ public function onMessage(Connection $from, $msg) { if (true !== $from->WebSocket->handshake) { if (!isset($from->WebSocket->version)) { $from->WebSocket->headers .= $msg; if (!$this->isMessageComplete($from->WebSocket->headers)) { return; } $headers = RequestFactory::fromRequest($from->WebSocket->headers); $from->WebSocket->version = $this->getVersion($headers); $from->WebSocket->headers = $headers; } $response = $from->WebSocket->version->handshake($from->WebSocket->headers); $from->WebSocket->handshake = true; if (is_array($response)) { if ($this->_app instanceof WebSocketAppInterface) { // Note: this logic is wrong - we're supposed to check what the client sent // as its sub protocol and if we support any of the requested we send that back. // This is just sending what ever one wwe support // This will be changed when I rewrite how headers are handled // Also...what happens if something like a logger is put between this and the sub-protocol app? $response['Sec-WebSocket-Protocol'] = $this->_app->getSubProtocol(); } $header = ''; foreach ($response as $key => $val) { if (!empty($key)) { $header .= "{$key}: "; } $header .= "{$val}\r\n"; } $header .= "\r\n"; } else { $header = $response; } $comp = $this->_factory->newComposite(); $comp->enqueue($this->_factory->newCommand('SendMessage', $from)->setMessage($header)); $comp->enqueue($this->prepareCommand($this->_app->onOpen($from, $msg))); // Need to send headers/handshake to application, let it have the cookies, etc return $comp; } if (!isset($from->WebSocket->message)) { $from->WebSocket->message = $from->WebSocket->version->newMessage(); } // There is a frame fragment attatched to the connection, add to it if (!isset($from->WebSocket->frame)) { $from->WebSocket->frame = $from->WebSocket->version->newFrame(); } $from->WebSocket->frame->addBuffer($msg); if ($from->WebSocket->frame->isCoalesced()) { if ($from->WebSocket->frame->getOpcode() > 2) { throw new \UnexpectedValueException('Control frame support coming soon!'); } // Check frame // If is control frame, do your thing // Else, add to message // Control frames (ping, pong, close) can be sent in between a fragmented message $from->WebSocket->message->addFrame($from->WebSocket->frame); unset($from->WebSocket->frame); } if ($from->WebSocket->message->isCoalesced()) { $cmds = $this->prepareCommand($this->_app->onMessage($from, (string)$from->WebSocket->message)); unset($from->WebSocket->message); return $cmds; } } public function onClose(Connection $conn) { return $this->prepareCommand($this->_app->onClose($conn)); } /** * @todo Shouldn't I be using prepareCommand() on the return? look into this */ public function onError(Connection $conn, \Exception $e) { return $this->_app->onError($conn, $e); } /** * Incomplete, WebSocket protocol allows client to ask to use a sub-protocol, I'm thinking/wanting to somehow implement this in an application decorated class * @param string * @todo Implement or delete... */ public function setSubProtocol($name) { } /** * Checks if a return Command from your application is a message, if so encode it/them * @param Ratchet\Resource\Command\CommandInterface|NULL * @return Ratchet\Resource\Command\CommandInterface|NULL */ protected function prepareCommand(CommandInterface $command = null) { $cache = array(); return $this->mungCommand($command, $cache); } /** * Does the actual work of prepareCommand * Separated to pass the cache array by reference, so we're not framing the same stirng over and over * @param Ratchet\Resource\Command\CommandInterface|NULL * @param array * @return Ratchet\Resource\Command\CommandInterface|NULL */ protected function mungCommand(CommandInterface $command = null, &$cache) { if ($command instanceof SendMessage) { if (!isset($command->getConnection()->WebSocket->version)) { // Client could close connection before handshake complete or invalid handshake return $command; } $version = $command->getConnection()->WebSocket->version; $hash = md5($command->getMessage()) . '-' . spl_object_hash($version); if (!isset($cache[$hash])) { $cache[$hash] = $version->frame($command->getMessage(), $this->_mask_payload); } return $command->setMessage($cache[$hash]); } if ($command instanceof \Traversable) { foreach ($command as $cmd) { $cmd = $this->mungCommand($cmd, $cache); } } return $command; } /** * Detect the WebSocket protocol version a client is using based on the HTTP header request * @param string HTTP handshake request * @return Version\VersionInterface * @throws UnderFlowException If we think the entire header message hasn't been buffered yet * @throws InvalidArgumentException If we can't understand protocol version request * @todo Verify the first line of the HTTP header as per page 16 of RFC 6455 */ protected function getVersion(RequestInterface $request) { foreach ($this->_versions as $name => $instance) { if (null !== $instance) { if ($instance::isProtocol($request)) { return $instance; } } else { $ns = __NAMESPACE__ . "\\Version\\{$name}"; if ($ns::isProtocol($request)) { $this->_versions[$name] = new $ns; return $this->_versions[$name]; } } } throw new \InvalidArgumentException('Could not identify WebSocket protocol'); } /** * @param string * @return bool * @todo Abstract, some hard coding done for (stupid) Hixie protocol */ protected function isMessageComplete($message) { static $crlf = "\r\n\r\n"; $headers = (boolean)strstr($message, $crlf); if (!$headers) { return false; } if (strstr($message, 'Sec-WebSocket-Key2')) { if (8 !== strlen(substr($message, strpos($message, $crlf) + strlen($crlf)))) { return false; } } return true; } /** * Disable a version of the WebSocket protocol *cough*Hixie76*cough* * @param string The name of the version to disable * @throws InvalidArgumentException If the given version does not exist */ public function disableVersion($name) { if (!array_key_exists($name, $this->_versions)) { throw new \InvalidArgumentException("Version {$name} not found"); } unset($this->_versions[$name]); } /** * Set the option to mask the payload upon sending to client * If WebSocket is used as server, this should be false, client to true * @param bool * @todo User shouldn't have to know/set this, need to figure out how to do this automatically */ public function setMaskPayload($opt) { $this->_mask_payload = (boolean)$opt; } }