Merge branch 'refs/heads/header' into wamp

This commit is contained in:
Chris Boden 2012-01-08 21:59:17 -05:00
commit 5334f94b91
14 changed files with 171 additions and 130 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "vendor/guzzle"]
path = vendor/guzzle
url = git://github.com/guzzle/guzzle.git

View File

@ -6,7 +6,8 @@ use Ratchet\Resource\Connection;
use Ratchet\Resource\Command\Factory; use Ratchet\Resource\Command\Factory;
use Ratchet\Resource\Command\CommandInterface; use Ratchet\Resource\Command\CommandInterface;
use Ratchet\Resource\Command\Action\SendMessage; use Ratchet\Resource\Command\Action\SendMessage;
use Ratchet\Application\WebSocket\Util\HTTP; use Guzzle\Http\Message\RequestInterface;
use Ratchet\Application\WebSocket\Guzzle\Http\Message\RequestFactory;
/** /**
* The adapter to handle WebSocket requests/responses * The adapter to handle WebSocket requests/responses
@ -77,12 +78,14 @@ class App implements ApplicationInterface, ConfiguratorInterface {
public function onMessage(Connection $from, $msg) { public function onMessage(Connection $from, $msg) {
if (true !== $from->WebSocket->handshake) { if (true !== $from->WebSocket->handshake) {
if (!isset($from->WebSocket->version)) { if (!isset($from->WebSocket->version)) {
try { $from->WebSocket->headers .= $msg;
$from->WebSocket->headers .= $msg; if (!$this->isMessageComplete($from->WebSocket->headers)) {
$from->WebSocket->version = $this->getVersion($from->WebSocket->headers);
} catch (\UnderflowException $e) {
return; return;
} }
$headers = RequestFactory::fromRequest($from->WebSocket->headers);
$from->WebSocket->version = $this->getVersion($headers);
$from->WebSocket->headers = $headers;
} }
$response = $from->WebSocket->version->handshake($from->WebSocket->headers); $response = $from->WebSocket->version->handshake($from->WebSocket->headers);
@ -218,21 +221,15 @@ class App implements ApplicationInterface, ConfiguratorInterface {
* @throws InvalidArgumentException If we can't understand protocol version request * @throws InvalidArgumentException If we can't understand protocol version request
* @todo Verify the first line of the HTTP header as per page 16 of RFC 6455 * @todo Verify the first line of the HTTP header as per page 16 of RFC 6455
*/ */
protected function getVersion($message) { protected function getVersion(RequestInterface $request) {
if (false === strstr($message, "\r\n\r\n")) { // This CAN fail with Hixie, depending on the TCP buffer in between
throw new \UnderflowException;
}
$headers = HTTP::getHeaders($message);
foreach ($this->_versions as $name => $instance) { foreach ($this->_versions as $name => $instance) {
if (null !== $instance) { if (null !== $instance) {
if ($instance::isProtocol($headers)) { if ($instance::isProtocol($request)) {
return $instance; return $instance;
} }
} else { } else {
$ns = __NAMESPACE__ . "\\Version\\{$name}"; $ns = __NAMESPACE__ . "\\Version\\{$name}";
if ($ns::isProtocol($headers)) { if ($ns::isProtocol($request)) {
$this->_versions[$name] = new $ns; $this->_versions[$name] = new $ns;
return $this->_versions[$name]; return $this->_versions[$name];
} }
@ -242,6 +239,29 @@ class App implements ApplicationInterface, ConfiguratorInterface {
throw new \InvalidArgumentException('Could not identify WebSocket protocol'); throw new \InvalidArgumentException('Could not identify WebSocket protocol');
} }
/**
* @param string
* @return bool
* @todo Abstract, some hard coding done for (stupid) Hixie protocol
*/
protected function isMessageComplete($message) {
static $crlf = "\r\n\r\n";
$headers = (boolean)strstr($message, $crlf);
if (!$headers) {
return false;
}
if (strstr($message, 'Sec-WebSocket-Key2')) {
if (8 !== strlen(substr($message, strpos($message, $crlf) + strlen($crlf)))) {
return false;
}
}
return true;
}
/** /**
* Disable a version of the WebSocket protocol *cough*Hixie76*cough* * Disable a version of the WebSocket protocol *cough*Hixie76*cough*
* @param string The name of the version to disable * @param string The name of the version to disable

View File

@ -0,0 +1,43 @@
<?php
namespace Ratchet\Application\WebSocket\Guzzle\Http\Message;
use Guzzle\Http\Message\RequestFactory as gReqFac;
use Guzzle\Http\Url;
/**
* Just slighly changing the Guzzle fromMessage() method to always return an EntityEnclosingRequest instance instead of Request
*/
class RequestFactory extends gReqFac {
/**
* @param string
* @return Guzzle\Http\Message\RequestInterface
*/
public static function fromRequest($message) {
$parsed = static::parseMessage($message);
if (!$parsed) {
return false;
}
return self::fromRequestParts(
$parsed['method'],
$parsed['parts'],
$parsed['headers'],
$parsed['body'],
$parsed['protocol'],
$parsed['protocol_version']
);
}
protected static function fromRequestParts($method, array $parts, $headers = null, $body = null, $protocol = 'HTTP', $protocolVersion = '1.1') {
return self::requestCreate($method, Url::buildUrl($parts, true), $headers, $body)
->setProtocolVersion($protocolVersion);
}
protected static function requestCreate($method, $url, $headers = null, $body = null) {
$c = static::$entityEnclosingRequestClass;
$request = new $c($method, $url, $headers);
$request->setBody($body);
return $request;
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace Ratchet\Application\WebSocket\Util;
/**
* A helper class for handling HTTP requests
* @todo Needs re-write...http_parse_headers is a PECL extension that changes the case to unexpected values
* @todo Again, RE-WRITE - I want all the expected headers to at least be set in the returned, even if not there, set as null - having to do too much work in HandshaekVerifier
*/
class HTTP {
/**
* @todo Probably should iterate through the array, strtolower all the things, then return it
* @param string
* @return array
*/
public static function getHeaders($http_message) {
$header_array = function_exists('http_parse_headers') ? http_parse_headers($http_message) : self::http_parse_headers($http_message);
return $header_array + array(
'Host' => null
, 'Upgrade' => null
, 'Connection' => null
, 'Sec-Websocket-Key' => null
, 'Origin' => null
, 'Sec-Websocket-Protocol' => null
, 'Sec-Websocket-Version' => null
, 'Sec-Websocket-Origin' => null
);
}
/**
* @param string
* @return array
* This is a fallback method for http_parse_headers as not all php installs have the HTTP module present
* @internal
*/
protected static function http_parse_headers($http_message) {
$retVal = array();
$fields = explode("br", preg_replace("%(<|/\>|>)%", "", nl2br($http_message)));
foreach ($fields as $field) {
if (preg_match('%^(GET|POST|PUT|DELETE|PATCH)(\s)(.*)%', $field, $matchReq)) {
$retVal["Request Method"] = $matchReq[1];
$retVal["Request Url"] = $matchReq[3];
} elseif (preg_match('/([^:]+): (.+)/m', $field, $match) ) {
$match[1] = preg_replace('/(?<=^|[\x09\x20\x2D])./e', 'strtoupper("\0")', strtolower(trim($match[1])));
if (isset($retVal[$match[1]])) {
$retVal[$match[1]] = array($retVal[$match[1]], $match[2]);
} else {
$retVal[$match[1]] = trim($match[2]);
}
}
}
return $retVal;
}
}

View File

@ -1,5 +1,6 @@
<?php <?php
namespace Ratchet\Application\WebSocket\Version; namespace Ratchet\Application\WebSocket\Version;
use Guzzle\Http\Message\RequestInterface;
/** /**
* FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application! * FOR THE LOVE OF BEER, PLEASE PLEASE PLEASE DON'T allow the use of this in your application!
@ -14,16 +15,17 @@ namespace Ratchet\Application\WebSocket\Version;
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76 * @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
*/ */
class Hixie76 implements VersionInterface { class Hixie76 implements VersionInterface {
public static function isProtocol(array $headers) { public static function isProtocol(RequestInterface $request) {
return isset($headers['Sec-Websocket-Key2']); return !(null === $request->getHeader('Sec-WebSocket-Key2'));
} }
/** /**
* @param string * @param string
* @return string * @return string
* @todo Unhack this mess...or wait for Hixie to die (HURRY UP APPLE)
*/ */
public function handshake($message) { public function handshake(RequestInterface $request) {
$buffer = $message; $buffer = $request->getRawHeaders() . "\r\n\r\n" . $request->getBody();
$resource = $host = $origin = $key1 = $key2 = $protocol = $code = $handshake = null; $resource = $host = $origin = $key1 = $key2 = $protocol = $code = $handshake = null;
preg_match('#GET (.*?) HTTP#', $buffer, $match) && $resource = $match[1]; preg_match('#GET (.*?) HTTP#', $buffer, $match) && $resource = $match[1];

View File

@ -1,18 +1,14 @@
<?php <?php
namespace Ratchet\Application\WebSocket\Version; namespace Ratchet\Application\WebSocket\Version;
use Guzzle\Http\Message\RequestInterface;
/** /**
* @todo Note: Even though this is the "legacy" HyBi version, it's using the RFC Message and Frame classes - change if needed * @todo Note: Even though this is the "legacy" HyBi version, it's using the RFC Message and Frame classes - change if needed
*/ */
class HyBi10 extends RFC6455 { class HyBi10 extends RFC6455 {
public static function isProtocol(array $headers) { public static function isProtocol(RequestInterface $request) {
if (isset($headers['Sec-Websocket-Version'])) { $version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
if ((int)$headers['Sec-Websocket-Version'] >= 6 && (int)$headers['Sec-Websocket-Version'] < 13) { return ($version >= 6 && $version < 13);
return true;
}
}
return false;
} }
/** /**

View File

@ -1,7 +1,7 @@
<?php <?php
namespace Ratchet\Application\WebSocket\Version; namespace Ratchet\Application\WebSocket\Version;
use Ratchet\Application\WebSocket\Version\RFC6455\HandshakeVerifier; use Ratchet\Application\WebSocket\Version\RFC6455\HandshakeVerifier;
use Ratchet\Application\WebSocket\Util\HTTP; use Guzzle\Http\Message\RequestInterface;
/** /**
* @link http://www.rfc-editor.org/authors/rfc6455.txt * @link http://www.rfc-editor.org/authors/rfc6455.txt
@ -18,14 +18,12 @@ class RFC6455 implements VersionInterface {
$this->_verifier = new HandshakeVerifier; $this->_verifier = new HandshakeVerifier;
} }
public static function isProtocol(array $headers) { /**
if (isset($headers['Sec-Websocket-Version'])) { * @todo Change the request to be a Guzzle RequestInterface
if ((int)$headers['Sec-Websocket-Version'] == 13) { */
return true; public static function isProtocol(RequestInterface $request) {
} $version = (int)$request->getHeader('Sec-WebSocket-Version', -1);
} return (13 === $version);
return false;
} }
/** /**
@ -33,11 +31,8 @@ class RFC6455 implements VersionInterface {
* I kept this as an array and combined in App for future considerations...easier to add a subprotol as a key value than edit a string * I kept this as an array and combined in App for future considerations...easier to add a subprotol as a key value than edit a string
* @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where? * @todo Decide what to do on failure...currently throwing an exception and I think socket connection is closed. Should be sending 40x error - but from where?
*/ */
public function handshake($message) { public function handshake(RequestInterface $request) {
$headers = HTTP::getHeaders($message); if (true !== $this->_verifier->verifyAll($request)) {
$key = $this->sign($headers['Sec-Websocket-Key']);
if (true !== $this->_verifier->verifyAll($headers)) {
throw new \InvalidArgumentException('Invalid HTTP header'); throw new \InvalidArgumentException('Invalid HTTP header');
} }
@ -45,7 +40,7 @@ class RFC6455 implements VersionInterface {
'' => 'HTTP/1.1 101 Switching Protocols' '' => 'HTTP/1.1 101 Switching Protocols'
, 'Upgrade' => 'websocket' , 'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade' , 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign($headers['Sec-Websocket-Key']) , 'Sec-WebSocket-Accept' => $this->sign($request->getHeader('Sec-WebSocket-Key'))
// , 'Sec-WebSocket-Protocol' => '' // , 'Sec-WebSocket-Protocol' => ''
); );
} }

View File

@ -1,5 +1,6 @@
<?php <?php
namespace Ratchet\Application\WebSocket\Version\RFC6455; namespace Ratchet\Application\WebSocket\Version\RFC6455;
use Guzzle\Http\Message\RequestInterface;
/** /**
* These are checks to ensure the client requested handshake are valid * These are checks to ensure the client requested handshake are valid
@ -9,22 +10,24 @@ namespace Ratchet\Application\WebSocket\Version\RFC6455;
class HandshakeVerifier { class HandshakeVerifier {
/** /**
* Given an array of the headers this method will run through all verification methods * Given an array of the headers this method will run through all verification methods
* @param array * @param Guzzle\Http\Message\RequestInterface
* @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid * @return bool TRUE if all headers are valid, FALSE if 1 or more were invalid
*/ */
public function verifyAll(array $headers) { public function verifyAll(RequestInterface $request) {
$headers = $request->getHeaders();
$passes = 0; $passes = 0;
$passes += (int)$this->verifyMethod($headers['Request Method']); $passes += (int)$this->verifyMethod($request->getMethod());
//$passes += (int)$this->verifyHTTPVersion($headers['???']); // This isn't in the array! $passes += (int)$this->verifyHTTPVersion($request->getProtocolVersion());
$passes += (int)$this->verifyRequestURI($headers['Request Url']); $passes += (int)$this->verifyRequestURI($request->getPath());
$passes += (int)$this->verifyHost($headers['Host']); $passes += (int)$this->verifyHost($headers['Host']);
$passes += (int)$this->verifyUpgradeRequest($headers['Upgrade']); $passes += (int)$this->verifyUpgradeRequest($headers['Upgrade']);
$passes += (int)$this->verifyConnection($headers['Connection']); $passes += (int)$this->verifyConnection($headers['Connection']);
$passes += (int)$this->verifyKey($headers['Sec-Websocket-Key']); $passes += (int)$this->verifyKey($headers['Sec-WebSocket-Key']);
//$passes += (int)$this->verifyVersion($headers['Sec-Websocket-Version']); // Temporarily breaking functionality //$passes += (int)$this->verifyVersion($headers['Sec-WebSocket-Version']); // Temporarily breaking functionality
return (6 === $passes); return (7 === $passes);
} }
/** /**

View File

@ -1,5 +1,6 @@
<?php <?php
namespace Ratchet\Application\WebSocket\Version; namespace Ratchet\Application\WebSocket\Version;
use Guzzle\Http\Message\RequestInterface;
/** /**
* Despite the version iterations of WebInterface the actions they go through are similar * Despite the version iterations of WebInterface the actions they go through are similar
@ -10,20 +11,21 @@ namespace Ratchet\Application\WebSocket\Version;
interface VersionInterface { interface VersionInterface {
/** /**
* Given an HTTP header, determine if this version should handle the protocol * Given an HTTP header, determine if this version should handle the protocol
* @param array * @param Guzzle\Http\Message\RequestInterface
* @return bool * @return bool
* @throws UnderflowException If the protocol thinks the headers are still fragmented * @throws UnderflowException If the protocol thinks the headers are still fragmented
*/ */
static function isProtocol(array $headers); static function isProtocol(RequestInterface $request);
/** /**
* Perform the handshake and return the response headers * Perform the handshake and return the response headers
* @param string * @param Guzzle\Http\Message\RequestInterface
* @return array|string * @return array|string
* @throws InvalidArgumentException If the HTTP handshake is mal-formed * @throws InvalidArgumentException If the HTTP handshake is mal-formed
* @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version) * @throws UnderflowException If the message hasn't finished buffering (not yet implemented, theoretically will only happen with Hixie version)
* @todo Change param to accept a Guzzle RequestInterface object
*/ */
function handshake($message); function handshake(RequestInterface $request);
/** /**
* @return MessageInterface * @return MessageInterface

View File

@ -20,6 +20,17 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
} }
protected static function convert($in) { protected static function convert($in) {
if (strlen($in) > 8) {
$out = '';
while (strlen($in) > 8) {
$out .= static::convert(substr($in, 0, 8));
$in = substr($in, 8);
}
return $out;
}
return pack('C', bindec($in)); return pack('C', bindec($in));
} }
@ -37,6 +48,31 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
); );
} }
public static function underflowProvider() {
return array(
array('isFinal', '')
, array('getOpcode', '')
, array('isMasked', '10000001')
, array('getPayloadLength', '10000001')
, array('getPayloadLength', '1000000111111110')
, array('getMaskingKey', '1000000110000111')
, array('getPayload', '100000011000000100011100101010101001100111110100')
);
}
/**
* @dataProvider underflowProvider
*/
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering($method, $bin) {
$this->setExpectedException('\UnderflowException');
if (!empty($bin)) {
$this->_frame->addBuffer(static::convert($bin));
}
call_user_func(array($this->_frame, $method));
}
/** /**
* A data provider for testing the first byte of a WebSocket frame * A data provider for testing the first byte of a WebSocket frame
* @param bool Given, is the byte indicate this is the final frame * @param bool Given, is the byte indicate this is the final frame
@ -53,12 +89,6 @@ class FrameTest extends \PHPUnit_Framework_TestCase {
); );
} }
public function testUnderflowExceptionFromAllTheMethodsMimickingBuffering() {
return $this->markTestIncomplete();
$this->expectException('\UnderflowException');
}
/** /**
* @dataProvider firstByteProvider * @dataProvider firstByteProvider
*/ */

View File

@ -2,6 +2,7 @@
namespace Ratchet\Tests\Application\WebSocket\Version; namespace Ratchet\Tests\Application\WebSocket\Version;
use Ratchet\Application\WebSocket\Version\RFC6455; use Ratchet\Application\WebSocket\Version\RFC6455;
use Ratchet\Application\WebSocket\Version\RFC6455\Frame; use Ratchet\Application\WebSocket\Version\RFC6455\Frame;
use Guzzle\Http\Message\RequestFactory;
/** /**
* @covers Ratchet\Application\WebSocket\Version\RFC6455 * @covers Ratchet\Application\WebSocket\Version\RFC6455
@ -103,32 +104,25 @@ class RFC6455Test extends \PHPUnit_Framework_TestCase {
public static function headerHandshakeProvider() { public static function headerHandshakeProvider() {
return array( return array(
array(false, "GET /test HTTP/1.0\r\n" . static::getAndSpliceHeader()) array(false, "GET /test HTTP/1.0\r\n" . static::getAndSpliceHeader())
, array(true, static::$good_rest . "\r\n" . static::getAndSpliceHeader())
, array(false, "POST / HTTP:/1.1\r\n" . static::getAndSpliceHeader())
, array(false, static::$good_rest . "\r\n" . static::getAndSpliceHeader('Upgrade', 'useless'))
, array(false, "GET /ಠ_ಠ HTTP/1.1\r\n" . static::getAndSpliceHeader())
, array(true, static::$good_rest . "\r\n" . static::getAndSpliceHeader('Connection', 'Herp, Upgrade, Derp'))
); );
} }
/* RFC example of a good header
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
*/
/** /**
* @dataProvider headerHandshakeProvider * @dataProvider headerHandshakeProvider
* @todo Can't finish this test until I rewrite headers
*/ */
public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) { public function testVariousHeadersToCheckHandshakeTolerance($pass, $header) {
return $this->markTestIncomplete(); $request = RequestFactory::fromMessage($header);
if ($pass) { if ($pass) {
$this->assertTrue(is_array($this->_version->handshake($header))); $this->assertTrue(is_array($this->_version->handshake($request)));
} else { } else {
$this->setExpectedException('InvalidArgumentException'); $this->setExpectedException('InvalidArgumentException');
$this->_version->handshake($header); $this->_version->handshake($request);
} }
} }
} }

View File

@ -5,4 +5,7 @@
$app->register(); $app->register();
$app = new SplClassLoader('Ratchet', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'lib'); $app = new SplClassLoader('Ratchet', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'lib');
$app->register();
$app = new SplClassLoader('Guzzle', dirname(__DIR__) . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'guzzle' . DIRECTORY_SEPARATOR . 'src');
$app->register(); $app->register();

5
vendor/README.md vendored Normal file
View File

@ -0,0 +1,5 @@
## External Libraries
### Guzzle
Used to parse the incoming HTTP handshake request. A Guzzle Request object is then passed around the application for a consistent API.

1
vendor/guzzle vendored Submodule

@ -0,0 +1 @@
Subproject commit 13ea2e723eaba72d99eefb24ec36f95ff7ef0068