diff --git a/HttpRequestParser.php b/HttpRequestParser.php new file mode 100644 index 0000000..f8159df --- /dev/null +++ b/HttpRequestParser.php @@ -0,0 +1,54 @@ +httpBuffer)) { + $context->httpBuffer = ''; + } + + $context->httpBuffer .= $data; + + if (strlen($context->httpBuffer) > (int)$this->maxSize) { + throw new \OverflowException("Maximum buffer size of {$this->maxSize} exceeded parsing HTTP header"); + } + + if ($this->isEom($context->httpBuffer)) { + $request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); + + unset($context->httpBuffer); + + return $request; + } + } + + /** + * Determine if the message has been buffered as per the HTTP specification + * @param string + * @return boolean + */ + public function isEom($message) { + //return (static::EOM === substr($message, 0 - strlen(static::EOM))); + return (boolean)strpos($message, static::EOM); + } +} \ No newline at end of file diff --git a/Version/DataInterface.php b/Version/DataInterface.php new file mode 100644 index 0000000..ef44565 --- /dev/null +++ b/Version/DataInterface.php @@ -0,0 +1,28 @@ +getHeader('Sec-WebSocket-Key2', true)); } + /** + * {@inheritdoc} + */ + public function getVersionNumber() { + return 0; + } + /** * @param Guzzle\Http\Message\RequestInterface * @return Guzzle\Http\Message\Response @@ -37,31 +47,53 @@ class Hixie76 implements VersionInterface { , 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath() ); - $response = new Response('101', $headers, $body); - $response->setStatus('101', 'WebSocket Protocol Handshake'); + $response = new Response(101, $headers, $body); + $response->setStatus(101, 'WebSocket Protocol Handshake'); return $response; } - /** - * @return Hixie76\Message - */ - public function newMessage() { - return new Hixie76\Message; - } - - /** - * @return Hixie76\Frame - */ - public function newFrame() { - return new Hixie76\Frame; - } - /** * {@inheritdoc} */ - public function frame($message, $mask = true) { - return chr(0) . $message . chr(255); + public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { + $upgraded = new Connection($conn); + + if (!isset($upgraded->WebSocket)) { + $upgraded->WebSocket = new \StdClass; + } + + $upgraded->WebSocket->coalescedCallback = $coalescedCallback; + + return $upgraded; + } + + public function onMessage(ConnectionInterface $from, $data) { + $overflow = ''; + + if (!isset($from->WebSocket->frame)) { + $from->WebSocket->frame = $this->newFrame(); + } + + $from->WebSocket->frame->addBuffer($data); + if ($from->WebSocket->frame->isCoalesced()) { + $overflow = $from->WebSocket->frame->extractOverflow(); + + $parsed = $from->WebSocket->frame->getPayload(); + unset($from->WebSocket->frame); + + $from->WebSocket->coalescedCallback->onMessage($from, $parsed); + + unset($from->WebSocket->frame); + } + + if (strlen($overflow) > 0) { + $this->onMessage($from, $overflow); + } + } + + public function newFrame() { + return new Frame; } public function generateKeyNumber($key) { diff --git a/Version/Hixie76/Connection.php b/Version/Hixie76/Connection.php new file mode 100644 index 0000000..2011ac1 --- /dev/null +++ b/Version/Hixie76/Connection.php @@ -0,0 +1,16 @@ +getConnection()->send(chr(0) . $msg . chr(255)); + } + + public function close() { + return $this->getConnection()->close(); + } +} \ No newline at end of file diff --git a/Version/Hixie76/Frame.php b/Version/Hixie76/Frame.php index b9af87d..a172207 100644 --- a/Version/Hixie76/Frame.php +++ b/Version/Hixie76/Frame.php @@ -75,4 +75,12 @@ class Frame implements FrameInterface { return substr($this->_data, 1, strlen($this->_data) - 2); } + + public function getContents() { + return $this->_data; + } + + public function extractOverflow() { + return ''; + } } \ No newline at end of file diff --git a/Version/Hixie76/Message.php b/Version/Hixie76/Message.php deleted file mode 100644 index f783e8b..0000000 --- a/Version/Hixie76/Message.php +++ /dev/null @@ -1,66 +0,0 @@ -getPayload(); - } - - /** - * {@inheritdoc} - */ - public function isCoalesced() { - if (!($this->_frame instanceof FrameInterface)) { - return false; - } - - return $this->_frame->isCoalesced(); - } - - /** - * {@inheritdoc} - */ - public function addFrame(FrameInterface $fragment) { - if (null !== $this->_frame) { - throw new \OverflowException('Hixie76 does not support multiple framing of messages'); - } - - $this->_frame = $fragment; - } - - /** - * {@inheritdoc} - */ - public function getOpcode() { - // Hixie76 only supported text messages - return 1; - } - - /** - * {@inheritdoc} - */ - public function getPayloadLength() { - throw new \DomainException('Please sir, may I have some code? (' . __FUNCTION__ . ')'); - } - - /** - * {@inheritdoc} - */ - public function getPayload() { - if (!$this->isCoalesced()) { - throw new \UnderflowException('Message has not been fully buffered yet'); - } - - return $this->_frame->getPayload(); - } -} \ No newline at end of file diff --git a/Version/HyBi10.php b/Version/HyBi10.php index 734d7e4..bbf3dc4 100644 --- a/Version/HyBi10.php +++ b/Version/HyBi10.php @@ -3,11 +3,15 @@ namespace Ratchet\WebSocket\Version; use Guzzle\Http\Message\RequestInterface; class HyBi10 extends RFC6455 { - public static function isProtocol(RequestInterface $request) { + public function isProtocol(RequestInterface $request) { $version = (int)$request->getHeader('Sec-WebSocket-Version', -1); return ($version >= 6 && $version < 13); } + public function getVersionNumber() { + return 6; + } + /** * @return HyBi10\Message * / diff --git a/Version/MessageInterface.php b/Version/MessageInterface.php index 90d0179..dc126d6 100644 --- a/Version/MessageInterface.php +++ b/Version/MessageInterface.php @@ -1,22 +1,10 @@ getHeader('Sec-WebSocket-Version', -1); - return (13 === $version); + + return ($this->getVersionNumber() === $version); + } + + /** + * {@inheritdoc} + */ + public function getVersionNumber() { + return 13; } /** * {@inheritdoc} - * @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where? */ public function handshake(RequestInterface $request) { if (true !== $this->_verifier->verifyAll($request)) { - throw new \InvalidArgumentException('Invalid HTTP header'); + return new Response(400); } - $headers = array( + return new Response(101, array( 'Upgrade' => 'websocket' , 'Connection' => 'Upgrade' , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) - ); + )); + } - return new Response('101', $headers); + /** + * @param Ratchet\ConnectionInterface + * @return Ratchet\WebSocket\Version\RFC6455\Connection + */ + public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) { + $upgraded = new Connection($conn); + + if (!isset($upgraded->WebSocket)) { + $upgraded->WebSocket = new \StdClass; + } + + $upgraded->WebSocket->coalescedCallback = $coalescedCallback; + + return $upgraded; + } + + /** + * @param Ratchet\WebSocket\Version\RFC6455\Connection + * @param string + */ + public function onMessage(ConnectionInterface $from, $data) { + $overflow = ''; + + if (!isset($from->WebSocket->message)) { + $from->WebSocket->message = $this->newMessage(); + } + + // There is a frame fragment attatched to the connection, add to it + if (!isset($from->WebSocket->frame)) { + $from->WebSocket->frame = $this->newFrame(); + } + + $from->WebSocket->frame->addBuffer($data); + if ($from->WebSocket->frame->isCoalesced()) { + $frame = $from->WebSocket->frame; + + if (!$frame->isMasked()) { + unset($from->WebSocket->frame); + + $from->send($this->newFrame($frame::CLOSE_PROTOCOL, true, $frame::OP_CLOSE)); + $from->getConnection()->close(); + + return; + } + + $opcode = $frame->getOpcode(); + + if ($opcode > 2) { + switch ($opcode) { + case $frame::OP_CLOSE: + $from->close($frame->getPayload()); +/* + $from->send($frame->unMaskPayload()); + $from->getConnection()->close(); +*/ +// $from->send(Frame::create(Frame::CLOSE_NORMAL, true, Frame::OP_CLOSE)); + + return; + break; + case $frame::OP_PING: + $from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG)); + break; + case $frame::OP_PONG: + break; + default: + return $from->close($frame::CLOSE_PROTOCOL); + break; + } + + $overflow = $from->WebSocket->frame->extractOverflow(); + + unset($from->WebSocket->frame, $frame, $opcode); + + if (strlen($overflow) > 0) { + $this->onMessage($from, $overflow); + } + + return; + } + + $overflow = $from->WebSocket->frame->extractOverflow(); + + $from->WebSocket->message->addFrame($from->WebSocket->frame); + unset($from->WebSocket->frame); + } + + if ($from->WebSocket->message->isCoalesced()) { + $parsed = $from->WebSocket->message->getPayload(); + unset($from->WebSocket->message); + + $from->WebSocket->coalescedCallback->onMessage($from, $parsed); + } + + if (strlen($overflow) > 0) { + $this->onMessage($from, $overflow); + } } /** * @return RFC6455\Message */ public function newMessage() { - return new RFC6455\Message; + return new Message; } /** * @return RFC6455\Frame */ - public function newFrame() { - return new RFC6455\Frame; + public function newFrame($payload = null, $final = true, $opcode = 1) { + return new Frame($payload, $final, $opcode); } /** - * Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame - * @link https://github.com/lemmingzshadow/php-websocket - * @todo look into what happens when false is returned here * @todo This is needed when a client is created - needs re-write as missing parts of protocol * @param string * @return string */ public function frame($message, $mask = true) { - $payload = $message; - $type = 'text'; - $masked = $mask; - - $frameHead = array(); - $frame = ''; - $payloadLength = strlen($payload); - - switch($type) { - case 'text': - // first byte indicates FIN, Text-Frame (10000001): - $frameHead[0] = 129; - break; - - case 'close': - // first byte indicates FIN, Close Frame(10001000): - $frameHead[0] = 136; - break; - - case 'ping': - // first byte indicates FIN, Ping frame (10001001): - $frameHead[0] = 137; - break; - - case 'pong': - // first byte indicates FIN, Pong frame (10001010): - $frameHead[0] = 138; - break; - } - - // set mask and payload length (using 1, 3 or 9 bytes) - if($payloadLength > 65535) { - $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 255 : 127; - for($i = 0; $i < 8; $i++) { - $frameHead[$i+2] = bindec($payloadLengthBin[$i]); - } - // most significant bit MUST be 0 (return false if to much data) - if($frameHead[2] > 127) { - return false; - } - } elseif($payloadLength > 125) { - $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); - $frameHead[1] = ($masked === true) ? 254 : 126; - $frameHead[2] = bindec($payloadLengthBin[0]); - $frameHead[3] = bindec($payloadLengthBin[1]); - } else { - $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; - } - - // convert frame-head to string: - foreach(array_keys($frameHead) as $i) { - $frameHead[$i] = chr($frameHead[$i]); - } if($masked === true) { - // generate a random mask: - $mask = array(); - for($i = 0; $i < 4; $i++) - { - $mask[$i] = chr(rand(0, 255)); - } - - $frameHead = array_merge($frameHead, $mask); - } - $frame = implode('', $frameHead); - - // append payload to frame: - $framePayload = array(); - for($i = 0; $i < $payloadLength; $i++) { - $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; - } - - return $frame; + return $this->newFrame($message)->getContents(); } /** @@ -150,6 +184,6 @@ class RFC6455 implements VersionInterface { * @internal */ public function sign($key) { - return base64_encode(sha1($key . static::GUID, 1)); + return base64_encode(sha1($key . static::GUID, true)); } } \ No newline at end of file diff --git a/Version/RFC6455/Connection.php b/Version/RFC6455/Connection.php new file mode 100644 index 0000000..88b0707 --- /dev/null +++ b/Version/RFC6455/Connection.php @@ -0,0 +1,27 @@ +getConnection()->send($msg->getContents()); + } + + /** + * {@inheritdoc} + */ + public function close($code = 1000) { + $this->send(new Frame($code, true, Frame::OP_CLOSE)); + + $this->getConnection()->close(); + } +} \ No newline at end of file diff --git a/Version/RFC6455/Frame.php b/Version/RFC6455/Frame.php index 60c8a2f..12fde14 100644 --- a/Version/RFC6455/Frame.php +++ b/Version/RFC6455/Frame.php @@ -3,17 +3,39 @@ 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; + + const CLOSE_NORMAL = 1000; + const CLOSE_GOING_AWAY = 1001; + const CLOSE_PROTOCOL = 1002; + const CLOSE_BAD_DATA = 1003; + const CLOSE_NO_STATUS = 1005; + const CLOSE_ABNORMAL = 1006; + const CLOSE_BAD_PAYLOAD = 1007; + const CLOSE_POLICY = 1008; + const CLOSE_TOO_BIG = 1009; + const CLOSE_MAND_EXT = 1010; + const CLOSE_SRV_ERR = 1011; + const CLOSE_TLS = 1015; + + const MASK_LENGTH = 4; + /** * The contents of the frame * @var string */ - protected $_data = ''; + protected $data = ''; /** * Number of bytes received from the frame * @var int */ - public $_bytes_rec = 0; + public $bytesRecvd = 0; /** * Number of bytes in the payload (as per framing protocol) @@ -21,11 +43,57 @@ class Frame implements FrameInterface { */ protected $_pay_len_def = -1; + public function __construct($payload = null, $final = true, $opcode = 1) { + if (null === $payload) { + return; + } + + $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); + } + + $this->addBuffer(static::encode($raw) . $payload); + } + /** - * 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 LengthException If the payload is too big */ - protected $_pay_check = -1; + public static function create($payload, $final = true, $opcode = 1) { + return new static($payload, $final, $opcode); + } + + /** + * 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)); + } /** * {@inheritdoc} @@ -38,7 +106,7 @@ class Frame implements FrameInterface { return false; } - return $payload_length + $payload_start === $this->_bytes_rec; + return $this->bytesRecvd >= $payload_length + $payload_start; } /** @@ -47,19 +115,20 @@ class Frame implements FrameInterface { public function addBuffer($buf) { $buf = (string)$buf; - $this->_data .= $buf; - $this->_bytes_rec += strlen($buf); + $this->data .= $buf; + $this->bytesRecvd += strlen($buf); } /** * {@inheritdoc} */ public function isFinal() { - if ($this->_bytes_rec < 1) { + if ($this->bytesRecvd < 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])); + $fbb = sprintf('%08b', ord(substr($this->data, 0, 1))); + return (boolean)(int)$fbb[0]; } @@ -67,22 +136,124 @@ class Frame implements FrameInterface { * {@inheritdoc} */ public function isMasked() { - if ($this->_bytes_rec < 2) { - throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); + if ($this->bytesRecvd < 2) { + throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set"); } - return (boolean)bindec(substr(sprintf('%08b', ord($this->_data[1])), 0, 1)); + return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->data, 1, 1))), 0, 1)); + } + + /** + * {@inheritdoc} + */ + public function getMaskingKey() { + if (!$this->isMasked()) { + return ''; + } + + $start = 1 + $this->getNumPayloadBytes(); + + if ($this->bytesRecvd < $start + static::MASK_LENGTH) { + throw new \UnderflowException('Not enough data buffered to calculate the masking key'); + } + + return substr($this->data, $start, static::MASK_LENGTH); + } + + /** + * @return string + */ + public function generateMaskingKey() { + $mask = ''; + + for ($i = 1; $i <= static::MASK_LENGTH; $i++) { + $mask .= sprintf("%c", rand(32, 126)); + } + + return $mask; + } + + /** + * Apply a mask to the payload + * @param string|null + * @throws InvalidArgumentException If there is an issue with the given masking key + * @throws UnderflowException If the frame is not coalesced + */ + public function maskPayload($maskingKey = null) { + if (null === $maskingKey) { + $maskingKey = $this->generateMaskingKey(); + } + + if (static::MASK_LENGTH !== strlen($maskingKey)) { + throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters"); + } + + if (!mb_check_encoding($maskingKey, 'US-ASCII')) { + throw new \InvalidArgumentException("Masking key MUST be ASCII"); + } + + $this->unMaskPayload(); + + $byte = sprintf('%08b', ord(substr($this->data, 1, 1))); + + $this->data = substr_replace($this->data, static::encode(substr_replace($byte, '1', 0, 1)), 1, 1); + $this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0); + + $this->bytesRecvd += static::MASK_LENGTH; + $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); + + return $this; + } + + /** + * Remove a mask from the payload + * @throws UnderFlowException If the frame is not coalesced + * @return Frame + */ + public function unMaskPayload() { + if (!$this->isMasked()) { + return $this; + } + + $maskingKey = $this->getMaskingKey(); + + $byte = sprintf('%08b', ord(substr($this->data, 1, 1))); + + $this->data = substr_replace($this->data, static::encode(substr_replace($byte, '0', 0, 1)), 1, 1); + $this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH); + + $this->bytesRecvd -= static::MASK_LENGTH; + $this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength()); + + return $this; + } + + protected function applyMask($maskingKey, $payload = null) { + if (null === $payload) { + if (!$this->isCoalesced()) { + throw new \UnderflowException('Frame must be coalesced to apply a mask'); + } + + $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); + } + + $applied = ''; + for ($i = 0, $len = strlen($payload); $i < $len; $i++) { + $applied .= substr($payload, $i, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1); + } + + return $applied; } /** * {@inheritdoc} */ public function getOpcode() { - if ($this->_bytes_rec < 1) { + if ($this->bytesRecvd < 1) { throw new \UnderflowException('Not enough bytes received to determine opcode'); } - return bindec(substr(sprintf('%08b', ord($this->_data[0])), 4, 4)); + return bindec(substr(sprintf('%08b', ord(substr($this->data, 0, 1))), 4, 4)); } /** @@ -91,11 +262,11 @@ class Frame implements FrameInterface { * @throws UnderflowException If the buffer doesn't have enough data to determine this */ protected function getFirstPayloadVal() { - if ($this->_bytes_rec < 2) { + if ($this->bytesRecvd < 2) { throw new \UnderflowException('Not enough bytes received'); } - return ord($this->_data[1]) & 127; + return ord(substr($this->data, 1, 1)) & 127; } /** @@ -103,7 +274,7 @@ class Frame implements FrameInterface { * @throws UnderflowException */ protected function getNumPayloadBits() { - if ($this->_bytes_rec < 2) { + if ($this->bytesRecvd < 2) { throw new \UnderflowException('Not enough bytes received'); } @@ -152,41 +323,25 @@ class Frame implements FrameInterface { if ($length_check <= 125) { $this->_pay_len_def = $length_check; + return $this->getPayloadLength(); } $byte_length = $this->getNumPayloadBytes(); - if ($this->_bytes_rec < 1 + $byte_length) { + if ($this->bytesRecvd < 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]); + $strings[] = ord(substr($this->data, $i, 1)); } $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); + return $this->getPayloadLength(); } - /** - * {@inheritdoc} - */ - 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); - } - /** * {@inheritdoc} */ @@ -196,31 +351,49 @@ class Frame implements FrameInterface { /** * {@inheritdoc} + * @todo Consider not checking mask, always returning the payload, masked or not */ 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]; - } + $payload = $this->applyMask($this->getMaskingKey()); } 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'); + $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength()); } return $payload; } + + /** + * Get the raw contents of the frame + * @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow + */ + public function getContents() { + return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength()); + } + + /** + * Sometimes clients will concatinate more than one frame over the wire + * This method will take the extra bytes off the end and return them + * @todo Consider returning new Frame + * @return string + */ + public function extractOverflow() { + if ($this->isCoalesced()) { + $endPoint = $this->getPayloadLength(); + $endPoint += $this->getPayloadStartingByte(); + + if ($this->bytesRecvd > $endPoint) { + $overflow = substr($this->data, $endPoint); + $this->data = substr($this->data, 0, $endPoint); + + return $overflow; + } + } + + return ''; + } } \ No newline at end of file diff --git a/Version/RFC6455/HandshakeVerifier.php b/Version/RFC6455/HandshakeVerifier.php index 6898e2e..93d7928 100644 --- a/Version/RFC6455/HandshakeVerifier.php +++ b/Version/RFC6455/HandshakeVerifier.php @@ -32,10 +32,9 @@ class HandshakeVerifier { * Test the HTTP method. MUST be "GET" * @param string * @return bool - * @todo Look into STD if "get" is valid (am I supposed to do case conversion?) */ public function verifyMethod($val) { - return ('GET' === $val); + return ('get' === strtolower($val)); } /** @@ -50,7 +49,6 @@ class HandshakeVerifier { /** * @param string * @return bool - * @todo Verify the logic here is correct */ public function verifyRequestURI($val) { if ($val[0] != '/') { @@ -61,7 +59,7 @@ class HandshakeVerifier { return false; } - return mb_check_encoding($val, 'ASCII'); + return mb_check_encoding($val, 'US-ASCII'); } /** @@ -80,7 +78,7 @@ class HandshakeVerifier { * @return bool */ public function verifyUpgradeRequest($val) { - return ('websocket' === $val); + return ('websocket' === strtolower($val)); } /** @@ -89,12 +87,15 @@ class HandshakeVerifier { * @return bool */ public function verifyConnection($val) { - if ('Upgrade' === $val) { + $val = strtolower($val); + + if ('upgrade' === $val) { return true; } $vals = explode(',', str_replace(', ', ',', $val)); - return (false !== array_search('Upgrade', $vals)); + + return (false !== array_search('upgrade', $vals)); } /** @@ -102,9 +103,10 @@ class HandshakeVerifier { * @param string|null * @return bool * @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? + * @todo Check the spec to see what the encoding of the key could be */ public function verifyKey($val) { - return (16 === mb_strlen(base64_decode((string)$val), '8bit')); + return (16 === strlen(base64_decode((string)$val))); } /** diff --git a/Version/RFC6455/Message.php b/Version/RFC6455/Message.php index 5385475..14b3c9b 100644 --- a/Version/RFC6455/Message.php +++ b/Version/RFC6455/Message.php @@ -13,13 +13,6 @@ class Message implements MessageInterface { $this->_frames = new \SplDoublyLinkedList; } - /** - * {@inheritdoc} - */ - public function __toString() { - return $this->getPayload(); - } - /** * {@inheritdoc} */ @@ -35,11 +28,12 @@ class Message implements MessageInterface { /** * {@inheritdoc} - * @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); + + return $this; } /** @@ -63,6 +57,7 @@ class Message implements MessageInterface { try { $len += $frame->getPayloadLength(); } catch (\UnderflowException $e) { + // Not an error, want the current amount buffered } } @@ -74,7 +69,7 @@ class Message implements MessageInterface { */ public function getPayload() { if (!$this->isCoalesced()) { - throw new \UnderflowMessage('Message has not been put back together yet'); + throw new \UnderflowException('Message has not been put back together yet'); } $buffer = ''; @@ -85,4 +80,21 @@ class Message implements MessageInterface { return $buffer; } + + /** + * {@inheritdoc} + */ + public function getContents() { + if (!$this->isCoalesced()) { + throw new \UnderflowException("Message has not been put back together yet"); + } + + $buffer = ''; + + foreach ($this->_frames as $frame) { + $buffer .= $frame->getContents(); + } + + return $buffer; + } } \ No newline at end of file diff --git a/Version/VersionInterface.php b/Version/VersionInterface.php index 1602641..9a3c29e 100644 --- a/Version/VersionInterface.php +++ b/Version/VersionInterface.php @@ -1,41 +1,51 @@ versions as $version) { + if ($version->isProtocol($request)) { + return $version; + } + } + + throw new \InvalidArgumentException("Version not found"); + } + + /** + * @param Guzzle\Http\Message\RequestInterface + * @return bool + */ + public function isVersionEnabled(RequestInterface $request) { + foreach ($this->versions as $version) { + if ($version->isProtocol($request)) { + return true; + } + } + + return false; + } + + /** + * Enable support for a specific version of the WebSocket protocol + * @param Ratchet\WebSocket\Vesion\VersionInterface + * @return HandshakeNegotiator + */ + public function enableVersion(VersionInterface $version) { + $this->versions[$version->getVersionNumber()] = $version; + + if (empty($this->versionString)) { + $this->versionString = (string)$version->getVersionNumber(); + } else { + $this->versionString .= ", {$version->getVersionNumber()}"; + } + + return $this; + } + + /** + * Disable support for a specific WebSocket protocol version + * @param int The version ID to un-support + * @return HandshakeNegotiator + */ + public function disableVersion($versionId) { + unset($this->versions[$versionId]); + + $this->versionString = implode(',', array_keys($this->versions)); + + return $this; + } + + /** + * Get a string of version numbers supported (comma delimited) + * @return string + */ + public function getSupportedVersionString() { + return $this->versionString; + } +} \ No newline at end of file diff --git a/WsConnection.php b/WsConnection.php deleted file mode 100644 index c0f127b..0000000 --- a/WsConnection.php +++ /dev/null @@ -1,33 +0,0 @@ -WebSocket->version->frame($data, false); - - $this->getConnection()->send($data); - } - - public function close() { - // send close frame - - // ??? - - // profit - - $this->getConnection()->close(); // temporary - } - - public function ping() { - } - - public function pong() { - } -} \ No newline at end of file diff --git a/WsServer.php b/WsServer.php index b69119c..2e44a8c 100644 --- a/WsServer.php +++ b/WsServer.php @@ -2,17 +2,30 @@ namespace Ratchet\WebSocket; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; -use Guzzle\Http\Message\RequestInterface; -use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; +use Ratchet\WebSocket\Version; +use Guzzle\Http\Message\Response; /** * The adapter to handle WebSocket requests/responses * This is a mediator between the Server and your application to handle real-time messaging through a web browser - * @todo Separate this class into a two classes: Component and a protocol handler * @link http://ca.php.net/manual/en/ref.http.php * @link http://dev.w3.org/html5/websockets/ */ class WsServer implements MessageComponentInterface { + /** + * Buffers incoming HTTP requests returning a Guzzle Request when coalesced + * @var HttpRequestParser + * @note May not expose this in the future, may do through facade methods + */ + public $reqParser; + + /** + * Manage the various WebSocket versions to support + * @var VersionManager + * @note May not expose this in the future, may do through facade methods + */ + public $versioner; + /** * Decorated component * @var Ratchet\MessageComponentInterface|WsServerInterface @@ -24,18 +37,6 @@ class WsServer implements MessageComponentInterface { */ protected $connections; - /** - * Re-entrant instances of protocol version classes - * @internal - */ - protected $_versions = array( - 'HyBi10' => null - , 'Hixie76' => null - , 'RFC6455' => null - ); - - protected $_mask_payload = false; - /** * For now, array_push accepted subprotocols to this array * @deprecated @@ -53,6 +54,17 @@ class WsServer implements MessageComponentInterface { * @param Ratchet\MessageComponentInterface Your application to run with WebSockets */ public function __construct(MessageComponentInterface $component) { + //mb_internal_encoding('UTF-8'); + + $this->reqParser = new HttpRequestParser; + $this->versioner = new VersionManager; + + $this->versioner + ->enableVersion(new Version\RFC6455($component)) + ->enableVersion(new Version\HyBi10($component)) + ->enableVersion(new Version\Hixie76) + ; + $this->_decorating = $component; $this->connections = new \SplObjectStorage; } @@ -61,86 +73,68 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onOpen(ConnectionInterface $conn) { - $conn->WebSocket = new \stdClass; - $conn->WebSocket->handshake = false; - $conn->WebSocket->headers = ''; + $conn->WebSocket = new \StdClass; + $conn->WebSocket->established = false; } /** - * Do handshake, frame/unframe messages coming/going in stack * {@inheritdoc} */ public function onMessage(ConnectionInterface $from, $msg) { - if (true !== $from->WebSocket->handshake) { - if (!isset($from->WebSocket->version)) { - $from->WebSocket->headers .= $msg; - if (!$this->isMessageComplete($from->WebSocket->headers)) { + if (true !== $from->WebSocket->established) { + try { + if (null === ($request = $this->reqParser->onMessage($from, $msg))) { return; } - - $headers = RequestFactory::getInstance()->fromMessage($from->WebSocket->headers); - $from->WebSocket->version = $this->getVersion($headers); - $from->WebSocket->headers = $headers; + } catch (\OverflowException $oe) { + return $this->close($from, 413); } - $response = $from->WebSocket->version->handshake($from->WebSocket->headers); - $from->WebSocket->handshake = true; + if (!$this->versioner->isVersionEnabled($request)) { + return $this->close($from); + } - if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { + $from->WebSocket->request = $request; + $from->WebSocket->version = $this->versioner->getVersion($request); + + $response = $from->WebSocket->version->handshake($request); + $response->setHeader('X-Powered-By', \Ratchet\VERSION); + + // This needs to be refactored later on, incorporated with routing + if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); } - $response->setHeader('X-Powered-By', \Ratchet\VERSION); - $header = (string)$response; + $from->send((string)$response); - $from->send($header); - - $conn = new WsConnection($from); - $this->connections->attach($from, $conn); - - return $this->_decorating->onOpen($conn); - } - - if (!isset($from->WebSocket->message)) { - $from->WebSocket->message = $from->WebSocket->version->newMessage(); - } - - // 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) { - $from->close(); - throw new \UnexpectedValueException('Control frame support coming soon!'); + if (101 != $response->getStatusCode()) { + return $from->close(); } - // 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); + $upgraded = $from->WebSocket->version->upgradeConnection($from, $this->_decorating); + + $this->connections->attach($from, $upgraded); + + $upgraded->WebSocket->established = true; + + return $this->_decorating->onOpen($upgraded); } - if ($from->WebSocket->message->isCoalesced()) { - $this->_decorating->onMessage($this->connections[$from], (string)$from->WebSocket->message); - unset($from->WebSocket->message); - } + $from->WebSocket->version->onMessage($this->connections[$from], $msg); } /** * {@inheritdoc} */ public function onClose(ConnectionInterface $conn) { - // WS::onOpen is not called when the socket connects, it's call when the handshake is done - // The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet if ($this->connections->contains($conn)) { $decor = $this->connections[$conn]; $this->connections->detach($conn); + } + // WS::onOpen is not called when the socket connects, it's call when the handshake is done + // The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet + if (isset($decor)) { $this->_decorating->onClose($decor); } } @@ -149,62 +143,13 @@ class WsServer implements MessageComponentInterface { * {@inheritdoc} */ public function onError(ConnectionInterface $conn, \Exception $e) { - if ($this->connections->contains($conn)) { + if ($conn->WebSocket->established) { $this->_decorating->onError($this->connections[$conn], $e); } else { $conn->close(); } } - /** - * Detect the WebSocket protocol version a client is using based on the HTTP header request - * @param string HTTP handshake request - * @return Version\VersionInterface - * @throws UnderFlowException If we think the entire header message hasn't been buffered yet - * @throws InvalidArgumentException If we can't understand protocol version request - * @todo Verify the first line of the HTTP header as per page 16 of RFC 6455 - */ - protected function getVersion(RequestInterface $request) { - foreach ($this->_versions as $name => $instance) { - if (null !== $instance) { - if ($instance::isProtocol($request)) { - return $instance; - } - } else { - $ns = __NAMESPACE__ . "\\Version\\{$name}"; - if ($ns::isProtocol($request)) { - $this->_versions[$name] = new $ns; - return $this->_versions[$name]; - } - } - } - - throw new \InvalidArgumentException('Could not identify WebSocket protocol'); - } - - /** - * @param string - * @return bool - * @todo Abstract, some hard coding done for (stupid) Hixie protocol - */ - protected function isMessageComplete($message) { - static $crlf = "\r\n\r\n"; - - $headers = (boolean)strstr($message, $crlf); - if (!$headers) { - - return false; - } - - if (strstr($message, 'Sec-WebSocket-Key2')) { - if (8 !== strlen(substr($message, strpos($message, $crlf) + strlen($crlf)))) { - return false; - } - } - - return true; - } - /** * @param string * @return boolean @@ -242,25 +187,17 @@ class WsServer implements MessageComponentInterface { } /** - * Disable a version of the WebSocket protocol *cough*Hixie76*cough* - * @param string The name of the version to disable - * @throws InvalidArgumentException If the given version does not exist + * Close a connection with an HTTP response + * @param Ratchet\ConnectionInterface + * @param int HTTP status code */ - public function disableVersion($name) { - if (!array_key_exists($name, $this->_versions)) { - throw new \InvalidArgumentException("Version {$name} not found"); - } + protected function close(ConnectionInterface $conn, $code = 400) { + $response = new Response($code, array( + 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString() + , 'X-Powered-By' => \Ratchet\VERSION + )); - unset($this->_versions[$name]); - } - - /** - * Set the option to mask the payload upon sending to client - * If WebSocket is used as server, this should be false, client to true - * @param bool - * @todo User shouldn't have to know/set this, need to figure out how to do this automatically - */ - public function setMaskPayload($opt) { - $this->_mask_payload = (boolean)$opt; + $conn->send((string)$response); + $conn->close(); } } \ No newline at end of file diff --git a/WsServerInterface.php b/WsServerInterface.php index 8cb378d..91a83cd 100644 --- a/WsServerInterface.php +++ b/WsServerInterface.php @@ -5,7 +5,7 @@ interface WsServerInterface { /** * If any component in a stack supports a WebSocket sub-protocol return each supported in an array * @return array - * @temporary This method may be removed in future version (note tha twill not break code, just make some code obsolete) + * @temporary This method may be removed in future version (note that will not break code, just make some code obsolete) */ function getSubProtocols(); } \ No newline at end of file