Merge branch 'refs/heads/ws-refactor'

This commit is contained in:
Chris Boden 2012-06-15 10:07:23 -04:00
commit ed83f67a84
19 changed files with 746 additions and 477 deletions

54
HttpRequestParser.php Normal file
View 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
View 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();
}

View File

@ -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();
}

View File

@ -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) {

View 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();
}
}

View File

@ -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 '';
}
}

View File

@ -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();
}
}

View File

@ -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
* /

View File

@ -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();
}

View File

@ -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));
}
}

View 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();
}
}

View File

@ -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 '';
}
}

View File

@ -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)));
}
/**

View File

@ -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;
}
}

View File

@ -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
View 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;
}
}

View File

@ -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() {
}
}

View File

@ -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();
}
}