Skip to content

Commit

Permalink
Add supports of many websockets frames in the same binary frame
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Nek- committed Nov 6, 2016
1 parent f89082d commit 8dc874d
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 118 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 23 additions & 1 deletion src/Exception/Frame/TooBigFrameException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,39 @@
/**
* This file is a part of Woketo package.
*
* (c) Ci-tron <dev@ci-tron.org>
* (c) Nekland <dev@nekland.fr>
*
* For the full license, take a look to the LICENSE file
* on the root directory of this project
*/

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;
}
}
123 changes: 74 additions & 49 deletions src/Rfc6455/Frame.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -89,7 +90,7 @@ class Frame
* @var int
*/
private $secondByte;

/**
* @var bool
*/
Expand Down Expand Up @@ -133,25 +134,35 @@ 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
*/
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;
}

Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -221,23 +232,23 @@ public function setFinal(bool $final) : Frame
*/
public function getRsv1() : bool
{
return BitManipulation::nthBit($this->firstByte, 2);
return (bool) BitManipulation::nthBit($this->firstByte, 2);
}

/**
* @return boolean
*/
public function getRsv2() : bool
{
return BitManipulation::nthBit($this->firstByte, 3);
return (bool) BitManipulation::nthBit($this->firstByte, 3);
}

/**
* @return boolean
*/
public function getRsv3() : bool
{
return BitManipulation::nthBit($this->firstByte, 4);
return (bool) BitManipulation::nthBit($this->firstByte, 4);
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -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()
Expand All @@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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;
Expand All @@ -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];
}

Expand All @@ -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())) {
Expand All @@ -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
Expand Down
22 changes: 14 additions & 8 deletions src/Rfc6455/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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 '';
}

/**
Expand Down
Loading

0 comments on commit 8dc874d

Please sign in to comment.