diff --git a/.gitignore b/.gitignore index 2e580df..793ef58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -phpunit.xml -reports -sandbox -vendor -composer.lock +phpunit.xml +reports +sandbox +vendor +composer.lock \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1113aa6..f0b9273 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ php: - 5.3 - 5.4 - 5.5 + - 5.6 + - 7 - hhvm before_script: diff --git a/CHANGELOG.md b/CHANGELOG.md index e1585c2..9ee7f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ CHANGELOG --- +* 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 + +* 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 + * 0.3.2 (2014-06-08) * BF: No messages after closing handshake (fixed rare race condition causing 100% CPU) @@ -111,4 +126,4 @@ CHANGELOG * 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 \ No newline at end of file + * I/O now handled by React, making Ratchet fully asynchronous diff --git a/LICENSE b/LICENSE index 66857ea..7f8c128 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2014 Chris Boden +Copyright (c) 2011-2016 Chris Boden Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e1ed87e..5cc5976 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ #Ratchet -[![Build Status](https://secure.travis-ci.org/cboden/Ratchet.png?branch=master)](http://travis-ci.org/cboden/Ratchet) -[![Latest Stable Version](https://poser.pugx.org/cboden/Ratchet/v/stable.png)](https://packagist.org/packages/cboden/Ratchet) +[![Build Status](https://secure.travis-ci.org/ratchetphp/Ratchet.png?branch=master)](http://travis-ci.org/ratchetphp/Ratchet) +[![Latest Stable Version](https://poser.pugx.org/cboden/ratchet/v/stable.png)](https://packagist.org/packages/cboden/ratchet) A PHP 5.3 library for asynchronously serving WebSockets. Build up your application through simple interfaces and re-use your application without changing any of its code just by combining different components. diff --git a/composer.json b/composer.json index c785ce0..80a3aa5 100644 --- a/composer.json +++ b/composer.json @@ -14,19 +14,19 @@ ] , "support": { "forum": "https://groups.google.com/forum/#!forum/ratchet-php" - , "issues": "https://github.com/cboden/Ratchet/issues" + , "issues": "https://github.com/ratchetphp/Ratchet/issues" , "irc": "irc://irc.freenode.org/reactphp" } , "autoload": { - "psr-0": { - "Ratchet": "src" + "psr-4": { + "Ratchet\\": "src/Ratchet" } } , "require": { "php": ">=5.3.9" - , "react/socket": "0.3.*|0.4.*" - , "guzzle/http": "~3.6" - , "symfony/http-foundation": "~2.2" - , "symfony/routing": "~2.2" + , "react/socket": "^0.3 || ^0.4" + , "guzzle/http": "^3.6" + , "symfony/http-foundation": "^2.2|^3.0" + , "symfony/routing": "^2.2|^3.0" } } diff --git a/src/Ratchet/App.php b/src/Ratchet/App.php index c6d9ceb..b7d0e55 100644 --- a/src/Ratchet/App.php +++ b/src/Ratchet/App.php @@ -43,6 +43,12 @@ class App { */ protected $httpHost; + /*** + * The port the socket is listening + * @var int + */ + protected $port; + /** * @var int */ @@ -56,7 +62,7 @@ class App { */ 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); + trigger_error('XDebug extension detected. Remember to disable this if performance testing or going live!', E_USER_WARNING); } if (3 !== strlen('✓')) { @@ -68,6 +74,7 @@ class App { } $this->httpHost = $httpHost; + $this->port = $port; $socket = new Reactor($loop); $socket->listen($port, $address); @@ -80,7 +87,6 @@ class App { $policy->addAllowedAccess($httpHost, $port); $flashSock = new Reactor($loop); $this->flashServer = new IoServer($policy, $flashSock); - if (80 == $port) { $flashSock->listen(843, '0.0.0.0'); } else { @@ -89,7 +95,7 @@ class App { } /** - * Add an endpiont/application to the server + * Add an endpoint/application to the server * @param string $path The URI the client will connect to * @param ComponentInterface $controller Your application to server for the route. If not specified, assumed to be for a WebSocket * @param array $allowedOrigins An array of hosts allowed to connect (same host by default), ['*'] for any @@ -119,6 +125,13 @@ class App { $decorated = new OriginCheck($decorated, $allowedOrigins); } + //allow origins in flash policy server + if(empty($this->flashServer) === false) { + foreach($allowedOrigins as $allowedOrgin) { + $this->flashServer->app->addAllowedAccess($allowedOrgin, $this->port); + } + } + $this->routes->add('rr-' . ++$this->_routeCounter, new Route($path, array('_controller' => $decorated), array('Origin' => $this->httpHost), array(), $httpHost)); return $decorated; @@ -130,4 +143,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 594e339..5c07a2d 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.2'; +const VERSION = 'Ratchet/0.3.4'; /** * A proxy object representing a connection to the application diff --git a/src/Ratchet/Http/Router.php b/src/Ratchet/Http/Router.php index 817f6a8..bfc8193 100644 --- a/src/Ratchet/Http/Router.php +++ b/src/Ratchet/Http/Router.php @@ -53,6 +53,8 @@ class Router implements HttpServerInterface { $parameters[$key] = $value; } } + $parameters = array_merge($parameters, $request->getQuery()->getAll()); + $url = Url::factory($request->getPath()); $url->setQuery($parameters); $request->setUrl($url); diff --git a/src/Ratchet/Session/Storage/VirtualSessionStorage.php b/src/Ratchet/Session/Storage/VirtualSessionStorage.php index 9fb0eb7..daa10bb 100644 --- a/src/Ratchet/Session/Storage/VirtualSessionStorage.php +++ b/src/Ratchet/Session/Storage/VirtualSessionStorage.php @@ -30,6 +30,12 @@ class VirtualSessionStorage extends NativeSessionStorage { return true; } + // You have to call Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler::open() to use + // pdo_sqlite (and possible pdo_*) as session storage, if you are using a DSN string instead of a \PDO object + // in the constructor. The method arguments are filled with the values, which are also used by the symfony + // framework in this case. This must not be the best choice, but it works. + $this->saveHandler->open(session_save_path(), session_name()); + $rawData = $this->saveHandler->read($this->saveHandler->getId()); $sessionData = $this->_serializer->unserialize($rawData); diff --git a/src/Ratchet/Wamp/ServerProtocol.php b/src/Ratchet/Wamp/ServerProtocol.php index 92dbd85..28badd3 100644 --- a/src/Ratchet/Wamp/ServerProtocol.php +++ b/src/Ratchet/Wamp/ServerProtocol.php @@ -107,7 +107,7 @@ class ServerProtocol implements MessageComponentInterface, WsServerInterface { $json = $json[0]; } - $this->_decorating->onCall($from, $callID, $procURI, $json); + $this->_decorating->onCall($from, $callID, $from->getUri($procURI), $json); break; case static::MSG_SUBSCRIBE: diff --git a/src/Ratchet/Wamp/WampConnection.php b/src/Ratchet/Wamp/WampConnection.php index 95e1969..dda1e4e 100644 --- a/src/Ratchet/Wamp/WampConnection.php +++ b/src/Ratchet/Wamp/WampConnection.php @@ -17,7 +17,7 @@ class WampConnection extends AbstractConnectionDecorator { parent::__construct($conn); $this->WAMP = new \StdClass; - $this->WAMP->sessionId = uniqid(); + $this->WAMP->sessionId = str_replace('.', '', uniqid(mt_rand(), true)); $this->WAMP->prefixes = array(); $this->send(json_encode(array(WAMP::MSG_WELCOME, $this->WAMP->sessionId, 1, \Ratchet\VERSION))); @@ -26,10 +26,10 @@ class WampConnection extends AbstractConnectionDecorator { /** * Successfully respond to a call made by the client * @param string $id The unique ID given by the client to respond to - * @param array $data An array of data to return to the client + * @param array $data an object or array * @return WampConnection */ - public function callResult($id, array $data = array()) { + public function callResult($id, $data = array()) { return $this->send(json_encode(array(WAMP::MSG_CALL_RESULT, $id, $data))); } @@ -81,7 +81,19 @@ class WampConnection extends AbstractConnectionDecorator { * @return string */ public function getUri($uri) { - return (array_key_exists($uri, $this->WAMP->prefixes) ? $this->WAMP->prefixes[$uri] : $uri); + $curieSeperator = ':'; + + if (preg_match('/http(s*)\:\/\//', $uri) == false) { + if (strpos($uri, $curieSeperator) !== false) { + list($prefix, $action) = explode($curieSeperator, $uri); + + if(isset($this->WAMP->prefixes[$prefix]) === true){ + return $this->WAMP->prefixes[$prefix] . '#' . $action; + } + } + } + + return $uri; } /** diff --git a/src/Ratchet/Wamp/WampServer.php b/src/Ratchet/Wamp/WampServer.php index d839fb8..f0675a3 100644 --- a/src/Ratchet/Wamp/WampServer.php +++ b/src/Ratchet/Wamp/WampServer.php @@ -15,7 +15,7 @@ class WampServer implements MessageComponentInterface, WsServerInterface { /** * @var ServerProtocol */ - private $wampProtocol; + protected $wampProtocol; /** * This class just makes it 1 step easier to use Topic objects in WAMP @@ -41,8 +41,6 @@ class WampServer implements MessageComponentInterface, WsServerInterface { $this->wampProtocol->onMessage($conn, $msg); } catch (Exception $we) { $conn->close(1007); - } catch (JsonException $je) { - $conn->close(1007); } } diff --git a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php index 1a92c1f..77d1258 100644 --- a/src/Ratchet/WebSocket/Version/RFC6455/Frame.php +++ b/src/Ratchet/WebSocket/Version/RFC6455/Frame.php @@ -379,6 +379,7 @@ class Frame implements FrameInterface { $byte_length = $this->getNumPayloadBytes(); if ($this->bytesRecvd < 1 + $byte_length) { + $this->defPayLen = -1; throw new \UnderflowException('Not enough data buffered to determine payload length'); } diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index b4be1f0..5783789 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/WsServer.php @@ -102,14 +102,14 @@ class WsServer implements HttpServerInterface { protected function attemptUpgrade(ConnectionInterface $conn, $data = '') { if ('' !== $data) { $conn->WebSocket->request->getBody()->write($data); - } else { - if (!$this->versioner->isVersionEnabled($conn->WebSocket->request)) { - return $this->close($conn); - } - - $conn->WebSocket->version = $this->versioner->getVersion($conn->WebSocket->request); } + 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) { diff --git a/src/Ratchet/WebSocket/WsServerInterface.php b/src/Ratchet/WebSocket/WsServerInterface.php index 03b0710..15d1f7b 100644 --- a/src/Ratchet/WebSocket/WsServerInterface.php +++ b/src/Ratchet/WebSocket/WsServerInterface.php @@ -8,7 +8,7 @@ interface WsServerInterface { /** * If any component in a stack supports a WebSocket sub-protocol return each supported in an array * @return array - * @temporary This method may be removed in future version (note that will not break code, just make some code obsolete) + * @todo This method may be removed in future version (note that will not break code, just make some code obsolete) */ function getSubProtocols(); } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9d21c77..40791ba 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,4 @@ add('Ratchet', __DIR__ . '/helpers'); - $loader->register(); + $loader->addPsr4('Ratchet\\', __DIR__ . '/helpers/Ratchet'); diff --git a/tests/unit/Http/RouterTest.php b/tests/unit/Http/RouterTest.php index 412e774..5a1128e 100644 --- a/tests/unit/Http/RouterTest.php +++ b/tests/unit/Http/RouterTest.php @@ -15,8 +15,18 @@ class RouterTest extends \PHPUnit_Framework_TestCase { protected $_req; public function setUp() { + $queryMock = $this->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->_req + ->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryMock)); $this->_matcher = $this->getMock('Symfony\Component\Routing\Matcher\UrlMatcherInterface'); $this->_matcher ->expects($this->any()) @@ -88,8 +98,7 @@ class RouterTest extends \PHPUnit_Framework_TestCase { $this->_router->onError($this->_conn, $e); } - public function testRouterGeneratesRouteParameters() - { + public function testRouterGeneratesRouteParameters() { /** @var $controller WsServerInterface */ $controller = $this->getMockBuilder('\Ratchet\WebSocket\WsServer')->disableOriginalConstructor()->getMock(); /** @var $matcher UrlMatcherInterface */ @@ -110,4 +119,22 @@ class RouterTest extends \PHPUnit_Framework_TestCase { $this->assertEquals(array('foo' => 'bar', 'baz' => 'qux'), $request->getQuery()->getAll()); } + + 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')) + ); + + $conn = $this->getMock('Ratchet\Mock\Connection'); + $request = $this->getMock('Guzzle\Http\Message\Request', array('getPath'), array('GET', ''), '', false); + + $request->setHeaderFactory($this->getMock('Guzzle\Http\Message\Header\HeaderFactoryInterface')); + $request->setUrl('ws://doesnt.matter?hello=world&foo=nope'); + + $router = new Router($this->_matcher); + $router->onOpen($conn, $request); + + $this->assertEquals(array('foo' => 'nope', 'baz' => 'qux', 'hello' => 'world'), $request->getQuery()->getAll()); + } } diff --git a/tests/unit/Session/SessionComponentTest.php b/tests/unit/Session/SessionComponentTest.php index c7df77d..e889637 100644 --- a/tests/unit/Session/SessionComponentTest.php +++ b/tests/unit/Session/SessionComponentTest.php @@ -70,14 +70,17 @@ class SessionProviderTest extends AbstractMessageComponentTestCase { , 'db_id_col' => 'sess_id' , 'db_data_col' => 'sess_data' , 'db_time_col' => 'sess_time' + , 'db_lifetime_col' => 'sess_lifetime' ); $pdo = new \PDO("sqlite::memory:"); $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); - $pdo->exec(vsprintf("CREATE TABLE %s (%s VARCHAR(255) PRIMARY KEY, %s TEXT, %s INTEGER)", $dbOptions)); - $pdo->prepare(vsprintf("INSERT INTO %s (%s, %s, %s) VALUES (?, ?, ?)", $dbOptions))->execute(array($sessionId, base64_encode('_sf2_attributes|a:2:{s:5:"hello";s:5:"world";s:4:"last";i:1332872102;}_sf2_flashes|a:0:{}'), time())); + $pdo->exec(vsprintf("CREATE TABLE %s (%s TEXT NOT NULL PRIMARY KEY, %s BLOB NOT NULL, %s INTEGER NOT NULL, %s INTEGER)", $dbOptions)); - $component = new SessionProvider($this->getMock($this->getComponentClassString()), new PdoSessionHandler($pdo, $dbOptions), array('auto_start' => 1)); + $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($this->getComponentClassString()), $pdoHandler, array('auto_start' => 1)); $connection = $this->getMock('Ratchet\\ConnectionInterface'); $headers = $this->getMock('Guzzle\\Http\\Message\\Request', array('getCookie'), array('POST', '/', array())); diff --git a/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php b/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php new file mode 100644 index 0000000..9909bce --- /dev/null +++ b/tests/unit/Session/Storage/VirtualSessionStoragePDOTest.php @@ -0,0 +1,58 @@ +_pathToDB = tempnam(sys_get_temp_dir(), 'SQ3');; + $dsn = 'sqlite:' . $this->_pathToDB; + + $pdo = new \PDO($dsn); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $pdo->exec($schema); + $pdo = null; + + $sessionHandler = new PdoSessionHandler($dsn); + $serializer = new PhpHandler(); + $this->_virtualSessionStorage = new VirtualSessionStorage($sessionHandler, 'foobar', $serializer); + $this->_virtualSessionStorage->registerBag(new FlashBag()); + $this->_virtualSessionStorage->registerBag(new AttributeBag()); + } + + public function tearDown() + { + unlink($this->_pathToDB); + } + + 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 1b423d2..082a3f5 100644 --- a/tests/unit/Wamp/ServerProtocolTest.php +++ b/tests/unit/Wamp/ServerProtocolTest.php @@ -211,13 +211,14 @@ class ServerProtocolTest extends \PHPUnit_Framework_TestCase { $conn = new WampConnection($this->newConn()); $this->_comp->onOpen($conn); - $shortIn = 'incoming'; - $longIn = 'http://example.com/incoming/'; + $prefix = 'incoming'; + $fullURI = "http://example.com/$prefix"; + $method = 'call'; - $this->_comp->onMessage($conn, json_encode(array(1, $shortIn, $longIn))); + $this->_comp->onMessage($conn, json_encode(array(1, $prefix, $fullURI))); - $this->assertEquals($longIn, $conn->WAMP->prefixes[$shortIn]); - $this->assertEquals($longIn, $conn->getUri($shortIn)); + $this->assertEquals($fullURI, $conn->WAMP->prefixes[$prefix]); + $this->assertEquals("$fullURI#$method", $conn->getUri("$prefix:$method")); } public function testMessageMustBeJson() { diff --git a/tests/unit/WebSocket/Version/Hixie76Test.php b/tests/unit/WebSocket/Version/Hixie76Test.php index d09cdf7..75998aa 100644 --- a/tests/unit/WebSocket/Version/Hixie76Test.php +++ b/tests/unit/WebSocket/Version/Hixie76Test.php @@ -80,4 +80,24 @@ class Hixie76Test extends \PHPUnit_Framework_TestCase { $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/RFC6455/FrameTest.php b/tests/unit/WebSocket/Version/RFC6455/FrameTest.php index 7f56a6e..eff9513 100644 --- a/tests/unit/WebSocket/Version/RFC6455/FrameTest.php +++ b/tests/unit/WebSocket/Version/RFC6455/FrameTest.php @@ -500,4 +500,44 @@ class FrameTest extends \PHPUnit_Framework_TestCase { 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()); + } }