From 8dc874d7ae8a8a7f97a80beb160193b5add48540 Mon Sep 17 00:00:00 2001 From: "Nek (Maxime Veber)" Date: Fri, 4 Nov 2016 00:19:50 +0100 Subject: [PATCH] Add supports of many websockets frames in the same binary frame This commit modify the way of processing binary entry frames. Old way: taking binary frame as websocket frame (and bufferize if it was not complete). New way: it take a binary frame as one or many frames composing one or many messages and bufferize if frames are not complete. This new behavior makes autobahn execute new tests that was crashing woketo. The fix is taking 4 bytes for the mask (sometimes it was less because of a wrong usage of the BitManipulation class). Switch to usage of generators --- composer.json | 3 +- src/Exception/Frame/TooBigFrameException.php | 24 +++- src/Rfc6455/Frame.php | 123 +++++++++++------- src/Rfc6455/Message.php | 22 ++-- src/Rfc6455/MessageProcessor.php | 50 ++++--- src/Server/Connection.php | 38 +++--- src/Utils/BitManipulation.php | 25 +++- tests/Woketo/Core/ConnectionTest.php | 4 +- tests/Woketo/Rfc6455/FrameTest.php | 28 +++- tests/Woketo/Rfc6455/MessageProcessorTest.php | 62 +++++++-- tests/Woketo/Rfc6455/MessageTest.php | 34 +++++ tests/Woketo/Utils/BitManipulationTest.php | 6 + 12 files changed, 301 insertions(+), 118 deletions(-) diff --git a/composer.json b/composer.json index aeafe80..dab413c 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "require": { "react/event-loop": "~0.4.0", - "react/socket": "~0.4.0" + "react/socket": "~0.4.0", + "nekland/tools": "^1.0" }, "require-dev": { "phpunit/phpunit": "^5.3" diff --git a/src/Exception/Frame/TooBigFrameException.php b/src/Exception/Frame/TooBigFrameException.php index 7477cab..b6cbe4a 100644 --- a/src/Exception/Frame/TooBigFrameException.php +++ b/src/Exception/Frame/TooBigFrameException.php @@ -2,7 +2,7 @@ /** * This file is a part of Woketo package. * - * (c) Ci-tron + * (c) Nekland * * For the full license, take a look to the LICENSE file * on the root directory of this project @@ -10,9 +10,31 @@ namespace Nekland\Woketo\Exception\Frame; +use Exception; use Nekland\Woketo\Exception\LimitationException; class TooBigFrameException extends LimitationException { + /** + * @var int + */ + private $maxLength; + /** + * @param int $maxLength + * @param string $message + */ + public function __construct(int $maxLength, string $message = 'The frame is too big to be processed.') + { + parent::__construct($message, null, null); + $this->maxLength = $maxLength; + } + + /** + * @return int + */ + public function getMaxLength() + { + return $this->maxLength; + } } diff --git a/src/Rfc6455/Frame.php b/src/Rfc6455/Frame.php index 2f89ffb..f2766d8 100644 --- a/src/Rfc6455/Frame.php +++ b/src/Rfc6455/Frame.php @@ -8,6 +8,7 @@ * on the root directory of this project */ +declare(strict_types=1); namespace Nekland\Woketo\Rfc6455; use Nekland\Woketo\Exception\Frame\ControlFrameException; @@ -89,7 +90,7 @@ class Frame * @var int */ private $secondByte; - + /** * @var bool */ @@ -133,11 +134,12 @@ public function __construct($data=null) { if (null !== $data) { $this->setRawData($data); - $this->checkFrameSize(); } } /** + * It also run checks on data. + * * @param string|int $rawData Probably more likely a string than an int, but well... why not. * @return self * @throws InvalidFrameException @@ -145,13 +147,22 @@ public function __construct($data=null) public function setRawData($rawData) { $this->rawData = $rawData; - $this->frameSize = strlen($rawData); + $this->frameSize = BitManipulation::frameSize($rawData); if ($this->frameSize < 2) { throw new InvalidFrameException('Not enough data to be a frame.'); } $this->getInformationFromRawData(); + try { + $this->checkFrameSize(); + } catch (TooBigFrameException $e) { + $this->frameSize = $e->getMaxLength(); + $this->rawData = BitManipulation::bytesFromToString($this->rawData, 0, $this->frameSize, BitManipulation::MODE_PHP); + } + + Frame::checkFrame($this); + return $this; } @@ -188,12 +199,12 @@ public function getRawData() : string } if ($this->isMasked()) { $data .= $this->getMaskingKey(); - $data .= $this->applyMask($this->getPayload()); - - return $data; + $data .= $this->applyMask(); + + return $this->rawData = $data; } - - return $data . $this->getPayload(); + + return $this->rawData = $data . $this->getPayload(); } /** @@ -221,7 +232,7 @@ public function setFinal(bool $final) : Frame */ public function getRsv1() : bool { - return BitManipulation::nthBit($this->firstByte, 2); + return (bool) BitManipulation::nthBit($this->firstByte, 2); } /** @@ -229,7 +240,7 @@ public function getRsv1() : bool */ public function getRsv2() : bool { - return BitManipulation::nthBit($this->firstByte, 3); + return (bool) BitManipulation::nthBit($this->firstByte, 3); } /** @@ -237,7 +248,7 @@ public function getRsv2() : bool */ public function getRsv3() : bool { - return BitManipulation::nthBit($this->firstByte, 4); + return (bool) BitManipulation::nthBit($this->firstByte, 4); } /** @@ -247,15 +258,15 @@ public function getOpcode() : int { return BitManipulation::partOfByte($this->firstByte, 2); } - + public function setOpcode(int $opcode) : Frame { if (!in_array($opcode, [Frame::OP_TEXT, Frame::OP_BINARY, Frame::OP_CLOSE, Frame::OP_CONTINUE, Frame::OP_PING, Frame::OP_PONG])) { throw new \InvalidArgumentException('Wrong opcode !'); } - + $this->opcode = $opcode; - + return $this; } @@ -287,7 +298,7 @@ public function getMaskingKey() : string $value = BitManipulation::bytesFromTo($this->rawData, $start, $start + 3); - return $this->mask = BitManipulation::intToString($value); + return $this->mask = BitManipulation::intToString($value, 4); } public function getPayload() @@ -296,13 +307,13 @@ public function getPayload() return $this->payload; } - $this->checkFrameSize(); - $infoBytesLen = $this->getInfoBytesLen(); - $payload = (string) substr($this->rawData, $infoBytesLen, $this->payloadLen); + $payload = (string) BitManipulation::bytesFromToString($this->rawData, $infoBytesLen, $this->payloadLen, BitManipulation::MODE_PHP); if ($this->isMasked()) { - return $this->payload = $this->applyMask($payload); + $this->payload = $payload; + + return $this->payload = $this->applyMask(); } return $this->payload = $payload; @@ -319,37 +330,18 @@ public function getInfoBytesLen() return $this->infoBytesLen = (9 + $this->payloadLenSize) / 8 + ($this->isMasked() ? 4 : 0); } - public function checkFrameSize() - { - $infoBytesLen = $this->getInfoBytesLen(); - $realDataLength = BitManipulation::frameSize($this->rawData); - $theoricDataLength = $infoBytesLen + $this->payloadLen; - - if ($realDataLength < $theoricDataLength) { - throw new IncompleteFrameException( - sprintf('Impossible to retrieve %s bytes of payload when the full frame is %s bytes long.', $theoricDataLength, $realDataLength) - ); - } - - if ($realDataLength > $theoricDataLength) { - throw new TooBigFrameException(); - } - } - - - public function setPayload(string $payload) : Frame { $this->payload = $payload; $this->payloadLen = BitManipulation::frameSize($this->payload); $this->payloadLenSize = 7; - + if ($this->payloadLen > 126 && $this->payloadLen < 65536) { $this->payloadLenSize += 16; } else if ($this->payloadLen > 126) { $this->payloadLenSize += 64; } - + return $this; } @@ -382,7 +374,7 @@ public function getPayloadLength() : int // Check < 0 because 64th bit is the negative one in PHP. if ($payloadLen < 0 || $payloadLen > Frame::$maxPayloadSize) { - throw new TooBigFrameException; + throw new TooBigFrameException(Frame::$maxPayloadSize); } return $this->payloadLen = $payloadLen; @@ -393,22 +385,26 @@ public function isMasked() : bool if ($this->mask !== null) { return true; } - + if ($this->rawData !== null) { return (bool) BitManipulation::nthBit($this->secondByte, 1); } - + return false; } - public function applyMask(string $payload) : string + /** + * This method works for mask and unmask (it's the same operation) + * + * @return string + */ + public function applyMask() : string { $res = ''; $mask = $this->getMaskingKey(); - for ($i = 0; $i < $this->payloadLen; $i++) { - $payloadByte = $payload[$i]; + $payloadByte = $this->payload[$i]; $res .= $payloadByte ^ $mask[$i % 4]; } @@ -422,10 +418,39 @@ private function getInformationFromRawData() $this->final = (bool) BitManipulation::nthBit($this->firstByte, 1); $this->payloadLen = $this->getPayloadLength(); + } - Frame::checkFrame($this); + /** + * Check if the frame have the good size based on payload size. + * + * @throws IncompleteFrameException + * @throws TooBigFrameException + */ + public function checkFrameSize() + { + $infoBytesLen = $this->getInfoBytesLen(); + $this->frameSize = BitManipulation::frameSize($this->rawData); + $theoricDataLength = $infoBytesLen + $this->payloadLen; + + if ($this->frameSize < $theoricDataLength) { + throw new IncompleteFrameException( + sprintf('Impossible to retrieve %s bytes of payload when the full frame is %s bytes long.', $theoricDataLength, $this->frameSize) + ); + } + + if ($this->frameSize > $theoricDataLength) { + throw new TooBigFrameException($theoricDataLength); + } } + /** + * Validate a frame with RFC criteria + * + * @param Frame $frame + * @throws ControlFrameException + * @throws InvalidFrameException + * @throws TooBigControlFrameException + */ public static function checkFrame(Frame $frame) { if ($frame->getOpcode() === Frame::OP_TEXT && !mb_check_encoding($frame->getPayload())) { @@ -438,14 +463,14 @@ public static function checkFrame(Frame $frame) } if ($frame->getPayloadLength() > Frame::MAX_CONTROL_FRAME_SIZE) { - throw new TooBigControlFrameException('A control frame cannot be larger than 125 bytes.'); + throw new TooBigControlFrameException(Frame::MAX_CONTROL_FRAME_SIZE, 'A control frame cannot be larger than 125 bytes.'); } } } /** * You can call this method to be sure your frame is valid before trying to get the raw data. - * + * * @return bool */ public function isValid() : bool diff --git a/src/Rfc6455/Message.php b/src/Rfc6455/Message.php index 7296503..3589a01 100644 --- a/src/Rfc6455/Message.php +++ b/src/Rfc6455/Message.php @@ -10,9 +10,11 @@ namespace Nekland\Woketo\Rfc6455; +use Nekland\Tools\StringTools; use Nekland\Woketo\Exception\Frame\IncompleteFrameException; use Nekland\Woketo\Exception\LimitationException; use Nekland\Woketo\Exception\MissingDataException; +use Nekland\Woketo\Utils\BitManipulation; class Message { @@ -40,17 +42,21 @@ public function __construct() public function addData($data) { - try { - if ('' === $this->buffer) { - $this->addFrame(new Frame($data)); - } else { - $this->addFrame(new Frame($this->buffer . $data)); + $this->buffer .= $data; + do { + try { + $this->addFrame($frame = new Frame($this->buffer)); + $this->buffer = StringTools::removeStart($this->buffer, $frame->getRawData(), '8bit'); + } catch (IncompleteFrameException $e) { + return ''; // There is no more frame we can generate, the data is saved as buffer. } - $this->buffer = ''; + } while(!$this->isComplete() && !empty($this->buffer)); - } catch (IncompleteFrameException $e) { - $this->buffer .= $data; + if ($this->isComplete()) { + return $this->buffer; } + + return ''; } /** diff --git a/src/Rfc6455/MessageProcessor.php b/src/Rfc6455/MessageProcessor.php index 95d09cd..ce5ca94 100644 --- a/src/Rfc6455/MessageProcessor.php +++ b/src/Rfc6455/MessageProcessor.php @@ -10,8 +10,10 @@ namespace Nekland\Woketo\Rfc6455; +use Nekland\Tools\StringTools; use Nekland\Woketo\Exception\LimitationException; use Nekland\Woketo\Rfc6455\MessageHandler\Rfc6455MessageHandlerInterface; +use Nekland\Woketo\Utils\BitManipulation; use React\Socket\ConnectionInterface; /** @@ -39,35 +41,41 @@ public function __construct(FrameFactory $factory = null) } /** - * @param string $data + * @param string $data * @param ConnectionInterface $socket - * @param Message|null $message - * @return Message|null + * @param Message|null $message + * @return \Generator */ public function onData(string $data, ConnectionInterface $socket, Message $message = null) { - if (null === $message) { - $message = new Message(); - } + do { - try { - $message->addData($data); - if ($message->isComplete()) { - foreach ($this->handlers as $handler) { - if ($handler->supports($message)) { - $handler->process($message, $this, $socket); - return null; - } - } + if (null === $message) { + $message = new Message(); } - return $message; - } catch (LimitationException $e) { - $this->write($this->frameFactory->createCloseFrame(Frame::CLOSE_TOO_BIG_TO_PROCESS), $socket); - $socket->end(); - } + try { + $data = $message->addData($data); - return null; + if ($message->isComplete()) { + foreach ($this->handlers as $handler) { + if ($handler->supports($message)) { + $handler->process($message, $this, $socket); + } + } + + yield $message; + $message = null; + } else { + yield $message; + } + + } catch (LimitationException $e) { + $this->write($this->frameFactory->createCloseFrame(Frame::CLOSE_TOO_BIG_TO_PROCESS), $socket); + $socket->end(); + $data = ''; + } + } while(!empty($data)); } /** diff --git a/src/Server/Connection.php b/src/Server/Connection.php index e9ed51c..3d3f802 100644 --- a/src/Server/Connection.php +++ b/src/Server/Connection.php @@ -118,26 +118,28 @@ protected function processMessage($data) $this->timeout = null; } - $this->currentMessage = $this->messageProcessor->onData($data, $this->socketStream, $this->currentMessage); - - if (null !== $this->currentMessage && $this->currentMessage->isComplete()) { - // Sending the message through the woketo API. - switch($this->currentMessage->getOpcode()) { - case Frame::OP_TEXT: - $this->handler->onMessage($this->currentMessage->getContent(), $this); - break; - case Frame::OP_BINARY: - $this->handler->onBinary($this->currentMessage->getContent(), $this); - break; - } - $this->currentMessage = null; + foreach ($this->messageProcessor->onData($data, $this->socketStream, $this->currentMessage) as $message) { + $this->currentMessage = $message; + if ($this->currentMessage->isComplete()) { + // Sending the message through the woketo API. + switch($this->currentMessage->getOpcode()) { + case Frame::OP_TEXT: + $this->handler->onMessage($this->currentMessage->getContent(), $this); + break; + case Frame::OP_BINARY: + $this->handler->onBinary($this->currentMessage->getContent(), $this); + break; + } + $this->currentMessage = null; - } else if (null !== $this->currentMessage && !$this->currentMessage->isComplete()) { - // We wait for more data so we start a timeout. - $this->timeout = $this->loop->addTimer(Connection::DEFAULT_TIMEOUT, function () { - $this->messageProcessor->timeout($this->socketStream); - }); + } else { + // We wait for more data so we start a timeout. + $this->timeout = $this->loop->addTimer(Connection::DEFAULT_TIMEOUT, function () { + $this->messageProcessor->timeout($this->socketStream); + }); + } } + } /** diff --git a/src/Utils/BitManipulation.php b/src/Utils/BitManipulation.php index 69c44d0..ed64f18 100644 --- a/src/Utils/BitManipulation.php +++ b/src/Utils/BitManipulation.php @@ -20,6 +20,12 @@ */ class BitManipulation { + /** + * Mode from to is the default mode of inspection of frames. But PHP usually uses from and length to inspect frames. + */ + const MODE_FROM_TO = 0; + const MODE_PHP = 1; + /** * Get a specific bit from a byte. * @@ -193,12 +199,29 @@ public static function bytesFromTo($frame, int $from, int $to, bool $force8bytes return $res; } + /** + * Proxy to the substr to be sure to be use the right method (mb_substr) + * + * @param string $frame + * @param int $from + * @param int $to + * @return string + */ + public static function bytesFromToString(string $frame, int $from, int $to, int $mode = BitManipulation::MODE_FROM_TO) : string + { + if ($mode === BitManipulation::MODE_FROM_TO) { + return mb_substr($frame, $from, $to - $from + 1, '8bit'); + } + + return mb_substr($frame, $from, $to, '8bit'); + } + /** * Take a frame represented by a decimal int to transform it in a string. * Notice that any int is a frame and cannot be more than 8 bytes * * @param int $frame - * @param int|null $size In bytes. + * @param int|null $size In bytes. This value should always be precise. Be careful if you don't ! * @return string */ public static function intToString(int $frame, int $size = null) : string diff --git a/tests/Woketo/Core/ConnectionTest.php b/tests/Woketo/Core/ConnectionTest.php index 42ae3b2..9de203d 100644 --- a/tests/Woketo/Core/ConnectionTest.php +++ b/tests/Woketo/Core/ConnectionTest.php @@ -44,7 +44,7 @@ public function testItSupportsTextMessage() $message->isComplete()->willReturn(true); $message->getOpcode()->willReturn(Frame::OP_TEXT); $message->getContent()->willReturn('Hello'); - $processor->onData($helloFrame, Argument::cetera())->willReturn($message->reveal()); + $processor->onData($helloFrame, Argument::cetera())->willReturn([$message->reveal()]); $handler->onConnection(Argument::type(Connection::class))->shouldBeCalled(); $handler->onMessage('Hello', Argument::type(Connection::class))->shouldBeCalled(); @@ -76,7 +76,7 @@ public function testItSupportsBinaryMessage() $message->isComplete()->willReturn(true); $message->getOpcode()->willReturn(Frame::OP_BINARY); $message->getContent()->willReturn($binary); - $processor->onData($binaryFrame, Argument::cetera())->willReturn($message->reveal()); + $processor->onData($binaryFrame, Argument::cetera())->willReturn([$message->reveal()]); $handler->onConnection(Argument::type(Connection::class))->shouldBeCalled(); $handler->onBinary($binary, Argument::type(Connection::class))->shouldBeCalled(); diff --git a/tests/Woketo/Rfc6455/FrameTest.php b/tests/Woketo/Rfc6455/FrameTest.php index 4bc6b2d..0e02586 100644 --- a/tests/Woketo/Rfc6455/FrameTest.php +++ b/tests/Woketo/Rfc6455/FrameTest.php @@ -79,14 +79,14 @@ public function testPingUnmaskedFrameContainingHello() public function testPongMaskedFrameContainingHello() { - $helloUnmaskedPingFrame = new Frame( - BitManipulation::hexArrayToString('8a', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58') - ); + $raw = BitManipulation::hexArrayToString('8a', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + $helloUnmaskedPingFrame = new Frame($raw); $this->assertSame($helloUnmaskedPingFrame->isMasked(), true); $this->assertSame($helloUnmaskedPingFrame->isFinal(), true); $this->assertSame($helloUnmaskedPingFrame->getPayload(), 'Hello'); $this->assertSame($helloUnmaskedPingFrame->getOpcode(), Frame::OP_PONG); + $this->assertSame($helloUnmaskedPingFrame->getRawData(), $raw); } public function testItSupportsEmptyFrames() @@ -100,6 +100,18 @@ public function testItSupportsEmptyFrames() ])); } + public function testItTakesOnlyFirstWebsocketFrameFromEntryData() + { + // 2 frames masked with `Hello` as content + $entryData = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58', '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + $firstDataFrame = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + + $frame = new Frame($entryData); + + $this->assertSame($frame->getRawData(), $firstDataFrame); + $this->assertSame($frame->getPayload(), 'Hello'); + } + /** * @dataProvider frameDataGenerationTestProvider * @@ -134,6 +146,16 @@ public function testItGenerateFrameWith65536Bytes() $this->assertSame($expectedData, $frame->getRawData()); } + public function testItSupportsFrameWith65536PayloadFromRawData() + { + $payload = file_get_contents(__DIR__ . '/../../fixtures/65536.data'); + $rawData = BitManipulation::hexArrayToString('81', '7F', '00', '00', '00', '00', '00', '01', '00', '00') . $payload; + + $frame = new Frame($rawData); + + $this->assertSame($rawData, $frame->getRawData()); + } + public function frameDataGenerationTestProvider() { return [ diff --git a/tests/Woketo/Rfc6455/MessageProcessorTest.php b/tests/Woketo/Rfc6455/MessageProcessorTest.php index 4b3d0cf..0eb0ab8 100644 --- a/tests/Woketo/Rfc6455/MessageProcessorTest.php +++ b/tests/Woketo/Rfc6455/MessageProcessorTest.php @@ -15,6 +15,7 @@ use Nekland\Woketo\Rfc6455\Frame; use Nekland\Woketo\Rfc6455\FrameFactory; use Nekland\Woketo\Rfc6455\Message; +use Nekland\Woketo\Rfc6455\MessageHandler\PingFrameHandler; use Nekland\Woketo\Rfc6455\MessageHandler\Rfc6455MessageHandlerInterface; use Nekland\Woketo\Rfc6455\MessageProcessor; use Nekland\Woketo\Utils\BitManipulation; @@ -37,14 +38,47 @@ public function testItBuildMessagesUsingMessageClass() $processor = new MessageProcessor(); /** @var Message $message */ - $message = $processor->onData( + $messages = iterator_to_array($processor->onData( // Hello normal frame BitManipulation::hexArrayToString(['81', '05', '48', '65', '6c', '6c', '6f']), $this->socket->reveal() + )); + + $this->assertInstanceOf(Message::class, $messages[0]); + $this->assertSame('Hello', $messages[0]->getContent()); + } + + public function testItBuildManyMessagesWithOnlyOneFrameData() + { + $multipleFrameData = BitManipulation::hexArrayToString( + '01', '03', '48', '65', '6c', // Data part 1 + '80', '02', '6c', '6f', // Data part 2 + '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58' // Another message (Hello frame) + ); + + $processor = new MessageProcessor(); + + $messages = iterator_to_array($processor->onData($multipleFrameData, $this->socket->reveal())); + + $this->assertSame(count($messages), 2); + $this->assertSame($messages[1]->getContent(), 'Hello'); + } + + public function testItContinueFrameEvaluationAfterControlFrame() + { + $multipleFrameData = BitManipulation::hexArrayToString( + '89', '00', // Ping + '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58', // Another message (Hello frame) + '89', '00' // Ping ); - $this->assertInstanceOf(Message::class, $message); - $this->assertSame('Hello', $message->getContent()); + $processor = new MessageProcessor(); + $processor->addHandler(new PingFrameHandler()); + + $messages = iterator_to_array($processor->onData($multipleFrameData, $this->socket->reveal())); + + $this->assertSame(count($messages), 3); + $this->assertSame($messages[1]->getContent(), 'Hello'); } public function testItBuildPartialMessage() @@ -52,23 +86,23 @@ public function testItBuildPartialMessage() $processor = new MessageProcessor(); $socket = $this->socket->reveal(); - $message = $processor->onData( + $messages = iterator_to_array($processor->onData( // "Hel" normal frame unmasked BitManipulation::hexArrayToString(['01', '03', '48', '65', '6c']), $socket - ); + )); - $this->assertSame($message->isComplete(), false); + $this->assertSame($messages[0]->isComplete(), false); - $processor->onData( + iterator_to_array($processor->onData( // "lo" normal frame unmasked BitManipulation::hexArrayToString(['80', '02', '6c', '6f']), $socket, - $message - ); + $messages[0] + )); - $this->assertSame($message->isComplete(), true); - $this->assertSame($message->getContent(), 'Hello'); + $this->assertSame($messages[0]->isComplete(), true); + $this->assertSame($messages[0]->getContent(), 'Hello'); } public function testItHandleSpecialMessagesWithHandler() @@ -88,12 +122,12 @@ public function process(Message $message, MessageProcessor $messageProcessor, Co $this->socket->write(Argument::cetera())->shouldBeCalled(); - $message = $processor->onData( + $messages = iterator_to_array($processor->onData( BitManipulation::hexArrayToString(['88', '02', '03', 'E8']), $this->socket->reveal() - ); + )); - $this->assertSame($message, null); + $this->assertSame($messages[0]->getOpcode(), Frame::OP_CLOSE); } public function testItReturnTheFrameFactory() diff --git a/tests/Woketo/Rfc6455/MessageTest.php b/tests/Woketo/Rfc6455/MessageTest.php index 3051399..853d15c 100644 --- a/tests/Woketo/Rfc6455/MessageTest.php +++ b/tests/Woketo/Rfc6455/MessageTest.php @@ -40,6 +40,40 @@ public function testItStackFramesAndReturnCompleteMessage() $this->assertSame($message->getContent(), 'foo bar baz'); } + public function testItReturnUnusedDataOnAddDataCall() + { + // 2 frames "Hello" + $entryData = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58', '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58'); + + $message = new Message(); + $data = $message->addData($entryData); + + $this->assertSame($data, BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58')); + } + + public function testItCompleteMessageWithMultipleFramesWhenDataAllowIt() + { + $multipleFrameData = BitManipulation::hexArrayToString( + '01', '03', '48', '65', '6c', // Data part 1 + '80', '02', '6c', '6f', // Data part 2 + '81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58' // Another message (Hello frame) + ); + + $message = new Message(); + + $this->assertSame($message->addData($multipleFrameData), BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d', '7f', '9f', '4d', '51', '58')); + } + + public function testItReturnNothingWhenBufferingWhenAddData() + { + $incompleteFrame = BitManipulation::hexArrayToString('81', '85', '37', 'fa', '21', '3d'); + + $message = new Message(); + + $this->assertSame($message->addData($incompleteFrame), ''); + $this->assertSame($message->isComplete(), false); + } + public function testItThrowErrorWhenMissingFrame() { /** @var Frame $frame1 */ diff --git a/tests/Woketo/Utils/BitManipulationTest.php b/tests/Woketo/Utils/BitManipulationTest.php index f5ca671..3dc59de 100644 --- a/tests/Woketo/Utils/BitManipulationTest.php +++ b/tests/Woketo/Utils/BitManipulationTest.php @@ -119,6 +119,12 @@ public function testItTransformToHex($a, $b) $this->assertSame($a, BitManipulation::frameToHex($b)); } + public function testItRetrieveSubFrames() + { + $this->assertSame('bcd', BitManipulation::bytesFromToString('abcdefg', 1, 3)); + $this->assertSame('ef', BitManipulation::bytesFromToString('abcdefg', 4, 2, BitManipulation::MODE_PHP)); + } + // // Providers //