Merge branch 'refs/heads/ws-refactor'

This commit is contained in:
Chris Boden 2012-06-15 10:07:23 -04:00
commit 5c521af229
44 changed files with 1196 additions and 597 deletions

View File

@ -1,20 +1,21 @@
[![Build Status](https://secure.travis-ci.org/cboden/Ratchet.png?branch=master)](http://travis-ci.org/cboden/Ratchet)
#Ratchet #Ratchet
A PHP 5.3 (PSR-0) library for serving WebSockets and building socket based applications. [![Build Status](https://secure.travis-ci.org/cboden/Ratchet.png?branch=master)](http://travis-ci.org/cboden/Ratchet)
A PHP 5.3 library for serving WebSockets and building socket based applications.
Build up your application through simple interfaces and re-use your application without changing any of its code just by combining different components. Build up your application through simple interfaces and re-use your application without changing any of its code just by combining different components.
##WebSocket Compliance ##WebSocket Compliance
* Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time) * Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time)
* Tested on Chrome 18 - 16, Firefox 6 - 12, Safari 5, iOS 4.2, iOS 5 * Tested on Chrome 13 - 19, Firefox 6 - 12, Safari 5.0.1+, iOS 4.2, iOS 5
##Requirements ##Requirements
Shell access is required and a dedicated machine with root access is recommended. Shell access is required and a dedicated machine with root access is recommended.
To avoid proxy/firewall blockage it's recommended WebSockets are run on port 80, which requires root access. To avoid proxy/firewall blockage it's recommended WebSockets are run on port 80, which requires root access.
Note that you can not run two applications (Apache and Ratchet) on the same port, thus the requirement for a separate machine (for now). Note that you can not run two applications (Apache and Ratchet) on the same port, thus the requirement for a separate machine (for now).
PHP 5.3.2 (or higher) is required with mbstring enabled (*--enable-mbstring* flag during compile time). PHP5.4 is recommended for its performance improvements.
Cookies from your domain will be passed to the socket server, allowing you to identify users. Cookies from your domain will be passed to the socket server, allowing you to identify users.
Accessing your website's session data in Ratchet requires you to use [Symfony2 Sessions](http://symfony.com/doc/master/components/http_foundation/sessions.html) on your website. Accessing your website's session data in Ratchet requires you to use [Symfony2 Sessions](http://symfony.com/doc/master/components/http_foundation/sessions.html) on your website.

View File

@ -21,6 +21,7 @@
} }
, "require": { , "require": {
"php": ">=5.3.2" "php": ">=5.3.2"
, "ext-mbstring": "*"
, "guzzle/guzzle": "2.5.*" , "guzzle/guzzle": "2.5.*"
, "symfony/http-foundation": "2.1.*" , "symfony/http-foundation": "2.1.*"
, "react/socket": "dev-master" , "react/socket": "dev-master"

34
composer.lock generated
View File

@ -1,10 +1,10 @@
{ {
"hash": "cbea4e3e4d74a22ba34d4edf2ce44df3", "hash": "253370657f067dacf104d5fae531f20a",
"packages": [ "packages": [
{ {
"package": "evenement/evenement", "package": "evenement/evenement",
"version": "dev-master", "version": "dev-master",
"source-reference": "808e3aaea8d4f908e455b0e047cc1acc46b38d44" "source-reference": "fa966683e7df3e5dd5929d984a44abfbd6bafe8d"
}, },
{ {
"package": "guzzle/guzzle", "package": "guzzle/guzzle",
@ -13,48 +13,26 @@
{ {
"package": "react/event-loop", "package": "react/event-loop",
"version": "dev-master", "version": "dev-master",
"source-reference": "cc341b109feae06fa33dff7486aa567e3b9d1406" "source-reference": "0927a2129394f10cc8534994271c6073ca9e350c"
}, },
{ {
"package": "react/socket", "package": "react/socket",
"version": "dev-master", "version": "dev-master",
"source-reference": "6801c6d8653e1999cb34b235cdb4b3a287e4d528" "source-reference": "b78d96a2cde9a78ab2f923e9aa9a40f778d051df"
}, },
{ {
"package": "symfony/event-dispatcher", "package": "symfony/event-dispatcher",
"version": "dev-master", "version": "dev-master",
"source-reference": "eb82542e8ec9506096caf7c528564c740a214f56", "source-reference": "30d3f5da80c2aeab15bcdb5a7d448d15bc294b23"
"alias-pretty-version": "2.1.x-dev",
"alias-version": "2.1.9999999.9999999-dev"
},
{
"package": "symfony/event-dispatcher",
"version": "dev-master",
"source-reference": "eb82542e8ec9506096caf7c528564c740a214f56"
},
{
"package": "symfony/event-dispatcher",
"version": "dev-master",
"source-reference": "0b58a4019befc0bd038bc0ec0165101d5dd31754",
"alias-pretty-version": "2.1.x-dev",
"alias-version": "2.1.9999999.9999999-dev"
}, },
{ {
"package": "symfony/http-foundation", "package": "symfony/http-foundation",
"version": "dev-master", "version": "dev-master",
"source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0", "source-reference": "d9ef2afd0218415a8c04ea48a2c83bb5b8f0f51c"
"alias-pretty-version": "2.1.x-dev",
"alias-version": "2.1.9999999.9999999-dev"
}, },
{ {
"package": "symfony/http-foundation", "package": "symfony/http-foundation",
"version": "dev-master", "version": "dev-master",
"source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0"
},
{
"package": "symfony/http-foundation",
"version": "dev-master",
"source-reference": "cf8e8324c68ce584525502702866485f17f1c8a5",
"alias-pretty-version": "2.1.x-dev", "alias-pretty-version": "2.1.x-dev",
"alias-version": "2.1.9999999.9999999-dev" "alias-version": "2.1.9999999.9999999-dev"
} }

View File

@ -4,6 +4,7 @@ namespace Ratchet;
/** /**
* Wraps ConnectionInterface objects via the decorator pattern but allows * Wraps ConnectionInterface objects via the decorator pattern but allows
* parameters to bubble through with magic methods * parameters to bubble through with magic methods
* @todo It sure would be nice if I could make most of this a trait...
*/ */
abstract class AbstractConnectionDecorator implements ConnectionInterface { abstract class AbstractConnectionDecorator implements ConnectionInterface {
/** /**

View File

@ -1,6 +1,5 @@
<?php <?php
namespace Ratchet; namespace Ratchet;
use Ratchet\ConnectionInterface;
/** /**
* This is the interface to build a Ratchet application with * This is the interface to build a Ratchet application with

View File

@ -1,7 +1,7 @@
<?php <?php
namespace Ratchet; namespace Ratchet;
const VERSION = 'Ratchet/0.1.2'; const VERSION = 'Ratchet/0.2b';
/** /**
* A proxy object representing a connection to the application * A proxy object representing a connection to the application

View File

@ -1,13 +1,5 @@
<?php <?php
namespace Ratchet; namespace Ratchet;
use Ratchet\ConnectionInterface;
interface MessageComponentInterface extends ComponentInterface { interface MessageComponentInterface extends ComponentInterface, MessageInterface {
/**
* Triggered when a client sends data through the socket
* @param Ratchet\ConnectionInterface The socket/connection that sent the message to your application
* @param string The message received
* @throws Exception
*/
function onMessage(ConnectionInterface $from, $msg);
} }

View File

@ -0,0 +1,12 @@
<?php
namespace Ratchet;
interface MessageInterface {
/**
* Triggered when a client sends data through the socket
* @param Ratchet\ConnectionInterface The socket/connection that sent the message to your application
* @param string The message received
* @throws Exception
*/
function onMessage(ConnectionInterface $from, $msg);
}

View File

@ -4,6 +4,7 @@ use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\Socket\ServerInterface; use React\Socket\ServerInterface;
use React\EventLoop\StreamSelectLoop;
use React\EventLoop\Factory as LoopFactory; use React\EventLoop\Factory as LoopFactory;
use React\Socket\Server as Reactor; use React\Socket\Server as Reactor;
@ -48,8 +49,7 @@ class IoServer {
} }
public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') { public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') {
$loop = LoopFactory::create(); $loop = new StreamSelectLoop;
$socket = new Reactor($loop); $socket = new Reactor($loop);
$socket->listen($port, $address); $socket->listen($port, $address);

View File

@ -71,7 +71,7 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
function onOpen(ConnectionInterface $conn) { function onOpen(ConnectionInterface $conn) {
if (null === ($id = $conn->WebSocket->headers->getCookie(ini_get('session.name')))) { if (null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) {
$saveHandler = $this->_null; $saveHandler = $this->_null;
$id = ''; $id = '';
} else { } else {

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

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 <?php
namespace Ratchet\WebSocket\Version; namespace Ratchet\WebSocket\Version;
interface FrameInterface { interface FrameInterface extends DataInterface {
/**
* Dunno if I'll use this
* Thinking could be used if a control frame?
*/
// function __invoke();
/**
* @return bool
*/
function isCoalesced();
/** /**
* Add incoming data to the frame from peer
* @param string * @param string
* @todo Theoretically, there won't be a buffer overflow (end of frame + start of new frame) - but test later, return a string with overflow here
*/ */
function addBuffer($buf); function addBuffer($buf);
/** /**
* @return bool * Is this the final frame in a fragmented message?
*/
// function isFragment();
/**
* @return bool * @return bool
*/ */
function isFinal(); function isFinal();
/** /**
* Was the payload masked?
* @return bool * @return bool
*/ */
function isMasked(); function isMasked();
@ -42,21 +28,11 @@ interface FrameInterface {
/** /**
* @return int * @return int
*/ */
function getPayloadLength(); //function getReceivedPayloadLength();
/**
* @return int
*/
// function getReceivedPayloadLength();
/** /**
* 32-big string * 32-big string
* @return string * @return string
*/ */
function getMaskingKey(); function getMaskingKey();
/**
* @param string
*/
function getPayload();
} }

View File

@ -1,7 +1,11 @@
<?php <?php
namespace Ratchet\WebSocket\Version; namespace Ratchet\WebSocket\Version;
use Ratchet\ConnectionInterface;
use Ratchet\MessageInterface;
use Ratchet\WebSocket\Version\Hixie76\Connection;
use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response; use Guzzle\Http\Message\Response;
use Ratchet\WebSocket\Version\Hixie76\Frame;
/** /**
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application! * FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
@ -12,17 +16,23 @@ use Guzzle\Http\Message\Response;
* man-in-the-middle attack on 10%-15% of the people who saw their ad who had a browser (currently only Safari) supporting the Hixie76 protocol. * man-in-the-middle attack on 10%-15% of the people who saw their ad who had a browser (currently only Safari) supporting the Hixie76 protocol.
* This was exploited by taking advantage of proxy servers in front of the user who ignored some HTTP headers in the handshake * This was exploited by taking advantage of proxy servers in front of the user who ignored some HTTP headers in the handshake
* The Hixie76 is currently implemented by Safari * The Hixie76 is currently implemented by Safari
* Handshake from Andrea Giammarchi (http://webreflection.blogspot.com/2010/06/websocket-handshake-76-simplified.html)
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 * @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
*/ */
class Hixie76 implements VersionInterface { class Hixie76 implements VersionInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function isProtocol(RequestInterface $request) { public function isProtocol(RequestInterface $request) {
return !(null === $request->getHeader('Sec-WebSocket-Key2', true)); return !(null === $request->getHeader('Sec-WebSocket-Key2', true));
} }
/**
* {@inheritdoc}
*/
public function getVersionNumber() {
return 0;
}
/** /**
* @param Guzzle\Http\Message\RequestInterface * @param Guzzle\Http\Message\RequestInterface
* @return Guzzle\Http\Message\Response * @return Guzzle\Http\Message\Response
@ -37,31 +47,53 @@ class Hixie76 implements VersionInterface {
, 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath() , 'Sec-WebSocket-Location' => 'ws://' . $request->getHeader('Host', true) . $request->getPath()
); );
$response = new Response('101', $headers, $body); $response = new Response(101, $headers, $body);
$response->setStatus('101', 'WebSocket Protocol Handshake'); $response->setStatus(101, 'WebSocket Protocol Handshake');
return $response; return $response;
} }
/**
* @return Hixie76\Message
*/
public function newMessage() {
return new Hixie76\Message;
}
/**
* @return Hixie76\Frame
*/
public function newFrame() {
return new Hixie76\Frame;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function frame($message, $mask = true) { public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) {
return chr(0) . $message . chr(255); $upgraded = new Connection($conn);
if (!isset($upgraded->WebSocket)) {
$upgraded->WebSocket = new \StdClass;
}
$upgraded->WebSocket->coalescedCallback = $coalescedCallback;
return $upgraded;
}
public function onMessage(ConnectionInterface $from, $data) {
$overflow = '';
if (!isset($from->WebSocket->frame)) {
$from->WebSocket->frame = $this->newFrame();
}
$from->WebSocket->frame->addBuffer($data);
if ($from->WebSocket->frame->isCoalesced()) {
$overflow = $from->WebSocket->frame->extractOverflow();
$parsed = $from->WebSocket->frame->getPayload();
unset($from->WebSocket->frame);
$from->WebSocket->coalescedCallback->onMessage($from, $parsed);
unset($from->WebSocket->frame);
}
if (strlen($overflow) > 0) {
$this->onMessage($from, $overflow);
}
}
public function newFrame() {
return new Frame;
} }
public function generateKeyNumber($key) { public function generateKeyNumber($key) {

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); 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; use Guzzle\Http\Message\RequestInterface;
class HyBi10 extends RFC6455 { class HyBi10 extends RFC6455 {
public static function isProtocol(RequestInterface $request) { public function isProtocol(RequestInterface $request) {
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1); $version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
return ($version >= 6 && $version < 13); return ($version >= 6 && $version < 13);
} }
public function getVersionNumber() {
return 6;
}
/** /**
* @return HyBi10\Message * @return HyBi10\Message
* / * /

View File

@ -1,22 +1,10 @@
<?php <?php
namespace Ratchet\WebSocket\Version; namespace Ratchet\WebSocket\Version;
/** interface MessageInterface extends DataInterface {
* @todo Consider making parent interface/composite for Message/Frame with (isCoalesced, getOpcdoe, getPayloadLength, getPayload)
*/
interface MessageInterface {
/**
* @alias getPayload
*/
function __toString();
/**
* @return bool
*/
function isCoalesced();
/** /**
* @param FragmentInterface * @param FragmentInterface
* @return MessageInterface
*/ */
function addFrame(FrameInterface $fragment); function addFrame(FrameInterface $fragment);
@ -24,14 +12,4 @@ interface MessageInterface {
* @return int * @return int
*/ */
function getOpcode(); function getOpcode();
/**
* @return int
*/
function getPayloadLength();
/**
* @return string
*/
function getPayload();
} }

View File

@ -1,12 +1,17 @@
<?php <?php
namespace Ratchet\WebSocket\Version; namespace Ratchet\WebSocket\Version;
use Ratchet\ConnectionInterface;
use Ratchet\MessageInterface;
use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier; use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier;
use Ratchet\WebSocket\Version\RFC6455\Message;
use Ratchet\WebSocket\Version\RFC6455\Frame;
use Ratchet\WebSocket\Version\RFC6455\Connection;
use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response; use Guzzle\Http\Message\Response;
/** /**
* @link http://tools.ietf.org/html/rfc6455 * @link http://tools.ietf.org/html/rfc6455
* @todo Unicode: return mb_convert_encoding(pack("N",$u), mb_internal_encoding(), 'UCS-4BE');
*/ */
class RFC6455 implements VersionInterface { class RFC6455 implements VersionInterface {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
@ -23,124 +28,153 @@ class RFC6455 implements VersionInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function isProtocol(RequestInterface $request) { public function isProtocol(RequestInterface $request) {
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1); $version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
return (13 === $version);
return ($this->getVersionNumber() === $version);
}
/**
* {@inheritdoc}
*/
public function getVersionNumber() {
return 13;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
* @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where?
*/ */
public function handshake(RequestInterface $request) { public function handshake(RequestInterface $request) {
if (true !== $this->_verifier->verifyAll($request)) { if (true !== $this->_verifier->verifyAll($request)) {
throw new \InvalidArgumentException('Invalid HTTP header'); return new Response(400);
} }
$headers = array( return new Response(101, array(
'Upgrade' => 'websocket' 'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade' , 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key'))
); ));
}
return new Response('101', $headers); /**
* @param Ratchet\ConnectionInterface
* @return Ratchet\WebSocket\Version\RFC6455\Connection
*/
public function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback) {
$upgraded = new Connection($conn);
if (!isset($upgraded->WebSocket)) {
$upgraded->WebSocket = new \StdClass;
}
$upgraded->WebSocket->coalescedCallback = $coalescedCallback;
return $upgraded;
}
/**
* @param Ratchet\WebSocket\Version\RFC6455\Connection
* @param string
*/
public function onMessage(ConnectionInterface $from, $data) {
$overflow = '';
if (!isset($from->WebSocket->message)) {
$from->WebSocket->message = $this->newMessage();
}
// There is a frame fragment attatched to the connection, add to it
if (!isset($from->WebSocket->frame)) {
$from->WebSocket->frame = $this->newFrame();
}
$from->WebSocket->frame->addBuffer($data);
if ($from->WebSocket->frame->isCoalesced()) {
$frame = $from->WebSocket->frame;
if (!$frame->isMasked()) {
unset($from->WebSocket->frame);
$from->send($this->newFrame($frame::CLOSE_PROTOCOL, true, $frame::OP_CLOSE));
$from->getConnection()->close();
return;
}
$opcode = $frame->getOpcode();
if ($opcode > 2) {
switch ($opcode) {
case $frame::OP_CLOSE:
$from->close($frame->getPayload());
/*
$from->send($frame->unMaskPayload());
$from->getConnection()->close();
*/
// $from->send(Frame::create(Frame::CLOSE_NORMAL, true, Frame::OP_CLOSE));
return;
break;
case $frame::OP_PING:
$from->send($this->newFrame($frame->getPayload(), true, $frame::OP_PONG));
break;
case $frame::OP_PONG:
break;
default:
return $from->close($frame::CLOSE_PROTOCOL);
break;
}
$overflow = $from->WebSocket->frame->extractOverflow();
unset($from->WebSocket->frame, $frame, $opcode);
if (strlen($overflow) > 0) {
$this->onMessage($from, $overflow);
}
return;
}
$overflow = $from->WebSocket->frame->extractOverflow();
$from->WebSocket->message->addFrame($from->WebSocket->frame);
unset($from->WebSocket->frame);
}
if ($from->WebSocket->message->isCoalesced()) {
$parsed = $from->WebSocket->message->getPayload();
unset($from->WebSocket->message);
$from->WebSocket->coalescedCallback->onMessage($from, $parsed);
}
if (strlen($overflow) > 0) {
$this->onMessage($from, $overflow);
}
} }
/** /**
* @return RFC6455\Message * @return RFC6455\Message
*/ */
public function newMessage() { public function newMessage() {
return new RFC6455\Message; return new Message;
} }
/** /**
* @return RFC6455\Frame * @return RFC6455\Frame
*/ */
public function newFrame() { public function newFrame($payload = null, $final = true, $opcode = 1) {
return new RFC6455\Frame; return new Frame($payload, $final, $opcode);
} }
/** /**
* Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame
* @link https://github.com/lemmingzshadow/php-websocket
* @todo look into what happens when false is returned here
* @todo This is needed when a client is created - needs re-write as missing parts of protocol * @todo This is needed when a client is created - needs re-write as missing parts of protocol
* @param string * @param string
* @return string * @return string
*/ */
public function frame($message, $mask = true) { public function frame($message, $mask = true) {
$payload = $message; return $this->newFrame($message)->getContents();
$type = 'text';
$masked = $mask;
$frameHead = array();
$frame = '';
$payloadLength = strlen($payload);
switch($type) {
case 'text':
// first byte indicates FIN, Text-Frame (10000001):
$frameHead[0] = 129;
break;
case 'close':
// first byte indicates FIN, Close Frame(10001000):
$frameHead[0] = 136;
break;
case 'ping':
// first byte indicates FIN, Ping frame (10001001):
$frameHead[0] = 137;
break;
case 'pong':
// first byte indicates FIN, Pong frame (10001010):
$frameHead[0] = 138;
break;
}
// set mask and payload length (using 1, 3 or 9 bytes)
if($payloadLength > 65535) {
$payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
$frameHead[1] = ($masked === true) ? 255 : 127;
for($i = 0; $i < 8; $i++) {
$frameHead[$i+2] = bindec($payloadLengthBin[$i]);
}
// most significant bit MUST be 0 (return false if to much data)
if($frameHead[2] > 127) {
return false;
}
} elseif($payloadLength > 125) {
$payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
$frameHead[1] = ($masked === true) ? 254 : 126;
$frameHead[2] = bindec($payloadLengthBin[0]);
$frameHead[3] = bindec($payloadLengthBin[1]);
} else {
$frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
}
// convert frame-head to string:
foreach(array_keys($frameHead) as $i) {
$frameHead[$i] = chr($frameHead[$i]);
} if($masked === true) {
// generate a random mask:
$mask = array();
for($i = 0; $i < 4; $i++)
{
$mask[$i] = chr(rand(0, 255));
}
$frameHead = array_merge($frameHead, $mask);
}
$frame = implode('', $frameHead);
// append payload to frame:
$framePayload = array();
for($i = 0; $i < $payloadLength; $i++) {
$frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
}
return $frame;
} }
/** /**
@ -150,6 +184,6 @@ class RFC6455 implements VersionInterface {
* @internal * @internal
*/ */
public function sign($key) { public function sign($key) {
return base64_encode(sha1($key . static::GUID, 1)); return base64_encode(sha1($key . static::GUID, true));
} }
} }

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; use Ratchet\WebSocket\Version\FrameInterface;
class Frame implements FrameInterface { class Frame implements FrameInterface {
const OP_CONTINUE = 0;
const OP_TEXT = 1;
const OP_BINARY = 2;
const OP_CLOSE = 8;
const OP_PING = 9;
const OP_PONG = 10;
const CLOSE_NORMAL = 1000;
const CLOSE_GOING_AWAY = 1001;
const CLOSE_PROTOCOL = 1002;
const CLOSE_BAD_DATA = 1003;
const CLOSE_NO_STATUS = 1005;
const CLOSE_ABNORMAL = 1006;
const CLOSE_BAD_PAYLOAD = 1007;
const CLOSE_POLICY = 1008;
const CLOSE_TOO_BIG = 1009;
const CLOSE_MAND_EXT = 1010;
const CLOSE_SRV_ERR = 1011;
const CLOSE_TLS = 1015;
const MASK_LENGTH = 4;
/** /**
* The contents of the frame * The contents of the frame
* @var string * @var string
*/ */
protected $_data = ''; protected $data = '';
/** /**
* Number of bytes received from the frame * Number of bytes received from the frame
* @var int * @var int
*/ */
public $_bytes_rec = 0; public $bytesRecvd = 0;
/** /**
* Number of bytes in the payload (as per framing protocol) * Number of bytes in the payload (as per framing protocol)
@ -21,11 +43,57 @@ class Frame implements FrameInterface {
*/ */
protected $_pay_len_def = -1; protected $_pay_len_def = -1;
public function __construct($payload = null, $final = true, $opcode = 1) {
if (null === $payload) {
return;
}
$raw = (int)(boolean)$final . sprintf('%07b', (int)$opcode);
$plLen = strlen($payload);
if ($plLen <= 125) {
$raw .= sprintf('%08b', $plLen);
} elseif ($plLen <= 65535) {
$raw .= sprintf('%08b', 126) . sprintf('%016b', $plLen);
} else { // todo, make sure msg isn't longer than b1x71
$raw .= sprintf('%08b', 127) . sprintf('%064b', $plLen);
}
$this->addBuffer(static::encode($raw) . $payload);
}
/** /**
* Bit 9-15 * @param string A valid UTF-8 string to send over the wire
* @var int * @param bool Is the final frame in a message
* @param int The opcode of the frame, see constants
* @param bool Mask the payload
* @return Frame
* @throws InvalidArgumentException If the payload is not a valid UTF-8 string
* @throws LengthException If the payload is too big
*/ */
protected $_pay_check = -1; public static function create($payload, $final = true, $opcode = 1) {
return new static($payload, $final, $opcode);
}
/**
* Encode the fake binary string to send over the wire
* @param string of 1's and 0's
* @return string
*/
public static function encode($in) {
if (strlen($in) > 8) {
$out = '';
while (strlen($in) >= 8) {
$out .= static::encode(substr($in, 0, 8));
$in = substr($in, 8);
}
return $out;
}
return chr(bindec($in));
}
/** /**
* {@inheritdoc} * {@inheritdoc}
@ -38,7 +106,7 @@ class Frame implements FrameInterface {
return false; return false;
} }
return $payload_length + $payload_start === $this->_bytes_rec; return $this->bytesRecvd >= $payload_length + $payload_start;
} }
/** /**
@ -47,19 +115,20 @@ class Frame implements FrameInterface {
public function addBuffer($buf) { public function addBuffer($buf) {
$buf = (string)$buf; $buf = (string)$buf;
$this->_data .= $buf; $this->data .= $buf;
$this->_bytes_rec += strlen($buf); $this->bytesRecvd += strlen($buf);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function isFinal() { public function isFinal() {
if ($this->_bytes_rec < 1) { if ($this->bytesRecvd < 1) {
throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message'); throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message');
} }
$fbb = sprintf('%08b', ord($this->_data[0])); $fbb = sprintf('%08b', ord(substr($this->data, 0, 1)));
return (boolean)(int)$fbb[0]; return (boolean)(int)$fbb[0];
} }
@ -67,22 +136,124 @@ class Frame implements FrameInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function isMasked() { public function isMasked() {
if ($this->_bytes_rec < 2) { if ($this->bytesRecvd < 2) {
throw new \UnderflowException("Not enough bytes received ({$this->_bytes_rec}) to determine if mask is set"); throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
} }
return (boolean)bindec(substr(sprintf('%08b', ord($this->_data[1])), 0, 1)); return (boolean)bindec(substr(sprintf('%08b', ord(substr($this->data, 1, 1))), 0, 1));
}
/**
* {@inheritdoc}
*/
public function getMaskingKey() {
if (!$this->isMasked()) {
return '';
}
$start = 1 + $this->getNumPayloadBytes();
if ($this->bytesRecvd < $start + static::MASK_LENGTH) {
throw new \UnderflowException('Not enough data buffered to calculate the masking key');
}
return substr($this->data, $start, static::MASK_LENGTH);
}
/**
* @return string
*/
public function generateMaskingKey() {
$mask = '';
for ($i = 1; $i <= static::MASK_LENGTH; $i++) {
$mask .= sprintf("%c", rand(32, 126));
}
return $mask;
}
/**
* Apply a mask to the payload
* @param string|null
* @throws InvalidArgumentException If there is an issue with the given masking key
* @throws UnderflowException If the frame is not coalesced
*/
public function maskPayload($maskingKey = null) {
if (null === $maskingKey) {
$maskingKey = $this->generateMaskingKey();
}
if (static::MASK_LENGTH !== strlen($maskingKey)) {
throw new \InvalidArgumentException("Masking key must be " . static::MASK_LENGTH ." characters");
}
if (!mb_check_encoding($maskingKey, 'US-ASCII')) {
throw new \InvalidArgumentException("Masking key MUST be ASCII");
}
$this->unMaskPayload();
$byte = sprintf('%08b', ord(substr($this->data, 1, 1)));
$this->data = substr_replace($this->data, static::encode(substr_replace($byte, '1', 0, 1)), 1, 1);
$this->data = substr_replace($this->data, $maskingKey, $this->getNumPayloadBytes() + 1, 0);
$this->bytesRecvd += static::MASK_LENGTH;
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
return $this;
}
/**
* Remove a mask from the payload
* @throws UnderFlowException If the frame is not coalesced
* @return Frame
*/
public function unMaskPayload() {
if (!$this->isMasked()) {
return $this;
}
$maskingKey = $this->getMaskingKey();
$byte = sprintf('%08b', ord(substr($this->data, 1, 1)));
$this->data = substr_replace($this->data, static::encode(substr_replace($byte, '0', 0, 1)), 1, 1);
$this->data = substr_replace($this->data, '', $this->getNumPayloadBytes() + 1, static::MASK_LENGTH);
$this->bytesRecvd -= static::MASK_LENGTH;
$this->data = substr_replace($this->data, $this->applyMask($maskingKey), $this->getPayloadStartingByte(), $this->getPayloadLength());
return $this;
}
protected function applyMask($maskingKey, $payload = null) {
if (null === $payload) {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Frame must be coalesced to apply a mask');
}
$payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
}
$applied = '';
for ($i = 0, $len = strlen($payload); $i < $len; $i++) {
$applied .= substr($payload, $i, 1) ^ substr($maskingKey, $i % static::MASK_LENGTH, 1);
}
return $applied;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getOpcode() { public function getOpcode() {
if ($this->_bytes_rec < 1) { if ($this->bytesRecvd < 1) {
throw new \UnderflowException('Not enough bytes received to determine opcode'); throw new \UnderflowException('Not enough bytes received to determine opcode');
} }
return bindec(substr(sprintf('%08b', ord($this->_data[0])), 4, 4)); return bindec(substr(sprintf('%08b', ord(substr($this->data, 0, 1))), 4, 4));
} }
/** /**
@ -91,11 +262,11 @@ class Frame implements FrameInterface {
* @throws UnderflowException If the buffer doesn't have enough data to determine this * @throws UnderflowException If the buffer doesn't have enough data to determine this
*/ */
protected function getFirstPayloadVal() { protected function getFirstPayloadVal() {
if ($this->_bytes_rec < 2) { if ($this->bytesRecvd < 2) {
throw new \UnderflowException('Not enough bytes received'); throw new \UnderflowException('Not enough bytes received');
} }
return ord($this->_data[1]) & 127; return ord(substr($this->data, 1, 1)) & 127;
} }
/** /**
@ -103,7 +274,7 @@ class Frame implements FrameInterface {
* @throws UnderflowException * @throws UnderflowException
*/ */
protected function getNumPayloadBits() { protected function getNumPayloadBits() {
if ($this->_bytes_rec < 2) { if ($this->bytesRecvd < 2) {
throw new \UnderflowException('Not enough bytes received'); throw new \UnderflowException('Not enough bytes received');
} }
@ -152,41 +323,25 @@ class Frame implements FrameInterface {
if ($length_check <= 125) { if ($length_check <= 125) {
$this->_pay_len_def = $length_check; $this->_pay_len_def = $length_check;
return $this->getPayloadLength(); return $this->getPayloadLength();
} }
$byte_length = $this->getNumPayloadBytes(); $byte_length = $this->getNumPayloadBytes();
if ($this->_bytes_rec < 1 + $byte_length) { if ($this->bytesRecvd < 1 + $byte_length) {
throw new \UnderflowException('Not enough data buffered to determine payload length'); throw new \UnderflowException('Not enough data buffered to determine payload length');
} }
$strings = array(); $strings = array();
for ($i = 2; $i < $byte_length + 1; $i++) { for ($i = 2; $i < $byte_length + 1; $i++) {
$strings[] = ord($this->_data[$i]); $strings[] = ord(substr($this->data, $i, 1));
} }
$this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings)); $this->_pay_len_def = bindec(vsprintf(str_repeat('%08b', $byte_length - 1), $strings));
return $this->getPayloadLength(); return $this->getPayloadLength();
} }
/**
* {@inheritdoc}
*/
public function getMaskingKey() {
if (!$this->isMasked()) {
return '';
}
$length = 4;
$start = 1 + $this->getNumPayloadBytes();
if ($this->_bytes_rec < $start + $length) {
throw new \UnderflowException('Not enough data buffered to calculate the masking key');
}
return substr($this->_data, $start, $length);
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -196,31 +351,49 @@ class Frame implements FrameInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
* @todo Consider not checking mask, always returning the payload, masked or not
*/ */
public function getPayload() { public function getPayload() {
if (!$this->isCoalesced()) { if (!$this->isCoalesced()) {
throw new \UnderflowException('Can not return partial message'); throw new \UnderflowException('Can not return partial message');
} }
$payload = '';
$length = $this->getPayloadLength();
if ($this->isMasked()) { if ($this->isMasked()) {
$mask = $this->getMaskingKey(); $payload = $this->applyMask($this->getMaskingKey());
$start = $this->getPayloadStartingByte();
for ($i = 0; $i < $length; $i++) {
$payload .= $this->_data[$i + $start] ^ $mask[$i % 4];
}
} else { } else {
$payload = substr($this->_data, $start, $this->getPayloadLength()); $payload = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
}
if (strlen($payload) !== $length) {
// Is this possible? isCoalesced() math _should_ ensure if there is mal-formed data, it would return false
throw new \UnexpectedValueException('Payload length does not match expected length');
} }
return $payload; return $payload;
} }
/**
* Get the raw contents of the frame
* @todo This is untested, make sure the substr is right - trying to return the frame w/o the overflow
*/
public function getContents() {
return substr($this->data, 0, $this->getPayloadStartingByte() + $this->getPayloadLength());
}
/**
* Sometimes clients will concatinate more than one frame over the wire
* This method will take the extra bytes off the end and return them
* @todo Consider returning new Frame
* @return string
*/
public function extractOverflow() {
if ($this->isCoalesced()) {
$endPoint = $this->getPayloadLength();
$endPoint += $this->getPayloadStartingByte();
if ($this->bytesRecvd > $endPoint) {
$overflow = substr($this->data, $endPoint);
$this->data = substr($this->data, 0, $endPoint);
return $overflow;
}
}
return '';
}
} }

View File

@ -32,10 +32,9 @@ class HandshakeVerifier {
* Test the HTTP method. MUST be "GET" * Test the HTTP method. MUST be "GET"
* @param string * @param string
* @return bool * @return bool
* @todo Look into STD if "get" is valid (am I supposed to do case conversion?)
*/ */
public function verifyMethod($val) { public function verifyMethod($val) {
return ('GET' === $val); return ('get' === strtolower($val));
} }
/** /**
@ -50,7 +49,6 @@ class HandshakeVerifier {
/** /**
* @param string * @param string
* @return bool * @return bool
* @todo Verify the logic here is correct
*/ */
public function verifyRequestURI($val) { public function verifyRequestURI($val) {
if ($val[0] != '/') { if ($val[0] != '/') {
@ -61,7 +59,7 @@ class HandshakeVerifier {
return false; return false;
} }
return mb_check_encoding($val, 'ASCII'); return mb_check_encoding($val, 'US-ASCII');
} }
/** /**
@ -80,7 +78,7 @@ class HandshakeVerifier {
* @return bool * @return bool
*/ */
public function verifyUpgradeRequest($val) { public function verifyUpgradeRequest($val) {
return ('websocket' === $val); return ('websocket' === strtolower($val));
} }
/** /**
@ -89,12 +87,15 @@ class HandshakeVerifier {
* @return bool * @return bool
*/ */
public function verifyConnection($val) { public function verifyConnection($val) {
if ('Upgrade' === $val) { $val = strtolower($val);
if ('upgrade' === $val) {
return true; return true;
} }
$vals = explode(',', str_replace(', ', ',', $val)); $vals = explode(',', str_replace(', ', ',', $val));
return (false !== array_search('Upgrade', $vals));
return (false !== array_search('upgrade', $vals));
} }
/** /**
@ -102,9 +103,10 @@ class HandshakeVerifier {
* @param string|null * @param string|null
* @return bool * @return bool
* @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode? * @todo The spec says we don't need to base64_decode - can I just check if the length is 24 and not decode?
* @todo Check the spec to see what the encoding of the key could be
*/ */
public function verifyKey($val) { public function verifyKey($val) {
return (16 === mb_strlen(base64_decode((string)$val), '8bit')); return (16 === strlen(base64_decode((string)$val)));
} }
/** /**

View File

@ -13,13 +13,6 @@ class Message implements MessageInterface {
$this->_frames = new \SplDoublyLinkedList; $this->_frames = new \SplDoublyLinkedList;
} }
/**
* {@inheritdoc}
*/
public function __toString() {
return $this->getPayload();
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -35,11 +28,12 @@ class Message implements MessageInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
* @todo Should I allow addFrame if the frame is not coalesced yet? I believe I'm assuming this class will only receive fully formed frame messages
* @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message * @todo Also, I should perhaps check the type...control frames (ping/pong/close) are not to be considered part of a message
*/ */
public function addFrame(FrameInterface $fragment) { public function addFrame(FrameInterface $fragment) {
$this->_frames->push($fragment); $this->_frames->push($fragment);
return $this;
} }
/** /**
@ -63,6 +57,7 @@ class Message implements MessageInterface {
try { try {
$len += $frame->getPayloadLength(); $len += $frame->getPayloadLength();
} catch (\UnderflowException $e) { } catch (\UnderflowException $e) {
// Not an error, want the current amount buffered
} }
} }
@ -74,7 +69,7 @@ class Message implements MessageInterface {
*/ */
public function getPayload() { public function getPayload() {
if (!$this->isCoalesced()) { if (!$this->isCoalesced()) {
throw new \UnderflowMessage('Message has not been put back together yet'); throw new \UnderflowException('Message has not been put back together yet');
} }
$buffer = ''; $buffer = '';
@ -85,4 +80,21 @@ class Message implements MessageInterface {
return $buffer; return $buffer;
} }
/**
* {@inheritdoc}
*/
public function getContents() {
if (!$this->isCoalesced()) {
throw new \UnderflowException("Message has not been put back together yet");
}
$buffer = '';
foreach ($this->_frames as $frame) {
$buffer .= $frame->getContents();
}
return $buffer;
}
} }

View File

@ -1,41 +1,51 @@
<?php <?php
namespace Ratchet\WebSocket\Version; namespace Ratchet\WebSocket\Version;
use Ratchet\MessageInterface;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface; use Guzzle\Http\Message\RequestInterface;
/** /**
* Despite the version iterations of WebInterface the actions they go through are similar * A standard interface for interacting with the various version of the WebSocket protocol
* This standardizes how the server handles communication with each protocol version
* @todo Need better naming conventions...newMessage and newFrame are for reading incoming framed messages (action is unframing)
* The current method names suggest you could create a new message/frame to send, which they can not do
*/ */
interface VersionInterface { interface VersionInterface extends MessageInterface {
/** /**
* Given an HTTP header, determine if this version should handle the protocol * Given an HTTP header, determine if this version should handle the protocol
* @param Guzzle\Http\Message\RequestInterface * @param Guzzle\Http\Message\RequestInterface
* @return bool * @return bool
* @throws UnderflowException If the protocol thinks the headers are still fragmented * @throws UnderflowException If the protocol thinks the headers are still fragmented
*/ */
static function isProtocol(RequestInterface $request); function isProtocol(RequestInterface $request);
/**
* Although the version has a name associated with it the integer returned is the proper identification
* @return int
*/
function getVersionNumber();
/** /**
* Perform the handshake and return the response headers * Perform the handshake and return the response headers
* @param Guzzle\Http\Message\RequestInterface * @param Guzzle\Http\Message\RequestInterface
* @return array|string * @return Guzzle\Http\Message\Response
* @throws InvalidArgumentException If the HTTP handshake is mal-formed
* @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version) * @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version)
* @todo Change param to accept a Guzzle RequestInterface object
*/ */
function handshake(RequestInterface $request); function handshake(RequestInterface $request);
/**
* @param Ratchet\ConnectionInterface
* @param Ratchet\MessageInterface
* @return Ratchet\ConnectionInterface
*/
function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback);
/** /**
* @return MessageInterface * @return MessageInterface
*/ */
function newMessage(); //function newMessage();
/** /**
* @return FrameInterface * @return FrameInterface
*/ */
function newFrame(); //function newFrame();
/** /**
* @param string * @param string
@ -43,5 +53,5 @@ interface VersionInterface {
* @return string * @return string
* @todo Change to use other classes, this will be removed eventually * @todo Change to use other classes, this will be removed eventually
*/ */
function frame($message, $mask = true); //function frame($message, $mask = true);
} }

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; namespace Ratchet\WebSocket;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface; use Ratchet\WebSocket\Version;
use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; use Guzzle\Http\Message\Response;
/** /**
* The adapter to handle WebSocket requests/responses * The adapter to handle WebSocket requests/responses
* This is a mediator between the Server and your application to handle real-time messaging through a web browser * This is a mediator between the Server and your application to handle real-time messaging through a web browser
* @todo Separate this class into a two classes: Component and a protocol handler
* @link http://ca.php.net/manual/en/ref.http.php * @link http://ca.php.net/manual/en/ref.http.php
* @link http://dev.w3.org/html5/websockets/ * @link http://dev.w3.org/html5/websockets/
*/ */
class WsServer implements MessageComponentInterface { class WsServer implements MessageComponentInterface {
/**
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced
* @var HttpRequestParser
* @note May not expose this in the future, may do through facade methods
*/
public $reqParser;
/**
* Manage the various WebSocket versions to support
* @var VersionManager
* @note May not expose this in the future, may do through facade methods
*/
public $versioner;
/** /**
* Decorated component * Decorated component
* @var Ratchet\MessageComponentInterface|WsServerInterface * @var Ratchet\MessageComponentInterface|WsServerInterface
@ -24,18 +37,6 @@ class WsServer implements MessageComponentInterface {
*/ */
protected $connections; protected $connections;
/**
* Re-entrant instances of protocol version classes
* @internal
*/
protected $_versions = array(
'HyBi10' => null
, 'Hixie76' => null
, 'RFC6455' => null
);
protected $_mask_payload = false;
/** /**
* For now, array_push accepted subprotocols to this array * For now, array_push accepted subprotocols to this array
* @deprecated * @deprecated
@ -53,6 +54,17 @@ class WsServer implements MessageComponentInterface {
* @param Ratchet\MessageComponentInterface Your application to run with WebSockets * @param Ratchet\MessageComponentInterface Your application to run with WebSockets
*/ */
public function __construct(MessageComponentInterface $component) { public function __construct(MessageComponentInterface $component) {
//mb_internal_encoding('UTF-8');
$this->reqParser = new HttpRequestParser;
$this->versioner = new VersionManager;
$this->versioner
->enableVersion(new Version\RFC6455($component))
->enableVersion(new Version\HyBi10($component))
->enableVersion(new Version\Hixie76)
;
$this->_decorating = $component; $this->_decorating = $component;
$this->connections = new \SplObjectStorage; $this->connections = new \SplObjectStorage;
} }
@ -61,86 +73,68 @@ class WsServer implements MessageComponentInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onOpen(ConnectionInterface $conn) { public function onOpen(ConnectionInterface $conn) {
$conn->WebSocket = new \stdClass; $conn->WebSocket = new \StdClass;
$conn->WebSocket->handshake = false; $conn->WebSocket->established = false;
$conn->WebSocket->headers = '';
} }
/** /**
* Do handshake, frame/unframe messages coming/going in stack
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onMessage(ConnectionInterface $from, $msg) { public function onMessage(ConnectionInterface $from, $msg) {
if (true !== $from->WebSocket->handshake) { if (true !== $from->WebSocket->established) {
if (!isset($from->WebSocket->version)) { try {
$from->WebSocket->headers .= $msg; if (null === ($request = $this->reqParser->onMessage($from, $msg))) {
if (!$this->isMessageComplete($from->WebSocket->headers)) {
return; return;
} }
} catch (\OverflowException $oe) {
$headers = RequestFactory::getInstance()->fromMessage($from->WebSocket->headers); return $this->close($from, 413);
$from->WebSocket->version = $this->getVersion($headers);
$from->WebSocket->headers = $headers;
} }
$response = $from->WebSocket->version->handshake($from->WebSocket->headers); if (!$this->versioner->isVersionEnabled($request)) {
$from->WebSocket->handshake = true; return $this->close($from);
}
if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { $from->WebSocket->request = $request;
$from->WebSocket->version = $this->versioner->getVersion($request);
$response = $from->WebSocket->version->handshake($request);
$response->setHeader('X-Powered-By', \Ratchet\VERSION);
// This needs to be refactored later on, incorporated with routing
if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) {
$response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols);
} }
$response->setHeader('X-Powered-By', \Ratchet\VERSION); $from->send((string)$response);
$header = (string)$response;
$from->send($header); if (101 != $response->getStatusCode()) {
return $from->close();
$conn = new WsConnection($from);
$this->connections->attach($from, $conn);
return $this->_decorating->onOpen($conn);
} }
if (!isset($from->WebSocket->message)) { $upgraded = $from->WebSocket->version->upgradeConnection($from, $this->_decorating);
$from->WebSocket->message = $from->WebSocket->version->newMessage();
$this->connections->attach($from, $upgraded);
$upgraded->WebSocket->established = true;
return $this->_decorating->onOpen($upgraded);
} }
// There is a frame fragment attatched to the connection, add to it $from->WebSocket->version->onMessage($this->connections[$from], $msg);
if (!isset($from->WebSocket->frame)) {
$from->WebSocket->frame = $from->WebSocket->version->newFrame();
}
$from->WebSocket->frame->addBuffer($msg);
if ($from->WebSocket->frame->isCoalesced()) {
if ($from->WebSocket->frame->getOpcode() > 2) {
$from->close();
throw new \UnexpectedValueException('Control frame support coming soon!');
}
// Check frame
// If is control frame, do your thing
// Else, add to message
// Control frames (ping, pong, close) can be sent in between a fragmented message
$from->WebSocket->message->addFrame($from->WebSocket->frame);
unset($from->WebSocket->frame);
}
if ($from->WebSocket->message->isCoalesced()) {
$this->_decorating->onMessage($this->connections[$from], (string)$from->WebSocket->message);
unset($from->WebSocket->message);
}
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onClose(ConnectionInterface $conn) { public function onClose(ConnectionInterface $conn) {
// WS::onOpen is not called when the socket connects, it's call when the handshake is done
// The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet
if ($this->connections->contains($conn)) { if ($this->connections->contains($conn)) {
$decor = $this->connections[$conn]; $decor = $this->connections[$conn];
$this->connections->detach($conn); $this->connections->detach($conn);
}
// WS::onOpen is not called when the socket connects, it's call when the handshake is done
// The socket could close before WS calls onOpen, so we need to check if we've "opened" it for the developer yet
if (isset($decor)) {
$this->_decorating->onClose($decor); $this->_decorating->onClose($decor);
} }
} }
@ -149,62 +143,13 @@ class WsServer implements MessageComponentInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onError(ConnectionInterface $conn, \Exception $e) { public function onError(ConnectionInterface $conn, \Exception $e) {
if ($this->connections->contains($conn)) { if ($conn->WebSocket->established) {
$this->_decorating->onError($this->connections[$conn], $e); $this->_decorating->onError($this->connections[$conn], $e);
} else { } else {
$conn->close(); $conn->close();
} }
} }
/**
* Detect the WebSocket protocol version a client is using based on the HTTP header request
* @param string HTTP handshake request
* @return Version\VersionInterface
* @throws UnderFlowException If we think the entire header message hasn't been buffered yet
* @throws InvalidArgumentException If we can't understand protocol version request
* @todo Verify the first line of the HTTP header as per page 16 of RFC 6455
*/
protected function getVersion(RequestInterface $request) {
foreach ($this->_versions as $name => $instance) {
if (null !== $instance) {
if ($instance::isProtocol($request)) {
return $instance;
}
} else {
$ns = __NAMESPACE__ . "\\Version\\{$name}";
if ($ns::isProtocol($request)) {
$this->_versions[$name] = new $ns;
return $this->_versions[$name];
}
}
}
throw new \InvalidArgumentException('Could not identify WebSocket protocol');
}
/**
* @param string
* @return bool
* @todo Abstract, some hard coding done for (stupid) Hixie protocol
*/
protected function isMessageComplete($message) {
static $crlf = "\r\n\r\n";
$headers = (boolean)strstr($message, $crlf);
if (!$headers) {
return false;
}
if (strstr($message, 'Sec-WebSocket-Key2')) {
if (8 !== strlen(substr($message, strpos($message, $crlf) + strlen($crlf)))) {
return false;
}
}
return true;
}
/** /**
* @param string * @param string
* @return boolean * @return boolean
@ -242,25 +187,17 @@ class WsServer implements MessageComponentInterface {
} }
/** /**
* Disable a version of the WebSocket protocol *cough*Hixie76*cough* * Close a connection with an HTTP response
* @param string The name of the version to disable * @param Ratchet\ConnectionInterface
* @throws InvalidArgumentException If the given version does not exist * @param int HTTP status code
*/ */
public function disableVersion($name) { protected function close(ConnectionInterface $conn, $code = 400) {
if (!array_key_exists($name, $this->_versions)) { $response = new Response($code, array(
throw new \InvalidArgumentException("Version {$name} not found"); 'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString()
} , 'X-Powered-By' => \Ratchet\VERSION
));
unset($this->_versions[$name]); $conn->send((string)$response);
} $conn->close();
/**
* Set the option to mask the payload upon sending to client
* If WebSocket is used as server, this should be false, client to true
* @param bool
* @todo User shouldn't have to know/set this, need to figure out how to do this automatically
*/
public function setMaskPayload($opt) {
$this->_mask_payload = (boolean)$opt;
} }
} }

View File

@ -5,7 +5,7 @@ interface WsServerInterface {
/** /**
* If any component in a stack supports a WebSocket sub-protocol return each supported in an array * If any component in a stack supports a WebSocket sub-protocol return each supported in an array
* @return array * @return array
* @temporary This method may be removed in future version (note tha twill not break code, just make some code obsolete) * @temporary This method may be removed in future version (note that will not break code, just make some code obsolete)
*/ */
function getSubProtocols(); function getSubProtocols();
} }

View File

@ -0,0 +1,22 @@
<?php
namespace Ratchet\Tests;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class AbFuzzyServer implements MessageComponentInterface {
public function onOpen(ConnectionInterface $conn) {
}
public function onMessage(ConnectionInterface $from, $msg) {
$from->send($msg);
}
public function onClose(ConnectionInterface $conn) {
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo $e->getMessage() . "\n";
$conn->close();
}
}

View File

@ -5,6 +5,7 @@ use Ratchet\Tests\Mock\Connection;
/** /**
* @covers Ratchet\AbstractConnectionDecorator * @covers Ratchet\AbstractConnectionDecorator
* @covers Ratchet\ConnectionInterface
*/ */
class AbstractConnectionDecoratorTest extends \PHPUnit_Framework_TestCase { class AbstractConnectionDecoratorTest extends \PHPUnit_Framework_TestCase {
protected $mock; protected $mock;

View File

@ -43,6 +43,10 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
* I think I have severly butchered this test...it's not so much of a unit test as it is a full-fledged component test * I think I have severly butchered this test...it's not so much of a unit test as it is a full-fledged component test
*/ */
public function testConnectionValueFromPdo() { public function testConnectionValueFromPdo() {
if (!extension_loaded('PDO')) {
return $this->markTestSkipped();
}
$sessionId = md5('testSession'); $sessionId = md5('testSession');
$dbOptions = array( $dbOptions = array(
@ -63,7 +67,7 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
$headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue($sessionId)); $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue($sessionId));
$connection->WebSocket = new \StdClass; $connection->WebSocket = new \StdClass;
$connection->WebSocket->headers = $headers; $connection->WebSocket->request = $headers;
$component->onOpen($connection); $component->onOpen($connection);
@ -79,7 +83,7 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
$headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue(null)); $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue(null));
$conns[$i]->WebSocket = new \StdClass; $conns[$i]->WebSocket = new \StdClass;
$conns[$i]->WebSocket->headers = $headers; $conns[$i]->WebSocket->request = $headers;
} }
$mock = new MockComponent; $mock = new MockComponent;

View File

@ -0,0 +1,45 @@
<?php
namespace Ratchet\Tests\WebSocket;
use Ratchet\WebSocket\HttpRequestParser;
use Ratchet\Tests\Mock\Connection as ConnectionStub;
/**
* @covers Ratchet\WebSocket\HttpRequestParser
*/
class HttpRequestParserTest extends \PHPUnit_Framework_TestCase {
protected $parser;
public function setUp() {
$this->parser = new HttpRequestParser;
}
public function headersProvider() {
return array(
array(false, "GET / HTTP/1.1\r\nHost: socketo.me\r\n")
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n")
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n1")
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖")
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie✖\r\n\r\n")
, array(true, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\nHixie\r\n")
);
}
/**
* @dataProvider headersProvider
*/
public function testIsEom($expected, $message) {
$this->assertEquals($expected, $this->parser->isEom($message));
}
public function testBufferOverflowResponse() {
$conn = new ConnectionStub;
$this->parser->maxSize = 20;
$this->assertNull($this->parser->onMessage($conn, "GET / HTTP/1.1\r\n"));
$this->setExpectedException('OverflowException');
$this->parser->onMessage($conn, "Header-Is: Too Big");
}
}

View File

@ -9,7 +9,7 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase {
protected $_version; protected $_version;
public function setUp() { public function setUp() {
$this->_version = new Hixie76(); $this->_version = new Hixie76;
} }
public function testClassImplementsVersionInterface() { public function testClassImplementsVersionInterface() {
@ -18,27 +18,13 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase {
} }
/** /**
* @dataProvider HandshakeProvider * @dataProvider keyProvider
*/
public function INCOMPLETEtestKeySigningForHandshake($key, $accept) {
// $this->assertEquals($accept, $this->_version->sign($key));
}
public static function HandshakeProvider() {
return array(
array('', '')
, array('', '')
);
}
/**
* @dataProvider KeyProvider
*/ */
public function testKeySigningForHandshake($accept, $key) { public function testKeySigningForHandshake($accept, $key) {
$this->assertEquals($accept, $this->_version->generateKeyNumber($key)); $this->assertEquals($accept, $this->_version->generateKeyNumber($key));
} }
public static function KeyProvider() { public static function keyProvider() {
return array( return array(
array(179922739, '17 9 G`ZD9 2 2b 7X 3 /r90') array(179922739, '17 9 G`ZD9 2 2b 7X 3 /r90')
, array('', '17 9 G`ZD9 2 2b 7X 3 /r91') , array('', '17 9 G`ZD9 2 2b 7X 3 /r91')

View File

@ -57,7 +57,7 @@ class HyBi10Test extends \PHPUnit_Framework_TestCase {
public function testUnframeMatchesPreFraming() { public function testUnframeMatchesPreFraming() {
$string = 'Hello World!'; $string = 'Hello World!';
$framed = $this->_version->frame($string); $framed = $this->_version->newFrame($string)->getContents();
$frame = new Frame; $frame = new Frame;
$frame->addBuffer($framed); $frame->addBuffer($framed);

View File

@ -19,21 +19,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
$this->_frame = new Frame; $this->_frame = new Frame;
} }
protected static function convert($in) {
if (strlen($in) > 8) {
$out = '';
while (strlen($in) > 8) {
$out .= static::convert(substr($in, 0, 8));
$in = substr($in, 8);
}
return $out;
}
return pack('C', bindec($in));
}
/** /**
* This is a data provider * This is a data provider
* @param string The UTF8 message * @param string The UTF8 message
@ -67,7 +52,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
$this->setExpectedException('\UnderflowException'); $this->setExpectedException('\UnderflowException');
if (!empty($bin)) { if (!empty($bin)) {
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
} }
call_user_func(array($this->_frame, $method)); call_user_func(array($this->_frame, $method));
@ -93,7 +78,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider firstByteProvider * @dataProvider firstByteProvider
*/ */
public function testFinCodeFromBits($fin, $opcode, $bin) { public function testFinCodeFromBits($fin, $opcode, $bin) {
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
$this->assertEquals($fin, $this->_frame->isFinal()); $this->assertEquals($fin, $this->_frame->isFinal());
} }
@ -109,7 +94,7 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider firstByteProvider * @dataProvider firstByteProvider
*/ */
public function testOpcodeFromBits($fin, $opcode, $bin) { public function testOpcodeFromBits($fin, $opcode, $bin) {
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
$this->assertEquals($opcode, $this->_frame->getOpcode()); $this->assertEquals($opcode, $this->_frame->getOpcode());
} }
@ -136,8 +121,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider payloadLengthDescriptionProvider * @dataProvider payloadLengthDescriptionProvider
*/ */
public function testFirstPayloadDesignationValue($bits, $bin) { public function testFirstPayloadDesignationValue($bits, $bin) {
$this->_frame->addBuffer(static::convert($this->_firstByteFinText)); $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
$ref = new \ReflectionClass($this->_frame); $ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal'); $cb = $ref->getMethod('getFirstPayloadVal');
@ -150,8 +135,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider payloadLengthDescriptionProvider * @dataProvider payloadLengthDescriptionProvider
*/ */
public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) { public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) {
$this->_frame->addBuffer(static::convert($this->_firstByteFinText)); $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
$ref = new \ReflectionClass($this->_frame); $ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits'); $cb = $ref->getMethod('getNumPayloadBits');
@ -172,8 +157,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider secondByteProvider * @dataProvider secondByteProvider
*/ */
public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) { public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) {
$this->_frame->addBuffer(static::convert($this->_firstByteFinText)); $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
$this->assertEquals($masked, $this->_frame->isMasked()); $this->assertEquals($masked, $this->_frame->isMasked());
} }
@ -190,8 +175,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider secondByteProvider * @dataProvider secondByteProvider
*/ */
public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) { public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) {
$this->_frame->addBuffer(static::convert($this->_firstByteFinText)); $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::convert($bin)); $this->_frame->addBuffer(Frame::encode($bin));
$this->assertEquals($payload_length, $this->_frame->getPayloadLength()); $this->assertEquals($payload_length, $this->_frame->getPayloadLength());
} }
@ -230,8 +215,8 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @todo I I wrote the dataProvider incorrectly, skpping for now * @todo I I wrote the dataProvider incorrectly, skpping for now
*/ */
public function testGetMaskingKey($mask) { public function testGetMaskingKey($mask) {
$this->_frame->addBuffer(static::convert($this->_firstByteFinText)); $this->_frame->addBuffer(Frame::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::convert($this->_secondByteMaskedSPL)); $this->_frame->addBuffer(Frame::encode($this->_secondByteMaskedSPL));
$this->_frame->addBuffer($mask); $this->_frame->addBuffer($mask);
$this->assertEquals($mask, $this->_frame->getMaskingKey()); $this->assertEquals($mask, $this->_frame->getMaskingKey());
@ -256,8 +241,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
* @dataProvider UnframeMessageProvider * @dataProvider UnframeMessageProvider
*/ */
public function testCheckPiecingTogetherMessage($msg, $encoded) { public function testCheckPiecingTogetherMessage($msg, $encoded) {
// return $this->markTestIncomplete('Ran out of time, had to attend to something else, come finish me!');
$framed = base64_decode($encoded); $framed = base64_decode($encoded);
for ($i = 0, $len = strlen($framed);$i < $len; $i++) { for ($i = 0, $len = strlen($framed);$i < $len; $i++) {
$this->_frame->addBuffer(substr($framed, $i, 1)); $this->_frame->addBuffer(substr($framed, $i, 1));
@ -265,4 +248,94 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals($msg, $this->_frame->getPayload()); $this->assertEquals($msg, $this->_frame->getPayload());
} }
public function testLongCreate() {
$len = 65525;
$pl = $this->generateRandomString($len);
$frame = Frame::create($pl, true, Frame::OP_PING);
$this->assertTrue($frame->isFinal());
$this->assertEquals(Frame::OP_PING, $frame->getOpcode());
$this->assertFalse($frame->isMasked());
$this->assertEquals($len, $frame->getPayloadLength());
$this->assertEquals($pl, $frame->getPayload());
}
public function testReallyLongCreate() {
$len = 65575;
$frame = Frame::create($this->generateRandomString($len));
$this->assertEquals($len, $frame->getPayloadLength());
}
public function testExtractOverflow() {
$string1 = $this->generateRandomString();
$frame1 = Frame::create($string1);
$string2 = $this->generateRandomString();
$frame2 = Frame::create($string2);
$cat = new Frame;
$cat->addBuffer($frame1->getContents() . $frame2->getContents());
$this->assertEquals($frame1->getContents(), $cat->getContents());
$this->assertEquals($string1, $cat->getPayload());
$uncat = new Frame;
$uncat->addBuffer($cat->extractOverflow());
$this->assertEquals($string1, $cat->getPayload());
$this->assertEquals($string2, $uncat->getPayload());
}
public function testEmptyExtractOverflow() {
$string = $this->generateRandomString();
$frame = Frame::create($string);
$this->assertEquals($string, $frame->getPayload());
$this->assertEquals('', $frame->extractOverflow());
$this->assertEquals($string, $frame->getPayload());
}
public function testMasking() {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame = Frame::create($msg)->maskPayload();
$this->assertTrue($frame->isMasked());
$this->assertEquals($msg, $frame->getPayload());
}
public function testUnMaskPayload() {
$string = $this->generateRandomString();
$frame = Frame::create($string)->maskPayload()->unMaskPayload();
$this->assertFalse($frame->isMasked());
$this->assertEquals($string, $frame->getPayload());
}
protected function generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) {
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$%&/()=[]{}'; // ยง
$useChars = array();
for($i = 0; $i < $length; $i++) {
$useChars[] = $characters[mt_rand(0, strlen($characters) - 1)];
}
if($addSpaces === true) {
array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' ');
}
if($addNumbers === true) {
array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9));
}
shuffle($useChars);
$randomString = trim(implode('', $useChars));
$randomString = substr($randomString, 0, $length);
return $randomString;
}
} }

View File

@ -18,7 +18,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
public static function methodProvider() { public static function methodProvider() {
return array( return array(
array(true, 'GET') array(true, 'GET')
, array(false, 'get') // I'm not sure if this is valid or not, need to check standard , array(true, 'get')
, array(true, 'Get')
, array(false, 'POST') , array(false, 'POST')
, array(false, 'DELETE') , array(false, 'DELETE')
, array(false, 'PUT') , array(false, 'PUT')
@ -64,6 +65,7 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
, array(false, '/chat#bad') , array(false, '/chat#bad')
, array(false, 'nope') , array(false, 'nope')
, array(false, '/ ಠ_ಠ ') , array(false, '/ ಠ_ಠ ')
, array(false, '/✖')
); );
} }
@ -91,7 +93,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
public static function upgradeProvider() { public static function upgradeProvider() {
return array( return array(
array(true, 'websocket') array(true, 'websocket')
, array(false, 'Websocket') , array(true, 'Websocket')
, array(true, 'webSocket')
, array(false, null) , array(false, null)
, array(false, '') , array(false, '')
); );
@ -107,7 +110,7 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
public static function connectionProvider() { public static function connectionProvider() {
return array( return array(
array(true, 'Upgrade') array(true, 'Upgrade')
, array(false, 'upgrade') , array(true, 'upgrade')
, array(true, 'keep-alive, Upgrade') , array(true, 'keep-alive, Upgrade')
, array(true, 'Upgrade, keep-alive') , array(true, 'Upgrade, keep-alive')
, array(true, 'keep-alive, Upgrade, something') , array(true, 'keep-alive, Upgrade, something')
@ -133,6 +136,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
, array(false, 'Hello World') , array(false, 'Hello World')
, array(false, '1234567890123456') , array(false, '1234567890123456')
, array(false, '123456789012345678901234') , array(false, '123456789012345678901234')
, array(true, base64_encode('UTF8allthngs+✓'))
, array(true, 'dGhlIHNhbXBsZSBub25jZQ==')
); );
} }

View File

@ -0,0 +1,63 @@
<?php
namespace Ratchet\Tests\WebSocket\Version\RFC6455\Message;
use Ratchet\WebSocket\Version\RFC6455\Message;
use Ratchet\WebSocket\Version\RFC6455\Frame;
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Message
*/
class MessageTest extends \PHPUnit_Framework_TestCase {
protected $message;
public function setUp() {
$this->message = new Message;
}
public function testNoFrames() {
$this->assertFalse($this->message->isCoalesced());
}
public function testNoFramesOpCode() {
$this->setExpectedException('UnderflowException');
$this->message->getOpCode();
}
public function testFragmentationPayload() {
$a = 'Hello ';
$b = 'World!';
$f1 = Frame::create($a, false);
$f2 = Frame::create($b, true, Frame::OP_CONTINUE);
$this->message->addFrame($f1)->addFrame($f2);
$this->assertEquals(strlen($a . $b), $this->message->getPayloadLength());
$this->assertEquals($a . $b, $this->message->getPayload());
}
public function testUnbufferedFragment() {
$this->message->addFrame(Frame::create('The quick brow', false));
$this->setExpectedException('UnderflowException');
$this->message->getPayload();
}
public function testGetOpCode() {
$this->message
->addFrame(Frame::create('The quick brow', false, Frame::OP_TEXT))
->addFrame(Frame::create('n fox jumps ov', false, Frame::OP_CONTINUE))
->addFrame(Frame::create('er the lazy dog', true, Frame::OP_CONTINUE))
;
$this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode());
}
public function testGetUnBufferedPayloadLength() {
$this->message
->addFrame(Frame::create('The quick brow', false, Frame::OP_TEXT))
->addFrame(Frame::create('n fox jumps ov', false, Frame::OP_CONTINUE))
;
$this->assertEquals(28, $this->message->getPayloadLength());
}
}

View File

@ -3,33 +3,26 @@ namespace Ratchet\Tests\WebSocket\Version;
use Ratchet\WebSocket\Version\RFC6455; use Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\RFC6455\Frame; use Ratchet\WebSocket\Version\RFC6455\Frame;
use Guzzle\Http\Message\RequestFactory; use Guzzle\Http\Message\RequestFactory;
use Guzzle\Http\Message\EntityEnclosingRequest;
/** /**
* @covers Ratchet\WebSocket\Version\RFC6455 * @covers Ratchet\WebSocket\Version\RFC6455
*/ */
class RFC6455Test extends \PHPUnit_Framework_TestCase { class RFC6455Test extends \PHPUnit_Framework_TestCase {
protected $_version; protected $version;
public function setUp() { public function setUp() {
$this->_version = new RFC6455(); $this->version = new RFC6455;
} }
/** /**
* Is this useful? * @dataProvider handshakeProvider
*/
public function testClassImplementsVersionInterface() {
$constraint = $this->isInstanceOf('\\Ratchet\\WebSocket\\Version\\VersionInterface');
$this->assertThat($this->_version, $constraint);
}
/**
* @dataProvider HandshakeProvider
*/ */
public function testKeySigningForHandshake($key, $accept) { public function testKeySigningForHandshake($key, $accept) {
$this->assertEquals($accept, $this->_version->sign($key)); $this->assertEquals($accept, $this->version->sign($key));
} }
public static function HandshakeProvider() { public static function handshakeProvider() {
return array( return array(
array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=') array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=')
, array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') , array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=')
@ -57,7 +50,7 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
public function testUnframeMatchesPreFraming() { public function testUnframeMatchesPreFraming() {
$string = 'Hello World!'; $string = 'Hello World!';
$framed = $this->_version->frame($string); $framed = $this->version->newFrame($string)->getContents();
$frame = new Frame; $frame = new Frame;
$frame->addBuffer($framed); $frame->addBuffer($framed);
@ -77,6 +70,26 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
, 'Sec-WebSocket-Version' => 13 , 'Sec-WebSocket-Version' => 13
); );
public function caseVariantProvider() {
return array(
array('Sec-Websocket-Version')
, array('sec-websocket-version')
, array('SEC-WEBSOCKET-VERSION')
, array('sEC-wEBsOCKET-vERSION')
);
}
/**
* @dataProvider caseVariantProvider
*/
public function testIsProtocolWithCaseInsensitivity($headerName) {
$header = static::$good_header;
unset($header['Sec-WebSocket-Version']);
$header[$headerName] = 13;
$this->assertTrue($this->version->isProtocol(new EntityEnclosingRequest('get', '/', $header)));
}
/** /**
* A helper function to try and quickly put together a valid WebSocket HTTP handshake * A helper function to try and quickly put together a valid WebSocket HTTP handshake
* but optionally replace a piece to an invalid value for failure testing * but optionally replace a piece to an invalid value for failure testing
@ -117,20 +130,22 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
*/ */
public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) { public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) {
$request = RequestFactory::getInstance()->fromMessage($header); $request = RequestFactory::getInstance()->fromMessage($header);
$response = $this->version->handshake($request);
$this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $response);
if ($pass) { if ($pass) {
$this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $this->_version->handshake($request)); $this->assertEquals(101, $response->getStatusCode());
} else { } else {
$this->setExpectedException('InvalidArgumentException'); $this->assertGreaterThanOrEqual(400, $response->getStatusCode());
$this->_version->handshake($request);
} }
} }
public function testNewMessage() { public function testNewMessage() {
$this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->_version->newMessage()); $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->version->newMessage());
} }
public function testNewFrame() { public function testNewFrame() {
$this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->_version->newFrame()); $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->version->newFrame());
} }
} }

View File

@ -0,0 +1,91 @@
<?php
namespace Ratchet\Tests\WebSocket;
use Ratchet\WebSocket\VersionManager;
use Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\HyBi10;
use Ratchet\WebSocket\Version\Hixie76;
use Guzzle\Http\Message\EntityEnclosingRequest;
/**
* @covers Ratchet\WebSocket\VersionManager
*/
class VersionManagerTest extends \PHPUnit_Framework_TestCase {
protected $vm;
public function setUp() {
$this->vm = new VersionManager;
}
public function testFluentInterface() {
$rfc = new RFC6455;
$this->assertSame($this->vm, $this->vm->enableVersion($rfc));
$this->assertSame($this->vm, $this->vm->disableVersion(13));
}
public function testGetVersion() {
$rfc = new RFC6455;
$this->vm->enableVersion($rfc);
$req = new EntityEnclosingRequest('get', '/', array(
'Host' => 'socketo.me'
, 'Sec-WebSocket-Version' => 13
));
$this->assertSame($rfc, $this->vm->getVersion($req));
}
public function testGetNopeVersionAndDisable() {
$req = new EntityEnclosingRequest('get', '/', array(
'Host' => 'socketo.me'
, 'Sec-WebSocket-Version' => 13
));
$this->setExpectedException('InvalidArgumentException');
$this->vm->getVersion($req);
}
public function testYesIsVersionEnabled() {
$this->vm->enableVersion(new RFC6455);
$this->assertTrue($this->vm->isVersionEnabled(new EntityEnclosingRequest('get', '/', array(
'Host' => 'socketo.me'
, 'Sec-WebSocket-Version' => 13
))));
}
public function testNoIsVersionEnabled() {
$this->assertFalse($this->vm->isVersionEnabled(new EntityEnclosingRequest('get', '/', array(
'Host' => 'socketo.me'
, 'Sec-WebSocket-Version' => 9000
))));
}
public function testGetSupportedVersionString() {
$v1 = new RFC6455;
$v2 = new HyBi10;
$this->vm->enableVersion($v1);
$this->vm->enableVersion($v2);
$string = $this->vm->getSupportedVersionString();
$values = explode(',', $string);
$this->assertContains($v1->getVersionNumber(), $values);
$this->assertContains($v2->getVersionNumber(), $values);
}
public function testGetSupportedVersionAfterRemoval() {
$this->vm->enableVersion(new RFC6455);
$this->vm->enableVersion(new HyBi10);
$this->vm->enableVersion(new Hixie76);
$this->vm->disableVersion(0);
$values = explode(',', $this->vm->getSupportedVersionString());
$this->assertEquals(2, count($values));
$this->assertFalse(array_search(0, $values));
}
}

View File

@ -5,6 +5,8 @@ use Ratchet\Tests\Mock\Component as MockComponent;
/** /**
* @covers Ratchet\WebSocket\WsServer * @covers Ratchet\WebSocket\WsServer
* @covers Ratchet\ComponentInterface
* @covers Ratchet\MessageComponentInterface
*/ */
class WsServerTest extends \PHPUnit_Framework_TestCase { class WsServerTest extends \PHPUnit_Framework_TestCase {
protected $comp; protected $comp;

View File

@ -0,0 +1,15 @@
{
"options": {"failByDrop": false}
, "outdir": "../reports/ab"
, "servers": [
{"agent": "Ratchet-libevent/v0.2b", "url": "ws://localhost:8000", "options": {"version": 18}}
, {"agent": "Ratchet-stream/v0.2b", "url": "ws://localhost:8001", "options": {"version": 18}}
]
, "cases": ["*"]
, "exclude-cases": ["9.*"]
, "limit-exclude-cases": ["2.*", "3.*", "4.*", "5.*", "6.*", "7.*", "8.*", "9.2.*", "9.3.*", "9.4.*", "9.5.*", "9.6.*", "9.7.*", "9.8.*"]
, "exclude-agent-cases": {}
}

View File

@ -0,0 +1,12 @@
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
$loop = new React\EventLoop\LibEventLoop;
$sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer);
$sock->listen(8000, '0.0.0.0');
$server = new Ratchet\Server\IoServer($app, $sock, $loop);
$server->run();

View File

@ -0,0 +1,12 @@
<?php
require dirname(__DIR__) . '/vendor/autoload.php';
$loop = new React\EventLoop\StreamSelectLoop;
$sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer);
$sock->listen(8001, '0.0.0.0');
$server = new Ratchet\Server\IoServer($app, $sock, $loop);
$server->run();