[WebSockets] Handshake encoding + case insensitivity

Updated RFC6455 handshaker to check values case insensitively
Made sure RFC6455 handshaker matches encoding properly
Added mbstring as a requirement for Ratchet
Refs #28, #30
This commit is contained in:
Chris Boden 2012-05-19 23:36:32 -04:00
parent 27716fef78
commit d075b99c26
7 changed files with 52 additions and 50 deletions

View File

@ -8,13 +8,14 @@ Build up your application through simple interfaces and re-use your application
##WebSocket Compliance ##WebSocket Compliance
* Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time) * Supports the RFC6455, HyBi-10+, and Hixie76 protocol versions (at the same time)
* Tested on Chrome 18 - 16, Firefox 6 - 12, Safari 5, iOS 4.2, iOS 5 * Tested on Chrome 13 - 19, Firefox 6 - 12, Safari 5.0.1+, iOS 4.2, iOS 5
##Requirements ##Requirements
Shell access is required and a dedicated machine with root access is recommended. Shell access is required and a dedicated machine with root access is recommended.
To avoid proxy/firewall blockage it's recommended WebSockets are run on port 80, which requires root access. To avoid proxy/firewall blockage it's recommended WebSockets are run on port 80, which requires root access.
Note that you can not run two applications (Apache and Ratchet) on the same port, thus the requirement for a separate machine (for now). Note that you can not run two applications (Apache and Ratchet) on the same port, thus the requirement for a separate machine (for now).
PHP 5.3.2 (or higher) is required with mbstring enabled (*--enable-mbstring* flag during compile time). PHP5.4 is recommended for its performance improvements.
Cookies from your domain will be passed to the socket server, allowing you to identify users. Cookies from your domain will be passed to the socket server, allowing you to identify users.
Accessing your website's session data in Ratchet requires you to use [Symfony2 Sessions](http://symfony.com/doc/master/components/http_foundation/sessions.html) on your website. Accessing your website's session data in Ratchet requires you to use [Symfony2 Sessions](http://symfony.com/doc/master/components/http_foundation/sessions.html) on your website.

View File

@ -21,6 +21,7 @@
} }
, "require": { , "require": {
"php": ">=5.3.2" "php": ">=5.3.2"
, "ext-mbstring": "*"
, "guzzle/guzzle": "2.5.*" , "guzzle/guzzle": "2.5.*"
, "symfony/http-foundation": "2.1.*" , "symfony/http-foundation": "2.1.*"
, "react/socket": "dev-master" , "react/socket": "dev-master"

16
composer.lock generated
View File

@ -1,5 +1,5 @@
{ {
"hash": "cbea4e3e4d74a22ba34d4edf2ce44df3", "hash": "253370657f067dacf104d5fae531f20a",
"packages": [ "packages": [
{ {
"package": "evenement/evenement", "package": "evenement/evenement",
@ -32,13 +32,6 @@
"version": "dev-master", "version": "dev-master",
"source-reference": "eb82542e8ec9506096caf7c528564c740a214f56" "source-reference": "eb82542e8ec9506096caf7c528564c740a214f56"
}, },
{
"package": "symfony/event-dispatcher",
"version": "dev-master",
"source-reference": "0b58a4019befc0bd038bc0ec0165101d5dd31754",
"alias-pretty-version": "2.1.x-dev",
"alias-version": "2.1.9999999.9999999-dev"
},
{ {
"package": "symfony/http-foundation", "package": "symfony/http-foundation",
"version": "dev-master", "version": "dev-master",
@ -50,13 +43,6 @@
"package": "symfony/http-foundation", "package": "symfony/http-foundation",
"version": "dev-master", "version": "dev-master",
"source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0" "source-reference": "3d9f4ce435f6322b9720c209ad610202526373c0"
},
{
"package": "symfony/http-foundation",
"version": "dev-master",
"source-reference": "cf8e8324c68ce584525502702866485f17f1c8a5",
"alias-pretty-version": "2.1.x-dev",
"alias-version": "2.1.9999999.9999999-dev"
} }
], ],
"packages-dev": null, "packages-dev": null,

View File

@ -2,12 +2,6 @@
namespace Ratchet\WebSocket\Version; namespace Ratchet\WebSocket\Version;
interface FrameInterface { interface FrameInterface {
/**
* Dunno if I'll use this
* Thinking could be used if a control frame?
*/
// function __invoke();
/** /**
* @return bool * @return bool
*/ */

View File

@ -32,10 +32,9 @@ class HandshakeVerifier {
* Test the HTTP method. MUST be "GET" * Test the HTTP method. MUST be "GET"
* @param string * @param string
* @return bool * @return bool
* @todo Look into STD if "get" is valid (am I supposed to do case conversion?)
*/ */
public function verifyMethod($val) { public function verifyMethod($val) {
return ('GET' === $val); return ('get' === mb_strtolower($val, 'ASCII'));
} }
/** /**
@ -50,7 +49,6 @@ class HandshakeVerifier {
/** /**
* @param string * @param string
* @return bool * @return bool
* @todo Verify the logic here is correct
*/ */
public function verifyRequestURI($val) { public function verifyRequestURI($val) {
if ($val[0] != '/') { if ($val[0] != '/') {
@ -80,7 +78,7 @@ class HandshakeVerifier {
* @return bool * @return bool
*/ */
public function verifyUpgradeRequest($val) { public function verifyUpgradeRequest($val) {
return ('websocket' === $val); return ('websocket' === mb_strtolower($val, 'ASCII'));
} }
/** /**
@ -89,12 +87,16 @@ class HandshakeVerifier {
* @return bool * @return bool
*/ */
public function verifyConnection($val) { public function verifyConnection($val) {
if ('Upgrade' === $val) { $val = mb_strtolower($val, 'ASCII');
if ('upgrade' === $val) {
return true; return true;
} }
// todo change this to mb_eregi_replace
$vals = explode(',', str_replace(', ', ',', $val)); $vals = explode(',', str_replace(', ', ',', $val));
return (false !== array_search('Upgrade', $vals));
return (false !== array_search('upgrade', $vals));
} }
/** /**

View File

@ -18,7 +18,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
public static function methodProvider() { public static function methodProvider() {
return array( return array(
array(true, 'GET') array(true, 'GET')
, array(false, 'get') // I'm not sure if this is valid or not, need to check standard , array(true, 'get')
, array(true, 'Get')
, array(false, 'POST') , array(false, 'POST')
, array(false, 'DELETE') , array(false, 'DELETE')
, array(false, 'PUT') , array(false, 'PUT')
@ -64,6 +65,7 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
, array(false, '/chat#bad') , array(false, '/chat#bad')
, array(false, 'nope') , array(false, 'nope')
, array(false, '/ ಠ_ಠ ') , array(false, '/ ಠ_ಠ ')
, array(false, '/✖')
); );
} }
@ -91,7 +93,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
public static function upgradeProvider() { public static function upgradeProvider() {
return array( return array(
array(true, 'websocket') array(true, 'websocket')
, array(false, 'Websocket') , array(true, 'Websocket')
, array(true, 'webSocket')
, array(false, null) , array(false, null)
, array(false, '') , array(false, '')
); );
@ -107,7 +110,7 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
public static function connectionProvider() { public static function connectionProvider() {
return array( return array(
array(true, 'Upgrade') array(true, 'Upgrade')
, array(false, 'upgrade') , array(true, 'upgrade')
, array(true, 'keep-alive, Upgrade') , array(true, 'keep-alive, Upgrade')
, array(true, 'Upgrade, keep-alive') , array(true, 'Upgrade, keep-alive')
, array(true, 'keep-alive, Upgrade, something') , array(true, 'keep-alive, Upgrade, something')
@ -133,6 +136,8 @@ class HandshakeVerifierTest extends \PHPUnit_Framework_TestCase {
, array(false, 'Hello World') , array(false, 'Hello World')
, array(false, '1234567890123456') , array(false, '1234567890123456')
, array(false, '123456789012345678901234') , array(false, '123456789012345678901234')
, array(true, base64_encode('UTF8allthngs+✓'))
, array(true, 'dGhlIHNhbXBsZSBub25jZQ==')
); );
} }

View File

@ -3,33 +3,26 @@ namespace Ratchet\Tests\WebSocket\Version;
use Ratchet\WebSocket\Version\RFC6455; use Ratchet\WebSocket\Version\RFC6455;
use Ratchet\WebSocket\Version\RFC6455\Frame; use Ratchet\WebSocket\Version\RFC6455\Frame;
use Guzzle\Http\Message\RequestFactory; use Guzzle\Http\Message\RequestFactory;
use Guzzle\Http\Message\EntityEnclosingRequest;
/** /**
* @covers Ratchet\WebSocket\Version\RFC6455 * @covers Ratchet\WebSocket\Version\RFC6455
*/ */
class RFC6455Test extends \PHPUnit_Framework_TestCase { class RFC6455Test extends \PHPUnit_Framework_TestCase {
protected $_version; protected $version;
public function setUp() { public function setUp() {
$this->_version = new RFC6455(); $this->version = new RFC6455;
} }
/** /**
* Is this useful? * @dataProvider handshakeProvider
*/
public function testClassImplementsVersionInterface() {
$constraint = $this->isInstanceOf('\\Ratchet\\WebSocket\\Version\\VersionInterface');
$this->assertThat($this->_version, $constraint);
}
/**
* @dataProvider HandshakeProvider
*/ */
public function testKeySigningForHandshake($key, $accept) { public function testKeySigningForHandshake($key, $accept) {
$this->assertEquals($accept, $this->_version->sign($key)); $this->assertEquals($accept, $this->version->sign($key));
} }
public static function HandshakeProvider() { public static function handshakeProvider() {
return array( return array(
array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=') array('x3JJHMbDL1EzLkh9GBhXDw==', 'HSmrc0sMlYUkAGmm5OPpG2HaGWk=')
, array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=') , array('dGhlIHNhbXBsZSBub25jZQ==', 's3pPLMBiTxaQ9kYGzzhZRbK+xOo=')
@ -57,7 +50,7 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
public function testUnframeMatchesPreFraming() { public function testUnframeMatchesPreFraming() {
$string = 'Hello World!'; $string = 'Hello World!';
$framed = $this->_version->frame($string); $framed = $this->version->frame($string);
$frame = new Frame; $frame = new Frame;
$frame->addBuffer($framed); $frame->addBuffer($framed);
@ -77,6 +70,26 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
, 'Sec-WebSocket-Version' => 13 , 'Sec-WebSocket-Version' => 13
); );
public function caseVariantProvider() {
return array(
array('Sec-Websocket-Version')
, array('sec-websocket-version')
, array('SEC-WEBSOCKET-VERSION')
, array('sEC-wEBsOCKET-vERSION')
);
}
/**
* @dataProvider caseVariantProvider
*/
public function testIsProtocolWithCaseInsensitivity($headerName) {
$header = static::$good_header;
unset($header['Sec-WebSocket-Version']);
$header[$headerName] = 13;
$this->assertTrue($this->version->isProtocol(new EntityEnclosingRequest('get', '/', $header)));
}
/** /**
* A helper function to try and quickly put together a valid WebSocket HTTP handshake * A helper function to try and quickly put together a valid WebSocket HTTP handshake
* but optionally replace a piece to an invalid value for failure testing * but optionally replace a piece to an invalid value for failure testing
@ -119,18 +132,18 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
$request = RequestFactory::getInstance()->fromMessage($header); $request = RequestFactory::getInstance()->fromMessage($header);
if ($pass) { if ($pass) {
$this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $this->_version->handshake($request)); $this->assertInstanceOf('\\Guzzle\\Http\\Message\\Response', $this->version->handshake($request));
} else { } else {
$this->setExpectedException('InvalidArgumentException'); $this->setExpectedException('InvalidArgumentException');
$this->_version->handshake($request); $this->version->handshake($request);
} }
} }
public function testNewMessage() { public function testNewMessage() {
$this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->_version->newMessage()); $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Message', $this->version->newMessage());
} }
public function testNewFrame() { public function testNewFrame() {
$this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->_version->newFrame()); $this->assertInstanceOf('\\Ratchet\\WebSocket\\Version\\RFC6455\\Frame', $this->version->newFrame());
} }
} }