diff --git a/.travis.yml b/.travis.yml index 2c7bef4..1113aa6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,9 +6,6 @@ php: - 5.5 - hhvm -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;' - composer install --dev --prefer-source diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aae881..e1585c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ CHANGELOG --- +* 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 + +* 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 + * 0.3.0 (2013-10-14) * Added the `App` class to help making Ratchet so easy to use it's silly diff --git a/README.md b/README.md index a863173..e1ed87e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Build up your application through simple interfaces and re-use your application ##WebSocket Compliance * Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time) -* Tested on Chrome 13 - 31, Firefox 6 - 26, Safari 5.0.1 - 6.1, iOS 4.2 - 7 +* 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 @@ -19,7 +19,7 @@ To avoid proxy/firewall blockage it's recommended WebSockets are requested on po In order to do this, along with your sync web stack, you can either use a reverse proxy or two separate machines. 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 is *highly* recommended for its performance improvements. +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 diff --git a/src/Ratchet/App.php b/src/Ratchet/App.php index da634af..c6d9ceb 100644 --- a/src/Ratchet/App.php +++ b/src/Ratchet/App.php @@ -107,7 +107,9 @@ class App { $decorated = $controller; } - $httpHost = $httpHost ?: $this->httpHost; + if ($httpHost === null) { + $httpHost = $this->httpHost; + } $allowedOrigins = array_values($allowedOrigins); if (0 === count($allowedOrigins)) { @@ -128,4 +130,4 @@ class App { public function run() { $this->_server->run(); } -} +} \ No newline at end of file diff --git a/src/Ratchet/ConnectionInterface.php b/src/Ratchet/ConnectionInterface.php index 8741ed5..594e339 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'; +const VERSION = 'Ratchet/0.3.2'; /** * A proxy object representing a connection to the application diff --git a/src/Ratchet/Server/FlashPolicy.php b/src/Ratchet/Server/FlashPolicy.php index 4997362..4a1b8bd 100644 --- a/src/Ratchet/Server/FlashPolicy.php +++ b/src/Ratchet/Server/FlashPolicy.php @@ -71,6 +71,18 @@ class FlashPolicy implements MessageComponentInterface { return $this; } + + /** + * Removes all domains from the allowed access list. + * + * @return \Ratchet\Server\FlashPolicy + */ + public function clearAllowedAccess() { + $this->_access = array(); + $this->_cacheValid = false; + + return $this; + } /** * site-control defines the meta-policy for the current domain. A meta-policy specifies acceptable diff --git a/src/Ratchet/Server/IoServer.php b/src/Ratchet/Server/IoServer.php index b27ee93..921c7b1 100644 --- a/src/Ratchet/Server/IoServer.php +++ b/src/Ratchet/Server/IoServer.php @@ -27,6 +27,12 @@ class IoServer { */ protected $handlers; + /** + * The socket server the Ratchet Application is run off of + * @var \React\Socket\ServerInterface + */ + public $socket; + /** * @param \Ratchet\MessageComponentInterface $app The Ratchet application stack to host * @param \React\Socket\ServerInterface $socket The React socket server to run the Ratchet application off of @@ -42,6 +48,7 @@ class IoServer { $this->loop = $loop; $this->app = $app; + $this->socket = $socket; $socket->on('connection', array($this, 'handleConnect')); diff --git a/src/Ratchet/Session/SessionProvider.php b/src/Ratchet/Session/SessionProvider.php index f9f2462..043b3ab 100644 --- a/src/Ratchet/Session/SessionProvider.php +++ b/src/Ratchet/Session/SessionProvider.php @@ -57,14 +57,7 @@ class SessionProvider implements HttpServerInterface { $this->setOptions($options); if (null === $serializer) { - // Temporarily fixing HHVM issue w/ reading ini values - $handler_name = ini_get('session.serialize_handler'); - if ('' === $handler_name) { - trigger_error('ini value session.seralize_handler was empty, assuming "php" - tmp hack/fix, bad things might happen', E_USER_WARNING); - $handler_name = 'php'; - } - - $serialClass = __NAMESPACE__ . "\\Serialize\\{$this->toClassCase($handler_name)}Handler"; // awesome/terrible hack, eh? + $serialClass = __NAMESPACE__ . "\\Serialize\\{$this->toClassCase(ini_get('session.serialize_handler'))}Handler"; // awesome/terrible hack, eh? if (!class_exists($serialClass)) { throw new \RuntimeException('Unable to parse session serialize handler'); } diff --git a/src/Ratchet/Wamp/ServerProtocol.php b/src/Ratchet/Wamp/ServerProtocol.php index 0a5805e..92dbd85 100644 --- a/src/Ratchet/Wamp/ServerProtocol.php +++ b/src/Ratchet/Wamp/ServerProtocol.php @@ -79,8 +79,8 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface { /** * {@inheritdoc} - * @throws \Exception - * @throws JsonException + * @throws \Ratchet\Wamp\Exception + * @throws \Ratchet\Wamp\JsonException */ public function onMessage(ConnectionInterface $from, $msg) { $from = $this->connections[$from]; @@ -90,7 +90,7 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface { } if (!is_array($json) || $json !== array_values($json)) { - throw new \UnexpectedValueException("Invalid WAMP message format"); + throw new Exception("Invalid WAMP message format"); } switch ($json[0]) { @@ -134,7 +134,7 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface { break; default: - throw new Exception('Invalid message type'); + throw new Exception('Invalid WAMP message type'); } } diff --git a/src/Ratchet/Wamp/Topic.php b/src/Ratchet/Wamp/Topic.php index f1bd68a..3fe73d1 100644 --- a/src/Ratchet/Wamp/Topic.php +++ b/src/Ratchet/Wamp/Topic.php @@ -6,6 +6,13 @@ 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; diff --git a/src/Ratchet/Wamp/TopicManager.php b/src/Ratchet/Wamp/TopicManager.php index a69e315..318b986 100644 --- a/src/Ratchet/Wamp/TopicManager.php +++ b/src/Ratchet/Wamp/TopicManager.php @@ -54,13 +54,12 @@ class TopicManager implements WsServerInterface, WampServerInterface { public function onUnsubscribe(ConnectionInterface $conn, $topic) { $topicObj = $this->getTopic($topic); - if ($conn->WAMP->subscriptions->contains($topicObj)) { - $conn->WAMP->subscriptions->detach($topicObj); - } else { + if (!$conn->WAMP->subscriptions->contains($topicObj)) { return; } - $this->topicLookup[$topic]->remove($conn); + $this->cleanTopic($topicObj, $conn); + $this->app->onUnsubscribe($conn, $topicObj); } @@ -77,11 +76,8 @@ class TopicManager implements WsServerInterface, WampServerInterface { public function onClose(ConnectionInterface $conn) { $this->app->onClose($conn); - foreach ($this->topicLookup as $topic => $storage) { - $storage->remove($conn); - if (0 === $storage->count()) { - unset($this->topicLookup[$topic]); - } + foreach ($this->topicLookup as $topic) { + $this->cleanTopic($topic, $conn); } } @@ -114,4 +110,16 @@ class TopicManager implements WsServerInterface, WampServerInterface { return $this->topicLookup[$topic]; } + + protected function cleanTopic(Topic $topic, ConnectionInterface $conn) { + if ($conn->WAMP->subscriptions->contains($topic)) { + $conn->WAMP->subscriptions->detach($topic); + } + + $this->topicLookup[$topic->getId()]->remove($conn); + + if ($topic->autoDelete && 0 === $topic->count()) { + unset($this->topicLookup[$topic->getId()]); + } + } } diff --git a/src/Ratchet/Wamp/WampServer.php b/src/Ratchet/Wamp/WampServer.php index 7644fdc..d839fb8 100644 --- a/src/Ratchet/Wamp/WampServer.php +++ b/src/Ratchet/Wamp/WampServer.php @@ -39,9 +39,9 @@ class WampServer implements MessageComponentInterface, WsServerInterface { public function onMessage(ConnectionInterface $conn, $msg) { try { $this->wampProtocol->onMessage($conn, $msg); - } catch (JsonException $je) { + } catch (Exception $we) { $conn->close(1007); - } catch (\UnexpectedValueException $uve) { + } catch (JsonException $je) { $conn->close(1007); } } diff --git a/src/Ratchet/WebSocket/Version/Hixie76/Connection.php b/src/Ratchet/WebSocket/Version/Hixie76/Connection.php index 2a5f845..e3d0834 100644 --- a/src/Ratchet/WebSocket/Version/Hixie76/Connection.php +++ b/src/Ratchet/WebSocket/Version/Hixie76/Connection.php @@ -8,12 +8,19 @@ use Ratchet\AbstractConnectionDecorator; */ class Connection extends AbstractConnectionDecorator { public function send($msg) { - $this->getConnection()->send(chr(0) . $msg . chr(255)); + if (!$this->WebSocket->closing) { + $this->getConnection()->send(chr(0) . $msg . chr(255)); + } return $this; } public function close() { - $this->getConnection()->close(); + if (!$this->WebSocket->closing) { + $this->getConnection()->send(chr(255)); + $this->getConnection()->close(); + + $this->WebSocket->closing = true; + } } } diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php b/src/Ratchet/WebSocket/Version/RFC6455/Connection.php index 5bfa4a9..a17e382 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Connection.php +++ b/src/Ratchet/WebSocket/Version/RFC6455/Connection.php @@ -8,12 +8,17 @@ use Ratchet\WebSocket\Version\DataInterface; * @property \StdClass $WebSocket */ class Connection extends AbstractConnectionDecorator { + /** + * {@inheritdoc} + */ public function send($msg) { - if (!($msg instanceof DataInterface)) { - $msg = new Frame($msg); - } + if (!$this->WebSocket->closing) { + if (!($msg instanceof DataInterface)) { + $msg = new Frame($msg); + } - $this->getConnection()->send($msg->getContents()); + $this->getConnection()->send($msg->getContents()); + } return $this; } @@ -22,6 +27,10 @@ class Connection extends AbstractConnectionDecorator { * {@inheritdoc} */ public function close($code = 1000) { + if ($this->WebSocket->closing) { + return; + } + if ($code instanceof DataInterface) { $this->send($code); } else { @@ -29,5 +38,7 @@ class Connection extends AbstractConnectionDecorator { } $this->getConnection()->close(); + + $this->WebSocket->closing = true; } } diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index 8ad6e4e..b4be1f0 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/WsServer.php @@ -79,6 +79,7 @@ class WsServer implements HttpServerInterface { $conn->WebSocket = new \StdClass; $conn->WebSocket->request = $request; $conn->WebSocket->established = false; + $conn->WebSocket->closing = false; $this->attemptUpgrade($conn); } @@ -87,6 +88,10 @@ class WsServer implements HttpServerInterface { * {@inheritdoc} */ public function onMessage(ConnectionInterface $from, $msg) { + if ($from->WebSocket->closing) { + return; + } + if (true === $from->WebSocket->established) { return $from->WebSocket->version->onMessage($this->connections[$from], $msg); } diff --git a/tests/unit/Wamp/ServerProtocolTest.php b/tests/unit/Wamp/ServerProtocolTest.php index 3cc44f0..1b423d2 100644 --- a/tests/unit/Wamp/ServerProtocolTest.php +++ b/tests/unit/Wamp/ServerProtocolTest.php @@ -258,7 +258,7 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { * @dataProvider badFormatProvider */ public function testValidJsonButInvalidProtocol($message) { - $this->setExpectedException('\UnexpectedValueException'); + $this->setExpectedException('\Ratchet\Wamp\Exception'); $conn = $this->newConn(); $this->_comp->onOpen($conn); diff --git a/tests/unit/Wamp/TopicManagerTest.php b/tests/unit/Wamp/TopicManagerTest.php index 7439064..8482877 100644 --- a/tests/unit/Wamp/TopicManagerTest.php +++ b/tests/unit/Wamp/TopicManagerTest.php @@ -159,9 +159,7 @@ class TopicManagerTest extends \PHPUnit_Framework_TestCase { $this->mngr->onClose($this->conn); } - public function testConnIsRemovedFromTopicOnClose() { - $name = 'State testing'; - + protected function topicProvider($name) { $class = new \ReflectionClass('Ratchet\Wamp\TopicManager'); $method = $class->getMethod('getTopic'); $method->setAccessible(true); @@ -171,14 +169,42 @@ class TopicManagerTest extends \PHPUnit_Framework_TestCase { $topic = $method->invokeArgs($this->mngr, array($name)); + return array($topic, $attribute); + } + + public function testConnIsRemovedFromTopicOnClose() { + $name = 'State Testing'; + list($topic, $attribute) = $this->topicProvider($name); + $this->assertCount(1, $attribute->getValue($this->mngr)); $this->mngr->onSubscribe($this->conn, $name); $this->mngr->onClose($this->conn); $this->assertFalse($topic->has($this->conn)); + } - $this->assertCount(0, $attribute->getValue($this->mngr)); + public static function topicConnExpectationProvider() { + return array( + array(true, 'onClose', 0) + , array(true, 'onUnsubscribe', 0) + , array(false, 'onClose', 1) + , array(false, 'onUnsubscribe', 1) + ); + } + + /** + * @dataProvider topicConnExpectationProvider + */ + public function testTopicRetentionFromLeavingConnections($autoDelete, $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)); + + $this->assertCount($expectation, $attribute->getValue($this->mngr)); } public function testOnErrorBubbles() {