From f729be2ef35526288ef3c83bfc1afd90c285c36c Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Sat, 12 May 2012 22:42:56 -0400 Subject: [PATCH] [WebSocket] [WAMP] Sub-Protocols Updated how Ratchet handles WebSocket sub-protocols Broke out WsServerInterface to not extend MessageInterface; Components will instead use Interface segregation principle WAMP is now able to work without the developer having to manually enable the WAMP sub-protocol --- src/Ratchet/Session/SessionProvider.php | 14 +++- src/Ratchet/Wamp/WampServer.php | 16 +++-- src/Ratchet/Wamp/WampServerInterface.php | 1 - src/Ratchet/WebSocket/WsServer.php | 64 +++++++++++++++---- src/Ratchet/WebSocket/WsServerInterface.php | 15 ++--- tests/Ratchet/Tests/Mock/Component.php | 12 ++-- tests/Ratchet/Tests/Mock/WampComponent.php | 9 ++- .../Tests/Session/SessionComponentTest.php | 15 +++++ tests/Ratchet/Tests/Wamp/WampServerTest.php | 10 +++ .../Ratchet/Tests/WebSocket/WsServerTest.php | 48 ++++++++++++++ 10 files changed, 169 insertions(+), 35 deletions(-) create mode 100644 tests/Ratchet/Tests/WebSocket/WsServerTest.php diff --git a/src/Ratchet/Session/SessionProvider.php b/src/Ratchet/Session/SessionProvider.php index 7dd1f33..cda8723 100644 --- a/src/Ratchet/Session/SessionProvider.php +++ b/src/Ratchet/Session/SessionProvider.php @@ -2,6 +2,7 @@ namespace Ratchet\Session; use Ratchet\MessageComponentInterface; use Ratchet\ConnectionInterface; +use Ratchet\WebSocket\WsServerInterface; use Ratchet\Session\Storage\VirtualSessionStorage; use Ratchet\Session\Serialize\HandlerInterface; use Symfony\Component\HttpFoundation\Session\Session; @@ -13,7 +14,7 @@ use Symfony\Component\HttpFoundation\Session\Storage\Handler\NullSessionHandler; * Your website must also use Symfony HttpFoundation Sessions to read your sites session data * If your are not using at least PHP 5.4 you must include a SessionHandlerInterface stub (is included in Symfony HttpFoundation, loaded w/ composer) */ -class SessionProvider implements MessageComponentInterface { +class SessionProvider implements MessageComponentInterface, WsServerInterface { /** * @var Ratchet\MessageComponentInterface */ @@ -109,6 +110,17 @@ class SessionProvider implements MessageComponentInterface { return $this->_app->onError($conn, $e); } + /** + * {@inheritdoc} + */ + public function getSubProtocols() { + if ($this->_app instanceof WsServerInterface) { + return $this->_app->getSubProtocols(); + } else { + return array(); + } + } + /** * Set all the php session. ini options * © Symfony diff --git a/src/Ratchet/Wamp/WampServer.php b/src/Ratchet/Wamp/WampServer.php index ae2e03b..72d3739 100644 --- a/src/Ratchet/Wamp/WampServer.php +++ b/src/Ratchet/Wamp/WampServer.php @@ -1,5 +1,6 @@ _decorating instanceof WsServerInterface) { + $subs = $this->_decorating->getSubProtocols(); + $subs[] = 'wamp'; + + return $subs; + } else { + return array('wamp'); + } } /** @@ -70,7 +78,7 @@ class WampServer implements WsServerInterface { } /** - * @{inheritdoc} + * {@inheritdoc} * @throws Exception * @throws JsonException */ diff --git a/src/Ratchet/Wamp/WampServerInterface.php b/src/Ratchet/Wamp/WampServerInterface.php index 528d627..3bb4476 100644 --- a/src/Ratchet/Wamp/WampServerInterface.php +++ b/src/Ratchet/Wamp/WampServerInterface.php @@ -6,7 +6,6 @@ use Ratchet\ConnectionInterface; /** * A (not literal) extension of Ratchet\ConnectionInterface * onMessage is replaced by various types of messages for this protocol (pub/sub or rpc) - * @todo Thought: URI as class. Class has short and long version stored (if as prefix) */ interface WampServerInterface extends ComponentInterface { /** diff --git a/src/Ratchet/WebSocket/WsServer.php b/src/Ratchet/WebSocket/WsServer.php index c3b1a48..748facc 100644 --- a/src/Ratchet/WebSocket/WsServer.php +++ b/src/Ratchet/WebSocket/WsServer.php @@ -15,7 +15,7 @@ use Ratchet\WebSocket\Guzzle\Http\Message\RequestFactory; class WsServer implements MessageComponentInterface { /** * Decorated component - * @var Ratchet\MessageComponentInterface + * @var Ratchet\MessageComponentInterface|WsServerInterface */ protected $_decorating; @@ -41,8 +41,17 @@ class WsServer implements MessageComponentInterface { * @deprecated * @temporary */ - public $accepted_subprotocols = array(); + protected $acceptedSubProtocols = array(); + /** + * Flag if we have checked the decorated component for sub-protocols + * @var boolean + */ + private $isSpGenerated = false; + + /** + * @param Ratchet\MessageComponentInterface Your application to run with WebSockets + */ public function __construct(MessageComponentInterface $component) { $this->_decorating = $component; $this->connections = new \SplObjectStorage; @@ -77,19 +86,10 @@ class WsServer implements MessageComponentInterface { $response = $from->WebSocket->version->handshake($from->WebSocket->headers); $from->WebSocket->handshake = true; - // This block is to be moved/changed later - $agreed_protocols = array(); - $requested_protocols = $from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ','); - if (null !== $requested_protocols) { - foreach ($this->accepted_subprotocols as $sub_protocol) { - if (false !== $requested_protocols->hasValue($sub_protocol)) { - $agreed_protocols[] = $sub_protocol; - } - } - } - if (count($agreed_protocols) > 0) { - $response->setHeader('Sec-WebSocket-Protocol', implode(',', $agreed_protocols)); + if ('' !== ($agreedSubProtocols = $this->getSubProtocolString($from->WebSocket->headers->getTokenizedHeader('Sec-WebSocket-Protocol', ',')))) { + $response->setHeader('Sec-WebSocket-Protocol', $agreedSubProtocols); } + $response->setHeader('X-Powered-By', \Ratchet\VERSION); $header = (string)$response; @@ -205,6 +205,42 @@ class WsServer implements MessageComponentInterface { return true; } + /** + * @param string + * @return boolean + */ + public function isSubProtocolSupported($name) { + if (!$this->isSpGenerated) { + if ($this->_decorating instanceof WsServerInterface) { + $this->acceptedSubProtocols = array_flip($this->_decorating->getSubProtocols()); + } + + $this->isSpGenerated = true; + } + + return array_key_exists($name, $this->acceptedSubProtocols); + } + + /** + * @param Traversable + * @return string + */ + protected function getSubProtocolString(\Traversable $requested = null) { + if (null === $requested) { + return ''; + } + + $string = ''; + + foreach ($requested as $sub) { + if ($this->isSubProtocolSupported($sub)) { + $string .= $sub . ','; + } + } + + return substr($string, 0, -1); + } + /** * Disable a version of the WebSocket protocol *cough*Hixie76*cough* * @param string The name of the version to disable diff --git a/src/Ratchet/WebSocket/WsServerInterface.php b/src/Ratchet/WebSocket/WsServerInterface.php index 30b9ba4..8cb378d 100644 --- a/src/Ratchet/WebSocket/WsServerInterface.php +++ b/src/Ratchet/WebSocket/WsServerInterface.php @@ -1,16 +1,11 @@ last[__FUNCTION__] = func_get_args(); } @@ -29,4 +29,8 @@ class Component implements MessageComponentInterface { public function onError(ConnectionInterface $conn, \Exception $e) { $this->last[__FUNCTION__] = func_get_args(); } + + public function getSubProtocols() { + return $this->protocols; + } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/Mock/WampComponent.php b/tests/Ratchet/Tests/Mock/WampComponent.php index 8d1460d..803e999 100644 --- a/tests/Ratchet/Tests/Mock/WampComponent.php +++ b/tests/Ratchet/Tests/Mock/WampComponent.php @@ -1,11 +1,18 @@ protocols; + } + public function onCall(ConnectionInterface $conn, $id, $procURI, array $params) { $this->last[__FUNCTION__] = func_get_args(); } diff --git a/tests/Ratchet/Tests/Session/SessionComponentTest.php b/tests/Ratchet/Tests/Session/SessionComponentTest.php index 3222cca..56c37c2 100644 --- a/tests/Ratchet/Tests/Session/SessionComponentTest.php +++ b/tests/Ratchet/Tests/Session/SessionComponentTest.php @@ -107,4 +107,19 @@ class SessionProviderTest extends \PHPUnit_Framework_TestCase { $this->assertEquals($conns[2], $mock->last['onError'][0]); $this->assertEquals($e, $mock->last['onError'][1]); } + + public function testGetSubProtocolsReturnsArray() { + $mock = new MockComponent; + $comp = new SessionProvider($mock, new NullSessionHandler); + + $this->assertTrue(is_array($comp->getSubProtocols())); + } + + public function testGetSubProtocolsGetFromApp() { + $mock = new MockComponent; + $mock->protocols = array('hello', 'world'); + $comp = new SessionProvider($mock, new NullSessionHandler); + + $this->assertGreaterThanOrEqual(2, count($comp->getSubProtocols())); + } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/Wamp/WampServerTest.php b/tests/Ratchet/Tests/Wamp/WampServerTest.php index f829859..4643d9d 100644 --- a/tests/Ratchet/Tests/Wamp/WampServerTest.php +++ b/tests/Ratchet/Tests/Wamp/WampServerTest.php @@ -203,4 +203,14 @@ class WampServerTest extends \PHPUnit_Framework_TestCase { $this->_comp->onOpen($conn); $this->_comp->onMessage($conn, 'Hello World!'); } + + public function testGetSubProtocolsReturnsArray() { + $this->assertTrue(is_array($this->_comp->getSubProtocols())); + } + + public function testGetSubProtocolsGetFromApp() { + $this->_app->protocols = array('hello', 'world'); + + $this->assertGreaterThanOrEqual(3, count($this->_comp->getSubProtocols())); + } } \ No newline at end of file diff --git a/tests/Ratchet/Tests/WebSocket/WsServerTest.php b/tests/Ratchet/Tests/WebSocket/WsServerTest.php new file mode 100644 index 0000000..46079fb --- /dev/null +++ b/tests/Ratchet/Tests/WebSocket/WsServerTest.php @@ -0,0 +1,48 @@ +comp = new MockComponent; + $this->serv = new WsServer($this->comp); + } + + public function testIsSubProtocolSupported() { + $this->comp->protocols = array('hello', 'world'); + + $this->assertTrue($this->serv->isSubProtocolSupported('hello')); + $this->assertFalse($this->serv->isSubProtocolSupported('nope')); + } + + public function protocolProvider() { + return array( + array('hello,world', array('hello', 'world'), array('hello', 'world')) + , array('', array('hello', 'world'), array('wamp')) + , array('', array(), null) + , array('wamp', array('hello', 'wamp', 'world'), array('herp', 'derp', 'wamp')) + ); + } + + /** + * @dataProvider protocolProvider + */ + public function testGetSubProtocolString($expected, $supported, $requested) { + $this->comp->protocols = $supported; + $req = (null === $requested ? $requested : new \ArrayIterator($requested)); + + $class = new \ReflectionClass('Ratchet\\WebSocket\\WsServer'); + $method = $class->getMethod('getSubProtocolString'); + $method->setAccessible(true); + + $this->assertEquals($expected, $method->invokeArgs($this->serv, array($req))); + } +} \ No newline at end of file