Implementing Guzzle to parse incoming handshake request
Functional on RFC version
This commit is contained in:
Chris Boden 2012-01-06 16:18:12 -05:00
parent 8d1b2548e7
commit 08fa8a948f
8 changed files with 51 additions and 90 deletions

View File

@ -6,7 +6,8 @@ use Ratchet\Resource\Connection;
use Ratchet\Resource\Command\Factory;
use Ratchet\Resource\Command\CommandInterface;
use Ratchet\Resource\Command\Action\SendMessage;
use Ratchet\Application\WebSocket\Util\HTTP;
use Guzzle\Http\Message\RequestInterface;
use Guzzle\Http\Message\RequestFactory;
/**
* The adapter to handle WebSocket requests/responses
@ -80,12 +81,14 @@ class App implements ApplicationInterface, ConfiguratorInterface {
public function onMessage(Connection $from, $msg) {
if (true !== $from->WebSocket->handshake) {
if (!isset($from->WebSocket->version)) {
try {
$from->WebSocket->headers .= $msg;
$from->WebSocket->version = $this->getVersion($from->WebSocket->headers);
} catch (\UnderflowException $e) {
if (!$this->isMessageComplete($from->WebSocket->headers)) {
return;
}
$headers = RequestFactory::fromMessage($from->WebSocket->headers);
$from->WebSocket->version = $this->getVersion($headers);
$from->WebSocket->headers = $headers;
}
$response = $from->WebSocket->version->handshake($from->WebSocket->headers);
@ -212,12 +215,8 @@ class App implements ApplicationInterface, ConfiguratorInterface {
* @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
*/
protected function getVersion($message) {
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);
protected function getVersion(RequestInterface $request) {
$headers = $request->getHeaders();
foreach ($this->_versions as $name => $instance) {
if (null !== $instance) {
@ -236,6 +235,15 @@ class App implements ApplicationInterface, ConfiguratorInterface {
throw new \InvalidArgumentException('Could not identify WebSocket protocol');
}
/**
* @param string
* @return bool
* @todo Method is flawed, this CAN result in an error with the Hixie protocol
*/
protected function isMessageComplete($message) {
return (boolean)strstr($message, "\r\n\r\n");
}
/**
* Disable a version of the WebSocket protocol *cough*Hixie76*cough*
* @param string The name of the version to disable

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

@ -14,8 +14,8 @@ namespace Ratchet\Application\WebSocket\Version;
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
*/
class Hixie76 implements VersionInterface {
public static function isProtocol(array $headers) {
return isset($headers['Sec-Websocket-Key2']);
public static function isProtocol($headers) {
return isset($headers['Sec-WebSocket-Key2']);
}
/**

View File

@ -5,9 +5,9 @@ namespace Ratchet\Application\WebSocket\Version;
* @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 {
public static function isProtocol(array $headers) {
if (isset($headers['Sec-Websocket-Version'])) {
if ((int)$headers['Sec-Websocket-Version'] >= 6 && (int)$headers['Sec-Websocket-Version'] < 13) {
public static function isProtocol($headers) {
if (isset($headers['Sec-WebSocket-Version'])) {
if ((int)$headers['Sec-WebSocket-Version'] >= 6 && (int)$headers['Sec-WebSocket-Version'] < 13) {
return true;
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace Ratchet\Application\WebSocket\Version;
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
@ -18,9 +18,12 @@ class RFC6455 implements VersionInterface {
$this->_verifier = new HandshakeVerifier;
}
public static function isProtocol(array $headers) {
if (isset($headers['Sec-Websocket-Version'])) {
if ((int)$headers['Sec-Websocket-Version'] == 13) {
/**
* @todo Change the request to be a Guzzle RequestInterface
*/
public static function isProtocol($headers) {
if (isset($headers['Sec-WebSocket-Version'])) {
if ((int)$headers['Sec-WebSocket-Version'] == 13) {
return true;
}
}
@ -34,10 +37,10 @@ class RFC6455 implements VersionInterface {
* @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) {
$headers = HTTP::getHeaders($message);
$key = $this->sign($headers['Sec-Websocket-Key']);
$headers = $message->getHeaders();
$key = $this->sign($headers['Sec-WebSocket-Key']);
if (true !== $this->_verifier->verifyAll($headers)) {
if (true !== $this->_verifier->verifyAll($message)) {
throw new \InvalidArgumentException('Invalid HTTP header');
}
@ -45,7 +48,7 @@ class RFC6455 implements VersionInterface {
'' => 'HTTP/1.1 101 Switching Protocols'
, 'Upgrade' => 'websocket'
, 'Connection' => 'Upgrade'
, 'Sec-WebSocket-Accept' => $this->sign($headers['Sec-Websocket-Key'])
, 'Sec-WebSocket-Accept' => $this->sign($headers['Sec-WebSocket-Key'])
// , 'Sec-WebSocket-Protocol' => ''
);
}

View File

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

View File

@ -14,7 +14,7 @@ interface VersionInterface {
* @return bool
* @throws UnderflowException If the protocol thinks the headers are still fragmented
*/
static function isProtocol(array $headers);
static function isProtocol($headers);
/**
* Perform the handshake and return the response headers
@ -22,6 +22,7 @@ interface VersionInterface {
* @return array|string
* @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)
* @todo Change param to accept a Guzzle RequestInterface object
*/
function handshake($message);

View File

@ -6,3 +6,5 @@
$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');