Merge branch 'refs/heads/ws-refactor'
This commit is contained in:
commit
ed83f67a84
54
HttpRequestParser.php
Normal file
54
HttpRequestParser.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\WebSocket;
|
||||||
|
use Ratchet\MessageInterface;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory;
|
||||||
|
use Ratchet\WebSocket\Version\VersionInterface;
|
||||||
|
use Guzzle\Http\Message\RequestInterface;
|
||||||
|
|
||||||
|
class HttpRequestParser implements MessageInterface {
|
||||||
|
const EOM = "\r\n\r\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum number of bytes the request can be
|
||||||
|
* This is a security measure to prevent attacks
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $maxSize = 4096;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Ratchet\ConnectionInterface
|
||||||
|
* @param string Data stream to buffer
|
||||||
|
* @return Guzzle\Http\Message\RequestInterface|null
|
||||||
|
* @throws OverflowException
|
||||||
|
*/
|
||||||
|
public function onMessage(ConnectionInterface $context, $data) {
|
||||||
|
if (!isset($context->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);
|
||||||
|
}
|
||||||
|
}
|
28
Version/DataInterface.php
Normal file
28
Version/DataInterface.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\WebSocket\Version;
|
||||||
|
|
||||||
|
interface DataInterface {
|
||||||
|
/**
|
||||||
|
* Determine if the message is complete or still fragmented
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function isCoalesced();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of bytes the payload is set to be
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function getPayloadLength();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the payload (message) sent from peer
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getPayload();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get raw contents of the message
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getContents();
|
||||||
|
}
|
@ -1,35 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\WebSocket\Version;
|
namespace Ratchet\WebSocket\Version;
|
||||||
|
|
||||||
interface FrameInterface {
|
interface FrameInterface extends DataInterface {
|
||||||
/**
|
|
||||||
* Dunno if I'll use this
|
|
||||||
* Thinking could be used if a control frame?
|
|
||||||
*/
|
|
||||||
// function __invoke();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
function isCoalesced();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Add incoming data to the frame from peer
|
||||||
* @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
|
* Is this the final frame in a fragmented message?
|
||||||
*/
|
|
||||||
// function isFragment();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
function isFinal();
|
function isFinal();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Was the payload masked?
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
function isMasked();
|
function isMasked();
|
||||||
@ -42,21 +28,11 @@ interface FrameInterface {
|
|||||||
/**
|
/**
|
||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
function getPayloadLength();
|
//function getReceivedPayloadLength();
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
// function getReceivedPayloadLength();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 32-big string
|
* 32-big string
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
function getMaskingKey();
|
function getMaskingKey();
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string
|
|
||||||
*/
|
|
||||||
function getPayload();
|
|
||||||
}
|
}
|
@ -1,7 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\WebSocket\Version;
|
namespace Ratchet\WebSocket\Version;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use Ratchet\MessageInterface;
|
||||||
|
use Ratchet\WebSocket\Version\Hixie76\Connection;
|
||||||
use Guzzle\Http\Message\RequestInterface;
|
use Guzzle\Http\Message\RequestInterface;
|
||||||
use Guzzle\Http\Message\Response;
|
use Guzzle\Http\Message\Response;
|
||||||
|
use Ratchet\WebSocket\Version\Hixie76\Frame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
|
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
|
||||||
@ -12,17 +16,23 @@ use Guzzle\Http\Message\Response;
|
|||||||
* man-in-the-middle attack on 10%-15% of the people who saw their ad who had a browser (currently only Safari) supporting the Hixie76 protocol.
|
* man-in-the-middle attack on 10%-15% of the people who saw their ad 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
|
* 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)
|
|
||||||
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||||
*/
|
*/
|
||||||
class Hixie76 implements VersionInterface {
|
class Hixie76 implements VersionInterface {
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public static function isProtocol(RequestInterface $request) {
|
public function isProtocol(RequestInterface $request) {
|
||||||
return !(null === $request->getHeader('Sec-WebSocket-Key2', true));
|
return !(null === $request->getHeader('Sec-WebSocket-Key2', true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getVersionNumber() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Guzzle\Http\Message\RequestInterface
|
* @param Guzzle\Http\Message\RequestInterface
|
||||||
* @return Guzzle\Http\Message\Response
|
* @return Guzzle\Http\Message\Response
|
||||||
@ -37,31 +47,53 @@ class Hixie76 implements VersionInterface {
|
|||||||
, 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath()
|
, 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath()
|
||||||
);
|
);
|
||||||
|
|
||||||
$response = new Response('101', $headers, $body);
|
$response = new Response(101, $headers, $body);
|
||||||
$response->setStatus('101', 'WebSocket Protocol Handshake');
|
$response->setStatus(101, 'WebSocket Protocol Handshake');
|
||||||
|
|
||||||
return $response;
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Hixie76\Message
|
|
||||||
*/
|
|
||||||
public function newMessage() {
|
|
||||||
return new Hixie76\Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Hixie76\Frame
|
|
||||||
*/
|
|
||||||
public function newFrame() {
|
|
||||||
return new Hixie76\Frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function frame($message, $mask = true) {
|
public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) {
|
||||||
return chr(0) . $message . chr(255);
|
$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) {
|
public function generateKeyNumber($key) {
|
||||||
|
16
Version/Hixie76/Connection.php
Normal file
16
Version/Hixie76/Connection.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\WebSocket\Version\Hixie76;
|
||||||
|
use Ratchet\AbstractConnectionDecorator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
class Connection extends AbstractConnectionDecorator {
|
||||||
|
public function send($msg) {
|
||||||
|
return $this->getConnection()->send(chr(0) . $msg . chr(255));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close() {
|
||||||
|
return $this->getConnection()->close();
|
||||||
|
}
|
||||||
|
}
|
@ -75,4 +75,12 @@ class Frame implements FrameInterface {
|
|||||||
|
|
||||||
return substr($this->_data, 1, strlen($this->_data) - 2);
|
return substr($this->_data, 1, strlen($this->_data) - 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getContents() {
|
||||||
|
return $this->_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function extractOverflow() {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Ratchet\WebSocket\Version\Hixie76;
|
|
||||||
use Ratchet\WebSocket\Version\MessageInterface;
|
|
||||||
use Ratchet\WebSocket\Version\FrameInterface;
|
|
||||||
|
|
||||||
class Message implements MessageInterface {
|
|
||||||
/**
|
|
||||||
* @var Ratchet\WebSocket\Version\FrameInterface
|
|
||||||
*/
|
|
||||||
protected $_frame = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __toString() {
|
|
||||||
return $this->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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,11 +3,15 @@ namespace Ratchet\WebSocket\Version;
|
|||||||
use Guzzle\Http\Message\RequestInterface;
|
use Guzzle\Http\Message\RequestInterface;
|
||||||
|
|
||||||
class HyBi10 extends RFC6455 {
|
class HyBi10 extends RFC6455 {
|
||||||
public static function isProtocol(RequestInterface $request) {
|
public function isProtocol(RequestInterface $request) {
|
||||||
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
|
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
|
||||||
return ($version >= 6 && $version < 13);
|
return ($version >= 6 && $version < 13);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getVersionNumber() {
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return HyBi10\Message
|
* @return HyBi10\Message
|
||||||
* /
|
* /
|
||||||
|
@ -1,22 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\WebSocket\Version;
|
namespace Ratchet\WebSocket\Version;
|
||||||
|
|
||||||
/**
|
interface MessageInterface extends DataInterface {
|
||||||
* @todo Consider making parent interface/composite for Message/Frame with (isCoalesced, getOpcdoe, getPayloadLength, getPayload)
|
|
||||||
*/
|
|
||||||
interface MessageInterface {
|
|
||||||
/**
|
|
||||||
* @alias getPayload
|
|
||||||
*/
|
|
||||||
function __toString();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
function isCoalesced();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param FragmentInterface
|
* @param FragmentInterface
|
||||||
|
* @return MessageInterface
|
||||||
*/
|
*/
|
||||||
function addFrame(FrameInterface $fragment);
|
function addFrame(FrameInterface $fragment);
|
||||||
|
|
||||||
@ -24,14 +12,4 @@ interface MessageInterface {
|
|||||||
* @return int
|
* @return int
|
||||||
*/
|
*/
|
||||||
function getOpcode();
|
function getOpcode();
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
function getPayloadLength();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function getPayload();
|
|
||||||
}
|
}
|
@ -1,12 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\WebSocket\Version;
|
namespace Ratchet\WebSocket\Version;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use Ratchet\MessageInterface;
|
||||||
use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier;
|
use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier;
|
||||||
|
use Ratchet\WebSocket\Version\RFC6455\Message;
|
||||||
|
use Ratchet\WebSocket\Version\RFC6455\Frame;
|
||||||
|
use Ratchet\WebSocket\Version\RFC6455\Connection;
|
||||||
use Guzzle\Http\Message\RequestInterface;
|
use Guzzle\Http\Message\RequestInterface;
|
||||||
use Guzzle\Http\Message\Response;
|
use Guzzle\Http\Message\Response;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @link http://tools.ietf.org/html/rfc6455
|
* @link http://tools.ietf.org/html/rfc6455
|
||||||
|
* @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE');
|
||||||
*/
|
*/
|
||||||
class RFC6455 implements VersionInterface {
|
class RFC6455 implements VersionInterface {
|
||||||
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||||
@ -23,124 +28,153 @@ class RFC6455 implements VersionInterface {
|
|||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public static function isProtocol(RequestInterface $request) {
|
public function isProtocol(RequestInterface $request) {
|
||||||
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
|
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
|
||||||
return (13 === $version);
|
|
||||||
|
return ($this->getVersionNumber() === $version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getVersionNumber() {
|
||||||
|
return 13;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@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) {
|
public function handshake(RequestInterface $request) {
|
||||||
if (true !== $this->_verifier->verifyAll($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'
|
'Upgrade' => 'websocket'
|
||||||
, 'Connection' => 'Upgrade'
|
, 'Connection' => 'Upgrade'
|
||||||
, 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key'))
|
, '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
|
* @return RFC6455\Message
|
||||||
*/
|
*/
|
||||||
public function newMessage() {
|
public function newMessage() {
|
||||||
return new RFC6455\Message;
|
return new Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return RFC6455\Frame
|
* @return RFC6455\Frame
|
||||||
*/
|
*/
|
||||||
public function newFrame() {
|
public function newFrame($payload = null, $final = true, $opcode = 1) {
|
||||||
return new RFC6455\Frame;
|
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
|
* @todo This is needed when a client is created - needs re-write as missing parts of protocol
|
||||||
* @param string
|
* @param string
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function frame($message, $mask = true) {
|
public function frame($message, $mask = true) {
|
||||||
$payload = $message;
|
return $this->newFrame($message)->getContents();
|
||||||
$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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -150,6 +184,6 @@ class RFC6455 implements VersionInterface {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public function sign($key) {
|
public function sign($key) {
|
||||||
return base64_encode(sha1($key . static::GUID, 1));
|
return base64_encode(sha1($key . static::GUID, true));
|
||||||
}
|
}
|
||||||
}
|
}
|
27
Version/RFC6455/Connection.php
Normal file
27
Version/RFC6455/Connection.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\WebSocket\Version\RFC6455;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use Ratchet\AbstractConnectionDecorator;
|
||||||
|
use Ratchet\WebSocket\Version\DataInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
class Connection extends AbstractConnectionDecorator {
|
||||||
|
public function send($msg) {
|
||||||
|
if (!($msg instanceof DataInterface)) {
|
||||||
|
$msg = new Frame($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->getConnection()->send($msg->getContents());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function close($code = 1000) {
|
||||||
|
$this->send(new Frame($code, true, Frame::OP_CLOSE));
|
||||||
|
|
||||||
|
$this->getConnection()->close();
|
||||||
|
}
|
||||||
|
}
|
@ -3,17 +3,39 @@ namespace Ratchet\WebSocket\Version\RFC6455;
|
|||||||
use Ratchet\WebSocket\Version\FrameInterface;
|
use Ratchet\WebSocket\Version\FrameInterface;
|
||||||
|
|
||||||
class Frame implements 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
|
* The contents of the frame
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $_data = '';
|
protected $data = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of bytes received from the frame
|
* Number of bytes received from the frame
|
||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
public $_bytes_rec = 0;
|
public $bytesRecvd = 0;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of bytes in the payload (as per framing protocol)
|
* Number of bytes in the payload (as per framing protocol)
|
||||||
@ -21,11 +43,57 @@ class Frame implements FrameInterface {
|
|||||||
*/
|
*/
|
||||||
protected $_pay_len_def = -1;
|
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
|
* @param string A valid UTF-8 string to send over the wire
|
||||||
* @var int
|
* @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}
|
* {@inheritdoc}
|
||||||
@ -38,7 +106,7 @@ class Frame implements FrameInterface {
|
|||||||
return false;
|
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) {
|
public function addBuffer($buf) {
|
||||||
$buf = (string)$buf;
|
$buf = (string)$buf;
|
||||||
|
|
||||||
$this->_data .= $buf;
|
$this->data .= $buf;
|
||||||
$this->_bytes_rec += strlen($buf);
|
$this->bytesRecvd += strlen($buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function isFinal() {
|
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');
|
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];
|
return (boolean)(int)$fbb[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,22 +136,124 @@ class Frame implements FrameInterface {
|
|||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function isMasked() {
|
public function isMasked() {
|
||||||
if ($this->_bytes_rec < 2) {
|
if ($this->bytesRecvd < 2) {
|
||||||
throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set");
|
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}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function getOpcode() {
|
public function getOpcode() {
|
||||||
if ($this->_bytes_rec < 1) {
|
if ($this->bytesRecvd < 1) {
|
||||||
throw new \UnderflowException('Not enough bytes received to determine opcode');
|
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
|
* @throws UnderflowException If the buffer doesn't have enough data to determine this
|
||||||
*/
|
*/
|
||||||
protected function getFirstPayloadVal() {
|
protected function getFirstPayloadVal() {
|
||||||
if ($this->_bytes_rec < 2) {
|
if ($this->bytesRecvd < 2) {
|
||||||
throw new \UnderflowException('Not enough bytes received');
|
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
|
* @throws UnderflowException
|
||||||
*/
|
*/
|
||||||
protected function getNumPayloadBits() {
|
protected function getNumPayloadBits() {
|
||||||
if ($this->_bytes_rec < 2) {
|
if ($this->bytesRecvd < 2) {
|
||||||
throw new \UnderflowException('Not enough bytes received');
|
throw new \UnderflowException('Not enough bytes received');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,41 +323,25 @@ class Frame implements FrameInterface {
|
|||||||
|
|
||||||
if ($length_check <= 125) {
|
if ($length_check <= 125) {
|
||||||
$this->_pay_len_def = $length_check;
|
$this->_pay_len_def = $length_check;
|
||||||
|
|
||||||
return $this->getPayloadLength();
|
return $this->getPayloadLength();
|
||||||
}
|
}
|
||||||
|
|
||||||
$byte_length = $this->getNumPayloadBytes();
|
$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');
|
throw new \UnderflowException('Not enough data buffered to determine payload length');
|
||||||
}
|
}
|
||||||
|
|
||||||
$strings = array();
|
$strings = array();
|
||||||
for ($i = 2; $i < $byte_length + 1; $i++) {
|
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));
|
$this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings));
|
||||||
|
|
||||||
return $this->getPayloadLength();
|
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}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
@ -196,31 +351,49 @@ class Frame implements FrameInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
|
* @todo Consider not checking mask, always returning the payload, masked or not
|
||||||
*/
|
*/
|
||||||
public function getPayload() {
|
public function getPayload() {
|
||||||
if (!$this->isCoalesced()) {
|
if (!$this->isCoalesced()) {
|
||||||
throw new \UnderflowException('Can not return partial message');
|
throw new \UnderflowException('Can not return partial message');
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = '';
|
|
||||||
$length = $this->getPayloadLength();
|
|
||||||
|
|
||||||
if ($this->isMasked()) {
|
if ($this->isMasked()) {
|
||||||
$mask = $this->getMaskingKey();
|
$payload = $this->applyMask($this->getMaskingKey());
|
||||||
$start = $this->getPayloadStartingByte();
|
|
||||||
|
|
||||||
for ($i = 0; $i < $length; $i++) {
|
|
||||||
$payload .= $this->_data[$i + $start] ^ $mask[$i % 4];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
$payload = substr($this->_data, $start, $this->getPayloadLength());
|
$payload = substr($this->data, $this->getPayloadStartingByte(), $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;
|
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 '';
|
||||||
|
}
|
||||||
}
|
}
|
@ -32,10 +32,9 @@ class HandshakeVerifier {
|
|||||||
* Test the HTTP method. MUST be "GET"
|
* Test the HTTP method. MUST be "GET"
|
||||||
* @param string
|
* @param string
|
||||||
* @return bool
|
* @return bool
|
||||||
* @todo Look into STD if "get" is valid (am I supposed to do case conversion?)
|
|
||||||
*/
|
*/
|
||||||
public function verifyMethod($val) {
|
public function verifyMethod($val) {
|
||||||
return ('GET' === $val);
|
return ('get' === strtolower($val));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,7 +49,6 @@ class HandshakeVerifier {
|
|||||||
/**
|
/**
|
||||||
* @param string
|
* @param string
|
||||||
* @return bool
|
* @return bool
|
||||||
* @todo Verify the logic here is correct
|
|
||||||
*/
|
*/
|
||||||
public function verifyRequestURI($val) {
|
public function verifyRequestURI($val) {
|
||||||
if ($val[0] != '/') {
|
if ($val[0] != '/') {
|
||||||
@ -61,7 +59,7 @@ class HandshakeVerifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return mb_check_encoding($val, 'ASCII');
|
return mb_check_encoding($val, 'US-ASCII');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,7 +78,7 @@ class HandshakeVerifier {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function verifyUpgradeRequest($val) {
|
public function verifyUpgradeRequest($val) {
|
||||||
return ('websocket' === $val);
|
return ('websocket' === strtolower($val));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,12 +87,15 @@ class HandshakeVerifier {
|
|||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function verifyConnection($val) {
|
public function verifyConnection($val) {
|
||||||
if ('Upgrade' === $val) {
|
$val = strtolower($val);
|
||||||
|
|
||||||
|
if ('upgrade' === $val) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$vals = explode(',', str_replace(', ', ',', $val));
|
$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
|
* @param string|null
|
||||||
* @return bool
|
* @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 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) {
|
public function verifyKey($val) {
|
||||||
return (16 === mb_strlen(base64_decode((string)$val), '8bit'));
|
return (16 === strlen(base64_decode((string)$val)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,13 +13,6 @@ class Message implements MessageInterface {
|
|||||||
$this->_frames = new \SplDoublyLinkedList;
|
$this->_frames = new \SplDoublyLinkedList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function __toString() {
|
|
||||||
return $this->getPayload();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
@ -35,11 +28,12 @@ class Message implements MessageInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@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
|
* @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) {
|
public function addFrame(FrameInterface $fragment) {
|
||||||
$this->_frames->push($fragment);
|
$this->_frames->push($fragment);
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,6 +57,7 @@ class Message implements MessageInterface {
|
|||||||
try {
|
try {
|
||||||
$len += $frame->getPayloadLength();
|
$len += $frame->getPayloadLength();
|
||||||
} catch (\UnderflowException $e) {
|
} catch (\UnderflowException $e) {
|
||||||
|
// Not an error, want the current amount buffered
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +69,7 @@ class Message implements MessageInterface {
|
|||||||
*/
|
*/
|
||||||
public function getPayload() {
|
public function getPayload() {
|
||||||
if (!$this->isCoalesced()) {
|
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 = '';
|
$buffer = '';
|
||||||
@ -85,4 +80,21 @@ class Message implements MessageInterface {
|
|||||||
|
|
||||||
return $buffer;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,41 +1,51 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Ratchet\WebSocket\Version;
|
namespace Ratchet\WebSocket\Version;
|
||||||
|
use Ratchet\MessageInterface;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
use Guzzle\Http\Message\RequestInterface;
|
use Guzzle\Http\Message\RequestInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Despite the version iterations of WebInterface the actions they go through are similar
|
* A standard interface for interacting with the various version of the WebSocket protocol
|
||||||
* This standardizes how the server handles communication with each protocol version
|
|
||||||
* @todo Need better naming conventions...newMessage and newFrame are for reading incoming framed messages (action is unframing)
|
|
||||||
* The current method names suggest you could create a new message/frame to send, which they can not do
|
|
||||||
*/
|
*/
|
||||||
interface VersionInterface {
|
interface VersionInterface extends MessageInterface {
|
||||||
/**
|
/**
|
||||||
* Given an HTTP header, determine if this version should handle the protocol
|
* Given an HTTP header, determine if this version should handle the protocol
|
||||||
* @param Guzzle\Http\Message\RequestInterface
|
* @param Guzzle\Http\Message\RequestInterface
|
||||||
* @return bool
|
* @return bool
|
||||||
* @throws UnderflowException If the protocol thinks the headers are still fragmented
|
* @throws UnderflowException If the protocol thinks the headers are still fragmented
|
||||||
*/
|
*/
|
||||||
static function isProtocol(RequestInterface $request);
|
function isProtocol(RequestInterface $request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Although the version has a name associated with it the integer returned is the proper identification
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
function getVersionNumber();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the handshake and return the response headers
|
* Perform the handshake and return the response headers
|
||||||
* @param Guzzle\Http\Message\RequestInterface
|
* @param Guzzle\Http\Message\RequestInterface
|
||||||
* @return array|string
|
* @return Guzzle\Http\Message\Response
|
||||||
* @throws InvalidArgumentException If the HTTP handshake is mal-formed
|
|
||||||
* @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version)
|
* @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version)
|
||||||
* @todo Change param to accept a Guzzle RequestInterface object
|
|
||||||
*/
|
*/
|
||||||
function handshake(RequestInterface $request);
|
function handshake(RequestInterface $request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Ratchet\ConnectionInterface
|
||||||
|
* @param Ratchet\MessageInterface
|
||||||
|
* @return Ratchet\ConnectionInterface
|
||||||
|
*/
|
||||||
|
function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return MessageInterface
|
* @return MessageInterface
|
||||||
*/
|
*/
|
||||||
function newMessage();
|
//function newMessage();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return FrameInterface
|
* @return FrameInterface
|
||||||
*/
|
*/
|
||||||
function newFrame();
|
//function newFrame();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string
|
* @param string
|
||||||
@ -43,5 +53,5 @@ interface VersionInterface {
|
|||||||
* @return string
|
* @return string
|
||||||
* @todo Change to use other classes, this will be removed eventually
|
* @todo Change to use other classes, this will be removed eventually
|
||||||
*/
|
*/
|
||||||
function frame($message, $mask = true);
|
//function frame($message, $mask = true);
|
||||||
}
|
}
|
77
VersionManager.php
Normal file
77
VersionManager.php
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
namespace Ratchet\WebSocket;
|
||||||
|
use Ratchet\WebSocket\Version\VersionInterface;
|
||||||
|
use Guzzle\Http\Message\RequestInterface;
|
||||||
|
|
||||||
|
class VersionManager {
|
||||||
|
private $versionString = '';
|
||||||
|
|
||||||
|
protected $versions = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the protocol negotiator for the request, if supported
|
||||||
|
* @param Guzzle\Http\Message\RequestInterface
|
||||||
|
* @return Ratchet\WebSocket\Version\VersionInterface
|
||||||
|
*/
|
||||||
|
public function getVersion(RequestInterface $request) {
|
||||||
|
foreach ($this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,33 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Ratchet\WebSocket;
|
|
||||||
use Ratchet\AbstractConnectionDecorator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
* @property stdClass $WebSocket
|
|
||||||
*/
|
|
||||||
class WsConnection extends AbstractConnectionDecorator {
|
|
||||||
public function send($data) {
|
|
||||||
// need frame caching
|
|
||||||
|
|
||||||
$data = $this->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() {
|
|
||||||
}
|
|
||||||
}
|
|
205
WsServer.php
205
WsServer.php
@ -2,17 +2,30 @@
|
|||||||
namespace Ratchet\WebSocket;
|
namespace Ratchet\WebSocket;
|
||||||
use Ratchet\MessageComponentInterface;
|
use Ratchet\MessageComponentInterface;
|
||||||
use Ratchet\ConnectionInterface;
|
use Ratchet\ConnectionInterface;
|
||||||
use Guzzle\Http\Message\RequestInterface;
|
use Ratchet\WebSocket\Version;
|
||||||
use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory;
|
use Guzzle\Http\Message\Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The adapter to handle WebSocket requests/responses
|
* 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
|
* 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://ca.php.net/manual/en/ref.http.php
|
||||||
* @link http://dev.w3.org/html5/websockets/
|
* @link http://dev.w3.org/html5/websockets/
|
||||||
*/
|
*/
|
||||||
class WsServer implements MessageComponentInterface {
|
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
|
* Decorated component
|
||||||
* @var Ratchet\MessageComponentInterface|WsServerInterface
|
* @var Ratchet\MessageComponentInterface|WsServerInterface
|
||||||
@ -24,18 +37,6 @@ class WsServer implements MessageComponentInterface {
|
|||||||
*/
|
*/
|
||||||
protected $connections;
|
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
|
* For now, array_push accepted subprotocols to this array
|
||||||
* @deprecated
|
* @deprecated
|
||||||
@ -53,6 +54,17 @@ class WsServer implements MessageComponentInterface {
|
|||||||
* @param Ratchet\MessageComponentInterface Your application to run with WebSockets
|
* @param Ratchet\MessageComponentInterface Your application to run with WebSockets
|
||||||
*/
|
*/
|
||||||
public function __construct(MessageComponentInterface $component) {
|
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->_decorating = $component;
|
||||||
$this->connections = new \SplObjectStorage;
|
$this->connections = new \SplObjectStorage;
|
||||||
}
|
}
|
||||||
@ -61,86 +73,68 @@ class WsServer implements MessageComponentInterface {
|
|||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function onOpen(ConnectionInterface $conn) {
|
public function onOpen(ConnectionInterface $conn) {
|
||||||
$conn->WebSocket = new \stdClass;
|
$conn->WebSocket = new \StdClass;
|
||||||
$conn->WebSocket->handshake = false;
|
$conn->WebSocket->established = false;
|
||||||
$conn->WebSocket->headers = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do handshake, frame/unframe messages coming/going in stack
|
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function onMessage(ConnectionInterface $from, $msg) {
|
public function onMessage(ConnectionInterface $from, $msg) {
|
||||||
if (true !== $from->WebSocket->handshake) {
|
if (true !== $from->WebSocket->established) {
|
||||||
if (!isset($from->WebSocket->version)) {
|
try {
|
||||||
$from->WebSocket->headers .= $msg;
|
if (null === ($request = $this->reqParser->onMessage($from, $msg))) {
|
||||||
if (!$this->isMessageComplete($from->WebSocket->headers)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
} catch (\OverflowException $oe) {
|
||||||
$headers = RequestFactory::getInstance()->fromMessage($from->WebSocket->headers);
|
return $this->close($from, 413);
|
||||||
$from->WebSocket->version = $this->getVersion($headers);
|
|
||||||
$from->WebSocket->headers = $headers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = $from->WebSocket->version->handshake($from->WebSocket->headers);
|
if (!$this->versioner->isVersionEnabled($request)) {
|
||||||
$from->WebSocket->handshake = true;
|
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('Sec-WebSocket-Protocol', $agreedSubProtocols);
|
||||||
}
|
}
|
||||||
|
|
||||||
$response->setHeader('X-Powered-By', \Ratchet\VERSION);
|
$from->send((string)$response);
|
||||||
$header = (string)$response;
|
|
||||||
|
|
||||||
$from->send($header);
|
if (101 != $response->getStatusCode()) {
|
||||||
|
return $from->close();
|
||||||
$conn = new WsConnection($from);
|
|
||||||
$this->connections->attach($from, $conn);
|
|
||||||
|
|
||||||
return $this->_decorating->onOpen($conn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($from->WebSocket->message)) {
|
$upgraded = $from->WebSocket->version->upgradeConnection($from, $this->_decorating);
|
||||||
$from->WebSocket->message = $from->WebSocket->version->newMessage();
|
|
||||||
|
$this->connections->attach($from, $upgraded);
|
||||||
|
|
||||||
|
$upgraded->WebSocket->established = true;
|
||||||
|
|
||||||
|
return $this->_decorating->onOpen($upgraded);
|
||||||
}
|
}
|
||||||
|
|
||||||
// There is a frame fragment attatched to the connection, add to it
|
$from->WebSocket->version->onMessage($this->connections[$from], $msg);
|
||||||
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!');
|
|
||||||
}
|
|
||||||
// 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()) {
|
|
||||||
$this->_decorating->onMessage($this->connections[$from], (string)$from->WebSocket->message);
|
|
||||||
unset($from->WebSocket->message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function onClose(ConnectionInterface $conn) {
|
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)) {
|
if ($this->connections->contains($conn)) {
|
||||||
$decor = $this->connections[$conn];
|
$decor = $this->connections[$conn];
|
||||||
$this->connections->detach($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);
|
$this->_decorating->onClose($decor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,62 +143,13 @@ class WsServer implements MessageComponentInterface {
|
|||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
public function onError(ConnectionInterface $conn, \Exception $e) {
|
public function onError(ConnectionInterface $conn, \Exception $e) {
|
||||||
if ($this->connections->contains($conn)) {
|
if ($conn->WebSocket->established) {
|
||||||
$this->_decorating->onError($this->connections[$conn], $e);
|
$this->_decorating->onError($this->connections[$conn], $e);
|
||||||
} else {
|
} else {
|
||||||
$conn->close();
|
$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
|
* @param string
|
||||||
* @return boolean
|
* @return boolean
|
||||||
@ -242,25 +187,17 @@ class WsServer implements MessageComponentInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable a version of the WebSocket protocol *cough*Hixie76*cough*
|
* Close a connection with an HTTP response
|
||||||
* @param string The name of the version to disable
|
* @param Ratchet\ConnectionInterface
|
||||||
* @throws InvalidArgumentException If the given version does not exist
|
* @param int HTTP status code
|
||||||
*/
|
*/
|
||||||
public function disableVersion($name) {
|
protected function close(ConnectionInterface $conn, $code = 400) {
|
||||||
if (!array_key_exists($name, $this->_versions)) {
|
$response = new Response($code, array(
|
||||||
throw new \InvalidArgumentException("Version {$name} not found");
|
'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString()
|
||||||
}
|
, 'X-Powered-By' => \Ratchet\VERSION
|
||||||
|
));
|
||||||
|
|
||||||
unset($this->_versions[$name]);
|
$conn->send((string)$response);
|
||||||
}
|
$conn->close();
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,7 +5,7 @@ interface WsServerInterface {
|
|||||||
/**
|
/**
|
||||||
* If any component in a stack supports a WebSocket sub-protocol return each supported in an array
|
* If any component in a stack supports a WebSocket sub-protocol return each supported in an array
|
||||||
* @return 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();
|
function getSubProtocols();
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user