Message buffering & Refactoring
Refactored unframe() methods into Message/Frame classes (per protocol version) Change onRecv of WebSocket App to use new interfaces to test statuses, resulting in reuniting a message fragmented by TCP Wrote unit test covering most of new HyBi10 Frame class
This commit is contained in:
parent
1355e4400b
commit
5653f01f2f
@ -101,14 +101,35 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
|||||||
return $comp;
|
return $comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// buffer!
|
if (!isset($from->WebSocket->message)) {
|
||||||
|
$from->WebSocket->message = $from->WebSocket->version->newMessage();
|
||||||
$msg = $from->WebSocket->version->unframe($msg);
|
|
||||||
if (is_array($msg)) { // temporary
|
|
||||||
$msg = $msg['payload'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->prepareCommand($this->_app->onRecv($from, $msg));
|
// 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) {
|
||||||
|
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()) {
|
||||||
|
$cmds = $this->prepareCommand($this->_app->onRecv($from, (string)$from->WebSocket->message));
|
||||||
|
unset($from->WebSocket->message);
|
||||||
|
|
||||||
|
return $cmds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function onClose(Connection $conn) {
|
public function onClose(Connection $conn) {
|
||||||
@ -153,7 +174,7 @@ class App implements ApplicationInterface, ConfiguratorInterface {
|
|||||||
* @todo Can/will add more versions later, but perhaps a chain of responsibility, ask each version if they want to handle the request
|
* @todo Can/will add more versions later, but perhaps a chain of responsibility, ask each version if they want to handle the request
|
||||||
*/
|
*/
|
||||||
protected function getVersion($message) {
|
protected function getVersion($message) {
|
||||||
if (false === strstr($message, "\r\n\r\n")) { // This _could_ fail with Hixie
|
if (false === strstr($message, "\r\n\r\n")) { // This CAN fail with Hixie, depending on the TCP buffer in between
|
||||||
throw new \UnderflowException;
|
throw new \UnderflowException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\Protocol\WebSocket;
|
namespace Ratchet\Application\WebSocket\Version;
|
||||||
|
|
||||||
interface FrameInterface {
|
interface FrameInterface {
|
||||||
|
/**
|
||||||
|
* @alias getPayload
|
||||||
|
*/
|
||||||
|
function __toString();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dunno if I'll use this
|
||||||
|
* Thinking could be used if a control frame?
|
||||||
|
*/
|
||||||
|
// function __invoke();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
@ -9,18 +20,19 @@ interface FrameInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string
|
* @param string
|
||||||
|
* @todo Theoretically, there won't be a buffer overflow (end of frame + start of new frame) - but test later, return a string with overflow here
|
||||||
*/
|
*/
|
||||||
function addBuffer($buf);
|
function addBuffer($buf);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
function isFragment();
|
// function isFragment();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
function isFinial();
|
function isFinal();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
@ -40,7 +52,7 @@ interface FrameInterface {
|
|||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
function getReceivedPayloadLength();
|
// function getReceivedPayloadLength();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 32-big string
|
* 32-big string
|
@ -2,6 +2,13 @@
|
|||||||
namespace Ratchet\Application\WebSocket\Version;
|
namespace Ratchet\Application\WebSocket\Version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
|
||||||
|
* Hixie76 is bad for 2 (there's more) reasons:
|
||||||
|
* 1) The handshake is done in HTTP, which includes a key for signing in the body...
|
||||||
|
* BUT there is no Length defined in the header (as per HTTP spec) so the TCP buffer can't tell when the message is done!
|
||||||
|
* 2) By nature it's insecure. Google did a test study where they were able to do a
|
||||||
|
* man-in-the-middle attack on 10%-15% of the people who saw their add 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
|
* The Hixie76 is currently implemented by Safari
|
||||||
* Handshake from Andrea Giammarchi (http://webreflection.blogspot.com/2010/06/websocket-handshake-76-simplified.html)
|
* 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
|
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||||
@ -34,6 +41,14 @@ class Hixie76 implements VersionInterface {
|
|||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function newMessage() {
|
||||||
|
return new Hixie76\Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function newFrame() {
|
||||||
|
return new Hixie76\Frame;
|
||||||
|
}
|
||||||
|
|
||||||
public function unframe($message) {
|
public function unframe($message) {
|
||||||
return substr($message, 1, strlen($message) - 2);
|
return substr($message, 1, strlen($message) - 2);
|
||||||
}
|
}
|
||||||
|
58
lib/Ratchet/Application/WebSocket/Version/Hixie76/Frame.php
Normal file
58
lib/Ratchet/Application/WebSocket/Version/Hixie76/Frame.php
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\Application\WebSocket\Version\Hixie76;
|
||||||
|
use Ratchet\Application\WebSocket\Version\FrameInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This does not entirely follow the protocol to spec, but (mostly) works
|
||||||
|
* Hixie76 probably should not even be supported
|
||||||
|
*/
|
||||||
|
class Frame implements FrameInterface {
|
||||||
|
/**
|
||||||
|
* @type string
|
||||||
|
*/
|
||||||
|
protected $_data = '';
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
return $this->getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCoalesced() {
|
||||||
|
return (boolean)($this->_data[0] == chr(0) && substr($this->_data, -1) == chr(255));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addBuffer($buf) {
|
||||||
|
$this->_data .= (string)$buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFinal() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isMasked() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOpcode() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayloadLength() {
|
||||||
|
if (!$this->isCoalesced()) {
|
||||||
|
throw new \UnderflowException('Not enough of the message has been buffered to determine the length of the payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return strlen($this->_data) - 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMaskingKey() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayload() {
|
||||||
|
if (!$this->isCoalesced()) {
|
||||||
|
return new \UnderflowException('Not enough data buffered to read payload');
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($this->_data, 1, strlen($this->_data) - 2);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\Application\WebSocket\Version\Hixie76;
|
||||||
|
use Ratchet\Application\WebSocket\Version\MessageInterface;
|
||||||
|
use Ratchet\Application\WebSocket\Version\FrameInterface;
|
||||||
|
|
||||||
|
class Message implements MessageInterface {
|
||||||
|
/**
|
||||||
|
* @var Ratchet\Application\WebSocket\Version\FrameInterface
|
||||||
|
*/
|
||||||
|
protected $_frame = null;
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
return $this->getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCoalesced() {
|
||||||
|
if (!($this->_frame instanceof FrameInterface)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_frame->isCoalesced();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addFrame(FrameInterface $fragment) {
|
||||||
|
if (null !== $this->_frame) {
|
||||||
|
throw new \OverflowException('Hixie76 does not support multiple framing of messages');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_frame = $fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOpcode() {
|
||||||
|
// Hixie76 only supported text messages
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayloadLength() {
|
||||||
|
throw new \DomainException('Please sir, may I have some code? (' . __FUNCTION__ . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayload() {
|
||||||
|
if (!$this->isCoalesced()) {
|
||||||
|
throw new \UnderflowException('Message has not been fully buffered yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_frame->getPayload();
|
||||||
|
}
|
||||||
|
}
|
@ -5,12 +5,14 @@ use Ratchet\Application\WebSocket\Util\HTTP;
|
|||||||
/**
|
/**
|
||||||
* The HyBi-10 version, identified in the headers as version 8, is currently implemented by the latest Chrome and Firefix version
|
* The HyBi-10 version, identified in the headers as version 8, is currently implemented by the latest Chrome and Firefix version
|
||||||
* @link http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
|
* @link http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
|
||||||
|
* @todo Naming...I'm not fond of this naming convention...
|
||||||
*/
|
*/
|
||||||
class HyBi10 implements VersionInterface {
|
class HyBi10 implements VersionInterface {
|
||||||
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array
|
* @return array
|
||||||
|
* I kept this as an array and combined in App for future considerations...easier to add a subprotol as a key value than edit a string
|
||||||
*/
|
*/
|
||||||
public function handshake($message) {
|
public function handshake($message) {
|
||||||
$headers = HTTP::getHeaders($message);
|
$headers = HTTP::getHeaders($message);
|
||||||
@ -25,6 +27,20 @@ class HyBi10 implements VersionInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HyBi10\Message
|
||||||
|
*/
|
||||||
|
public function newMessage() {
|
||||||
|
return new HyBi10\Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HyBi10\Frame
|
||||||
|
*/
|
||||||
|
public function newFrame() {
|
||||||
|
return new HyBi10\Frame;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unframe a message received from the client
|
* Unframe a message received from the client
|
||||||
* Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame
|
* Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame
|
||||||
|
203
lib/Ratchet/Application/WebSocket/Version/HyBi10/Frame.php
Normal file
203
lib/Ratchet/Application/WebSocket/Version/HyBi10/Frame.php
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\Application\WebSocket\Version\HyBi10;
|
||||||
|
use Ratchet\Application\WebSocket\Version\FrameInterface;
|
||||||
|
|
||||||
|
class Frame implements FrameInterface {
|
||||||
|
/**
|
||||||
|
* The contents of the frame
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $_data = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of bytes received from the frame
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $_bytes_rec = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of bytes in the payload (as per framing protocol)
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $_pay_len_def = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bit 9-15
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $_pay_check = -1;
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
return (string)$this->getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCoalesced() {
|
||||||
|
try {
|
||||||
|
$payload_length = $this->getPayloadLength();
|
||||||
|
$payload_start = $this->getPayloadStartingByte();
|
||||||
|
} catch (\UnderflowException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload_length + $payload_start === $this->_bytes_rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addBuffer($buf) {
|
||||||
|
$buf = (string)$buf;
|
||||||
|
|
||||||
|
$this->_data .= $buf;
|
||||||
|
$this->_bytes_rec += strlen($buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFinal() {
|
||||||
|
if ($this->_bytes_rec < 1) {
|
||||||
|
throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fbb = sprintf('%08b', ord($this->_data[0]));
|
||||||
|
return (boolean)(int)$fbb[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isMasked() {
|
||||||
|
if ($this->_bytes_rec < 2) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOpcode() {
|
||||||
|
if ($this->_bytes_rec < 1) {
|
||||||
|
throw new \UnderflowException('Not enough bytes received to determine opcode');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindec(substr(sprintf('%08b', ord($this->_data[0])), 4, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the decimal value of bits 9 (10th) through 15 inclusive
|
||||||
|
* @return int
|
||||||
|
* @throws UnderflowException If the buffer doesn't have enough data to determine this
|
||||||
|
*/
|
||||||
|
protected function getFirstPayloadVal() {
|
||||||
|
if ($this->_bytes_rec < 2) {
|
||||||
|
throw new \UnderflowException('Not enough bytes received');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ord($this->_data[1]) & 127;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int (7|23|71) Number of bits defined for the payload length in the fame
|
||||||
|
* @throws UnderflowException
|
||||||
|
*/
|
||||||
|
protected function getNumPayloadBits() {
|
||||||
|
if ($this->_bytes_rec < 2) {
|
||||||
|
throw new \UnderflowException('Not enough bytes received');
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default 7 bits are used to describe the payload length
|
||||||
|
// These are bits 9 (10th) through 15 inclusive
|
||||||
|
$bits = 7;
|
||||||
|
|
||||||
|
// Get the value of those bits
|
||||||
|
$check = $this->getFirstPayloadVal();
|
||||||
|
|
||||||
|
// If the value is 126 the 7 bits plus the next 16 are used to describe the payload length
|
||||||
|
if ($check >= 126) {
|
||||||
|
$bits += 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the value of the initial payload length are is 127 an additional 48 bits are used to describe length
|
||||||
|
// Note: The documentation specifies the length is to be 63 bits, but I think that's a type and is 64 (16+48)
|
||||||
|
if ($check === 127) {
|
||||||
|
$bits += 48;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($bits, array(7, 23, 71))) {
|
||||||
|
throw new \UnexpectedValueException("Malformed frame, invalid payload length provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits)
|
||||||
|
* @see getNumPayloadBits
|
||||||
|
*/
|
||||||
|
protected function getNumPayloadBytes() {
|
||||||
|
return (1 + $this->getNumPayloadBits()) / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayloadLength() {
|
||||||
|
if ($this->_pay_len_def !== -1) {
|
||||||
|
return $this->_pay_len_def;
|
||||||
|
}
|
||||||
|
|
||||||
|
$length_check = $this->getFirstPayloadVal();
|
||||||
|
|
||||||
|
if ($length_check <= 125) {
|
||||||
|
$this->_pay_len_def = $length_check;
|
||||||
|
return $this->getPayloadLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
$byte_length = $this->getNumPayloadBytes();
|
||||||
|
if ($this->_bytes_rec < 1 + $byte_length) {
|
||||||
|
throw new \UnderflowException('Not enough data buffered to determine payload length');
|
||||||
|
}
|
||||||
|
|
||||||
|
$strings = array();
|
||||||
|
for ($i = 2; $i < $byte_length + 1; $i++) {
|
||||||
|
$strings[] = ord($this->_data[$i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings));
|
||||||
|
return $this->getPayloadLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayloadStartingByte() {
|
||||||
|
return 1 + $this->getNumPayloadBytes() + strlen($this->getMaskingKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayload() {
|
||||||
|
if (!$this->isCoalesced()) {
|
||||||
|
throw new \UnderflowException('Can not return partial message');
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = '';
|
||||||
|
$length = $this->getPayloadLength();
|
||||||
|
|
||||||
|
if ($this->isMasked()) {
|
||||||
|
$mask = $this->getMaskingKey();
|
||||||
|
$start = $this->getPayloadStartingByte();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$payload .= $this->_data[$i + $start] ^ $mask[$i % 4];
|
||||||
|
}
|
||||||
|
} 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
}
|
63
lib/Ratchet/Application/WebSocket/Version/HyBi10/Message.php
Normal file
63
lib/Ratchet/Application/WebSocket/Version/HyBi10/Message.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\Application\WebSocket\Version\HyBi10;
|
||||||
|
use Ratchet\Application\WebSocket\Version\MessageInterface;
|
||||||
|
use Ratchet\Application\WebSocket\Version\FrameInterface;
|
||||||
|
|
||||||
|
class Message implements MessageInterface {
|
||||||
|
/**
|
||||||
|
* @var SplDoublyLinkedList
|
||||||
|
*/
|
||||||
|
protected $_frames;
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->_frames = new \SplDoublyLinkedList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
return $this->getPayload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isCoalesced() {
|
||||||
|
if (count($this->_frames) == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$last = $this->_frames->top();
|
||||||
|
|
||||||
|
return ($last->isCoalesced() && $last->isFinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
||||||
|
$this->_frames->push($fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOpcode() {
|
||||||
|
if (count($this->_frames) == 0) {
|
||||||
|
throw new \UnderflowException('No frames have been added to this message');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->_frames->bottom()->getOpcode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayloadLength() {
|
||||||
|
throw new \DomainException('Please sir, may I have some code? (' . __FUNCTION__ . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayload() {
|
||||||
|
if (!$this->isCoalesced()) {
|
||||||
|
throw new \UnderflowMessage('Message has not been put back together yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
$buffer = '';
|
||||||
|
|
||||||
|
foreach ($this->_frames as $frame) {
|
||||||
|
$buffer .= (string)$frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $buffer;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\Protocol\WebSocket;
|
namespace Ratchet\Application\WebSocket\Version;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo Consider making parent interface/composite for Message/Frame with (isCoalesced, getOpcdoe, getPayloadLength, getPayload)
|
* @todo Consider making parent interface/composite for Message/Frame with (isCoalesced, getOpcdoe, getPayloadLength, getPayload)
|
||||||
*/
|
*/
|
||||||
interface MessageInterface {
|
interface MessageInterface {
|
||||||
|
/**
|
||||||
|
* @alias getPayload
|
||||||
|
*/
|
||||||
|
function __toString();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
@ -13,7 +18,7 @@ interface MessageInterface {
|
|||||||
/**
|
/**
|
||||||
* @param FragmentInterface
|
* @param FragmentInterface
|
||||||
*/
|
*/
|
||||||
function addFragment(FragmentInterface $fragment);
|
function addFrame(FrameInterface $fragment);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
@ -13,11 +13,22 @@ interface VersionInterface {
|
|||||||
*/
|
*/
|
||||||
function handshake($message);
|
function handshake($message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return MessageInterface
|
||||||
|
*/
|
||||||
|
function newMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return FrameInterface
|
||||||
|
*/
|
||||||
|
function newFrame();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a framed message as per the protocol and return the decoded message
|
* Get a framed message as per the protocol and return the decoded message
|
||||||
* @param string
|
* @param string
|
||||||
* @return string
|
* @return string
|
||||||
* @todo Return a frame object with message, type, masked?
|
* @todo Return a frame object with message, type, masked?
|
||||||
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
function unframe($message);
|
function unframe($message);
|
||||||
|
|
||||||
|
@ -0,0 +1,256 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\Tests\Application\WebSocket\Version\HyBi10;
|
||||||
|
use Ratchet\Application\WebSocket\Version\HyBi10\Frame;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers Ratchet\Application\WebSocket\Version\HyBi10\Frame
|
||||||
|
* @todo getMaskingKey, getPayloadStartingByte don't have tests yet
|
||||||
|
* @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry.
|
||||||
|
*/
|
||||||
|
class FrameTest extends \PHPUnit_Framework_TestCase {
|
||||||
|
protected $_firstByteFinText = '10000001';
|
||||||
|
protected $_secondByteMaskedSPL = '11111101';
|
||||||
|
|
||||||
|
protected $_frame;
|
||||||
|
|
||||||
|
protected $_packer;
|
||||||
|
|
||||||
|
public function setUp() {
|
||||||
|
$this->_frame = new Frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function convert($in) {
|
||||||
|
return pack('C', 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=')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, 8, '00001000')
|
||||||
|
, array(true, 10, '10001010')
|
||||||
|
, array(false, 15, '00001111')
|
||||||
|
, array(true, 1, '10000001')
|
||||||
|
, array(true, 15, '11111111')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering() {
|
||||||
|
return $this->markTestIncomplete();
|
||||||
|
|
||||||
|
$this->expectException('\UnderflowException');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider firstByteProvider
|
||||||
|
*/
|
||||||
|
public function testFinCodeFromBits($fin, $opcode, $bin) {
|
||||||
|
$this->_frame->addBuffer(static::convert($bin));
|
||||||
|
$this->assertEquals($fin, $this->_frame->isFinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider UnframeMessageProvider
|
||||||
|
*/
|
||||||
|
public function testFinCodeFromFullMessage($msg, $encoded) {
|
||||||
|
$this->_frame->addBuffer(base64_decode($encoded));
|
||||||
|
$this->assertTrue($this->_frame->isFinal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider firstByteProvider
|
||||||
|
*/
|
||||||
|
public function testOpcodeFromBits($fin, $opcode, $bin) {
|
||||||
|
$this->_frame->addBuffer(static::convert($bin));
|
||||||
|
$this->assertEquals($opcode, $this->_frame->getOpcode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider UnframeMessageProvider
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
public function testFirstPayloadDesignationValue($bits, $bin) {
|
||||||
|
$this->_frame->addBuffer(static::convert($this->_firstByteFinText));
|
||||||
|
$this->_frame->addBuffer(static::convert($bin));
|
||||||
|
|
||||||
|
$ref = new \ReflectionClass($this->_frame);
|
||||||
|
$cb = $ref->getMethod('getFirstPayloadVal');
|
||||||
|
$cb->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals(bindec($bin), $cb->invoke($this->_frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider payloadLengthDescriptionProvider
|
||||||
|
*/
|
||||||
|
public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) {
|
||||||
|
$this->_frame->addBuffer(static::convert($this->_firstByteFinText));
|
||||||
|
$this->_frame->addBuffer(static::convert($bin));
|
||||||
|
|
||||||
|
$ref = new \ReflectionClass($this->_frame);
|
||||||
|
$cb = $ref->getMethod('getNumPayloadBits');
|
||||||
|
$cb->setAccessible(true);
|
||||||
|
|
||||||
|
$this->assertEquals($expected_bits, $cb->invoke($this->_frame));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function secondByteProvider() {
|
||||||
|
return array(
|
||||||
|
array(true, 1, '10000001')
|
||||||
|
, array(false, 1, '00000001')
|
||||||
|
, array(true, 125, $this->_secondByteMaskedSPL)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider secondByteProvider
|
||||||
|
*/
|
||||||
|
public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) {
|
||||||
|
$this->_frame->addBuffer(static::convert($this->_firstByteFinText));
|
||||||
|
$this->_frame->addBuffer(static::convert($bin));
|
||||||
|
|
||||||
|
$this->assertEquals($masked, $this->_frame->isMasked());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider UnframeMessageProvider
|
||||||
|
*/
|
||||||
|
public function testIsMaskedFromFullMessage($msg, $encoded) {
|
||||||
|
$this->_frame->addBuffer(base64_decode($encoded));
|
||||||
|
$this->assertTrue($this->_frame->isMasked());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider secondByteProvider
|
||||||
|
*/
|
||||||
|
public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) {
|
||||||
|
$this->_frame->addBuffer(static::convert($this->_firstByteFinText));
|
||||||
|
$this->_frame->addBuffer(static::convert($bin));
|
||||||
|
|
||||||
|
$this->assertEquals($payload_length, $this->_frame->getPayloadLength());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider UnframeMessageProvider
|
||||||
|
* @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());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateMask() {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getPacker() {
|
||||||
|
return function($str) {
|
||||||
|
$packed = '';
|
||||||
|
for ($i = 0, $len = strlen($str); $i < $len; $i++) {
|
||||||
|
$packed .= pack('C', ord($str[$i]));
|
||||||
|
// $packed .= pack("c", ord(substr($str, $i, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $packed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function maskingKeyProvider() {
|
||||||
|
$packer = $this->getPacker();
|
||||||
|
$mask_generator = function() use ($packer) {
|
||||||
|
$characters = 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*() -=_+`~[]{}\|/,.<>';
|
||||||
|
$mask = '';
|
||||||
|
|
||||||
|
for ($i = 0; $i < 32; $i++) {
|
||||||
|
$mask .= $characters[mt_rand(0, strlen($characters) -1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array($mask, $packer($mask));
|
||||||
|
};
|
||||||
|
|
||||||
|
return array(
|
||||||
|
$mask_generator()
|
||||||
|
, $mask_generator()
|
||||||
|
, $mask_generator()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider maskingKeyProvider
|
||||||
|
* @todo I I wrote the dataProvider incorrectly, skpping for now
|
||||||
|
*/
|
||||||
|
public function testGetMaskingKey($mask, $bin) {
|
||||||
|
return $this->markTestIncomplete("I'm not packing the data properly in the provider, I think");
|
||||||
|
|
||||||
|
$this->_frame->addBuffer(static::convert($this->_firstByteFinText));
|
||||||
|
$this->_frame->addBuffer(static::convert($this->_secondByteMaskedSPL));
|
||||||
|
$this->_frame->addBuffer($bin);
|
||||||
|
|
||||||
|
$this->assertEquals($mask, $this->_frame->getMaskingKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider UnframeMessageProvider
|
||||||
|
* @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 messageFragmentProvider
|
||||||
|
*/
|
||||||
|
public function testCheckPiecingTogetherMessage($coalesced, $first_bin, $secnd_bin, $mask, $payload1, $payload2) {
|
||||||
|
return $this->markTestIncomplete('Ran out of time, had to attend to something else, come finish me!');
|
||||||
|
|
||||||
|
$this->_frame->addBuffer(static::convert($first_bin));
|
||||||
|
$this->_frame->addBuffer(static::convert($second_bin));
|
||||||
|
// mask?
|
||||||
|
// $this->_frame->addBuffer(
|
||||||
|
// $this->_frame->addBuffer(
|
||||||
|
|
||||||
|
$this->assertEquals($coalesced, $this->_frame->isCoalesced());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user