From d075b99c264f08da857adbc619e0477c1ee377c4 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 19 May 2012 23:36:32 -0400 Subject: [PATCH 01/28] [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 --- README.md | 3 +- composer.json | 1 + composer.lock | 16 +----- .../WebSocket/Version/FrameInterface.php | 6 --- .../Version/RFC6455/HandshakeVerifier.php | 14 ++--- .../Version/RFC6455/HandshakeVerifierTest.php | 11 ++-- .../Tests/WebSocket/Version/RFC6455Test.php | 51 ++++++++++++------- 7 files changed, 52 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 776668b..ba5e54b 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,14 @@ Build up your application through simple interfaces and re-use your application ##WebSocket Compliance * Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time) -* Tested on Chrome 18 - 16, Firefox 6 - 12, Safari 5, iOS 4.2, iOS 5 +* Tested on Chrome 13 - 19, Firefox 6 - 12, Safari 5.0.1+, iOS 4.2, iOS 5 ##Requirements Shell access is required and a dedicated machine with root access is recommended. To avoid proxy/firewall blockage it's recommended WebSockets are run on port 80, which requires root access. Note that you can not run two applications (Apache and Ratchet) on the same port, thus the requirement for a separate machine (for now). +PHP 5.3.2 (or higher) is required with mbstring enabled (*--enable-mbstring* flag during compile time). PHP5.4 is recommended for its performance improvements. Cookies from your domain will be passed to the socket server, allowing you to identify users. Accessing your website's session data in Ratchet requires you to use [Symfony2 Sessions](http://symfony.com/doc/master/components/http_foundation/sessions.html) on your website. diff --git a/composer.json b/composer.json index a0a09a8..3e2c07f 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ } , "require": { "php": ">=5.3.2" + , "ext-mbstring": "*" , "guzzle/guzzle": "2.5.*" , "symfony/http-foundation": "2.1.*" , "react/socket": "dev-master" diff --git a/composer.lock b/composer.lock index 378a998..d85b62a 100644 --- a/composer.lock +++ b/composer.lock @@ -1,5 +1,5 @@ { - "hash": "cbea4e3e4d74a22ba34d4edf2ce44df3", + "hash": "253370657f067dacf104d5fae531f20a", "packages": [ { "package": "evenement/evenement", @@ -32,13 +32,6 @@ "version": "dev-master", "source-reference": "eb82542e8ec9506096caf7c528564c740a214f56" }, - { - "package": "symfony/event-dispatcher", - "version": "dev-master", - "source-reference": "0b58a4019befc0bd038bc0ec0165101d5dd31754", - "alias-pretty-version": "2.1.x-dev", - "alias-version": "2.1.9999999.9999999-dev" - }, { "package": "symfony/http-foundation", "version": "dev-master", @@ -50,13 +43,6 @@ "package": "symfony/http-foundation", "version": "dev-master", "source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0" - }, - { - "package": "symfony/http-foundation", - "version": "dev-master", - "source-reference": "cf8e8324c68ce584525502702866485f17f1c8a5", - "alias-pretty-version": "2.1.x-dev", - "alias-version": "2.1.9999999.9999999-dev" } ], "packages-dev": null, diff --git a/src/Ratchet/WebSocket/Version/FrameInterface.php b/src/Ratchet/WebSocket/Version/FrameInterface.php index 57b27ea..61451cc 100644 --- a/src/Ratchet/WebSocket/Version/FrameInterface.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php b/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php index 6898e2e..e9172ef 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php +++ b/src/Ratchet/WebSocket/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)); } /** diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/HandshakeVerifierTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/HandshakeVerifierTest.php index 2e51648..3822ff3 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/HandshakeVerifierTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/HandshakeVerifierTest.php @@ -18,7 +18,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase { public static function methodProvider() { return array( array(true, 'GET') - , array(false, 'get') // I'm not sure if this is valid or not, need to check standard + , array(true, 'get') + , array(true, 'Get') , array(false, 'POST') , array(false, 'DELETE') , array(false, 'PUT') @@ -64,6 +65,7 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase { , array(false, '/chat#bad') , array(false, 'nope') , array(false, '/ ಠ_ಠ ') + , array(false, '/✖') ); } @@ -91,7 +93,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase { public static function upgradeProvider() { return array( array(true, 'websocket') - , array(false, 'Websocket') + , array(true, 'Websocket') + , array(true, 'webSocket') , array(false, null) , array(false, '') ); @@ -107,7 +110,7 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase { public static function connectionProvider() { return array( array(true, 'Upgrade') - , array(false, 'upgrade') + , array(true, 'upgrade') , array(true, 'keep-alive, Upgrade') , array(true, 'Upgrade, keep-alive') , array(true, 'keep-alive, Upgrade, something') @@ -133,6 +136,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase { , array(false, 'Hello World') , array(false, '1234567890123456') , array(false, '123456789012345678901234') + , array(true, base64_encode('UTF8allthngs+✓')) + , array(true, 'dGhlIHNhbXBsZSBub25jZQ==') ); } diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php index 2e6dd0b..99ba1f6 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php @@ -3,33 +3,26 @@ namespace Ratchet\Tests\WebSocket\Version; use Ratchet\WebSocket\Version\RFC6455; use Ratchet\WebSocket\Version\RFC6455\Frame; use Guzzle\Http\Message\RequestFactory; +use Guzzle\Http\Message\EntityEnclosingRequest; /** * @covers Ratchet\WebSocket\Version\RFC6455 */ class RFC6455Test extends \PHPUnit_Framework_TestCase { - protected $_version; + protected $version; public function setUp() { - $this->_version = new RFC6455(); + $this->version = new RFC6455; } /** - * Is this useful? - */ - public function testClassImplementsVersionInterface() { - $constraint = $this->isInstanceOf('\\Ratchet\\WebSocket\\Version\\VersionInterface'); - $this->assertThat($this->_version, $constraint); - } - - /** - * @dataProvider HandshakeProvider + * @dataProvider handshakeProvider */ public function testKeySigningForHandshake($key, $accept) { - $this->assertEquals($accept, $this->_version->sign($key)); + $this->assertEquals($accept, $this->version->sign($key)); } - public static function HandshakeProvider() { + public static function handshakeProvider() { return array( array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=') , array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') @@ -56,8 +49,8 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase { } public function testUnframeMatchesPreFraming() { - $string = 'Hello World!'; - $framed = $this->_version->frame($string); + $string = 'Hello World!'; + $framed = $this->version->frame($string); $frame = new Frame; $frame->addBuffer($framed); @@ -77,6 +70,26 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase { , 'Sec-WebSocket-Version' => 13 ); + public function caseVariantProvider() { + return array( + array('Sec-Websocket-Version') + , array('sec-websocket-version') + , array('SEC-WEBSOCKET-VERSION') + , array('sEC-wEBsOCKET-vERSION') + ); + } + + /** + * @dataProvider caseVariantProvider + */ + public function testIsProtocolWithCaseInsensitivity($headerName) { + $header = static::$good_header; + unset($header['Sec-WebSocket-Version']); + $header[$headerName] = 13; + + $this->assertTrue($this->version->isProtocol(new EntityEnclosingRequest('get', '/', $header))); + } + /** * A helper function to try and quickly put together a valid WebSocket HTTP handshake * but optionally replace a piece to an invalid value for failure testing @@ -119,18 +132,18 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase { $request = RequestFactory::getInstance()->fromMessage($header); if ($pass) { - $this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $this->_version->handshake($request)); + $this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $this->version->handshake($request)); } else { $this->setExpectedException('InvalidArgumentException'); - $this->_version->handshake($request); + $this->version->handshake($request); } } public function testNewMessage() { - $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->_version->newMessage()); + $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->version->newMessage()); } public function testNewFrame() { - $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->_version->newFrame()); + $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->version->newFrame()); } } \ No newline at end of file From 935866c036fb93e0c29d030c446719d884305a57 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 19 May 2012 23:43:30 -0400 Subject: [PATCH 02/28] [WebSocket] Refactoring Separated handshake negotiation into its own class `HandshakeNegotiator` deals with Request/Response classes These changes are geared towards separate responsibility Refs #29 --- src/Ratchet/WebSocket/HandshakeNegotiator.php | 140 ++++++++++++++++ src/Ratchet/WebSocket/Version/Hixie76.php | 7 +- src/Ratchet/WebSocket/Version/HyBi10.php | 6 +- src/Ratchet/WebSocket/Version/RFC6455.php | 11 +- .../WebSocket/Version/VersionInterface.php | 16 +- src/Ratchet/WebSocket/WsConnection.php | 38 ++++- src/Ratchet/WebSocket/WsServer.php | 149 ++++++------------ .../WebSocket/HandshakeNegotiatorTest.php | 103 ++++++++++++ 8 files changed, 355 insertions(+), 115 deletions(-) create mode 100644 src/Ratchet/WebSocket/HandshakeNegotiator.php create mode 100644 tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php diff --git a/src/Ratchet/WebSocket/HandshakeNegotiator.php b/src/Ratchet/WebSocket/HandshakeNegotiator.php new file mode 100644 index 0000000..3f19bf1 --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/Hixie76.php b/src/Ratchet/WebSocket/Version/Hixie76.php index 941eec4..1692eb0 100644 --- a/src/Ratchet/WebSocket/Version/Hixie76.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/HyBi10.php b/src/Ratchet/WebSocket/Version/HyBi10.php index 734d7e4..bbf3dc4 100644 --- a/src/Ratchet/WebSocket/Version/HyBi10.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 50d7632..1be2246 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/VersionInterface.php b/src/Ratchet/WebSocket/Version/VersionInterface.php index 1602641..d0839b9 100644 --- a/src/Ratchet/WebSocket/Version/VersionInterface.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsConnection.php b/src/Ratchet/WebSocket/WsConnection.php index c0f127b..a346fa9 100644 --- a/src/Ratchet/WebSocket/WsConnection.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index b69119c..619ae9e 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 diff --git a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php new file mode 100644 index 0000000..288e30a --- /dev/null +++ b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php @@ -0,0 +1,103 @@ +parser = new HandshakeNegotiator(); + } + + public function headersProvider() { + return array( + array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") + , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") + , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") + , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") + ); + } + + /** + * @dataProvider headersProvider + */ + public function testIsEom($expected, $message) { + $this->assertEquals($expected, $this->parser->isEom($message)); + } + + public function testFluentInterface() { + $rfc = new RFC6455; + + $this->assertSame($this->parser, $this->parser->disableVersion(13)); + $this->assertSame($this->parser, $this->parser->enableVersion($rfc)); + } + + public function testGetVersion() { + $this->parser->disableVersion(13); + $rfc = new RFC6455; + $this->parser->enableVersion($rfc); + + $req = new EntityEnclosingRequest('get', '/', array( + 'Host' => 'socketo.me' + , 'Sec-WebSocket-Version' => 13 + )); + + $this->assertSame($rfc, $this->parser->getVersion($req)); + } + + public function testGetNopeVersionAndDisable() { + $this->parser->disableVersion(13); + + $req = new EntityEnclosingRequest('get', '/', array( + 'Host' => 'socketo.me' + , 'Sec-WebSocket-Version' => 13 + )); + + $this->assertNull($this->parser->getVersion($req)); + } + + public function testGetSupportedVersionString() { + $v1 = new RFC6455; + $v2 = new HyBi10; + + $parser = new HandshakeNegotiator(); + $parser->enableVersion($v1); + $parser->enableVersion($v2); + + $string = $parser->getSupportedVersionString(); + $values = explode(',', $string); + + $this->assertContains($v1->getVersionNumber(), $values); + $this->assertContains($v2->getVersionNumber(), $values); + } + + public function testGetSupportedVersionAfterRemoval() { + $this->parser->disableVersion(0); + + $values = explode(',', $this->parser->getSupportedVersionString()); + + $this->assertEquals(2, count($values)); + $this->assertFalse(array_search(0, $values)); + } + + public function testBufferOverflowResponse() { + $conn = new WsConnection(new ConnectionStub); + $this->parser->onOpen($conn); + + $this->parser->maxSize = 20; + + $this->assertNull($this->parser->onData($conn, "GET / HTTP/1.1\r\n")); + $this->assertGreaterThan(400, $this->parser->onData($conn, "Header-Is: Too Big")->getStatusCode()); + } +} \ No newline at end of file From 0ef0410ab656ae065b2ccf68a5a43a0fe022bd1d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 19 May 2012 23:57:20 -0400 Subject: [PATCH 03/28] Cleanup CS Removed a var_dump Removed garbage from a unit test --- src/Ratchet/WebSocket/Version/Hixie76.php | 6 +++--- src/Ratchet/WebSocket/Version/RFC6455.php | 1 - src/Ratchet/WebSocket/WsServer.php | 1 - .../Tests/WebSocket/Version/Hixie76Test.php | 20 +++---------------- 4 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/Hixie76.php b/src/Ratchet/WebSocket/Version/Hixie76.php index 1692eb0..6f91435 100644 --- a/src/Ratchet/WebSocket/Version/Hixie76.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 1be2246..0fcf009 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 619ae9e..c746f3c 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 { diff --git a/tests/Ratchet/Tests/WebSocket/Version/Hixie76Test.php b/tests/Ratchet/Tests/WebSocket/Version/Hixie76Test.php index 9773c13..4aacacc 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/Hixie76Test.php +++ b/tests/Ratchet/Tests/WebSocket/Version/Hixie76Test.php @@ -9,7 +9,7 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase { protected $_version; public function setUp() { - $this->_version = new Hixie76(); + $this->_version = new Hixie76; } public function testClassImplementsVersionInterface() { @@ -18,27 +18,13 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase { } /** - * @dataProvider HandshakeProvider - */ - public function INCOMPLETEtestKeySigningForHandshake($key, $accept) { -// $this->assertEquals($accept, $this->_version->sign($key)); - } - - public static function HandshakeProvider() { - return array( - array('', '') - , array('', '') - ); - } - - /** - * @dataProvider KeyProvider + * @dataProvider keyProvider */ public function testKeySigningForHandshake($accept, $key) { $this->assertEquals($accept, $this->_version->generateKeyNumber($key)); } - public static function KeyProvider() { + public static function keyProvider() { return array( array(179922739, '17 9 G`ZD9 2 2b 7X 3 /r90') , array('', '17 9 G`ZD9 2 2b 7X 3 /r91') From ac7cc55d5f77b07b1ebb2b0aef14d29567606deb Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 00:11:04 -0400 Subject: [PATCH 04/28] Oops - TCI fix --- src/Ratchet/WebSocket/WsServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index c746f3c..2ca5e75 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 e9825e0ba7bd608c2b3303fdcd264e017f3e2721 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 01:04:09 -0400 Subject: [PATCH 05/28] [WebSocket] Message refactoring Moved the message buffering into its own class --- src/Ratchet/WebSocket/MessageParser.php | 37 +++++++++++++++++++++++ src/Ratchet/WebSocket/WsServer.php | 39 ++----------------------- 2 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 src/Ratchet/WebSocket/MessageParser.php diff --git a/src/Ratchet/WebSocket/MessageParser.php b/src/Ratchet/WebSocket/MessageParser.php new file mode 100644 index 0000000..4ac294a --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 2ca5e75..1dac9e2 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 530469295bd7f142034f2ed0e0eec02de5ccf182 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 02:03:53 -0400 Subject: [PATCH 06/28] [WebSocket] Un-framing encoding Parsing incoming RFC6455 frames with mb_string --- .../WebSocket/Version/RFC6455/Frame.php | 22 +++++++++---------- src/Ratchet/WebSocket/WsServer.php | 3 +-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 60c8a2f..4314b95 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 1dac9e2..b7a85fc 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 ff07104316a9c49b33589fa3dc376f797d419980 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 20 May 2012 13:24:37 -0400 Subject: [PATCH 07/28] AutobahnTestSuite Added files to test Ratchet against the AutobahnTestSuite Bumped version v0.2b Updated how to handle control frames to run the test suite --- src/Ratchet/ConnectionInterface.php | 2 +- src/Ratchet/WebSocket/MessageParser.php | 5 +++-- tests/Ratchet/Tests/AbFuzzyServer.php | 22 +++++++++++++++++++ .../WebSocket/HandshakeNegotiatorTest.php | 6 ++--- tests/ab-wstest-fuzzyconf.json | 11 ++++++++++ tests/ab-wstest-server.php | 10 +++++++++ 6 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 tests/Ratchet/Tests/AbFuzzyServer.php create mode 100644 tests/ab-wstest-fuzzyconf.json create mode 100644 tests/ab-wstest-server.php diff --git a/src/Ratchet/ConnectionInterface.php b/src/Ratchet/ConnectionInterface.php index 8a22472..8a8ef34 100644 --- a/src/Ratchet/ConnectionInterface.php +++ b/src/Ratchet/ConnectionInterface.php @@ -1,7 +1,7 @@ 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 diff --git a/tests/Ratchet/Tests/AbFuzzyServer.php b/tests/Ratchet/Tests/AbFuzzyServer.php new file mode 100644 index 0000000..025cb4b --- /dev/null +++ b/tests/Ratchet/Tests/AbFuzzyServer.php @@ -0,0 +1,22 @@ +send($msg); + } + + public function onClose(ConnectionInterface $conn) { + } + + public function onError(ConnectionInterface $conn, \Exception $e) { + echo $e->getMessage() . "\n"; + + $conn->close(); + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php index 288e30a..cfef252 100644 --- a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php +++ b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php @@ -22,10 +22,10 @@ class HandshakeNegotiatorTest extends \PHPUnit_Framework_TestCase { return array( array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") - , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") - , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") + , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") + , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") - , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") + , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") ); } diff --git a/tests/ab-wstest-fuzzyconf.json b/tests/ab-wstest-fuzzyconf.json new file mode 100644 index 0000000..e435908 --- /dev/null +++ b/tests/ab-wstest-fuzzyconf.json @@ -0,0 +1,11 @@ + +{ + "options": {"failByDrop": false}, + "outdir": "../reports/ab", + + "servers": [{"agent": "Ratchet/v0.2b", "url": "ws://localhost:8000", "options": {"version": 18}}], + + "cases": ["*"], + "exclude-cases": ["9.*"], + "exclude-agent-cases": {} +} diff --git a/tests/ab-wstest-server.php b/tests/ab-wstest-server.php new file mode 100644 index 0000000..d7ee557 --- /dev/null +++ b/tests/ab-wstest-server.php @@ -0,0 +1,10 @@ +run(); From 9a27adfcd8897a4aa9c363762786328782f3b976 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 21 May 2012 11:38:24 -0400 Subject: [PATCH 08/28] [Tests] Multiple AB WS tests --- tests/ab-wstest-fuzzyconf.json | 16 ++++++++++------ tests/ab-wstest-libevent.php | 12 ++++++++++++ tests/ab-wstest-server.php | 10 ---------- tests/ab-wstest-stream.php | 12 ++++++++++++ 4 files changed, 34 insertions(+), 16 deletions(-) create mode 100644 tests/ab-wstest-libevent.php delete mode 100644 tests/ab-wstest-server.php create mode 100644 tests/ab-wstest-stream.php diff --git a/tests/ab-wstest-fuzzyconf.json b/tests/ab-wstest-fuzzyconf.json index e435908..ec4a1c9 100644 --- a/tests/ab-wstest-fuzzyconf.json +++ b/tests/ab-wstest-fuzzyconf.json @@ -1,11 +1,15 @@ { - "options": {"failByDrop": false}, - "outdir": "../reports/ab", + "options": {"failByDrop": false} + , "outdir": "../reports/ab" - "servers": [{"agent": "Ratchet/v0.2b", "url": "ws://localhost:8000", "options": {"version": 18}}], + , "servers": [ + {"agent": "Ratchet-libevent/v0.2b", "url": "ws://localhost:8000", "options": {"version": 18}} + , {"agent": "Ratchet-stream/v0.2b", "url": "ws://localhost:8001", "options": {"version": 18}} + ] - "cases": ["*"], - "exclude-cases": ["9.*"], - "exclude-agent-cases": {} + , "cases": ["*"] + , "exclude-cases": ["9.*"] + , "limit-exclude-cases": ["2.*", "3.*", "4.*", "5.*", "6.*", "7.*", "8.*", "9.2.*", "9.3.*", "9.4.*", "9.5.*", "9.6.*", "9.7.*", "9.8.*"] + , "exclude-agent-cases": {} } diff --git a/tests/ab-wstest-libevent.php b/tests/ab-wstest-libevent.php new file mode 100644 index 0000000..af120e4 --- /dev/null +++ b/tests/ab-wstest-libevent.php @@ -0,0 +1,12 @@ +listen(8000, '0.0.0.0'); + + $server = new Ratchet\Server\IoServer($app, $sock, $loop); + $server->run(); diff --git a/tests/ab-wstest-server.php b/tests/ab-wstest-server.php deleted file mode 100644 index d7ee557..0000000 --- a/tests/ab-wstest-server.php +++ /dev/null @@ -1,10 +0,0 @@ -run(); diff --git a/tests/ab-wstest-stream.php b/tests/ab-wstest-stream.php new file mode 100644 index 0000000..d0b08dd --- /dev/null +++ b/tests/ab-wstest-stream.php @@ -0,0 +1,12 @@ +listen(8001, '0.0.0.0'); + + $server = new Ratchet\Server\IoServer($app, $sock, $loop); + $server->run(); From e38f81af6414be392bef2e5debacdecf542995cb Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Mon, 21 May 2012 13:16:33 -0400 Subject: [PATCH 09/28] Minor cleanups --- src/Ratchet/AbstractConnectionDecorator.php | 1 + src/Ratchet/WebSocket/MessageParser.php | 2 ++ src/Ratchet/WebSocket/Version/RFC6455/Message.php | 1 - src/Ratchet/WebSocket/WsServer.php | 15 ++------------- 4 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Ratchet/AbstractConnectionDecorator.php b/src/Ratchet/AbstractConnectionDecorator.php index c85fa32..8db272a 100644 --- a/src/Ratchet/AbstractConnectionDecorator.php +++ b/src/Ratchet/AbstractConnectionDecorator.php @@ -4,6 +4,7 @@ namespace Ratchet; /** * Wraps ConnectionInterface objects via the decorator pattern but allows * parameters to bubble through with magic methods + * @todo It sure would be nice if I could make most of this a trait... */ abstract class AbstractConnectionDecorator implements ConnectionInterface { /** diff --git a/src/Ratchet/WebSocket/MessageParser.php b/src/Ratchet/WebSocket/MessageParser.php index 11fc388..017c4f5 100644 --- a/src/Ratchet/WebSocket/MessageParser.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Message.php b/src/Ratchet/WebSocket/Version/RFC6455/Message.php index 5385475..1071115 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Message.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index b7a85fc..40c3973 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 e42abf912f3a944a7a47d23dd9d3f2d4001a99cf Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 1 Jun 2012 23:07:25 -0400 Subject: [PATCH 10/28] 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. --- src/Ratchet/WebSocket/HandshakeNegotiator.php | 4 +-- src/Ratchet/WebSocket/Version/Hixie76.php | 2 +- .../WebSocket/Version/RFC6455/Frame.php | 25 +++++++++++-------- .../Version/RFC6455/HandshakeVerifier.php | 12 ++++----- src/Ratchet/WebSocket/WsServer.php | 2 +- 5 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/Ratchet/WebSocket/HandshakeNegotiator.php b/src/Ratchet/WebSocket/HandshakeNegotiator.php index 3f19bf1..0cdef10 100644 --- a/src/Ratchet/WebSocket/HandshakeNegotiator.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/Hixie76.php b/src/Ratchet/WebSocket/Version/Hixie76.php index 6f91435..4601b51 100644 --- a/src/Ratchet/WebSocket/Version/Hixie76.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 4314b95..8a17d6a 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php b/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php index e9172ef..93d7928 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 40c3973..d1e0f2b 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 070a4f1c34abe8ca01e6b298c26b07637c75bade Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 1 Jun 2012 23:41:51 -0400 Subject: [PATCH 11/28] [Tests] Coverage and extension checking --- tests/Ratchet/Tests/AbstractConnectionDecoratorTest.php | 1 + tests/Ratchet/Tests/Session/SessionComponentTest.php | 4 ++++ tests/Ratchet/Tests/WebSocket/WsServerTest.php | 2 ++ 3 files changed, 7 insertions(+) diff --git a/tests/Ratchet/Tests/AbstractConnectionDecoratorTest.php b/tests/Ratchet/Tests/AbstractConnectionDecoratorTest.php index 197537d..707460c 100644 --- a/tests/Ratchet/Tests/AbstractConnectionDecoratorTest.php +++ b/tests/Ratchet/Tests/AbstractConnectionDecoratorTest.php @@ -5,6 +5,7 @@ use Ratchet\Tests\Mock\Connection; /** * @covers Ratchet\AbstractConnectionDecorator + * @covers Ratchet\ConnectionInterface */ class AbstractConnectionDecoratorTest extends \PHPUnit_Framework_TestCase { protected $mock; diff --git a/tests/Ratchet/Tests/Session/SessionComponentTest.php b/tests/Ratchet/Tests/Session/SessionComponentTest.php index 56c37c2..5594c06 100644 --- a/tests/Ratchet/Tests/Session/SessionComponentTest.php +++ b/tests/Ratchet/Tests/Session/SessionComponentTest.php @@ -43,6 +43,10 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase { * I think I have severly butchered this test...it's not so much of a unit test as it is a full-fledged component test */ public function testConnectionValueFromPdo() { + if (!extension_loaded('PDO')) { + return $this->markTestSkipped(); + } + $sessionId = md5('testSession'); $dbOptions = array( diff --git a/tests/Ratchet/Tests/WebSocket/WsServerTest.php b/tests/Ratchet/Tests/WebSocket/WsServerTest.php index 46079fb..b02e301 100644 --- a/tests/Ratchet/Tests/WebSocket/WsServerTest.php +++ b/tests/Ratchet/Tests/WebSocket/WsServerTest.php @@ -5,6 +5,8 @@ use Ratchet\Tests\Mock\Component as MockComponent; /** * @covers Ratchet\WebSocket\WsServer + * @covers Ratchet\ComponentInterface + * @covers Ratchet\MessageComponentInterface */ class WsServerTest extends \PHPUnit_Framework_TestCase { protected $comp; From 291bd5da5a7eeecec9832aaaa64a57ea4106956a Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 15:44:18 -0400 Subject: [PATCH 12/28] [WebSocket] RFC6455 Framing work New code to create a frame Unit tests for new code API cleanup --- src/Ratchet/WebSocket/HandshakeNegotiator.php | 1 + src/Ratchet/WebSocket/Version/RFC6455.php | 2 + .../WebSocket/Version/RFC6455/Frame.php | 96 +++++++++++++++---- src/Ratchet/WebSocket/WsConnection.php | 9 +- .../WebSocket/Version/RFC6455/FrameTest.php | 79 +++++++++------ 5 files changed, 136 insertions(+), 51 deletions(-) diff --git a/src/Ratchet/WebSocket/HandshakeNegotiator.php b/src/Ratchet/WebSocket/HandshakeNegotiator.php index 0cdef10..1bce9a3 100644 --- a/src/Ratchet/WebSocket/HandshakeNegotiator.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 0fcf009..59cbdde 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 8a17d6a..5a7a7ff 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsConnection.php b/src/Ratchet/WebSocket/WsConnection.php index a346fa9..de6c3fa 100644 --- a/src/Ratchet/WebSocket/WsConnection.php +++ b/src/Ratchet/WebSocket/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); diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index 84352fb..65827e0 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -19,21 +19,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->_frame = new Frame; } - protected static function convert($in) { - if (strlen($in) > 8) { - $out = ''; - - while (strlen($in) > 8) { - $out .= static::convert(substr($in, 0, 8)); - $in = substr($in, 8); - } - - return $out; - } - - return pack('C', bindec($in)); - } - /** * This is a data provider * @param string The UTF8 message @@ -67,7 +52,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->setExpectedException('\UnderflowException'); if (!empty($bin)) { - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($bin)); } call_user_func(array($this->_frame, $method)); @@ -93,7 +78,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider firstByteProvider */ public function testFinCodeFromBits($fin, $opcode, $bin) { - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($fin, $this->_frame->isFinal()); } @@ -109,7 +94,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider firstByteProvider */ public function testOpcodeFromBits($fin, $opcode, $bin) { - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($opcode, $this->_frame->getOpcode()); } @@ -136,8 +121,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider payloadLengthDescriptionProvider */ public function testFirstPayloadDesignationValue($bits, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getFirstPayloadVal'); @@ -150,8 +135,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider payloadLengthDescriptionProvider */ public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getNumPayloadBits'); @@ -172,8 +157,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider secondByteProvider */ public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($masked, $this->_frame->isMasked()); } @@ -190,8 +175,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider secondByteProvider */ public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($payload_length, $this->_frame->getPayloadLength()); } @@ -230,8 +215,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @todo I I wrote the dataProvider incorrectly, skpping for now */ public function testGetMaskingKey($mask) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($this->_secondByteMaskedSPL)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($this->_secondByteMaskedSPL)); $this->_frame->addBuffer($mask); $this->assertEquals($mask, $this->_frame->getMaskingKey()); @@ -265,4 +250,42 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($msg, $this->_frame->getPayload()); } + + public function testCreate() { + $len = 65525; + $len = 65575; + $pl = $this->generateRandomString($len); + + $frame = Frame::create($pl, true, Frame::OP_PING); + + $this->assertTrue($frame->isFinal()); + $this->assertEquals(Frame::OP_PING, $frame->getOpcode()); + $this->assertFalse($frame->isMasked()); + $this->assertEquals($len, $frame->getPayloadLength()); + $this->assertEquals($pl, $frame->getPayload()); + } + + protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { + $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง + + $useChars = array(); + for($i = 0; $i < $length; $i++) { + $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; + } + + if($addSpaces === true) { + array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); + } + + if($addNumbers === true) { + array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); + } + + shuffle($useChars); + + $randomString = trim(implode('', $useChars)); + $randomString = substr($randomString, 0, $length); + + return $randomString; + } } \ No newline at end of file From 7790ef39a172fe6f49c2aa0b9f568645c368af98 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 21:11:29 -0400 Subject: [PATCH 13/28] [WebSocket] Frame overflow --- src/Ratchet/WebSocket/Version/RFC6455.php | 79 +------------------ .../WebSocket/Version/RFC6455/Frame.php | 24 +++++- .../WebSocket/Version/RFC6455/FrameTest.php | 30 ++++++- 3 files changed, 53 insertions(+), 80 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 59cbdde..c01613b 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 5a7a7ff..af8e961 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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 diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index 65827e0..b011671 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -241,8 +241,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider UnframeMessageProvider */ public function testCheckPiecingTogetherMessage($msg, $encoded) { -// return $this->markTestIncomplete('Ran out of time, had to attend to something else, come finish me!'); - $framed = base64_decode($encoded); for ($i = 0, $len = strlen($framed);$i < $len; $i++) { $this->_frame->addBuffer(substr($framed, $i, 1)); @@ -265,6 +263,34 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($pl, $frame->getPayload()); } + public function testExtractOverflow() { + $string1 = $this->generateRandomString(); + $frame1 = Frame::create($string1); + + $string2 = $this->generateRandomString(); + $frame2 = Frame::create($string2); + + $cat = new Frame; + $cat->addBuffer($frame1->data . $frame2->data); + + $this->assertEquals($string1, $cat->getPayload()); + + $uncat = new Frame; + $uncat->addBuffer($cat->extractOverflow()); + + $this->assertEquals($string1, $cat->getPayload()); + $this->assertEquals($string2, $uncat->getPayload()); + } + + public function testEmptyExtractOverflow() { + $string = $this->generateRandomString(); + $frame = Frame::create($string); + + $this->assertEquals($string, $frame->getPayload()); + $this->assertEquals('', $frame->extractOverflow()); + $this->assertEquals($string, $frame->getPayload()); + } + protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง From 54479da9d58b06ab5c3f7dfdc821dc506c6042d1 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 22:08:27 -0400 Subject: [PATCH 14/28] [WebSocket] Messaging Fluent interface on MessageInterface::addFrame RFC6455 Message unit tests RFC handling TCP concatenation (refs #31) --- src/Ratchet/WebSocket/MessageParser.php | 6 +- .../WebSocket/Version/Hixie76/Message.php | 2 + .../WebSocket/Version/MessageInterface.php | 1 + .../WebSocket/Version/RFC6455/Frame.php | 5 -- .../WebSocket/Version/RFC6455/Message.php | 5 +- .../WebSocket/Version/RFC6455/FrameTest.php | 11 ++- .../WebSocket/Version/RFC6455/MessageTest.php | 70 +++++++++++++++++++ 7 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php diff --git a/src/Ratchet/WebSocket/MessageParser.php b/src/Ratchet/WebSocket/MessageParser.php index 017c4f5..b28592d 100644 --- a/src/Ratchet/WebSocket/MessageParser.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/Hixie76/Message.php b/src/Ratchet/WebSocket/Version/Hixie76/Message.php index f783e8b..818d95d 100644 --- a/src/Ratchet/WebSocket/Version/Hixie76/Message.php +++ b/src/Ratchet/WebSocket/Version/Hixie76/Message.php @@ -36,6 +36,8 @@ class Message implements MessageInterface { } $this->_frame = $fragment; + + return $this; } /** diff --git a/src/Ratchet/WebSocket/Version/MessageInterface.php b/src/Ratchet/WebSocket/Version/MessageInterface.php index 90d0179..4c21114 100644 --- a/src/Ratchet/WebSocket/Version/MessageInterface.php +++ b/src/Ratchet/WebSocket/Version/MessageInterface.php @@ -17,6 +17,7 @@ interface MessageInterface { /** * @param FragmentInterface + * @return MessageInterface */ function addFrame(FrameInterface $fragment); diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index af8e961..ec73393 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Message.php b/src/Ratchet/WebSocket/Version/RFC6455/Message.php index 1071115..53bb8dd 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Message.php +++ b/src/Ratchet/WebSocket/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 = ''; diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index b011671..028c151 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -249,9 +249,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($msg, $this->_frame->getPayload()); } - public function testCreate() { + public function testLongCreate() { $len = 65525; - $len = 65575; $pl = $this->generateRandomString($len); $frame = Frame::create($pl, true, Frame::OP_PING); @@ -263,6 +262,14 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($pl, $frame->getPayload()); } + public function testReallyLongCreate() { + $len = 65575; + + $frame = Frame::create($this->generateRandomString($len)); + + $this->assertEquals($len, $frame->getPayloadLength()); + } + public function testExtractOverflow() { $string1 = $this->generateRandomString(); $frame1 = Frame::create($string1); diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php new file mode 100644 index 0000000..5a5d773 --- /dev/null +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php @@ -0,0 +1,70 @@ +message = new Message; + } + + public function testNoFrames() { + $this->assertFalse($this->message->isCoalesced()); + } + + public function testNoFramesOpCode() { + $this->setExpectedException('UnderflowException'); + $this->message->getOpCode(); + } + + public function testFragmentationPayload() { + $a = 'Hello '; + $b = 'World!'; + + $f1 = Frame::create($a, false); + $f2 = Frame::create($b, true, Frame::OP_CONTINUE); + + $this->message->addFrame($f1)->addFrame($f2); + + $this->assertEquals(strlen($a . $b), $this->message->getPayloadLength()); + $this->assertEquals($a . $b, $this->message->getPayload()); + } + + public function testUnbufferedFragment() { + $this->message->addFrame(Frame::create('The quick brow', false)); + + $this->setExpectedException('UnderflowException'); + $this->message->getPayload(); + } + + public function testGetOpCode() { + $this->message + ->addFrame(Frame::create('The quick brow', false, Frame::OP_TEXT)) + ->addFrame(Frame::create('n fox jumps ov', false, Frame::OP_CONTINUE)) + ->addFrame(Frame::create('er the lazy dog', true, Frame::OP_CONTINUE)) + ; + + $this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode()); + } + + public function testGetUnBufferedPayloadLength() { + $this->message + ->addFrame(Frame::create('The quick brow', false, Frame::OP_TEXT)) + ->addFrame(Frame::create('n fox jumps ov', false, Frame::OP_CONTINUE)) + ; + + $this->assertEquals(28, $this->message->getPayloadLength()); + } + + public function testToString() { + $msg = 'Who likes short shorts?'; + $this->message->addFrame(Frame::create($msg)); + + $this->assertEquals($msg, (string)$this->message); + } +} \ No newline at end of file From 9f0e29fe7f7cc76f0b3f2552615e3be7685c8088 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 02:03:16 -0400 Subject: [PATCH 15/28] [WebSocket] Frame masking --- src/Ratchet/WebSocket/MessageParser.php | 3 + .../WebSocket/Version/RFC6455/Frame.php | 132 ++++++++++++++---- .../WebSocket/Version/RFC6455/FrameTest.php | 8 ++ 3 files changed, 117 insertions(+), 26 deletions(-) diff --git a/src/Ratchet/WebSocket/MessageParser.php b/src/Ratchet/WebSocket/MessageParser.php index b28592d..51a6941 100644 --- a/src/Ratchet/WebSocket/MessageParser.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index ec73393..94c463f 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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 diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index 028c151..c1fc716 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -298,6 +298,14 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($string, $frame->getPayload()); } + public function testMasking() { + $msg = 'The quick brown fox jumps over the lazy dog.'; + $frame = Frame::create($msg)->maskPayload(); + + $this->assertTrue($frame->isMasked()); + $this->assertEquals($msg, $frame->getPayload()); + } + protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง From 3a530c8c244718225265ccf7627ef8377d6c5d4b Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 11:55:35 -0400 Subject: [PATCH 16/28] [WebSocket] RFC Masking Full un/masking capabilities on RFC6455 Frames --- .../WebSocket/Version/RFC6455/Frame.php | 54 +++++++++---------- .../WebSocket/Version/RFC6455/FrameTest.php | 8 +++ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 94c463f..d7c25b0 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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()); } diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index c1fc716..538335b 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -306,6 +306,14 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($msg, $frame->getPayload()); } + public function testUnMaskPayload() { + $string = $this->generateRandomString(); + $frame = Frame::create($string)->maskPayload()->unMaskPayload(); + + $this->assertFalse($frame->isMasked()); + $this->assertEquals($string, $frame->getPayload()); + } + protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง From b27c9700f41737cbb202c44596216557fa0809e1 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 12:14:53 -0400 Subject: [PATCH 17/28] [WebSocket] Refactored Frame masking/payload --- .../WebSocket/Version/RFC6455/Frame.php | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index d7c25b0..03601ff 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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 724a15ceb264dada4696c589cb46084ca2664697 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sun, 3 Jun 2012 21:26:20 -0400 Subject: [PATCH 18/28] [Server] Stream over Libevent --- composer.lock | 12 +++++------- src/Ratchet/Server/IoServer.php | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/composer.lock b/composer.lock index d85b62a..d04d99d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ { "package": "evenement/evenement", "version": "dev-master", - "source-reference": "808e3aaea8d4f908e455b0e047cc1acc46b38d44" + "source-reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d" }, { "package": "guzzle/guzzle", @@ -13,36 +13,34 @@ { "package": "react/event-loop", "version": "dev-master", - "source-reference": "cc341b109feae06fa33dff7486aa567e3b9d1406" + "source-reference": "b8b05f583afeafdc9748bf114b367440112bea79" }, { "package": "react/socket", "version": "dev-master", - "source-reference": "6801c6d8653e1999cb34b235cdb4b3a287e4d528" + "source-reference": "3b183d18e11789e0d3b37a88b19ce7170132930a" }, { "package": "symfony/event-dispatcher", "version": "dev-master", - "source-reference": "eb82542e8ec9506096caf7c528564c740a214f56", "alias-pretty-version": "2.1.x-dev", "alias-version": "2.1.9999999.9999999-dev" }, { "package": "symfony/event-dispatcher", "version": "dev-master", - "source-reference": "eb82542e8ec9506096caf7c528564c740a214f56" + "source-reference": "30d3f5da80c2aeab15bcdb5a7d448d15bc294b23" }, { "package": "symfony/http-foundation", "version": "dev-master", - "source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0", "alias-pretty-version": "2.1.x-dev", "alias-version": "2.1.9999999.9999999-dev" }, { "package": "symfony/http-foundation", "version": "dev-master", - "source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0" + "source-reference": "d9ef2afd0218415a8c04ea48a2c83bb5b8f0f51c" } ], "packages-dev": null, diff --git a/src/Ratchet/Server/IoServer.php b/src/Ratchet/Server/IoServer.php index acf545c..cf62520 100644 --- a/src/Ratchet/Server/IoServer.php +++ b/src/Ratchet/Server/IoServer.php @@ -4,6 +4,7 @@ use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; use React\EventLoop\LoopInterface; use React\Socket\ServerInterface; +use React\EventLoop\StreamSelectLoop; use React\EventLoop\Factory as LoopFactory; use React\Socket\Server as Reactor; @@ -48,8 +49,7 @@ class IoServer { } public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') { - $loop = LoopFactory::create(); - + $loop = new StreamSelectLoop; $socket = new Reactor($loop); $socket->listen($port, $address); From 55243550afc6f88555c8b7865dc57d4af95a3a16 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 9 Jun 2012 19:38:44 -0400 Subject: [PATCH 19/28] [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 --- composer.lock | 22 ++-- src/Ratchet/ComponentInterface.php | 1 - src/Ratchet/MessageComponentInterface.php | 10 +- src/Ratchet/MessageInterface.php | 12 ++ src/Ratchet/WebSocket/HandshakeNegotiator.php | 24 ++-- src/Ratchet/WebSocket/MessageParser.php | 47 -------- .../WebSocket/Version/MessageInterface.php | 5 - src/Ratchet/WebSocket/Version/RFC6455.php | 103 +++++++++++++++++- .../WebSocket/Version/RFC6455/Frame.php | 73 ++++++++----- .../WebSocket/Version/VersionInterface.php | 5 +- src/Ratchet/WebSocket/WsConnection.php | 38 ++----- src/Ratchet/WebSocket/WsServer.php | 23 ++-- .../WebSocket/HandshakeNegotiatorTest.php | 14 ++- 13 files changed, 204 insertions(+), 173 deletions(-) create mode 100644 src/Ratchet/MessageInterface.php delete mode 100644 src/Ratchet/WebSocket/MessageParser.php diff --git a/composer.lock b/composer.lock index d85b62a..e4f1796 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ { "package": "evenement/evenement", "version": "dev-master", - "source-reference": "808e3aaea8d4f908e455b0e047cc1acc46b38d44" + "source-reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d" }, { "package": "guzzle/guzzle", @@ -13,36 +13,28 @@ { "package": "react/event-loop", "version": "dev-master", - "source-reference": "cc341b109feae06fa33dff7486aa567e3b9d1406" + "source-reference": "0927a2129394f10cc8534994271c6073ca9e350c" }, { "package": "react/socket", "version": "dev-master", - "source-reference": "6801c6d8653e1999cb34b235cdb4b3a287e4d528" + "source-reference": "b78d96a2cde9a78ab2f923e9aa9a40f778d051df" }, { "package": "symfony/event-dispatcher", "version": "dev-master", - "source-reference": "eb82542e8ec9506096caf7c528564c740a214f56", - "alias-pretty-version": "2.1.x-dev", - "alias-version": "2.1.9999999.9999999-dev" - }, - { - "package": "symfony/event-dispatcher", - "version": "dev-master", - "source-reference": "eb82542e8ec9506096caf7c528564c740a214f56" + "source-reference": "30d3f5da80c2aeab15bcdb5a7d448d15bc294b23" }, { "package": "symfony/http-foundation", "version": "dev-master", - "source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0", - "alias-pretty-version": "2.1.x-dev", - "alias-version": "2.1.9999999.9999999-dev" + "source-reference": "d9ef2afd0218415a8c04ea48a2c83bb5b8f0f51c" }, { "package": "symfony/http-foundation", "version": "dev-master", - "source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0" + "alias-pretty-version": "2.1.x-dev", + "alias-version": "2.1.9999999.9999999-dev" } ], "packages-dev": null, diff --git a/src/Ratchet/ComponentInterface.php b/src/Ratchet/ComponentInterface.php index abebe3f..ab57ed3 100644 --- a/src/Ratchet/ComponentInterface.php +++ b/src/Ratchet/ComponentInterface.php @@ -1,6 +1,5 @@ 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/src/Ratchet/WebSocket/MessageParser.php b/src/Ratchet/WebSocket/MessageParser.php deleted file mode 100644 index 51a6941..0000000 --- a/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/MessageInterface.php b/src/Ratchet/WebSocket/Version/MessageInterface.php index 4c21114..1dc91fc 100644 --- a/src/Ratchet/WebSocket/Version/MessageInterface.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index c01613b..2f06de8 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 03601ff..21ad94d 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/VersionInterface.php b/src/Ratchet/WebSocket/Version/VersionInterface.php index d0839b9..630c62a 100644 --- a/src/Ratchet/WebSocket/Version/VersionInterface.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index d1e0f2b..20b2a6d 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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); } /** diff --git a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php index cfef252..877e235 100644 --- a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php +++ b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php @@ -22,10 +22,10 @@ class HandshakeNegotiatorTest extends \PHPUnit_Framework_TestCase { return array( array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") - , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") - , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") - , array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") ); } @@ -83,6 +83,10 @@ class HandshakeNegotiatorTest extends \PHPUnit_Framework_TestCase { } public function testGetSupportedVersionAfterRemoval() { + $this->parser->enableVersion(new RFC6455); + $this->parser->enableVersion(new HyBi10); + $this->parser->enableVersion(new Hixie76); + $this->parser->disableVersion(0); $values = explode(',', $this->parser->getSupportedVersionString()); @@ -97,7 +101,7 @@ class HandshakeNegotiatorTest extends \PHPUnit_Framework_TestCase { $this->parser->maxSize = 20; - $this->assertNull($this->parser->onData($conn, "GET / HTTP/1.1\r\n")); - $this->assertGreaterThan(400, $this->parser->onData($conn, "Header-Is: Too Big")->getStatusCode()); + $this->assertNull($this->parser->onMessage($conn, "GET / HTTP/1.1\r\n")); + $this->assertGreaterThan(400, $this->parser->onMessage($conn, "Header-Is: Too Big")->getStatusCode()); } } \ No newline at end of file From f9ce641f04023590ecf8091a029adb99aed7eca9 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 12 Jun 2012 20:49:05 -0400 Subject: [PATCH 20/28] CS --- src/Ratchet/WebSocket/HandshakeNegotiator.php | 7 +------ src/Ratchet/WebSocket/Version/RFC6455.php | 2 +- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Ratchet/WebSocket/HandshakeNegotiator.php b/src/Ratchet/WebSocket/HandshakeNegotiator.php index 1b80b13..07eac1e 100644 --- a/src/Ratchet/WebSocket/HandshakeNegotiator.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 2f06de8..f734042 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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 f1612a603fb88b10d6036bd389db0e5e5611c5d2 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 13 Jun 2012 22:46:08 -0400 Subject: [PATCH 21/28] [WebSocket] Separating responsibilities Separated HandshakeNegotiator into HttpRequestParser and VersionManager Moved WsConnection to Version specific Connection --- src/Ratchet/WebSocket/HandshakeNegotiator.php | 130 ------------------ src/Ratchet/WebSocket/HttpRequestParser.php | 57 ++++++++ src/Ratchet/WebSocket/Version/RFC6455.php | 35 +++-- .../WebSocket/Version/RFC6455/Connection.php | 37 +++++ src/Ratchet/WebSocket/VersionManager.php | 77 +++++++++++ src/Ratchet/WebSocket/WsConnection.php | 46 ------- src/Ratchet/WebSocket/WsServer.php | 69 +++++++--- .../WebSocket/HandshakeNegotiatorTest.php | 107 -------------- .../Tests/WebSocket/HtpRequestParserTest.php | 48 +++++++ .../Tests/WebSocket/VersionManagerTest.php | 77 +++++++++++ 10 files changed, 368 insertions(+), 315 deletions(-) delete mode 100644 src/Ratchet/WebSocket/HandshakeNegotiator.php create mode 100644 src/Ratchet/WebSocket/HttpRequestParser.php create mode 100644 src/Ratchet/WebSocket/Version/RFC6455/Connection.php create mode 100644 src/Ratchet/WebSocket/VersionManager.php delete mode 100644 src/Ratchet/WebSocket/WsConnection.php delete mode 100644 tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php create mode 100644 tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php create mode 100644 tests/Ratchet/Tests/WebSocket/VersionManagerTest.php diff --git a/src/Ratchet/WebSocket/HandshakeNegotiator.php b/src/Ratchet/WebSocket/HandshakeNegotiator.php deleted file mode 100644 index 07eac1e..0000000 --- a/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/HttpRequestParser.php b/src/Ratchet/WebSocket/HttpRequestParser.php new file mode 100644 index 0000000..73bbcf7 --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index f734042..29673ea 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Connection.php b/src/Ratchet/WebSocket/Version/RFC6455/Connection.php new file mode 100644 index 0000000..ee746fc --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/VersionManager.php b/src/Ratchet/WebSocket/VersionManager.php new file mode 100644 index 0000000..942fc95 --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsConnection.php b/src/Ratchet/WebSocket/WsConnection.php deleted file mode 100644 index 87ce4e6..0000000 --- a/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 20b2a6d..6ee3e7a 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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); } } diff --git a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php b/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php deleted file mode 100644 index 877e235..0000000 --- a/tests/Ratchet/Tests/WebSocket/HandshakeNegotiatorTest.php +++ /dev/null @@ -1,107 +0,0 @@ -parser = new HandshakeNegotiator(); - } - - public function headersProvider() { - return array( - array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") - , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") - , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") - , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") - , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") - , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") - ); - } - - /** - * @dataProvider headersProvider - */ - public function testIsEom($expected, $message) { - $this->assertEquals($expected, $this->parser->isEom($message)); - } - - public function testFluentInterface() { - $rfc = new RFC6455; - - $this->assertSame($this->parser, $this->parser->disableVersion(13)); - $this->assertSame($this->parser, $this->parser->enableVersion($rfc)); - } - - public function testGetVersion() { - $this->parser->disableVersion(13); - $rfc = new RFC6455; - $this->parser->enableVersion($rfc); - - $req = new EntityEnclosingRequest('get', '/', array( - 'Host' => 'socketo.me' - , 'Sec-WebSocket-Version' => 13 - )); - - $this->assertSame($rfc, $this->parser->getVersion($req)); - } - - public function testGetNopeVersionAndDisable() { - $this->parser->disableVersion(13); - - $req = new EntityEnclosingRequest('get', '/', array( - 'Host' => 'socketo.me' - , 'Sec-WebSocket-Version' => 13 - )); - - $this->assertNull($this->parser->getVersion($req)); - } - - public function testGetSupportedVersionString() { - $v1 = new RFC6455; - $v2 = new HyBi10; - - $parser = new HandshakeNegotiator(); - $parser->enableVersion($v1); - $parser->enableVersion($v2); - - $string = $parser->getSupportedVersionString(); - $values = explode(',', $string); - - $this->assertContains($v1->getVersionNumber(), $values); - $this->assertContains($v2->getVersionNumber(), $values); - } - - public function testGetSupportedVersionAfterRemoval() { - $this->parser->enableVersion(new RFC6455); - $this->parser->enableVersion(new HyBi10); - $this->parser->enableVersion(new Hixie76); - - $this->parser->disableVersion(0); - - $values = explode(',', $this->parser->getSupportedVersionString()); - - $this->assertEquals(2, count($values)); - $this->assertFalse(array_search(0, $values)); - } - - public function testBufferOverflowResponse() { - $conn = new WsConnection(new ConnectionStub); - $this->parser->onOpen($conn); - - $this->parser->maxSize = 20; - - $this->assertNull($this->parser->onMessage($conn, "GET / HTTP/1.1\r\n")); - $this->assertGreaterThan(400, $this->parser->onMessage($conn, "Header-Is: Too Big")->getStatusCode()); - } -} \ No newline at end of file diff --git a/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php b/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php new file mode 100644 index 0000000..91b4a2b --- /dev/null +++ b/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php @@ -0,0 +1,48 @@ +parser = new HttpRequestParser; + } + + public function headersProvider() { + return array( + array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n") + , array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n") + ); + } + + /** + * @dataProvider headersProvider + */ + public function testIsEom($expected, $message) { + $this->assertEquals($expected, $this->parser->isEom($message)); + } + + + public function testBufferOverflowResponse() { + $conn = new ConnectionStub; + + $this->parser->maxSize = 20; + + $this->assertNull($this->parser->onMessage($conn, "GET / HTTP/1.1\r\n")); + + $this->setExpectedException('OverflowException'); + + $this->parser->onMessage($conn, "Header-Is: Too Big"); + + //$this->assertGreaterThan(400, $this->parser->onMessage($conn, "Header-Is: Too Big")->getStatusCode()); + } +} \ No newline at end of file diff --git a/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php b/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php new file mode 100644 index 0000000..85cde70 --- /dev/null +++ b/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php @@ -0,0 +1,77 @@ +vm = new VersionManager; + } + + public function testFluentInterface() { + $rfc = new RFC6455; + + $this->assertSame($this->vm, $this->vm->enableVersion($rfc)); + $this->assertSame($this->vm, $this->vm->disableVersion(13)); + } + + public function testGetVersion() { + $rfc = new RFC6455; + $this->vm->enableVersion($rfc); + + $req = new EntityEnclosingRequest('get', '/', array( + 'Host' => 'socketo.me' + , 'Sec-WebSocket-Version' => 13 + )); + + $this->assertSame($rfc, $this->vm->getVersion($req)); + } + + public function testGetNopeVersionAndDisable() { + $req = new EntityEnclosingRequest('get', '/', array( + 'Host' => 'socketo.me' + , 'Sec-WebSocket-Version' => 13 + )); + + $this->setExpectedException('InvalidArgumentException'); + + $this->vm->getVersion($req); + + //$this->assertFalse($this->vm->getVersion($req)); + } + + public function testGetSupportedVersionString() { + $v1 = new RFC6455; + $v2 = new HyBi10; + + $this->vm->enableVersion($v1); + $this->vm->enableVersion($v2); + + $string = $this->vm->getSupportedVersionString(); + $values = explode(',', $string); + + $this->assertContains($v1->getVersionNumber(), $values); + $this->assertContains($v2->getVersionNumber(), $values); + } + + public function testGetSupportedVersionAfterRemoval() { + $this->vm->enableVersion(new RFC6455); + $this->vm->enableVersion(new HyBi10); + $this->vm->enableVersion(new Hixie76); + + $this->vm->disableVersion(0); + + $values = explode(',', $this->vm->getSupportedVersionString()); + + $this->assertEquals(2, count($values)); + $this->assertFalse(array_search(0, $values)); + } +} \ No newline at end of file From 9d2939e1e83c2c4521a73b7d7168d6ca394506fc Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Wed, 13 Jun 2012 22:51:42 -0400 Subject: [PATCH 22/28] [WebSocket] Cleanup Added a couple coverage unit tests CS --- src/Ratchet/WebSocket/Version/RFC6455/Frame.php | 10 +++++----- src/Ratchet/WebSocket/WsServer.php | 7 ------- .../Tests/WebSocket/HtpRequestParserTest.php | 3 --- .../Tests/WebSocket/VersionManagerTest.php | 16 +++++++++++++++- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 21ad94d..126ad4c 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 6ee3e7a..dc388d1 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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; } diff --git a/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php b/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php index 91b4a2b..b3f1600 100644 --- a/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php +++ b/tests/Ratchet/Tests/WebSocket/HtpRequestParserTest.php @@ -31,7 +31,6 @@ class HttpRequestParserTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($expected, $this->parser->isEom($message)); } - public function testBufferOverflowResponse() { $conn = new ConnectionStub; @@ -42,7 +41,5 @@ class HttpRequestParserTest extends \PHPUnit_Framework_TestCase { $this->setExpectedException('OverflowException'); $this->parser->onMessage($conn, "Header-Is: Too Big"); - - //$this->assertGreaterThan(400, $this->parser->onMessage($conn, "Header-Is: Too Big")->getStatusCode()); } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php b/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php index 85cde70..68fa336 100644 --- a/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php +++ b/tests/Ratchet/Tests/WebSocket/VersionManagerTest.php @@ -44,8 +44,22 @@ class VersionManagerTest extends \PHPUnit_Framework_TestCase { $this->setExpectedException('InvalidArgumentException'); $this->vm->getVersion($req); + } - //$this->assertFalse($this->vm->getVersion($req)); + public function testYesIsVersionEnabled() { + $this->vm->enableVersion(new RFC6455); + + $this->assertTrue($this->vm->isVersionEnabled(new EntityEnclosingRequest('get', '/', array( + 'Host' => 'socketo.me' + , 'Sec-WebSocket-Version' => 13 + )))); + } + + public function testNoIsVersionEnabled() { + $this->assertFalse($this->vm->isVersionEnabled(new EntityEnclosingRequest('get', '/', array( + 'Host' => 'socketo.me' + , 'Sec-WebSocket-Version' => 9000 + )))); } public function testGetSupportedVersionString() { From 87dcd1d5100a35b78b155bef1827f67616fb3f45 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 10:54:26 -0400 Subject: [PATCH 23/28] [WebSocket] Re-scoped variable Protecting Frame::$data to prevent overflow error Correct frame contents is fetched from ::getContents() --- src/Ratchet/WebSocket/Version/RFC6455.php | 2 +- .../WebSocket/Version/RFC6455/Connection.php | 13 ++++--------- src/Ratchet/WebSocket/Version/RFC6455/Frame.php | 10 +++++++++- .../Tests/WebSocket/Version/RFC6455/FrameTest.php | 3 ++- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 29673ea..0d04513 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Connection.php b/src/Ratchet/WebSocket/Version/RFC6455/Connection.php index ee746fc..badd488 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 126ad4c..12fde14 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/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 diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index 538335b..18a6249 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -278,8 +278,9 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $frame2 = Frame::create($string2); $cat = new Frame; - $cat->addBuffer($frame1->data . $frame2->data); + $cat->addBuffer($frame1->getContents() . $frame2->getContents()); + $this->assertEquals($frame1->getContents(), $cat->getContents()); $this->assertEquals($string1, $cat->getPayload()); $uncat = new Frame; From 439ac1234f0c539c310a5f79f65efb66bbcae9d5 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 11:24:18 -0400 Subject: [PATCH 24/28] [WebSocket] Cleanup Removed some obsolete code Handshakes always returns a response --- src/Ratchet/WebSocket/HttpRequestParser.php | 7 ++--- src/Ratchet/WebSocket/Version/RFC6455.php | 15 ++++------ .../WebSocket/Version/RFC6455/Connection.php | 5 ---- src/Ratchet/WebSocket/WsServer.php | 29 ++++++++++++------- .../Tests/WebSocket/Version/RFC6455Test.php | 10 ++++--- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/Ratchet/WebSocket/HttpRequestParser.php b/src/Ratchet/WebSocket/HttpRequestParser.php index 73bbcf7..f8159df 100644 --- a/src/Ratchet/WebSocket/HttpRequestParser.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 0d04513..21841cd 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/RFC6455/Connection.php b/src/Ratchet/WebSocket/Version/RFC6455/Connection.php index badd488..1e1110b 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index dc388d1..4108b76 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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 diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php index 99ba1f6..c9989bf 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php @@ -129,13 +129,15 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase { * @dataProvider headerHandshakeProvider */ public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) { - $request = RequestFactory::getInstance()->fromMessage($header); + $request = RequestFactory::getInstance()->fromMessage($header); + $response = $this->version->handshake($request); + + $this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $response); if ($pass) { - $this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $this->version->handshake($request)); + $this->assertEquals(101, $response->getStatusCode()); } else { - $this->setExpectedException('InvalidArgumentException'); - $this->version->handshake($request); + $this->assertGreaterThanOrEqual(400, $response->getStatusCode()); } } From 49d68ba7df25ecffe653c97a6ed62a283e5e796d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 15:07:52 -0400 Subject: [PATCH 25/28] [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 --- .../WebSocket/Version/DataInterface.php | 28 ++++++++ .../WebSocket/Version/FrameInterface.php | 28 ++------ src/Ratchet/WebSocket/Version/Hixie76.php | 61 ++++++++++++----- .../WebSocket/Version/Hixie76/Connection.php | 16 +++++ .../WebSocket/Version/Hixie76/Frame.php | 8 +++ .../WebSocket/Version/Hixie76/Message.php | 68 ------------------- .../WebSocket/Version/MessageInterface.php | 20 +----- .../WebSocket/Version/RFC6455/Connection.php | 4 +- .../WebSocket/Version/RFC6455/Message.php | 24 +++++-- .../WebSocket/Version/VersionInterface.php | 13 +++- src/Ratchet/WebSocket/WsServer.php | 2 +- .../Tests/WebSocket/Version/HyBi10Test.php | 2 +- .../WebSocket/Version/RFC6455/MessageTest.php | 7 -- .../Tests/WebSocket/Version/RFC6455Test.php | 2 +- 14 files changed, 135 insertions(+), 148 deletions(-) create mode 100644 src/Ratchet/WebSocket/Version/DataInterface.php create mode 100644 src/Ratchet/WebSocket/Version/Hixie76/Connection.php delete mode 100644 src/Ratchet/WebSocket/Version/Hixie76/Message.php diff --git a/src/Ratchet/WebSocket/Version/DataInterface.php b/src/Ratchet/WebSocket/Version/DataInterface.php new file mode 100644 index 0000000..ef44565 --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/Hixie76/Connection.php b/src/Ratchet/WebSocket/Version/Hixie76/Connection.php new file mode 100644 index 0000000..2011ac1 --- /dev/null +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/Hixie76/Frame.php b/src/Ratchet/WebSocket/Version/Hixie76/Frame.php index b9af87d..a172207 100644 --- a/src/Ratchet/WebSocket/Version/Hixie76/Frame.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/Hixie76/Message.php b/src/Ratchet/WebSocket/Version/Hixie76/Message.php deleted file mode 100644 index 818d95d..0000000 --- a/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/MessageInterface.php b/src/Ratchet/WebSocket/Version/MessageInterface.php index 1dc91fc..dc126d6 100644 --- a/src/Ratchet/WebSocket/Version/MessageInterface.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/Version/VersionInterface.php b/src/Ratchet/WebSocket/Version/VersionInterface.php index 630c62a..9a3c29e 100644 --- a/src/Ratchet/WebSocket/Version/VersionInterface.php +++ b/src/Ratchet/WebSocket/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; diff --git a/tests/Ratchet/Tests/WebSocket/Version/HyBi10Test.php b/tests/Ratchet/Tests/WebSocket/Version/HyBi10Test.php index c8ded8b..11ebaa2 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/HyBi10Test.php +++ b/tests/Ratchet/Tests/WebSocket/Version/HyBi10Test.php @@ -57,7 +57,7 @@ class HyBi10Test extends \PHPUnit_Framework_TestCase { public function testUnframeMatchesPreFraming() { $string = 'Hello World!'; - $framed = $this->_version->frame($string); + $framed = $this->_version->newFrame($string)->getContents(); $frame = new Frame; $frame->addBuffer($framed); diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php index 5a5d773..67e11b9 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/MessageTest.php @@ -60,11 +60,4 @@ class MessageTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(28, $this->message->getPayloadLength()); } - - public function testToString() { - $msg = 'Who likes short shorts?'; - $this->message->addFrame(Frame::create($msg)); - - $this->assertEquals($msg, (string)$this->message); - } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php index c9989bf..007eb83 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455Test.php @@ -50,7 +50,7 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase { public function testUnframeMatchesPreFraming() { $string = 'Hello World!'; - $framed = $this->version->frame($string); + $framed = $this->version->newFrame($string)->getContents(); $frame = new Frame; $frame->addBuffer($framed); From 5d1cabc751120f376a7c74cc20a829fb87d58159 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Thu, 14 Jun 2012 16:07:16 -0400 Subject: [PATCH 26/28] [WebSocket] Fixed missing request headers bug Accidentally removed HTTP request headers from connections Added them back --- src/Ratchet/Session/SessionProvider.php | 2 +- src/Ratchet/WebSocket/WsServer.php | 2 ++ tests/Ratchet/Tests/Session/SessionComponentTest.php | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Ratchet/Session/SessionProvider.php b/src/Ratchet/Session/SessionProvider.php index cda8723..0fffd71 100644 --- a/src/Ratchet/Session/SessionProvider.php +++ b/src/Ratchet/Session/SessionProvider.php @@ -71,7 +71,7 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface { * {@inheritdoc} */ function onOpen(ConnectionInterface $conn) { - if (null === ($id = $conn->WebSocket->headers->getCookie(ini_get('session.name')))) { + if (null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) { $saveHandler = $this->_null; $id = ''; } else { diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 90cf305..efdabfc 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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); diff --git a/tests/Ratchet/Tests/Session/SessionComponentTest.php b/tests/Ratchet/Tests/Session/SessionComponentTest.php index 5594c06..7e8382a 100644 --- a/tests/Ratchet/Tests/Session/SessionComponentTest.php +++ b/tests/Ratchet/Tests/Session/SessionComponentTest.php @@ -67,7 +67,7 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase { $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue($sessionId)); $connection->WebSocket = new \StdClass; - $connection->WebSocket->headers = $headers; + $connection->WebSocket->request = $headers; $component->onOpen($connection); @@ -83,7 +83,7 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase { $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue(null)); $conns[$i]->WebSocket = new \StdClass; - $conns[$i]->WebSocket->headers = $headers; + $conns[$i]->WebSocket->request = $headers; } $mock = new MockComponent; From fdff37ba6099a0b85cf1997ff1e9ddeb385b04ad Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 15 Jun 2012 09:56:16 -0400 Subject: [PATCH 27/28] CS --- src/Ratchet/WebSocket/WsServer.php | 28 +++++++++++++-------- src/Ratchet/WebSocket/WsServerInterface.php | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index efdabfc..2e44a8c 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/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/src/Ratchet/WebSocket/WsServerInterface.php b/src/Ratchet/WebSocket/WsServerInterface.php index 8cb378d..91a83cd 100644 --- a/src/Ratchet/WebSocket/WsServerInterface.php +++ b/src/Ratchet/WebSocket/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 From 28a28513a5a2cb268b93c4dae34d762a97f5cf14 Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Fri, 15 Jun 2012 10:06:14 -0400 Subject: [PATCH 28/28] Readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ba5e54b..b72c1e7 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -[![Build Status](https://secure.travis-ci.org/cboden/Ratchet.png?branch=master)](http://travis-ci.org/cboden/Ratchet) - #Ratchet -A PHP 5.3 (PSR-0) library for serving WebSockets and building socket based applications. +[![Build Status](https://secure.travis-ci.org/cboden/Ratchet.png?branch=master)](http://travis-ci.org/cboden/Ratchet) + +A PHP 5.3 library for serving WebSockets and building socket based applications. Build up your application through simple interfaces and re-use your application without changing any of its code just by combining different components. ##WebSocket Compliance