From 291bd5da5a7eeecec9832aaaa64a57ea4106956a Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 2 Jun 2012 15:44:18 -0400 Subject: [PATCH] [WebSocket] RFC6455 Framing work New code to create a frame Unit tests for new code API cleanup --- src/Ratchet/WebSocket/HandshakeNegotiator.php | 1 + src/Ratchet/WebSocket/Version/RFC6455.php | 2 + .../WebSocket/Version/RFC6455/Frame.php | 96 +++++++++++++++---- src/Ratchet/WebSocket/WsConnection.php | 9 +- .../WebSocket/Version/RFC6455/FrameTest.php | 79 +++++++++------ 5 files changed, 136 insertions(+), 51 deletions(-) diff --git a/src/Ratchet/WebSocket/HandshakeNegotiator.php b/src/Ratchet/WebSocket/HandshakeNegotiator.php index 0cdef10..1bce9a3 100644 --- a/src/Ratchet/WebSocket/HandshakeNegotiator.php +++ b/src/Ratchet/WebSocket/HandshakeNegotiator.php @@ -77,6 +77,7 @@ class HandshakeNegotiator { * Determine if the message has been buffered as per the HTTP specification * @param string * @return boolean + * @todo Safari does not send 2xCRLF after the 6 byte body...this will always return false for Hixie */ public function isEom($message) { return (static::EOM === substr($message, 0 - strlen(static::EOM))); diff --git a/src/Ratchet/WebSocket/Version/RFC6455.php b/src/Ratchet/WebSocket/Version/RFC6455.php index 0fcf009..59cbdde 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455.php +++ b/src/Ratchet/WebSocket/Version/RFC6455.php @@ -73,6 +73,8 @@ class RFC6455 implements VersionInterface { * @return string */ public function frame($message, $mask = true) { +return RFC6455\Frame::create($message)->data; + $payload = $message; $type = 'text'; $masked = $mask; diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 8a17d6a..5a7a7ff 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php @@ -3,11 +3,18 @@ namespace Ratchet\WebSocket\Version\RFC6455; use Ratchet\WebSocket\Version\FrameInterface; class Frame implements FrameInterface { + const OP_CONTINUE = 0; + const OP_TEXT = 1; + const OP_BINARY = 2; + const OP_CLOSE = 8; + const OP_PING = 9; + const OP_PONG = 10; + /** * The contents of the frame * @var string */ - protected $_data = ''; + public $data = ''; /** * Number of bytes received from the frame @@ -22,10 +29,66 @@ class Frame implements FrameInterface { protected $_pay_len_def = -1; /** - * Bit 9-15 - * @var int + * @param string A valid UTF-8 string to send over the wire + * @param bool Is the final frame in a message + * @param int The opcode of the frame, see constants + * @param bool Mask the payload + * @return Frame + * @throws InvalidArgumentException If the payload is not a valid UTF-8 string + * @throws BadMethodCallException If there is a problem with miss-matching parameters + * @throws LengthException If the payload is too big */ - protected $_pay_check = -1; + public static function create($payload, $final = true, $opcode = 1, $mask = false) { + $frame = new static(); + + if (!mb_check_encoding($payload, 'UTF-8')) { + throw new \InvalidArgumentException("Payload is not a valid UTF-8 string"); + } + + if (false === (boolean)$final && $opcode !== static::OP_CONTINUE) { + throw new \BadMethodCallException("opcode MUST be 'continue' if the frame is not final"); + } + + $raw = (int)(boolean)$final . sprintf('%07b', (int)$opcode); + + $plLen = strlen($payload); + if ($plLen <= 125) { + $raw .= sprintf('%08b', $plLen); + } elseif ($plLen <= 65535) { + $raw .= sprintf('%08b', 126) . sprintf('%016b', $plLen); + } else { // todo, make sure msg isn't longer than b1x71 + $raw .= sprintf('%08b', 127) . sprintf('%064b', $plLen); + } + + $frame->addBuffer(static::encode($raw) . $payload); + + if ($mask) { + // create masking key + // insert it + // mask the payload + } + + return $frame; + } + + /** + * @param string of 1's and 0's + * @return string + */ + public static function encode($in) { + if (strlen($in) > 8) { + $out = ''; + + while (strlen($in) >= 8) { + $out .= static::encode(substr($in, 0, 8)); + $in = substr($in, 8); + } + + return $out; + } + + return chr(bindec($in)); + } /** * {@inheritdoc} @@ -38,7 +101,7 @@ class Frame implements FrameInterface { return false; } - return $payload_length + $payload_start === $this->_bytes_rec; + return $this->_bytes_rec >= $payload_length + $payload_start; } /** @@ -47,7 +110,7 @@ class Frame implements FrameInterface { public function addBuffer($buf) { $buf = (string)$buf; - $this->_data .= $buf; + $this->data .= $buf; $this->_bytes_rec += strlen($buf); } @@ -59,7 +122,8 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); } - $fbb = sprintf('%08b', ord(substr($this->_data, 0, 1))); + $fbb = sprintf('%08b', ord(substr($this->data, 0, 1))); + return (boolean)(int)$fbb[0]; } @@ -71,7 +135,7 @@ class Frame implements FrameInterface { throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); } - return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->_data, 1, 1))), 0, 1)); + return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->data, 1, 1))), 0, 1)); } /** @@ -82,7 +146,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received to determine opcode'); } - return bindec(substr(sprintf('%08b', ord(substr($this->_data, 0, 1))), 4, 4)); + return bindec(substr(sprintf('%08b', ord(substr($this->data, 0, 1))), 4, 4)); } /** @@ -95,7 +159,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough bytes received'); } - return ord(substr($this->_data, 1, 1)) & 127; + return ord(substr($this->data, 1, 1)) & 127; } /** @@ -163,7 +227,7 @@ class Frame implements FrameInterface { $strings = array(); for ($i = 2; $i < $byte_length + 1; $i++) { - $strings[] = ord(substr($this->_data, $i, 1)); + $strings[] = ord(substr($this->data, $i, 1)); } $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); @@ -186,7 +250,7 @@ class Frame implements FrameInterface { throw new \UnderflowException('Not enough data buffered to calculate the masking key'); } - return substr($this->_data, $start, $length); + return substr($this->data, $start, $length); } /** @@ -206,17 +270,17 @@ class Frame implements FrameInterface { $payload = ''; $length = $this->getPayloadLength(); + $start = $this->getPayloadStartingByte(); if ($this->isMasked()) { - $mask = $this->getMaskingKey(); - $start = $this->getPayloadStartingByte(); + $mask = $this->getMaskingKey(); for ($i = 0; $i < $length; $i++) { // Double check the RFC - is the masking byte level or character level? - $payload .= substr($this->_data, $i + $start, 1) ^ substr($mask, $i % 4, 1); + $payload .= substr($this->data, $i + $start, 1) ^ substr($mask, $i % 4, 1); } } else { - $payload = substr($this->_data, $start, $this->getPayloadLength()); + $payload = substr($this->data, $start, $this->getPayloadLength()); } if (strlen($payload) !== $length) { diff --git a/src/Ratchet/WebSocket/WsConnection.php b/src/Ratchet/WebSocket/WsConnection.php index a346fa9..de6c3fa 100644 --- a/src/Ratchet/WebSocket/WsConnection.php +++ b/src/Ratchet/WebSocket/WsConnection.php @@ -30,7 +30,7 @@ class WsConnection extends AbstractConnectionDecorator { } public function close() { - // send close frame + // send close frame with code 1000 // ??? @@ -39,14 +39,9 @@ class WsConnection extends AbstractConnectionDecorator { $this->getConnection()->close(); // temporary } - public function ping() { - } - - public function pong() { - } - /** * @return boolean + * @internal */ public function hasVersion() { return (null === $this->version); diff --git a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php index 84352fb..65827e0 100644 --- a/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/Ratchet/Tests/WebSocket/Version/RFC6455/FrameTest.php @@ -19,21 +19,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->_frame = new Frame; } - protected static function convert($in) { - if (strlen($in) > 8) { - $out = ''; - - while (strlen($in) > 8) { - $out .= static::convert(substr($in, 0, 8)); - $in = substr($in, 8); - } - - return $out; - } - - return pack('C', bindec($in)); - } - /** * This is a data provider * @param string The UTF8 message @@ -67,7 +52,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->setExpectedException('\UnderflowException'); if (!empty($bin)) { - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($bin)); } call_user_func(array($this->_frame, $method)); @@ -93,7 +78,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider firstByteProvider */ public function testFinCodeFromBits($fin, $opcode, $bin) { - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($fin, $this->_frame->isFinal()); } @@ -109,7 +94,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider firstByteProvider */ public function testOpcodeFromBits($fin, $opcode, $bin) { - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($opcode, $this->_frame->getOpcode()); } @@ -136,8 +121,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider payloadLengthDescriptionProvider */ public function testFirstPayloadDesignationValue($bits, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getFirstPayloadVal'); @@ -150,8 +135,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider payloadLengthDescriptionProvider */ public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $ref = new \ReflectionClass($this->_frame); $cb = $ref->getMethod('getNumPayloadBits'); @@ -172,8 +157,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider secondByteProvider */ public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($masked, $this->_frame->isMasked()); } @@ -190,8 +175,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @dataProvider secondByteProvider */ public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($bin)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($bin)); $this->assertEquals($payload_length, $this->_frame->getPayloadLength()); } @@ -230,8 +215,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase { * @todo I I wrote the dataProvider incorrectly, skpping for now */ public function testGetMaskingKey($mask) { - $this->_frame->addBuffer(static::convert($this->_firstByteFinText)); - $this->_frame->addBuffer(static::convert($this->_secondByteMaskedSPL)); + $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText)); + $this->_frame->addBuffer(Frame::encode($this->_secondByteMaskedSPL)); $this->_frame->addBuffer($mask); $this->assertEquals($mask, $this->_frame->getMaskingKey()); @@ -265,4 +250,42 @@ class FrameTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($msg, $this->_frame->getPayload()); } + + public function testCreate() { + $len = 65525; + $len = 65575; + $pl = $this->generateRandomString($len); + + $frame = Frame::create($pl, true, Frame::OP_PING); + + $this->assertTrue($frame->isFinal()); + $this->assertEquals(Frame::OP_PING, $frame->getOpcode()); + $this->assertFalse($frame->isMasked()); + $this->assertEquals($len, $frame->getPayloadLength()); + $this->assertEquals($pl, $frame->getPayload()); + } + + protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { + $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง + + $useChars = array(); + for($i = 0; $i < $length; $i++) { + $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)]; + } + + if($addSpaces === true) { + array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); + } + + if($addNumbers === true) { + array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9)); + } + + shuffle($useChars); + + $randomString = trim(implode('', $useChars)); + $randomString = substr($randomString, 0, $length); + + return $randomString; + } } \ No newline at end of file