From 2d7774fd6547b0a6df8556825b1ba6c01f642d7d Mon Sep 17 00:00:00 2001 From: Chris Boden Date: Tue, 1 Nov 2011 14:10:12 -0400 Subject: [PATCH] Hixie-76 protocol Implemented WebSocket Hixie-76 protocol --- lib/Ratchet/Protocol/WebSocket.php | 90 +++++++++++++------ lib/Ratchet/Protocol/WebSocket/Client.php | 6 +- .../Protocol/WebSocket/Version/Hixie76.php | 48 ++++++++-- .../Protocol/WebSocket/Version/HyBi10.php | 15 +++- .../WebSocket/Version/VersionInterface.php | 14 +-- 5 files changed, 122 insertions(+), 51 deletions(-) diff --git a/lib/Ratchet/Protocol/WebSocket.php b/lib/Ratchet/Protocol/WebSocket.php index d7bc906..efb0a45 100644 --- a/lib/Ratchet/Protocol/WebSocket.php +++ b/lib/Ratchet/Protocol/WebSocket.php @@ -61,19 +61,21 @@ class WebSocket implements ProtocolInterface { public function onRecv(SocketInterface $from, $msg) { $client = $this->_clients[$from]; if (true !== $client->isHandshakeComplete()) { + $response = $client->setVersion($this->getVersion($msg))->doHandshake($msg); - $headers = $this->getHeaders($msg); - $response = $client->setVersion($this->getVersion($headers))->doHandshake($headers); + if (is_array($response)) { + $header = ''; + foreach ($response as $key => $val) { + if (!empty($key)) { + $header .= "{$key}: "; + } - $header = ''; - foreach ($response as $key => $val) { - if (!empty($key)) { - $header .= "{$key}: "; + $header .= "{$val}\r\n"; } - - $header .= "{$val}\r\n"; + $header .= "\r\n"; + } else { + $header = $response; } - $header .= "\r\n"; $to = new \Ratchet\SocketCollection; $to->enqueue($from); @@ -133,30 +135,66 @@ class WebSocket implements ProtocolInterface { } /** - * @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 - * @todo Put in fallback code if http_parse_headers is not a function + * @param array of HTTP headers + * @return Version\VersionInterface */ - protected function getHeaders($http_message) { - return http_parse_headers($http_message); + protected function getVersion($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 */ - protected function getVersion(array $headers) { - if (isset($headers['Sec-Websocket-Version'])) { // HyBi - if ($headers['Sec-Websocket-Version'] == '8') { - if (null === $this->_versions['HyBi10']) { - $this->_versions['HyBi10'] = new Version\HyBi10; - } - - return $this->_versions['HyBi10']; - } - } elseif (isset($headers['Sec-Websocket-Key2'])) { // Hixie + protected function versionFactory($version) { + if (null === $this->_versions[$version]) { + $ns = __CLASS__ . "\\Version\\{$version}"; + $this->_version[$version] = new $ns; } - throw new \UnexpectedValueException('Could not identify WebSocket protocol'); + return $this->_version[$version]; + } + + /** + * @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; } } \ No newline at end of file diff --git a/lib/Ratchet/Protocol/WebSocket/Client.php b/lib/Ratchet/Protocol/WebSocket/Client.php index fd65d91..6f4c6e2 100644 --- a/lib/Ratchet/Protocol/WebSocket/Client.php +++ b/lib/Ratchet/Protocol/WebSocket/Client.php @@ -33,10 +33,10 @@ class Client { } /** - * @param array - * @return array + * @param string + * @return array|string */ - public function doHandshake(array $headers) { + public function doHandshake($headers) { $this->_hands_shook = true; return $this->_version->handshake($headers); diff --git a/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php b/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php index b97786f..9aea98a 100644 --- a/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php +++ b/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php @@ -3,26 +3,56 @@ namespace Ratchet\Protocol\WebSocket\Version; /** * 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 { - 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) { + return substr($message, 1, strlen($message) - 2); } 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]) : + '' + ; } - /** - * What was I doing here? - * @param Headers - * @return string - */ - public function concatinateKeyString($headers) { + protected function _createHandshakeThingy($key1, $key2, $code) { + return md5( + pack('N', $this->_doStuffToObtainAnInt32($key1)) + . pack('N', $this->_doStuffToObtainAnInt32($key2)) + . $code + , true); } } \ No newline at end of file diff --git a/lib/Ratchet/Protocol/WebSocket/Version/HyBi10.php b/lib/Ratchet/Protocol/WebSocket/Version/HyBi10.php index 6e86785..c677574 100644 --- a/lib/Ratchet/Protocol/WebSocket/Version/HyBi10.php +++ b/lib/Ratchet/Protocol/WebSocket/Version/HyBi10.php @@ -8,8 +8,13 @@ namespace Ratchet\Protocol\WebSocket\Version; class HyBi10 implements VersionInterface { const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; - public function handshake(array $headers) { - $key = $this->sign($headers['Sec-Websocket-Key']); + /** + * @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']); return array( '' => 'HTTP/1.1 101 Switching Protocols' @@ -178,6 +183,12 @@ class HyBi10 implements VersionInterface { 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) { return base64_encode(sha1($key . static::GUID, 1)); } diff --git a/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php b/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php index 59e5b27..3471cd2 100644 --- a/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php +++ b/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php @@ -8,10 +8,10 @@ namespace Ratchet\Protocol\WebSocket\Version; interface VersionInterface { /** * Perform the handshake and return the response headers - * @param array - * @return array + * @param string + * @return array|string */ - function handshake(array $headers); + function handshake($message); /** * Get a framed message as per the protocol and return the decoded message @@ -26,12 +26,4 @@ interface VersionInterface { * @return string */ 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); } \ No newline at end of file