diff --git a/.travis.yml b/.travis.yml index f0b9273..6c0dc15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,20 @@ language: php php: - - 5.3 - 5.4 - 5.5 - 5.6 - - 7 + - 7.0 + - 7.1 - hhvm +dist: trusty + +matrix: + allow_failures: + - php: hhvm + before_script: - 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf1597..5169993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,138 +1,135 @@ CHANGELOG ========= -###Legend +### Legend * "BC": Backwards compatibility break (from public component APIs) * "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) - * BF: Keep host and scheme in HTTP request object attatched to connection - * BF: Return correct HTTP response (405) when non-GET request made + * BF: Keep host and scheme in HTTP request object attatched to connection + * BF: Return correct HTTP response (405) when non-GET request made * 0.3.5 (2016-05-25) - - * BF: Unmask responding close frame - * Added write handler for PHP session serializer + * BF: Unmask responding close frame + * Added write handler for PHP session serializer * 0.3.4 (2015-12-23) - - * BF: Edge case where version check wasn't run on message coalesce - * BF: Session didn't start when using pdo_sqlite - * BF: WAMP currie prefix check when using '#' - * Compatibility with Symfony 3 + * BF: Edge case where version check wasn't run on message coalesce + * BF: Session didn't start when using pdo_sqlite + * BF: WAMP currie prefix check when using '#' + * Compatibility with Symfony 3 * 0.3.3 (2015-05-26) - - * BF: Framing bug on large messages upon TCP fragmentation - * BF: Symfony Router query parameter defaults applied to Request - * BF: WAMP CURIE on all URIs - * OriginCheck rules applied to FlashPolicy - * Switched from PSR-0 to PSR-4 + * BF: Framing bug on large messages upon TCP fragmentation + * BF: Symfony Router query parameter defaults applied to Request + * BF: WAMP CURIE on all URIs + * OriginCheck rules applied to FlashPolicy + * Switched from PSR-0 to PSR-4 * 0.3.2 (2014-06-08) - - * BF: No messages after closing handshake (fixed rare race condition causing 100% CPU) - * BF: Fixed accidental BC break from v0.3.1 - * Added autoDelete parameter to Topic to destroy when empty of connections - * Exposed React Socket on IoServer (allowing FlashPolicy shutdown in App) - * Normalized Exceptions in WAMP + * BF: No messages after closing handshake (fixed rare race condition causing 100% CPU) + * BF: Fixed accidental BC break from v0.3.1 + * Added autoDelete parameter to Topic to destroy when empty of connections + * Exposed React Socket on IoServer (allowing FlashPolicy shutdown in App) + * Normalized Exceptions in WAMP * 0.3.1 (2014-05-26) - - * Added query parameter support to Router, set in HTTP request (ws://server?hello=world) - * HHVM compatibility - * BF: React/0.4 support; CPU starvation bug fixes - * BF: Allow App::route to ignore Host header - * Added expected filters to WAMP Topic broadcast method - * Resource cleanup in WAMP TopicManager + * Added query parameter support to Router, set in HTTP request (ws://server?hello=world) + * HHVM compatibility + * BF: React/0.4 support; CPU starvation bug fixes + * BF: Allow App::route to ignore Host header + * Added expected filters to WAMP Topic broadcast method + * Resource cleanup in WAMP TopicManager * 0.3.0 (2013-10-14) - - * 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 - * 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 - * BF: Single sub-protocol selection to conform with RFC6455 - * BF: Sanity checks on WAMP protocol to prevent errors + * 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 + * 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 + * BF: Single sub-protocol selection to conform with RFC6455 + * BF: Sanity checks on WAMP protocol to prevent errors * 0.2.8 (2013-09-19) - - * React 0.3 support + * React 0.3 support * 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) - - * Guzzle 3.6 support + * Guzzle 3.6 support * 0.2.5 (2013-04-01) - - * Fixed Hixie-76 handshake bug + * Fixed Hixie-76 handshake bug * 0.2.4 (2013-03-09) - - * Support for Symfony 2.2 and Guzzle 2.3 - * Minor bug fixes when handling errors + * Support for Symfony 2.2 and Guzzle 2.3 + * Minor bug fixes when handling errors * 0.2.3 (2012-11-21) - - * Bumped dep: Guzzle to v3, React to v0.2.4 - * More tests + * Bumped dep: Guzzle to v3, React to v0.2.4 + * More tests * 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) - - * BF: No more UTF-8 warnings in browsers (no longer sending empty sub-protocol string) - * Documentation corrections - * Using new composer structure + * BF: No more UTF-8 warnings in browsers (no longer sending empty sub-protocol string) + * Documentation corrections + * Using new composer structure * 0.2 (2012-09-07) - - * Ratchet passes every non-binary-frame test from the Autobahn Testsuite - * Major performance improvements - * BC: Renamed "WampServer" to "ServerProtocol" - * BC: New "WampServer" component passes Topic container objects of subscribed Connections - * Option to turn off UTF-8 checks in order to increase performance - * Switched dependency guzzle/guzzle to guzzle/http (no API changes) - * mbstring no longer required + * Ratchet passes every non-binary-frame test from the Autobahn Testsuite + * Major performance improvements + * BC: Renamed "WampServer" to "ServerProtocol" + * BC: New "WampServer" component passes Topic container objects of subscribed Connections + * Option to turn off UTF-8 checks in order to increase performance + * Switched dependency guzzle/guzzle to guzzle/http (no API changes) + * mbstring no longer required * 0.1.5 (2012-07-12) - - * BF: Error where service wouldn't run on PHP <= 5.3.8 - * Dependency library updates + * BF: Error where service wouldn't run on PHP <= 5.3.8 + * Dependency library updates * 0.1.4 (2012-06-17) - - * Fixed dozens of failing AB tests - * BF: Proper socket buffer handling + * Fixed dozens of failing AB tests + * BF: Proper socket buffer handling * 0.1.3 (2012-06-15) - - * Major refactor inside WebSocket protocol handling, more loosley coupled - * BF: Proper error handling on failed WebSocket connections - * BF: Handle TCP message concatenation - * Inclusion of the AutobahnTestSuite checking WebSocket protocol compliance - * mb_string now a requirement + * Major refactor inside WebSocket protocol handling, more loosley coupled + * BF: Proper error handling on failed WebSocket connections + * BF: Handle TCP message concatenation + * Inclusion of the AutobahnTestSuite checking WebSocket protocol compliance + * mb_string now a requirement * 0.1.2 (2012-05-19) - - * BC/BF: Updated WAMP API to coincide with the official spec - * Tweaks to improve running as a long lived process + * BC/BF: Updated WAMP API to coincide with the official spec + * Tweaks to improve running as a long lived process * 0.1.1 (2012-05-14) - - * Separated interfaces allowing WebSockets to support multiple sub protocols - * BF: remoteAddress variable on connections returns proper value + * Separated interfaces allowing WebSockets to support multiple sub protocols + * BF: remoteAddress variable on connections returns proper value * 0.1 (2012-05-11) - - * First release with components: IoServer, WsServer, SessionProvider, WampServer, FlashPolicy, IpBlackList - * I/O now handled by React, making Ratchet fully asynchronous + * First release with components: IoServer, WsServer, SessionProvider, WampServer, FlashPolicy, IpBlackList + * I/O now handled by React, making Ratchet fully asynchronous diff --git a/Makefile b/Makefile index 9bd90f0..a2526c0 100644 --- a/Makefile +++ b/Makefile @@ -10,26 +10,33 @@ cover: abtests: 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-noutf8.php 8003 StreamSelect & ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8004 LibEv & wstest -m testeeserver -w ws://localhost:8000 & + sleep 1 wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-all.json killall php wstest abtest: ulimit -n 2048 && php tests/autobahn/bin/fuzzingserver.php 8000 StreamSelect & + sleep 1 wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-quick.json killall php profile: php -d 'xdebug.profiler_enable=1' tests/autobahn/bin/fuzzingserver.php 8000 LibEvent & + sleep 1 wstest -m fuzzingclient -s tests/autobahn/fuzzingclient-profile.json killall php apidocs: - apigen --title Ratchet -d reports/api -s src/ \ - -s vendor/react \ - -s vendor/guzzle \ - -s vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Session \ - -s vendor/symfony/routing/Symfony/Component/Routing \ - -s vendor/evenement/evenement/src/Evenement + apigen --title Ratchet -d reports/api \ + -s src/ \ + -s vendor/ratchet/rfc6455/src \ + -s vendor/react/event-loop/src \ + -s vendor/react/socket/src \ + -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 \ diff --git a/README.md b/README.md index 5cc5976..3f1a345 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,19 @@ -#Ratchet +# 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) -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. -##WebSocket Compliance - -* Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time) -* Tested on Chrome 13+, Firefox 6+, Safari 5+, iOS 4.2+, IE 8+ -* Ratchet [passes](http://socketo.me/reports/ab/) the [Autobahn Testsuite](http://autobahn.ws/testsuite) (non-binary messages) - -##Requirements +## Requirements Shell access is required and root access is recommended. 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. 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 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 =5.3.9" - , "react/socket": "^0.3 || ^0.4" - , "guzzle/http": "^3.6" - , "symfony/http-foundation": "^2.2|^3.0" - , "symfony/routing": "^2.2|^3.0" + "php": ">=5.4.2" + , "ratchet/rfc6455": "^0.2" + , "react/socket": "^1.0 || ^0.8 || ^0.7 || ^0.6 || ^0.5" + , "guzzlehttp/psr7": "^1.0" + , "symfony/http-foundation": "^2.6|^3.0|^4.0" + , "symfony/routing": "^2.6|^3.0|^4.0" + } + , "require-dev": { + "phpunit/phpunit": "~4.8" } } diff --git a/src/Ratchet/App.php b/src/Ratchet/App.php index c52a2c7..f378534 100644 --- a/src/Ratchet/App.php +++ b/src/Ratchet/App.php @@ -3,6 +3,7 @@ namespace Ratchet; use React\EventLoop\LoopInterface; use React\EventLoop\Factory as LoopFactory; use React\Socket\Server as Reactor; +use React\Socket\SecureServer as SecureReactor; use Ratchet\Http\HttpServerInterface; use Ratchet\Http\OriginCheck; use Ratchet\Wamp\WampServerInterface; @@ -55,20 +56,16 @@ class App { protected $_routeCounter = 0; /** - * @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 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 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 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. */ public function __construct($httpHost = 'localhost', $port = 8080, $address = '127.0.0.1', LoopInterface $loop = null) { if (extension_loaded('xdebug')) { 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) { $loop = LoopFactory::create(); } @@ -76,8 +73,7 @@ class App { $this->httpHost = $httpHost; $this->port = $port; - $socket = new Reactor($loop); - $socket->listen($port, $address); + $socket = new Reactor($address . ':' . $port, $loop); $this->routes = new RouteCollection; $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->addAllowedAccess($httpHost, 80); $policy->addAllowedAccess($httpHost, $port); - $flashSock = new Reactor($loop); - $this->flashServer = new IoServer($policy, $flashSock); + if (80 == $port) { - $flashSock->listen(843, '0.0.0.0'); + $flashUri = '0.0.0.0:843'; } 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; } elseif ($controller instanceof WampServerInterface) { $decorated = new WsServer(new WampServer($controller)); + $decorated->enableKeepAlive($this->_server->loop); } elseif ($controller instanceof MessageComponentInterface) { $decorated = new WsServer($controller); + $decorated->enableKeepAlive($this->_server->loop); } else { $decorated = $controller; } diff --git a/src/Ratchet/ConnectionInterface.php b/src/Ratchet/ConnectionInterface.php index 1b3839c..26fb8a4 100644 --- a/src/Ratchet/ConnectionInterface.php +++ b/src/Ratchet/ConnectionInterface.php @@ -5,7 +5,7 @@ namespace Ratchet; * The version of Ratchet being used * @var string */ -const VERSION = 'Ratchet/0.3.6'; +const VERSION = 'Ratchet/0.4.1'; /** * A proxy object representing a connection to the application diff --git a/src/Ratchet/Http/CloseResponseTrait.php b/src/Ratchet/Http/CloseResponseTrait.php new file mode 100644 index 0000000..abdf5c4 --- /dev/null +++ b/src/Ratchet/Http/CloseResponseTrait.php @@ -0,0 +1,22 @@ + \Ratchet\VERSION + ], $additional_headers)); + + $conn->send(gPsr\str($response)); + $conn->close(); + } +} \ No newline at end of file diff --git a/src/Ratchet/Http/Guzzle/Http/Message/RequestFactory.php b/src/Ratchet/Http/Guzzle/Http/Message/RequestFactory.php deleted file mode 100644 index 8f68e5e..0000000 --- a/src/Ratchet/Http/Guzzle/Http/Message/RequestFactory.php +++ /dev/null @@ -1,34 +0,0 @@ -entityEnclosingRequestClass; - $request = new $c($method, $url, $headers); - $request->setBody(EntityBody::factory($body)); - - return $request; - } -} diff --git a/src/Ratchet/Http/HttpRequestParser.php b/src/Ratchet/Http/HttpRequestParser.php index cbb5bbd..9c44114 100644 --- a/src/Ratchet/Http/HttpRequestParser.php +++ b/src/Ratchet/Http/HttpRequestParser.php @@ -2,11 +2,11 @@ namespace Ratchet\Http; use Ratchet\MessageInterface; use Ratchet\ConnectionInterface; -use Ratchet\Http\Guzzle\Http\Message\RequestFactory; +use GuzzleHttp\Psr7 as gPsr; /** * 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 */ class HttpRequestParser implements MessageInterface { @@ -22,7 +22,7 @@ class HttpRequestParser implements MessageInterface { /** * @param \Ratchet\ConnectionInterface $context * @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 */ public function onMessage(ConnectionInterface $context, $data) { @@ -37,7 +37,7 @@ class HttpRequestParser implements MessageInterface { } if ($this->isEom($context->httpBuffer)) { - $request = RequestFactory::getInstance()->fromMessage($context->httpBuffer); + $request = $this->parse($context->httpBuffer); unset($context->httpBuffer); @@ -53,4 +53,12 @@ class HttpRequestParser implements MessageInterface { public function isEom($message) { return (boolean)strpos($message, static::EOM); } + + /** + * @param string $headers + * @return \Psr\Http\Message\RequestInterface + */ + public function parse($headers) { + return gPsr\parse_request($headers); + } } diff --git a/src/Ratchet/Http/HttpServer.php b/src/Ratchet/Http/HttpServer.php index f1b8f69..bbd8d53 100644 --- a/src/Ratchet/Http/HttpServer.php +++ b/src/Ratchet/Http/HttpServer.php @@ -2,9 +2,10 @@ namespace Ratchet\Http; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; -use Guzzle\Http\Message\Response; class HttpServer implements MessageComponentInterface { + use CloseResponseTrait; + /** * Buffers incoming HTTP requests returning a Guzzle Request when coalesced * @var HttpRequestParser @@ -72,19 +73,4 @@ class HttpServer implements MessageComponentInterface { $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(); - } } diff --git a/src/Ratchet/Http/HttpServerInterface.php b/src/Ratchet/Http/HttpServerInterface.php index 79b7d55..2c37c49 100644 --- a/src/Ratchet/Http/HttpServerInterface.php +++ b/src/Ratchet/Http/HttpServerInterface.php @@ -2,12 +2,12 @@ namespace Ratchet\Http; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; -use Guzzle\Http\Message\RequestInterface; +use Psr\Http\Message\RequestInterface; interface HttpServerInterface extends MessageComponentInterface { /** * @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 */ public function onOpen(ConnectionInterface $conn, RequestInterface $request = null); diff --git a/src/Ratchet/Http/NoOpHttpServerController.php b/src/Ratchet/Http/NoOpHttpServerController.php new file mode 100644 index 0000000..4f72e66 --- /dev/null +++ b/src/Ratchet/Http/NoOpHttpServerController.php @@ -0,0 +1,18 @@ +_component = $component; $this->allowedOrigins += $allowed; } @@ -31,7 +32,7 @@ class OriginCheck implements HttpServerInterface { * {@inheritdoc} */ 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; if (!in_array($origin, $this->allowedOrigins)) { @@ -61,19 +62,4 @@ class OriginCheck implements HttpServerInterface { function onError(ConnectionInterface $conn, \Exception $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(); - } -} +} \ No newline at end of file diff --git a/src/Ratchet/Http/Router.php b/src/Ratchet/Http/Router.php index 7839295..df7fe82 100644 --- a/src/Ratchet/Http/Router.php +++ b/src/Ratchet/Http/Router.php @@ -1,21 +1,25 @@ _matcher = $matcher; + $this->_noopController = new NoOpHttpServerController; } /** @@ -27,12 +31,16 @@ class Router implements HttpServerInterface { throw new \UnexpectedValueException('$request can not be null'); } + $conn->controller = $this->_noopController; + + $uri = $request->getUri(); + $context = $this->_matcher->getContext(); $context->setMethod($request->getMethod()); - $context->setHost($request->getHost()); + $context->setHost($uri->getHost()); try { - $route = $this->_matcher->match($request->getPath()); + $route = $this->_matcher->match($uri->getPath()); } catch (MethodNotAllowedException $nae) { return $this->close($conn, 405, array('Allow' => $nae->getAllowedMethods())); } catch (ResourceNotFoundException $nfe) { @@ -47,17 +55,15 @@ class Router implements HttpServerInterface { throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface'); } - $parameters = array(); + $parameters = []; foreach($route as $key => $value) { if ((is_string($key)) && ('_' !== substr($key, 0, 1))) { $parameters[$key] = $value; } } - $parameters = array_merge($parameters, $request->getQuery()->getAll()); + $parameters = array_merge($parameters, gPsr\parse_query($uri->getQuery() ?: '')); - $url = Url::factory($request->getUrl()); - $url->setQuery($parameters); - $request->setUrl($url); + $request = $request->withUri($uri->withQuery(gPsr\build_query($parameters))); $conn->controller = $route['_controller']; $conn->controller->onOpen($conn, $request); @@ -66,14 +72,14 @@ class Router implements HttpServerInterface { /** * {@inheritdoc} */ - function onMessage(ConnectionInterface $from, $msg) { + public function onMessage(ConnectionInterface $from, $msg) { $from->controller->onMessage($from, $msg); } /** * {@inheritdoc} */ - function onClose(ConnectionInterface $conn) { + public function onClose(ConnectionInterface $conn) { if (isset($conn->controller)) { $conn->controller->onClose($conn); } @@ -82,26 +88,9 @@ class Router implements HttpServerInterface { /** * {@inheritdoc} */ - function onError(ConnectionInterface $conn, \Exception $e) { + public function onError(ConnectionInterface $conn, \Exception $e) { if (isset($conn->controller)) { $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(); - } } diff --git a/src/Ratchet/Server/IoServer.php b/src/Ratchet/Server/IoServer.php index 921c7b1..b3fb7e0 100644 --- a/src/Ratchet/Server/IoServer.php +++ b/src/Ratchet/Server/IoServer.php @@ -5,6 +5,7 @@ use React\EventLoop\LoopInterface; use React\Socket\ServerInterface; use React\EventLoop\Factory as LoopFactory; 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. @@ -21,12 +22,6 @@ class IoServer { */ public $app; - /** - * Array of React event handlers - * @var \SplFixedArray - */ - protected $handlers; - /** * The socket server the Ratchet Application is run off of * @var \React\Socket\ServerInterface @@ -51,23 +46,17 @@ class IoServer { $this->socket = $socket; $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 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 \Ratchet\MessageComponentInterface $component The application that I/O will call when events are received + * @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) * @return IoServer */ public static function factory(MessageComponentInterface $component, $port = 80, $address = '0.0.0.0') { $loop = LoopFactory::create(); - $socket = new Reactor($loop); - $socket->listen($port, $address); + $socket = new Reactor($address . ':' . $port, $loop); return new static($component, $socket, $loop); } @@ -92,15 +81,25 @@ class IoServer { */ public function handleConnect($conn) { $conn->decor = new IoConnection($conn); + $conn->decor->resourceId = (int)$conn->stream; - $conn->decor->resourceId = (int)$conn->stream; - $conn->decor->remoteAddress = $conn->getRemoteAddress(); + $uri = $conn->getRemoteAddress(); + $conn->decor->remoteAddress = trim( + parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri, PHP_URL_HOST), + '[]' + ); $this->app->onOpen($conn->decor); - $conn->on('data', $this->handlers[0]); - $conn->on('end', $this->handlers[1]); - $conn->on('error', $this->handlers[2]); + $conn->on('data', function ($data) use ($conn) { + $this->handleData($data, $conn); + }); + $conn->on('close', function () use ($conn) { + $this->handleEnd($conn); + }); + $conn->on('error', function (\Exception $e) use ($conn) { + $this->handleError($e, $conn); + }); } /** diff --git a/src/Ratchet/Session/SessionProvider.php b/src/Ratchet/Session/SessionProvider.php index a8495b2..44276c5 100644 --- a/src/Ratchet/Session/SessionProvider.php +++ b/src/Ratchet/Session/SessionProvider.php @@ -1,8 +1,8 @@ _app = $app; $this->_handler = $handler; $this->_null = new NullSessionHandler; @@ -70,8 +70,20 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface { /** * {@inheritdoc} */ - function onOpen(ConnectionInterface $conn) { - if (!isset($conn->WebSocket) || null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) { + public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) { + $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; $id = ''; } else { @@ -84,7 +96,7 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface { $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); } - /** - * {@inheritdoc} - */ - public function getSubProtocols() { - if ($this->_app instanceof WsServerInterface) { - return $this->_app->getSubProtocols(); - } else { - return array(); - } - } - /** * Set all the php session. ini options * © Symfony @@ -158,4 +159,85 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface { protected function toClassCase($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; + } } diff --git a/src/Ratchet/Wamp/ServerProtocol.php b/src/Ratchet/Wamp/ServerProtocol.php index 28badd3..2d6d799 100644 --- a/src/Ratchet/Wamp/ServerProtocol.php +++ b/src/Ratchet/Wamp/ServerProtocol.php @@ -8,7 +8,7 @@ use Ratchet\ConnectionInterface; * WebSocket Application Messaging Protocol * * @link http://wamp.ws/spec - * @link https://github.com/oberstet/AutobahnJS + * @link https://github.com/oberstet/autobahn-js * * +--------------+----+------------------+ * | Message Type | ID | DIRECTION | @@ -62,9 +62,9 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface { $subs[] = 'wamp'; return $subs; - } else { - return array('wamp'); } + + return ['wamp']; } /** @@ -93,6 +93,10 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface { 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]) { case static::MSG_PREFIX: $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); if (!is_array($exclude)) { if (true === (boolean)$exclude) { - $exclude = array($from->WAMP->sessionId); + $exclude = [$from->WAMP->sessionId]; } 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); break; diff --git a/src/Ratchet/Wamp/Topic.php b/src/Ratchet/Wamp/Topic.php index 3fe73d1..bca8f67 100644 --- a/src/Ratchet/Wamp/Topic.php +++ b/src/Ratchet/Wamp/Topic.php @@ -6,13 +6,6 @@ use Ratchet\ConnectionInterface; * A topic/channel containing connections that have subscribed to it */ 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 $subscribers; @@ -38,7 +31,7 @@ class Topic implements \IteratorAggregate, \Countable { /** * 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 $eligible A list of session Ids the message should be send to (whitelist) * @return Topic The same Topic object to chain diff --git a/src/Ratchet/Wamp/TopicManager.php b/src/Ratchet/Wamp/TopicManager.php index 318b986..dd06ada 100644 --- a/src/Ratchet/Wamp/TopicManager.php +++ b/src/Ratchet/Wamp/TopicManager.php @@ -118,7 +118,7 @@ class TopicManager implements WsServerInterface, WampServerInterface { $this->topicLookup[$topic->getId()]->remove($conn); - if ($topic->autoDelete && 0 === $topic->count()) { + if (0 === $topic->count()) { unset($this->topicLookup[$topic->getId()]); } } diff --git a/src/Ratchet/Wamp/WampServer.php b/src/Ratchet/Wamp/WampServer.php index f0675a3..5d710aa 100644 --- a/src/Ratchet/Wamp/WampServer.php +++ b/src/Ratchet/Wamp/WampServer.php @@ -8,7 +8,7 @@ use Ratchet\ConnectionInterface; * Enable support for the official WAMP sub-protocol in your application * WAMP allows for Pub/Sub and RPC * @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 */ class WampServer implements MessageComponentInterface, WsServerInterface { diff --git a/src/Ratchet/WebSocket/ConnContext.php b/src/Ratchet/WebSocket/ConnContext.php new file mode 100644 index 0000000..2eba782 --- /dev/null +++ b/src/Ratchet/WebSocket/ConnContext.php @@ -0,0 +1,20 @@ +connection = $conn; + $this->buffer = $buffer; + } +} diff --git a/src/Ratchet/WebSocket/Encoding/ToggleableValidator.php b/src/Ratchet/WebSocket/Encoding/ToggleableValidator.php deleted file mode 100644 index edf14bc..0000000 --- a/src/Ratchet/WebSocket/Encoding/ToggleableValidator.php +++ /dev/null @@ -1,31 +0,0 @@ -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); - } -} diff --git a/src/Ratchet/WebSocket/Encoding/Validator.php b/src/Ratchet/WebSocket/Encoding/Validator.php deleted file mode 100644 index 3b02230..0000000 --- a/src/Ratchet/WebSocket/Encoding/Validator.php +++ /dev/null @@ -1,93 +0,0 @@ -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; - } -} diff --git a/src/Ratchet/WebSocket/Encoding/ValidatorInterface.php b/src/Ratchet/WebSocket/Encoding/ValidatorInterface.php deleted file mode 100644 index 374f220..0000000 --- a/src/Ratchet/WebSocket/Encoding/ValidatorInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -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); - } -} diff --git a/src/Ratchet/WebSocket/Version/Hixie76/Connection.php b/src/Ratchet/WebSocket/Version/Hixie76/Connection.php deleted file mode 100644 index e3d0834..0000000 --- a/src/Ratchet/WebSocket/Version/Hixie76/Connection.php +++ /dev/null @@ -1,26 +0,0 @@ -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; - } - } -} diff --git a/src/Ratchet/WebSocket/Version/Hixie76/Frame.php b/src/Ratchet/WebSocket/Version/Hixie76/Frame.php deleted file mode 100644 index 28eb90e..0000000 --- a/src/Ratchet/WebSocket/Version/Hixie76/Frame.php +++ /dev/null @@ -1,86 +0,0 @@ -_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 ''; - } -} diff --git a/src/Ratchet/WebSocket/Version/HyBi10.php b/src/Ratchet/WebSocket/Version/HyBi10.php deleted file mode 100644 index a53d338..0000000 --- a/src/Ratchet/WebSocket/Version/HyBi10.php +++ /dev/null @@ -1,15 +0,0 @@ -getHeader('Sec-WebSocket-Version'); - - return ($version >= 6 && $version < 13); - } - - public function getVersionNumber() { - return 6; - } -} diff --git a/src/Ratchet/WebSocket/Version/MessageInterface.php b/src/Ratchet/WebSocket/Version/MessageInterface.php deleted file mode 100644 index 476c091..0000000 --- a/src/Ratchet/WebSocket/Version/MessageInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -_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; - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php deleted file mode 100644 index 77d1258..0000000 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ /dev/null @@ -1,451 +0,0 @@ -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 ''; - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php b/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php deleted file mode 100644 index fd783f6..0000000 --- a/src/Ratchet/WebSocket/Version/RFC6455/HandshakeVerifier.php +++ /dev/null @@ -1,137 +0,0 @@ -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) { - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Message.php b/src/Ratchet/WebSocket/Version/RFC6455/Message.php deleted file mode 100644 index a839f2d..0000000 --- a/src/Ratchet/WebSocket/Version/RFC6455/Message.php +++ /dev/null @@ -1,107 +0,0 @@ -_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; - } -} diff --git a/src/Ratchet/WebSocket/Version/VersionInterface.php b/src/Ratchet/WebSocket/Version/VersionInterface.php deleted file mode 100644 index 5bbe534..0000000 --- a/src/Ratchet/WebSocket/Version/VersionInterface.php +++ /dev/null @@ -1,57 +0,0 @@ -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; - } -} diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php b/src/Ratchet/WebSocket/WsConnection.php similarity index 78% rename from src/Ratchet/WebSocket/Version/RFC6455/Connection.php rename to src/Ratchet/WebSocket/WsConnection.php index a17e382..d2d04ef 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php +++ b/src/Ratchet/WebSocket/WsConnection.php @@ -1,13 +1,14 @@ WebSocket->closing) { diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 5783789..8030604 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/WsServer.php @@ -1,12 +1,20 @@ versioner = new VersionManager; - $this->validator = new ToggleableValidator; + private $pongReceiver; - $this->versioner - ->enableVersion(new Version\RFC6455($this->validator)) - ->enableVersion(new Version\HyBi10($this->validator)) - ->enableVersion(new Version\Hixie76) - ; + /** + * @var \Closure + */ + 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->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'); } - $conn->WebSocket = new \StdClass; - $conn->WebSocket->request = $request; - $conn->WebSocket->established = false; - $conn->WebSocket->closing = false; + $conn->httpRequest = $request; - $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; } - if (true === $from->WebSocket->established) { - 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); + $this->connections[$from]->buffer->onData($msg); } /** @@ -143,10 +158,10 @@ class WsServer implements HttpServerInterface { */ public function onClose(ConnectionInterface $conn) { if ($this->connections->contains($conn)) { - $decor = $this->connections[$conn]; + $context = $this->connections[$conn]; $this->connections->detach($conn); - $this->component->onClose($decor); + $this->delegate->onClose($context->connection); } } @@ -154,79 +169,57 @@ class WsServer implements HttpServerInterface { * {@inheritdoc} */ public function onError(ConnectionInterface $conn, \Exception $e) { - if ($conn->WebSocket->established && $this->connections->contains($conn)) { - $this->component->onError($this->connections[$conn], $e); + if ($this->connections->contains($conn)) { + $this->delegate->onError($this->connections[$conn]->connection, $e); } else { $conn->close(); } } - /** - * Disable a specific version of the WebSocket protocol - * @param int $versionId Version ID to disable - * @return WsServer - */ - public function disableVersion($versionId) { - $this->versioner->disableVersion($versionId); - - return $this; - } - - /** - * 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; + public function onControlFrame(FrameInterface $frame, WsConnection $conn) { + switch ($frame->getOpCode()) { + case Frame::OP_CLOSE: + $conn->close($frame); + break; + case Frame::OP_PING: + $conn->send(new Frame($frame->getPayload(), true, Frame::OP_PONG)); + break; + case Frame::OP_PONG: + $pongReceiver = $this->pongReceiver; + $pongReceiver($frame, $conn); + break; } - - return array_key_exists($name, $this->acceptedSubProtocols); } - /** - * @param \Traversable|null $requested - * @return string - */ - protected function getSubProtocolString(\Traversable $requested = null) { - if (null !== $requested) { - foreach ($requested as $sub) { - if ($this->isSubProtocolSupported($sub)) { - return $sub; - } + public function setStrictSubProtocolCheck($enable) { + $this->handshakeNegotiator->setStrictSubProtocolCheck($enable); + } + + public function enableKeepAlive(LoopInterface $loop, $interval = 30) { + $lastPing = new Frame(uniqid(), true, Frame::OP_PING); + $pingedConnections = new \SplObjectStorage; + $splClearer = new \SplObjectStorage; + + $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); - /** - * 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 - )); + $lastPing = new Frame(uniqid(), true, Frame::OP_PING); - $conn->send((string)$response); - $conn->close(); - } + foreach ($this->connections as $key => $conn) { + $wsConn = $this->connections[$conn]->connection; + + $wsConn->send($lastPing); + $pingedConnections->attach($wsConn); + } + }); + } } diff --git a/tests/autobahn/bin/fuzzingserver-noutf8.php b/tests/autobahn/bin/fuzzingserver-noutf8.php deleted file mode 100644 index 5ce1cb4..0000000 --- a/tests/autobahn/bin/fuzzingserver-noutf8.php +++ /dev/null @@ -1,17 +0,0 @@ - 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(); diff --git a/tests/autobahn/bin/fuzzingserver.php b/tests/autobahn/bin/fuzzingserver.php index 093a5cf..66d3704 100644 --- a/tests/autobahn/bin/fuzzingserver.php +++ b/tests/autobahn/bin/fuzzingserver.php @@ -1,15 +1,36 @@ 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; $impl = sprintf('React\EventLoop\%sLoop', $argc > 2 ? $argv[2] : 'StreamSelect'); $loop = new $impl; - $sock = new React\Socket\Server($loop); - $app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Server\EchoServer)); + $sock = new React\Socket\Server('0.0.0.0:' . $port, $loop); - $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->run(); diff --git a/tests/autobahn/fuzzingclient-all.json b/tests/autobahn/fuzzingclient-all.json index dd9cddb..0494cf3 100644 --- a/tests/autobahn/fuzzingclient-all.json +++ b/tests/autobahn/fuzzingclient-all.json @@ -3,14 +3,13 @@ , "outdir": "reports/ab" , "servers": [ - {"agent": "Ratchet/0.3 libevent", "url": "ws://localhost:8001", "options": {"version": 18}} - , {"agent": "Ratchet/0.3 libev", "url": "ws://localhost:8004", "options": {"version": 18}} - , {"agent": "Ratchet/0.3 streams", "url": "ws://localhost:8002", "options": {"version": 18}} - , {"agent": "Ratchet/0.3 -utf8", "url": "ws://localhost:8003", "options": {"version": 18}} + {"agent": "Ratchet/0.4 libevent", "url": "ws://localhost:8001", "options": {"version": 18}} + , {"agent": "Ratchet/0.4 libev", "url": "ws://localhost:8004", "options": {"version": 18}} + , {"agent": "Ratchet/0.4 streams", "url": "ws://localhost:8002", "options": {"version": 18}} , {"agent": "AutobahnTestSuite/0.5.9", "url": "ws://localhost:8000", "options": {"version": 18}} ] , "cases": ["*"] - , "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] + , "exclude-cases": [] , "exclude-agent-cases": {} } diff --git a/tests/autobahn/fuzzingclient-quick.json b/tests/autobahn/fuzzingclient-quick.json index 3023fb8..c92e805 100644 --- a/tests/autobahn/fuzzingclient-quick.json +++ b/tests/autobahn/fuzzingclient-quick.json @@ -7,6 +7,6 @@ ] , "cases": ["*"] - , "exclude-cases": ["1.2.*", "2.3", "2.4", "2.6", "9.2.*", "9.4.*", "9.6.*", "9.8.*"] + , "exclude-cases": [] , "exclude-agent-cases": {} } diff --git a/tests/integration/GuzzleTest.php b/tests/integration/GuzzleTest.php deleted file mode 100644 index 5e4d8aa..0000000 --- a/tests/integration/GuzzleTest.php +++ /dev/null @@ -1,53 +0,0 @@ - '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()); - } -} diff --git a/tests/unit/Http/Guzzle/Http/Message/RequestFactoryTest.php b/tests/unit/Http/Guzzle/Http/Message/RequestFactoryTest.php deleted file mode 100644 index 7860f72..0000000 --- a/tests/unit/Http/Guzzle/Http/Message/RequestFactoryTest.php +++ /dev/null @@ -1,67 +0,0 @@ -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()); - } -} diff --git a/tests/unit/Http/HttpRequestParserTest.php b/tests/unit/Http/HttpRequestParserTest.php index 4df7d8d..6af8402 100644 --- a/tests/unit/Http/HttpRequestParserTest.php +++ b/tests/unit/Http/HttpRequestParserTest.php @@ -1,6 +1,5 @@ getMock('\Ratchet\ConnectionInterface'); $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); } } diff --git a/tests/unit/Http/OriginCheckTest.php b/tests/unit/Http/OriginCheckTest.php index 34db439..c1c4012 100644 --- a/tests/unit/Http/OriginCheckTest.php +++ b/tests/unit/Http/OriginCheckTest.php @@ -9,8 +9,8 @@ class OriginCheckTest extends AbstractMessageComponentTestCase { protected $_reqStub; public function setUp() { - $this->_reqStub = $this->getMock('Guzzle\Http\Message\RequestInterface'); - $this->_reqStub->expects($this->any())->method('getHeader')->will($this->returnValue('localhost')); + $this->_reqStub = $this->getMock('Psr\Http\Message\RequestInterface'); + $this->_reqStub->expects($this->any())->method('getHeader')->will($this->returnValue(['localhost'])); parent::setUp(); @@ -34,7 +34,7 @@ class OriginCheckTest extends AbstractMessageComponentTestCase { } public function testCloseOnNonMatchingOrigin() { - $this->_serv->allowedOrigins = array('socketo.me'); + $this->_serv->allowedOrigins = ['socketo.me']; $this->_conn->expects($this->once())->method('close'); $this->_serv->onOpen($this->_conn, $this->_reqStub); diff --git a/tests/unit/Http/RouterTest.php b/tests/unit/Http/RouterTest.php index 2b07cf7..1ca4cbc 100644 --- a/tests/unit/Http/RouterTest.php +++ b/tests/unit/Http/RouterTest.php @@ -1,9 +1,12 @@ getMock('Guzzle\Http\QueryString'); - $queryMock - ->expects($this->any()) - ->method('getAll') - ->will($this->returnValue(array())); - - $this->_conn = $this->getMock('\Ratchet\ConnectionInterface'); - $this->_req = $this->getMock('\Guzzle\Http\Message\RequestInterface'); + $this->_conn = $this->getMock('\Ratchet\ConnectionInterface'); + $this->_uri = $this->getMock('Psr\Http\Message\UriInterface'); + $this->_req = $this->getMock('\Psr\Http\Message\RequestInterface'); $this->_req ->expects($this->any()) - ->method('getQuery') - ->will($this->returnValue($queryMock)); + ->method('getUri') + ->will($this->returnValue($this->_uri)); $this->_matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); $this->_matcher ->expects($this->any()) @@ -34,7 +33,14 @@ class RouterTest extends \PHPUnit_Framework_TestCase { ->will($this->returnValue($this->getMock('Symfony\Component\Routing\RequestContext'))); $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() { @@ -103,41 +109,57 @@ class RouterTest extends \PHPUnit_Framework_TestCase { $controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); /** @var $matcher UrlMatcherInterface */ $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'); - $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->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() { $controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); $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'); - /**@var $request \Guzzle\Http\Message\Request */ - $request = $this->getMock('Guzzle\Http\Message\Request', array('getPath'), array('GET', ''), '', false); + $request = $this->getMock('Psr\Http\Message\RequestInterface'); + $uri = new \GuzzleHttp\Psr7\Uri('ws://doesnt.matter/endpoint?hello=world&foo=nope'); - $request->setHeaderFactory($this->getMock('Guzzle\Http\Message\Header\HeaderFactoryInterface')); - $request->setUrl('ws://doesnt.matter?hello=world&foo=nope'); + $request->expects($this->any())->method('getUri')->will($this->returnCallback(function() use (&$uri) { + 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->onOpen($conn, $request); - $this->assertEquals(array('foo' => 'nope', 'baz' => 'qux', 'hello' => 'world'), $request->getQuery()->getAll()); - $this->assertEquals('ws', $request->getScheme()); - $this->assertEquals('doesnt.matter', $request->getHost()); + $this->assertEquals('foo=nope&baz=qux&hello=world', $request->getUri()->getQuery()); + $this->assertEquals('ws', $request->getUri()->getScheme()); + $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'); } } diff --git a/tests/unit/Server/IoServerTest.php b/tests/unit/Server/IoServerTest.php index 808098a..284fbde 100644 --- a/tests/unit/Server/IoServerTest.php +++ b/tests/unit/Server/IoServerTest.php @@ -20,10 +20,10 @@ class IoServerTest extends \PHPUnit_Framework_TestCase { $this->app = $this->getMock('\\Ratchet\\MessageComponentInterface'); $loop = new StreamSelectLoop; - $this->reactor = new Server($loop); - $this->reactor->listen(0); + $this->reactor = new Server(0, $loop); - $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); } diff --git a/tests/unit/Session/SessionComponentTest.php b/tests/unit/Session/SessionComponentTest.php index 7f91639..ebfdde4 100644 --- a/tests/unit/Session/SessionComponentTest.php +++ b/tests/unit/Session/SessionComponentTest.php @@ -1,11 +1,8 @@ getMethod('toClassCase'); $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))); } @@ -82,16 +79,13 @@ class SessionProviderTest extends AbstractMessageComponentTestCase { $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:{}'); - $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'); - $headers = $this->getMock('Guzzle\\Http\\Message\\Request', array('getCookie'), array('POST', '/', array())); - $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue($sessionId)); + $headers = $this->getMock('Psr\Http\Message\RequestInterface'); + $headers->expects($this->once())->method('getHeader')->will($this->returnValue([ini_get('session.name') . "={$sessionId};"])); - $connection->WebSocket = new \StdClass; - $connection->WebSocket->request = $headers; - - $component->onOpen($connection); + $component->onOpen($connection, $headers); $this->assertEquals('world', $connection->Session->get('hello')); } @@ -99,12 +93,9 @@ class SessionProviderTest extends AbstractMessageComponentTestCase { protected function newConn() { $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)); - $conn->WebSocket = new \StdClass; - $conn->WebSocket->request = $headers; - return $conn; } @@ -114,21 +105,6 @@ class SessionProviderTest extends AbstractMessageComponentTestCase { $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() { if (!function_exists('wddx_serialize_value')) { $this->markTestSkipped(); @@ -136,6 +112,13 @@ class SessionProviderTest extends AbstractMessageComponentTestCase { ini_set('session.serialize_handler', 'wddx'); $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); } } diff --git a/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php b/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php index 9909bce..2727484 100644 --- a/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php +++ b/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php @@ -1,15 +1,11 @@ markTestSkipped('Session test requires PDO and pdo_sqlite'); + } + $schema = <<_virtualSessionStorage->registerBag(new AttributeBag()); } - public function tearDown() - { + public function tearDown() { unlink($this->_pathToDB); } - public function testStartWithDSN() - { + public function testStartWithDSN() { $this->_virtualSessionStorage->start(); $this->assertTrue($this->_virtualSessionStorage->isStarted()); } - - } diff --git a/tests/unit/Wamp/ServerProtocolTest.php b/tests/unit/Wamp/ServerProtocolTest.php index 082a3f5..8ff68c2 100644 --- a/tests/unit/Wamp/ServerProtocolTest.php +++ b/tests/unit/Wamp/ServerProtocolTest.php @@ -4,9 +4,9 @@ use Ratchet\Mock\Connection; use Ratchet\Mock\WampComponent as TestComponent; /** - * @covers Ratchet\Wamp\ServerProtocol - * @covers Ratchet\Wamp\WampServerInterface - * @covers Ratchet\Wamp\WampConnection + * @covers \Ratchet\Wamp\ServerProtocol + * @covers \Ratchet\Wamp\WampServerInterface + * @covers \Ratchet\Wamp\WampConnection */ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { protected $_comp; @@ -23,13 +23,13 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { } public function invalidMessageProvider() { - return array( - array(0) - , array(3) - , array(4) - , array(8) - , array(9) - ); + return [ + [0] + , [3] + , [4] + , [8] + , [9] + ]; } /** @@ -40,7 +40,7 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { $conn = $this->newConn(); $this->_comp->onOpen($conn); - $this->_comp->onMessage($conn, json_encode(array($type))); + $this->_comp->onMessage($conn, json_encode([$type])); } public function testWelcomeMessage() { @@ -82,16 +82,16 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { } public function callProvider() { - return array( - array(2, 'a', 'b') - , array(2, array('a', 'b')) - , array(1, 'one') - , array(3, 'one', 'two', 'three') - , array(3, array('un', 'deux', 'trois')) - , array(2, 'hi', array('hello', 'world')) - , array(2, array('hello', 'world'), 'hi') - , array(2, array('hello' => 'world', 'herp' => 'derp')) - ); + return [ + [2, 'a', 'b'] + , [2, ['a', 'b']] + , [1, 'one'] + , [3, 'one', 'two', 'three'] + , [3, ['un', 'deux', 'trois']] + , [2, 'hi', ['hello', 'world']] + , [2, ['hello', 'world'], 'hi'] + , [2, ['hello' => 'world', 'herp' => 'derp']] + ]; } /** @@ -102,7 +102,7 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { $paramNum = array_shift($args); $uri = 'http://example.com/endpoint/' . rand(1, 100); - $id = uniqid(); + $id = uniqid('', false); $clientMessage = array_merge(array(2, $id, $uri), $args); $conn = $this->newConn(); @@ -145,8 +145,8 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { public function testPublishAndEligible() { $conn = $this->newConn(); - $buddy = uniqid(); - $friend = uniqid(); + $buddy = uniqid('', false); + $friend = uniqid('', false); $this->_comp->onOpen($conn); $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->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'])); + } } diff --git a/tests/unit/Wamp/TopicManagerTest.php b/tests/unit/Wamp/TopicManagerTest.php index 8482877..b21b6bc 100644 --- a/tests/unit/Wamp/TopicManagerTest.php +++ b/tests/unit/Wamp/TopicManagerTest.php @@ -185,21 +185,18 @@ class TopicManagerTest extends \PHPUnit_Framework_TestCase { } public static function topicConnExpectationProvider() { - return array( - array(true, 'onClose', 0) - , array(true, 'onUnsubscribe', 0) - , array(false, 'onClose', 1) - , array(false, 'onUnsubscribe', 1) - ); + return [ + [ 'onClose', 0] + , ['onUnsubscribe', 0] + ]; } /** * @dataProvider topicConnExpectationProvider */ - public function testTopicRetentionFromLeavingConnections($autoDelete, $methodCall, $expectation) { + public function testTopicRetentionFromLeavingConnections($methodCall, $expectation) { $topicName = 'checkTopic'; list($topic, $attribute) = $this->topicProvider($topicName); - $topic->autoDelete = $autoDelete; $this->mngr->onSubscribe($this->conn, $topicName); call_user_func_array(array($this->mngr, $methodCall), array($this->conn, $topicName)); diff --git a/tests/unit/WebSocket/Version/Hixie76Test.php b/tests/unit/WebSocket/Version/Hixie76Test.php deleted file mode 100644 index 75998aa..0000000 --- a/tests/unit/WebSocket/Version/Hixie76Test.php +++ /dev/null @@ -1,103 +0,0 @@ -_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); - } -} diff --git a/tests/unit/WebSocket/Version/HyBi10Test.php b/tests/unit/WebSocket/Version/HyBi10Test.php deleted file mode 100644 index 1d9e8a9..0000000 --- a/tests/unit/WebSocket/Version/HyBi10Test.php +++ /dev/null @@ -1,67 +0,0 @@ -_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()); - } -} diff --git a/tests/unit/WebSocket/Version/RFC6455/FrameTest.php b/tests/unit/WebSocket/Version/RFC6455/FrameTest.php deleted file mode 100644 index eff9513..0000000 --- a/tests/unit/WebSocket/Version/RFC6455/FrameTest.php +++ /dev/null @@ -1,543 +0,0 @@ -_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()); - } -} diff --git a/tests/unit/WebSocket/Version/RFC6455/HandshakeVerifierTest.php b/tests/unit/WebSocket/Version/RFC6455/HandshakeVerifierTest.php deleted file mode 100644 index 6761c32..0000000 --- a/tests/unit/WebSocket/Version/RFC6455/HandshakeVerifierTest.php +++ /dev/null @@ -1,170 +0,0 @@ -_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)); - } -} diff --git a/tests/unit/WebSocket/Version/RFC6455/MessageTest.php b/tests/unit/WebSocket/Version/RFC6455/MessageTest.php deleted file mode 100644 index b2d21d2..0000000 --- a/tests/unit/WebSocket/Version/RFC6455/MessageTest.php +++ /dev/null @@ -1,63 +0,0 @@ -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()); - } -} diff --git a/tests/unit/WebSocket/Version/RFC6455Test.php b/tests/unit/WebSocket/Version/RFC6455Test.php deleted file mode 100644 index 86e5631..0000000 --- a/tests/unit/WebSocket/Version/RFC6455Test.php +++ /dev/null @@ -1,151 +0,0 @@ -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()); - } -} diff --git a/tests/unit/WebSocket/VersionManagerTest.php b/tests/unit/WebSocket/VersionManagerTest.php deleted file mode 100644 index d9c55fe..0000000 --- a/tests/unit/WebSocket/VersionManagerTest.php +++ /dev/null @@ -1,91 +0,0 @@ -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)); - } -} diff --git a/tests/unit/WebSocket/WsServerTest.php b/tests/unit/WebSocket/WsServerTest.php deleted file mode 100644 index 3f4aa43..0000000 --- a/tests/unit/WebSocket/WsServerTest.php +++ /dev/null @@ -1,51 +0,0 @@ -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))); - } -}