diff --git a/.gitignore b/.gitignore index 06a6b3b..42ab5d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ composer.lock vendor tests/ab/reports +reports diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b5758b7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7 + - hhvm + +matrix: + allow_failures: + - php: hhvm + +before_install: + - export PATH=$HOME/.local/bin:$PATH + - pip install autobahntestsuite --user `whoami` + - pip list autobahntestsuite --user `whoami` + +before_script: + - composer install + - sh tests/ab/run_ab_tests.sh + +script: + - phpunit \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..8f2e7d1 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,27 @@ + + + + + + tests + + test/ab + + + + + + + ./src/ + + + \ No newline at end of file diff --git a/src/Handshake/RequestVerifier.php b/src/Handshake/RequestVerifier.php index 87d069a..aa6a419 100644 --- a/src/Handshake/RequestVerifier.php +++ b/src/Handshake/RequestVerifier.php @@ -92,7 +92,9 @@ class RequestVerifier { * @return bool */ public function verifyConnection(array $connectionHeader) { - return (1 === count($connectionHeader) && 'upgrade' === strtolower(($connectionHeader[0]))); + return count(array_filter($connectionHeader, function ($x) { + return 'upgrade' === strtolower($x); + })) > 0; } /** diff --git a/src/Messaging/Protocol/Message.php b/src/Messaging/Protocol/Message.php index b06e4b4..6f35344 100644 --- a/src/Messaging/Protocol/Message.php +++ b/src/Messaging/Protocol/Message.php @@ -40,6 +40,8 @@ class Message implements \IteratorAggregate, MessageInterface { */ public function addFrame(FrameInterface $fragment) { $this->_frames->push($fragment); + + return $this; } /** diff --git a/tests/AbResultsTest.php b/tests/AbResultsTest.php new file mode 100644 index 0000000..9bc502d --- /dev/null +++ b/tests/AbResultsTest.php @@ -0,0 +1,30 @@ +markTestSkipped('Autobahn TestSuite results not found'); + } + + $resultsJson = file_get_contents($fileName); + $results = json_decode($resultsJson); + $agentName = array_keys(get_object_vars($results))[0]; + + foreach ($results->$agentName as $name => $result) { + if ($result->behavior === "INFORMATIONAL") { + continue; + } + + $this->assertTrue(in_array($result->behavior, ["OK", "NON-STRICT"]), "Autobahn test case " . $name . " in " . $fileName); + } + } + + public function testAutobahnClientResults() { + $this->verifyAutobahnResults(__DIR__ . '/ab/reports/clients/index.json'); + } + + public function testAutobahnServerResults() { + $this->verifyAutobahnResults(__DIR__ . '/ab/reports/servers/index.json'); + } +} diff --git a/tests/ab/AbConnectionContext.php b/tests/ab/AbConnectionContext.php deleted file mode 100644 index 8f106e1..0000000 --- a/tests/ab/AbConnectionContext.php +++ /dev/null @@ -1,67 +0,0 @@ -_conn = $connectionContext; - $this->maskPayload = $maskPayload; - } - - public function setFrame(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame = null) { - $this->_frame = $frame; - } - - public function getFrame() { - return $this->_frame; - } - - public function setMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $message = null) { - $this->_message = $message; - } - - public function getMessage() { - return $this->_message; - } - - public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { - $frame = new \Ratchet\RFC6455\Messaging\Protocol\Frame($msg->getPayload(), true, $msg[0]->getOpcode()); - if ($this->maskPayload) { - $frame->maskPayload(); - } - $this->_conn->write($frame->getContents()); - } - - public function onPing(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) { - $pong = new \Ratchet\RFC6455\Messaging\Protocol\Frame($frame->getPayload(), true, \Ratchet\RFC6455\Messaging\Protocol\Frame::OP_PONG); - if ($this->maskPayload) { - $pong->maskPayload(); - } - $this->_conn->write($pong->getContents()); - } - - public function onPong(\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $msg) { - // TODO: Implement onPong() method. - } - - public function onClose($code = 1000) { - $frame = new \Ratchet\RFC6455\Messaging\Protocol\Frame( - pack('n', $code), - true, - \Ratchet\RFC6455\Messaging\Protocol\Frame::OP_CLOSE - ); - if ($this->maskPayload) { - $frame->maskPayload(); - } - - $this->_conn->end($frame->getContents()); - } -} \ No newline at end of file diff --git a/tests/ab/clientRunner.php b/tests/ab/clientRunner.php index 94109f2..13a99a7 100644 --- a/tests/ab/clientRunner.php +++ b/tests/ab/clientRunner.php @@ -4,34 +4,11 @@ use Ratchet\RFC6455\Messaging\Protocol\Frame; use Ratchet\RFC6455\Messaging\Protocol\Message; require __DIR__ . '/../bootstrap.php'; -require __DIR__ . '/AbConnectionContext.php'; define('AGENT', 'RatchetRFC/0.0.0'); $testServer = "127.0.0.1"; - -class EmConnectionContext extends AbConnectionContext implements \Evenement\EventEmitterInterface, Ratchet\RFC6455\Messaging\Streaming\ContextInterface { - use \Evenement\EventEmitterTrait; - - public function onMessage(\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) { - $this->emit('message', [$msg]); - } - - public function sendMessage(Frame $frame) { - if ($this->maskPayload) { - $frame->maskPayload(); - } - $this->_conn->write($frame->getContents()); - } - - public function close($closeCode = Frame::CLOSE_NORMAL) { - $closeFrame = new Frame(pack('n', $closeCode), true, Frame::OP_CLOSE); - $closeFrame->maskPayload(); - $this->_conn->end($closeFrame->getContents()); - } -} - $loop = React\EventLoop\Factory::create(); $dnsResolverFactory = new React\Dns\Resolver\Factory(); @@ -39,6 +16,32 @@ $dnsResolver = $dnsResolverFactory->createCached('8.8.8.8', $loop); $factory = new \React\SocketClient\Connector($loop, $dnsResolver); +function echoStreamerFactory($conn) +{ + return new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( + new \Ratchet\RFC6455\Encoding\Validator, + new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, + function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($conn) { + /** @var Frame $frame */ + foreach ($msg as $frame) { + $frame->maskPayload(); + } + $conn->write($msg->getContents()); + }, + function (\Ratchet\RFC6455\Messaging\Protocol\FrameInterface $frame) use ($conn) { + switch ($frame->getOpcode()) { + case Frame::OP_PING: + return $conn->write((new Frame($frame->getPayload(), true, Frame::OP_PONG))->maskPayload()->getContents()); + break; + case Frame::OP_CLOSE: + return $conn->end((new Frame($frame->getPayload(), true, Frame::OP_CLOSE))->maskPayload()->getContents()); + break; + } + }, + false + ); +} + function getTestCases() { global $factory; global $testServer; @@ -52,12 +55,10 @@ function getTestCases() { $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); + /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ + $ms = null; - /** @var EmConnectionContext $context */ - $context = null; - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -70,19 +71,23 @@ function getTestCases() { $stream->end(); $deferred->reject(); } else { - $context = new EmConnectionContext($stream, true); - - $context->on('message', function (Message $msg) use ($stream, $deferred, $context) { - $deferred->resolve($msg->getPayload()); - $context->close(); - }); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( + new \Ratchet\RFC6455\Encoding\Validator, + new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, + function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { + $deferred->resolve($msg->getPayload()); + $stream->close(); + }, + null, + false + ); } } } // feed the message streamer - if ($response && $context) { - $ms->onData($data, $context); + if ($ms) { + $ms->onData($data); } }); @@ -108,12 +113,9 @@ function runTest($case) $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); + $ms = null; - /** @var AbConnectionContext $context */ - $context = null; - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -126,14 +128,14 @@ function runTest($case) $stream->end(); $deferred->reject(); } else { - $context = new AbConnectionContext($stream, true); + $ms = echoStreamerFactory($stream); } } } // feed the message streamer - if ($response && $context) { - $ms->onData($data, $context); + if ($ms) { + $ms->onData($data); } }); @@ -154,18 +156,17 @@ function createReport() { $deferred = new Deferred(); $factory->create($testServer, 9001)->then(function (\React\Stream\Stream $stream) use ($deferred) { - $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator('/updateReports?agent=' . AGENT); + $reportPath = "/updateReports?agent=" . AGENT . "&shutdownOnComplete=true"; + $cn = new \Ratchet\RFC6455\Handshake\ClientNegotiator($reportPath); $cnRequest = $cn->getRequest(); $rawResponse = ""; $response = null; - $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer(new \Ratchet\RFC6455\Encoding\Validator(), true); + /** @var \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer $ms */ + $ms = null; - /** @var EmConnectionContext $context */ - $context = null; - - $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, $ms, $cn, $deferred, &$context) { + $stream->on('data', function ($data) use ($stream, &$rawResponse, &$response, &$ms, $cn, $deferred, &$context) { if ($response === null) { $rawResponse .= $data; $pos = strpos($rawResponse, "\r\n\r\n"); @@ -178,19 +179,23 @@ function createReport() { $stream->end(); $deferred->reject(); } else { - $context = new EmConnectionContext($stream, true); - - $context->on('message', function (Message $msg) use ($stream, $deferred, $context) { - $deferred->resolve($msg->getPayload()); - $context->close(); - }); + $ms = new \Ratchet\RFC6455\Messaging\Streaming\MessageStreamer( + new \Ratchet\RFC6455\Encoding\Validator, + new \Ratchet\RFC6455\Messaging\Protocol\CloseFrameChecker, + function (\Ratchet\RFC6455\Messaging\Protocol\MessageInterface $msg) use ($deferred, $stream) { + $deferred->resolve($msg->getPayload()); + $stream->close(); + }, + null, + false + ); } } } // feed the message streamer - if ($response && $context) { - $ms->onData($data, $context); + if ($ms) { + $ms->onData($data); } }); @@ -204,8 +209,6 @@ function createReport() { $testPromises = []; getTestCases()->then(function ($count) use ($loop) { - echo "Running " . $count . " test cases.\n"; - $allDeferred = new Deferred(); $runNextCase = function () use (&$i, &$runNextCase, $count, $allDeferred) { @@ -214,7 +217,6 @@ getTestCases()->then(function ($count) use ($loop) { $allDeferred->resolve(); return; } - echo "Running " . $i . "\n"; runTest($i)->then($runNextCase); }; @@ -222,7 +224,6 @@ getTestCases()->then(function ($count) use ($loop) { $runNextCase(); $allDeferred->promise()->then(function () { - echo "Generating report...\n"; createReport(); }); }); diff --git a/tests/ab/fuzzingclient.json b/tests/ab/fuzzingclient.json index 28cdd4a..75b1cc9 100644 --- a/tests/ab/fuzzingclient.json +++ b/tests/ab/fuzzingclient.json @@ -2,11 +2,11 @@ "options": {"failByDrop": false}, "outdir": "./reports/servers", - "servers": [ - {"agent": "AutobahnServer", + "servers": [{ + "agent": "RatchetRFC/0.1.0", "url": "ws://localhost:9001", - "options": {"version": 18}} - ], + "options": {"version": 18} + }], "cases": ["*"], "exclude-cases": ["12.*","13.*"], "exclude-agent-cases": {} diff --git a/tests/ab/run_ab_tests.sh b/tests/ab/run_ab_tests.sh new file mode 100644 index 0000000..aeb62d9 --- /dev/null +++ b/tests/ab/run_ab_tests.sh @@ -0,0 +1,14 @@ +cd tests/ab + +wstest -m fuzzingserver -s fuzzingserver.json & +sleep 5 +php clientRunner.php + +sleep 2 + +php startServer.php & +sleep 3 +wstest -m fuzzingclient -s fuzzingclient.json +sleep 2 + +killall php wstest diff --git a/tests/ab/startServer.php b/tests/ab/startServer.php index 47a5316..58b7814 100644 --- a/tests/ab/startServer.php +++ b/tests/ab/startServer.php @@ -6,6 +6,7 @@ use Ratchet\RFC6455\Messaging\Protocol\Frame; require_once __DIR__ . "/../bootstrap.php"; $loop = \React\EventLoop\Factory::create(); + $socket = new \React\Socket\Server($loop); $server = new \React\Http\Server($socket); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6fa5dc9..511b041 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -4,15 +4,16 @@ * Find the auto loader file */ $files = [ - __DIR__ . '/../../../../vendor/autoload.php', - __DIR__ . '/../../../vendor/autoload.php', - __DIR__ . '/../../vendor/autoload.php', __DIR__ . '/../vendor/autoload.php', + __DIR__ . '/../../vendor/autoload.php', + __DIR__ . '/../../../vendor/autoload.php', + __DIR__ . '/../../../../vendor/autoload.php', ]; foreach ($files as $file) { if (file_exists($file)) { - require $file; + $loader = require $file; + $loader->addPsr4('Ratchet\\RFC6455\\Test\\', __DIR__); break; } } diff --git a/tests/unit/Handshake/RequestVerifierTest.php b/tests/unit/Handshake/RequestVerifierTest.php new file mode 100644 index 0000000..a7277ff --- /dev/null +++ b/tests/unit/Handshake/RequestVerifierTest.php @@ -0,0 +1,174 @@ +_v = new RequestVerifier(); + } + + public static function methodProvider() { + return array( + array(true, 'GET'), + array(true, 'get'), + array(true, 'Get'), + array(false, 'POST'), + array(false, 'DELETE'), + array(false, 'PUT'), + array(false, 'PATCH') + ); + } + /** + * @dataProvider methodProvider + */ + public function testMethodMustBeGet($result, $in) { + $this->assertEquals($result, $this->_v->verifyMethod($in)); + } + + public static function httpVersionProvider() { + return array( + array(true, 1.1), + array(true, '1.1'), + array(true, 1.2), + array(true, '1.2'), + array(true, 2), + array(true, '2'), + array(true, '2.0'), + array(false, '1.0'), + array(false, 1), + array(false, '0.9'), + array(false, ''), + array(false, 'hello') + ); + } + + /** + * @dataProvider httpVersionProvider + */ + public function testHttpVersionIsAtLeast1Point1($expected, $in) { + $this->assertEquals($expected, $this->_v->verifyHTTPVersion($in)); + } + + public static function uRIProvider() { + return array( + array(true, '/chat'), + array(true, '/hello/world?key=val'), + array(false, '/chat#bad'), + array(false, 'nope'), + array(false, '/ ಠ_ಠ '), + array(false, '/✖') + ); + } + + /** + * @dataProvider URIProvider + */ + public function testRequestUri($expected, $in) { + $this->assertEquals($expected, $this->_v->verifyRequestURI($in)); + } + + public static function hostProvider() { + return array( + array(true, ['server.example.com']), + array(false, []) + ); + } + + /** + * @dataProvider HostProvider + */ + public function testVerifyHostIsSet($expected, $in) { + $this->assertEquals($expected, $this->_v->verifyHost($in)); + } + + public static function upgradeProvider() { + return array( + array(true, ['websocket']), + array(true, ['Websocket']), + array(true, ['webSocket']), + array(false, []), + array(false, ['']) + ); + } + + /** + * @dataProvider upgradeProvider + */ + public function testVerifyUpgradeIsWebSocket($expected, $val) { + $this->assertEquals($expected, $this->_v->verifyUpgradeRequest($val)); + } + + public static function connectionProvider() { + return array( + array(true, ['Upgrade']), + array(true, ['upgrade']), + array(true, ['keep-alive', 'Upgrade']), + array(true, ['Upgrade', 'keep-alive']), + array(true, ['keep-alive', 'Upgrade', 'something']), + array(false, ['']), + array(false, []) + ); + } + + /** + * @dataProvider connectionProvider + */ + public function testConnectionHeaderVerification($expected, $val) { + $this->assertEquals($expected, $this->_v->verifyConnection($val)); + } + + public static function keyProvider() { + return array( + array(true, ['hkfa1L7uwN6DCo4IS3iWAw==']), + array(true, ['765vVoQpKSGJwPzJIMM2GA==']), + array(true, ['AQIDBAUGBwgJCgsMDQ4PEC==']), + array(true, ['axa2B/Yz2CdpfQAY2Q5P7w==']), + array(false, [0]), + array(false, ['Hello World']), + array(false, ['1234567890123456']), + array(false, ['123456789012345678901234']), + array(true, [base64_encode('UTF8allthngs+✓')]), + array(true, ['dGhlIHNhbXBsZSBub25jZQ==']), + array(false, []), + array(false, ['dGhlIHNhbXBsZSBub25jZQ==', 'Some other value']), + array(false, ['Some other value', 'dGhlIHNhbXBsZSBub25jZQ==']) + ); + } + + /** + * @dataProvider keyProvider + */ + public function testKeyIsBase64Encoded16BitNonce($expected, $val) { + $this->assertEquals($expected, $this->_v->verifyKey($val)); + } + + public static function versionProvider() { + return array( + array(true, [13]), + array(true, ['13']), + array(false, [12]), + array(false, [14]), + array(false, ['14']), + array(false, ['hi']), + array(false, ['']), + array(false, []) + ); + } + + /** + * @dataProvider versionProvider + */ + public function testVersionEquals13($expected, $in) { + $this->assertEquals($expected, $this->_v->verifyVersion($in)); + } +} \ No newline at end of file diff --git a/tests/unit/Messaging/Protocol/FrameTest.php b/tests/unit/Messaging/Protocol/FrameTest.php new file mode 100644 index 0000000..7622599 --- /dev/null +++ b/tests/unit/Messaging/Protocol/FrameTest.php @@ -0,0 +1,502 @@ +_frame = new Frame; + } + + /** + * Encode the fake binary string to send over the wire + * @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)); + } + + /** + * This is a data provider + * param string The UTF8 message + * param string The WebSocket framed message, then base64_encoded + */ + public static function UnframeMessageProvider() { + return array( + array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7'), + array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg'), + array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow=='), + array( + "The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...", + 'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY=' + ) + ); + } + + public static function underflowProvider() { + return array( + array('isFinal', ''), + array('getRsv1', ''), + array('getRsv2', ''), + array('getRsv3', ''), + array('getOpcode', ''), + array('isMasked', '10000001'), + array('getPayloadLength', '10000001'), + array('getPayloadLength', '1000000111111110'), + array('getMaskingKey', '1000000110000111'), + array('getPayload', '100000011000000100011100101010101001100111110100') + ); + } + + /** + * @dataProvider underflowProvider + * + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + */ + public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) { + $this->setExpectedException('\UnderflowException'); + if (!empty($bin)) { + $this->_frame->addBuffer(static::encode($bin)); + } + call_user_func(array($this->_frame, $method)); + } + + /** + * A data provider for testing the first byte of a WebSocket frame + * param bool Given, is the byte indicate this is the final frame + * param int Given, what is the expected opcode + * param string of 0|1 Each character represents a bit in the byte + */ + public static function firstByteProvider() { + return array( + array(false, false, false, true, 8, '00011000'), + array(true, false, true, false, 10, '10101010'), + array(false, false, false, false, 15, '00001111'), + array(true, false, false, false, 1, '10000001'), + array(true, true, true, true, 15, '11111111'), + array(true, true, false, false, 7, '11000111') + ); + } + + /** + * @dataProvider firstByteProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal + */ + public function testFinCodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { + $this->_frame->addBuffer(static::encode($bin)); + $this->assertEquals($fin, $this->_frame->isFinal()); + } + + /** + * @dataProvider firstByteProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2 + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3 + */ + public function testGetRsvFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { + $this->_frame->addBuffer(static::encode($bin)); + $this->assertEquals($rsv1, $this->_frame->getRsv1()); + $this->assertEquals($rsv2, $this->_frame->getRsv2()); + $this->assertEquals($rsv3, $this->_frame->getRsv3()); + } + + /** + * @dataProvider firstByteProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode + */ + public function testOpcodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) { + $this->_frame->addBuffer(static::encode($bin)); + $this->assertEquals($opcode, $this->_frame->getOpcode()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal + */ + public function testFinCodeFromFullMessage($msg, $encoded) { + $this->_frame->addBuffer(base64_decode($encoded)); + $this->assertTrue($this->_frame->isFinal()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode + */ + public function testOpcodeFromFullMessage($msg, $encoded) { + $this->_frame->addBuffer(base64_decode($encoded)); + $this->assertEquals(1, $this->_frame->getOpcode()); + } + + public static function payloadLengthDescriptionProvider() { + return array( + array(7, '01110101'), + array(7, '01111101'), + array(23, '01111110'), + array(71, '01111111'), + array(7, '00000000'), // Should this throw an exception? Can a payload be empty? + array(7, '00000001') + ); + } + + /** + * @dataProvider payloadLengthDescriptionProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::addBuffer + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getFirstPayloadVal + */ + public function testFirstPayloadDesignationValue($bits, $bin) { + $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(static::encode($bin)); + $ref = new \ReflectionClass($this->_frame); + $cb = $ref->getMethod('getFirstPayloadVal'); + $cb->setAccessible(true); + $this->assertEquals(bindec($bin), $cb->invoke($this->_frame)); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getFirstPayloadVal + */ + public function testFirstPayloadValUnderflow() { + $ref = new \ReflectionClass($this->_frame); + $cb = $ref->getMethod('getFirstPayloadVal'); + $cb->setAccessible(true); + $this->setExpectedException('UnderflowException'); + $cb->invoke($this->_frame); + } + + /** + * @dataProvider payloadLengthDescriptionProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getNumPayloadBits + */ + public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { + $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(static::encode($bin)); + $ref = new \ReflectionClass($this->_frame); + $cb = $ref->getMethod('getNumPayloadBits'); + $cb->setAccessible(true); + $this->assertEquals($expected_bits, $cb->invoke($this->_frame)); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getNumPayloadBits + */ + public function testgetNumPayloadBitsUnderflow() { + $ref = new \ReflectionClass($this->_frame); + $cb = $ref->getMethod('getNumPayloadBits'); + $cb->setAccessible(true); + $this->setExpectedException('UnderflowException'); + $cb->invoke($this->_frame); + } + + public function secondByteProvider() { + return array( + array(true, 1, '10000001'), + array(false, 1, '00000001'), + array(true, 125, $this->_secondByteMaskedSPL) + ); + } + /** + * @dataProvider secondByteProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked + */ + public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { + $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(static::encode($bin)); + $this->assertEquals($masked, $this->_frame->isMasked()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked + */ + public function testIsMaskedFromFullMessage($msg, $encoded) { + $this->_frame->addBuffer(base64_decode($encoded)); + $this->assertTrue($this->_frame->isMasked()); + } + + /** + * @dataProvider secondByteProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + */ + public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { + $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(static::encode($bin)); + $this->assertEquals($payload_length, $this->_frame->getPayloadLength()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * @todo Not yet testing when second additional payload length descriptor + */ + public function testGetPayloadLengthFromFullMessage($msg, $encoded) { + $this->_frame->addBuffer(base64_decode($encoded)); + $this->assertEquals(strlen($msg), $this->_frame->getPayloadLength()); + } + + public function maskingKeyProvider() { + $frame = new Frame; + return array( + array($frame->generateMaskingKey()), + array($frame->generateMaskingKey()), + array($frame->generateMaskingKey()) + ); + } + + /** + * @dataProvider maskingKeyProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey + * @todo I I wrote the dataProvider incorrectly, skipping for now + */ + public function testGetMaskingKey($mask) { + $this->_frame->addBuffer(static::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(static::encode($this->_secondByteMaskedSPL)); + $this->_frame->addBuffer($mask); + $this->assertEquals($mask, $this->_frame->getMaskingKey()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey + */ + public function testGetMaskingKeyOnUnmaskedPayload() { + $frame = new Frame('Hello World!'); + $this->assertEquals('', $frame->getMaskingKey()); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + * @todo Move this test to bottom as it requires all methods of the class + */ + public function testUnframeFullMessage($unframed, $base_framed) { + $this->_frame->addBuffer(base64_decode($base_framed)); + $this->assertEquals($unframed, $this->_frame->getPayload()); + } + + public static function messageFragmentProvider() { + return array( + array(false, '', '', '', '', '') + ); + } + + /** + * @dataProvider UnframeMessageProvider + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + */ + public function testCheckPiecingTogetherMessage($msg, $encoded) { + $framed = base64_decode($encoded); + for ($i = 0, $len = strlen($framed);$i < $len; $i++) { + $this->_frame->addBuffer(substr($framed, $i, 1)); + } + $this->assertEquals($msg, $this->_frame->getPayload()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload + */ + public function testLongCreate() { + $len = 65525; + $pl = $this->generateRandomString($len); + $frame = new Frame($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()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + */ + public function testReallyLongCreate() { + $len = 65575; + $frame = new Frame($this->generateRandomString($len)); + $this->assertEquals($len, $frame->getPayloadLength()); + } + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct + * covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow + */ + public function testExtractOverflow() { + $string1 = $this->generateRandomString(); + $frame1 = new Frame($string1); + $string2 = $this->generateRandomString(); + $frame2 = new Frame($string2); + $cat = new Frame; + $cat->addBuffer($frame1->getContents() . $frame2->getContents()); + $this->assertEquals($frame1->getContents(), $cat->getContents()); + $this->assertEquals($string1, $cat->getPayload()); + $uncat = new Frame; + $uncat->addBuffer($cat->extractOverflow()); + $this->assertEquals($string1, $cat->getPayload()); + $this->assertEquals($string2, $uncat->getPayload()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow + */ + public function testEmptyExtractOverflow() { + $string = $this->generateRandomString(); + $frame = new Frame($string); + $this->assertEquals($string, $frame->getPayload()); + $this->assertEquals('', $frame->extractOverflow()); + $this->assertEquals($string, $frame->getPayload()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getContents + */ + public function testGetContents() { + $msg = 'The quick brown fox jumps over the lazy dog.'; + $frame1 = new Frame($msg); + $frame2 = new Frame($msg); + $frame2->maskPayload(); + $this->assertNotEquals($frame1->getContents(), $frame2->getContents()); + $this->assertEquals(strlen($frame1->getContents()) + 4, strlen($frame2->getContents())); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload + */ + public function testMasking() { + $msg = 'The quick brown fox jumps over the lazy dog.'; + $frame = new Frame($msg); + $frame->maskPayload(); + $this->assertTrue($frame->isMasked()); + $this->assertEquals($msg, $frame->getPayload()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::unMaskPayload + */ + public function testUnMaskPayload() { + $string = $this->generateRandomString(); + $frame = new Frame($string); + $frame->maskPayload()->unMaskPayload(); + $this->assertFalse($frame->isMasked()); + $this->assertEquals($string, $frame->getPayload()); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::generateMaskingKey + */ + public function testGenerateMaskingKey() { + $dupe = false; + $done = array(); + for ($i = 0; $i < 10; $i++) { + $new = $this->_frame->generateMaskingKey(); + if (in_array($new, $done)) { + $dupe = true; + } + $done[] = $new; + } + $this->assertEquals(4, strlen($new)); + $this->assertFalse($dupe); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload + */ + public function testGivenMaskIsValid() { + $this->setExpectedException('InvalidArgumentException'); + $this->_frame->maskPayload('hello world'); + } + + /** + * covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload + */ + public function testGivenMaskIsValidAscii() { + if (!extension_loaded('mbstring')) { + $this->markTestSkipped("mbstring required for this test"); + return; + } + $this->setExpectedException('OutOfBoundsException'); + $this->_frame->maskPayload('x✖'); + } + + 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; + } + + /** + * There was a frame boundary issue when the first 3 bytes of a frame with a payload greater than + * 126 was added to the frame buffer and then Frame::getPayloadLength was called. It would cause the frame + * to set the payload length to 126 and then not recalculate it once the full length information was available. + * + * This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown. + * + * covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength + * covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow + */ + public function testFrameDeliveredOneByteAtATime() { + $startHeader = "\x01\x7e\x01\x00"; // header for a text frame of 256 - non-final + $framePayload = str_repeat("*", 256); + $rawOverflow = "xyz"; + $rawFrame = $startHeader . $framePayload . $rawOverflow; + $frame = new Frame(); + $payloadLen = 256; + for ($i = 0; $i < strlen($rawFrame); $i++) { + $frame->addBuffer($rawFrame[$i]); + try { + // payloadLen will + $payloadLen = $frame->getPayloadLength(); + } catch (\UnderflowException $e) { + if ($i > 2) { // we should get an underflow on 0,1,2 + $this->fail("Underflow exception when the frame length should be available"); + } + } + if ($payloadLen !== 256) { + $this->fail("Payload length of " . $payloadLen . " should have been 256."); + } + } + // make sure the overflow is good + $this->assertEquals($rawOverflow, $frame->extractOverflow()); + } +} \ No newline at end of file diff --git a/tests/unit/Messaging/Protocol/MessageTest.php b/tests/unit/Messaging/Protocol/MessageTest.php new file mode 100644 index 0000000..f0e8711 --- /dev/null +++ b/tests/unit/Messaging/Protocol/MessageTest.php @@ -0,0 +1,60 @@ +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 = new Frame($a, false); + $f2 = new Frame($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(new Frame('The quick brow', false)); + $this->setExpectedException('UnderflowException'); + $this->message->getPayload(); + } + + public function testGetOpCode() { + $this->message + ->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT)) + ->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE)) + ->addFrame(new Frame('er the lazy dog', true, Frame::OP_CONTINUE)) + ; + $this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode()); + } + + public function testGetUnBufferedPayloadLength() { + $this->message + ->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT)) + ->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE)) + ; + $this->assertEquals(28, $this->message->getPayloadLength()); + } +} \ No newline at end of file