Merge branch 'master' of github.com:ratchetphp/Ratchet

This commit is contained in:
samizdam 2018-03-02 23:57:14 +03:00
commit 82f2505b13
64 changed files with 698 additions and 3522 deletions

View File

@ -1,13 +1,20 @@
language: php language: php
php: php:
- 5.3
- 5.4 - 5.4
- 5.5 - 5.5
- 5.6 - 5.6
- 7 - 7.0
- 7.1
- hhvm - hhvm
dist: trusty
matrix:
allow_failures:
- php: hhvm
before_script: before_script:
- sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "session.serialize_handler = php" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;' - sh -c 'if [ "$TRAVIS_PHP_VERSION" != "hhvm" ]; then echo "session.serialize_handler = php" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;'
- php -m
- composer install --dev --prefer-source - composer install --dev --prefer-source

View File

@ -1,138 +1,135 @@
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.1 (2017-12-11)
* Only enableKeepAlive in App if no WsServer passed allowing user to set their own timeout duration
* Support Symfony 4
* BF: Plug NOOP controller in connection from router in case of misbehaving client
* BF: Raise error from invalid WAMP payload
* 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
* 0.3.5 (2016-05-25) * 0.3.5 (2016-05-25)
* BF: Unmask responding close frame
* BF: Unmask responding close frame * Added write handler for PHP session serializer
* Added write handler for PHP session serializer
* 0.3.4 (2015-12-23) * 0.3.4 (2015-12-23)
* BF: Edge case where version check wasn't run on message coalesce
* BF: Edge case where version check wasn't run on message coalesce * BF: Session didn't start when using pdo_sqlite
* BF: Session didn't start when using pdo_sqlite * BF: WAMP currie prefix check when using '#'
* BF: WAMP currie prefix check when using '#' * Compatibility with Symfony 3
* Compatibility with Symfony 3
* 0.3.3 (2015-05-26) * 0.3.3 (2015-05-26)
* BF: Framing bug on large messages upon TCP fragmentation
* BF: Framing bug on large messages upon TCP fragmentation * BF: Symfony Router query parameter defaults applied to Request
* BF: Symfony Router query parameter defaults applied to Request * BF: WAMP CURIE on all URIs
* BF: WAMP CURIE on all URIs * OriginCheck rules applied to FlashPolicy
* OriginCheck rules applied to FlashPolicy * Switched from PSR-0 to PSR-4
* Switched from PSR-0 to PSR-4
* 0.3.2 (2014-06-08) * 0.3.2 (2014-06-08)
* BF: No messages after closing handshake (fixed rare race condition causing 100% CPU)
* BF: No messages after closing handshake (fixed rare race condition causing 100% CPU) * BF: Fixed accidental BC break from v0.3.1
* BF: Fixed accidental BC break from v0.3.1 * Added autoDelete parameter to Topic to destroy when empty of connections
* Added autoDelete parameter to Topic to destroy when empty of connections * Exposed React Socket on IoServer (allowing FlashPolicy shutdown in App)
* Exposed React Socket on IoServer (allowing FlashPolicy shutdown in App) * Normalized Exceptions in WAMP
* Normalized Exceptions in WAMP
* 0.3.1 (2014-05-26) * 0.3.1 (2014-05-26)
* Added query parameter support to Router, set in HTTP request (ws://server?hello=world)
* Added query parameter support to Router, set in HTTP request (ws://server?hello=world) * HHVM compatibility
* HHVM compatibility * BF: React/0.4 support; CPU starvation bug fixes
* BF: React/0.4 support; CPU starvation bug fixes * BF: Allow App::route to ignore Host header
* BF: Allow App::route to ignore Host header * Added expected filters to WAMP Topic broadcast method
* Added expected filters to WAMP Topic broadcast method * Resource cleanup in WAMP TopicManager
* Resource cleanup in WAMP TopicManager
* 0.3.0 (2013-10-14) * 0.3.0 (2013-10-14)
* Added the `App` class to help making Ratchet so easy to use it's silly
* Added the `App` class to help making Ratchet so easy to use it's silly * BC: Require hostname to do HTTP Host header match and do Origin HTTP header check, verify same name by default, helping prevent CSRF attacks
* BC: Require hostname to do HTTP Host header match and do Origin HTTP header check, verify same name by default, helping prevent CSRF attacks * Added Symfony/2.2 based HTTP Router component to allowing for a single Ratchet server to handle multiple apps -> Ratchet\Http\Router
* Added Symfony/2.2 based HTTP Router component to allowing for a single Ratchet server to handle multiple apps -> Ratchet\Http\Router * BC: Decoupled HTTP from WebSocket component -> Ratchet\Http\HttpServer
* BC: Decoupled HTTP from WebSocket component -> Ratchet\Http\HttpServer * BF: Single sub-protocol selection to conform with RFC6455
* BF: Single sub-protocol selection to conform with RFC6455 * BF: Sanity checks on WAMP protocol to prevent errors
* BF: Sanity checks on WAMP protocol to prevent errors
* 0.2.8 (2013-09-19) * 0.2.8 (2013-09-19)
* React 0.3 support
* React 0.3 support
* 0.2.7 (2013-06-09) * 0.2.7 (2013-06-09)
* BF: Sub-protocol negotation with Guzzle 3.6
* BF: Sub-protocol negotation with Guzzle 3.6
* 0.2.6 (2013-06-01) * 0.2.6 (2013-06-01)
* Guzzle 3.6 support
* Guzzle 3.6 support
* 0.2.5 (2013-04-01) * 0.2.5 (2013-04-01)
* Fixed Hixie-76 handshake bug
* Fixed Hixie-76 handshake bug
* 0.2.4 (2013-03-09) * 0.2.4 (2013-03-09)
* Support for Symfony 2.2 and Guzzle 2.3
* Support for Symfony 2.2 and Guzzle 2.3 * Minor bug fixes when handling errors
* Minor bug fixes when handling errors
* 0.2.3 (2012-11-21) * 0.2.3 (2012-11-21)
* Bumped dep: Guzzle to v3, React to v0.2.4
* Bumped dep: Guzzle to v3, React to v0.2.4 * More tests
* More tests
* 0.2.2 (2012-10-20) * 0.2.2 (2012-10-20)
* Bumped deps to use React v0.2
* Bumped deps to use React v0.2
* 0.2.1 (2012-10-13) * 0.2.1 (2012-10-13)
* BF: No more UTF-8 warnings in browsers (no longer sending empty sub-protocol string)
* BF: No more UTF-8 warnings in browsers (no longer sending empty sub-protocol string) * Documentation corrections
* Documentation corrections * Using new composer structure
* Using new composer structure
* 0.2 (2012-09-07) * 0.2 (2012-09-07)
* Ratchet passes every non-binary-frame test from the Autobahn Testsuite
* Ratchet passes every non-binary-frame test from the Autobahn Testsuite * Major performance improvements
* Major performance improvements * BC: Renamed "WampServer" to "ServerProtocol"
* BC: Renamed "WampServer" to "ServerProtocol" * BC: New "WampServer" component passes Topic container objects of subscribed Connections
* BC: New "WampServer" component passes Topic container objects of subscribed Connections * Option to turn off UTF-8 checks in order to increase performance
* Option to turn off UTF-8 checks in order to increase performance * Switched dependency guzzle/guzzle to guzzle/http (no API changes)
* Switched dependency guzzle/guzzle to guzzle/http (no API changes) * mbstring no longer required
* mbstring no longer required
* 0.1.5 (2012-07-12) * 0.1.5 (2012-07-12)
* BF: Error where service wouldn't run on PHP <= 5.3.8
* BF: Error where service wouldn't run on PHP <= 5.3.8 * Dependency library updates
* Dependency library updates
* 0.1.4 (2012-06-17) * 0.1.4 (2012-06-17)
* Fixed dozens of failing AB tests
* Fixed dozens of failing AB tests * BF: Proper socket buffer handling
* BF: Proper socket buffer handling
* 0.1.3 (2012-06-15) * 0.1.3 (2012-06-15)
* Major refactor inside WebSocket protocol handling, more loosley coupled
* Major refactor inside WebSocket protocol handling, more loosley coupled * BF: Proper error handling on failed WebSocket connections
* BF: Proper error handling on failed WebSocket connections * BF: Handle TCP message concatenation
* BF: Handle TCP message concatenation * Inclusion of the AutobahnTestSuite checking WebSocket protocol compliance
* Inclusion of the AutobahnTestSuite checking WebSocket protocol compliance * mb_string now a requirement
* mb_string now a requirement
* 0.1.2 (2012-05-19) * 0.1.2 (2012-05-19)
* BC/BF: Updated WAMP API to coincide with the official spec
* BC/BF: Updated WAMP API to coincide with the official spec * Tweaks to improve running as a long lived process
* Tweaks to improve running as a long lived process
* 0.1.1 (2012-05-14) * 0.1.1 (2012-05-14)
* Separated interfaces allowing WebSockets to support multiple sub protocols
* Separated interfaces allowing WebSockets to support multiple sub protocols * BF: remoteAddress variable on connections returns proper value
* BF: remoteAddress variable on connections returns proper value
* 0.1 (2012-05-11) * 0.1 (2012-05-11)
* First release with components: IoServer, WsServer, SessionProvider, WampServer, FlashPolicy, IpBlackList
* First release with components: IoServer, WsServer, SessionProvider, WampServer, FlashPolicy, IpBlackList * I/O now handled by React, making Ratchet fully asynchronous
* I/O now handled by React, making Ratchet fully asynchronous

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,26 +1,19 @@
#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 ## Requirements
* 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
Shell access is required and root access is recommended. Shell access is required and root access is recommended.
To avoid proxy/firewall blockage it's recommended WebSockets are requested on port 80 or 443 (SSL), which requires root access. To avoid proxy/firewall blockage it's recommended WebSockets are requested on port 80 or 443 (SSL), which requires root access.
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
@ -31,7 +24,7 @@ Need help? Have a question? Want to provide feedback? Write a message on the
--- ---
###A quick example ### A quick example
```php ```php
<?php <?php
@ -86,5 +79,5 @@ class MyChat implements MessageComponentInterface {
// Then some JavaScript in the browser: // Then some JavaScript in the browser:
var conn = new WebSocket('ws://localhost:8080/echo'); var conn = new WebSocket('ws://localhost:8080/echo');
conn.onmessage = function(e) { console.log(e.data); }; conn.onmessage = function(e) { console.log(e.data); };
conn.send('Hello Me!'); conn.onopen = function(e) { conn.send('Hello 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": [
@ -23,10 +23,14 @@
} }
} }
, "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"
, "symfony/http-foundation": "^2.2|^3.0" , "guzzlehttp/psr7": "^1.0"
, "symfony/routing": "^2.2|^3.0" , "symfony/http-foundation": "^2.6|^3.0|^4.0"
, "symfony/routing": "^2.6|^3.0|^4.0"
}
, "require-dev": {
"phpunit/phpunit": "~4.8"
} }
} }

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);
} }
/** /**
@ -107,8 +104,10 @@ class App {
$decorated = $controller; $decorated = $controller;
} elseif ($controller instanceof WampServerInterface) { } elseif ($controller instanceof WampServerInterface) {
$decorated = new WsServer(new WampServer($controller)); $decorated = new WsServer(new WampServer($controller));
$decorated->enableKeepAlive($this->_server->loop);
} elseif ($controller instanceof MessageComponentInterface) { } elseif ($controller instanceof MessageComponentInterface) {
$decorated = new WsServer($controller); $decorated = new WsServer($controller);
$decorated->enableKeepAlive($this->_server->loop);
} else { } else {
$decorated = $controller; $decorated = $controller;
} }

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.1';
/** /**
* 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

@ -0,0 +1,18 @@
<?php
namespace Ratchet\Http;
use Ratchet\ConnectionInterface;
use Psr\Http\Message\RequestInterface;
class NoOpHttpServerController implements HttpServerInterface {
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
}
public function onMessage(ConnectionInterface $from, $msg) {
}
public function onClose(ConnectionInterface $conn) {
}
public function onError(ConnectionInterface $conn, \Exception $e) {
}
}

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,21 +1,25 @@
<?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
*/ */
protected $_matcher; protected $_matcher;
private $_noopController;
public function __construct(UrlMatcherInterface $matcher) { public function __construct(UrlMatcherInterface $matcher) {
$this->_matcher = $matcher; $this->_matcher = $matcher;
$this->_noopController = new NoOpHttpServerController;
} }
/** /**
@ -27,12 +31,16 @@ class Router implements HttpServerInterface {
throw new \UnexpectedValueException('$request can not be null'); throw new \UnexpectedValueException('$request can not be null');
} }
$conn->controller = $this->_noopController;
$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 +55,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);
@ -66,14 +72,14 @@ class Router implements HttpServerInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
function onMessage(ConnectionInterface $from, $msg) { public function onMessage(ConnectionInterface $from, $msg) {
$from->controller->onMessage($from, $msg); $from->controller->onMessage($from, $msg);
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
function onClose(ConnectionInterface $conn) { public function onClose(ConnectionInterface $conn) {
if (isset($conn->controller)) { if (isset($conn->controller)) {
$conn->controller->onClose($conn); $conn->controller->onClose($conn);
} }
@ -82,26 +88,9 @@ class Router implements HttpServerInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
function onError(ConnectionInterface $conn, \Exception $e) { public function onError(ConnectionInterface $conn, \Exception $e) {
if (isset($conn->controller)) { if (isset($conn->controller)) {
$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

@ -8,7 +8,7 @@ use Ratchet\ConnectionInterface;
* WebSocket Application Messaging Protocol * WebSocket Application Messaging Protocol
* *
* @link http://wamp.ws/spec * @link http://wamp.ws/spec
* @link https://github.com/oberstet/AutobahnJS * @link https://github.com/oberstet/autobahn-js
* *
* +--------------+----+------------------+ * +--------------+----+------------------+
* | Message Type | ID | DIRECTION | * | Message Type | ID | DIRECTION |
@ -62,9 +62,9 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface {
$subs[] = 'wamp'; $subs[] = 'wamp';
return $subs; return $subs;
} else {
return array('wamp');
} }
return ['wamp'];
} }
/** /**
@ -93,6 +93,10 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface {
throw new Exception("Invalid WAMP message format"); throw new Exception("Invalid WAMP message format");
} }
if (isset($json[1]) && !(is_string($json[1]) || is_numeric($json[1]))) {
throw new Exception('Invalid Topic, must be a string');
}
switch ($json[0]) { switch ($json[0]) {
case static::MSG_PREFIX: case static::MSG_PREFIX:
$from->WAMP->prefixes[$json[1]] = $json[2]; $from->WAMP->prefixes[$json[1]] = $json[2];
@ -122,13 +126,13 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface {
$exclude = (array_key_exists(3, $json) ? $json[3] : null); $exclude = (array_key_exists(3, $json) ? $json[3] : null);
if (!is_array($exclude)) { if (!is_array($exclude)) {
if (true === (boolean)$exclude) { if (true === (boolean)$exclude) {
$exclude = array($from->WAMP->sessionId); $exclude = [$from->WAMP->sessionId];
} else { } else {
$exclude = array(); $exclude = [];
} }
} }
$eligible = (array_key_exists(4, $json) ? $json[4] : array()); $eligible = (array_key_exists(4, $json) ? $json[4] : []);
$this->_decorating->onPublish($from, $from->getUri($json[1]), $json[2], $exclude, $eligible); $this->_decorating->onPublish($from, $from->getUri($json[1]), $json[2], $exclude, $eligible);
break; break;

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;
@ -38,7 +31,7 @@ class Topic implements \IteratorAggregate, \Countable {
/** /**
* Send a message to all the connections in this topic * Send a message to all the connections in this topic
* @param string $msg Payload to publish * @param string|array $msg Payload to publish
* @param array $exclude A list of session IDs the message should be excluded from (blacklist) * @param array $exclude A list of session IDs the message should be excluded from (blacklist)
* @param array $eligible A list of session Ids the message should be send to (whitelist) * @param array $eligible A list of session Ids the message should be send to (whitelist)
* @return Topic The same Topic object to chain * @return Topic The same Topic object to chain

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

@ -8,7 +8,7 @@ use Ratchet\ConnectionInterface;
* Enable support for the official WAMP sub-protocol in your application * Enable support for the official WAMP sub-protocol in your application
* WAMP allows for Pub/Sub and RPC * WAMP allows for Pub/Sub and RPC
* @link http://wamp.ws The WAMP specification * @link http://wamp.ws The WAMP specification
* @link https://github.com/oberstet/AutobahnJS Souce for client side library * @link https://github.com/oberstet/autobahn-js Souce for client side library
* @link http://autobahn.s3.amazonaws.com/js/autobahn.min.js Minified client side library * @link http://autobahn.s3.amazonaws.com/js/autobahn.min.js Minified client side library
*/ */
class WampServer implements MessageComponentInterface, WsServerInterface { class WampServer implements MessageComponentInterface, WsServerInterface {

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,9 +1,12 @@
<?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;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Matcher\UrlMatcher;
/** /**
* @covers Ratchet\Http\Router * @covers Ratchet\Http\Router
@ -12,21 +15,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 +33,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 +109,57 @@ 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());
}
public function testImpatientClientOverflow() {
$this->_conn->expects($this->once())->method('close');
$header = "GET /nope HTTP/1.1
Upgrade: websocket
Connection: upgrade
Host: localhost
Origin: http://localhost
Sec-WebSocket-Version: 13\r\n\r\n";
$app = new HttpServer(new Router(new UrlMatcher(new RouteCollection, new RequestContext)));
$app->onOpen($this->_conn);
$app->onMessage($this->_conn, $header);
$app->onMessage($this->_conn, 'Silly body');
} }
} }

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

@ -4,9 +4,9 @@ use Ratchet\Mock\Connection;
use Ratchet\Mock\WampComponent as TestComponent; use Ratchet\Mock\WampComponent as TestComponent;
/** /**
* @covers Ratchet\Wamp\ServerProtocol * @covers \Ratchet\Wamp\ServerProtocol
* @covers Ratchet\Wamp\WampServerInterface * @covers \Ratchet\Wamp\WampServerInterface
* @covers Ratchet\Wamp\WampConnection * @covers \Ratchet\Wamp\WampConnection
*/ */
class ServerProtocolTest extends \PHPUnit_Framework_TestCase { class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
protected $_comp; protected $_comp;
@ -23,13 +23,13 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
} }
public function invalidMessageProvider() { public function invalidMessageProvider() {
return array( return [
array(0) [0]
, array(3) , [3]
, array(4) , [4]
, array(8) , [8]
, array(9) , [9]
); ];
} }
/** /**
@ -40,7 +40,7 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
$conn = $this->newConn(); $conn = $this->newConn();
$this->_comp->onOpen($conn); $this->_comp->onOpen($conn);
$this->_comp->onMessage($conn, json_encode(array($type))); $this->_comp->onMessage($conn, json_encode([$type]));
} }
public function testWelcomeMessage() { public function testWelcomeMessage() {
@ -82,16 +82,16 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
} }
public function callProvider() { public function callProvider() {
return array( return [
array(2, 'a', 'b') [2, 'a', 'b']
, array(2, array('a', 'b')) , [2, ['a', 'b']]
, array(1, 'one') , [1, 'one']
, array(3, 'one', 'two', 'three') , [3, 'one', 'two', 'three']
, array(3, array('un', 'deux', 'trois')) , [3, ['un', 'deux', 'trois']]
, array(2, 'hi', array('hello', 'world')) , [2, 'hi', ['hello', 'world']]
, array(2, array('hello', 'world'), 'hi') , [2, ['hello', 'world'], 'hi']
, array(2, array('hello' => 'world', 'herp' => 'derp')) , [2, ['hello' => 'world', 'herp' => 'derp']]
); ];
} }
/** /**
@ -102,7 +102,7 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
$paramNum = array_shift($args); $paramNum = array_shift($args);
$uri = 'http://example.com/endpoint/' . rand(1, 100); $uri = 'http://example.com/endpoint/' . rand(1, 100);
$id = uniqid(); $id = uniqid('', false);
$clientMessage = array_merge(array(2, $id, $uri), $args); $clientMessage = array_merge(array(2, $id, $uri), $args);
$conn = $this->newConn(); $conn = $this->newConn();
@ -145,8 +145,8 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
public function testPublishAndEligible() { public function testPublishAndEligible() {
$conn = $this->newConn(); $conn = $this->newConn();
$buddy = uniqid(); $buddy = uniqid('', false);
$friend = uniqid(); $friend = uniqid('', false);
$this->_comp->onOpen($conn); $this->_comp->onOpen($conn);
$this->_comp->onMessage($conn, json_encode(array(7, 'topic', 'event', false, array($buddy, $friend)))); $this->_comp->onMessage($conn, json_encode(array(7, 'topic', 'event', false, array($buddy, $friend))));
@ -265,4 +265,31 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
$this->_comp->onOpen($conn); $this->_comp->onOpen($conn);
$this->_comp->onMessage($conn, $message); $this->_comp->onMessage($conn, $message);
} }
public function testBadClientInputFromNonStringTopic() {
$this->setExpectedException('\Ratchet\Wamp\Exception');
$conn = new WampConnection($this->newConn());
$this->_comp->onOpen($conn);
$this->_comp->onMessage($conn, json_encode([5, ['hells', 'nope']]));
}
public function testBadPrefixWithNonStringTopic() {
$this->setExpectedException('\Ratchet\Wamp\Exception');
$conn = new WampConnection($this->newConn());
$this->_comp->onOpen($conn);
$this->_comp->onMessage($conn, json_encode([1, ['hells', 'nope'], ['bad', 'input']]));
}
public function testBadPublishWithNonStringTopic() {
$this->setExpectedException('\Ratchet\Wamp\Exception');
$conn = new WampConnection($this->newConn());
$this->_comp->onOpen($conn);
$this->_comp->onMessage($conn, json_encode([7, ['bad', 'input'], 'Hider']));
}
} }

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