Hixie-76 protocol

Implemented WebSocket Hixie-76 protocol
This commit is contained in:
Chris Boden 2011-11-01 14:10:12 -04:00
parent 7c5c5ed6ce
commit 2d7774fd65
5 changed files with 122 additions and 51 deletions

View File

@ -61,10 +61,9 @@ class WebSocket implements ProtocolInterface {
public function onRecv(SocketInterface $from, $msg) { public function onRecv(SocketInterface $from, $msg) {
$client = $this->_clients[$from]; $client = $this->_clients[$from];
if (true !== $client->isHandshakeComplete()) { if (true !== $client->isHandshakeComplete()) {
$response = $client->setVersion($this->getVersion($msg))->doHandshake($msg);
$headers = $this->getHeaders($msg); if (is_array($response)) {
$response = $client->setVersion($this->getVersion($headers))->doHandshake($headers);
$header = ''; $header = '';
foreach ($response as $key => $val) { foreach ($response as $key => $val) {
if (!empty($key)) { if (!empty($key)) {
@ -74,6 +73,9 @@ class WebSocket implements ProtocolInterface {
$header .= "{$val}\r\n"; $header .= "{$val}\r\n";
} }
$header .= "\r\n"; $header .= "\r\n";
} else {
$header = $response;
}
$to = new \Ratchet\SocketCollection; $to = new \Ratchet\SocketCollection;
$to->enqueue($from); $to->enqueue($from);
@ -133,30 +135,66 @@ class WebSocket implements ProtocolInterface {
} }
/** /**
* @param string * @param array of HTTP headers
* @return array * @return Version\VersionInterface
* @todo Consider strtolower all the header keys...right now PHP Changes Sec-WebSocket-X to Sec-Websocket-X...this could change
* @todo Put in fallback code if http_parse_headers is not a function
*/ */
protected function getHeaders($http_message) { protected function getVersion($message) {
return http_parse_headers($http_message); $headers = $this->getHeaders($message);
if (isset($headers['Sec-Websocket-Version'])) { // HyBi
if ($headers['Sec-Websocket-Version'] == '8') {
return $this->versionFactory('HyBi10');
}
} elseif (isset($headers['Sec-Websocket-Key2'])) { // Hixie
return $this->versionFactory('Hixie76');
}
throw new \UnexpectedValueException('Could not identify WebSocket protocol');
} }
/** /**
* @return Version\VersionInterface * @return Version\VersionInterface
*/ */
protected function getVersion(array $headers) { protected function versionFactory($version) {
if (isset($headers['Sec-Websocket-Version'])) { // HyBi if (null === $this->_versions[$version]) {
if ($headers['Sec-Websocket-Version'] == '8') { $ns = __CLASS__ . "\\Version\\{$version}";
if (null === $this->_versions['HyBi10']) { $this->_version[$version] = new $ns;
$this->_versions['HyBi10'] = new Version\HyBi10;
} }
return $this->_versions['HyBi10']; return $this->_version[$version];
}
} elseif (isset($headers['Sec-Websocket-Key2'])) { // Hixie
} }
throw new \UnexpectedValueException('Could not identify WebSocket protocol'); /**
* @param string
* @return array
* @todo Consider strtolower all the header keys...right now PHP Changes Sec-WebSocket-X to Sec-Websocket-X...this could change
*/
protected function getHeaders($http_message) {
return function_exists('http_parse_headers') ? http_parse_headers($http_message) : $this->http_parse_headers($http_message);
}
/**
* This is a fallback method for http_parse_headers as not all php installs have the HTTP module present
* @internal
*/
protected function http_parse_headers($http_message) {
$retVal = array();
$fields = explode("br", preg_replace("%(<|/\>|>)%", "", nl2br($header)));
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

@ -33,10 +33,10 @@ class Client {
} }
/** /**
* @param array * @param string
* @return array * @return array|string
*/ */
public function doHandshake(array $headers) { public function doHandshake($headers) {
$this->_hands_shook = true; $this->_hands_shook = true;
return $this->_version->handshake($headers); return $this->_version->handshake($headers);

View File

@ -3,26 +3,56 @@ namespace Ratchet\Protocol\WebSocket\Version;
/** /**
* The Hixie76 is currently implemented by Safari * The Hixie76 is currently implemented by Safari
* Not yet complete * Handshake from Andrea Giammarchi (http://webreflection.blogspot.com/2010/06/websocket-handshake-76-simplified.html)
* @link http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
*/ */
class Hixie76 implements VersionInterface { class Hixie76 implements VersionInterface {
public function handshake(array $headers) { /**
* @return string
*/
public function handshake($message) {
$buffer = $message;
$resource = $host = $origin = $key1 = $key2 = $protocol = $code = $handshake = null;
preg_match('#GET (.*?) HTTP#', $buffer, $match) && $resource = $match[1];
preg_match("#Host: (.*?)\r\n#", $buffer, $match) && $host = $match[1];
preg_match("#Sec-WebSocket-Key1: (.*?)\r\n#", $buffer, $match) && $key1 = $match[1];
preg_match("#Sec-WebSocket-Key2: (.*?)\r\n#", $buffer, $match) && $key2 = $match[1];
preg_match("#Sec-WebSocket-Protocol: (.*?)\r\n#", $buffer, $match) && $protocol = $match[1];
preg_match("#Origin: (.*?)\r\n#", $buffer, $match) && $origin = $match[1];
preg_match("#\r\n(.*?)\$#", $buffer, $match) && $code = $match[1];
return "HTTP/1.1 101 WebSocket Protocol Handshake\r\n".
"Upgrade: WebSocket\r\n"
. "Connection: Upgrade\r\n"
. "Sec-WebSocket-Origin: {$origin}\r\n"
. "Sec-WebSocket-Location: ws://{$host}{$resource}\r\n"
. ($protocol ? "Sec-WebSocket-Protocol: {$protocol}\r\n" : "")
. "\r\n"
. $this->_createHandshakeThingy($key1, $key2, $code)
;
} }
public function unframe($message) { public function unframe($message) {
return substr($message, 1, strlen($message) - 2);
} }
public function frame($message) { public function frame($message) {
return chr(0) . $message . chr(255);
} }
public function sign($key) { protected function _doStuffToObtainAnInt32($key) {
return preg_match_all('#[0-9]#', $key, $number) && preg_match_all('# #', $key, $space) ?
implode('', $number[0]) / count($space[0]) :
''
;
} }
/** protected function _createHandshakeThingy($key1, $key2, $code) {
* What was I doing here? return md5(
* @param Headers pack('N', $this->_doStuffToObtainAnInt32($key1))
* @return string . pack('N', $this->_doStuffToObtainAnInt32($key2))
*/ . $code
public function concatinateKeyString($headers) { , true);
} }
} }

View File

@ -8,7 +8,12 @@ namespace Ratchet\Protocol\WebSocket\Version;
class HyBi10 implements VersionInterface { class HyBi10 implements VersionInterface {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
public function handshake(array $headers) { /**
* @return array
* @todo Use the WebSocket::http_parse_headers wrapper instead of native function (how to get to method is question)
*/
public function handshake($message) {
$headers = http_parse_headers($message);
$key = $this->sign($headers['Sec-Websocket-Key']); $key = $this->sign($headers['Sec-Websocket-Key']);
return array( return array(
@ -178,6 +183,12 @@ class HyBi10 implements VersionInterface {
return $frame; return $frame;
} }
/**
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language
* @param string
* @return string
* @internal
*/
public function sign($key) { public function sign($key) {
return base64_encode(sha1($key . static::GUID, 1)); return base64_encode(sha1($key . static::GUID, 1));
} }

View File

@ -8,10 +8,10 @@ namespace Ratchet\Protocol\WebSocket\Version;
interface VersionInterface { interface VersionInterface {
/** /**
* Perform the handshake and return the response headers * Perform the handshake and return the response headers
* @param array * @param string
* @return array * @return array|string
*/ */
function handshake(array $headers); function handshake($message);
/** /**
* Get a framed message as per the protocol and return the decoded message * Get a framed message as per the protocol and return the decoded message
@ -26,12 +26,4 @@ interface VersionInterface {
* @return string * @return string
*/ */
function frame($message); function frame($message);
/**
* Used when doing the handshake to encode the key, verifying client/server are speaking the same language
* @param string
* @return string
* @internal
*/
function sign($key);
} }