Merge branch '0.4'

This commit is contained in:
Chris Boden 2017-09-14 08:09:48 -04:00
commit 07f0af72ad
60 changed files with 506 additions and 3396 deletions

View File

@ -11,9 +11,6 @@ php:
dist: trusty dist: trusty
matrix: matrix:
include:
- php: 5.3
dist: precise
allow_failures: allow_failures:
- php: hhvm - php: hhvm

View File

@ -1,13 +1,26 @@
CHANGELOG CHANGELOG
========= =========
###Legend ### Legend
* "BC": Backwards compatibility break (from public component APIs) * "BC": Backwards compatibility break (from public component APIs)
* "BF": Bug fix * "BF": Bug fix
--- ---
* 0.4 (2017-09-14)
* BC: $conn->WebSocket->request replaced with $conn->httpRequest which is a PSR-7 object
* Binary messages now supported via Ratchet\WebSocket\MessageComponentInterface
* Added heartbeat support via ping/pong in WsServer
* BC: No longer support old (and insecure) Hixie76 and Hybi protocols
* BC: No longer support disabling UTF-8 checks
* BC: The Session component implements HttpServerInterface instead of WsServerInterface
* BC: PHP 5.3 no longer supported
* BC: Update to newer version of react/socket dependency
* BC: WAMP topics reduced to 0 subscriptions are deleted, new subs to same name will result in new Topic instance
* Significant performance enhancements
* 0.3.6 (2017-01-06) * 0.3.6 (2017-01-06)
* BF: Keep host and scheme in HTTP request object attatched to connection * BF: Keep host and scheme in HTTP request object attatched to connection
* BF: Return correct HTTP response (405) when non-GET request made * BF: Return correct HTTP response (405) when non-GET request made

View File

@ -10,26 +10,33 @@ cover:
abtests: abtests:
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8001 LibEvent & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8001 LibEvent &
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8002 StreamSelect & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8002 StreamSelect &
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver-noutf8.php 8003 StreamSelect &
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv &
wstest -m testeeserver -w ws://localhost:8000 & wstest -m testeeserver -w ws://localhost:8000 &
sleep 1
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json
killall php wstest killall php wstest
abtest: abtest:
ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect &
sleep 1
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json
killall php killall php
profile: profile:
php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent & php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent &
sleep 1
wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json
killall php killall php
apidocs: apidocs:
apigen --title Ratchet -d reports/api -s src/ \ apigen --title Ratchet -d reports/api \
-s vendor/react \ -s src/ \
-s vendor/guzzle \ -s vendor/ratchet/rfc6455/src \
-s vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Session \ -s vendor/react/event-loop/src \
-s vendor/symfony/routing/Symfony/Component/Routing \ -s vendor/react/socket/src \
-s vendor/evenement/evenement/src/Evenement -s vendor/react/stream/src \
-s vendor/psr/http-message/src \
-s vendor/symfony/http-foundation/Session \
-s vendor/symfony/routing \
-s vendor/evenement/evenement/src/Evenement \
--exclude=vendor/symfony/routing/Tests \

View File

@ -1,17 +1,12 @@
# Ratchet # Ratchet
[![Build Status](https://secure.travis-ci.org/ratchetphp/Ratchet.png?branch=master)](http://travis-ci.org/ratchetphp/Ratchet) [![Build Status](https://secure.travis-ci.org/ratchetphp/Ratchet.png?branch=master)](http://travis-ci.org/ratchetphp/Ratchet)
[![Autobahn Testsuite](https://img.shields.io/badge/Autobahn-passing-brightgreen.svg)](http://socketo.me/reports/ab/index.html)
[![Latest Stable Version](https://poser.pugx.org/cboden/ratchet/v/stable.png)](https://packagist.org/packages/cboden/ratchet) [![Latest Stable Version](https://poser.pugx.org/cboden/ratchet/v/stable.png)](https://packagist.org/packages/cboden/ratchet)
A PHP 5.3 library for asynchronously serving WebSockets. A PHP library for asynchronously serving WebSockets.
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
* Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time)
* Tested on Chrome 13+, Firefox 6+, Safari 5+, iOS 4.2+, IE 8+
* Ratchet [passes](http://socketo.me/reports/ab/) the [Autobahn Testsuite](http://autobahn.ws/testsuite) (non-binary messages)
## Requirements ## Requirements
Shell access is required and root access is recommended. Shell access is required and root access is recommended.
@ -19,8 +14,6 @@ To avoid proxy/firewall blockage it's recommended WebSockets are requested on po
In order to do this, along with your sync web stack, you can either use a reverse proxy or two separate machines. In order to do this, along with your sync web stack, you can either use a reverse proxy or two separate machines.
You can find more details in the [server conf docs](http://socketo.me/docs/deploy#serverconfiguration). You can find more details in the [server conf docs](http://socketo.me/docs/deploy#serverconfiguration).
PHP 5.3.9 (or higher) is required. If you have access, PHP 5.4 (or higher) is *highly* recommended for its performance improvements.
### Documentation ### Documentation
User and API documentation is available on Ratchet's website: http://socketo.me User and API documentation is available on Ratchet's website: http://socketo.me

View File

@ -2,7 +2,7 @@
"name": "cboden/ratchet" "name": "cboden/ratchet"
, "type": "library" , "type": "library"
, "description": "PHP WebSocket library" , "description": "PHP WebSocket library"
, "keywords": ["WebSockets", "Server", "Ratchet", "Sockets"] , "keywords": ["WebSockets", "Server", "Ratchet", "Sockets", "WebSocket"]
, "homepage": "http://socketo.me" , "homepage": "http://socketo.me"
, "license": "MIT" , "license": "MIT"
, "authors": [ , "authors": [
@ -22,10 +22,14 @@
"Ratchet\\": "src/Ratchet" "Ratchet\\": "src/Ratchet"
} }
} }
, "suggest": {
"ext-pecl_http": "^2.0"
}
, "require": { , "require": {
"php": ">=5.3.9" "php": ">=5.4.2"
, "react/socket": "^0.3 || ^0.4" , "ratchet/rfc6455": "^0.2"
, "guzzle/http": "^3.6" , "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5"
, "guzzlehttp/psr7": "^1.0"
, "symfony/http-foundation": "^2.2|^3.0" , "symfony/http-foundation": "^2.2|^3.0"
, "symfony/routing": "^2.2|^3.0" , "symfony/routing": "^2.2|^3.0"
} }

View File

@ -3,6 +3,7 @@ namespace Ratchet;
use React\EventLoop\LoopInterface; use React\EventLoop\LoopInterface;
use React\EventLoop\Factory as LoopFactory; use React\EventLoop\Factory as LoopFactory;
use React\Socket\Server as Reactor; use React\Socket\Server as Reactor;
use React\Socket\SecureServer as SecureReactor;
use Ratchet\Http\HttpServerInterface; use Ratchet\Http\HttpServerInterface;
use Ratchet\Http\OriginCheck; use Ratchet\Http\OriginCheck;
use Ratchet\Wamp\WampServerInterface; use Ratchet\Wamp\WampServerInterface;
@ -55,20 +56,16 @@ class App {
protected $_routeCounter = 0; protected $_routeCounter = 0;
/** /**
* @param string $httpHost HTTP hostname clients intend to connect to. MUST match JS `new WebSocket('ws://$httpHost');` * @param string $httpHost HTTP hostname clients intend to connect to. MUST match JS `new WebSocket('ws://$httpHost');`
* @param int $port Port to listen on. If 80, assuming production, Flash on 843 otherwise expecting Flash to be proxied through 8843 * @param int $port Port to listen on. If 80, assuming production, Flash on 843 otherwise expecting Flash to be proxied through 8843
* @param string $address IP address to bind to. Default is localhost/proxy only. '0.0.0.0' for any machine. * @param string $address IP address to bind to. Default is localhost/proxy only. '0.0.0.0' for any machine.
* @param LoopInterface $loop Specific React\EventLoop to bind the application to. null will create one for you. * @param LoopInterface $loop Specific React\EventLoop to bind the application to. null will create one for you.
*/ */
public function __construct($httpHost = 'localhost', $port = 8080, $address = '127.0.0.1', LoopInterface $loop = null) { public function __construct($httpHost = 'localhost', $port = 8080, $address = '127.0.0.1', LoopInterface $loop = null) {
if (extension_loaded('xdebug')) { if (extension_loaded('xdebug')) {
trigger_error('XDebug extension detected. Remember to disable this if performance testing or going live!', E_USER_WARNING); trigger_error('XDebug extension detected. Remember to disable this if performance testing or going live!', E_USER_WARNING);
} }
if (3 !== strlen('✓')) {
throw new \DomainException('Bad encoding, length of unicode character ✓ should be 3. Ensure charset UTF-8 and check ini val mbstring.func_autoload');
}
if (null === $loop) { if (null === $loop) {
$loop = LoopFactory::create(); $loop = LoopFactory::create();
} }
@ -76,8 +73,7 @@ class App {
$this->httpHost = $httpHost; $this->httpHost = $httpHost;
$this->port = $port; $this->port = $port;
$socket = new Reactor($loop); $socket = new Reactor($address . ':' . $port, $loop);
$socket->listen($port, $address);
$this->routes = new RouteCollection; $this->routes = new RouteCollection;
$this->_server = new IoServer(new HttpServer(new Router(new UrlMatcher($this->routes, new RequestContext))), $socket, $loop); $this->_server = new IoServer(new HttpServer(new Router(new UrlMatcher($this->routes, new RequestContext))), $socket, $loop);
@ -85,13 +81,14 @@ class App {
$policy = new FlashPolicy; $policy = new FlashPolicy;
$policy->addAllowedAccess($httpHost, 80); $policy->addAllowedAccess($httpHost, 80);
$policy->addAllowedAccess($httpHost, $port); $policy->addAllowedAccess($httpHost, $port);
$flashSock = new Reactor($loop);
$this->flashServer = new IoServer($policy, $flashSock);
if (80 == $port) { if (80 == $port) {
$flashSock->listen(843, '0.0.0.0'); $flashUri = '0.0.0.0:843';
} else { } else {
$flashSock->listen(8843); $flashUri = 8843;
} }
$flashSock = new Reactor($flashUri, $loop);
$this->flashServer = new IoServer($policy, $flashSock);
} }
/** /**
@ -113,6 +110,10 @@ class App {
$decorated = $controller; $decorated = $controller;
} }
if ($decorated instanceof WsServer) {
$decorated->enableKeepAlive($this->_server->loop, 30);
}
if ($httpHost === null) { if ($httpHost === null) {
$httpHost = $this->httpHost; $httpHost = $this->httpHost;
} }

View File

@ -5,7 +5,7 @@ namespace Ratchet;
* The version of Ratchet being used * The version of Ratchet being used
* @var string * @var string
*/ */
const VERSION = 'Ratchet/0.3.6'; const VERSION = 'Ratchet/0.4';
/** /**
* A proxy object representing a connection to the application * A proxy object representing a connection to the application

View File

@ -0,0 +1,22 @@
<?php
namespace Ratchet\Http;
use Ratchet\ConnectionInterface;
use GuzzleHttp\Psr7 as gPsr;
use GuzzleHttp\Psr7\Response;
trait CloseResponseTrait {
/**
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
* @return null
*/
private function close(ConnectionInterface $conn, $code = 400, array $additional_headers = []) {
$response = new Response($code, array_merge([
'X-Powered-By' => \Ratchet\VERSION
], $additional_headers));
$conn->send(gPsr\str($response));
$conn->close();
}
}

View File

@ -1,34 +0,0 @@
<?php
namespace Ratchet\Http\Guzzle\Http\Message;
use Guzzle\Http\Message\RequestFactory as GuzzleRequestFactory;
use Guzzle\Http\EntityBody;
class RequestFactory extends GuzzleRequestFactory {
protected static $ratchetInstance;
/**
* {@inheritdoc}
*/
public static function getInstance()
{
// @codeCoverageIgnoreStart
if (!static::$ratchetInstance) {
static::$ratchetInstance = new static();
}
// @codeCoverageIgnoreEnd
return static::$ratchetInstance;
}
/**
* {@inheritdoc}
*/
public function create($method, $url, $headers = null, $body = '', array $options = array()) {
$c = $this->entityEnclosingRequestClass;
$request = new $c($method, $url, $headers);
$request->setBody(EntityBody::factory($body));
return $request;
}
}

View File

@ -2,11 +2,11 @@
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\MessageInterface; use Ratchet\MessageInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Http\Guzzle\Http\Message\RequestFactory; use GuzzleHttp\Psr7 as gPsr;
/** /**
* This class receives streaming data from a client request * This class receives streaming data from a client request
* and parses HTTP headers, returning a Guzzle Request object * and parses HTTP headers, returning a PSR-7 Request object
* once it's been buffered * once it's been buffered
*/ */
class HttpRequestParser implements MessageInterface { class HttpRequestParser implements MessageInterface {
@ -22,7 +22,7 @@ class HttpRequestParser implements MessageInterface {
/** /**
* @param \Ratchet\ConnectionInterface $context * @param \Ratchet\ConnectionInterface $context
* @param string $data Data stream to buffer * @param string $data Data stream to buffer
* @return \Guzzle\Http\Message\RequestInterface|null * @return \Psr\Http\Message\RequestInterface
* @throws \OverflowException If the message buffer has become too large * @throws \OverflowException If the message buffer has become too large
*/ */
public function onMessage(ConnectionInterface $context, $data) { public function onMessage(ConnectionInterface $context, $data) {
@ -37,7 +37,7 @@ class HttpRequestParser implements MessageInterface {
} }
if ($this->isEom($context->httpBuffer)) { if ($this->isEom($context->httpBuffer)) {
$request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); $request = $this->parse($context->httpBuffer);
unset($context->httpBuffer); unset($context->httpBuffer);
@ -53,4 +53,12 @@ class HttpRequestParser implements MessageInterface {
public function isEom($message) { public function isEom($message) {
return (boolean)strpos($message, static::EOM); return (boolean)strpos($message, static::EOM);
} }
/**
* @param string $headers
* @return \Psr\Http\Message\RequestInterface
*/
public function parse($headers) {
return gPsr\parse_request($headers);
}
} }

View File

@ -2,9 +2,10 @@
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\Response;
class HttpServer implements MessageComponentInterface { class HttpServer implements MessageComponentInterface {
use CloseResponseTrait;
/** /**
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced * Buffers incoming HTTP requests returning a Guzzle Request when coalesced
* @var HttpRequestParser * @var HttpRequestParser
@ -72,19 +73,4 @@ class HttpServer implements MessageComponentInterface {
$this->close($conn, 500); $this->close($conn, 500);
} }
} }
/**
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
* @return null
*/
protected function close(ConnectionInterface $conn, $code = 400) {
$response = new Response($code, array(
'X-Powered-By' => \Ratchet\VERSION
));
$conn->send((string)$response);
$conn->close();
}
} }

View File

@ -2,12 +2,12 @@
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
interface HttpServerInterface extends MessageComponentInterface { interface HttpServerInterface extends MessageComponentInterface {
/** /**
* @param \Ratchet\ConnectionInterface $conn * @param \Ratchet\ConnectionInterface $conn
* @param \Guzzle\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!! * @param \Psr\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!!
* @throws \UnexpectedValueException if a RequestInterface is not passed * @throws \UnexpectedValueException if a RequestInterface is not passed
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); public function onOpen(ConnectionInterface $conn, RequestInterface $request = null);

View File

@ -1,9 +1,8 @@
<?php <?php
namespace Ratchet\Http; namespace Ratchet\Http;
use Guzzle\Http\Message\RequestInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Guzzle\Http\Message\Response; use Psr\Http\Message\RequestInterface;
/** /**
* A middleware to ensure JavaScript clients connecting are from the expected domain. * A middleware to ensure JavaScript clients connecting are from the expected domain.
@ -11,18 +10,20 @@ use Guzzle\Http\Message\Response;
* Note: This can be spoofed from non-web browser clients * Note: This can be spoofed from non-web browser clients
*/ */
class OriginCheck implements HttpServerInterface { class OriginCheck implements HttpServerInterface {
use CloseResponseTrait;
/** /**
* @var \Ratchet\MessageComponentInterface * @var \Ratchet\MessageComponentInterface
*/ */
protected $_component; protected $_component;
public $allowedOrigins = array(); public $allowedOrigins = [];
/** /**
* @param MessageComponentInterface $component Component/Application to decorate * @param MessageComponentInterface $component Component/Application to decorate
* @param array $allowed An array of allowed domains that are allowed to connect from * @param array $allowed An array of allowed domains that are allowed to connect from
*/ */
public function __construct(MessageComponentInterface $component, array $allowed = array()) { public function __construct(MessageComponentInterface $component, array $allowed = []) {
$this->_component = $component; $this->_component = $component;
$this->allowedOrigins += $allowed; $this->allowedOrigins += $allowed;
} }
@ -31,7 +32,7 @@ class OriginCheck implements HttpServerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
$header = (string)$request->getHeader('Origin'); $header = (string)$request->getHeader('Origin')[0];
$origin = parse_url($header, PHP_URL_HOST) ?: $header; $origin = parse_url($header, PHP_URL_HOST) ?: $header;
if (!in_array($origin, $this->allowedOrigins)) { if (!in_array($origin, $this->allowedOrigins)) {
@ -61,19 +62,4 @@ class OriginCheck implements HttpServerInterface {
function onError(ConnectionInterface $conn, \Exception $e) { function onError(ConnectionInterface $conn, \Exception $e) {
return $this->_component->onError($conn, $e); return $this->_component->onError($conn, $e);
} }
}
/**
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
* @return null
*/
protected function close(ConnectionInterface $conn, $code = 400) {
$response = new Response($code, array(
'X-Powered-By' => \Ratchet\VERSION
));
$conn->send((string)$response);
$conn->close();
}
}

View File

@ -1,14 +1,15 @@
<?php <?php
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Guzzle\Http\Url;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException; use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use GuzzleHttp\Psr7 as gPsr;
class Router implements HttpServerInterface { class Router implements HttpServerInterface {
use CloseResponseTrait;
/** /**
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface * @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/ */
@ -27,12 +28,14 @@ class Router implements HttpServerInterface {
throw new \UnexpectedValueException('$request can not be null'); throw new \UnexpectedValueException('$request can not be null');
} }
$uri = $request->getUri();
$context = $this->_matcher->getContext(); $context = $this->_matcher->getContext();
$context->setMethod($request->getMethod()); $context->setMethod($request->getMethod());
$context->setHost($request->getHost()); $context->setHost($uri->getHost());
try { try {
$route = $this->_matcher->match($request->getPath()); $route = $this->_matcher->match($uri->getPath());
} catch (MethodNotAllowedException $nae) { } catch (MethodNotAllowedException $nae) {
return $this->close($conn, 405, array('Allow' => $nae->getAllowedMethods())); return $this->close($conn, 405, array('Allow' => $nae->getAllowedMethods()));
} catch (ResourceNotFoundException $nfe) { } catch (ResourceNotFoundException $nfe) {
@ -47,17 +50,15 @@ class Router implements HttpServerInterface {
throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface'); throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface');
} }
$parameters = array(); $parameters = [];
foreach($route as $key => $value) { foreach($route as $key => $value) {
if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { if ((is_string($key)) && ('_' !== substr($key, 0, 1))) {
$parameters[$key] = $value; $parameters[$key] = $value;
} }
} }
$parameters = array_merge($parameters, $request->getQuery()->getAll()); $parameters = array_merge($parameters, gPsr\parse_query($uri->getQuery() ?: ''));
$url = Url::factory($request->getUrl()); $request = $request->withUri($uri->withQuery(gPsr\build_query($parameters)));
$url->setQuery($parameters);
$request->setUrl($url);
$conn->controller = $route['_controller']; $conn->controller = $route['_controller'];
$conn->controller->onOpen($conn, $request); $conn->controller->onOpen($conn, $request);
@ -87,21 +88,4 @@ class Router implements HttpServerInterface {
$conn->controller->onError($conn, $e); $conn->controller->onError($conn, $e);
} }
} }
/**
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
* @param array $additionalHeaders
* @return null
*/
protected function close(ConnectionInterface $conn, $code = 400, array $additionalHeaders = array()) {
$headers = array_merge(array(
'X-Powered-By' => \Ratchet\VERSION
), $additionalHeaders);
$response = new Response($code, $headers);
$conn->send((string)$response);
$conn->close();
}
} }

View File

@ -5,6 +5,7 @@ use React\EventLoop\LoopInterface;
use React\Socket\ServerInterface; use React\Socket\ServerInterface;
use React\EventLoop\Factory as LoopFactory; use React\EventLoop\Factory as LoopFactory;
use React\Socket\Server as Reactor; use React\Socket\Server as Reactor;
use React\Socket\SecureServer as SecureReactor;
/** /**
* Creates an open-ended socket to listen on a port for incoming connections. * Creates an open-ended socket to listen on a port for incoming connections.
@ -21,12 +22,6 @@ class IoServer {
*/ */
public $app; public $app;
/**
* Array of React event handlers
* @var \SplFixedArray
*/
protected $handlers;
/** /**
* The socket server the Ratchet Application is run off of * The socket server the Ratchet Application is run off of
* @var \React\Socket\ServerInterface * @var \React\Socket\ServerInterface
@ -51,23 +46,17 @@ class IoServer {
$this->socket = $socket; $this->socket = $socket;
$socket->on('connection', array($this, 'handleConnect')); $socket->on('connection', array($this, 'handleConnect'));
$this->handlers = new \SplFixedArray(3);
$this->handlers[0] = array($this, 'handleData');
$this->handlers[1] = array($this, 'handleEnd');
$this->handlers[2] = array($this, 'handleError');
} }
/** /**
* @param \Ratchet\MessageComponentInterface $component The application that I/O will call when events are received * @param \Ratchet\MessageComponentInterface $component The application that I/O will call when events are received
* @param int $port The port to server sockets on * @param int $port The port to server sockets on
* @param string $address The address to receive sockets on (0.0.0.0 means receive connections from any) * @param string $address The address to receive sockets on (0.0.0.0 means receive connections from any)
* @return IoServer * @return 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 = LoopFactory::create();
$socket = new Reactor($loop); $socket = new Reactor($address . ':' . $port, $loop);
$socket->listen($port, $address);
return new static($component, $socket, $loop); return new static($component, $socket, $loop);
} }
@ -92,15 +81,25 @@ class IoServer {
*/ */
public function handleConnect($conn) { public function handleConnect($conn) {
$conn->decor = new IoConnection($conn); $conn->decor = new IoConnection($conn);
$conn->decor->resourceId = (int)$conn->stream;
$conn->decor->resourceId = (int)$conn->stream; $uri = $conn->getRemoteAddress();
$conn->decor->remoteAddress = $conn->getRemoteAddress(); $conn->decor->remoteAddress = trim(
parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_HOST),
'[]'
);
$this->app->onOpen($conn->decor); $this->app->onOpen($conn->decor);
$conn->on('data', $this->handlers[0]); $conn->on('data', function ($data) use ($conn) {
$conn->on('end', $this->handlers[1]); $this->handleData($data, $conn);
$conn->on('error', $this->handlers[2]); });
$conn->on('close', function () use ($conn) {
$this->handleEnd($conn);
});
$conn->on('error', function (\Exception $e) use ($conn) {
$this->handleError($e, $conn);
});
} }
/** /**

View File

@ -1,8 +1,8 @@
<?php <?php
namespace Ratchet\Session; namespace Ratchet\Session;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\WsServerInterface; use Ratchet\Http\HttpServerInterface;
use Psr\Http\Message\RequestInterface;
use Ratchet\Session\Storage\VirtualSessionStorage; use Ratchet\Session\Storage\VirtualSessionStorage;
use Ratchet\Session\Serialize\HandlerInterface; use Ratchet\Session\Serialize\HandlerInterface;
use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Session;
@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler;
* Your website must also use Symfony HttpFoundation Sessions to read your sites session data * Your website must also use Symfony HttpFoundation Sessions to read your sites session data
* If your are not using at least PHP 5.4 you must include a SessionHandlerInterface stub (is included in Symfony HttpFoundation, loaded w/ composer) * If your are not using at least PHP 5.4 you must include a SessionHandlerInterface stub (is included in Symfony HttpFoundation, loaded w/ composer)
*/ */
class SessionProvider implements MessageComponentInterface, WsServerInterface { class SessionProvider implements HttpServerInterface {
/** /**
* @var \Ratchet\MessageComponentInterface * @var \Ratchet\MessageComponentInterface
*/ */
@ -38,13 +38,13 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
protected $_serializer; protected $_serializer;
/** /**
* @param \Ratchet\MessageComponentInterface $app * @param \Ratchet\Http\HttpServerInterface $app
* @param \SessionHandlerInterface $handler * @param \SessionHandlerInterface $handler
* @param array $options * @param array $options
* @param \Ratchet\Session\Serialize\HandlerInterface $serializer * @param \Ratchet\Session\Serialize\HandlerInterface $serializer
* @throws \RuntimeException * @throws \RuntimeException
*/ */
public function __construct(MessageComponentInterface $app, \SessionHandlerInterface $handler, array $options = array(), HandlerInterface $serializer = null) { public function __construct(HttpServerInterface $app, \SessionHandlerInterface $handler, array $options = array(), HandlerInterface $serializer = null) {
$this->_app = $app; $this->_app = $app;
$this->_handler = $handler; $this->_handler = $handler;
$this->_null = new NullSessionHandler; $this->_null = new NullSessionHandler;
@ -70,8 +70,20 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
function onOpen(ConnectionInterface $conn) { public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
if (!isset($conn->WebSocket) || null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) { $sessionName = ini_get('session.name');
$id = array_reduce($request->getHeader('Cookie'), function($accumulator, $cookie) use ($sessionName) {
if ($accumulator) {
return $accumulator;
}
$crumbs = $this->parseCookie($cookie);
return isset($crumbs['cookies'][$sessionName]) ? $crumbs['cookies'][$sessionName] : false;
}, false);
if (null === $request || false === $id) {
$saveHandler = $this->_null; $saveHandler = $this->_null;
$id = ''; $id = '';
} else { } else {
@ -84,7 +96,7 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
$conn->Session->start(); $conn->Session->start();
} }
return $this->_app->onOpen($conn); return $this->_app->onOpen($conn, $request);
} }
/** /**
@ -110,17 +122,6 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
return $this->_app->onError($conn, $e); return $this->_app->onError($conn, $e);
} }
/**
* {@inheritdoc}
*/
public function getSubProtocols() {
if ($this->_app instanceof WsServerInterface) {
return $this->_app->getSubProtocols();
} else {
return array();
}
}
/** /**
* Set all the php session. ini options * Set all the php session. ini options
* © Symfony * © Symfony
@ -158,4 +159,85 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
protected function toClassCase($langDef) { protected function toClassCase($langDef) {
return str_replace(' ', '', ucwords(str_replace('_', ' ', $langDef))); return str_replace(' ', '', ucwords(str_replace('_', ' ', $langDef)));
} }
/**
* Taken from Guzzle3
*/
private static $cookieParts = array(
'domain' => 'Domain',
'path' => 'Path',
'max_age' => 'Max-Age',
'expires' => 'Expires',
'version' => 'Version',
'secure' => 'Secure',
'port' => 'Port',
'discard' => 'Discard',
'comment' => 'Comment',
'comment_url' => 'Comment-Url',
'http_only' => 'HttpOnly'
);
/**
* Taken from Guzzle3
*/
private function parseCookie($cookie, $host = null, $path = null, $decode = false) {
// Explode the cookie string using a series of semicolons
$pieces = array_filter(array_map('trim', explode(';', $cookie)));
// The name of the cookie (first kvp) must include an equal sign.
if (empty($pieces) || !strpos($pieces[0], '=')) {
return false;
}
// Create the default return array
$data = array_merge(array_fill_keys(array_keys(self::$cookieParts), null), array(
'cookies' => array(),
'data' => array(),
'path' => $path ?: '/',
'http_only' => false,
'discard' => false,
'domain' => $host
));
$foundNonCookies = 0;
// Add the cookie pieces into the parsed data array
foreach ($pieces as $part) {
$cookieParts = explode('=', $part, 2);
$key = trim($cookieParts[0]);
if (count($cookieParts) == 1) {
// Can be a single value (e.g. secure, httpOnly)
$value = true;
} else {
// Be sure to strip wrapping quotes
$value = trim($cookieParts[1], " \n\r\t\0\x0B\"");
if ($decode) {
$value = urldecode($value);
}
}
// Only check for non-cookies when cookies have been found
if (!empty($data['cookies'])) {
foreach (self::$cookieParts as $mapValue => $search) {
if (!strcasecmp($search, $key)) {
$data[$mapValue] = $mapValue == 'port' ? array_map('trim', explode(',', $value)) : $value;
$foundNonCookies++;
continue 2;
}
}
}
// If cookies have not yet been retrieved, or this value was not found in the pieces array, treat it as a
// cookie. IF non-cookies have been parsed, then this isn't a cookie, it's cookie data. Cookies then data.
$data[$foundNonCookies ? 'data' : 'cookies'][$key] = $value;
}
// Calculate the expires date
if (!$data['expires'] && $data['max_age']) {
$data['expires'] = time() + (int) $data['max_age'];
}
return $data;
}
} }

View File

@ -6,13 +6,6 @@ use Ratchet\ConnectionInterface;
* A topic/channel containing connections that have subscribed to it * A topic/channel containing connections that have subscribed to it
*/ */
class Topic implements \IteratorAggregate, \Countable { class Topic implements \IteratorAggregate, \Countable {
/**
* If true the TopicManager will destroy this object if it's ever empty of connections
* @deprecated in v0.4
* @type bool
*/
public $autoDelete = false;
private $id; private $id;
private $subscribers; private $subscribers;

View File

@ -118,7 +118,7 @@ class TopicManager implements WsServerInterface, WampServerInterface {
$this->topicLookup[$topic->getId()]->remove($conn); $this->topicLookup[$topic->getId()]->remove($conn);
if ($topic->autoDelete && 0 === $topic->count()) { if (0 === $topic->count()) {
unset($this->topicLookup[$topic->getId()]); unset($this->topicLookup[$topic->getId()]);
} }
} }

View File

@ -0,0 +1,20 @@
<?php
namespace Ratchet\WebSocket;
use Ratchet\RFC6455\Messaging\MessageBuffer;
class ConnContext {
/**
* @var \Ratchet\WebSocket\WsConnection
*/
public $connection;
/**
* @var \Ratchet\RFC6455\Messaging\MessageBuffer;
*/
public $buffer;
public function __construct(WsConnection $conn, MessageBuffer $buffer) {
$this->connection = $conn;
$this->buffer = $buffer;
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace Ratchet\WebSocket\Encoding;
class ToggleableValidator implements ValidatorInterface {
/**
* Toggle if checkEncoding checks the encoding or not
* @var bool
*/
public $on;
/**
* @var Validator
*/
private $validator;
public function __construct($on = true) {
$this->validator = new Validator;
$this->on = (boolean)$on;
}
/**
* {@inheritdoc}
*/
public function checkEncoding($str, $encoding) {
if (!(boolean)$this->on) {
return true;
}
return $this->validator->checkEncoding($str, $encoding);
}
}

View File

@ -1,93 +0,0 @@
<?php
namespace Ratchet\WebSocket\Encoding;
/**
* This class handled encoding validation
*/
class Validator {
const UTF8_ACCEPT = 0;
const UTF8_REJECT = 1;
/**
* Incremental UTF-8 validator with constant memory consumption (minimal state).
*
* Implements the algorithm "Flexible and Economical UTF-8 Decoder" by
* Bjoern Hoehrmann (http://bjoern.hoehrmann.de/utf-8/decoder/dfa/).
*/
protected static $dfa = array(
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 00..1f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 20..3f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 40..5f
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, # 60..7f
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, # 80..9f
7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, # a0..bf
8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, # c0..df
0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, # e0..ef
0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, # f0..ff
0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, # s0..s0
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, # s1..s2
1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, # s3..s4
1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, # s5..s6
1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, # s7..s8
);
/**
* Lookup if mbstring is available
* @var bool
*/
private $hasMbString = false;
/**
* Lookup if iconv is available
* @var bool
*/
private $hasIconv = false;
public function __construct() {
$this->hasMbString = extension_loaded('mbstring');
$this->hasIconv = extension_loaded('iconv');
}
/**
* @param string $str The value to check the encoding
* @param string $against The type of encoding to check against
* @return bool
*/
public function checkEncoding($str, $against) {
if ('UTF-8' == $against) {
return $this->isUtf8($str);
}
if ($this->hasMbString) {
return mb_check_encoding($str, $against);
} elseif ($this->hasIconv) {
return ($str == iconv($against, "{$against}//IGNORE", $str));
}
return true;
}
protected function isUtf8($str) {
if ($this->hasMbString) {
if (false === mb_check_encoding($str, 'UTF-8')) {
return false;
}
} elseif ($this->hasIconv) {
if ($str != iconv('UTF-8', 'UTF-8//IGNORE', $str)) {
return false;
}
}
$state = static::UTF8_ACCEPT;
for ($i = 0, $len = strlen($str); $i < $len; $i++) {
$state = static::$dfa[256 + ($state << 4) + static::$dfa[ord($str[$i])]];
if (static::UTF8_REJECT === $state) {
return false;
}
}
return true;
}
}

View File

@ -1,12 +0,0 @@
<?php
namespace Ratchet\WebSocket\Encoding;
interface ValidatorInterface {
/**
* Verify a string matches the encoding type
* @param string $str The string to check
* @param string $encoding The encoding type to check against
* @return bool
*/
function checkEncoding($str, $encoding);
}

View File

@ -0,0 +1,8 @@
<?php
namespace Ratchet\WebSocket;
use Ratchet\ConnectionInterface;
use Ratchet\RFC6455\Messaging\MessageInterface;
interface MessageCallableInterface {
public function onMessage(ConnectionInterface $conn, MessageInterface $msg);
}

View File

@ -0,0 +1,6 @@
<?php
namespace Ratchet\WebSocket;
use Ratchet\ComponentInterface;
interface MessageComponentInterface extends ComponentInterface, MessageCallableInterface {
}

View File

@ -1,28 +0,0 @@
<?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,38 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
interface FrameInterface extends DataInterface {
/**
* Add incoming data to the frame from peer
* @param string
*/
function addBuffer($buf);
/**
* Is this the final frame in a fragmented message?
* @return bool
*/
function isFinal();
/**
* Is the payload masked?
* @return bool
*/
function isMasked();
/**
* @return int
*/
function getOpcode();
/**
* @return int
*/
//function getReceivedPayloadLength();
/**
* 32-big string
* @return string
*/
function getMaskingKey();
}

View File

@ -1,120 +0,0 @@
<?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!
* Hixie76 is bad for 2 (there's more) reasons:
* 1) The handshake is done in HTTP, which includes a key for signing in the body...
* BUT there is no Length defined in the header (as per HTTP spec) so the TCP buffer can't tell when the message is done!
* 2) By nature it's insecure. Google did a test study where they were able to do a
* 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
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
*/
class Hixie76 implements VersionInterface {
/**
* {@inheritdoc}
*/
public function isProtocol(RequestInterface $request) {
return !(null === $request->getHeader('Sec-WebSocket-Key2'));
}
/**
* {@inheritdoc}
*/
public function getVersionNumber() {
return 0;
}
/**
* @param \Guzzle\Http\Message\RequestInterface $request
* @return \Guzzle\Http\Message\Response
* @throws \UnderflowException If there hasn't been enough data received
*/
public function handshake(RequestInterface $request) {
$body = substr($request->getBody(), 0, 8);
if (8 !== strlen($body)) {
throw new \UnderflowException("Not enough data received to issue challenge response");
}
$challenge = $this->sign((string)$request->getHeader('Sec-WebSocket-Key1'), (string)$request->getHeader('Sec-WebSocket-Key2'), $body);
$headers = array(
'Upgrade' => 'WebSocket'
, 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Origin' => (string)$request->getHeader('Origin')
, 'Sec-WebSocket-Location' => 'ws://' . (string)$request->getHeader('Host') . $request->getPath()
);
$response = new Response(101, $headers, $challenge);
$response->setStatus(101, 'WebSocket Protocol Handshake');
return $response;
}
/**
* {@inheritdoc}
*/
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) {
if (0 === substr_count($key, ' ')) {
return 0;
}
return preg_replace('[\D]', '', $key) / substr_count($key, ' ');
}
protected function sign($key1, $key2, $code) {
return md5(
pack('N', $this->generateKeyNumber($key1))
. pack('N', $this->generateKeyNumber($key2))
. $code
, true);
}
}

View File

@ -1,26 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\Hixie76;
use Ratchet\AbstractConnectionDecorator;
/**
* {@inheritdoc}
* @property \StdClass $WebSocket
*/
class Connection extends AbstractConnectionDecorator {
public function send($msg) {
if (!$this->WebSocket->closing) {
$this->getConnection()->send(chr(0) . $msg . chr(255));
}
return $this;
}
public function close() {
if (!$this->WebSocket->closing) {
$this->getConnection()->send(chr(255));
$this->getConnection()->close();
$this->WebSocket->closing = true;
}
}
}

View File

@ -1,86 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\Hixie76;
use Ratchet\WebSocket\Version\FrameInterface;
/**
* This does not entirely follow the protocol to spec, but (mostly) works
* Hixie76 probably should not even be supported
*/
class Frame implements FrameInterface {
/**
* @type string
*/
protected $_data = '';
/**
* {@inheritdoc}
*/
public function isCoalesced() {
return (boolean)($this->_data[0] == chr(0) && substr($this->_data, -1) == chr(255));
}
/**
* {@inheritdoc}
*/
public function addBuffer($buf) {
$this->_data .= (string)$buf;
}
/**
* {@inheritdoc}
*/
public function isFinal() {
return true;
}
/**
* {@inheritdoc}
*/
public function isMasked() {
return false;
}
/**
* {@inheritdoc}
*/
public function getOpcode() {
return 1;
}
/**
* {@inheritdoc}
*/
public function getPayloadLength() {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Not enough of the message has been buffered to determine the length of the payload');
}
return strlen($this->_data) - 2;
}
/**
* {@inheritdoc}
*/
public function getMaskingKey() {
return '';
}
/**
* {@inheritdoc}
*/
public function getPayload() {
if (!$this->isCoalesced()) {
return new \UnderflowException('Not enough data buffered to read payload');
}
return substr($this->_data, 1, strlen($this->_data) - 2);
}
public function getContents() {
return $this->_data;
}
public function extractOverflow() {
return '';
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
use Guzzle\Http\Message\RequestInterface;
class HyBi10 extends RFC6455 {
public function isProtocol(RequestInterface $request) {
$version = (int)(string)$request->getHeader('Sec-WebSocket-Version');
return ($version >= 6 && $version < 13);
}
public function getVersionNumber() {
return 6;
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
interface MessageInterface extends DataInterface {
/**
* @param FrameInterface $fragment
* @return MessageInterface
*/
function addFrame(FrameInterface $fragment);
/**
* @return int
*/
function getOpcode();
}

View File

@ -1,273 +0,0 @@
<?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 Ratchet\WebSocket\Encoding\ValidatorInterface;
use Ratchet\WebSocket\Encoding\Validator;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
/**
* The latest version of the WebSocket protocol
* @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';
/**
* @var RFC6455\HandshakeVerifier
*/
protected $_verifier;
/**
* A lookup of the valid close codes that can be sent in a frame
* @var array
*/
private $closeCodes = array();
/**
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface
*/
protected $validator;
public function __construct(ValidatorInterface $validator = null) {
$this->_verifier = new HandshakeVerifier;
$this->setCloseCodes();
if (null === $validator) {
$validator = new Validator;
}
$this->validator = $validator;
}
/**
* {@inheritdoc}
*/
public function isProtocol(RequestInterface $request) {
$version = (int)(string)$request->getHeader('Sec-WebSocket-Version');
return ($this->getVersionNumber() === $version);
}
/**
* {@inheritdoc}
*/
public function getVersionNumber() {
return 13;
}
/**
* {@inheritdoc}
*/
public function handshake(RequestInterface $request) {
if (true !== $this->_verifier->verifyAll($request)) {
return new Response(400);
}
return new Response(101, array(
'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign((string)$request->getHeader('Sec-WebSocket-Key'))
));
}
/**
* @param \Ratchet\ConnectionInterface $conn
* @param \Ratchet\MessageInterface $coalescedCallback
* @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 $from
* @param string $data
*/
public function onMessage(ConnectionInterface $from, $data) {
$overflow = '';
if (!isset($from->WebSocket->message)) {
$from->WebSocket->message = $this->newMessage();
}
// There is a frame fragment attached 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 (false !== $frame->getRsv1() ||
false !== $frame->getRsv2() ||
false !== $frame->getRsv3()
) {
return $from->close($frame::CLOSE_PROTOCOL);
}
if (!$frame->isMasked()) {
return $from->close($frame::CLOSE_PROTOCOL);
}
$opcode = $frame->getOpcode();
if ($opcode > 2) {
if ($frame->getPayloadLength() > 125 || !$frame->isFinal()) {
return $from->close($frame::CLOSE_PROTOCOL);
}
switch ($opcode) {
case $frame::OP_CLOSE:
$closeCode = 0;
$bin = $frame->getPayload();
if (empty($bin)) {
return $from->close();
}
if (strlen($bin) >= 2) {
list($closeCode) = array_merge(unpack('n*', substr($bin, 0, 2)));
}
if (!$this->isValidCloseCode($closeCode)) {
return $from->close($frame::CLOSE_PROTOCOL);
}
if (!$this->validator->checkEncoding(substr($bin, 2), 'UTF-8')) {
return $from->close($frame::CLOSE_BAD_PAYLOAD);
}
$frame->unMaskPayload();
return $from->close($frame);
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();
if ($frame::OP_CONTINUE == $frame->getOpcode() && 0 == count($from->WebSocket->message)) {
return $from->close($frame::CLOSE_PROTOCOL);
}
if (count($from->WebSocket->message) > 0 && $frame::OP_CONTINUE != $frame->getOpcode()) {
return $from->close($frame::CLOSE_PROTOCOL);
}
$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);
if (!$this->validator->checkEncoding($parsed, 'UTF-8')) {
return $from->close(Frame::CLOSE_BAD_PAYLOAD);
}
$from->WebSocket->coalescedCallback->onMessage($from, $parsed);
}
if (strlen($overflow) > 0) {
$this->onMessage($from, $overflow);
}
}
/**
* @return RFC6455\Message
*/
public function newMessage() {
return new Message;
}
/**
* @param string|null $payload
* @param bool|null $final
* @param int|null $opcode
* @return RFC6455\Frame
*/
public function newFrame($payload = null, $final = null, $opcode = null) {
return new Frame($payload, $final, $opcode);
}
/**
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language
* @param string $key
* @return string
* @internal
*/
public function sign($key) {
return base64_encode(sha1($key . static::GUID, true));
}
/**
* Determine if a close code is valid
* @param int|string
* @return bool
*/
public function isValidCloseCode($val) {
if (array_key_exists($val, $this->closeCodes)) {
return true;
}
if ($val >= 3000 && $val <= 4999) {
return true;
}
return false;
}
/**
* Creates a private lookup of valid, private close codes
*/
protected function setCloseCodes() {
$this->closeCodes[Frame::CLOSE_NORMAL] = true;
$this->closeCodes[Frame::CLOSE_GOING_AWAY] = true;
$this->closeCodes[Frame::CLOSE_PROTOCOL] = true;
$this->closeCodes[Frame::CLOSE_BAD_DATA] = true;
//$this->closeCodes[Frame::CLOSE_NO_STATUS] = true;
//$this->closeCodes[Frame::CLOSE_ABNORMAL] = true;
$this->closeCodes[Frame::CLOSE_BAD_PAYLOAD] = true;
$this->closeCodes[Frame::CLOSE_POLICY] = true;
$this->closeCodes[Frame::CLOSE_TOO_BIG] = true;
$this->closeCodes[Frame::CLOSE_MAND_EXT] = true;
$this->closeCodes[Frame::CLOSE_SRV_ERR] = true;
//$this->closeCodes[Frame::CLOSE_TLS] = true;
}
}

View File

@ -1,451 +0,0 @@
<?php
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 = '';
/**
* Number of bytes received from the frame
* @var int
*/
public $bytesRecvd = 0;
/**
* Number of bytes in the payload (as per framing protocol)
* @var int
*/
protected $defPayLen = -1;
/**
* If the frame is coalesced this is true
* This is to prevent doing math every time ::isCoalesced is called
* @var boolean
*/
private $isCoalesced = false;
/**
* The unpacked first byte of the frame
* @var int
*/
protected $firstByte = -1;
/**
* The unpacked second byte of the frame
* @var int
*/
protected $secondByte = -1;
/**
* @param string|null $payload
* @param bool $final
* @param int $opcode
*/
public function __construct($payload = null, $final = true, $opcode = 1) {
if (null === $payload) {
return;
}
$this->defPayLen = strlen($payload);
$this->firstByte = ($final ? 128 : 0) + $opcode;
$this->secondByte = $this->defPayLen;
$this->isCoalesced = true;
$ext = '';
if ($this->defPayLen > 65535) {
$ext = pack('NN', 0, $this->defPayLen);
$this->secondByte = 127;
} elseif ($this->defPayLen > 125) {
$ext = pack('n', $this->defPayLen);
$this->secondByte = 126;
}
$this->data = chr($this->firstByte) . chr($this->secondByte) . $ext . $payload;
$this->bytesRecvd = 2 + strlen($ext) + $this->defPayLen;
}
/**
* {@inheritdoc}
*/
public function isCoalesced() {
if (true === $this->isCoalesced) {
return true;
}
try {
$payload_length = $this->getPayloadLength();
$payload_start = $this->getPayloadStartingByte();
} catch (\UnderflowException $e) {
return false;
}
$this->isCoalesced = $this->bytesRecvd >= $payload_length + $payload_start;
return $this->isCoalesced;
}
/**
* {@inheritdoc}
*/
public function addBuffer($buf) {
$len = strlen($buf);
$this->data .= $buf;
$this->bytesRecvd += $len;
if ($this->firstByte === -1 && $this->bytesRecvd !== 0) {
$this->firstByte = ord($this->data[0]);
}
if ($this->secondByte === -1 && $this->bytesRecvd >= 2) {
$this->secondByte = ord($this->data[1]);
}
}
/**
* {@inheritdoc}
*/
public function isFinal() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine if this is the final frame in message');
}
return 128 === ($this->firstByte & 128);
}
/**
* @return boolean
* @throws \UnderflowException
*/
public function getRsv1() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
}
return 64 === ($this->firstByte & 64);
}
/**
* @return boolean
* @throws \UnderflowException
*/
public function getRsv2() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
}
return 32 === ($this->firstByte & 32);
}
/**
* @return boolean
* @throws \UnderflowException
*/
public function getRsv3() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine reserved bit');
}
return 16 == ($this->firstByte & 16);
}
/**
* {@inheritdoc}
*/
public function isMasked() {
if (-1 === $this->secondByte) {
throw new \UnderflowException("Not enough bytes received ({$this->bytesRecvd}) to determine if mask is set");
}
return 128 === ($this->secondByte & 128);
}
/**
* {@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);
}
/**
* Create a 4 byte masking key
* @return string
*/
public function generateMaskingKey() {
$mask = '';
for ($i = 1; $i <= static::MASK_LENGTH; $i++) {
$mask .= chr(rand(32, 126));
}
return $mask;
}
/**
* Apply a mask to the payload
* @param string|null If NULL is passed a masking key will be generated
* @throws \OutOfBoundsException
* @throws \InvalidArgumentException If there is an issue with the given masking key
* @return Frame
*/
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 (extension_loaded('mbstring') && true !== mb_check_encoding($maskingKey, 'US-ASCII')) {
throw new \OutOfBoundsException("Masking key MUST be ASCII");
}
$this->unMaskPayload();
$this->secondByte = $this->secondByte | 128;
$this->data[1] = chr($this->secondByte);
$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->isCoalesced()) {
throw new \UnderflowException('Frame must be coalesced before applying mask');
}
if (!$this->isMasked()) {
return $this;
}
$maskingKey = $this->getMaskingKey();
$this->secondByte = $this->secondByte & ~128;
$this->data[1] = chr($this->secondByte);
$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;
}
/**
* Apply a mask to a string or the payload of the instance
* @param string $maskingKey The 4 character masking key to be applied
* @param string|null $payload A string to mask or null to use the payload
* @throws \UnderflowException If using the payload but enough hasn't been buffered
* @return string The masked string
*/
public 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 .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH];
}
return $applied;
}
/**
* {@inheritdoc}
*/
public function getOpcode() {
if (-1 === $this->firstByte) {
throw new \UnderflowException('Not enough bytes received to determine opcode');
}
return ($this->firstByte & ~240);
}
/**
* Gets the decimal value of bits 9 (10th) through 15 inclusive
* @return int
* @throws \UnderflowException If the buffer doesn't have enough data to determine this
*/
protected function getFirstPayloadVal() {
if (-1 === $this->secondByte) {
throw new \UnderflowException('Not enough bytes received');
}
return $this->secondByte & 127;
}
/**
* @return int (7|23|71) Number of bits defined for the payload length in the fame
* @throws \UnderflowException
*/
protected function getNumPayloadBits() {
if (-1 === $this->secondByte) {
throw new \UnderflowException('Not enough bytes received');
}
// By default 7 bits are used to describe the payload length
// These are bits 9 (10th) through 15 inclusive
$bits = 7;
// Get the value of those bits
$check = $this->getFirstPayloadVal();
// If the value is 126 the 7 bits plus the next 16 are used to describe the payload length
if ($check >= 126) {
$bits += 16;
}
// If the value of the initial payload length are is 127 an additional 48 bits are used to describe length
// Note: The documentation specifies the length is to be 63 bits, but I think that's a typo and is 64 (16+48)
if ($check === 127) {
$bits += 48;
}
return $bits;
}
/**
* This just returns the number of bytes used in the frame to describe the payload length (as opposed to # of bits)
* @see getNumPayloadBits
*/
protected function getNumPayloadBytes() {
return (1 + $this->getNumPayloadBits()) / 8;
}
/**
* {@inheritdoc}
*/
public function getPayloadLength() {
if ($this->defPayLen !== -1) {
return $this->defPayLen;
}
$this->defPayLen = $this->getFirstPayloadVal();
if ($this->defPayLen <= 125) {
return $this->getPayloadLength();
}
$byte_length = $this->getNumPayloadBytes();
if ($this->bytesRecvd < 1 + $byte_length) {
$this->defPayLen = -1;
throw new \UnderflowException('Not enough data buffered to determine payload length');
}
$len = 0;
for ($i = 2; $i <= $byte_length; $i++) {
$len <<= 8;
$len += ord($this->data[$i]);
}
$this->defPayLen = $len;
return $this->getPayloadLength();
}
/**
* {@inheritdoc}
*/
public function getPayloadStartingByte() {
return 1 + $this->getNumPayloadBytes() + ($this->isMasked() ? static::MASK_LENGTH : 0);
}
/**
* {@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 = substr($this->data, $this->getPayloadStartingByte(), $this->getPayloadLength());
if ($this->isMasked()) {
$payload = $this->applyMask($this->getMaskingKey(), $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 concatenate 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

@ -1,137 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Guzzle\Http\Message\RequestInterface;
/**
* These are checks to ensure the client requested handshake are valid
* Verification rules come from section 4.2.1 of the RFC6455 document
* @todo Currently just returning invalid - should consider returning appropriate HTTP status code error #s
*/
class HandshakeVerifier {
/**
* Given an array of the headers this method will run through all verification methods
* @param \Guzzle\Http\Message\RequestInterface $request
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid
*/
public function verifyAll(RequestInterface $request) {
$passes = 0;
$passes += (int)$this->verifyMethod($request->getMethod());
$passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion());
$passes += (int)$this->verifyRequestURI($request->getPath());
$passes += (int)$this->verifyHost((string)$request->getHeader('Host'));
$passes += (int)$this->verifyUpgradeRequest((string)$request->getHeader('Upgrade'));
$passes += (int)$this->verifyConnection((string)$request->getHeader('Connection'));
$passes += (int)$this->verifyKey((string)$request->getHeader('Sec-WebSocket-Key'));
//$passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); // Temporarily breaking functionality
return (7 === $passes);
}
/**
* Test the HTTP method. MUST be "GET"
* @param string
* @return bool
*/
public function verifyMethod($val) {
return ('get' === strtolower($val));
}
/**
* Test the HTTP version passed. MUST be 1.1 or greater
* @param string|int
* @return bool
*/
public function verifyHTTPVersion($val) {
return (1.1 <= (double)$val);
}
/**
* @param string
* @return bool
*/
public function verifyRequestURI($val) {
if ($val[0] != '/') {
return false;
}
if (false !== strstr($val, '#')) {
return false;
}
if (!extension_loaded('mbstring')) {
return true;
}
return mb_check_encoding($val, 'US-ASCII');
}
/**
* @param string|null
* @return bool
* @todo Find out if I can find the master socket, ensure the port is attached to header if not 80 or 443 - not sure if this is possible, as I tried to hide it
* @todo Once I fix HTTP::getHeaders just verify this isn't NULL or empty...or maybe need to verify it's a valid domain??? Or should it equal $_SERVER['HOST'] ?
*/
public function verifyHost($val) {
return (null !== $val);
}
/**
* Verify the Upgrade request to WebSockets.
* @param string $val MUST equal "websocket"
* @return bool
*/
public function verifyUpgradeRequest($val) {
return ('websocket' === strtolower($val));
}
/**
* Verify the Connection header
* @param string $val MUST equal "Upgrade"
* @return bool
*/
public function verifyConnection($val) {
$val = strtolower($val);
if ('upgrade' === $val) {
return true;
}
$vals = explode(',', str_replace(', ', ',', $val));
return (false !== array_search('upgrade', $vals));
}
/**
* This function verifies the nonce is valid (64 big encoded, 16 bytes random string)
* @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 === strlen(base64_decode((string)$val)));
}
/**
* Verify the version passed matches this RFC
* @param string|int MUST equal 13|"13"
* @return bool
* @todo Ran in to a problem here...I'm having HyBi use the RFC files, this breaks it! oops
*/
public function verifyVersion($val) {
return (13 === (int)$val);
}
/**
* @todo Write logic for this method. See section 4.2.1.8
*/
public function verifyProtocol($val) {
}
/**
* @todo Write logic for this method. See section 4.2.1.9
*/
public function verifyExtensions($val) {
}
}

View File

@ -1,107 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\MessageInterface;
use Ratchet\WebSocket\Version\FrameInterface;
class Message implements MessageInterface, \Countable {
/**
* @var \SplDoublyLinkedList
*/
protected $_frames;
public function __construct() {
$this->_frames = new \SplDoublyLinkedList;
}
/**
* {@inheritdoc}
*/
public function count() {
return count($this->_frames);
}
/**
* {@inheritdoc}
*/
public function isCoalesced() {
if (count($this->_frames) == 0) {
return false;
}
$last = $this->_frames->top();
return ($last->isCoalesced() && $last->isFinal());
}
/**
* {@inheritdoc}
* @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;
}
/**
* {@inheritdoc}
*/
public function getOpcode() {
if (count($this->_frames) == 0) {
throw new \UnderflowException('No frames have been added to this message');
}
return $this->_frames->bottom()->getOpcode();
}
/**
* {@inheritdoc}
*/
public function getPayloadLength() {
$len = 0;
foreach ($this->_frames as $frame) {
try {
$len += $frame->getPayloadLength();
} catch (\UnderflowException $e) {
// Not an error, want the current amount buffered
}
}
return $len;
}
/**
* {@inheritdoc}
*/
public function getPayload() {
if (!$this->isCoalesced()) {
throw new \UnderflowException('Message has not been put back together yet');
}
$buffer = '';
foreach ($this->_frames as $frame) {
$buffer .= $frame->getPayload();
}
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,57 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
use Ratchet\MessageInterface;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface;
/**
* A standard interface for interacting with the various version of the WebSocket protocol
*/
interface VersionInterface extends MessageInterface {
/**
* Given an HTTP header, determine if this version should handle the protocol
* @param \Guzzle\Http\Message\RequestInterface $request
* @return bool
* @throws \UnderflowException If the protocol thinks the headers are still fragmented
*/
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 $request
* @return \Guzzle\Http\Message\Response
* @throws \UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version)
*/
function handshake(RequestInterface $request);
/**
* @param \Ratchet\ConnectionInterface $conn
* @param \Ratchet\MessageInterface $coalescedCallback
* @return \Ratchet\ConnectionInterface
*/
function upgradeConnection(ConnectionInterface $conn, MessageInterface $coalescedCallback);
/**
* @return MessageInterface
*/
//function newMessage();
/**
* @return FrameInterface
*/
//function newFrame();
/**
* @param string
* @param bool
* @return string
* @todo Change to use other classes, this will be removed eventually
*/
//function frame($message, $mask = true);
}

View File

@ -1,90 +0,0 @@
<?php
namespace Ratchet\WebSocket;
use Ratchet\WebSocket\Version\VersionInterface;
use Guzzle\Http\Message\RequestInterface;
/**
* Manage the various versions of the WebSocket protocol
* This accepts interfaces of versions to enable/disable
*/
class VersionManager {
/**
* The header string to let clients know which versions are supported
* @var string
*/
private $versionString = '';
/**
* Storage of each version enabled
* @var array
*/
protected $versions = array();
/**
* Get the protocol negotiator for the request, if supported
* @param \Guzzle\Http\Message\RequestInterface $request
* @throws \InvalidArgumentException
* @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\Version\VersionInterface $version
* @return VersionManager
*/
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 $versionId The version ID to un-support
* @return VersionManager
*/
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,13 +1,14 @@
<?php <?php
namespace Ratchet\WebSocket\Version\RFC6455; namespace Ratchet\WebSocket;
use Ratchet\AbstractConnectionDecorator; use Ratchet\AbstractConnectionDecorator;
use Ratchet\WebSocket\Version\DataInterface; use Ratchet\RFC6455\Messaging\DataInterface;
use Ratchet\RFC6455\Messaging\Frame;
/** /**
* {@inheritdoc} * {@inheritdoc}
* @property \StdClass $WebSocket * @property \StdClass $WebSocket
*/ */
class Connection extends AbstractConnectionDecorator { class WsConnection extends AbstractConnectionDecorator {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
@ -24,7 +25,7 @@ class Connection extends AbstractConnectionDecorator {
} }
/** /**
* {@inheritdoc} * @param int|\Ratchet\RFC6455\Messaging\DataInterface
*/ */
public function close($code = 1000) { public function close($code = 1000) {
if ($this->WebSocket->closing) { if ($this->WebSocket->closing) {

View File

@ -1,12 +1,20 @@
<?php <?php
namespace Ratchet\WebSocket; namespace Ratchet\WebSocket;
use Ratchet\MessageComponentInterface; use Ratchet\ComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface as DataComponentInterface;
use Ratchet\Http\HttpServerInterface; use Ratchet\Http\HttpServerInterface;
use Guzzle\Http\Message\RequestInterface; use Ratchet\Http\CloseResponseTrait;
use Guzzle\Http\Message\Response; use Psr\Http\Message\RequestInterface;
use Ratchet\WebSocket\Version; use Ratchet\RFC6455\Messaging\MessageInterface;
use Ratchet\WebSocket\Encoding\ToggleableValidator; use Ratchet\RFC6455\Messaging\FrameInterface;
use Ratchet\RFC6455\Messaging\Frame;
use Ratchet\RFC6455\Messaging\MessageBuffer;
use Ratchet\RFC6455\Messaging\CloseFrameChecker;
use Ratchet\RFC6455\Handshake\ServerNegotiator;
use Ratchet\RFC6455\Handshake\RequestVerifier;
use React\EventLoop\LoopInterface;
use GuzzleHttp\Psr7 as gPsr;
/** /**
* The adapter to handle WebSocket requests/responses * The adapter to handle WebSocket requests/responses
@ -15,18 +23,13 @@ use Ratchet\WebSocket\Encoding\ToggleableValidator;
* @link http://dev.w3.org/html5/websockets/ * @link http://dev.w3.org/html5/websockets/
*/ */
class WsServer implements HttpServerInterface { class WsServer implements HttpServerInterface {
/** use CloseResponseTrait;
* 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 * @var \Ratchet\ComponentInterface
*/ */
public $component; private $delegate;
/** /**
* @var \SplObjectStorage * @var \SplObjectStorage
@ -34,38 +37,68 @@ class WsServer implements HttpServerInterface {
protected $connections; protected $connections;
/** /**
* Holder of accepted protocols, implement through WampServerInterface * @var \Ratchet\RFC6455\Messaging\CloseFrameChecker
*/ */
protected $acceptedSubProtocols = array(); private $closeFrameChecker;
/** /**
* UTF-8 validator * @var \Ratchet\RFC6455\Handshake\ServerNegotiator
* @var \Ratchet\WebSocket\Encoding\ValidatorInterface
*/ */
protected $validator; private $handshakeNegotiator;
/** /**
* Flag if we have checked the decorated component for sub-protocols * @var \Closure
* @var boolean
*/ */
private $isSpGenerated = false; private $ueFlowFactory;
/** /**
* @param \Ratchet\MessageComponentInterface $component Your application to run with WebSockets * @var \Closure
* If you want to enable sub-protocols have your component implement WsServerInterface as well
*/ */
public function __construct(MessageComponentInterface $component) { private $pongReceiver;
$this->versioner = new VersionManager;
$this->validator = new ToggleableValidator;
$this->versioner /**
->enableVersion(new Version\RFC6455($this->validator)) * @var \Closure
->enableVersion(new Version\HyBi10($this->validator)) */
->enableVersion(new Version\Hixie76) private $msgCb;
;
$this->component = $component; /**
* @param \Ratchet\WebSocket\MessageComponentInterface|\Ratchet\MessageComponentInterface $component Your application to run with WebSockets
* @note If you want to enable sub-protocols have your component implement WsServerInterface as well
*/
public function __construct(ComponentInterface $component) {
if ($component instanceof MessageComponentInterface) {
$this->msgCb = function(ConnectionInterface $conn, MessageInterface $msg) {
$this->delegate->onMessage($conn, $msg);
};
} elseif ($component instanceof DataComponentInterface) {
$this->msgCb = function(ConnectionInterface $conn, MessageInterface $msg) {
$this->delegate->onMessage($conn, $msg->getPayload());
};
} else {
throw new \UnexpectedValueException('Expected instance of \Ratchet\WebSocket\MessageComponentInterface or \Ratchet\MessageComponentInterface');
}
if (bin2hex('✓') !== 'e29c93') {
throw new \DomainException('Bad encoding, unicode character ✓ did not match expected value. Ensure charset UTF-8 and check ini val mbstring.func_autoload');
}
$this->delegate = $component;
$this->connections = new \SplObjectStorage; $this->connections = new \SplObjectStorage;
$this->closeFrameChecker = new CloseFrameChecker;
$this->handshakeNegotiator = new ServerNegotiator(new RequestVerifier);
$this->handshakeNegotiator->setStrictSubProtocolCheck(true);
if ($component instanceof WsServerInterface) {
$this->handshakeNegotiator->setSupportedSubProtocols($component->getSubProtocols());
}
$this->pongReceiver = function() {};
$reusableUnderflowException = new \UnderflowException;
$this->ueFlowFactory = function() use ($reusableUnderflowException) {
return $reusableUnderflowException;
};
} }
/** /**
@ -76,12 +109,37 @@ class WsServer implements HttpServerInterface {
throw new \UnexpectedValueException('$request can not be null'); throw new \UnexpectedValueException('$request can not be null');
} }
$conn->WebSocket = new \StdClass; $conn->httpRequest = $request;
$conn->WebSocket->request = $request;
$conn->WebSocket->established = false;
$conn->WebSocket->closing = false;
$this->attemptUpgrade($conn); $conn->WebSocket = new \StdClass;
$conn->WebSocket->closing = false;
$response = $this->handshakeNegotiator->handshake($request)->withHeader('X-Powered-By', \Ratchet\VERSION);
$conn->send(gPsr\str($response));
if (101 !== $response->getStatusCode()) {
return $conn->close();
}
$wsConn = new WsConnection($conn);
$streamer = new MessageBuffer(
$this->closeFrameChecker,
function(MessageInterface $msg) use ($wsConn) {
$cb = $this->msgCb;
$cb($wsConn, $msg);
},
function(FrameInterface $frame) use ($wsConn) {
$this->onControlFrame($frame, $wsConn);
},
true,
$this->ueFlowFactory
);
$this->connections->attach($conn, new ConnContext($wsConn, $streamer));
return $this->delegate->onOpen($wsConn);
} }
/** /**
@ -92,50 +150,7 @@ class WsServer implements HttpServerInterface {
return; return;
} }
if (true === $from->WebSocket->established) { $this->connections[$from]->buffer->onData($msg);
return $from->WebSocket->version->onMessage($this->connections[$from], $msg);
}
$this->attemptUpgrade($from, $msg);
}
protected function attemptUpgrade(ConnectionInterface $conn, $data = '') {
if ('' !== $data) {
$conn->WebSocket->request->getBody()->write($data);
}
if (!$this->versioner->isVersionEnabled($conn->WebSocket->request)) {
return $this->close($conn);
}
$conn->WebSocket->version = $this->versioner->getVersion($conn->WebSocket->request);
try {
$response = $conn->WebSocket->version->handshake($conn->WebSocket->request);
} catch (\UnderflowException $e) {
return;
}
if (null !== ($subHeader = $conn->WebSocket->request->getHeader('Sec-WebSocket-Protocol'))) {
if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($subHeader->normalize()))) {
$response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols);
}
}
$response->setHeader('X-Powered-By', \Ratchet\VERSION);
$conn->send((string)$response);
if (101 != $response->getStatusCode()) {
return $conn->close();
}
$upgraded = $conn->WebSocket->version->upgradeConnection($conn, $this->component);
$this->connections->attach($conn, $upgraded);
$upgraded->WebSocket->established = true;
return $this->component->onOpen($upgraded);
} }
/** /**
@ -143,10 +158,10 @@ class WsServer implements HttpServerInterface {
*/ */
public function onClose(ConnectionInterface $conn) { public function onClose(ConnectionInterface $conn) {
if ($this->connections->contains($conn)) { if ($this->connections->contains($conn)) {
$decor = $this->connections[$conn]; $context = $this->connections[$conn];
$this->connections->detach($conn); $this->connections->detach($conn);
$this->component->onClose($decor); $this->delegate->onClose($context->connection);
} }
} }
@ -154,79 +169,57 @@ class WsServer implements HttpServerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onError(ConnectionInterface $conn, \Exception $e) { public function onError(ConnectionInterface $conn, \Exception $e) {
if ($conn->WebSocket->established && $this->connections->contains($conn)) { if ($this->connections->contains($conn)) {
$this->component->onError($this->connections[$conn], $e); $this->delegate->onError($this->connections[$conn]->connection, $e);
} else { } else {
$conn->close(); $conn->close();
} }
} }
/** public function onControlFrame(FrameInterface $frame, WsConnection $conn) {
* Disable a specific version of the WebSocket protocol switch ($frame->getOpCode()) {
* @param int $versionId Version ID to disable case Frame::OP_CLOSE:
* @return WsServer $conn->close($frame);
*/ break;
public function disableVersion($versionId) { case Frame::OP_PING:
$this->versioner->disableVersion($versionId); $conn->send(new Frame($frame->getPayload(), true, Frame::OP_PONG));
break;
return $this; case Frame::OP_PONG:
} $pongReceiver = $this->pongReceiver;
$pongReceiver($frame, $conn);
/** break;
* Toggle weather to check encoding of incoming messages
* @param bool
* @return WsServer
*/
public function setEncodingChecks($opt) {
$this->validator->on = (boolean)$opt;
return $this;
}
/**
* @param string
* @return boolean
*/
public function isSubProtocolSupported($name) {
if (!$this->isSpGenerated) {
if ($this->component instanceof WsServerInterface) {
$this->acceptedSubProtocols = array_flip($this->component->getSubProtocols());
}
$this->isSpGenerated = true;
} }
return array_key_exists($name, $this->acceptedSubProtocols);
} }
/** public function setStrictSubProtocolCheck($enable) {
* @param \Traversable|null $requested $this->handshakeNegotiator->setStrictSubProtocolCheck($enable);
* @return string }
*/
protected function getSubProtocolString(\Traversable $requested = null) { public function enableKeepAlive(LoopInterface $loop, $interval = 30) {
if (null !== $requested) { $lastPing = new Frame(uniqid(), true, Frame::OP_PING);
foreach ($requested as $sub) { $pingedConnections = new \SplObjectStorage;
if ($this->isSubProtocolSupported($sub)) { $splClearer = new \SplObjectStorage;
return $sub;
} $this->pongReceiver = function(FrameInterface $frame, $wsConn) use ($pingedConnections, &$lastPing) {
if ($frame->getPayload() === $lastPing->getPayload()) {
$pingedConnections->detach($wsConn);
} }
} };
return ''; $loop->addPeriodicTimer((int)$interval, function() use ($pingedConnections, &$lastPing, $splClearer) {
} foreach ($pingedConnections as $wsConn) {
$wsConn->close();
}
$pingedConnections->removeAllExcept($splClearer);
/** $lastPing = new Frame(uniqid(), true, Frame::OP_PING);
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
*/
protected function close(ConnectionInterface $conn, $code = 400) {
$response = new Response($code, array(
'Sec-WebSocket-Version' => $this->versioner->getSupportedVersionString()
, 'X-Powered-By' => \Ratchet\VERSION
));
$conn->send((string)$response); foreach ($this->connections as $key => $conn) {
$conn->close(); $wsConn = $this->connections[$conn]->connection;
}
$wsConn->send($lastPing);
$pingedConnections->attach($wsConn);
}
});
}
} }

View File

@ -1,17 +0,0 @@
<?php
require dirname(dirname(dirname(__DIR__))) . '/vendor/autoload.php';
$port = $argc > 1 ? $argv[1] : 8000;
$impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect');
$loop = new $impl;
$sock = new React\Socket\Server($loop);
$web = new Ratchet\WebSocket\WsServer(new Ratchet\Server\EchoServer);
$app = new Ratchet\Http\HttpServer($web);
$web->setEncodingChecks(false);
$sock->listen($port, '0.0.0.0');
$server = new Ratchet\Server\IoServer($app, $sock, $loop);
$server->run();

View File

@ -1,15 +1,36 @@
<?php <?php
use Ratchet\ConnectionInterface;
require dirname(dirname(dirname(__DIR__))) . '/vendor/autoload.php'; require dirname(dirname(dirname(__DIR__))) . '/vendor/autoload.php';
class BinaryEcho implements \Ratchet\WebSocket\MessageComponentInterface {
public function onMessage(ConnectionInterface $from, \Ratchet\RFC6455\Messaging\MessageInterface $msg) {
$from->send($msg);
}
public function onOpen(ConnectionInterface $conn) {
}
public function onClose(ConnectionInterface $conn) {
}
public function onError(ConnectionInterface $conn, \Exception $e) {
}
}
$port = $argc > 1 ? $argv[1] : 8000; $port = $argc > 1 ? $argv[1] : 8000;
$impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect'); $impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect');
$loop = new $impl; $loop = new $impl;
$sock = new React\Socket\Server($loop); $sock = new React\Socket\Server('0.0.0.0:' . $port, $loop);
$app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Server\EchoServer));
$sock->listen($port, '0.0.0.0'); $wsServer = new Ratchet\WebSocket\WsServer(new BinaryEcho);
// This is enabled to test https://github.com/ratchetphp/Ratchet/issues/430
// The time is left at 10 minutes so that it will not try to every ping anything
// This causes the Ratchet server to crash on test 2.7
$wsServer->enableKeepAlive($loop, 600);
$app = new Ratchet\Http\HttpServer($wsServer);
$server = new Ratchet\Server\IoServer($app, $sock, $loop); $server = new Ratchet\Server\IoServer($app, $sock, $loop);
$server->run(); $server->run();

View File

@ -3,14 +3,13 @@
, "outdir": "reports/ab" , "outdir": "reports/ab"
, "servers": [ , "servers": [
{"agent": "Ratchet/0.3 libevent", "url": "ws://localhost:8001", "options": {"version": 18}} {"agent": "Ratchet/0.4 libevent", "url": "ws://localhost:8001", "options": {"version": 18}}
, {"agent": "Ratchet/0.3 libev", "url": "ws://localhost:8004", "options": {"version": 18}} , {"agent": "Ratchet/0.4 libev", "url": "ws://localhost:8004", "options": {"version": 18}}
, {"agent": "Ratchet/0.3 streams", "url": "ws://localhost:8002", "options": {"version": 18}} , {"agent": "Ratchet/0.4 streams", "url": "ws://localhost:8002", "options": {"version": 18}}
, {"agent": "Ratchet/0.3 -utf8", "url": "ws://localhost:8003", "options": {"version": 18}}
, {"agent": "AutobahnTestSuite/0.5.9", "url": "ws://localhost:8000", "options": {"version": 18}} , {"agent": "AutobahnTestSuite/0.5.9", "url": "ws://localhost:8000", "options": {"version": 18}}
] ]
, "cases": ["*"] , "cases": ["*"]
, "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] , "exclude-cases": []
, "exclude-agent-cases": {} , "exclude-agent-cases": {}
} }

View File

@ -7,6 +7,6 @@
] ]
, "cases": ["*"] , "cases": ["*"]
, "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] , "exclude-cases": []
, "exclude-agent-cases": {} , "exclude-agent-cases": {}
} }

View File

@ -1,53 +0,0 @@
<?php
use Guzzle\Http\Message\Request;
class GuzzleTest extends \PHPUnit_Framework_TestCase {
protected $_request;
protected $_headers = array(
'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade'
, 'Host' => 'localhost:8080'
, 'Origin' => 'chrome://newtab'
, 'Sec-WebSocket-Protocol' => 'one, two, three'
, 'Sec-WebSocket-Key' => '9bnXNp3ae6FbFFRtPdiPXA=='
, 'Sec-WebSocket-Version' => '13'
);
public function setUp() {
$this->_request = new Request('GET', 'http://localhost', $this->_headers);
}
public function testGetHeaderString() {
$this->assertEquals('Upgrade', (string)$this->_request->getHeader('connection'));
$this->assertEquals('9bnXNp3ae6FbFFRtPdiPXA==', (string)$this->_request->getHeader('Sec-Websocket-Key'));
}
public function testGetHeaderInteger() {
$this->assertSame('13', (string)$this->_request->getHeader('Sec-Websocket-Version'));
$this->assertSame(13, (int)(string)$this->_request->getHeader('Sec-WebSocket-Version'));
}
public function testGetHeaderObject() {
$this->assertInstanceOf('Guzzle\Http\Message\Header', $this->_request->getHeader('Origin'));
$this->assertNull($this->_request->getHeader('Non-existant-header'));
}
public function testHeaderObjectNormalizeValues() {
$expected = 1 + substr_count($this->_headers['Sec-WebSocket-Protocol'], ',');
$protocols = $this->_request->getHeader('Sec-WebSocket-Protocol')->normalize();
$count = 0;
foreach ($protocols as $protocol) {
$count++;
}
$this->assertEquals($expected, $count);
$this->assertEquals($expected, count($protocols));
}
public function testRequestFactoryCreateSignature() {
$ref = new \ReflectionMethod('Guzzle\Http\Message\RequestFactory', 'create');
$this->assertEquals(2, $ref->getNumberOfRequiredParameters());
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace Ratchet\Http\Guzzle\Http\Message;
use Ratchet\Http\Guzzle\Http\Message\RequestFactory;
/**
* @covers Ratchet\Http\Guzzle\Http\Message\RequestFactory
*/
class RequestFactoryTest extends \PHPUnit_Framework_TestCase {
protected $factory;
public function setUp() {
$this->factory = RequestFactory::getInstance();
}
public function testMessageProvider() {
return array(
'status' => 'GET / HTTP/1.1'
, 'headers' => array(
'Upgrade' => 'WebSocket'
, 'Connection' => 'Upgrade'
, 'Host' => 'localhost:8000'
, 'Sec-WebSocket-Key1' => '> b3lU Z0 fh f 3+83394 6 (zG4'
, 'Sec-WebSocket-Key2' => ',3Z0X0677 dV-d [159 Z*4'
)
, 'body' => "123456\r\n\r\n"
);
}
public function combineMessage($status, array $headers, $body = '') {
$message = $status . "\r\n";
foreach ($headers as $key => $val) {
$message .= "{$key}: {$val}\r\n";
}
$message .= "\r\n{$body}";
return $message;
}
public function testExpectedDataFromGuzzleHeaders() {
$parts = $this->testMessageProvider();
$message = $this->combineMessage($parts['status'], $parts['headers'], $parts['body']);
$object = $this->factory->fromMessage($message);
foreach ($parts['headers'] as $key => $val) {
$this->assertEquals($val, $object->getHeader($key, true));
}
}
public function testExpectedDataFromNonGuzzleHeaders() {
$parts = $this->testMessageProvider();
$message = $this->combineMessage($parts['status'], $parts['headers'], $parts['body']);
$object = $this->factory->fromMessage($message);
$this->assertNull($object->getHeader('Nope', true));
$this->assertNull($object->getHeader('Nope'));
}
public function testExpectedDataFromNonGuzzleBody() {
$parts = $this->testMessageProvider();
$message = $this->combineMessage($parts['status'], $parts['headers'], $parts['body']);
$object = $this->factory->fromMessage($message);
$this->assertEquals($parts['body'], (string)$object->getBody());
}
}

View File

@ -1,6 +1,5 @@
<?php <?php
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\Http\HttpRequestParser;
/** /**
* @covers Ratchet\Http\HttpRequestParser * @covers Ratchet\Http\HttpRequestParser
@ -46,6 +45,6 @@ class HttpRequestParserTest extends \PHPUnit_Framework_TestCase {
$conn = $this->getMock('\Ratchet\ConnectionInterface'); $conn = $this->getMock('\Ratchet\ConnectionInterface');
$return = $this->parser->onMessage($conn, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n"); $return = $this->parser->onMessage($conn, "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n");
$this->assertInstanceOf('\Guzzle\Http\Message\RequestInterface', $return); $this->assertInstanceOf('\Psr\Http\Message\RequestInterface', $return);
} }
} }

View File

@ -9,8 +9,8 @@ class OriginCheckTest extends AbstractMessageComponentTestCase {
protected $_reqStub; protected $_reqStub;
public function setUp() { public function setUp() {
$this->_reqStub = $this->getMock('Guzzle\Http\Message\RequestInterface'); $this->_reqStub = $this->getMock('Psr\Http\Message\RequestInterface');
$this->_reqStub->expects($this->any())->method('getHeader')->will($this->returnValue('localhost')); $this->_reqStub->expects($this->any())->method('getHeader')->will($this->returnValue(['localhost']));
parent::setUp(); parent::setUp();
@ -34,7 +34,7 @@ class OriginCheckTest extends AbstractMessageComponentTestCase {
} }
public function testCloseOnNonMatchingOrigin() { public function testCloseOnNonMatchingOrigin() {
$this->_serv->allowedOrigins = array('socketo.me'); $this->_serv->allowedOrigins = ['socketo.me'];
$this->_conn->expects($this->once())->method('close'); $this->_conn->expects($this->once())->method('close');
$this->_serv->onOpen($this->_conn, $this->_reqStub); $this->_serv->onOpen($this->_conn, $this->_reqStub);

View File

@ -1,6 +1,5 @@
<?php <?php
namespace Ratchet\Http; namespace Ratchet\Http;
use Ratchet\Http\Router;
use Ratchet\WebSocket\WsServerInterface; use Ratchet\WebSocket\WsServerInterface;
use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface; use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
@ -12,21 +11,17 @@ class RouterTest extends \PHPUnit_Framework_TestCase {
protected $_router; protected $_router;
protected $_matcher; protected $_matcher;
protected $_conn; protected $_conn;
protected $_uri;
protected $_req; protected $_req;
public function setUp() { public function setUp() {
$queryMock = $this->getMock('Guzzle\Http\QueryString'); $this->_conn = $this->getMock('\Ratchet\ConnectionInterface');
$queryMock $this->_uri = $this->getMock('Psr\Http\Message\UriInterface');
->expects($this->any()) $this->_req = $this->getMock('\Psr\Http\Message\RequestInterface');
->method('getAll')
->will($this->returnValue(array()));
$this->_conn = $this->getMock('\Ratchet\ConnectionInterface');
$this->_req = $this->getMock('\Guzzle\Http\Message\RequestInterface');
$this->_req $this->_req
->expects($this->any()) ->expects($this->any())
->method('getQuery') ->method('getUri')
->will($this->returnValue($queryMock)); ->will($this->returnValue($this->_uri));
$this->_matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); $this->_matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface');
$this->_matcher $this->_matcher
->expects($this->any()) ->expects($this->any())
@ -34,7 +29,14 @@ class RouterTest extends \PHPUnit_Framework_TestCase {
->will($this->returnValue($this->getMock('Symfony\Component\Routing\RequestContext'))); ->will($this->returnValue($this->getMock('Symfony\Component\Routing\RequestContext')));
$this->_router = new Router($this->_matcher); $this->_router = new Router($this->_matcher);
$this->_req->expects($this->any())->method('getPath')->will($this->returnValue('/whatever')); $this->_uri->expects($this->any())->method('getPath')->will($this->returnValue('ws://doesnt.matter/'));
$this->_uri->expects($this->any())->method('withQuery')->with($this->callback(function($val) {
$this->setResult($val);
return true;
}))->will($this->returnSelf());
$this->_uri->expects($this->any())->method('getQuery')->will($this->returnCallback([$this, 'getResult']));
$this->_req->expects($this->any())->method('withUri')->will($this->returnSelf());
} }
public function testFourOhFour() { public function testFourOhFour() {
@ -103,41 +105,41 @@ class RouterTest extends \PHPUnit_Framework_TestCase {
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); $controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock();
/** @var $matcher UrlMatcherInterface */ /** @var $matcher UrlMatcherInterface */
$this->_matcher->expects($this->any())->method('match')->will( $this->_matcher->expects($this->any())->method('match')->will(
$this->returnValue(array('_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux')) $this->returnValue(['_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux'])
); );
$conn = $this->getMock('Ratchet\Mock\Connection'); $conn = $this->getMock('Ratchet\Mock\Connection');
$request = $this->getMock('Guzzle\Http\Message\Request', array('getPath'), array('GET', 'ws://random.url'), '', false);
$request->expects($this->any())->method('getPath')->will($this->returnValue('ws://doesnt.matter/'));
$request->setHeaderFactory($this->getMock('Guzzle\Http\Message\Header\HeaderFactoryInterface'));
$request->setUrl('ws://doesnt.matter/');
$router = new Router($this->_matcher); $router = new Router($this->_matcher);
$router->onOpen($conn, $request); $router->onOpen($conn, $this->_req);
$this->assertEquals(array('foo' => 'bar', 'baz' => 'qux'), $request->getQuery()->getAll()); $this->assertEquals('foo=bar&baz=qux', $this->_req->getUri()->getQuery());
} }
public function testQueryParams() { public function testQueryParams() {
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); $controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock();
$this->_matcher->expects($this->any())->method('match')->will( $this->_matcher->expects($this->any())->method('match')->will(
$this->returnValue(array('_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux')) $this->returnValue(['_controller' => $controller, 'foo' => 'bar', 'baz' => 'qux'])
); );
$conn = $this->getMock('Ratchet\Mock\Connection'); $conn = $this->getMock('Ratchet\Mock\Connection');
/**@var $request \Guzzle\Http\Message\Request */ $request = $this->getMock('Psr\Http\Message\RequestInterface');
$request = $this->getMock('Guzzle\Http\Message\Request', array('getPath'), array('GET', ''), '', false); $uri = new \GuzzleHttp\Psr7\Uri('ws://doesnt.matter/endpoint?hello=world&foo=nope');
$request->setHeaderFactory($this->getMock('Guzzle\Http\Message\Header\HeaderFactoryInterface')); $request->expects($this->any())->method('getUri')->will($this->returnCallback(function() use (&$uri) {
$request->setUrl('ws://doesnt.matter?hello=world&foo=nope'); return $uri;
}));
$request->expects($this->any())->method('withUri')->with($this->callback(function($url) use (&$uri) {
$uri = $url;
return true;
}))->will($this->returnSelf());
$router = new Router($this->_matcher); $router = new Router($this->_matcher);
$router->onOpen($conn, $request); $router->onOpen($conn, $request);
$this->assertEquals(array('foo' => 'nope', 'baz' => 'qux', 'hello' => 'world'), $request->getQuery()->getAll()); $this->assertEquals('foo=nope&baz=qux&hello=world', $request->getUri()->getQuery());
$this->assertEquals('ws', $request->getScheme()); $this->assertEquals('ws', $request->getUri()->getScheme());
$this->assertEquals('doesnt.matter', $request->getHost()); $this->assertEquals('doesnt.matter', $request->getUri()->getHost());
} }
} }

View File

@ -20,10 +20,10 @@ class IoServerTest extends \PHPUnit_Framework_TestCase {
$this->app = $this->getMock('\\Ratchet\\MessageComponentInterface'); $this->app = $this->getMock('\\Ratchet\\MessageComponentInterface');
$loop = new StreamSelectLoop; $loop = new StreamSelectLoop;
$this->reactor = new Server($loop); $this->reactor = new Server(0, $loop);
$this->reactor->listen(0);
$this->port = $this->reactor->getPort(); $uri = $this->reactor->getAddress();
$this->port = parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_PORT);
$this->server = new IoServer($this->app, $this->reactor, $loop); $this->server = new IoServer($this->app, $this->reactor, $loop);
} }

View File

@ -1,11 +1,8 @@
<?php <?php
namespace Ratchet\Session; namespace Ratchet\Session;
use Ratchet\AbstractMessageComponentTestCase; use Ratchet\AbstractMessageComponentTestCase;
use Ratchet\Session\SessionProvider;
use Ratchet\Mock\MemorySessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler;
use Guzzle\Http\Message\Request;
/** /**
* @covers Ratchet\Session\SessionProvider * @covers Ratchet\Session\SessionProvider
@ -35,7 +32,7 @@ class SessionProviderTest extends AbstractMessageComponentTestCase {
} }
public function getComponentClassString() { public function getComponentClassString() {
return '\Ratchet\MessageComponentInterface'; return '\Ratchet\Http\HttpServerInterface';
} }
public function classCaseProvider() { public function classCaseProvider() {
@ -53,7 +50,7 @@ class SessionProviderTest extends AbstractMessageComponentTestCase {
$method = $ref->getMethod('toClassCase'); $method = $ref->getMethod('toClassCase');
$method->setAccessible(true); $method->setAccessible(true);
$component = new SessionProvider($this->getMock('Ratchet\\MessageComponentInterface'), $this->getMock('\SessionHandlerInterface')); $component = new SessionProvider($this->getMock($this->getComponentClassString()), $this->getMock('\SessionHandlerInterface'));
$this->assertEquals($out, $method->invokeArgs($component, array($in))); $this->assertEquals($out, $method->invokeArgs($component, array($in)));
} }
@ -82,16 +79,13 @@ class SessionProviderTest extends AbstractMessageComponentTestCase {
$pdoHandler = new PdoSessionHandler($pdo, $dbOptions); $pdoHandler = new PdoSessionHandler($pdo, $dbOptions);
$pdoHandler->write($sessionId, '_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}'); $pdoHandler->write($sessionId, '_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}');
$component = new SessionProvider($this->getMock('Ratchet\\MessageComponentInterface'), $pdoHandler, array('auto_start' => 1)); $component = new SessionProvider($this->getMock($this->getComponentClassString()), $pdoHandler, array('auto_start' => 1));
$connection = $this->getMock('Ratchet\\ConnectionInterface'); $connection = $this->getMock('Ratchet\\ConnectionInterface');
$headers = $this->getMock('Guzzle\\Http\\Message\\Request', array('getCookie'), array('POST', '/', array())); $headers = $this->getMock('Psr\Http\Message\RequestInterface');
$headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue($sessionId)); $headers->expects($this->once())->method('getHeader')->will($this->returnValue([ini_get('session.name') . "={$sessionId};"]));
$connection->WebSocket = new \StdClass; $component->onOpen($connection, $headers);
$connection->WebSocket->request = $headers;
$component->onOpen($connection);
$this->assertEquals('world', $connection->Session->get('hello')); $this->assertEquals('world', $connection->Session->get('hello'));
} }
@ -99,12 +93,9 @@ class SessionProviderTest extends AbstractMessageComponentTestCase {
protected function newConn() { protected function newConn() {
$conn = $this->getMock('Ratchet\ConnectionInterface'); $conn = $this->getMock('Ratchet\ConnectionInterface');
$headers = $this->getMock('Guzzle\Http\Message\Request', array('getCookie'), array('POST', '/', array())); $headers = $this->getMock('Psr\Http\Message\Request', array('getCookie'), array('POST', '/', array()));
$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));
$conn->WebSocket = new \StdClass;
$conn->WebSocket->request = $headers;
return $conn; return $conn;
} }
@ -114,21 +105,6 @@ class SessionProviderTest extends AbstractMessageComponentTestCase {
$this->_serv->onMessage($this->_conn, $message); $this->_serv->onMessage($this->_conn, $message);
} }
public function testGetSubProtocolsReturnsArray() {
$mock = $this->getMock('Ratchet\\MessageComponentInterface');
$comp = new SessionProvider($mock, new NullSessionHandler);
$this->assertInternalType('array', $comp->getSubProtocols());
}
public function testGetSubProtocolsGetFromApp() {
$mock = $this->getMock('Ratchet\WebSocket\Stub\WsMessageComponentInterface');
$mock->expects($this->once())->method('getSubProtocols')->will($this->returnValue(array('hello', 'world')));
$comp = new SessionProvider($mock, new NullSessionHandler);
$this->assertGreaterThanOrEqual(2, count($comp->getSubProtocols()));
}
public function testRejectInvalidSeralizers() { public function testRejectInvalidSeralizers() {
if (!function_exists('wddx_serialize_value')) { if (!function_exists('wddx_serialize_value')) {
$this->markTestSkipped(); $this->markTestSkipped();
@ -136,6 +112,13 @@ class SessionProviderTest extends AbstractMessageComponentTestCase {
ini_set('session.serialize_handler', 'wddx'); ini_set('session.serialize_handler', 'wddx');
$this->setExpectedException('\RuntimeException'); $this->setExpectedException('\RuntimeException');
new SessionProvider($this->getMock('\Ratchet\MessageComponentInterface'), $this->getMock('\SessionHandlerInterface')); new SessionProvider($this->getMock($this->getComponentClassString()), $this->getMock('\SessionHandlerInterface'));
}
protected function doOpen($conn) {
$request = $this->getMock('Psr\Http\Message\RequestInterface');
$request->expects($this->any())->method('getHeader')->will($this->returnValue([]));
$this->_serv->onOpen($conn, $request);
} }
} }

View File

@ -1,15 +1,11 @@
<?php <?php
namespace Ratchet\Session\Storage; namespace Ratchet\Session\Storage;
use Ratchet\Session\Serialize\PhpHandler; use Ratchet\Session\Serialize\PhpHandler;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
class VirtualSessionStoragePDOTest extends \PHPUnit_Framework_TestCase class VirtualSessionStoragePDOTest extends \PHPUnit_Framework_TestCase {
{
/** /**
* @var VirtualSessionStorage * @var VirtualSessionStorage
*/ */
@ -17,8 +13,11 @@ class VirtualSessionStoragePDOTest extends \PHPUnit_Framework_TestCase
protected $_pathToDB; protected $_pathToDB;
public function setUp() public function setUp() {
{ if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
return $this->markTestSkipped('Session test requires PDO and pdo_sqlite');
}
$schema = <<<SQL $schema = <<<SQL
CREATE TABLE `sessions` ( CREATE TABLE `sessions` (
`sess_id` VARBINARY(128) NOT NULL PRIMARY KEY, `sess_id` VARBINARY(128) NOT NULL PRIMARY KEY,
@ -42,17 +41,13 @@ SQL;
$this->_virtualSessionStorage->registerBag(new AttributeBag()); $this->_virtualSessionStorage->registerBag(new AttributeBag());
} }
public function tearDown() public function tearDown() {
{
unlink($this->_pathToDB); unlink($this->_pathToDB);
} }
public function testStartWithDSN() public function testStartWithDSN() {
{
$this->_virtualSessionStorage->start(); $this->_virtualSessionStorage->start();
$this->assertTrue($this->_virtualSessionStorage->isStarted()); $this->assertTrue($this->_virtualSessionStorage->isStarted());
} }
} }

View File

@ -185,21 +185,18 @@ class TopicManagerTest extends \PHPUnit_Framework_TestCase {
} }
public static function topicConnExpectationProvider() { public static function topicConnExpectationProvider() {
return array( return [
array(true, 'onClose', 0) [ 'onClose', 0]
, array(true, 'onUnsubscribe', 0) , ['onUnsubscribe', 0]
, array(false, 'onClose', 1) ];
, array(false, 'onUnsubscribe', 1)
);
} }
/** /**
* @dataProvider topicConnExpectationProvider * @dataProvider topicConnExpectationProvider
*/ */
public function testTopicRetentionFromLeavingConnections($autoDelete, $methodCall, $expectation) { public function testTopicRetentionFromLeavingConnections($methodCall, $expectation) {
$topicName = 'checkTopic'; $topicName = 'checkTopic';
list($topic, $attribute) = $this->topicProvider($topicName); list($topic, $attribute) = $this->topicProvider($topicName);
$topic->autoDelete = $autoDelete;
$this->mngr->onSubscribe($this->conn, $topicName); $this->mngr->onSubscribe($this->conn, $topicName);
call_user_func_array(array($this->mngr, $methodCall), array($this->conn, $topicName)); call_user_func_array(array($this->mngr, $methodCall), array($this->conn, $topicName));

View File

@ -1,103 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
use Ratchet\WebSocket\Version\Hixie76;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
/**
* @covers Ratchet\WebSocket\Version\Hixie76
*/
class Hixie76Test extends \PHPUnit_Framework_TestCase {
protected $_crlf = "\r\n";
protected $_body = '6dW+XgKfWV0=';
protected $_version;
public function setUp() {
$this->_version = new Hixie76;
}
public function testClassImplementsVersionInterface() {
$constraint = $this->isInstanceOf('\\Ratchet\\WebSocket\\Version\\VersionInterface');
$this->assertThat($this->_version, $constraint);
}
/**
* @dataProvider keyProvider
*/
public function testKeySigningForHandshake($accept, $key) {
$this->assertEquals($accept, $this->_version->generateKeyNumber($key));
}
public static function keyProvider() {
return array(
array(179922739, '17 9 G`ZD9 2 2b 7X 3 /r90')
, array(906585445, '3e6b263 4 17 80')
, array(0, '3e6b26341780')
);
}
public function headerProvider() {
$key1 = base64_decode('QTN+ICszNiA2IDJvICBWOG4gNyAgc08yODhZ');
$key2 = base64_decode('TzEyICAgeVsgIFFSNDUgM1IgLiAyOFggNC00dn4z');
$headers = "GET / HTTP/1.1";
$headers .= "Upgrade: WebSocket{$this->_crlf}";
$headers .= "Connection: Upgrade{$this->_crlf}";
$headers .= "Host: socketo.me{$this->_crlf}";
$headers .= "Origin: http://fiddle.jshell.net{$this->_crlf}";
$headers .= "Sec-WebSocket-Key1:17 Z4< F94 N3 7P41 7{$this->_crlf}";
$headers .= "Sec-WebSocket-Key2:1 23C3:,2% 1-29 4 f0{$this->_crlf}";
$headers .= "(Key3):70:00:EE:6E:33:20:90:69{$this->_crlf}";
$headers .= $this->_crlf;
return $headers;
}
public function testNoUpgradeBeforeBody() {
$headers = $this->headerProvider();
$mockConn = $this->getMock('\Ratchet\ConnectionInterface');
$mockApp = $this->getMock('\Ratchet\MessageComponentInterface');
$server = new HttpServer(new WsServer($mockApp));
$server->onOpen($mockConn);
$mockApp->expects($this->exactly(0))->method('onOpen');
$server->onMessage($mockConn, $headers);
}
public function testTcpFragmentedUpgrade() {
$headers = $this->headerProvider();
$body = base64_decode($this->_body);
$mockConn = $this->getMock('\Ratchet\ConnectionInterface');
$mockApp = $this->getMock('\Ratchet\MessageComponentInterface');
$server = new HttpServer(new WsServer($mockApp));
$server->onOpen($mockConn);
$server->onMessage($mockConn, $headers);
$mockApp->expects($this->once())->method('onOpen');
$server->onMessage($mockConn, $body . $this->_crlf . $this->_crlf);
}
public function testTcpFragmentedBodyUpgrade() {
$headers = $this->headerProvider();
$body = base64_decode($this->_body);
$body1 = substr($body, 0, 4);
$body2 = substr($body, 4);
$mockConn = $this->getMock('\Ratchet\ConnectionInterface');
$mockApp = $this->getMock('\Ratchet\MessageComponentInterface');
$server = new HttpServer(new WsServer($mockApp));
$server->onOpen($mockConn);
$server->onMessage($mockConn, $headers);
$mockApp->expects($this->once())->method('onOpen');
$server->onMessage($mockConn, $body1);
$server->onMessage($mockConn, $body2);
$server->onMessage($mockConn, $this->_crlf . $this->_crlf);
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
use Ratchet\WebSocket\Version\HyBi10;
use Ratchet\WebSocket\Version\RFC6455\Frame;
/**
* @covers Ratchet\WebSocket\Version\Hybi10
*/
class HyBi10Test extends \PHPUnit_Framework_TestCase {
protected $_version;
public function setUp() {
$this->_version = new HyBi10();
}
/**
* Is this useful?
*/
public function testClassImplementsVersionInterface() {
$constraint = $this->isInstanceOf('\\Ratchet\\WebSocket\\Version\\VersionInterface');
$this->assertThat($this->_version, $constraint);
}
/**
* @dataProvider HandshakeProvider
*/
public function testKeySigningForHandshake($key, $accept) {
$this->assertEquals($accept, $this->_version->sign($key));
}
public static function HandshakeProvider() {
return array(
array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=')
, array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=')
);
}
/**
* @dataProvider UnframeMessageProvider
*/
public function testUnframeMessage($message, $framed) {
// $decoded = $this->_version->unframe(base64_decode($framed));
$frame = new Frame;
$frame->addBuffer(base64_decode($framed));
$this->assertEquals($message, $frame->getPayload());
}
public static function UnframeMessageProvider() {
return array(
array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7')
, array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg')
, array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow==')
, array("The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...", 'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY=')
);
}
public function testUnframeMatchesPreFraming() {
$string = 'Hello World!';
$framed = $this->_version->newFrame($string)->getContents();
$frame = new Frame;
$frame->addBuffer($framed);
$this->assertEquals($string, $frame->getPayload());
}
}

View File

@ -1,543 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\RFC6455\Frame;
/**
* @todo getMaskingKey, getPayloadStartingByte don't have tests yet
* @todo Could use some clean up in general, I had to rush to fix a bug for a deadline, sorry.
*/
class FrameTest extends \PHPUnit_Framework_TestCase {
protected $_firstByteFinText = '10000001';
protected $_secondByteMaskedSPL = '11111101';
protected $_frame;
protected $_packer;
public function setUp() {
$this->_frame = new Frame;
}
/**
* 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));
}
/**
* This is a data provider
* @param string The UTF8 message
* @param string The WebSocket framed message, then base64_encoded
*/
public static function UnframeMessageProvider() {
return array(
array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7')
, array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg')
, array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow==')
, array("The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...", 'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY=')
);
}
public static function underflowProvider() {
return array(
array('isFinal', '')
, array('getRsv1', '')
, array('getRsv2', '')
, array('getRsv3', '')
, array('getOpcode', '')
, array('isMasked', '10000001')
, array('getPayloadLength', '10000001')
, array('getPayloadLength', '1000000111111110')
, array('getMaskingKey', '1000000110000111')
, array('getPayload', '100000011000000100011100101010101001100111110100')
);
}
/**
* @dataProvider underflowProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload
*/
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) {
$this->setExpectedException('\UnderflowException');
if (!empty($bin)) {
$this->_frame->addBuffer(static::encode($bin));
}
call_user_func(array($this->_frame, $method));
}
/**
* A data provider for testing the first byte of a WebSocket frame
* @param bool Given, is the byte indicate this is the final frame
* @param int Given, what is the expected opcode
* @param string of 0|1 Each character represents a bit in the byte
*/
public static function firstByteProvider() {
return array(
array(false, false, false, true, 8, '00011000')
, array(true, false, true, false, 10, '10101010')
, array(false, false, false, false, 15, '00001111')
, array(true, false, false, false, 1, '10000001')
, array(true, true, true, true, 15, '11111111')
, array(true, true, false, false, 7, '11000111')
);
}
/**
* @dataProvider firstByteProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal
*/
public function testFinCodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($fin, $this->_frame->isFinal());
}
/**
* @dataProvider firstByteProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv1
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv2
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getRsv3
*/
public function testGetRsvFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($rsv1, $this->_frame->getRsv1());
$this->assertEquals($rsv2, $this->_frame->getRsv2());
$this->assertEquals($rsv3, $this->_frame->getRsv3());
}
/**
* @dataProvider firstByteProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode
*/
public function testOpcodeFromBits($fin, $rsv1, $rsv2, $rsv3, $opcode, $bin) {
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($opcode, $this->_frame->getOpcode());
}
/**
* @dataProvider UnframeMessageProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::isFinal
*/
public function testFinCodeFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertTrue($this->_frame->isFinal());
}
/**
* @dataProvider UnframeMessageProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getOpcode
*/
public function testOpcodeFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertEquals(1, $this->_frame->getOpcode());
}
public static function payloadLengthDescriptionProvider() {
return array(
array(7, '01110101')
, array(7, '01111101')
, array(23, '01111110')
, array(71, '01111111')
, array(7, '00000000') // Should this throw an exception? Can a payload be empty?
, array(7, '00000001')
);
}
/**
* @dataProvider payloadLengthDescriptionProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::addBuffer
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getFirstPayloadVal
*/
public function testFirstPayloadDesignationValue($bits, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal');
$cb->setAccessible(true);
$this->assertEquals(bindec($bin), $cb->invoke($this->_frame));
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getFirstPayloadVal
*/
public function testFirstPayloadValUnderflow() {
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getFirstPayloadVal');
$cb->setAccessible(true);
$this->setExpectedException('UnderflowException');
$cb->invoke($this->_frame);
}
/**
* @dataProvider payloadLengthDescriptionProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getNumPayloadBits
*/
public function testDetermineHowManyBitsAreUsedToDescribePayload($expected_bits, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits');
$cb->setAccessible(true);
$this->assertEquals($expected_bits, $cb->invoke($this->_frame));
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getNumPayloadBits
*/
public function testgetNumPayloadBitsUnderflow() {
$ref = new \ReflectionClass($this->_frame);
$cb = $ref->getMethod('getNumPayloadBits');
$cb->setAccessible(true);
$this->setExpectedException('UnderflowException');
$cb->invoke($this->_frame);
}
public function secondByteProvider() {
return array(
array(true, 1, '10000001')
, array(false, 1, '00000001')
, array(true, 125, $this->_secondByteMaskedSPL)
);
}
/**
* @dataProvider secondByteProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked
*/
public function testIsMaskedReturnsExpectedValue($masked, $payload_length, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($masked, $this->_frame->isMasked());
}
/**
* @dataProvider UnframeMessageProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::isMasked
*/
public function testIsMaskedFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertTrue($this->_frame->isMasked());
}
/**
* @dataProvider secondByteProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength
*/
public function testGetPayloadLengthWhenOnlyFirstFrameIsUsed($masked, $payload_length, $bin) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($bin));
$this->assertEquals($payload_length, $this->_frame->getPayloadLength());
}
/**
* @dataProvider UnframeMessageProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength
* @todo Not yet testing when second additional payload length descriptor
*/
public function testGetPayloadLengthFromFullMessage($msg, $encoded) {
$this->_frame->addBuffer(base64_decode($encoded));
$this->assertEquals(strlen($msg), $this->_frame->getPayloadLength());
}
public function maskingKeyProvider() {
$frame = new Frame;
return array(
array($frame->generateMaskingKey())
, array($frame->generateMaskingKey())
, array($frame->generateMaskingKey())
);
}
/**
* @dataProvider maskingKeyProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey
* @todo I I wrote the dataProvider incorrectly, skipping for now
*/
public function testGetMaskingKey($mask) {
$this->_frame->addBuffer(static::encode($this->_firstByteFinText));
$this->_frame->addBuffer(static::encode($this->_secondByteMaskedSPL));
$this->_frame->addBuffer($mask);
$this->assertEquals($mask, $this->_frame->getMaskingKey());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getMaskingKey
*/
public function testGetMaskingKeyOnUnmaskedPayload() {
$frame = new Frame('Hello World!');
$this->assertEquals('', $frame->getMaskingKey());
}
/**
* @dataProvider UnframeMessageProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload
* @todo Move this test to bottom as it requires all methods of the class
*/
public function testUnframeFullMessage($unframed, $base_framed) {
$this->_frame->addBuffer(base64_decode($base_framed));
$this->assertEquals($unframed, $this->_frame->getPayload());
}
public static function messageFragmentProvider() {
return array(
array(false, '', '', '', '', '')
);
}
/**
* @dataProvider UnframeMessageProvider
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload
*/
public function testCheckPiecingTogetherMessage($msg, $encoded) {
$framed = base64_decode($encoded);
for ($i = 0, $len = strlen($framed);$i < $len; $i++) {
$this->_frame->addBuffer(substr($framed, $i, 1));
}
$this->assertEquals($msg, $this->_frame->getPayload());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayload
*/
public function testLongCreate() {
$len = 65525;
$pl = $this->generateRandomString($len);
$frame = new Frame($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());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength
*/
public function testReallyLongCreate() {
$len = 65575;
$frame = new Frame($this->generateRandomString($len));
$this->assertEquals($len, $frame->getPayloadLength());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::__construct
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow
*/
public function testExtractOverflow() {
$string1 = $this->generateRandomString();
$frame1 = new Frame($string1);
$string2 = $this->generateRandomString();
$frame2 = new Frame($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());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow
*/
public function testEmptyExtractOverflow() {
$string = $this->generateRandomString();
$frame = new Frame($string);
$this->assertEquals($string, $frame->getPayload());
$this->assertEquals('', $frame->extractOverflow());
$this->assertEquals($string, $frame->getPayload());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getContents
*/
public function testGetContents() {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame1 = new Frame($msg);
$frame2 = new Frame($msg);
$frame2->maskPayload();
$this->assertNotEquals($frame1->getContents(), $frame2->getContents());
$this->assertEquals(strlen($frame1->getContents()) + 4, strlen($frame2->getContents()));
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload
*/
public function testMasking() {
$msg = 'The quick brown fox jumps over the lazy dog.';
$frame = new Frame($msg);
$frame->maskPayload();
$this->assertTrue($frame->isMasked());
$this->assertEquals($msg, $frame->getPayload());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::unMaskPayload
*/
public function testUnMaskPayload() {
$string = $this->generateRandomString();
$frame = new Frame($string);
$frame->maskPayload()->unMaskPayload();
$this->assertFalse($frame->isMasked());
$this->assertEquals($string, $frame->getPayload());
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::generateMaskingKey
*/
public function testGenerateMaskingKey() {
$dupe = false;
$done = array();
for ($i = 0; $i < 10; $i++) {
$new = $this->_frame->generateMaskingKey();
if (in_array($new, $done)) {
$dupe = true;
}
$done[] = $new;
}
$this->assertEquals(4, strlen($new));
$this->assertFalse($dupe);
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload
*/
public function testGivenMaskIsValid() {
$this->setExpectedException('InvalidArgumentException');
$this->_frame->maskPayload('hello world');
}
/**
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::maskPayload
*/
public function testGivenMaskIsValidAscii() {
if (!extension_loaded('mbstring')) {
return $this->markTestSkipped("mbstring required for this test");
}
$this->setExpectedException('OutOfBoundsException');
$this->_frame->maskPayload('x✖');
}
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;
}
/**
* There was a frame boundary issue when the first 3 bytes of a frame with a payload greater than
* 126 was added to the frame buffer and then Frame::getPayloadLength was called. It would cause the frame
* to set the payload length to 126 and then not recalculate it once the full length information was available.
*
* This is fixed by setting the defPayLen back to -1 before the underflow exception is thrown.
*
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::getPayloadLength
* @covers Ratchet\WebSocket\Version\RFC6455\Frame::extractOverflow
*/
public function testFrameDeliveredOneByteAtATime() {
$startHeader = "\x01\x7e\x01\x00"; // header for a text frame of 256 - non-final
$framePayload = str_repeat("*", 256);
$rawOverflow = "xyz";
$rawFrame = $startHeader . $framePayload . $rawOverflow;
$frame = new Frame();
$payloadLen = 256;
for ($i = 0; $i < strlen($rawFrame); $i++) {
$frame->addBuffer($rawFrame[$i]);
try {
// payloadLen will
$payloadLen = $frame->getPayloadLength();
} catch (\UnderflowException $e) {
if ($i > 2) { // we should get an underflow on 0,1,2
$this->fail("Underflow exception when the frame length should be available");
}
}
if ($payloadLen !== 256) {
$this->fail("Payload length of " . $payloadLen . " should have been 256.");
}
}
// make sure the overflow is good
$this->assertEquals($rawOverflow, $frame->extractOverflow());
}
}

View File

@ -1,170 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier;
/**
* @covers Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier
*/
class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
/**
* @var Ratchet\WebSocket\Version\RFC6455\HandshakeVerifier
*/
protected $_v;
public function setUp() {
$this->_v = new HandshakeVerifier;
}
public static function methodProvider() {
return array(
array(true, 'GET')
, array(true, 'get')
, array(true, 'Get')
, array(false, 'POST')
, array(false, 'DELETE')
, array(false, 'PUT')
, array(false, 'PATCH')
);
}
/**
* @dataProvider methodProvider
*/
public function testMethodMustBeGet($result, $in) {
$this->assertEquals($result, $this->_v->verifyMethod($in));
}
public static function httpVersionProvider() {
return array(
array(true, 1.1)
, array(true, '1.1')
, array(true, 1.2)
, array(true, '1.2')
, array(true, 2)
, array(true, '2')
, array(true, '2.0')
, array(false, '1.0')
, array(false, 1)
, array(false, '0.9')
, array(false, '')
, array(false, 'hello')
);
}
/**
* @dataProvider httpVersionProvider
*/
public function testHttpVersionIsAtLeast1Point1($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyHTTPVersion($in));
}
public static function uRIProvider() {
return array(
array(true, '/chat')
, array(true, '/hello/world?key=val')
, array(false, '/chat#bad')
, array(false, 'nope')
, array(false, '/ ಠ_ಠ ')
, array(false, '/✖')
);
}
/**
* @dataProvider URIProvider
*/
public function testRequestUri($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyRequestURI($in));
}
public static function hostProvider() {
return array(
array(true, 'server.example.com')
, array(false, null)
);
}
/**
* @dataProvider HostProvider
*/
public function testVerifyHostIsSet($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyHost($in));
}
public static function upgradeProvider() {
return array(
array(true, 'websocket')
, array(true, 'Websocket')
, array(true, 'webSocket')
, array(false, null)
, array(false, '')
);
}
/**
* @dataProvider upgradeProvider
*/
public function testVerifyUpgradeIsWebSocket($expected, $val) {
$this->assertEquals($expected, $this->_v->verifyUpgradeRequest($val));
}
public static function connectionProvider() {
return array(
array(true, 'Upgrade')
, array(true, 'upgrade')
, array(true, 'keep-alive, Upgrade')
, array(true, 'Upgrade, keep-alive')
, array(true, 'keep-alive, Upgrade, something')
, array(false, '')
, array(false, null)
);
}
/**
* @dataProvider connectionProvider
*/
public function testConnectionHeaderVerification($expected, $val) {
$this->assertEquals($expected, $this->_v->verifyConnection($val));
}
public static function keyProvider() {
return array(
array(true, 'hkfa1L7uwN6DCo4IS3iWAw==')
, array(true, '765vVoQpKSGJwPzJIMM2GA==')
, array(true, 'AQIDBAUGBwgJCgsMDQ4PEC==')
, array(true, 'axa2B/Yz2CdpfQAY2Q5P7w==')
, array(false, 0)
, array(false, 'Hello World')
, array(false, '1234567890123456')
, array(false, '123456789012345678901234')
, array(true, base64_encode('UTF8allthngs+✓'))
, array(true, 'dGhlIHNhbXBsZSBub25jZQ==')
);
}
/**
* @dataProvider keyProvider
*/
public function testKeyIsBase64Encoded16BitNonce($expected, $val) {
$this->assertEquals($expected, $this->_v->verifyKey($val));
}
public static function versionProvider() {
return array(
array(true, 13)
, array(true, '13')
, array(false, 12)
, array(false, 14)
, array(false, '14')
, array(false, 'hi')
, array(false, '')
, array(false, null)
);
}
/**
* @dataProvider versionProvider
*/
public function testVersionEquals13($expected, $in) {
$this->assertEquals($expected, $this->_v->verifyVersion($in));
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace Ratchet\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 = new Frame($a, false);
$f2 = new Frame($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(new Frame('The quick brow', false));
$this->setExpectedException('UnderflowException');
$this->message->getPayload();
}
public function testGetOpCode() {
$this->message
->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT))
->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE))
->addFrame(new Frame('er the lazy dog', true, Frame::OP_CONTINUE))
;
$this->assertEquals(Frame::OP_TEXT, $this->message->getOpCode());
}
public function testGetUnBufferedPayloadLength() {
$this->message
->addFrame(new Frame('The quick brow', false, Frame::OP_TEXT))
->addFrame(new Frame('n fox jumps ov', false, Frame::OP_CONTINUE))
;
$this->assertEquals(28, $this->message->getPayloadLength());
}
}

View File

@ -1,151 +0,0 @@
<?php
namespace Ratchet\WebSocket\Version;
use Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\RFC6455\Frame;
use Guzzle\Http\Message\RequestFactory;
use Guzzle\Http\Message\EntityEnclosingRequest;
/**
* @covers Ratchet\WebSocket\Version\RFC6455
*/
class RFC6455Test extends \PHPUnit_Framework_TestCase {
protected $version;
public function setUp() {
$this->version = new RFC6455;
}
/**
* @dataProvider handshakeProvider
*/
public function testKeySigningForHandshake($key, $accept) {
$this->assertEquals($accept, $this->version->sign($key));
}
public static function handshakeProvider() {
return array(
array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=')
, array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=')
);
}
/**
* @dataProvider UnframeMessageProvider
*/
public function testUnframeMessage($message, $framed) {
$frame = new Frame;
$frame->addBuffer(base64_decode($framed));
$this->assertEquals($message, $frame->getPayload());
}
public static function UnframeMessageProvider() {
return array(
array('Hello World!', 'gYydAIfa1WXrtvIg0LXvbOP7')
, array('!@#$%^&*()-=_+[]{}\|/.,<>`~', 'gZv+h96r38f9j9vZ+IHWrvOWoayF9oX6gtfRqfKXwOeg')
, array('ಠ_ಠ', 'gYfnSpu5B/g75gf4Ow==')
, array("The quick brown fox jumps over the lazy dog. All work and no play makes Chris a dull boy. I'm trying to get past 128 characters for a unit test here...", 'gf4Amahb14P8M7Kj2S6+4MN7tfHHLLmjzjSvo8IuuvPbe7j1zSn398A+9+/JIa6jzDSwrYh7lu/Ee6Ds2jD34sY/9+3He6fvySL37skwsvCIGL/xwSj34og/ou/Ee7Xs0XX3o+F8uqPcKa7qxjz398d7sObce6fi2y/3sppj9+DAOqXiyy+y8dt7sezae7aj3TW+94gvsvDce7/m2j75rYY=')
);
}
public function testUnframeMatchesPreFraming() {
$string = 'Hello World!';
$framed = $this->version->newFrame($string)->getContents();
$frame = new Frame;
$frame->addBuffer($framed);
$this->assertEquals($string, $frame->getPayload());
}
public static $good_rest = 'GET /chat HTTP/1.1';
public static $good_header = array(
'Host' => 'server.example.com'
, 'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Key' => 'dGhlIHNhbXBsZSBub25jZQ=='
, 'Origin' => 'http://example.com'
, 'Sec-WebSocket-Protocol' => 'chat, superchat'
, '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
* but optionally replace a piece to an invalid value for failure testing
*/
public static function getAndSpliceHeader($key = null, $val = null) {
$headers = static::$good_header;
if (null !== $key && null !== $val) {
$headers[$key] = $val;
}
$header = '';
foreach ($headers as $key => $val) {
if (!empty($key)) {
$header .= "{$key}: ";
}
$header .= "{$val}\r\n";
}
$header .= "\r\n";
return $header;
}
public static function headerHandshakeProvider() {
return array(
array(false, "GET /test HTTP/1.0\r\n" . static::getAndSpliceHeader())
, array(true, static::$good_rest . "\r\n" . static::getAndSpliceHeader())
, array(false, "POST / HTTP:/1.1\r\n" . static::getAndSpliceHeader())
, array(false, static::$good_rest . "\r\n" . static::getAndSpliceHeader('Upgrade', 'useless'))
, array(false, "GET /ಠ_ಠ HTTP/1.1\r\n" . static::getAndSpliceHeader())
, array(true, static::$good_rest . "\r\n" . static::getAndSpliceHeader('Connection', 'Herp, Upgrade, Derp'))
);
}
/**
* @dataProvider headerHandshakeProvider
*/
public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) {
$request = RequestFactory::getInstance()->fromMessage($header);
$response = $this->version->handshake($request);
$this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $response);
if ($pass) {
$this->assertEquals(101, $response->getStatusCode());
} else {
$this->assertGreaterThanOrEqual(400, $response->getStatusCode());
}
}
public function testNewMessage() {
$this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->version->newMessage());
}
public function testNewFrame() {
$this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->version->newFrame());
}
}

View File

@ -1,91 +0,0 @@
<?php
namespace Ratchet\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

@ -1,51 +0,0 @@
<?php
namespace Ratchet\WebSocket;
use Ratchet\WebSocket\WsServer;
use Ratchet\Mock\Component as MockComponent;
/**
* @covers Ratchet\WebSocket\WsServer
* @covers Ratchet\ComponentInterface
* @covers Ratchet\MessageComponentInterface
*/
class WsServerTest extends \PHPUnit_Framework_TestCase {
protected $comp;
protected $serv;
public function setUp() {
$this->comp = new MockComponent;
$this->serv = new WsServer($this->comp);
}
public function testIsSubProtocolSupported() {
$this->comp->protocols = array('hello', 'world');
$this->assertTrue($this->serv->isSubProtocolSupported('hello'));
$this->assertFalse($this->serv->isSubProtocolSupported('nope'));
}
public function protocolProvider() {
return array(
array('hello', array('hello', 'world'), array('hello', 'world'))
, array('', array('hello', 'world'), array('wamp'))
, array('', array(), null)
, array('wamp', array('hello', 'wamp', 'world'), array('herp', 'derp', 'wamp'))
, array('wamp', array('wamp'), array('wamp'))
);
}
/**
* @dataProvider protocolProvider
*/
public function testGetSubProtocolString($expected, $supported, $requested) {
$this->comp->protocols = $supported;
$req = (null === $requested ? $requested : new \ArrayIterator($requested));
$class = new \ReflectionClass('Ratchet\\WebSocket\\WsServer');
$method = $class->getMethod('getSubProtocolString');
$method->setAccessible(true);
$this->assertSame($expected, $method->invokeArgs($this->serv, array($req)));
}
}