Merge pull request #91 from cboden/http

HTTP and Routing
Fixes #89
Fixes #15
This commit is contained in:
Chris Boden 2013-05-08 16:11:35 -07:00
commit 0dba9fc50e
36 changed files with 761 additions and 272 deletions

View File

@ -1,7 +1,6 @@
language: php language: php
php: php:
- 5.3.3
- 5.3 - 5.3
- 5.4 - 5.4

View File

@ -8,6 +8,12 @@ CHANGELOG
--- ---
* 0.3.0 (2013-xx-xx)
* BC: Require hostname and do Origin HTTP header check against it by default, helping prevent CSRF attacks
* Added HTTP Router component to allowing for a single Ratchet server to handle multiple apps
* BC: Decoupled HTTP from WebSocket component
* 0.2.5 (2013-04-01) * 0.2.5 (2013-04-01)
* Fixed Hixie-76 handshake bug * Fixed Hixie-76 handshake bug

View File

@ -32,4 +32,5 @@ apidocs:
-s vendor/react \ -s vendor/react \
-s vendor/guzzle \ -s vendor/guzzle \
-s vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Session \ -s vendor/symfony/http-foundation/Symfony/Component/HttpFoundation/Session \
-s vendor/symfony/http-foundation/Symfony/Component/Routing \
-s vendor/evenement/evenement/src/Evenement -s vendor/evenement/evenement/src/Evenement

View File

@ -38,8 +38,9 @@ Need help? Have a question? Want to provide feedback? Write a message on the
<?php <?php
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Http\RoutedHttpServer;
use Ratchet\Server\IoServer; use Ratchet\Server\IoServer;
use Ratchet\WebSocket\WsServer; use Ratchet\Tests\AbFuzzyServer;
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
@ -76,7 +77,11 @@ class Chat implements MessageComponentInterface {
} }
// Run the server application through the WebSocket protocol on port 8080 // Run the server application through the WebSocket protocol on port 8080
$server = IoServer::factory(new WsServer(new Chat), 8080); $router = new RoutedHttpServer;
$router->addRoute('/echo', new AbFuzzyServer);
$router->addRoute('/chat', new Chat);
$server = IoServer::factory($router, 8000);
$server->run(); $server->run();
``` ```

View File

@ -25,9 +25,10 @@
} }
} }
, "require": { , "require": {
"php": ">=5.3.3" "php": ">=5.3.9"
, "react/socket": "0.2.*" , "react/socket": "~0.2"
, "guzzle/http": "~3.0" , "guzzle/http": "~3.0"
, "symfony/http-foundation": "~2.1" , "symfony/http-foundation": "~2.2"
, "symfony/routing": "~2.2"
} }
} }

185
composer.lock generated
View File

@ -1,5 +1,9 @@
{ {
"hash": "9ccce99ef687cb79dad8a4c581f38cc5", "_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file"
],
"hash": "6006ea344879ef78bf1a490e630a74fc",
"packages": [ "packages": [
{ {
"name": "evenement/evenement", "name": "evenement/evenement",
@ -42,17 +46,17 @@
}, },
{ {
"name": "guzzle/common", "name": "guzzle/common",
"version": "v3.3.0", "version": "v3.4.1",
"target-dir": "Guzzle/Common", "target-dir": "Guzzle/Common",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/common.git", "url": "https://github.com/guzzle/common.git",
"reference": "v3.3.0" "reference": "v3.4.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/common/zipball/v3.3.0", "url": "https://api.github.com/repos/guzzle/common/zipball/v3.4.1",
"reference": "v3.3.0", "reference": "v3.4.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -62,7 +66,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.0-dev" "dev-master": "3.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -82,21 +86,21 @@
"event", "event",
"exception" "exception"
], ],
"time": "2013-03-04 00:41:45" "time": "2013-04-16 20:56:26"
}, },
{ {
"name": "guzzle/http", "name": "guzzle/http",
"version": "v3.3.0", "version": "v3.4.1",
"target-dir": "Guzzle/Http", "target-dir": "Guzzle/Http",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/http.git", "url": "https://github.com/guzzle/http.git",
"reference": "v3.3.0" "reference": "v3.4.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/http/zipball/v3.3.0", "url": "https://api.github.com/repos/guzzle/http/zipball/v3.4.1",
"reference": "v3.3.0", "reference": "v3.4.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -111,7 +115,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.0-dev" "dev-master": "3.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -139,21 +143,21 @@
"http", "http",
"http client" "http client"
], ],
"time": "2013-03-03 21:40:51" "time": "2013-04-16 20:27:11"
}, },
{ {
"name": "guzzle/parser", "name": "guzzle/parser",
"version": "v3.3.0", "version": "v3.4.1",
"target-dir": "Guzzle/Parser", "target-dir": "Guzzle/Parser",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/parser.git", "url": "https://github.com/guzzle/parser.git",
"reference": "v3.3.0" "reference": "v3.4.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/parser/zipball/v3.3.0", "url": "https://api.github.com/repos/guzzle/parser/zipball/v3.4.1",
"reference": "v3.3.0", "reference": "v3.4.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -162,7 +166,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.0-dev" "dev-master": "3.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -183,31 +187,34 @@
"message", "message",
"url" "url"
], ],
"time": "2013-01-12 21:43:21" "time": "2013-03-07 22:13:59"
}, },
{ {
"name": "guzzle/stream", "name": "guzzle/stream",
"version": "v3.3.0", "version": "v3.4.1",
"target-dir": "Guzzle/Stream", "target-dir": "Guzzle/Stream",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/guzzle/stream.git", "url": "https://github.com/guzzle/stream.git",
"reference": "v3.3.0" "reference": "v3.4.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/guzzle/stream/zipball/v3.3.0", "url": "https://api.github.com/repos/guzzle/stream/zipball/v3.4.1",
"reference": "v3.3.0", "reference": "v3.4.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"guzzle/common": "self.version", "guzzle/common": "self.version",
"php": ">=5.3.2" "php": ">=5.3.2"
}, },
"suggest": {
"guzzle/http": "To convert Guzzle request objects to PHP streams"
},
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.0-dev" "dev-master": "3.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -233,21 +240,21 @@
"component", "component",
"stream" "stream"
], ],
"time": "2013-03-03 03:07:02" "time": "2013-04-06 18:28:51"
}, },
{ {
"name": "react/event-loop", "name": "react/event-loop",
"version": "v0.2.7", "version": "v0.3.1",
"target-dir": "React/EventLoop", "target-dir": "React/EventLoop",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/reactphp/event-loop", "url": "https://github.com/reactphp/event-loop.git",
"reference": "v0.2.7" "reference": "v0.3.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://github.com/reactphp/event-loop/archive/v0.2.7.zip", "url": "https://api.github.com/repos/reactphp/event-loop/zipball/v0.3.1",
"reference": "v0.2.7", "reference": "v0.3.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -260,7 +267,7 @@
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "0.2-dev" "dev-master": "0.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -276,33 +283,33 @@
"keywords": [ "keywords": [
"event-loop" "event-loop"
], ],
"time": "2013-01-05 11:41:26" "time": "2013-01-14 23:11:47"
}, },
{ {
"name": "react/socket", "name": "react/socket",
"version": "v0.2.7", "version": "v0.3.1",
"target-dir": "React/Socket", "target-dir": "React/Socket",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/reactphp/socket", "url": "https://github.com/reactphp/socket.git",
"reference": "v0.2.7" "reference": "v0.3.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://github.com/reactphp/socket/archive/v0.2.7.zip", "url": "https://api.github.com/repos/reactphp/socket/zipball/v0.3.1",
"reference": "v0.2.7", "reference": "v0.3.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"evenement/evenement": "1.0.*", "evenement/evenement": "1.0.*",
"php": ">=5.3.3", "php": ">=5.3.3",
"react/event-loop": "0.2.*", "react/event-loop": "0.3.*",
"react/stream": "0.2.*" "react/stream": "0.3.*"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "0.2-dev" "dev-master": "0.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -318,21 +325,21 @@
"keywords": [ "keywords": [
"Socket" "Socket"
], ],
"time": "2012-12-14 00:58:14" "time": "2013-04-20 14:53:10"
}, },
{ {
"name": "react/stream", "name": "react/stream",
"version": "v0.2.7", "version": "v0.3.1",
"target-dir": "React/Stream", "target-dir": "React/Stream",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/reactphp/stream", "url": "https://github.com/reactphp/stream.git",
"reference": "v0.2.7" "reference": "v0.3.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://github.com/reactphp/stream/archive/v0.2.7.zip", "url": "https://api.github.com/repos/reactphp/stream/zipball/v0.3.1",
"reference": "v0.2.7", "reference": "v0.3.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -340,13 +347,13 @@
"php": ">=5.3.3" "php": ">=5.3.3"
}, },
"suggest": { "suggest": {
"react/event-loop": "0.2.*", "react/event-loop": "0.3.*",
"react/promise": "1.0.*" "react/promise": "~1.0"
}, },
"type": "library", "type": "library",
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "0.2-dev" "dev-master": "0.3-dev"
} }
}, },
"autoload": { "autoload": {
@ -363,21 +370,21 @@
"pipe", "pipe",
"stream" "stream"
], ],
"time": "2012-12-14 00:58:14" "time": "2013-04-21 13:25:49"
}, },
{ {
"name": "symfony/event-dispatcher", "name": "symfony/event-dispatcher",
"version": "v2.2.0", "version": "v2.2.1",
"target-dir": "Symfony/Component/EventDispatcher", "target-dir": "Symfony/Component/EventDispatcher",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/EventDispatcher.git", "url": "https://github.com/symfony/EventDispatcher.git",
"reference": "v2.2.0-RC3" "reference": "v2.2.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.2.0-RC3", "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.2.1",
"reference": "v2.2.0-RC3", "reference": "v2.2.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -421,17 +428,17 @@
}, },
{ {
"name": "symfony/http-foundation", "name": "symfony/http-foundation",
"version": "v2.2.0", "version": "v2.2.1",
"target-dir": "Symfony/Component/HttpFoundation", "target-dir": "Symfony/Component/HttpFoundation",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/HttpFoundation.git", "url": "https://github.com/symfony/HttpFoundation.git",
"reference": "v2.2.0" "reference": "v2.2.1"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/HttpFoundation/zipball/v2.2.0", "url": "https://api.github.com/repos/symfony/HttpFoundation/zipball/v2.2.1",
"reference": "v2.2.0", "reference": "v2.2.1",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -467,7 +474,65 @@
], ],
"description": "Symfony HttpFoundation Component", "description": "Symfony HttpFoundation Component",
"homepage": "http://symfony.com", "homepage": "http://symfony.com",
"time": "2013-02-26 09:42:13" "time": "2013-04-06 10:15:43"
},
{
"name": "symfony/routing",
"version": "v2.2.1",
"target-dir": "Symfony/Component/Routing",
"source": {
"type": "git",
"url": "https://github.com/symfony/Routing.git",
"reference": "v2.2.1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/Routing/zipball/v2.2.1",
"reference": "v2.2.1",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"doctrine/common": ">=2.2,<3.0",
"psr/log": ">=1.0,<2.0",
"symfony/config": ">=2.2,<2.3-dev",
"symfony/yaml": ">=2.0,<3.0"
},
"suggest": {
"doctrine/common": "~2.2",
"symfony/config": "2.2.*",
"symfony/yaml": "2.2.*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
},
"autoload": {
"psr-0": {
"Symfony\\Component\\Routing\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "http://symfony.com/contributors"
}
],
"description": "Symfony Routing Component",
"homepage": "http://symfony.com",
"time": "2013-03-23 12:03:22"
} }
], ],
"packages-dev": [ "packages-dev": [
@ -481,7 +546,7 @@
], ],
"platform": { "platform": {
"php": ">=5.3.3" "php": ">=5.3.9"
}, },
"platform-dev": [ "platform-dev": [

View File

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

View File

@ -1,5 +1,5 @@
<?php <?php
namespace Ratchet\WebSocket\Guzzle\Http\Message; namespace Ratchet\Http\Guzzle\Http\Message;
use Guzzle\Http\Message\RequestFactory as GuzzleRequestFactory; use Guzzle\Http\Message\RequestFactory as GuzzleRequestFactory;
use Guzzle\Http\EntityBody; use Guzzle\Http\EntityBody;

View File

@ -1,8 +1,8 @@
<?php <?php
namespace Ratchet\WebSocket; namespace Ratchet\Http;
use Ratchet\MessageInterface; use Ratchet\MessageInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; use Ratchet\Http\Guzzle\Http\Message\RequestFactory;
/** /**
* This class receives streaming data from a client request * This class receives streaming data from a client request

View File

@ -0,0 +1,97 @@
<?php
namespace Ratchet\Http;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\Response;
/**
* @todo Detect HTTP proxy header IP, re-map remoteAddress
*/
class HttpServer implements MessageComponentInterface {
/**
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced
* @var HttpRequestParser
* @note May not expose this in the future, may do through facade methods
*/
protected $_reqParser;
/**
* @var \Ratchet\Http\HttpServerInterface
*/
protected $_httpServer;
/**
* @param HttpServerInterface
*/
public function __construct(HttpServerInterface $component) {
$this->_httpServer = $component;
$this->_reqParser = new HttpRequestParser;
}
/**
* {@inheritdoc}
*/
public function onOpen(ConnectionInterface $conn) {
$conn->httpHeadersReceived = false;
}
/**
* {@inheritdoc}
*/
public function onMessage(ConnectionInterface $from, $msg) {
if (true !== $from->httpHeadersReceived) {
try {
if (null === ($request = $this->_reqParser->onMessage($from, $msg))) {
return;
}
} catch (\OverflowException $oe) {
return $this->close($from, 413);
}
$from->httpHeadersReceived = true;
if (isset($from->remoteAddress) && '127.0.0.1' == $from->remoteAddress && $request->hasHeader('X-Forwarded-For')) {
$from->remoteAddress = $request->getHeader('X-Forwarded-For', true);
}
return $this->_httpServer->onOpen($from, $request);
}
$this->_httpServer->onMessage($from, $msg);
}
/**
* {@inheritdoc}
*/
public function onClose(ConnectionInterface $conn) {
if ($conn->httpHeadersReceived) {
$this->_httpServer->onClose($conn);
}
}
/**
* {@inheritdoc}
*/
public function onError(ConnectionInterface $conn, \Exception $e) {
if ($conn->httpHeadersReceived) {
$this->_httpServer->onError($conn, $e);
} else {
$this->close($conn, 500);
}
}
/**
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
* @return null
*/
protected function close(ConnectionInterface $conn, $code = 400) {
$response = new Response($code, array(
'X-Powered-By' => \Ratchet\VERSION
));
$conn->send((string)$response);
$conn->close();
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Ratchet\Http;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Guzzle\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!!!
* @throws \UnexpectedValueException if a RequestInterface is not passed
*/
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null);
}

View File

@ -0,0 +1,65 @@
<?php
namespace Ratchet\Http;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Ratchet\WebSocket\WsServer;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Matcher\UrlMatcher;
/**
*/
class RoutedHttpServer implements MessageComponentInterface {
protected $_routes;
protected $_server;
public function __construct(RouteCollection $routes = null) {
if (null == $routes) {
$routes = new RouteCollection;
}
$this->_routes = $routes;
$this->_server = new HttpServer(new Router(new UrlMatcher($routes, new RequestContext)));
}
public function addRoute($path, MessageComponentInterface $controller) {
$this->_routes->add(uniqid(), new Route($path, array(
'_controller' => new WsServer($controller)
)));
}
public function addHttpRoute($path, HttpServerInterface $controller) {
$this->_routes->add(uniqid(), new Route($path, array(
'_controller' => $controller
)));
}
/**
* {@inheritdoc}
*/
function onOpen(ConnectionInterface $conn) {
$this->_server->onOpen($conn);
}
/**
* {@inheritdoc}
*/
function onMessage(ConnectionInterface $from, $msg) {
$this->_server->onMessage($from, $msg);
}
/**
* {@inheritdoc}
*/
function onClose(ConnectionInterface $conn) {
$this->_server->onClose($conn);
}
/**
* {@inheritdoc}
*/
function onError(ConnectionInterface $conn, \Exception $e) {
$this->_server->onError($conn, $e);
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Ratchet\Http;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
class Router implements HttpServerInterface {
/**
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/
protected $_matcher;
public function __construct(UrlMatcherInterface $matcher) {
$this->_matcher = $matcher;
}
/**
* {@inheritdoc}
* @throws \UnexpectedValueException If a controller is not \Ratchet\Http\HttpServerInterface
*/
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
if (null === $request) {
throw new \UnexpectedValueException('$request can not be null');
}
try {
$route = $this->_matcher->match($request->getPath());
} catch (MethodNotAllowedException $nae) {
return $this->close($conn, 403);
} catch (ResourceNotFoundException $nfe) {
return $this->close($conn, 404);
}
if (is_string($route['_controller']) && class_exists($route['_controller'])) {
$route['_controller'] = new $route['_controller'];
}
if (!($route['_controller'] instanceof HttpServerInterface)) {
throw new \UnexpectedValueException('All routes must implement Ratchet\Http\HttpServerInterface');
}
$conn->controller = $route['_controller'];
$conn->controller->onOpen($conn, $request);
}
/**
* {@inheritdoc}
*/
function onMessage(ConnectionInterface $from, $msg) {
$from->controller->onMessage($from, $msg);
}
/**
* {@inheritdoc}
*/
function onClose(ConnectionInterface $conn) {
$conn->controller->onClose($conn);
}
/**
* {@inheritdoc}
*/
function onError(ConnectionInterface $conn, \Exception $e) {
$conn->controller->onError($conn, $e);
}
/**
* Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code
* @return null
*/
protected function close(ConnectionInterface $conn, $code = 400) {
$response = new Response($code, array(
'X-Powered-By' => \Ratchet\VERSION
));
$conn->send((string)$response);
$conn->close();
}
}

View File

@ -42,7 +42,7 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
* @param \SessionHandlerInterface $handler * @param \SessionHandlerInterface $handler
* @param array $options * @param array $options
* @param \Ratchet\Session\Serialize\HandlerInterface $serializer * @param \Ratchet\Session\Serialize\HandlerInterface $serializer
* @throws \RuntimeExcpetion * @throws \RuntimeException
*/ */
public function __construct(MessageComponentInterface $app, \SessionHandlerInterface $handler, array $options = array(), HandlerInterface $serializer = null) { public function __construct(MessageComponentInterface $app, \SessionHandlerInterface $handler, array $options = array(), HandlerInterface $serializer = null) {
$this->_app = $app; $this->_app = $app;
@ -71,7 +71,7 @@ class SessionProvider implements MessageComponentInterface, WsServerInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
function onOpen(ConnectionInterface $conn) { function onOpen(ConnectionInterface $conn) {
if (null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) { if (!isset($conn->WebSocket) || null === ($id = $conn->WebSocket->request->getCookie(ini_get('session.name')))) {
$saveHandler = $this->_null; $saveHandler = $this->_null;
$id = ''; $id = '';
} else { } else {

View File

@ -4,7 +4,8 @@ use Guzzle\Http\Message\RequestInterface;
class HyBi10 extends RFC6455 { class HyBi10 extends RFC6455 {
public function isProtocol(RequestInterface $request) { public function isProtocol(RequestInterface $request) {
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1); $version = $request->hasHeader('Sec-WebSocket-Version') ? (int)$request->getHeader('Sec-WebSocket-Version', true) : -1;
return ($version >= 6 && $version < 13); return ($version >= 6 && $version < 13);
} }

View File

@ -50,7 +50,7 @@ class RFC6455 implements VersionInterface {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function isProtocol(RequestInterface $request) { public function isProtocol(RequestInterface $request) {
$version = (int)$request->getHeader('Sec-WebSocket-Version', -1); $version = $request->hasHeader('Sec-WebSocket-Version') ? (int)$request->getHeader('Sec-WebSocket-Version', true) : -1;
return ($this->getVersionNumber() === $version); return ($this->getVersionNumber() === $version);
} }
@ -73,7 +73,7 @@ class RFC6455 implements VersionInterface {
return new Response(101, array( return new Response(101, array(
'Upgrade' => 'websocket' 'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade' , 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key')) , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key', true))
)); ));
} }

View File

@ -113,22 +113,6 @@ class HandshakeVerifier {
return (16 === strlen(base64_decode((string)$val))); return (16 === strlen(base64_decode((string)$val)));
} }
/**
* Verify Origin matches RFC6454 IF it is set
* Origin is an optional field
* @param string|null
* @return bool
* @todo Implement verification functionality - see section 4.2.1.7
*/
public function verifyOrigin($val) {
if (null === $val) {
return true;
}
// logic here
return true;
}
/** /**
* Verify the version passed matches this RFC * Verify the version passed matches this RFC
* @param string|int MUST equal 13|"13" * @param string|int MUST equal 13|"13"

View File

@ -2,9 +2,11 @@
namespace Ratchet\WebSocket; namespace Ratchet\WebSocket;
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Http\HttpServerInterface;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\Response;
use Ratchet\WebSocket\Version; use Ratchet\WebSocket\Version;
use Ratchet\WebSocket\Encoding\ToggleableValidator; use Ratchet\WebSocket\Encoding\ToggleableValidator;
use Guzzle\Http\Message\Response;
/** /**
* The adapter to handle WebSocket requests/responses * The adapter to handle WebSocket requests/responses
@ -12,14 +14,7 @@ use Guzzle\Http\Message\Response;
* @link http://ca.php.net/manual/en/ref.http.php * @link http://ca.php.net/manual/en/ref.http.php
* @link http://dev.w3.org/html5/websockets/ * @link http://dev.w3.org/html5/websockets/
*/ */
class WsServer implements MessageComponentInterface { class WsServer implements HttpServerInterface {
/**
* Buffers incoming HTTP requests returning a Guzzle Request when coalesced
* @var HttpRequestParser
* @note May not expose this in the future, may do through facade methods
*/
public $reqParser;
/** /**
* Manage the various WebSocket versions to support * Manage the various WebSocket versions to support
* @var VersionManager * @var VersionManager
@ -31,7 +26,7 @@ class WsServer implements MessageComponentInterface {
* Decorated component * Decorated component
* @var \Ratchet\MessageComponentInterface * @var \Ratchet\MessageComponentInterface
*/ */
protected $_decorating; public $component;
/** /**
* @var \SplObjectStorage * @var \SplObjectStorage
@ -39,9 +34,7 @@ class WsServer implements MessageComponentInterface {
protected $connections; protected $connections;
/** /**
* For now, array_push accepted subprotocols to this array * Holder of accepted protocols, implement through WampServerInterface
* @deprecated
* @temporary
*/ */
protected $acceptedSubProtocols = array(); protected $acceptedSubProtocols = array();
@ -62,7 +55,6 @@ class WsServer implements MessageComponentInterface {
* If you want to enable sub-protocols have your component implement WsServerInterface as well * If you want to enable sub-protocols have your component implement WsServerInterface as well
*/ */
public function __construct(MessageComponentInterface $component) { public function __construct(MessageComponentInterface $component) {
$this->reqParser = new HttpRequestParser;
$this->versioner = new VersionManager; $this->versioner = new VersionManager;
$this->validator = new ToggleableValidator; $this->validator = new ToggleableValidator;
@ -72,16 +64,23 @@ class WsServer implements MessageComponentInterface {
->enableVersion(new Version\Hixie76) ->enableVersion(new Version\Hixie76)
; ;
$this->_decorating = $component; $this->component = $component;
$this->connections = new \SplObjectStorage; $this->connections = new \SplObjectStorage;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onOpen(ConnectionInterface $conn) { public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
if (null === $request) {
throw new \UnexpectedValueException('$request can not be null');
}
$conn->WebSocket = new \StdClass; $conn->WebSocket = new \StdClass;
$conn->WebSocket->request = $request;
$conn->WebSocket->established = false; $conn->WebSocket->established = false;
$this->attemptUpgrade($conn);
} }
/** /**
@ -92,50 +91,44 @@ class WsServer implements MessageComponentInterface {
return $from->WebSocket->version->onMessage($this->connections[$from], $msg); return $from->WebSocket->version->onMessage($this->connections[$from], $msg);
} }
if (isset($from->WebSocket->request)) { $this->attemptUpgrade($from, $msg);
$from->WebSocket->request->getBody()->write($msg); }
protected function attemptUpgrade(ConnectionInterface $conn, $data = '') {
if ('' !== $data) {
$conn->WebSocket->request->getBody()->write($data);
} else { } else {
try { if (!$this->versioner->isVersionEnabled($conn->WebSocket->request)) {
if (null === ($request = $this->reqParser->onMessage($from, $msg))) { return $this->close($conn);
return;
}
} catch (\OverflowException $oe) {
return $this->close($from, 413);
} }
if (!$this->versioner->isVersionEnabled($request)) { $conn->WebSocket->version = $this->versioner->getVersion($conn->WebSocket->request);
return $this->close($from);
}
$from->WebSocket->request = $request;
$from->WebSocket->version = $this->versioner->getVersion($request);
} }
try { try {
$response = $from->WebSocket->version->handshake($from->WebSocket->request); $response = $conn->WebSocket->version->handshake($conn->WebSocket->request);
} catch (\UnderflowException $e) { } catch (\UnderflowException $e) {
return; return;
} }
// This needs to be refactored later on, incorporated with routing if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($conn->WebSocket->request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) {
if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->request->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) {
$response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols);
} }
$response->setHeader('X-Powered-By', \Ratchet\VERSION); $response->setHeader('X-Powered-By', \Ratchet\VERSION);
$from->send((string)$response); $conn->send((string)$response);
if (101 != $response->getStatusCode()) { if (101 != $response->getStatusCode()) {
return $from->close(); return $conn->close();
} }
$upgraded = $from->WebSocket->version->upgradeConnection($from, $this->_decorating); $upgraded = $conn->WebSocket->version->upgradeConnection($conn, $this->component);
$this->connections->attach($from, $upgraded); $this->connections->attach($conn, $upgraded);
$upgraded->WebSocket->established = true; $upgraded->WebSocket->established = true;
return $this->_decorating->onOpen($upgraded); return $this->component->onOpen($upgraded);
} }
/** /**
@ -146,7 +139,7 @@ class WsServer implements MessageComponentInterface {
$decor = $this->connections[$conn]; $decor = $this->connections[$conn];
$this->connections->detach($conn); $this->connections->detach($conn);
$this->_decorating->onClose($decor); $this->component->onClose($decor);
} }
} }
@ -155,7 +148,7 @@ class WsServer implements MessageComponentInterface {
*/ */
public function onError(ConnectionInterface $conn, \Exception $e) { public function onError(ConnectionInterface $conn, \Exception $e) {
if ($conn->WebSocket->established) { if ($conn->WebSocket->established) {
$this->_decorating->onError($this->connections[$conn], $e); $this->component->onError($this->connections[$conn], $e);
} else { } else {
$conn->close(); $conn->close();
} }
@ -189,8 +182,8 @@ class WsServer implements MessageComponentInterface {
*/ */
public function isSubProtocolSupported($name) { public function isSubProtocolSupported($name) {
if (!$this->isSpGenerated) { if (!$this->isSpGenerated) {
if ($this->_decorating instanceof WsServerInterface) { if ($this->component instanceof WsServerInterface) {
$this->acceptedSubProtocols = array_flip($this->_decorating->getSubProtocols()); $this->acceptedSubProtocols = array_flip($this->component->getSubProtocols());
} }
$this->isSpGenerated = true; $this->isSpGenerated = true;
@ -223,7 +216,6 @@ class WsServer implements MessageComponentInterface {
* Close a connection with an HTTP response * Close a connection with an HTTP response
* @param \Ratchet\ConnectionInterface $conn * @param \Ratchet\ConnectionInterface $conn
* @param int $code HTTP status code * @param int $code HTTP status code
* @return void
*/ */
protected function close(ConnectionInterface $conn, $code = 400) { protected function close(ConnectionInterface $conn, $code = 400) {
$response = new Response($code, array( $response = new Response($code, array(

View File

@ -4,7 +4,7 @@
$loop = new React\EventLoop\LibEvLoop; $loop = new React\EventLoop\LibEvLoop;
$sock = new React\Socket\Server($loop); $sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer); $app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer));
$port = $argc > 1 ? $argv[1] : 8000; $port = $argc > 1 ? $argv[1] : 8000;
$sock->listen($port, '0.0.0.0'); $sock->listen($port, '0.0.0.0');

View File

@ -4,7 +4,7 @@
$loop = new React\EventLoop\LibEventLoop; $loop = new React\EventLoop\LibEventLoop;
$sock = new React\Socket\Server($loop); $sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer); $app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer));
$port = $argc > 1 ? $argv[1] : 8000; $port = $argc > 1 ? $argv[1] : 8000;
$sock->listen($port, '0.0.0.0'); $sock->listen($port, '0.0.0.0');

View File

@ -4,7 +4,7 @@
$loop = new React\EventLoop\LibEvLoop; $loop = new React\EventLoop\LibEvLoop;
$sock = new React\Socket\Server($loop); $sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer); $app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer));
$port = $argc > 1 ? $argv[1] : 8000; $port = $argc > 1 ? $argv[1] : 8000;
$sock->listen($port, '0.0.0.0'); $sock->listen($port, '0.0.0.0');

View File

@ -4,8 +4,9 @@
$loop = new React\EventLoop\StreamSelectLoop; $loop = new React\EventLoop\StreamSelectLoop;
$sock = new React\Socket\Server($loop); $sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer); $web = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer);
$app->setEncodingChecks(false); $app = new Ratchet\Http\HttpServer($web);
$web->setEncodingChecks(false);
$port = $argc > 1 ? $argv[1] : 8000; $port = $argc > 1 ? $argv[1] : 8000;
$sock->listen($port, '0.0.0.0'); $sock->listen($port, '0.0.0.0');

View File

@ -4,7 +4,7 @@
$loop = new React\EventLoop\StreamSelectLoop; $loop = new React\EventLoop\StreamSelectLoop;
$sock = new React\Socket\Server($loop); $sock = new React\Socket\Server($loop);
$app = new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer); $app = new Ratchet\Http\HttpServer(new Ratchet\WebSocket\WsServer(new Ratchet\Tests\AbFuzzyServer));
$port = $argc > 1 ? $argv[1] : 8000; $port = $argc > 1 ? $argv[1] : 8000;
$sock->listen($port, '0.0.0.0'); $sock->listen($port, '0.0.0.0');

View File

@ -1,7 +1,6 @@
<?php <?php
namespace Ratchet\Tests; namespace Ratchet\Tests;
use Ratchet\Tests\Mock\ConnectionDecorator; use Ratchet\Tests\Mock\ConnectionDecorator;
use Ratchet\Tests\Mock\Connection;
/** /**
* @covers Ratchet\AbstractConnectionDecorator * @covers Ratchet\AbstractConnectionDecorator
@ -13,7 +12,7 @@ class AbstractConnectionDecoratorTest extends \PHPUnit_Framework_TestCase {
protected $l2; protected $l2;
public function setUp() { public function setUp() {
$this->mock = new Connection; $this->mock = $this->getMock('\Ratchet\ConnectionInterface');
$this->l1 = new ConnectionDecorator($this->mock); $this->l1 = new ConnectionDecorator($this->mock);
$this->l2 = new ConnectionDecorator($this->l1); $this->l2 = new ConnectionDecorator($this->l1);
} }

View File

@ -0,0 +1,41 @@
<?php
namespace Ratchet\Tests;
abstract class AbstractMessageComponentTestCase extends \PHPUnit_Framework_TestCase {
protected $_app;
protected $_serv;
protected $_conn;
abstract public function getConnectionClassString();
abstract public function getDecoratorClassString();
abstract public function getComponentClassString();
public function setUp() {
$this->_app = $this->getMock($this->getComponentClassString());
$decorator = $this->getDecoratorClassString();
$this->_serv = new $decorator($this->_app);
$this->_conn = $this->getMock('\Ratchet\ConnectionInterface');
$this->_serv->onOpen($this->_conn);
}
public function isExpectedConnection() {
return new \PHPUnit_Framework_Constraint_IsInstanceOf($this->getConnectionClassString());
}
public function testOpen() {
$this->_app->expects($this->once())->method('onOpen')->with($this->isExpectedConnection());
$this->_serv->onOpen($this->getMock('\Ratchet\ConnectionInterface'));
}
public function testOnClose() {
$this->_app->expects($this->once())->method('onClose')->with($this->isExpectedConnection());
$this->_serv->onClose($this->_conn);
}
public function testOnError() {
$e = new \Exception('Whoops!');
$this->_app->expects($this->once())->method('onError')->with($this->isExpectedConnection(), $e);
$this->_serv->onError($this->_conn, $e);
}
}

View File

@ -1,9 +1,9 @@
<?php <?php
namespace Ratchet\Tests\WebSocket\Guzzle\Http\Message; namespace Ratchet\Tests\Http\Guzzle\Http\Message;
use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; use Ratchet\Http\Guzzle\Http\Message\RequestFactory;
/** /**
* @covers Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory * @covers Ratchet\Http\Guzzle\Http\Message\RequestFactory
*/ */
class RequestFactoryTest extends \PHPUnit_Framework_TestCase { class RequestFactoryTest extends \PHPUnit_Framework_TestCase {
protected $factory; protected $factory;

View File

@ -1,10 +1,9 @@
<?php <?php
namespace Ratchet\Tests\WebSocket; namespace Ratchet\Tests\Http;
use Ratchet\WebSocket\HttpRequestParser; use Ratchet\Http\HttpRequestParser;
use Ratchet\Tests\Mock\Connection as ConnectionStub;
/** /**
* @covers Ratchet\WebSocket\HttpRequestParser * @covers Ratchet\Http\HttpRequestParser
*/ */
class HttpRequestParserTest extends \PHPUnit_Framework_TestCase { class HttpRequestParserTest extends \PHPUnit_Framework_TestCase {
protected $parser; protected $parser;
@ -32,7 +31,7 @@ class HttpRequestParserTest extends \PHPUnit_Framework_TestCase {
} }
public function testBufferOverflowResponse() { public function testBufferOverflowResponse() {
$conn = new ConnectionStub; $conn = $this->getMock('\Ratchet\ConnectionInterface');
$this->parser->maxSize = 20; $this->parser->maxSize = 20;
@ -42,4 +41,11 @@ class HttpRequestParserTest extends \PHPUnit_Framework_TestCase {
$this->parser->onMessage($conn, "Header-Is: Too Big"); $this->parser->onMessage($conn, "Header-Is: Too Big");
} }
public function testReturnTypeIsRequest() {
$conn = $this->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);
}
} }

View File

@ -0,0 +1,64 @@
<?php
namespace Ratchet\Tests\Http;
use Ratchet\Tests\AbstractMessageComponentTestCase;
/**
* @covers Ratchet\Http\HttpServer
*/
class HttpServerTest extends AbstractMessageComponentTestCase {
public function setUp() {
parent::setUp();
$this->_conn->httpHeadersReceived = true;
}
public function getConnectionClassString() {
return '\Ratchet\ConnectionInterface';
}
public function getDecoratorClassString() {
return '\Ratchet\Http\HttpServer';
}
public function getComponentClassString() {
return '\Ratchet\Http\HttpServerInterface';
}
public function testOpen() {
$headers = "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n";
$this->_conn->httpHeadersReceived = false;
$this->_app->expects($this->once())->method('onOpen')->with($this->isExpectedConnection());
$this->_serv->onMessage($this->_conn, $headers);
}
public function testOnMessageAfterHeaders() {
$headers = "GET / HTTP/1.1\r\nHost: socketo.me\r\n\r\n";
$this->_conn->httpHeadersReceived = false;
$this->_serv->onMessage($this->_conn, $headers);
$message = "Hello World!";
$this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $message);
$this->_serv->onMessage($this->_conn, $message);
}
public function testBufferOverflow() {
$this->_conn->expects($this->once())->method('close');
$this->_conn->httpHeadersReceived = false;
$this->_serv->onMessage($this->_conn, str_repeat('a', 5000));
}
public function testCloseIfNotEstablished() {
$this->_conn->httpHeadersReceived = false;
$this->_conn->expects($this->once())->method('close');
$this->_serv->onError($this->_conn, new \Exception('Whoops!'));
}
public function testBufferHeaders() {
$this->_conn->httpHeadersReceived = false;
$this->_app->expects($this->never())->method('onOpen');
$this->_app->expects($this->never())->method('onMessage');
$this->_serv->onMessage($this->_conn, "GET / HTTP/1.1");
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Ratchet\Tests\Http;
use Ratchet\Http\Router;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
/**
* @covers Ratchet\Http\Router
*/
class RouterTest extends \PHPUnit_Framework_TestCase {
protected $_router;
protected $_matcher;
protected $_conn;
protected $_req;
public function setUp() {
$this->_conn = $this->getMock('\Ratchet\ConnectionInterface');
$this->_req = $this->getMock('\Guzzle\Http\Message\RequestInterface');
$this->_matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface');
$this->_router = new Router($this->_matcher);
$this->_req->expects($this->any())->method('getPath')->will($this->returnValue('/whatever'));
}
public function testFourOhFour() {
$this->_conn->expects($this->once())->method('close');
$nope = new ResourceNotFoundException;
$this->_matcher->expects($this->any())->method('match')->will($this->throwException($nope));
$this->_router->onOpen($this->_conn, $this->_req);
}
public function testNullRequest() {
$this->setExpectedException('\UnexpectedValueException');
$this->_router->onOpen($this->_conn);
}
public function testControllerIsMessageComponentInterface() {
$this->setExpectedException('\UnexpectedValueException');
$this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => new \StdClass)));
$this->_router->onOpen($this->_conn, $this->_req);
}
public function testControllerOnOpen() {
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock();
$this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => $controller)));
$this->_router->onOpen($this->_conn, $this->_req);
$expectedConn = new \PHPUnit_Framework_Constraint_IsInstanceOf('\Ratchet\ConnectionInterface');
$controller->expects($this->once())->method('onOpen')->with($expectedConn, $this->_req);
$this->_matcher->expects($this->any())->method('match')->will($this->returnValue(array('_controller' => $controller)));
$this->_router->onOpen($this->_conn, $this->_req);
}
public function testControllerOnMessageBubbles() {
$message = "The greatest trick the Devil ever pulled was convincing the world he didn't exist";
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock();
$controller->expects($this->once())->method('onMessage')->with($this->_conn, $message);
$this->_conn->controller = $controller;
$this->_router->onMessage($this->_conn, $message);
}
public function testControllerOnCloseBubbles() {
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock();
$controller->expects($this->once())->method('onClose')->with($this->_conn);
$this->_conn->controller = $controller;
$this->_router->onClose($this->_conn);
}
public function testControllerOnErrorBubbles() {
$e= new \Exception('One cannot be betrayed if one has no exceptions');
$controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock();
$controller->expects($this->once())->method('onError')->with($this->_conn, $e);
$this->_conn->controller = $controller;
$this->_router->onError($this->_conn, $e);
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace Ratchet\Tests\Mock;
class MemorySessionHandler implements \SessionHandlerInterface {
protected $_sessions = array();
public function close() {
}
public function destroy($session_id) {
if (isset($this->_sessions[$session_id])) {
unset($this->_sessions[$session_id]);
}
return true;
}
public function gc($maxlifetime) {
return true;
}
public function open($save_path, $session_id) {
if (!isset($this->_sessions[$session_id])) {
$this->_sessions[$session_id] = '';
}
return true;
}
public function read($session_id) {
return $this->_sessions[$session_id];
}
public function write($session_id, $session_data) {
$this->_sessions[$session_id] = $session_data;
return true;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Ratchet\Tests\Mock;
use Ratchet\ConnectionInterface;
use Ratchet\MessageComponentInterface;
use Ratchet\WebSocket\WsServerInterface;
use Ratchet\Wamp\WampServerInterface;
class NullComponent implements MessageComponentInterface, WsServerInterface, WampServerInterface {
public function onOpen(ConnectionInterface $conn) {}
public function onMessage(ConnectionInterface $conn, $msg) {}
public function onClose(ConnectionInterface $conn) {}
public function onError(ConnectionInterface $conn, \Exception $e) {}
public function onCall(ConnectionInterface $conn, $id, $topic, array $params) {}
public function onSubscribe(ConnectionInterface $conn, $topic) {}
public function onUnSubscribe(ConnectionInterface $conn, $topic) {}
public function onPublish(ConnectionInterface $conn, $topic, $event, array $exclude = array(), array $eligible = array()) {}
public function getSubProtocols() {
return array();
}
}

View File

@ -137,4 +137,16 @@ class FlashPolicyTest extends \PHPUnit_Framework_TestCase {
$this->_policy->onMessage($conn, ' '); $this->_policy->onMessage($conn, ' ');
} }
public function testOnOpenExists() {
$this->assertTrue(method_exists($this->_policy, 'onOpen'));
$conn = $this->getMock('\Ratchet\ConnectionInterface');
$this->_policy->onOpen($conn);
}
public function testOnCloseExists() {
$this->assertTrue(method_exists($this->_policy, 'onClose'));
$conn = $this->getMock('\Ratchet\ConnectionInterface');
$this->_policy->onClose($conn);
}
} }

View File

@ -1,5 +1,6 @@
<?php <?php
namespace Ratchet\Tests\Session; namespace Ratchet\Tests\Session;
use Ratchet\Tests\AbstractMessageComponentTestCase;
use Ratchet\Session\SessionProvider; use Ratchet\Session\SessionProvider;
use Ratchet\Tests\Mock\MemorySessionHandler; use Ratchet\Tests\Mock\MemorySessionHandler;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler; use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
@ -11,11 +12,30 @@ use Guzzle\Http\Message\Request;
* @covers Ratchet\Session\Storage\VirtualSessionStorage * @covers Ratchet\Session\Storage\VirtualSessionStorage
* @covers Ratchet\Session\Storage\Proxy\VirtualProxy * @covers Ratchet\Session\Storage\Proxy\VirtualProxy
*/ */
class SessionProviderTest extends \PHPUnit_Framework_TestCase { class SessionProviderTest extends AbstractMessageComponentTestCase {
public function setUp() { public function setUp() {
if (!class_exists('Symfony\\Component\\HttpFoundation\\Session\\Session')) { if (!class_exists('Symfony\Component\HttpFoundation\Session\Session')) {
return $this->markTestSkipped('Dependency of Symfony HttpFoundation failed'); return $this->markTestSkipped('Dependency of Symfony HttpFoundation failed');
} }
parent::setUp();
$this->_serv = new SessionProvider($this->_app, new NullSessionHandler);
}
public function tearDown() {
ini_set('session.serialize_handler', 'php');
}
public function getConnectionClassString() {
return '\Ratchet\ConnectionInterface';
}
public function getDecoratorClassString() {
return '\Ratchet\Tests\Mock\NullComponent';
}
public function getComponentClassString() {
return '\Ratchet\MessageComponentInterface';
} }
public function classCaseProvider() { public function classCaseProvider() {
@ -33,7 +53,7 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
$method = $ref->getMethod('toClassCase'); $method = $ref->getMethod('toClassCase');
$method->setAccessible(true); $method->setAccessible(true);
$component = new SessionProvider($this->getMock('Ratchet\\MessageComponentInterface'), new MemorySessionHandler); $component = new SessionProvider($this->getMock('Ratchet\\MessageComponentInterface'), $this->getMock('\SessionHandlerInterface'));
$this->assertEquals($out, $method->invokeArgs($component, array($in))); $this->assertEquals($out, $method->invokeArgs($component, array($in)));
} }
@ -73,9 +93,9 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
} }
protected function newConn() { protected function newConn() {
$conn = $this->getMock('Ratchet\\ConnectionInterface'); $conn = $this->getMock('Ratchet\ConnectionInterface');
$headers = $this->getMock('Guzzle\\Http\\Message\\Request', array('getCookie'), array('POST', '/', array())); $headers = $this->getMock('Guzzle\Http\Message\Request', array('getCookie'), array('POST', '/', array()));
$headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue(null)); $headers->expects($this->once())->method('getCookie', array(ini_get('session.name')))->will($this->returnValue(null));
$conn->WebSocket = new \StdClass; $conn->WebSocket = new \StdClass;
@ -84,46 +104,10 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
return $conn; return $conn;
} }
public function testOnOpenBubbles() { public function testOnMessageDecorator() {
$conn = $this->newConn(); $message = "Database calls are usually blocking :(";
$mock = $this->getMock('Ratchet\\MessageComponentInterface'); $this->_app->expects($this->once())->method('onMessage')->with($this->isExpectedConnection(), $message);
$comp = new SessionProvider($mock, new NullSessionHandler); $this->_serv->onMessage($this->_conn, $message);
$mock->expects($this->once())->method('onOpen')->with($conn);
$comp->onOpen($conn);
}
protected function getOpenConn() {
$conn = $this->newConn();
$mock = $this->getMock('Ratchet\\MessageComponentInterface');
$prov = new SessionProvider($mock, new NullSessionHandler);
$prov->onOpen($conn);
return array($conn, $mock, $prov);
}
public function testOnMessageBubbles() {
list($conn, $mock, $prov) = $this->getOpenConn();
$msg = 'No sessions here';
$mock->expects($this->once())->method('onMessage')->with($conn, $msg);
$prov->onMessage($conn, $msg);
}
public function testOnCloseBubbles() {
list($conn, $mock, $prov) = $this->getOpenConn();
$mock->expects($this->once())->method('onClose')->with($conn);
$prov->onClose($conn);
}
public function testOnErrorBubbles() {
list($conn, $mock, $prov) = $this->getOpenConn();
$e = new \Exception('I made a boo boo');
$mock->expects($this->once())->method('onError')->with($conn, $e);
$prov->onError($conn, $e);
} }
public function testGetSubProtocolsReturnsArray() { public function testGetSubProtocolsReturnsArray() {
@ -140,4 +124,14 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase {
$this->assertGreaterThanOrEqual(2, count($comp->getSubProtocols())); $this->assertGreaterThanOrEqual(2, count($comp->getSubProtocols()));
} }
public function testRejectInvalidSeralizers() {
if (!function_exists('wddx_serialize_value')) {
$this->markTestSkipped();
}
ini_set('session.serialize_handler', 'wddx');
$this->setExpectedException('\RuntimeException');
new SessionProvider($this->getMock('\Ratchet\MessageComponentInterface'), $this->getMock('\SessionHandlerInterface'));
}
} }

View File

@ -38,7 +38,7 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase {
* @dataProvider invalidMessageProvider * @dataProvider invalidMessageProvider
*/ */
public function testInvalidMessages($type) { public function testInvalidMessages($type) {
$this->setExpectedException('\\Ratchet\\Wamp\\Exception'); $this->setExpectedException('\Ratchet\Wamp\Exception');
$conn = $this->newConn(); $conn = $this->newConn();
$this->_comp->onOpen($conn); $this->_comp->onOpen($conn);

View File

@ -1,59 +1,44 @@
<?php <?php
namespace Ratchet\Tests\Wamp; namespace Ratchet\Tests\Wamp;
use Ratchet\Wamp\WampServer; use Ratchet\Wamp\WampServer;
use Ratchet\Tests\AbstractMessageComponentTestCase;
/** /**
* @covers Ratchet\Wamp\WampServer * @covers Ratchet\Wamp\WampServer
*/ */
class WampServerTest extends \PHPUnit_Framework_TestCase { class WampServerTest extends AbstractMessageComponentTestCase {
private $serv; private $serv;
private $mock; private $mock;
private $conn; private $conn;
public function setUp() { public function getConnectionClassString() {
$this->mock = $this->getMock('\\Ratchet\\Wamp\\WampServerInterface'); return '\Ratchet\Wamp\WampConnection';
$this->serv = new WampServer($this->mock);
$this->conn = $this->getMock('\\Ratchet\\ConnectionInterface');
$this->serv->onOpen($this->conn);
} }
public function isWampConn() { public function getDecoratorClassString() {
return new \PHPUnit_Framework_Constraint_IsInstanceOf('\\Ratchet\\Wamp\\WampConnection'); return 'Ratchet\Wamp\WampServer';
} }
public function testOpen() { public function getComponentClassString() {
$this->mock->expects($this->once())->method('onOpen')->with($this->isWampConn()); return '\Ratchet\Wamp\WampServerInterface';
$this->serv->onOpen($this->getMock('\\Ratchet\\ConnectionInterface'));
}
public function testOnClose() {
$this->mock->expects($this->once())->method('onClose')->with($this->isWampConn());
$this->serv->onClose($this->conn);
}
public function testOnError() {
$e = new \Exception('hurr hurr');
$this->mock->expects($this->once())->method('onError')->with($this->isWampConn(), $e);
$this->serv->onError($this->conn, $e);
} }
public function testOnMessageToEvent() { public function testOnMessageToEvent() {
$published = 'Client published this message'; $published = 'Client published this message';
$this->mock->expects($this->once())->method('onPublish')->with( $this->_app->expects($this->once())->method('onPublish')->with(
$this->isWampConn() $this->isExpectedConnection()
, new \PHPUnit_Framework_Constraint_IsInstanceOf('\\Ratchet\\Wamp\\Topic') , new \PHPUnit_Framework_Constraint_IsInstanceOf('\Ratchet\Wamp\Topic')
, $published , $published
, array() , array()
, array() , array()
); );
$this->serv->onMessage($this->conn, json_encode(array(7, 'topic', $published))); $this->_serv->onMessage($this->_conn, json_encode(array(7, 'topic', $published)));
} }
public function testGetSubProtocols() { public function testGetSubProtocols() {
// todo: could expand on this // todo: could expand on this
$this->assertInternalType('array', $this->serv->getSubProtocols()); $this->assertInternalType('array', $this->_serv->getSubProtocols());
} }
} }

View File

@ -1,6 +1,7 @@
<?php <?php
namespace Ratchet\Tests\WebSocket\Version; namespace Ratchet\Tests\WebSocket\Version;
use Ratchet\WebSocket\Version\Hixie76; use Ratchet\WebSocket\Version\Hixie76;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer; use Ratchet\WebSocket\WsServer;
/** /**
@ -43,7 +44,7 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase {
$headers = "GET / HTTP/1.1"; $headers = "GET / HTTP/1.1";
$headers .= "Upgrade: WebSocket{$this->_crlf}"; $headers .= "Upgrade: WebSocket{$this->_crlf}";
$headers .= "Connection: Upgrade{$this->_crlf}"; $headers .= "Connection: Upgrade{$this->_crlf}";
$headers .= "Host: home.chrisboden.ca{$this->_crlf}"; $headers .= "Host: socketo.me{$this->_crlf}";
$headers .= "Origin: http://fiddle.jshell.net{$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-Key1:17 Z4< F94 N3 7P41 7{$this->_crlf}";
$headers .= "Sec-WebSocket-Key2:1 23C3:,2% 1-29 4 f0{$this->_crlf}"; $headers .= "Sec-WebSocket-Key2:1 23C3:,2% 1-29 4 f0{$this->_crlf}";
@ -55,12 +56,11 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase {
public function testNoUpgradeBeforeBody() { public function testNoUpgradeBeforeBody() {
$headers = $this->headerProvider(); $headers = $this->headerProvider();
$body = base64_decode($this->_body);
$mockConn = $this->getMock('\\Ratchet\\ConnectionInterface'); $mockConn = $this->getMock('\Ratchet\ConnectionInterface');
$mockApp = $this->getMock('\\Ratchet\\MessageComponentInterface'); $mockApp = $this->getMock('\Ratchet\MessageComponentInterface');
$server = new WsServer($mockApp); $server = new HttpServer(new WsServer($mockApp));
$server->onOpen($mockConn); $server->onOpen($mockConn);
$mockApp->expects($this->exactly(0))->method('onOpen'); $mockApp->expects($this->exactly(0))->method('onOpen');
$server->onMessage($mockConn, $headers); $server->onMessage($mockConn, $headers);
@ -70,10 +70,10 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase {
$headers = $this->headerProvider(); $headers = $this->headerProvider();
$body = base64_decode($this->_body); $body = base64_decode($this->_body);
$mockConn = $this->getMock('\\Ratchet\\ConnectionInterface'); $mockConn = $this->getMock('\Ratchet\ConnectionInterface');
$mockApp = $this->getMock('\\Ratchet\\MessageComponentInterface'); $mockApp = $this->getMock('\Ratchet\MessageComponentInterface');
$server = new WsServer($mockApp); $server = new HttpServer(new WsServer($mockApp));
$server->onOpen($mockConn); $server->onOpen($mockConn);
$server->onMessage($mockConn, $headers); $server->onMessage($mockConn, $headers);