Merge branch '0.4-merge-0.3.4' into 0.4

This commit is contained in:
Chris Boden 2016-02-17 18:54:06 -05:00
commit e463de0c6b
23 changed files with 240 additions and 48 deletions

View File

@ -4,6 +4,8 @@ php:
- 5.3
- 5.4
- 5.5
- 5.6
- 7
- hhvm
before_script:

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

@ -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"
}
}

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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:

View File

@ -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;
}
/**

View File

@ -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);
}
}

View File

@ -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');
}

View File

@ -102,13 +102,13 @@ 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);
}
try {
$response = $conn->WebSocket->version->handshake($conn->WebSocket->request);

View File

@ -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();
}

View File

@ -1,5 +1,4 @@
<?php
$loader = require __DIR__ . '/../vendor/autoload.php';
$loader->add('Ratchet', __DIR__ . '/helpers');
$loader->register();
$loader->addPsr4('Ratchet\\', __DIR__ . '/helpers/Ratchet');

View File

@ -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());
}
}

View File

@ -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()));

View File

@ -0,0 +1,53 @@
<?php
namespace Ratchet\Session\Storage;
use Ratchet\Session\Serialize\PhpHandler;
use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBag;
use Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler;
class VirtualSessionStoragePDOTest extends \PHPUnit_Framework_TestCase {
/**
* @var VirtualSessionStorage
*/
protected $_virtualSessionStorage;
protected $_pathToDB;
public function setUp() {
if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
return $this->markTestSkipped('Session test requires PDO and pdo_sqlite');
}
$schema = <<<SQL
CREATE TABLE `sessions` (
`sess_id` VARBINARY(128) NOT NULL PRIMARY KEY,
`sess_data` BLOB NOT NULL,
`sess_time` INTEGER UNSIGNED NOT NULL,
`sess_lifetime` MEDIUMINT NOT NULL
);
SQL;
$this->_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());
}
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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());
}
}