From 1be159f9faaf5520bf229e3b9f817e6ffe8b6151 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 19 May 2012 23:36:32 -0400 Subject: [PATCH 01/24] [WebSockets] Handshake encoding + case insensitivity Updated RFC6455 handshaker to check values case insensitively Made sure RFC6455 handshaker matches encoding properly Added mbstring as a requirement for Ratchet Refs #28, #30 --- Version/FrameInterface.php | 6 ------ Version/RFC6455/HandshakeVerifier.php | 14 ++++++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Version/FrameInterface.php b/Version/FrameInterface.php index 57b27ea..61451cc 100644 --- a/Version/FrameInterface.php +++ b/Version/FrameInterface.php @@ -2,12 +2,6 @@ namespace Ratchet\WebSocket\Version; interface FrameInterface { - /** - * Dunno if I'll use this - * Thinking could be used if a control frame? - */ -// function __invoke(); - /** * @return bool */ diff --git a/Version/RFC6455/HandshakeVerifier.php b/Version/RFC6455/HandshakeVerifier.php index 6898e2e..e9172ef 100644 --- a/Version/RFC6455/HandshakeVerifier.php +++ b/Version/RFC6455/HandshakeVerifier.php @@ -32,10 +32,9 @@ class HandshakeVerifier { * Test the HTTP method. MUST be "GET" * @param string * @return bool - * @todo Look into STD if "get" is valid (am I supposed to do case conversion?) */ public function verifyMethod($val) { - return ('GET' === $val); + return ('get' === mb_strtolower($val, 'ASCII')); } /** @@ -50,7 +49,6 @@ class HandshakeVerifier { /** * @param string * @return bool - * @todo Verify the logic here is correct */ public function verifyRequestURI($val) { if ($val[0] != '/') { @@ -80,7 +78,7 @@ class HandshakeVerifier { * @return bool */ public function verifyUpgradeRequest($val) { - return ('websocket' === $val); + return ('websocket' === mb_strtolower($val, 'ASCII')); } /** @@ -89,12 +87,16 @@ class HandshakeVerifier { * @return bool */ public function verifyConnection($val) { - if ('Upgrade' === $val) { + $val = mb_strtolower($val, 'ASCII'); + + if ('upgrade' === $val) { return true; } + // todo change this to mb_eregi_replace $vals = explode(',', str_replace(', ', ',', $val)); - return (false !== array_search('Upgrade', $vals)); + + return (false !== array_search('upgrade', $vals)); } /** From ef995377d1c6633b955cfacd5574d4229c58396e Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 19 May 2012 23:43:30 -0400 Subject: [PATCH 02/24] [WebSocket] Refactoring Separated handshake negotiation into its own class `HandshakeNegotiator` deals with Request/Response classes These changes are geared towards separate responsibility Refs #29 --- HandshakeNegotiator.php | 140 ++++++++++++++++++++++++++++++++ Version/Hixie76.php | 7 +- Version/HyBi10.php | 6 +- Version/RFC6455.php | 11 ++- Version/VersionInterface.php | 16 ++-- WsConnection.php | 38 ++++++++- WsServer.php | 149 ++++++++++++----------------------- 7 files changed, 252 insertions(+), 115 deletions(-) create mode 100644 HandshakeNegotiator.php diff --git a/HandshakeNegotiator.php b/HandshakeNegotiator.php new file mode 100644 index 0000000..3f19bf1 --- /dev/null +++ b/HandshakeNegotiator.php @@ -0,0 +1,140 @@ +enableVersion(new Version\RFC6455); + $this->enableVersion(new Version\HyBi10); + $this->enableVersion(new Version\Hixie76); + } + } + + /** + * @param WsConnection + */ + public function onOpen(WsConnection $conn) { + $conn->WebSocket->handshakeBuffer = ''; + } + + /** + * @param WsConnection + * @param string Data stream to buffer + * @return Guzzle\Http\Message\Response|null Response object if it's done parsing, null if there's more to be buffered + * @throws HttpException + */ + public function onData(WsConnection $conn, $data) { + $conn->WebSocket->handshakeBuffer .= $data; + + if (mb_strlen($conn->WebSocket->handshakeBuffer, '8bit') >= (int)$this->maxSize) { + return new Response(413, array('X-Powered-By' => \Ratchet\VERSION)); + } + + if ($this->isEom($conn->WebSocket->handshakeBuffer)) { + $conn->WebSocket->request = RequestFactory::getInstance()->fromMessage($conn->WebSocket->handshakeBuffer); + + if (null === ($version = $this->getVersion($conn->WebSocket->request))) { + return new Response(400, array( + 'Sec-WebSocket-Version' => $this->getSupportedVersionString() + , 'X-Powered-By' => \Ratchet\VERSION + )); + } + + // TODO: confirm message is buffered + // Hixie requires the body to complete the handshake (6 characters long) + // Update VersionInterface to check for this, ::canHandshake() maybe + // return if can't, continue buffering + + $response = $version->handshake($conn->WebSocket->request); + $response->setHeader('X-Powered-By', \Ratchet\VERSION); + + $conn->setVersion($version); + unset($conn->WebSocket->handshakeBuffer); + + return $response; + } + } + + /** + * Determine if the message has been buffered as per the HTTP specification + * @param string + * @return boolean + */ + public function isEom($message) { + return (static::EOM === substr($message, 0 - strlen(static::EOM))); + } + + /** + * Get the protocol negotiator for the request, if supported + * @param Guzzle\Http\Message\RequestInterface + * @return Ratchet\WebSocket\Version\VersionInterface + */ + public function getVersion(RequestInterface $request) { + foreach ($this->versions as $version) { + if ($version->isProtocol($request)) { + return $version; + } + } + } + + /** + * Enable support for a specific version of the WebSocket protocol + * @param Ratchet\WebSocket\Vesion\VersionInterface + * @return HandshakeNegotiator + */ + public function enableVersion(VersionInterface $version) { + $this->versions[$version->getVersionNumber()] = $version; + + if (empty($this->versionString)) { + $this->versionString = (string)$version->getVersionNumber(); + } else { + $this->versionString .= ", {$version->getVersionNumber()}"; + } + + return $this; + } + + /** + * Disable support for a specific WebSocket protocol version + * @param int The version ID to un-support + * @return HandshakeNegotiator + */ + public function disableVersion($versionId) { + unset($this->versions[$versionId]); + + $this->versionString = ''; + + foreach ($this->versions as $id => $object) { + $this->versionString .= "{$id}, "; + } + $this->versionString = substr($this->versionString, 0, -2); + + return $this; + } + + /** + * Get a string of version numbers supported (comma delimited) + * @return string + */ + public function getSupportedVersionString() { + return $this->versionString; + } +} \ No newline at end of file diff --git a/Version/Hixie76.php b/Version/Hixie76.php index 941eec4..1692eb0 100644 --- a/Version/Hixie76.php +++ b/Version/Hixie76.php @@ -12,17 +12,20 @@ use Guzzle\Http\Message\Response; * man-in-the-middle attack on 10%-15% of the people who saw their ad who had a browser (currently only Safari) supporting the Hixie76 protocol. * This was exploited by taking advantage of proxy servers in front of the user who ignored some HTTP headers in the handshake * The Hixie76 is currently implemented by Safari - * Handshake from Andrea Giammarchi (http://webreflection.blogspot.com/2010/06/websocket-handshake-76-simplified.html) * @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 */ class Hixie76 implements VersionInterface { /** * {@inheritdoc} */ - public static function isProtocol(RequestInterface $request) { + public function isProtocol(RequestInterface $request) { return !(null === $request->getHeader('Sec-WebSocket-Key2', true)); } + public function getVersionNumber() { + return 0; + } + /** * @param Guzzle\Http\Message\RequestInterface * @return Guzzle\Http\Message\Response diff --git a/Version/HyBi10.php b/Version/HyBi10.php index 734d7e4..bbf3dc4 100644 --- a/Version/HyBi10.php +++ b/Version/HyBi10.php @@ -3,11 +3,15 @@ namespace Ratchet\WebSocket\Version; use Guzzle\Http\Message\RequestInterface; class HyBi10 extends RFC6455 { - public static function isProtocol(RequestInterface $request) { + public function isProtocol(RequestInterface $request) { $version = (int)$request->getHeader('Sec-WebSocket-Version', -1); return ($version >= 6 && $version < 13); } + public function getVersionNumber() { + return 6; + } + /** * @return HyBi10\Message * / diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 50d7632..1be2246 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -23,9 +23,14 @@ class RFC6455 implements VersionInterface { /** * {@inheritdoc} */ - public static function isProtocol(RequestInterface $request) { + public function isProtocol(RequestInterface $request) { $version = (int)$request->getHeader('Sec-WebSocket-Version', -1); - return (13 === $version); + + return ($this->getVersionNumber() === $version); + } + + public function getVersionNumber() { + return 13; } /** @@ -43,7 +48,7 @@ class RFC6455 implements VersionInterface { , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) ); - return new Response('101', $headers); + return new Response(101, $headers); } /** diff --git a/Version/VersionInterface.php b/Version/VersionInterface.php index 1602641..d0839b9 100644 --- a/Version/VersionInterface.php +++ b/Version/VersionInterface.php @@ -3,10 +3,7 @@ namespace Ratchet\WebSocket\Version; use Guzzle\Http\Message\RequestInterface; /** - * Despite the version iterations of WebInterface the actions they go through are similar - * This standardizes how the server handles communication with each protocol version - * @todo Need better naming conventions...newMessage and newFrame are for reading incoming framed messages (action is unframing) - * The current method names suggest you could create a new message/frame to send, which they can not do + * A standard interface for interacting with the various version of the WebSocket protocol */ interface VersionInterface { /** @@ -15,15 +12,20 @@ interface VersionInterface { * @return bool * @throws UnderflowException If the protocol thinks the headers are still fragmented */ - static function isProtocol(RequestInterface $request); + function isProtocol(RequestInterface $request); + + /** + * Although the version has a name associated with it the integer returned is the proper identification + * @return int + */ + function getVersionNumber(); /** * Perform the handshake and return the response headers * @param Guzzle\Http\Message\RequestInterface - * @return array|string + * @return Guzzle\Http\Message\Response * @throws InvalidArgumentException If the HTTP handshake is mal-formed * @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version) - * @todo Change param to accept a Guzzle RequestInterface object */ function handshake(RequestInterface $request); diff --git a/WsConnection.php b/WsConnection.php index c0f127b..a346fa9 100644 --- a/WsConnection.php +++ b/WsConnection.php @@ -1,16 +1,30 @@ WebSocket->version->frame($data, false); + public function __construct(ConnectionInterface $conn) { + parent::__construct($conn); + + $this->WebSocket = new \StdClass; + } + + public function send($data) { + if ($this->hasVersion()) { + // need frame caching + $data = $this->WebSocket->version->frame($data, false); + } $this->getConnection()->send($data); } @@ -30,4 +44,22 @@ class WsConnection extends AbstractConnectionDecorator { public function pong() { } + + /** + * @return boolean + */ + public function hasVersion() { + return (null === $this->version); + } + + /** + * Set the WebSocket protocol version to communicate with + * @param Ratchet\WebSocket\Version\VersionInterface + * @internal + */ + public function setVersion(VersionInterface $version) { + $this->WebSocket->version = $version; + + return $this; + } } \ No newline at end of file diff --git a/WsServer.php b/WsServer.php index b69119c..619ae9e 100644 --- a/WsServer.php +++ b/WsServer.php @@ -13,6 +13,13 @@ use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; * @link http://dev.w3.org/html5/websockets/ */ class WsServer implements MessageComponentInterface { + /** + * Negotiates upgrading the HTTP connection to a WebSocket connection + * It contains useful configuration properties and methods + * @var HandshakeNegotiator + */ + public $handshaker; + /** * Decorated component * @var Ratchet\MessageComponentInterface|WsServerInterface @@ -24,6 +31,11 @@ class WsServer implements MessageComponentInterface { */ protected $connections; + /** + * @var MessageParser + */ + protected $messager; + /** * Re-entrant instances of protocol version classes * @internal @@ -53,6 +65,12 @@ class WsServer implements MessageComponentInterface { * @param Ratchet\MessageComponentInterface Your application to run with WebSockets */ public function __construct(MessageComponentInterface $component) { + // This will be enabled shortly, causing problems + // mb_internal_encoding('UTF-8'); + + $this->handshaker = new HandshakeNegotiator; + $this->messager = new MessageParser; + $this->_decorating = $component; $this->connections = new \SplObjectStorage; } @@ -61,46 +79,50 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onOpen(ConnectionInterface $conn) { - $conn->WebSocket = new \stdClass; - $conn->WebSocket->handshake = false; - $conn->WebSocket->headers = ''; + $wsConn = new WsConnection($conn); + + $this->connections->attach($conn, $wsConn); + + $this->handshaker->onOpen($wsConn); + + $conn->WebSocket->established = false; } /** - * Do handshake, frame/unframe messages coming/going in stack * {@inheritdoc} */ public function onMessage(ConnectionInterface $from, $msg) { - if (true !== $from->WebSocket->handshake) { - if (!isset($from->WebSocket->version)) { - $from->WebSocket->headers .= $msg; - if (!$this->isMessageComplete($from->WebSocket->headers)) { - return; - } + $conn = $this->connections[$from]; - $headers = RequestFactory::getInstance()->fromMessage($from->WebSocket->headers); - $from->WebSocket->version = $this->getVersion($headers); - $from->WebSocket->headers = $headers; + if (true !== $conn->WebSocket->established) { + if (null === ($response = $this->handshaker->onData($conn, $msg))) { + return; } - $response = $from->WebSocket->version->handshake($from->WebSocket->headers); - $from->WebSocket->handshake = true; - - if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { + // This needs to be refactored later on, incorporated with routing + if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); } - $response->setHeader('X-Powered-By', \Ratchet\VERSION); - $header = (string)$response; + $from->send((string)$response); - $from->send($header); + if (101 != $response->getStatusCode()) { + return $from->close(); + } - $conn = new WsConnection($from); - $this->connections->attach($from, $conn); + $conn->WebSocket->established = true; return $this->_decorating->onOpen($conn); } + /* + if (null !== ($parsed = $this->messager->onData($from, $msg))) { + $this->_decorating->onMessage($from, $parsed); + } + /**/ + +/*************************************************************/ + if (!isset($from->WebSocket->message)) { $from->WebSocket->message = $from->WebSocket->version->newMessage(); } @@ -135,12 +157,12 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onClose(ConnectionInterface $conn) { + $decor = $this->connections[$conn]; + $this->connections->detach($conn); + // WS::onOpen is not called when the socket connects, it's call when the handshake is done // The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet - if ($this->connections->contains($conn)) { - $decor = $this->connections[$conn]; - $this->connections->detach($conn); - + if ($decor->WebSocket->established) { $this->_decorating->onClose($decor); } } @@ -149,62 +171,14 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onError(ConnectionInterface $conn, \Exception $e) { - if ($this->connections->contains($conn)) { +var_dump($e); + if ($conn->WebSocket->established) { $this->_decorating->onError($this->connections[$conn], $e); } else { $conn->close(); } } - /** - * 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; - } - /** * @param string * @return boolean @@ -240,27 +214,4 @@ class WsServer implements MessageComponentInterface { return substr($string, 0, -1); } - - /** - * 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; - } } \ No newline at end of file From 71a2b33056942bc9d8fb466c2a5b3e63024c15da Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 19 May 2012 23:57:20 -0400 Subject: [PATCH 03/24] Cleanup CS Removed a var_dump Removed garbage from a unit test --- Version/Hixie76.php | 6 +++--- Version/RFC6455.php | 1 - WsServer.php | 1 - 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Version/Hixie76.php b/Version/Hixie76.php index 1692eb0..6f91435 100644 --- a/Version/Hixie76.php +++ b/Version/Hixie76.php @@ -40,8 +40,8 @@ class Hixie76 implements VersionInterface { , 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath() ); - $response = new Response('101', $headers, $body); - $response->setStatus('101', 'WebSocket Protocol Handshake'); + $response = new Response(101, $headers, $body); + $response->setStatus(101, 'WebSocket Protocol Handshake'); return $response; } @@ -68,7 +68,7 @@ class Hixie76 implements VersionInterface { } public function generateKeyNumber($key) { - if (0 === substr_count($key, ' ')) { + if (0 === mb_substr_count($key, ' ', 'ASCII')) { return ''; } diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 1be2246..0fcf009 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -4,7 +4,6 @@ use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier; use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\Response; - /** * @link http://tools.ietf.org/html/rfc6455 */ diff --git a/WsServer.php b/WsServer.php index 619ae9e..c746f3c 100644 --- a/WsServer.php +++ b/WsServer.php @@ -171,7 +171,6 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onError(ConnectionInterface $conn, \Exception $e) { -var_dump($e); if ($conn->WebSocket->established) { $this->_decorating->onError($this->connections[$conn], $e); } else { From 3041b829717fc98b491546b2da505b35ed3e343f Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 00:11:04 -0400 Subject: [PATCH 04/24] Oops - TCI fix --- WsServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WsServer.php b/WsServer.php index c746f3c..2ca5e75 100644 --- a/WsServer.php +++ b/WsServer.php @@ -69,7 +69,7 @@ class WsServer implements MessageComponentInterface { // mb_internal_encoding('UTF-8'); $this->handshaker = new HandshakeNegotiator; - $this->messager = new MessageParser; + //$this->messager = new MessageParser; $this->_decorating = $component; $this->connections = new \SplObjectStorage; From ee40816fe475ef577854c2a2304630604591c8e3 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 01:04:09 -0400 Subject: [PATCH 05/24] [WebSocket] Message refactoring Moved the message buffering into its own class --- MessageParser.php | 37 +++++++++++++++++++++++++++++++++++++ WsServer.php | 39 +++------------------------------------ 2 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 MessageParser.php diff --git a/MessageParser.php b/MessageParser.php new file mode 100644 index 0000000..4ac294a --- /dev/null +++ b/MessageParser.php @@ -0,0 +1,37 @@ +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($data); + if ($from->WebSocket->frame->isCoalesced()) { + if ($from->WebSocket->frame->getOpcode() > 2) { + $from->close(); + 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()) { + $parsed = (string)$from->WebSocket->message; + unset($from->WebSocket->message); + + return $parsed; + } + } +} \ No newline at end of file diff --git a/WsServer.php b/WsServer.php index 2ca5e75..1dac9e2 100644 --- a/WsServer.php +++ b/WsServer.php @@ -69,7 +69,7 @@ class WsServer implements MessageComponentInterface { // mb_internal_encoding('UTF-8'); $this->handshaker = new HandshakeNegotiator; - //$this->messager = new MessageParser; + $this->messager = new MessageParser; $this->_decorating = $component; $this->connections = new \SplObjectStorage; @@ -115,41 +115,8 @@ class WsServer implements MessageComponentInterface { return $this->_decorating->onOpen($conn); } - /* - if (null !== ($parsed = $this->messager->onData($from, $msg))) { - $this->_decorating->onMessage($from, $parsed); - } - /**/ - -/*************************************************************/ - - 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) { - $from->close(); - 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()) { - $this->_decorating->onMessage($this->connections[$from], (string)$from->WebSocket->message); - unset($from->WebSocket->message); + if (null !== ($parsed = $this->messager->onData($conn, $msg))) { + $this->_decorating->onMessage($conn, $parsed); } } From d383f3e829529178b39983df5b26eb5de9054664 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 02:03:53 -0400 Subject: [PATCH 06/24] [WebSocket] Un-framing encoding Parsing incoming RFC6455 frames with mb_string --- Version/RFC6455/Frame.php | 22 +++++++++++----------- WsServer.php | 3 +-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 60c8a2f..4314b95 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -48,7 +48,7 @@ class Frame implements FrameInterface { $buf = (string)$buf; $this->_data .= $buf; - $this->_bytes_rec += strlen($buf); + $this->_bytes_rec += mb_strlen($buf, '8bit'); } /** @@ -59,7 +59,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); } - $fbb = sprintf('%08b', ord($this->_data[0])); + $fbb = sprintf('%08b', ord(mb_substr($this->_data, 0, 1, '8bit'))); return (boolean)(int)$fbb[0]; } @@ -71,7 +71,7 @@ class Frame implements FrameInterface { throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); } - return (boolean)bindec(substr(sprintf('%08b', ord($this->_data[1])), 0, 1)); + return (boolean)bindec(mb_substr(sprintf('%08b', ord(mb_substr($this->_data, 1, 1, '8bit'))), 0, 1, '8bit')); } /** @@ -82,7 +82,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine opcode'); } - return bindec(substr(sprintf('%08b', ord($this->_data[0])), 4, 4)); + return bindec(mb_substr(sprintf('%08b', ord(mb_substr($this->_data, 0, 1, '8bit'))), 4, 4, '8bit')); } /** @@ -95,7 +95,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received'); } - return ord($this->_data[1]) & 127; + return ord(mb_substr($this->_data, 1, 1, '8bit')) & 127; } /** @@ -162,7 +162,7 @@ class Frame implements FrameInterface { $strings = array(); for ($i = 2; $i < $byte_length + 1; $i++) { - $strings[] = ord($this->_data[$i]); + $strings[] = ord(mb_substr($this->_data, $i, 1, '8bit')); } $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); @@ -184,14 +184,14 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough data buffered to calculate the masking key'); } - return substr($this->_data, $start, $length); + return mb_substr($this->_data, $start, $length, '8bit'); } /** * {@inheritdoc} */ public function getPayloadStartingByte() { - return 1 + $this->getNumPayloadBytes() + strlen($this->getMaskingKey()); + return 1 + $this->getNumPayloadBytes() + mb_strlen($this->getMaskingKey(), '8bit'); } /** @@ -210,13 +210,13 @@ class Frame implements FrameInterface { $start = $this->getPayloadStartingByte(); for ($i = 0; $i < $length; $i++) { - $payload .= $this->_data[$i + $start] ^ $mask[$i % 4]; + $payload .= mb_substr($this->_data, $i + $start, 1, '8bit') ^ mb_substr($mask, $i % 4, 1, '8bit'); } } else { - $payload = substr($this->_data, $start, $this->getPayloadLength()); + $payload = mb_substr($this->_data, $start, $this->getPayloadLength(), '8bit'); } - if (strlen($payload) !== $length) { + if (mb_strlen($payload, '8bit') !== $length) { // Is this possible? isCoalesced() math _should_ ensure if there is mal-formed data, it would return false throw new \UnexpectedValueException('Payload length does not match expected length'); } diff --git a/WsServer.php b/WsServer.php index 1dac9e2..b7a85fc 100644 --- a/WsServer.php +++ b/WsServer.php @@ -65,8 +65,7 @@ class WsServer implements MessageComponentInterface { * @param Ratchet\MessageComponentInterface Your application to run with WebSockets */ public function __construct(MessageComponentInterface $component) { - // This will be enabled shortly, causing problems - // mb_internal_encoding('UTF-8'); + mb_internal_encoding('UTF-8'); $this->handshaker = new HandshakeNegotiator; $this->messager = new MessageParser; From bf808d4c611b5d1ae2ad11603a317450f3b0313c Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 13:24:37 -0400 Subject: [PATCH 07/24] AutobahnTestSuite Added files to test Ratchet against the AutobahnTestSuite Bumped version v0.2b Updated how to handle control frames to run the test suite --- MessageParser.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/MessageParser.php b/MessageParser.php index 4ac294a..11fc388 100644 --- a/MessageParser.php +++ b/MessageParser.php @@ -15,8 +15,9 @@ class MessageParser { $from->WebSocket->frame->addBuffer($data); if ($from->WebSocket->frame->isCoalesced()) { if ($from->WebSocket->frame->getOpcode() > 2) { - $from->close(); - throw new \UnexpectedValueException('Control frame support coming soon!'); + unset($from->WebSocket->frame); + + return; } // Check frame // If is control frame, do your thing From 0caa6e814b87b1a36b81f462170fead953edb05b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 21 May 2012 13:16:33 -0400 Subject: [PATCH 08/24] Minor cleanups --- MessageParser.php | 2 ++ Version/RFC6455/Message.php | 1 - WsServer.php | 15 ++------------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/MessageParser.php b/MessageParser.php index 11fc388..017c4f5 100644 --- a/MessageParser.php +++ b/MessageParser.php @@ -15,6 +15,8 @@ class MessageParser { $from->WebSocket->frame->addBuffer($data); if ($from->WebSocket->frame->isCoalesced()) { if ($from->WebSocket->frame->getOpcode() > 2) { + // take action on the control frame + unset($from->WebSocket->frame); return; diff --git a/Version/RFC6455/Message.php b/Version/RFC6455/Message.php index 5385475..1071115 100644 --- a/Version/RFC6455/Message.php +++ b/Version/RFC6455/Message.php @@ -35,7 +35,6 @@ class Message implements MessageInterface { /** * {@inheritdoc} - * @todo Should I allow addFrame if the frame is not coalesced yet? I believe I'm assuming this class will only receive fully formed frame messages * @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message */ public function addFrame(FrameInterface $fragment) { diff --git a/WsServer.php b/WsServer.php index b7a85fc..40c3973 100644 --- a/WsServer.php +++ b/WsServer.php @@ -17,6 +17,7 @@ class WsServer implements MessageComponentInterface { * Negotiates upgrading the HTTP connection to a WebSocket connection * It contains useful configuration properties and methods * @var HandshakeNegotiator + * @note May not expose this in the future, may do through facade methods */ public $handshaker; @@ -36,18 +37,6 @@ class WsServer implements MessageComponentInterface { */ protected $messager; - /** - * Re-entrant instances of protocol version classes - * @internal - */ - protected $_versions = array( - 'HyBi10' => null - , 'Hixie76' => null - , 'RFC6455' => null - ); - - protected $_mask_payload = false; - /** * For now, array_push accepted subprotocols to this array * @deprecated @@ -177,6 +166,6 @@ class WsServer implements MessageComponentInterface { } } - return substr($string, 0, -1); + return mb_substr($string, 0, -1, 'ASCII'); } } \ No newline at end of file From 8e0175494de31a2beddb1b5ef495073cd6995a26 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 1 Jun 2012 23:07:25 -0400 Subject: [PATCH 09/24] Removed most of the mbstring calls Moving forward we're going to assume `mbstring.func_overload` is off. For that reason we're not going to call `mb_` functions when checking byte level strings. --- HandshakeNegotiator.php | 4 ++-- Version/Hixie76.php | 2 +- Version/RFC6455/Frame.php | 25 ++++++++++++++----------- Version/RFC6455/HandshakeVerifier.php | 12 ++++++------ WsServer.php | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/HandshakeNegotiator.php b/HandshakeNegotiator.php index 3f19bf1..0cdef10 100644 --- a/HandshakeNegotiator.php +++ b/HandshakeNegotiator.php @@ -44,7 +44,7 @@ class HandshakeNegotiator { public function onData(WsConnection $conn, $data) { $conn->WebSocket->handshakeBuffer .= $data; - if (mb_strlen($conn->WebSocket->handshakeBuffer, '8bit') >= (int)$this->maxSize) { + if (strlen($conn->WebSocket->handshakeBuffer) >= (int)$this->maxSize) { return new Response(413, array('X-Powered-By' => \Ratchet\VERSION)); } @@ -59,7 +59,7 @@ class HandshakeNegotiator { } // TODO: confirm message is buffered - // Hixie requires the body to complete the handshake (6 characters long) + // Hixie requires the body to complete the handshake (6 characters long) - is that 6 ASCII or UTF-8 characters? // Update VersionInterface to check for this, ::canHandshake() maybe // return if can't, continue buffering diff --git a/Version/Hixie76.php b/Version/Hixie76.php index 6f91435..4601b51 100644 --- a/Version/Hixie76.php +++ b/Version/Hixie76.php @@ -68,7 +68,7 @@ class Hixie76 implements VersionInterface { } public function generateKeyNumber($key) { - if (0 === mb_substr_count($key, ' ', 'ASCII')) { + if (0 === substr_count($key, ' ')) { return ''; } diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 4314b95..8a17d6a 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -48,7 +48,7 @@ class Frame implements FrameInterface { $buf = (string)$buf; $this->_data .= $buf; - $this->_bytes_rec += mb_strlen($buf, '8bit'); + $this->_bytes_rec += strlen($buf); } /** @@ -59,7 +59,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); } - $fbb = sprintf('%08b', ord(mb_substr($this->_data, 0, 1, '8bit'))); + $fbb = sprintf('%08b', ord(substr($this->_data, 0, 1))); return (boolean)(int)$fbb[0]; } @@ -71,7 +71,7 @@ class Frame implements FrameInterface { throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); } - return (boolean)bindec(mb_substr(sprintf('%08b', ord(mb_substr($this->_data, 1, 1, '8bit'))), 0, 1, '8bit')); + return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->_data, 1, 1))), 0, 1)); } /** @@ -82,7 +82,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine opcode'); } - return bindec(mb_substr(sprintf('%08b', ord(mb_substr($this->_data, 0, 1, '8bit'))), 4, 4, '8bit')); + return bindec(substr(sprintf('%08b', ord(substr($this->_data, 0, 1))), 4, 4)); } /** @@ -95,7 +95,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received'); } - return ord(mb_substr($this->_data, 1, 1, '8bit')) & 127; + return ord(substr($this->_data, 1, 1)) & 127; } /** @@ -152,6 +152,7 @@ class Frame implements FrameInterface { if ($length_check <= 125) { $this->_pay_len_def = $length_check; + return $this->getPayloadLength(); } @@ -162,10 +163,11 @@ class Frame implements FrameInterface { $strings = array(); for ($i = 2; $i < $byte_length + 1; $i++) { - $strings[] = ord(mb_substr($this->_data, $i, 1, '8bit')); + $strings[] = ord(substr($this->_data, $i, 1)); } $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); + return $this->getPayloadLength(); } @@ -184,14 +186,14 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough data buffered to calculate the masking key'); } - return mb_substr($this->_data, $start, $length, '8bit'); + return substr($this->_data, $start, $length); } /** * {@inheritdoc} */ public function getPayloadStartingByte() { - return 1 + $this->getNumPayloadBytes() + mb_strlen($this->getMaskingKey(), '8bit'); + return 1 + $this->getNumPayloadBytes() + strlen($this->getMaskingKey()); } /** @@ -210,13 +212,14 @@ class Frame implements FrameInterface { $start = $this->getPayloadStartingByte(); for ($i = 0; $i < $length; $i++) { - $payload .= mb_substr($this->_data, $i + $start, 1, '8bit') ^ mb_substr($mask, $i % 4, 1, '8bit'); + // Double check the RFC - is the masking byte level or character level? + $payload .= substr($this->_data, $i + $start, 1) ^ substr($mask, $i % 4, 1); } } else { - $payload = mb_substr($this->_data, $start, $this->getPayloadLength(), '8bit'); + $payload = substr($this->_data, $start, $this->getPayloadLength()); } - if (mb_strlen($payload, '8bit') !== $length) { + if (strlen($payload) !== $length) { // Is this possible? isCoalesced() math _should_ ensure if there is mal-formed data, it would return false throw new \UnexpectedValueException('Payload length does not match expected length'); } diff --git a/Version/RFC6455/HandshakeVerifier.php b/Version/RFC6455/HandshakeVerifier.php index e9172ef..93d7928 100644 --- a/Version/RFC6455/HandshakeVerifier.php +++ b/Version/RFC6455/HandshakeVerifier.php @@ -34,7 +34,7 @@ class HandshakeVerifier { * @return bool */ public function verifyMethod($val) { - return ('get' === mb_strtolower($val, 'ASCII')); + return ('get' === strtolower($val)); } /** @@ -59,7 +59,7 @@ class HandshakeVerifier { return false; } - return mb_check_encoding($val, 'ASCII'); + return mb_check_encoding($val, 'US-ASCII'); } /** @@ -78,7 +78,7 @@ class HandshakeVerifier { * @return bool */ public function verifyUpgradeRequest($val) { - return ('websocket' === mb_strtolower($val, 'ASCII')); + return ('websocket' === strtolower($val)); } /** @@ -87,13 +87,12 @@ class HandshakeVerifier { * @return bool */ public function verifyConnection($val) { - $val = mb_strtolower($val, 'ASCII'); + $val = strtolower($val); if ('upgrade' === $val) { return true; } - // todo change this to mb_eregi_replace $vals = explode(',', str_replace(', ', ',', $val)); return (false !== array_search('upgrade', $vals)); @@ -104,9 +103,10 @@ class HandshakeVerifier { * @param string|null * @return bool * @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? + * @todo Check the spec to see what the encoding of the key could be */ public function verifyKey($val) { - return (16 === mb_strlen(base64_decode((string)$val), '8bit')); + return (16 === strlen(base64_decode((string)$val))); } /** diff --git a/WsServer.php b/WsServer.php index 40c3973..d1e0f2b 100644 --- a/WsServer.php +++ b/WsServer.php @@ -166,6 +166,6 @@ class WsServer implements MessageComponentInterface { } } - return mb_substr($string, 0, -1, 'ASCII'); + return substr($string, 0, -1); } } \ No newline at end of file From cea538429def961c8768d7cb9d5b4eb8e538234f Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 15:44:18 -0400 Subject: [PATCH 10/24] [WebSocket] RFC6455 Framing work New code to create a frame Unit tests for new code API cleanup --- HandshakeNegotiator.php | 1 + Version/RFC6455.php | 2 + Version/RFC6455/Frame.php | 96 ++++++++++++++++++++++++++++++++------- WsConnection.php | 9 +--- 4 files changed, 85 insertions(+), 23 deletions(-) diff --git a/HandshakeNegotiator.php b/HandshakeNegotiator.php index 0cdef10..1bce9a3 100644 --- a/HandshakeNegotiator.php +++ b/HandshakeNegotiator.php @@ -77,6 +77,7 @@ class HandshakeNegotiator { * Determine if the message has been buffered as per the HTTP specification * @param string * @return boolean + * @todo Safari does not send 2xCRLF after the 6 byte body...this will always return false for Hixie */ public function isEom($message) { return (static::EOM === substr($message, 0 - strlen(static::EOM))); diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 0fcf009..59cbdde 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -73,6 +73,8 @@ class RFC6455 implements VersionInterface { * @return string */ public function frame($message, $mask = true) { +return RFC6455\Frame::create($message)->data; + $payload = $message; $type = 'text'; $masked = $mask; diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 8a17d6a..5a7a7ff 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -3,11 +3,18 @@ namespace Ratchet\WebSocket\Version\RFC6455; use Ratchet\WebSocket\Version\FrameInterface; class Frame implements FrameInterface { + const OP_CONTINUE = 0; + const OP_TEXT = 1; + const OP_BINARY = 2; + const OP_CLOSE = 8; + const OP_PING = 9; + const OP_PONG = 10; + /** * The contents of the frame * @var string */ - protected $_data = ''; + public $data = ''; /** * Number of bytes received from the frame @@ -22,10 +29,66 @@ class Frame implements FrameInterface { protected $_pay_len_def = -1; /** - * Bit 9-15 - * @var int + * @param string A valid UTF-8 string to send over the wire + * @param bool Is the final frame in a message + * @param int The opcode of the frame, see constants + * @param bool Mask the payload + * @return Frame + * @throws InvalidArgumentException If the payload is not a valid UTF-8 string + * @throws BadMethodCallException If there is a problem with miss-matching parameters + * @throws LengthException If the payload is too big */ - protected $_pay_check = -1; + public static function create($payload, $final = true, $opcode = 1, $mask = false) { + $frame = new static(); + + if (!mb_check_encoding($payload, 'UTF-8')) { + throw new \InvalidArgumentException("Payload is not a valid UTF-8 string"); + } + + if (false === (boolean)$final && $opcode !== static::OP_CONTINUE) { + throw new \BadMethodCallException("opcode MUST be 'continue' if the frame is not final"); + } + + $raw = (int)(boolean)$final . sprintf('%07b', (int)$opcode); + + $plLen = strlen($payload); + if ($plLen <= 125) { + $raw .= sprintf('%08b', $plLen); + } elseif ($plLen <= 65535) { + $raw .= sprintf('%08b', 126) . sprintf('%016b', $plLen); + } else { // todo, make sure msg isn't longer than b1x71 + $raw .= sprintf('%08b', 127) . sprintf('%064b', $plLen); + } + + $frame->addBuffer(static::encode($raw) . $payload); + + if ($mask) { + // create masking key + // insert it + // mask the payload + } + + return $frame; + } + + /** + * @param string of 1's and 0's + * @return string + */ + public static function encode($in) { + if (strlen($in) > 8) { + $out = ''; + + while (strlen($in) >= 8) { + $out .= static::encode(substr($in, 0, 8)); + $in = substr($in, 8); + } + + return $out; + } + + return chr(bindec($in)); + } /** * {@inheritdoc} @@ -38,7 +101,7 @@ class Frame implements FrameInterface { return false; } - return $payload_length + $payload_start === $this->_bytes_rec; + return $this->_bytes_rec >= $payload_length + $payload_start; } /** @@ -47,7 +110,7 @@ class Frame implements FrameInterface { public function addBuffer($buf) { $buf = (string)$buf; - $this->_data .= $buf; + $this->data .= $buf; $this->_bytes_rec += strlen($buf); } @@ -59,7 +122,8 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); } - $fbb = sprintf('%08b', ord(substr($this->_data, 0, 1))); + $fbb = sprintf('%08b', ord(substr($this->data, 0, 1))); + return (boolean)(int)$fbb[0]; } @@ -71,7 +135,7 @@ class Frame implements FrameInterface { throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); } - return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->_data, 1, 1))), 0, 1)); + return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->data, 1, 1))), 0, 1)); } /** @@ -82,7 +146,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine opcode'); } - return bindec(substr(sprintf('%08b', ord(substr($this->_data, 0, 1))), 4, 4)); + return bindec(substr(sprintf('%08b', ord(substr($this->data, 0, 1))), 4, 4)); } /** @@ -95,7 +159,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received'); } - return ord(substr($this->_data, 1, 1)) & 127; + return ord(substr($this->data, 1, 1)) & 127; } /** @@ -163,7 +227,7 @@ class Frame implements FrameInterface { $strings = array(); for ($i = 2; $i < $byte_length + 1; $i++) { - $strings[] = ord(substr($this->_data, $i, 1)); + $strings[] = ord(substr($this->data, $i, 1)); } $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); @@ -186,7 +250,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough data buffered to calculate the masking key'); } - return substr($this->_data, $start, $length); + return substr($this->data, $start, $length); } /** @@ -206,17 +270,17 @@ class Frame implements FrameInterface { $payload = ''; $length = $this->getPayloadLength(); + $start = $this->getPayloadStartingByte(); if ($this->isMasked()) { - $mask = $this->getMaskingKey(); - $start = $this->getPayloadStartingByte(); + $mask = $this->getMaskingKey(); for ($i = 0; $i < $length; $i++) { // Double check the RFC - is the masking byte level or character level? - $payload .= substr($this->_data, $i + $start, 1) ^ substr($mask, $i % 4, 1); + $payload .= substr($this->data, $i + $start, 1) ^ substr($mask, $i % 4, 1); } } else { - $payload = substr($this->_data, $start, $this->getPayloadLength()); + $payload = substr($this->data, $start, $this->getPayloadLength()); } if (strlen($payload) !== $length) { diff --git a/WsConnection.php b/WsConnection.php index a346fa9..de6c3fa 100644 --- a/WsConnection.php +++ b/WsConnection.php @@ -30,7 +30,7 @@ class WsConnection extends AbstractConnectionDecorator { } public function close() { - // send close frame + // send close frame with code 1000 // ??? @@ -39,14 +39,9 @@ class WsConnection extends AbstractConnectionDecorator { $this->getConnection()->close(); // temporary } - public function ping() { - } - - public function pong() { - } - /** * @return boolean + * @internal */ public function hasVersion() { return (null === $this->version); From 0326ac99bdff72decdf965764df81adc8694107b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 21:11:29 -0400 Subject: [PATCH 11/24] [WebSocket] Frame overflow --- Version/RFC6455.php | 79 +-------------------------------------- Version/RFC6455/Frame.php | 24 +++++++++++- 2 files changed, 25 insertions(+), 78 deletions(-) diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 59cbdde..c01613b 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -6,6 +6,7 @@ use Guzzle\Http\Message\Response; /** * @link http://tools.ietf.org/html/rfc6455 + * @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE'); */ class RFC6455 implements VersionInterface { const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; @@ -65,88 +66,12 @@ class RFC6455 implements VersionInterface { } /** - * Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame - * @link https://github.com/lemmingzshadow/php-websocket - * @todo look into what happens when false is returned here * @todo This is needed when a client is created - needs re-write as missing parts of protocol * @param string * @return string */ public function frame($message, $mask = true) { -return RFC6455\Frame::create($message)->data; - - $payload = $message; - $type = 'text'; - $masked = $mask; - - $frameHead = array(); - $frame = ''; - $payloadLength = strlen($payload); - - switch($type) { - case 'text': - // first byte indicates FIN, Text-Frame (10000001): - $frameHead[0] = 129; - break; - - case 'close': - // first byte indicates FIN, Close Frame(10001000): - $frameHead[0] = 136; - break; - - case 'ping': - // first byte indicates FIN, Ping frame (10001001): - $frameHead[0] = 137; - break; - - case 'pong': - // first byte indicates FIN, Pong frame (10001010): - $frameHead[0] = 138; - break; - } - - // set mask and payload length (using 1, 3 or 9 bytes) - if($payloadLength > 65535) { - $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 255 : 127; - for($i = 0; $i < 8; $i++) { - $frameHead[$i+2] = bindec($payloadLengthBin[$i]); - } - // most significant bit MUST be 0 (return false if to much data) - if($frameHead[2] > 127) { - return false; - } - } elseif($payloadLength > 125) { - $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 254 : 126; - $frameHead[2] = bindec($payloadLengthBin[0]); - $frameHead[3] = bindec($payloadLengthBin[1]); - } else { - $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; - } - - // convert frame-head to string: - foreach(array_keys($frameHead) as $i) { - $frameHead[$i] = chr($frameHead[$i]); - } if($masked === true) { - // generate a random mask: - $mask = array(); - for($i = 0; $i < 4; $i++) - { - $mask[$i] = chr(rand(0, 255)); - } - - $frameHead = array_merge($frameHead, $mask); - } - $frame = implode('', $frameHead); - - // append payload to frame: - $framePayload = array(); - for($i = 0; $i < $payloadLength; $i++) { - $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; - } - - return $frame; + return RFC6455\Frame::create($message)->data; } /** diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 5a7a7ff..af8e961 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -72,6 +72,7 @@ class Frame implements FrameInterface { } /** + * Encode the fake binary string to send over the wire * @param string of 1's and 0's * @return string */ @@ -110,7 +111,7 @@ class Frame implements FrameInterface { public function addBuffer($buf) { $buf = (string)$buf; - $this->data .= $buf; + $this->data .= $buf; $this->_bytes_rec += strlen($buf); } @@ -290,4 +291,25 @@ class Frame implements FrameInterface { return $payload; } + /** + * Sometimes clients will concatinate more than one frame over the wire + * This method will take the extra bytes off the end and return them + * @todo Consider returning new Frame + * @return string + */ + public function extractOverflow() { + if ($this->isCoalesced()) { + $endPoint = $this->getPayloadLength(); + $endPoint += $this->getPayloadStartingByte(); + + if ($this->_bytes_rec > $endPoint) { + $overflow = substr($this->data, $endPoint); + $this->data = substr($this->data, 0, $endPoint); + + return $overflow; + } + } + + return ''; + } } \ No newline at end of file From 8e30b861c7f2a6f080545c0c914f787c80f21c3b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 22:08:27 -0400 Subject: [PATCH 12/24] [WebSocket] Messaging Fluent interface on MessageInterface::addFrame RFC6455 Message unit tests RFC handling TCP concatenation (refs #31) --- MessageParser.php | 6 +++++- Version/Hixie76/Message.php | 2 ++ Version/MessageInterface.php | 1 + Version/RFC6455/Frame.php | 5 ----- Version/RFC6455/Message.php | 5 ++++- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/MessageParser.php b/MessageParser.php index 017c4f5..b28592d 100644 --- a/MessageParser.php +++ b/MessageParser.php @@ -21,13 +21,17 @@ class MessageParser { return; } + // 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 + $nextFrame = $from->WebSocket->version->newFrame(); + $nextFrame->addBuffer($from->WebSocket->frame->extractOverflow()); + $from->WebSocket->message->addFrame($from->WebSocket->frame); - unset($from->WebSocket->frame); + $from->WebSocket->frame = $nextFrame; } if ($from->WebSocket->message->isCoalesced()) { diff --git a/Version/Hixie76/Message.php b/Version/Hixie76/Message.php index f783e8b..818d95d 100644 --- a/Version/Hixie76/Message.php +++ b/Version/Hixie76/Message.php @@ -36,6 +36,8 @@ class Message implements MessageInterface { } $this->_frame = $fragment; + + return $this; } /** diff --git a/Version/MessageInterface.php b/Version/MessageInterface.php index 90d0179..4c21114 100644 --- a/Version/MessageInterface.php +++ b/Version/MessageInterface.php @@ -17,6 +17,7 @@ interface MessageInterface { /** * @param FragmentInterface + * @return MessageInterface */ function addFrame(FrameInterface $fragment); diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index af8e961..ec73393 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -35,7 +35,6 @@ class Frame implements FrameInterface { * @param bool Mask the payload * @return Frame * @throws InvalidArgumentException If the payload is not a valid UTF-8 string - * @throws BadMethodCallException If there is a problem with miss-matching parameters * @throws LengthException If the payload is too big */ public static function create($payload, $final = true, $opcode = 1, $mask = false) { @@ -45,10 +44,6 @@ class Frame implements FrameInterface { throw new \InvalidArgumentException("Payload is not a valid UTF-8 string"); } - if (false === (boolean)$final && $opcode !== static::OP_CONTINUE) { - throw new \BadMethodCallException("opcode MUST be 'continue' if the frame is not final"); - } - $raw = (int)(boolean)$final . sprintf('%07b', (int)$opcode); $plLen = strlen($payload); diff --git a/Version/RFC6455/Message.php b/Version/RFC6455/Message.php index 1071115..53bb8dd 100644 --- a/Version/RFC6455/Message.php +++ b/Version/RFC6455/Message.php @@ -39,6 +39,8 @@ class Message implements MessageInterface { */ public function addFrame(FrameInterface $fragment) { $this->_frames->push($fragment); + + return $this; } /** @@ -62,6 +64,7 @@ class Message implements MessageInterface { try { $len += $frame->getPayloadLength(); } catch (\UnderflowException $e) { + // Not an error, want the current amount buffered } } @@ -73,7 +76,7 @@ class Message implements MessageInterface { */ public function getPayload() { if (!$this->isCoalesced()) { - throw new \UnderflowMessage('Message has not been put back together yet'); + throw new \UnderflowException('Message has not been put back together yet'); } $buffer = ''; From 49d1dc33ef8db6370a6d1dff7818e27b660ee869 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 02:03:16 -0400 Subject: [PATCH 13/24] [WebSocket] Frame masking --- MessageParser.php | 3 + Version/RFC6455/Frame.php | 132 ++++++++++++++++++++++++++++++-------- 2 files changed, 109 insertions(+), 26 deletions(-) diff --git a/MessageParser.php b/MessageParser.php index b28592d..51a6941 100644 --- a/MessageParser.php +++ b/MessageParser.php @@ -14,6 +14,9 @@ class MessageParser { $from->WebSocket->frame->addBuffer($data); if ($from->WebSocket->frame->isCoalesced()) { + // check if masked + // close if not + if ($from->WebSocket->frame->getOpcode() > 2) { // take action on the control frame diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index ec73393..94c463f 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -10,6 +10,8 @@ class Frame implements FrameInterface { const OP_PING = 9; const OP_PONG = 10; + const MASK_LENGTH = 4; + /** * The contents of the frame * @var string @@ -37,7 +39,7 @@ class Frame implements FrameInterface { * @throws InvalidArgumentException If the payload is not a valid UTF-8 string * @throws LengthException If the payload is too big */ - public static function create($payload, $final = true, $opcode = 1, $mask = false) { + public static function create($payload, $final = true, $opcode = 1) { $frame = new static(); if (!mb_check_encoding($payload, 'UTF-8')) { @@ -57,12 +59,6 @@ class Frame implements FrameInterface { $frame->addBuffer(static::encode($raw) . $payload); - if ($mask) { - // create masking key - // insert it - // mask the payload - } - return $frame; } @@ -134,6 +130,107 @@ class Frame implements FrameInterface { return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->data, 1, 1))), 0, 1)); } + /** + * {@inheritdoc} + */ + public function getMaskingKey() { + if (!$this->isMasked()) { + return ''; + } + + $start = 1 + $this->getNumPayloadBytes(); + + if ($this->_bytes_rec < $start + static::MASK_LENGTH) { + throw new \UnderflowException('Not enough data buffered to calculate the masking key'); + } + + return substr($this->data, $start, static::MASK_LENGTH); + } + + /** + * @return string + */ + public function generateMaskingKey() { + $mask = ''; + + for ($i = 1; $i <= static::MASK_LENGTH; $i++) { + $mask .= sprintf("%c", rand(32, 126)); + } + + return $mask; + } + + /** + * Apply a mask to the payload + * @param string|null + * @throws InvalidArgumentException If there is an issue with the given masking key + * @throws UnderflowException If the frame is not coalesced + */ + public function maskPayload($maskingKey = null) { + if (null === $maskingKey) { + $maskingKey = $this->generateMaskingKey(); + } + + if (static::MASK_LENGTH !== strlen($maskingKey)) { + throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters"); + } + + if (!mb_check_encoding($maskingKey, 'US-ASCII')) { + throw new \InvalidArgumentException("Masking key MUST be ASCII"); + } + + if (!$this->isCoalesced()) { + throw new \UnderflowException('Frame must be coalesced to apply a mask'); + } + + if ($this->isMasked()) { + $this->unMaskPayload(); + } + + $byte = sprintf('%08b', ord(substr($this->data, 1, 1))); + + $this->data = substr_replace($this->data, static::encode(substr_replace($byte, '1', 0, 1)), 1, 1); + $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); + + $this->_bytes_rec += static::MASK_LENGTH; + + $plLen = $this->getPayloadLength(); + $start = $this->getPayloadStartingByte(); + $maskedPl = ''; + + for ($i = 0; $i < $plLen; $i++) { + $maskedPl .= substr($this->data, $i + $start, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1); + } + + $this->data = substr_replace($this->data, $maskedPl, $start, $plLen); + + return $this; + } + + /** + * Remove a mask from the payload + * @throws UnderFlowException If the frame is not coalesced + * @return Frame + */ + public function unMaskPayload() { + if (!$this->isMasked()) { + return $this; + } + + if (!$this->isCoalesced()) { + throw new \UnderflowException('Frame must be coalesced to apply a mask'); + } + + $maskingKey = $this->getMaskingKey(); + + // set the indicator bit to 0 + // remove the masking key + // get the masking key + // unmask the payload + + return $this; + } + /** * {@inheritdoc} */ @@ -231,24 +328,6 @@ class Frame implements FrameInterface { return $this->getPayloadLength(); } - /** - * {@inheritdoc} - */ - public function getMaskingKey() { - if (!$this->isMasked()) { - return ''; - } - - $length = 4; - $start = 1 + $this->getNumPayloadBytes(); - - if ($this->_bytes_rec < $start + $length) { - throw new \UnderflowException('Not enough data buffered to calculate the masking key'); - } - - return substr($this->data, $start, $length); - } - /** * {@inheritdoc} */ @@ -273,7 +352,7 @@ class Frame implements FrameInterface { for ($i = 0; $i < $length; $i++) { // Double check the RFC - is the masking byte level or character level? - $payload .= substr($this->data, $i + $start, 1) ^ substr($mask, $i % 4, 1); + $payload .= substr($this->data, $i + $start, 1) ^ substr($mask, $i % static::MASK_LENGTH, 1); } } else { $payload = substr($this->data, $start, $this->getPayloadLength()); @@ -286,6 +365,7 @@ class Frame implements FrameInterface { return $payload; } + /** * Sometimes clients will concatinate more than one frame over the wire * This method will take the extra bytes off the end and return them From 08d7d6f8a0ca483b91ce9ae7fa7ffdd429bf27a2 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 11:55:35 -0400 Subject: [PATCH 14/24] [WebSocket] RFC Masking Full un/masking capabilities on RFC6455 Frames --- Version/RFC6455/Frame.php | 54 +++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 94c463f..d7c25b0 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -179,13 +179,7 @@ class Frame implements FrameInterface { throw new \InvalidArgumentException("Masking key MUST be ASCII"); } - if (!$this->isCoalesced()) { - throw new \UnderflowException('Frame must be coalesced to apply a mask'); - } - - if ($this->isMasked()) { - $this->unMaskPayload(); - } + $this->unMaskPayload(); $byte = sprintf('%08b', ord(substr($this->data, 1, 1))); @@ -193,16 +187,7 @@ class Frame implements FrameInterface { $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); $this->_bytes_rec += static::MASK_LENGTH; - - $plLen = $this->getPayloadLength(); - $start = $this->getPayloadStartingByte(); - $maskedPl = ''; - - for ($i = 0; $i < $plLen; $i++) { - $maskedPl .= substr($this->data, $i + $start, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1); - } - - $this->data = substr_replace($this->data, $maskedPl, $start, $plLen); + $this->data = substr_replace($this->data, $this->applyMaskToPayload($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; } @@ -217,18 +202,33 @@ class Frame implements FrameInterface { return $this; } + $maskingKey = $this->getMaskingKey(); + + $byte = sprintf('%08b', ord(substr($this->data, 1, 1))); + + $this->data = substr_replace($this->data, static::encode(substr_replace($byte, '0', 0, 1)), 1, 1); + $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); + + $this->_bytes_rec -= static::MASK_LENGTH; + $this->data = substr_replace($this->data, $this->applyMaskToPayload($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); + + return $this; + } + + protected function applyMaskToPayload($maskingKey) { if (!$this->isCoalesced()) { throw new \UnderflowException('Frame must be coalesced to apply a mask'); } - $maskingKey = $this->getMaskingKey(); + $plLen = $this->getPayloadLength(); + $start = $this->getPayloadStartingByte(); + $applied = ''; - // set the indicator bit to 0 - // remove the masking key - // get the masking key - // unmask the payload + for ($i = 0; $i < $plLen; $i++) { + $applied .= substr($this->data, $i + $start, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1); + } - return $this; + return $applied; } /** @@ -343,17 +343,11 @@ class Frame implements FrameInterface { throw new \UnderflowException('Can not return partial message'); } - $payload = ''; $length = $this->getPayloadLength(); $start = $this->getPayloadStartingByte(); if ($this->isMasked()) { - $mask = $this->getMaskingKey(); - - for ($i = 0; $i < $length; $i++) { - // Double check the RFC - is the masking byte level or character level? - $payload .= substr($this->data, $i + $start, 1) ^ substr($mask, $i % static::MASK_LENGTH, 1); - } + $payload = $this->applyMaskToPayload($this->getMaskingKey()); } else { $payload = substr($this->data, $start, $this->getPayloadLength()); } From 9670876e51b52cf04baacd52fa5c78f51c0e305a Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 12:14:53 -0400 Subject: [PATCH 15/24] [WebSocket] Refactored Frame masking/payload --- Version/RFC6455/Frame.php | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index d7c25b0..03601ff 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -187,7 +187,7 @@ class Frame implements FrameInterface { $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); $this->_bytes_rec += static::MASK_LENGTH; - $this->data = substr_replace($this->data, $this->applyMaskToPayload($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); + $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; } @@ -210,22 +210,23 @@ class Frame implements FrameInterface { $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); $this->_bytes_rec -= static::MASK_LENGTH; - $this->data = substr_replace($this->data, $this->applyMaskToPayload($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); + $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; } - protected function applyMaskToPayload($maskingKey) { - if (!$this->isCoalesced()) { - throw new \UnderflowException('Frame must be coalesced to apply a mask'); + protected function applyMask($maskingKey, $payload = null) { + if (null === $payload) { + if (!$this->isCoalesced()) { + throw new \UnderflowException('Frame must be coalesced to apply a mask'); + } + + $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); } - $plLen = $this->getPayloadLength(); - $start = $this->getPayloadStartingByte(); $applied = ''; - - for ($i = 0; $i < $plLen; $i++) { - $applied .= substr($this->data, $i + $start, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1); + for ($i = 0, $len = strlen($payload); $i < $len; $i++) { + $applied .= substr($payload, $i, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1); } return $applied; @@ -337,24 +338,17 @@ class Frame implements FrameInterface { /** * {@inheritdoc} + * @todo Consider not checking mask, always returning the payload, masked or not */ public function getPayload() { if (!$this->isCoalesced()) { throw new \UnderflowException('Can not return partial message'); } - $length = $this->getPayloadLength(); - $start = $this->getPayloadStartingByte(); - if ($this->isMasked()) { - $payload = $this->applyMaskToPayload($this->getMaskingKey()); + $payload = $this->applyMask($this->getMaskingKey()); } else { - $payload = substr($this->data, $start, $this->getPayloadLength()); - } - - if (strlen($payload) !== $length) { - // Is this possible? isCoalesced() math _should_ ensure if there is mal-formed data, it would return false - throw new \UnexpectedValueException('Payload length does not match expected length'); + $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); } return $payload; From 6cbf0eb186c34176d18a654a396a0ac382e82664 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 9 Jun 2012 19:38:44 -0400 Subject: [PATCH 16/24] [WebSocket] Refactoring Updated deps; React Socket notify client of shutdown Separated core interfaces into many Removed initial version support out of handshake negotiator Moved message parser responsibility to each version Removed __toString method from MessageInterface as to not confuse message from payload Support for RFC control frames Support message concatenation [BCB] (temporary) WsConnection hard coded to RFC version Handshake checks for \r\n\r\n anywhere, not just at end of string --- HandshakeNegotiator.php | 24 +++----- MessageParser.php | 47 ---------------- Version/MessageInterface.php | 5 -- Version/RFC6455.php | 103 +++++++++++++++++++++++++++++++++-- Version/RFC6455/Frame.php | 73 +++++++++++++++---------- Version/VersionInterface.php | 5 +- WsConnection.php | 38 ++++--------- WsServer.php | 23 ++++---- 8 files changed, 175 insertions(+), 143 deletions(-) delete mode 100644 MessageParser.php diff --git a/HandshakeNegotiator.php b/HandshakeNegotiator.php index 1bce9a3..1b80b13 100644 --- a/HandshakeNegotiator.php +++ b/HandshakeNegotiator.php @@ -1,12 +1,13 @@ enableVersion(new Version\RFC6455); - $this->enableVersion(new Version\HyBi10); - $this->enableVersion(new Version\Hixie76); - } - } - /** * @param WsConnection */ - public function onOpen(WsConnection $conn) { + public function onOpen(ConnectionInterface $conn) { $conn->WebSocket->handshakeBuffer = ''; } @@ -41,7 +34,7 @@ class HandshakeNegotiator { * @return Guzzle\Http\Message\Response|null Response object if it's done parsing, null if there's more to be buffered * @throws HttpException */ - public function onData(WsConnection $conn, $data) { + public function onMessage(ConnectionInterface $conn, $data) { $conn->WebSocket->handshakeBuffer .= $data; if (strlen($conn->WebSocket->handshakeBuffer) >= (int)$this->maxSize) { @@ -66,7 +59,8 @@ class HandshakeNegotiator { $response = $version->handshake($conn->WebSocket->request); $response->setHeader('X-Powered-By', \Ratchet\VERSION); - $conn->setVersion($version); + // This needs to be decoupled + $conn->WebSocket->version = $version; unset($conn->WebSocket->handshakeBuffer); return $response; @@ -77,10 +71,10 @@ class HandshakeNegotiator { * Determine if the message has been buffered as per the HTTP specification * @param string * @return boolean - * @todo Safari does not send 2xCRLF after the 6 byte body...this will always return false for Hixie */ public function isEom($message) { - return (static::EOM === substr($message, 0 - strlen(static::EOM))); + //return (static::EOM === substr($message, 0 - strlen(static::EOM))); + return (boolean)strpos($message, static::EOM); } /** diff --git a/MessageParser.php b/MessageParser.php deleted file mode 100644 index 51a6941..0000000 --- a/MessageParser.php +++ /dev/null @@ -1,47 +0,0 @@ -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($data); - if ($from->WebSocket->frame->isCoalesced()) { - // check if masked - // close if not - - if ($from->WebSocket->frame->getOpcode() > 2) { - // take action on the control frame - - unset($from->WebSocket->frame); - - return; - } - - // 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 - - $nextFrame = $from->WebSocket->version->newFrame(); - $nextFrame->addBuffer($from->WebSocket->frame->extractOverflow()); - - $from->WebSocket->message->addFrame($from->WebSocket->frame); - $from->WebSocket->frame = $nextFrame; - } - - if ($from->WebSocket->message->isCoalesced()) { - $parsed = (string)$from->WebSocket->message; - unset($from->WebSocket->message); - - return $parsed; - } - } -} \ No newline at end of file diff --git a/Version/MessageInterface.php b/Version/MessageInterface.php index 4c21114..1dc91fc 100644 --- a/Version/MessageInterface.php +++ b/Version/MessageInterface.php @@ -5,11 +5,6 @@ namespace Ratchet\WebSocket\Version; * @todo Consider making parent interface/composite for Message/Frame with (isCoalesced, getOpcdoe, getPayloadLength, getPayload) */ interface MessageInterface { - /** - * @alias getPayload - */ - function __toString(); - /** * @return bool */ diff --git a/Version/RFC6455.php b/Version/RFC6455.php index c01613b..2f06de8 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -1,6 +1,10 @@ _verifier = new HandshakeVerifier; + /** + * @var Ratchet\MessageInterface + */ + protected $coalescedCallback; + + public function __construct(MessageInterface $coalescedCallback = null) { + $this->_verifier = new HandshakeVerifier; + $this->coalescedCallback = $coalescedCallback; } /** @@ -29,6 +39,9 @@ class RFC6455 implements VersionInterface { return ($this->getVersionNumber() === $version); } + /** + * {@inheritdoc} + */ public function getVersionNumber() { return 13; } @@ -51,18 +64,96 @@ class RFC6455 implements VersionInterface { return new Response(101, $headers); } + /** + * {@inheritdoc} + */ + public function onMessage(ConnectionInterface $from, $data) { + $overflow = ''; + + if (!isset($from->WebSocket->message)) { + $from->WebSocket->message = $this->newMessage(); + } + + // There is a frame fragment attatched to the connection, add to it + if (!isset($from->WebSocket->frame)) { + $from->WebSocket->frame = $this->newFrame(); + } + + $from->WebSocket->frame->addBuffer($data); + if ($from->WebSocket->frame->isCoalesced()) { + $frame = $from->WebSocket->frame; + + if (!$frame->isMasked()) { + unset($from->WebSocket->frame); + + $from->send($this->newFrame($frame::CLOSE_PROTOCOL, true, $frame::OP_CLOSE)); + $from->getConnection()->close(); + + return; + } + + $opcode = $frame->getOpcode(); + + if ($opcode > 2) { + switch ($opcode) { + case $frame::OP_CLOSE: + $from->send($frame->unMaskPayload()); + $from->getConnection()->close(); +// $from->send(Frame::create(Frame::CLOSE_NORMAL, true, Frame::OP_CLOSE)); + + return; + break; + case $frame::OP_PING: + $from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG)); + break; + case $frame::OP_PONG: + break; + default: + return $from->close($frame::CLOSE_PROTOCOL); + break; + } + + $overflow = $from->WebSocket->frame->extractOverflow(); + + unset($from->WebSocket->frame, $frame, $opcode); + + if (strlen($overflow) > 0) { + $this->onMessage($from, $overflow); + } + + return; + } + + $overflow = $from->WebSocket->frame->extractOverflow(); + + $from->WebSocket->message->addFrame($from->WebSocket->frame); + unset($from->WebSocket->frame); + } + + if ($from->WebSocket->message->isCoalesced()) { + $parsed = $from->WebSocket->message->getPayload(); + unset($from->WebSocket->message); + + $this->coalescedCallback->onMessage($from, $parsed); + } + + if (strlen($overflow) > 0) { + $this->onMessage($from, $overflow); + } + } + /** * @return RFC6455\Message */ public function newMessage() { - return new RFC6455\Message; + return new Message; } /** * @return RFC6455\Frame */ - public function newFrame() { - return new RFC6455\Frame; + public function newFrame($payload = null, $final = true, $opcode = 1) { + return new Frame($payload, $final, $opcode); } /** @@ -71,7 +162,7 @@ class RFC6455 implements VersionInterface { * @return string */ public function frame($message, $mask = true) { - return RFC6455\Frame::create($message)->data; + return $this->newFrame($message)->data; } /** diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 03601ff..21ad94d 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -10,6 +10,19 @@ class Frame implements FrameInterface { const OP_PING = 9; const OP_PONG = 10; + const CLOSE_NORMAL = 1000; + const CLOSE_GOING_AWAY = 1001; + const CLOSE_PROTOCOL = 1002; + const CLOSE_BAD_DATA = 1003; + const CLOSE_NO_STATUS = 1005; + const CLOSE_ABNORMAL = 1006; + const CLOSE_BAD_PAYLOAD = 1007; + const CLOSE_POLICY = 1008; + const CLOSE_TOO_BIG = 1009; + const CLOSE_MAND_EXT = 1010; + const CLOSE_SRV_ERR = 1011; + const CLOSE_TLS = 1015; + const MASK_LENGTH = 4; /** @@ -22,7 +35,7 @@ class Frame implements FrameInterface { * Number of bytes received from the frame * @var int */ - public $_bytes_rec = 0; + public $bytesRecvd = 0; /** * Number of bytes in the payload (as per framing protocol) @@ -30,20 +43,9 @@ class Frame implements FrameInterface { */ protected $_pay_len_def = -1; - /** - * @param string A valid UTF-8 string to send over the wire - * @param bool Is the final frame in a message - * @param int The opcode of the frame, see constants - * @param bool Mask the payload - * @return Frame - * @throws InvalidArgumentException If the payload is not a valid UTF-8 string - * @throws LengthException If the payload is too big - */ - public static function create($payload, $final = true, $opcode = 1) { - $frame = new static(); - - if (!mb_check_encoding($payload, 'UTF-8')) { - throw new \InvalidArgumentException("Payload is not a valid UTF-8 string"); + public function __construct($payload = null, $final = true, $opcode = 1) { + if (null === $payload) { + return; } $raw = (int)(boolean)$final . sprintf('%07b', (int)$opcode); @@ -57,9 +59,20 @@ class Frame implements FrameInterface { $raw .= sprintf('%08b', 127) . sprintf('%064b', $plLen); } - $frame->addBuffer(static::encode($raw) . $payload); + $this->addBuffer(static::encode($raw) . $payload); + } - return $frame; + /** + * @param string A valid UTF-8 string to send over the wire + * @param bool Is the final frame in a message + * @param int The opcode of the frame, see constants + * @param bool Mask the payload + * @return Frame + * @throws InvalidArgumentException If the payload is not a valid UTF-8 string + * @throws LengthException If the payload is too big + */ + public static function create($payload, $final = true, $opcode = 1) { + return new static($payload, $final, $opcode); } /** @@ -93,7 +106,7 @@ class Frame implements FrameInterface { return false; } - return $this->_bytes_rec >= $payload_length + $payload_start; + return $this->bytesRecvd >= $payload_length + $payload_start; } /** @@ -103,14 +116,14 @@ class Frame implements FrameInterface { $buf = (string)$buf; $this->data .= $buf; - $this->_bytes_rec += strlen($buf); + $this->bytesRecvd += strlen($buf); } /** * {@inheritdoc} */ public function isFinal() { - if ($this->_bytes_rec < 1) { + if ($this->bytesRecvd < 1) { throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); } @@ -123,8 +136,8 @@ class Frame implements FrameInterface { * {@inheritdoc} */ public function isMasked() { - if ($this->_bytes_rec < 2) { - throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); + if ($this->bytesRecvd < 2) { + throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); } return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->data, 1, 1))), 0, 1)); @@ -140,7 +153,7 @@ class Frame implements FrameInterface { $start = 1 + $this->getNumPayloadBytes(); - if ($this->_bytes_rec < $start + static::MASK_LENGTH) { + if ($this->bytesRecvd < $start + static::MASK_LENGTH) { throw new \UnderflowException('Not enough data buffered to calculate the masking key'); } @@ -186,7 +199,7 @@ class Frame implements FrameInterface { $this->data = substr_replace($this->data, static::encode(substr_replace($byte, '1', 0, 1)), 1, 1); $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); - $this->_bytes_rec += static::MASK_LENGTH; + $this->bytesRecvd += static::MASK_LENGTH; $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; @@ -209,7 +222,7 @@ class Frame implements FrameInterface { $this->data = substr_replace($this->data, static::encode(substr_replace($byte, '0', 0, 1)), 1, 1); $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); - $this->_bytes_rec -= static::MASK_LENGTH; + $this->bytesRecvd -= static::MASK_LENGTH; $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); return $this; @@ -236,7 +249,7 @@ class Frame implements FrameInterface { * {@inheritdoc} */ public function getOpcode() { - if ($this->_bytes_rec < 1) { + if ($this->bytesRecvd < 1) { throw new \UnderflowException('Not enough bytes received to determine opcode'); } @@ -249,7 +262,7 @@ class Frame implements FrameInterface { * @throws UnderflowException If the buffer doesn't have enough data to determine this */ protected function getFirstPayloadVal() { - if ($this->_bytes_rec < 2) { + if ($this->bytesRecvd < 2) { throw new \UnderflowException('Not enough bytes received'); } @@ -261,7 +274,7 @@ class Frame implements FrameInterface { * @throws UnderflowException */ protected function getNumPayloadBits() { - if ($this->_bytes_rec < 2) { + if ($this->bytesRecvd < 2) { throw new \UnderflowException('Not enough bytes received'); } @@ -315,7 +328,7 @@ class Frame implements FrameInterface { } $byte_length = $this->getNumPayloadBytes(); - if ($this->_bytes_rec < 1 + $byte_length) { + if ($this->bytesRecvd < 1 + $byte_length) { throw new \UnderflowException('Not enough data buffered to determine payload length'); } @@ -365,7 +378,7 @@ class Frame implements FrameInterface { $endPoint = $this->getPayloadLength(); $endPoint += $this->getPayloadStartingByte(); - if ($this->_bytes_rec > $endPoint) { + if ($this->bytesRecvd > $endPoint) { $overflow = substr($this->data, $endPoint); $this->data = substr($this->data, 0, $endPoint); diff --git a/Version/VersionInterface.php b/Version/VersionInterface.php index d0839b9..630c62a 100644 --- a/Version/VersionInterface.php +++ b/Version/VersionInterface.php @@ -1,11 +1,12 @@ hasVersion()) { + if ($data instanceof FrameInterface) { + $data = $data->data; + } elseif (isset($this->WebSocket->version)) { // need frame caching $data = $this->WebSocket->version->frame($data, false); } @@ -29,7 +29,12 @@ class WsConnection extends AbstractConnectionDecorator { $this->getConnection()->send($data); } - public function close() { + /** + * {@inheritdoc} + * @todo If code is 1000 send close frame - false is close w/o frame...? + */ + public function close($code = 1000) { + $this->send(Frame::create($code, true, Frame::OP_CLOSE)); // send close frame with code 1000 // ??? @@ -38,23 +43,4 @@ class WsConnection extends AbstractConnectionDecorator { $this->getConnection()->close(); // temporary } - - /** - * @return boolean - * @internal - */ - public function hasVersion() { - return (null === $this->version); - } - - /** - * Set the WebSocket protocol version to communicate with - * @param Ratchet\WebSocket\Version\VersionInterface - * @internal - */ - public function setVersion(VersionInterface $version) { - $this->WebSocket->version = $version; - - return $this; - } } \ No newline at end of file diff --git a/WsServer.php b/WsServer.php index d1e0f2b..20b2a6d 100644 --- a/WsServer.php +++ b/WsServer.php @@ -2,6 +2,7 @@ namespace Ratchet\WebSocket; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; +use Ratchet\WebSocket\Version; use Guzzle\Http\Message\RequestInterface; use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; @@ -32,11 +33,6 @@ class WsServer implements MessageComponentInterface { */ protected $connections; - /** - * @var MessageParser - */ - protected $messager; - /** * For now, array_push accepted subprotocols to this array * @deprecated @@ -54,10 +50,15 @@ class WsServer implements MessageComponentInterface { * @param Ratchet\MessageComponentInterface Your application to run with WebSockets */ public function __construct(MessageComponentInterface $component) { - mb_internal_encoding('UTF-8'); + //mb_internal_encoding('UTF-8'); - $this->handshaker = new HandshakeNegotiator; - $this->messager = new MessageParser; + $this->handshaker = new HandshakeNegotiator(); + + $this->handshaker + ->enableVersion(new Version\RFC6455($component)) + ->enableVersion(new Version\HyBi10($component)) + //->enableVersion(new Version\Hixie76) + ; $this->_decorating = $component; $this->connections = new \SplObjectStorage; @@ -83,7 +84,7 @@ class WsServer implements MessageComponentInterface { $conn = $this->connections[$from]; if (true !== $conn->WebSocket->established) { - if (null === ($response = $this->handshaker->onData($conn, $msg))) { + if (null === ($response = $this->handshaker->onMessage($conn, $msg))) { return; } @@ -103,9 +104,7 @@ class WsServer implements MessageComponentInterface { return $this->_decorating->onOpen($conn); } - if (null !== ($parsed = $this->messager->onData($conn, $msg))) { - $this->_decorating->onMessage($conn, $parsed); - } + $conn->WebSocket->version->onMessage($conn, $msg); } /** From ac660017feeb9f2c7c6b18c246362483693f6594 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 12 Jun 2012 20:49:05 -0400 Subject: [PATCH 17/24] CS --- HandshakeNegotiator.php | 7 +------ Version/RFC6455.php | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/HandshakeNegotiator.php b/HandshakeNegotiator.php index 1b80b13..07eac1e 100644 --- a/HandshakeNegotiator.php +++ b/HandshakeNegotiator.php @@ -115,12 +115,7 @@ class HandshakeNegotiator implements MessageInterface { public function disableVersion($versionId) { unset($this->versions[$versionId]); - $this->versionString = ''; - - foreach ($this->versions as $id => $object) { - $this->versionString .= "{$id}, "; - } - $this->versionString = substr($this->versionString, 0, -2); + $this->versionString = implode(',', array_keys($this->versions)); return $this; } diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 2f06de8..f734042 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -172,6 +172,6 @@ class RFC6455 implements VersionInterface { * @internal */ public function sign($key) { - return base64_encode(sha1($key . static::GUID, 1)); + return base64_encode(sha1($key . static::GUID, true)); } } \ No newline at end of file From 1c34e12be8b4801e0af84b82e401cba0ca94521b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 13 Jun 2012 22:46:08 -0400 Subject: [PATCH 18/24] [WebSocket] Separating responsibilities Separated HandshakeNegotiator into HttpRequestParser and VersionManager Moved WsConnection to Version specific Connection --- HandshakeNegotiator.php | 130 --------------------------------- HttpRequestParser.php | 57 +++++++++++++++ Version/RFC6455.php | 35 ++++++--- Version/RFC6455/Connection.php | 37 ++++++++++ VersionManager.php | 77 +++++++++++++++++++ WsConnection.php | 46 ------------ WsServer.php | 69 +++++++++++------ 7 files changed, 243 insertions(+), 208 deletions(-) delete mode 100644 HandshakeNegotiator.php create mode 100644 HttpRequestParser.php create mode 100644 Version/RFC6455/Connection.php create mode 100644 VersionManager.php delete mode 100644 WsConnection.php diff --git a/HandshakeNegotiator.php b/HandshakeNegotiator.php deleted file mode 100644 index 07eac1e..0000000 --- a/HandshakeNegotiator.php +++ /dev/null @@ -1,130 +0,0 @@ -WebSocket->handshakeBuffer = ''; - } - - /** - * @param WsConnection - * @param string Data stream to buffer - * @return Guzzle\Http\Message\Response|null Response object if it's done parsing, null if there's more to be buffered - * @throws HttpException - */ - public function onMessage(ConnectionInterface $conn, $data) { - $conn->WebSocket->handshakeBuffer .= $data; - - if (strlen($conn->WebSocket->handshakeBuffer) >= (int)$this->maxSize) { - return new Response(413, array('X-Powered-By' => \Ratchet\VERSION)); - } - - if ($this->isEom($conn->WebSocket->handshakeBuffer)) { - $conn->WebSocket->request = RequestFactory::getInstance()->fromMessage($conn->WebSocket->handshakeBuffer); - - if (null === ($version = $this->getVersion($conn->WebSocket->request))) { - return new Response(400, array( - 'Sec-WebSocket-Version' => $this->getSupportedVersionString() - , 'X-Powered-By' => \Ratchet\VERSION - )); - } - - // TODO: confirm message is buffered - // Hixie requires the body to complete the handshake (6 characters long) - is that 6 ASCII or UTF-8 characters? - // Update VersionInterface to check for this, ::canHandshake() maybe - // return if can't, continue buffering - - $response = $version->handshake($conn->WebSocket->request); - $response->setHeader('X-Powered-By', \Ratchet\VERSION); - - // This needs to be decoupled - $conn->WebSocket->version = $version; - unset($conn->WebSocket->handshakeBuffer); - - return $response; - } - } - - /** - * Determine if the message has been buffered as per the HTTP specification - * @param string - * @return boolean - */ - public function isEom($message) { - //return (static::EOM === substr($message, 0 - strlen(static::EOM))); - return (boolean)strpos($message, static::EOM); - } - - /** - * Get the protocol negotiator for the request, if supported - * @param Guzzle\Http\Message\RequestInterface - * @return Ratchet\WebSocket\Version\VersionInterface - */ - public function getVersion(RequestInterface $request) { - foreach ($this->versions as $version) { - if ($version->isProtocol($request)) { - return $version; - } - } - } - - /** - * Enable support for a specific version of the WebSocket protocol - * @param Ratchet\WebSocket\Vesion\VersionInterface - * @return HandshakeNegotiator - */ - public function enableVersion(VersionInterface $version) { - $this->versions[$version->getVersionNumber()] = $version; - - if (empty($this->versionString)) { - $this->versionString = (string)$version->getVersionNumber(); - } else { - $this->versionString .= ", {$version->getVersionNumber()}"; - } - - return $this; - } - - /** - * Disable support for a specific WebSocket protocol version - * @param int The version ID to un-support - * @return HandshakeNegotiator - */ - public function disableVersion($versionId) { - unset($this->versions[$versionId]); - - $this->versionString = implode(',', array_keys($this->versions)); - - return $this; - } - - /** - * Get a string of version numbers supported (comma delimited) - * @return string - */ - public function getSupportedVersionString() { - return $this->versionString; - } -} \ No newline at end of file diff --git a/HttpRequestParser.php b/HttpRequestParser.php new file mode 100644 index 0000000..73bbcf7 --- /dev/null +++ b/HttpRequestParser.php @@ -0,0 +1,57 @@ +httpBuffer)) { + $context->httpBuffer = ''; + } + + $context->httpBuffer .= $data; + + if (strlen($context->httpBuffer) > (int)$this->maxSize) { + throw new \OverflowException("Maximum buffer size of {$this->maxSize} exceeded parsing HTTP header"); + + //return new Response(413, array('X-Powered-By' => \Ratchet\VERSION)); + } + + if ($this->isEom($context->httpBuffer)) { + $request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); + + unset($context->httpBuffer); + + return $request; + } + } + + /** + * Determine if the message has been buffered as per the HTTP specification + * @param string + * @return boolean + */ + public function isEom($message) { + //return (static::EOM === substr($message, 0 - strlen(static::EOM))); + return (boolean)strpos($message, static::EOM); + } +} \ No newline at end of file diff --git a/Version/RFC6455.php b/Version/RFC6455.php index f734042..29673ea 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -5,6 +5,7 @@ use Ratchet\MessageInterface; use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier; use Ratchet\WebSocket\Version\RFC6455\Message; use Ratchet\WebSocket\Version\RFC6455\Frame; +use Ratchet\WebSocket\Version\RFC6455\Connection; use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\Response; @@ -20,14 +21,8 @@ class RFC6455 implements VersionInterface { */ protected $_verifier; - /** - * @var Ratchet\MessageInterface - */ - protected $coalescedCallback; - - public function __construct(MessageInterface $coalescedCallback = null) { - $this->_verifier = new HandshakeVerifier; - $this->coalescedCallback = $coalescedCallback; + public function __construct() { + $this->_verifier = new HandshakeVerifier; } /** @@ -52,6 +47,8 @@ class RFC6455 implements VersionInterface { */ public function handshake(RequestInterface $request) { if (true !== $this->_verifier->verifyAll($request)) { + // new header with 4xx error message + throw new \InvalidArgumentException('Invalid HTTP header'); } @@ -59,13 +56,31 @@ class RFC6455 implements VersionInterface { 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) + , 'X-Powered-By' => \Ratchet\VERSION ); return new Response(101, $headers); } /** - * {@inheritdoc} + * @param Ratchet\ConnectionInterface + * @return Ratchet\WebSocket\Version\RFC6455\Connection + */ + public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { + $upgraded = new Connection($conn); + + if (!isset($upgraded->WebSocket)) { + $upgraded->WebSocket = new \StdClass; + } + + $upgraded->WebSocket->coalescedCallback = $coalescedCallback; + + return $upgraded; + } + + /** + * @param Ratchet\WebSocket\Version\RFC6455\Connection + * @param string */ public function onMessage(ConnectionInterface $from, $data) { $overflow = ''; @@ -134,7 +149,7 @@ class RFC6455 implements VersionInterface { $parsed = $from->WebSocket->message->getPayload(); unset($from->WebSocket->message); - $this->coalescedCallback->onMessage($from, $parsed); + $from->WebSocket->coalescedCallback->onMessage($from, $parsed); } if (strlen($overflow) > 0) { diff --git a/Version/RFC6455/Connection.php b/Version/RFC6455/Connection.php new file mode 100644 index 0000000..ee746fc --- /dev/null +++ b/Version/RFC6455/Connection.php @@ -0,0 +1,37 @@ +data; + } else { + $frame = new Frame($msg); + $data = $frame->data; + } + + $this->getConnection()->send($data); + } + + /** + * {@inheritdoc} + */ + public function close($code = 1000) { + $frame = new Frame($code, true, Frame::OP_CLOSE); + + $this->send($frame->data); + + $this->getConnection()->close(); + } +} \ No newline at end of file diff --git a/VersionManager.php b/VersionManager.php new file mode 100644 index 0000000..942fc95 --- /dev/null +++ b/VersionManager.php @@ -0,0 +1,77 @@ +versions as $version) { + if ($version->isProtocol($request)) { + return $version; + } + } + + throw new \InvalidArgumentException("Version not found"); + } + + /** + * @param Guzzle\Http\Message\RequestInterface + * @return bool + */ + public function isVersionEnabled(RequestInterface $request) { + foreach ($this->versions as $version) { + if ($version->isProtocol($request)) { + return true; + } + } + + return false; + } + + /** + * Enable support for a specific version of the WebSocket protocol + * @param Ratchet\WebSocket\Vesion\VersionInterface + * @return HandshakeNegotiator + */ + public function enableVersion(VersionInterface $version) { + $this->versions[$version->getVersionNumber()] = $version; + + if (empty($this->versionString)) { + $this->versionString = (string)$version->getVersionNumber(); + } else { + $this->versionString .= ", {$version->getVersionNumber()}"; + } + + return $this; + } + + /** + * Disable support for a specific WebSocket protocol version + * @param int The version ID to un-support + * @return HandshakeNegotiator + */ + public function disableVersion($versionId) { + unset($this->versions[$versionId]); + + $this->versionString = implode(',', array_keys($this->versions)); + + return $this; + } + + /** + * Get a string of version numbers supported (comma delimited) + * @return string + */ + public function getSupportedVersionString() { + return $this->versionString; + } +} \ No newline at end of file diff --git a/WsConnection.php b/WsConnection.php deleted file mode 100644 index 87ce4e6..0000000 --- a/WsConnection.php +++ /dev/null @@ -1,46 +0,0 @@ -WebSocket = new \StdClass; - } - - public function send($data) { - if ($data instanceof FrameInterface) { - $data = $data->data; - } elseif (isset($this->WebSocket->version)) { - // need frame caching - $data = $this->WebSocket->version->frame($data, false); - } - - $this->getConnection()->send($data); - } - - /** - * {@inheritdoc} - * @todo If code is 1000 send close frame - false is close w/o frame...? - */ - public function close($code = 1000) { - $this->send(Frame::create($code, true, Frame::OP_CLOSE)); - // send close frame with code 1000 - - // ??? - - // profit - - $this->getConnection()->close(); // temporary - } -} \ No newline at end of file diff --git a/WsServer.php b/WsServer.php index 20b2a6d..6ee3e7a 100644 --- a/WsServer.php +++ b/WsServer.php @@ -3,8 +3,7 @@ namespace Ratchet\WebSocket; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; use Ratchet\WebSocket\Version; -use Guzzle\Http\Message\RequestInterface; -use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; +use Guzzle\Http\Message\Response; /** * The adapter to handle WebSocket requests/responses @@ -15,12 +14,17 @@ use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; */ class WsServer implements MessageComponentInterface { /** - * Negotiates upgrading the HTTP connection to a WebSocket connection - * It contains useful configuration properties and methods - * @var HandshakeNegotiator + * Buffers incoming HTTP requests returning a Guzzle Request when coalesced + * @var HttpRequestParser * @note May not expose this in the future, may do through facade methods */ - public $handshaker; + public $reqParser; + + /** + * Manage the various WebSocket versions to support + * @var VersionManager + */ + protected $versioner; /** * Decorated component @@ -52,9 +56,10 @@ class WsServer implements MessageComponentInterface { public function __construct(MessageComponentInterface $component) { //mb_internal_encoding('UTF-8'); - $this->handshaker = new HandshakeNegotiator(); + $this->reqParser = new HttpRequestParser; + $this->versioner = new VersionManager; - $this->handshaker + $this->versioner ->enableVersion(new Version\RFC6455($component)) ->enableVersion(new Version\HyBi10($component)) //->enableVersion(new Version\Hixie76) @@ -68,12 +73,13 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onOpen(ConnectionInterface $conn) { - $wsConn = new WsConnection($conn); + //$wsConn = new WsConnection($conn); - $this->connections->attach($conn, $wsConn); + //$this->connections->attach($conn, $wsConn); - $this->handshaker->onOpen($wsConn); + //$this->reqParser->onOpen($wsConn); + $conn->WebSocket = new \StdClass; $conn->WebSocket->established = false; } @@ -81,15 +87,28 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onMessage(ConnectionInterface $from, $msg) { - $conn = $this->connections[$from]; - - if (true !== $conn->WebSocket->established) { - if (null === ($response = $this->handshaker->onMessage($conn, $msg))) { + if (true !== $from->WebSocket->established) { + if (null === ($request = $this->reqParser->onMessage($from, $msg))) { return; } + if (!$this->versioner->isVersionEnabled($request)) { + $response = new Response(400, array( + 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() + , 'X-Powered-By' => \Ratchet\VERSION + )); + + $from->send((string)$response); + $from->close(); + + return; + } + + $from->WebSocket->version = $this->versioner->getVersion($request); + $response = $from->WebSocket->version->handshake($request); + // This needs to be refactored later on, incorporated with routing - if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { + if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); } @@ -99,24 +118,30 @@ class WsServer implements MessageComponentInterface { return $from->close(); } - $conn->WebSocket->established = true; + $upgraded = $from->WebSocket->version->upgradeConnection($from, $this->_decorating); - return $this->_decorating->onOpen($conn); + $this->connections->attach($from, $upgraded); + + $upgraded->WebSocket->established = true; + + return $this->_decorating->onOpen($upgraded); } - $conn->WebSocket->version->onMessage($conn, $msg); + $from->WebSocket->version->onMessage($this->connections[$from], $msg); } /** * {@inheritdoc} */ public function onClose(ConnectionInterface $conn) { - $decor = $this->connections[$conn]; - $this->connections->detach($conn); + if ($this->connections->contains($conn)) { + $decor = $this->connections[$conn]; + $this->connections->detach($conn); + } // WS::onOpen is not called when the socket connects, it's call when the handshake is done // The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet - if ($decor->WebSocket->established) { + if (isset($decor)) { $this->_decorating->onClose($decor); } } From 305865a938666030da01892381217b761518ff50 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 13 Jun 2012 22:51:42 -0400 Subject: [PATCH 19/24] [WebSocket] Cleanup Added a couple coverage unit tests CS --- Version/RFC6455/Frame.php | 10 +++++----- WsServer.php | 7 ------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 21ad94d..126ad4c 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -3,11 +3,11 @@ namespace Ratchet\WebSocket\Version\RFC6455; use Ratchet\WebSocket\Version\FrameInterface; class Frame implements FrameInterface { - const OP_CONTINUE = 0; - const OP_TEXT = 1; - const OP_BINARY = 2; - const OP_CLOSE = 8; - const OP_PING = 9; + const OP_CONTINUE = 0; + const OP_TEXT = 1; + const OP_BINARY = 2; + const OP_CLOSE = 8; + const OP_PING = 9; const OP_PONG = 10; const CLOSE_NORMAL = 1000; diff --git a/WsServer.php b/WsServer.php index 6ee3e7a..dc388d1 100644 --- a/WsServer.php +++ b/WsServer.php @@ -8,7 +8,6 @@ use Guzzle\Http\Message\Response; /** * The adapter to handle WebSocket requests/responses * This is a mediator between the Server and your application to handle real-time messaging through a web browser - * @todo Separate this class into a two classes: Component and a protocol handler * @link http://ca.php.net/manual/en/ref.http.php * @link http://dev.w3.org/html5/websockets/ */ @@ -73,12 +72,6 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onOpen(ConnectionInterface $conn) { - //$wsConn = new WsConnection($conn); - - //$this->connections->attach($conn, $wsConn); - - //$this->reqParser->onOpen($wsConn); - $conn->WebSocket = new \StdClass; $conn->WebSocket->established = false; } From 412f5c2d077e40a77ae2f3bb4ac37cb63d2354f0 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 10:54:26 -0400 Subject: [PATCH 20/24] [WebSocket] Re-scoped variable Protecting Frame::$data to prevent overflow error Correct frame contents is fetched from ::getContents() --- Version/RFC6455.php | 2 +- Version/RFC6455/Connection.php | 13 ++++--------- Version/RFC6455/Frame.php | 10 +++++++++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 29673ea..0d04513 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -177,7 +177,7 @@ class RFC6455 implements VersionInterface { * @return string */ public function frame($message, $mask = true) { - return $this->newFrame($message)->data; + return $this->newFrame($message)->getContents(); } /** diff --git a/Version/RFC6455/Connection.php b/Version/RFC6455/Connection.php index ee746fc..badd488 100644 --- a/Version/RFC6455/Connection.php +++ b/Version/RFC6455/Connection.php @@ -14,23 +14,18 @@ class Connection extends AbstractConnectionDecorator { } public function send($msg) { - if ($msg instanceof FrameInterface) { - $data = $msg->data; - } else { - $frame = new Frame($msg); - $data = $frame->data; + if (!($msg instanceof FrameInterface)) { + $msg = new Frame($msg); } - $this->getConnection()->send($data); + $this->getConnection()->send($msg->getContents()); } /** * {@inheritdoc} */ public function close($code = 1000) { - $frame = new Frame($code, true, Frame::OP_CLOSE); - - $this->send($frame->data); + $this->send(new Frame($code, true, Frame::OP_CLOSE)); $this->getConnection()->close(); } diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 126ad4c..12fde14 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -29,7 +29,7 @@ class Frame implements FrameInterface { * The contents of the frame * @var string */ - public $data = ''; + protected $data = ''; /** * Number of bytes received from the frame @@ -367,6 +367,14 @@ class Frame implements FrameInterface { return $payload; } + /** + * Get the raw contents of the frame + * @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow + */ + public function getContents() { + return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength()); + } + /** * Sometimes clients will concatinate more than one frame over the wire * This method will take the extra bytes off the end and return them From f0a277cec94fc844c225faafbb243d876539680d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 11:24:18 -0400 Subject: [PATCH 21/24] [WebSocket] Cleanup Removed some obsolete code Handshakes always returns a response --- HttpRequestParser.php | 7 ++----- Version/RFC6455.php | 15 ++++++--------- Version/RFC6455/Connection.php | 5 ----- WsServer.php | 29 ++++++++++++++++++----------- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/HttpRequestParser.php b/HttpRequestParser.php index 73bbcf7..f8159df 100644 --- a/HttpRequestParser.php +++ b/HttpRequestParser.php @@ -5,7 +5,6 @@ use Ratchet\ConnectionInterface; use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; use Ratchet\WebSocket\Version\VersionInterface; use Guzzle\Http\Message\RequestInterface; -use Guzzle\Http\Message\Response; class HttpRequestParser implements MessageInterface { const EOM = "\r\n\r\n"; @@ -18,9 +17,9 @@ class HttpRequestParser implements MessageInterface { public $maxSize = 4096; /** - * @param StdClass + * @param Ratchet\ConnectionInterface * @param string Data stream to buffer - * @return Guzzle\Http\Message\Response|null Response object if it's done parsing, null if there's more to be buffered + * @return Guzzle\Http\Message\RequestInterface|null * @throws OverflowException */ public function onMessage(ConnectionInterface $context, $data) { @@ -32,8 +31,6 @@ class HttpRequestParser implements MessageInterface { if (strlen($context->httpBuffer) > (int)$this->maxSize) { throw new \OverflowException("Maximum buffer size of {$this->maxSize} exceeded parsing HTTP header"); - - //return new Response(413, array('X-Powered-By' => \Ratchet\VERSION)); } if ($this->isEom($context->httpBuffer)) { diff --git a/Version/RFC6455.php b/Version/RFC6455.php index 0d04513..21841cd 100644 --- a/Version/RFC6455.php +++ b/Version/RFC6455.php @@ -43,23 +43,17 @@ class RFC6455 implements VersionInterface { /** * {@inheritdoc} - * @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where? */ public function handshake(RequestInterface $request) { if (true !== $this->_verifier->verifyAll($request)) { - // new header with 4xx error message - - throw new \InvalidArgumentException('Invalid HTTP header'); + return new Response(400); } - $headers = array( + return new Response(101, array( 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) - , 'X-Powered-By' => \Ratchet\VERSION - ); - - return new Response(101, $headers); + )); } /** @@ -112,8 +106,11 @@ class RFC6455 implements VersionInterface { if ($opcode > 2) { switch ($opcode) { case $frame::OP_CLOSE: + $from->close($frame->getPayload()); +/* $from->send($frame->unMaskPayload()); $from->getConnection()->close(); +*/ // $from->send(Frame::create(Frame::CLOSE_NORMAL, true, Frame::OP_CLOSE)); return; diff --git a/Version/RFC6455/Connection.php b/Version/RFC6455/Connection.php index badd488..1e1110b 100644 --- a/Version/RFC6455/Connection.php +++ b/Version/RFC6455/Connection.php @@ -2,17 +2,12 @@ namespace Ratchet\WebSocket\Version\RFC6455; use Ratchet\ConnectionInterface; use Ratchet\AbstractConnectionDecorator; -use Ratchet\WebSocket\Version\VersionInterface; use Ratchet\WebSocket\Version\FrameInterface; /** * {@inheritdoc} */ class Connection extends AbstractConnectionDecorator { - public function __construct(ConnectionInterface $conn) { - parent::__construct($conn); - } - public function send($msg) { if (!($msg instanceof FrameInterface)) { $msg = new Frame($msg); diff --git a/WsServer.php b/WsServer.php index dc388d1..4108b76 100644 --- a/WsServer.php +++ b/WsServer.php @@ -81,24 +81,21 @@ class WsServer implements MessageComponentInterface { */ public function onMessage(ConnectionInterface $from, $msg) { if (true !== $from->WebSocket->established) { - if (null === ($request = $this->reqParser->onMessage($from, $msg))) { - return; + try { + if (null === ($request = $this->reqParser->onMessage($from, $msg))) { + return; + } + } catch (\OverflowException $oe) { + return $this->close($from, 413); } if (!$this->versioner->isVersionEnabled($request)) { - $response = new Response(400, array( - 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() - , 'X-Powered-By' => \Ratchet\VERSION - )); - - $from->send((string)$response); - $from->close(); - - return; + return $this->close($from); } $from->WebSocket->version = $this->versioner->getVersion($request); $response = $from->WebSocket->version->handshake($request); + $response->setHeader('X-Powered-By', \Ratchet\VERSION); // This needs to be refactored later on, incorporated with routing if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { @@ -150,6 +147,16 @@ class WsServer implements MessageComponentInterface { } } + protected function close(ConnectionInterface $conn, $code = 400) { + $response = new Response($code, array( + 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() + , 'X-Powered-By' => \Ratchet\VERSION + )); + + $conn->send((string)$response); + $conn->close(); + } + /** * @param string * @return boolean From 8770c361bc4cca64318fb60483b346207e0b2d3d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 15:07:52 -0400 Subject: [PATCH 22/24] [WebSocket] Hixie refactoring Created parent interface for messages and frames Created Hixie Connection Applied updated interfaces to Hixie versions Removed __toString on msgs/frames since there could be 2-3 types returned --- Version/DataInterface.php | 28 ++++++++++++++ Version/FrameInterface.php | 28 +++----------- Version/Hixie76.php | 61 ++++++++++++++++++++++-------- Version/Hixie76/Connection.php | 16 ++++++++ Version/Hixie76/Frame.php | 8 ++++ Version/Hixie76/Message.php | 68 ---------------------------------- Version/MessageInterface.php | 20 +--------- Version/RFC6455/Connection.php | 4 +- Version/RFC6455/Message.php | 24 ++++++++---- Version/VersionInterface.php | 13 +++++-- WsServer.php | 2 +- 11 files changed, 133 insertions(+), 139 deletions(-) create mode 100644 Version/DataInterface.php create mode 100644 Version/Hixie76/Connection.php delete mode 100644 Version/Hixie76/Message.php diff --git a/Version/DataInterface.php b/Version/DataInterface.php new file mode 100644 index 0000000..ef44565 --- /dev/null +++ b/Version/DataInterface.php @@ -0,0 +1,28 @@ +getHeader('Sec-WebSocket-Key2', true)); } + /** + * {@inheritdoc} + */ public function getVersionNumber() { return 0; } @@ -46,25 +53,47 @@ class Hixie76 implements VersionInterface { return $response; } - /** - * @return Hixie76\Message - */ - public function newMessage() { - return new Hixie76\Message; - } - - /** - * @return Hixie76\Frame - */ - public function newFrame() { - return new Hixie76\Frame; - } - /** * {@inheritdoc} */ - public function frame($message, $mask = true) { - return chr(0) . $message . chr(255); + public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { + $upgraded = new Connection($conn); + + if (!isset($upgraded->WebSocket)) { + $upgraded->WebSocket = new \StdClass; + } + + $upgraded->WebSocket->coalescedCallback = $coalescedCallback; + + return $upgraded; + } + + public function onMessage(ConnectionInterface $from, $data) { + $overflow = ''; + + if (!isset($from->WebSocket->frame)) { + $from->WebSocket->frame = $this->newFrame(); + } + + $from->WebSocket->frame->addBuffer($data); + if ($from->WebSocket->frame->isCoalesced()) { + $overflow = $from->WebSocket->frame->extractOverflow(); + + $parsed = $from->WebSocket->frame->getPayload(); + unset($from->WebSocket->frame); + + $from->WebSocket->coalescedCallback->onMessage($from, $parsed); + + unset($from->WebSocket->frame); + } + + if (strlen($overflow) > 0) { + $this->onMessage($from, $overflow); + } + } + + public function newFrame() { + return new Frame; } public function generateKeyNumber($key) { diff --git a/Version/Hixie76/Connection.php b/Version/Hixie76/Connection.php new file mode 100644 index 0000000..2011ac1 --- /dev/null +++ b/Version/Hixie76/Connection.php @@ -0,0 +1,16 @@ +getConnection()->send(chr(0) . $msg . chr(255)); + } + + public function close() { + return $this->getConnection()->close(); + } +} \ No newline at end of file diff --git a/Version/Hixie76/Frame.php b/Version/Hixie76/Frame.php index b9af87d..a172207 100644 --- a/Version/Hixie76/Frame.php +++ b/Version/Hixie76/Frame.php @@ -75,4 +75,12 @@ class Frame implements FrameInterface { return substr($this->_data, 1, strlen($this->_data) - 2); } + + public function getContents() { + return $this->_data; + } + + public function extractOverflow() { + return ''; + } } \ No newline at end of file diff --git a/Version/Hixie76/Message.php b/Version/Hixie76/Message.php deleted file mode 100644 index 818d95d..0000000 --- a/Version/Hixie76/Message.php +++ /dev/null @@ -1,68 +0,0 @@ -getPayload(); - } - - /** - * {@inheritdoc} - */ - public function isCoalesced() { - if (!($this->_frame instanceof FrameInterface)) { - return false; - } - - return $this->_frame->isCoalesced(); - } - - /** - * {@inheritdoc} - */ - public function addFrame(FrameInterface $fragment) { - if (null !== $this->_frame) { - throw new \OverflowException('Hixie76 does not support multiple framing of messages'); - } - - $this->_frame = $fragment; - - return $this; - } - - /** - * {@inheritdoc} - */ - public function getOpcode() { - // Hixie76 only supported text messages - return 1; - } - - /** - * {@inheritdoc} - */ - public function getPayloadLength() { - throw new \DomainException('Please sir, may I have some code? (' . __FUNCTION__ . ')'); - } - - /** - * {@inheritdoc} - */ - public function getPayload() { - if (!$this->isCoalesced()) { - throw new \UnderflowException('Message has not been fully buffered yet'); - } - - return $this->_frame->getPayload(); - } -} \ No newline at end of file diff --git a/Version/MessageInterface.php b/Version/MessageInterface.php index 1dc91fc..dc126d6 100644 --- a/Version/MessageInterface.php +++ b/Version/MessageInterface.php @@ -1,15 +1,7 @@ _frames = new \SplDoublyLinkedList; } - /** - * {@inheritdoc} - */ - public function __toString() { - return $this->getPayload(); - } - /** * {@inheritdoc} */ @@ -87,4 +80,21 @@ class Message implements MessageInterface { return $buffer; } + + /** + * {@inheritdoc} + */ + public function getContents() { + if (!$this->isCoalesced()) { + throw new \UnderflowException("Message has not been put back together yet"); + } + + $buffer = ''; + + foreach ($this->_frames as $frame) { + $buffer .= $frame->getContents(); + } + + return $buffer; + } } \ No newline at end of file diff --git a/Version/VersionInterface.php b/Version/VersionInterface.php index 630c62a..9a3c29e 100644 --- a/Version/VersionInterface.php +++ b/Version/VersionInterface.php @@ -1,6 +1,7 @@ versioner ->enableVersion(new Version\RFC6455($component)) ->enableVersion(new Version\HyBi10($component)) - //->enableVersion(new Version\Hixie76) + ->enableVersion(new Version\Hixie76) ; $this->_decorating = $component; From c0d0c9d2bc2378537cb1100b5bd53030cdb4c787 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 16:07:16 -0400 Subject: [PATCH 23/24] [WebSocket] Fixed missing request headers bug Accidentally removed HTTP request headers from connections Added them back --- WsServer.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/WsServer.php b/WsServer.php index 90cf305..efdabfc 100644 --- a/WsServer.php +++ b/WsServer.php @@ -93,7 +93,9 @@ class WsServer implements MessageComponentInterface { return $this->close($from); } + $from->WebSocket->request = $request; $from->WebSocket->version = $this->versioner->getVersion($request); + $response = $from->WebSocket->version->handshake($request); $response->setHeader('X-Powered-By', \Ratchet\VERSION); From 2b9e4a4aee42f5ded574e2f853fa3674dc995e07 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 15 Jun 2012 09:56:16 -0400 Subject: [PATCH 24/24] CS --- WsServer.php | 28 +++++++++++++++++----------- WsServerInterface.php | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/WsServer.php b/WsServer.php index efdabfc..2e44a8c 100644 --- a/WsServer.php +++ b/WsServer.php @@ -22,8 +22,9 @@ class WsServer implements MessageComponentInterface { /** * Manage the various WebSocket versions to support * @var VersionManager + * @note May not expose this in the future, may do through facade methods */ - protected $versioner; + public $versioner; /** * Decorated component @@ -149,16 +150,6 @@ class WsServer implements MessageComponentInterface { } } - protected function close(ConnectionInterface $conn, $code = 400) { - $response = new Response($code, array( - 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() - , 'X-Powered-By' => \Ratchet\VERSION - )); - - $conn->send((string)$response); - $conn->close(); - } - /** * @param string * @return boolean @@ -194,4 +185,19 @@ class WsServer implements MessageComponentInterface { return substr($string, 0, -1); } + + /** + * Close a connection with an HTTP response + * @param Ratchet\ConnectionInterface + * @param int HTTP status code + */ + protected function close(ConnectionInterface $conn, $code = 400) { + $response = new Response($code, array( + 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() + , 'X-Powered-By' => \Ratchet\VERSION + )); + + $conn->send((string)$response); + $conn->close(); + } } \ No newline at end of file diff --git a/WsServerInterface.php b/WsServerInterface.php index 8cb378d..91a83cd 100644 --- a/WsServerInterface.php +++ b/WsServerInterface.php @@ -5,7 +5,7 @@ interface WsServerInterface { /** * If any component in a stack supports a WebSocket sub-protocol return each supported in an array * @return array - * @temporary This method may be removed in future version (note tha twill not break code, just make some code obsolete) + * @temporary This method may be removed in future version (note that will not break code, just make some code obsolete) */ function getSubProtocols(); } \ No newline at end of file