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
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
|
||||
interface FrameInterface {
|
||||
/**
|
||||
* Dunno if I'll use this
|
||||
* Thinking could be used if a control frame?
|
||||
*/
|
||||
// function __invoke();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
function isCoalesced();
|
||||
|
||||
interface FrameInterface extends DataInterface {
|
||||
/**
|
||||
* Add incoming data to the frame from peer
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
// function isFragment();
|
||||
|
||||
/**
|
||||
* Is this the final frame in a fragmented message?
|
||||
* @return bool
|
||||
*/
|
||||
function isFinal();
|
||||
|
||||
/**
|
||||
* Was the payload masked?
|
||||
* @return bool
|
||||
*/
|
||||
function isMasked();
|
||||
@ -39,11 +25,6 @@ interface FrameInterface {
|
||||
*/
|
||||
function getOpcode();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
function getPayloadLength();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
@ -54,9 +35,4 @@ interface FrameInterface {
|
||||
* @return string
|
||||
*/
|
||||
function getMaskingKey();
|
||||
|
||||
/**
|
||||
* @param string
|
||||
*/
|
||||
function getPayload();
|
||||
}
|
@ -1,7 +1,11 @@
|
||||
<?php
|
||||
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\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!
|
||||
@ -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.
|
||||
* 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
|
||||
* 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
|
||||
*/
|
||||
class Hixie76 implements VersionInterface {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isProtocol(RequestInterface $request) {
|
||||
public function isProtocol(RequestInterface $request) {
|
||||
return !(null === $request->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) {
|
||||
|
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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
|
||||
* /
|
||||
|
@ -1,22 +1,10 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
|
||||
/**
|
||||
* @todo Consider making parent interface/composite for Message/Frame with (isCoalesced, getOpcdoe, getPayloadLength, getPayload)
|
||||
*/
|
||||
interface MessageInterface {
|
||||
/**
|
||||
* @alias getPayload
|
||||
*/
|
||||
function __toString();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
function isCoalesced();
|
||||
|
||||
interface MessageInterface extends DataInterface {
|
||||
/**
|
||||
* @param FragmentInterface
|
||||
* @return MessageInterface
|
||||
*/
|
||||
function addFrame(FrameInterface $fragment);
|
||||
|
||||
@ -24,14 +12,4 @@ interface MessageInterface {
|
||||
* @return int
|
||||
*/
|
||||
function getOpcode();
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
function getPayloadLength();
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
function getPayload();
|
||||
}
|
@ -1,12 +1,17 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Ratchet\MessageInterface;
|
||||
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\Response;
|
||||
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
||||
@ -23,124 +28,153 @@ class RFC6455 implements VersionInterface {
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isProtocol(RequestInterface $request) {
|
||||
public function isProtocol(RequestInterface $request) {
|
||||
$version = (int)$request->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));
|
||||
}
|
||||
}
|
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;
|
||||
|
||||
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 '';
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,41 +1,51 @@
|
||||
<?php
|
||||
namespace Ratchet\WebSocket\Version;
|
||||
use Ratchet\MessageInterface;
|
||||
use Ratchet\ConnectionInterface;
|
||||
use Guzzle\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* Despite the version iterations of WebInterface the actions they go through are similar
|
||||
* 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
|
||||
* A standard interface for interacting with the various version of the WebSocket protocol
|
||||
*/
|
||||
interface VersionInterface {
|
||||
interface VersionInterface extends MessageInterface {
|
||||
/**
|
||||
* Given an HTTP header, determine if this version should handle the protocol
|
||||
* @param Guzzle\Http\Message\RequestInterface
|
||||
* @return bool
|
||||
* @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
|
||||
* @param Guzzle\Http\Message\RequestInterface
|
||||
* @return array|string
|
||||
* @throws InvalidArgumentException If the HTTP handshake is mal-formed
|
||||
* @return Guzzle\Http\Message\Response
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* @param Ratchet\ConnectionInterface
|
||||
* @param Ratchet\MessageInterface
|
||||
* @return Ratchet\ConnectionInterface
|
||||
*/
|
||||
function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback);
|
||||
|
||||
/**
|
||||
* @return MessageInterface
|
||||
*/
|
||||
function newMessage();
|
||||
//function newMessage();
|
||||
|
||||
/**
|
||||
* @return FrameInterface
|
||||
*/
|
||||
function newFrame();
|
||||
//function newFrame();
|
||||
|
||||
/**
|
||||
* @param string
|
||||
@ -43,5 +53,5 @@ interface VersionInterface {
|
||||
* @return string
|
||||
* @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;
|
||||
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 (101 != $response->getStatusCode()) {
|
||||
return $from->close();
|
||||
}
|
||||
|
||||
if (!isset($from->WebSocket->message)) {
|
||||
$from->WebSocket->message = $from->WebSocket->version->newMessage();
|
||||
$upgraded = $from->WebSocket->version->upgradeConnection($from, $this->_decorating);
|
||||
|
||||
$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
|
||||
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);
|
||||
}
|
||||
$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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user