diff --git a/lib/Ratchet/Server/Command/CloseConnection.php b/lib/Ratchet/Command/CloseConnection.php similarity index 89% rename from lib/Ratchet/Server/Command/CloseConnection.php rename to lib/Ratchet/Command/CloseConnection.php index 999943a..f190669 100644 --- a/lib/Ratchet/Server/Command/CloseConnection.php +++ b/lib/Ratchet/Command/CloseConnection.php @@ -1,5 +1,5 @@ null + , 'Hixie76' => null + ); + public function __construct(ReceiverInterface $application) { $this->_lookup = new \SplObjectStorage; $this->_app = $application; @@ -62,15 +69,29 @@ class WebSocket implements ProtocolInterface { } public function onRecv(SocketInterface $from, $msg) { - $client = $this->_lookup[$from]; + $client = $this->_lookup[$from]; if (true !== $client->isHandshakeComplete()) { - $headers = $this->getHeaders($msg); - $header = $client->doHandshake($this->getVersion($headers)); + +// remove client, get protocol, do handshake, return, etc + + $headers = $this->getHeaders($msg); + $response = $client->setVersion($this->getVersion($headers))->doHandshake($headers); + + $header = ''; + foreach ($response as $key => $val) { + if (!empty($key)) { + $header .= "{$key}: "; + } + + $header .= "{$val}\r\n"; + } + $header .= "\r\n"; +// $header = implode("\r\n", $response) . "\r\n"; // $from->write($header, strlen($header)); $to = new \Ratchet\SocketCollection; $to->enqueue($from); - $cmd = new \Ratchet\Server\Command\SendMessage($to); + $cmd = new \Ratchet\Command\SendMessage($to); $cmd->setMessage($header); // call my decorated onRecv() @@ -80,6 +101,20 @@ $this->_server->log('Returning handshake: ' . $header); return $cmd; } + try { + $msg = $client->getVersion()->unframe($msg); + if (is_array($msg)) { // temporary + $msg = $msg['payload']; + } + + } catch (\UnexpectedValueException $e) { + $to = new \Ratchet\SocketCollection; + $to->enqueue($from); + $cmd = new \Ratchet\Command\Close($to); + + return $cmd; + } + return $this->_app->onRecv($from, $msg); } @@ -98,6 +133,7 @@ $this->_server->log('Returning handshake: ' . $header); /** * @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 http_parse_headers($http_message); @@ -109,7 +145,11 @@ $this->_server->log('Returning handshake: ' . $header); protected function getVersion(array $headers) { if (isset($headers['Sec-Websocket-Version'])) { // HyBi if ($headers['Sec-Websocket-Version'] == '8') { - return new Version\Hybi10($headers); + if (null === $this->_versions['HyBi10']) { + $this->_versions['HyBi10'] = new Version\Hybi10; + } + + return $this->_versions['HyBi10']; } } elseif (isset($headers['Sec-Websocket-Key2'])) { // Hixie } diff --git a/lib/Ratchet/Protocol/WebSocket/AppInterface.php b/lib/Ratchet/Protocol/WebSocket/AppInterface.php new file mode 100644 index 0000000..2dedc13 --- /dev/null +++ b/lib/Ratchet/Protocol/WebSocket/AppInterface.php @@ -0,0 +1,14 @@ +sign(); -// $tosend['Sec-WebSocket-Accept'] = $key; + /** + * @param VersionInterface + * @return Client + */ + public function setVersion(VersionInterface $version) { + $this->_version = $version; + return $this; + } + /** + * @return VersionInterface + */ + public function getVersion() { + return $this->_version; + } + + /** + * @param array + * @return array + */ + public function doHandshake(array $headers) { $this->_hands_shook = true; - return "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {$key}\r\nSec-WebSocket-Protocol: test\r\n\r\n"; + return $this->_version->handshake($headers); } /** diff --git a/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php b/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php index 3ee2fce..a5d5993 100644 --- a/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php +++ b/lib/Ratchet/Protocol/WebSocket/Version/Hixie76.php @@ -2,24 +2,23 @@ namespace Ratchet\Protocol\WebSocket\Version; class Hixie76 implements VersionInterface { - protected $_headers = array(); + public function handshake(array $headers) { + } - public function __construct(array $headers) { + public function unframe($message) { + } + + public function frame($message) { + } + + public function sign($key) { } /** + * What was I doing here? * @param Headers * @return string */ public function concatinateKeyString($headers) { - - } - - /** - * @param string - * @return string - */ - public function sign($key) { - } } \ 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 08d0f94..67cba81 100644 --- a/lib/Ratchet/Protocol/WebSocket/Version/Hybi10.php +++ b/lib/Ratchet/Protocol/WebSocket/Version/Hybi10.php @@ -4,17 +4,109 @@ namespace Ratchet\Protocol\WebSocket\Version; class Hybi10 implements VersionInterface { const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; - protected $_headers = array(); + /** + */ + public function handshake(array $headers) { + $key = $this->sign($headers['Sec-Websocket-Key']); - public function __construct(array $headers) { - $this->_headers = $headers; + return array( + '' => 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade' => 'websocket' + , 'Connection' => 'Upgrade' + , 'Sec-WebSocket-Accept' => $this->sign($headers['Sec-Websocket-Key']) +// , 'Sec-WebSocket-Protocol' => '' + ); + } + + /** + * Unframe a message received from the client + * Thanks to @lemmingzshadow for the code on decoding a HyBi-10 frame + * @link https://github.com/lemmingzshadow/php-websocket + * @param string + * @return string + * @throws UnexpectedValueException + */ + public function unframe($message) { + $data = $message; + $payloadLength = ''; + $mask = ''; + $unmaskedPayload = ''; + $decodedData = array(); + + // estimate frame type: + $firstByteBinary = sprintf('%08b', ord($data[0])); + $secondByteBinary = sprintf('%08b', ord($data[1])); + $opcode = bindec(substr($firstByteBinary, 4, 4)); + $isMasked = ($secondByteBinary[0] == '1') ? true : false; + $payloadLength = ord($data[1]) & 127; + + // close connection if unmasked frame is received: + if($isMasked === false) { + throw new \UnexpectedValueException('Masked byte is false'); + } + + switch($opcode) { + // text frame: + case 1: + $decodedData['type'] = 'text'; + break; + + // connection close frame: + case 8: + $decodedData['type'] = 'close'; + break; + + // ping frame: + case 9: + $decodedData['type'] = 'ping'; + break; + + // pong frame: + case 10: + $decodedData['type'] = 'pong'; + break; + + default: + // Close connection on unknown opcode: + throw new UnexpectedValueException('Unknown opcode'); + break; + } + + if($payloadLength === 126) { + $mask = substr($data, 4, 4); + $payloadOffset = 8; + } elseif($payloadLength === 127) { + $mask = substr($data, 10, 4); + $payloadOffset = 14; + } else { + $mask = substr($data, 2, 4); + $payloadOffset = 6; + } + + $dataLength = strlen($data); + + if($isMasked === true) { + for($i = $payloadOffset; $i < $dataLength; $i++) { + $j = $i - $payloadOffset; + $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; + } + $decodedData['payload'] = $unmaskedPayload; + } else { + $payloadOffset = $payloadOffset - 4; + $decodedData['payload'] = substr($data, $payloadOffset); + } + + return $decodedData; } - public function sign($key = null) { -if (null === $key) { - $key = $this->_headers['Sec-Websocket-Key']; -} + /** + * @todo Complete this method + */ + public function frame($message) { + return $message; + } + public function sign($key) { return base64_encode(sha1($key . static::GUID, 1)); } } \ No newline at end of file diff --git a/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php b/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php index 53d1d1b..817344b 100644 --- a/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php +++ b/lib/Ratchet/Protocol/WebSocket/Version/VersionInterface.php @@ -3,13 +3,31 @@ namespace Ratchet\Protocol\WebSocket\Version; interface VersionInterface { /** + * Perform the handshake and return the response headers * @param array + * @return array */ - function __construct(array $headers); + function handshake(array $headers); + + /** + * Get a framed message as per the protocol and return the decoded message + * @param string + * @return string + * @todo Return a frame object with message, type, masked? + */ + function unframe($message); /** * @param string * @return string */ - function sign($header); + 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 diff --git a/lib/Ratchet/ReceiverInterface.php b/lib/Ratchet/ReceiverInterface.php index f06eb10..0c52b7a 100644 --- a/lib/Ratchet/ReceiverInterface.php +++ b/lib/Ratchet/ReceiverInterface.php @@ -3,6 +3,9 @@ namespace Ratchet; use Ratchet\Server; use Ratchet\SocketObserver; +/** + * @todo Should probably move this into \Ratchet\Server namespace + */ interface ReceiverInterface extends SocketObserver { /** * @return string diff --git a/lib/Ratchet/Server.php b/lib/Ratchet/Server.php index 437c7bf..59a547c 100644 --- a/lib/Ratchet/Server.php +++ b/lib/Ratchet/Server.php @@ -10,7 +10,7 @@ use Ratchet\Logging\NullLogger; * @todo Move SocketObserver methods to separate class, create, wrap class in __construct * @todo Currently passing Socket object down the decorated chain - should be sending reference to it instead; Receivers do not interact with the Socket directly, they do so through the Command pattern */ -class Server implements SocketObserver { +class Server implements SocketObserver, \IteratorAggregate { /** * The master socket, receives all connections * @type Socket diff --git a/tests/Ratchet/Tests/Protocol/WebSocketTest.php b/tests/Ratchet/Tests/Protocol/WebSocketTest.php index 3aabc49..3cc4b39 100644 --- a/tests/Ratchet/Tests/Protocol/WebSocketTest.php +++ b/tests/Ratchet/Tests/Protocol/WebSocketTest.php @@ -2,15 +2,16 @@ namespace Ratchet\Tests\Protocol; use Ratchet\Protocol\WebSocket; use Ratchet\Tests\Mock\Socket; +use Ratchet\Tests\Mock\Application; /** * @covers Ratchet\Protocol\WebSocket */ -class ServerTest extends \PHPUnit_Framework_TestCase { +class WebSocketTest extends \PHPUnit_Framework_TestCase { protected $_ws; public function setUp() { - $this->_ws = new WebSocket(); + $this->_ws = new WebSocket(new Application); } public function testServerImplementsServerInterface() { diff --git a/tests/Ratchet/Tests/ServerTest.php b/tests/Ratchet/Tests/ServerTest.php index e61c60b..dfd0002 100644 --- a/tests/Ratchet/Tests/ServerTest.php +++ b/tests/Ratchet/Tests/ServerTest.php @@ -11,10 +11,12 @@ use Ratchet\Tests\Mock\ArrayLogger; class ServerTest extends \PHPUnit_Framework_TestCase { protected $_catalyst; protected $_server; + protected $_app; public function setUp() { $this->_catalyst = new Socket; - $this->_server = new Server($this->_catalyst, new TestApp); + $this->_app = new TestApp; + $this->_server = new Server($this->_catalyst, $this->_app); } protected function getPrivateProperty($class, $name) { @@ -36,7 +38,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase { public function testPassedLoggerIsSetInConstruct() { $logger = new ArrayLogger; - $server = new Server(new Socket(), $logger); + $server = new Server(new Socket(), $this->_app, $logger); $this->assertSame($logger, $this->getPrivateProperty($server, '_log')); } @@ -70,8 +72,7 @@ class ServerTest extends \PHPUnit_Framework_TestCase { } public function testBindToInvalidAddress() { - $this->markTestIncomplete(); - return; + return $this->markTestIncomplete(); $app = new TestApp(); diff --git a/tests/Ratchet/Tests/SocketTest.php b/tests/Ratchet/Tests/SocketTest.php index 2994e1e..1f0c8e9 100644 --- a/tests/Ratchet/Tests/SocketTest.php +++ b/tests/Ratchet/Tests/SocketTest.php @@ -3,6 +3,7 @@ namespace Ratchet\Tests; use Ratchet\Tests\Mock\FakeSocket as Socket; use Ratchet\Socket as RealSocket; use Ratchet\Tests\Mock\Protocol; +use Ratchet\Tests\Mock\Application as TestApp; /** * @covers Ratchet\Socket @@ -51,14 +52,14 @@ class SocketTest extends \PHPUnit_Framework_TestCase { } public function testConstructionFromProtocolInterfaceConfig() { - $protocol = new Protocol(); + $protocol = new Protocol(new TestApp); $socket = Socket::createFromConfig($protocol); $this->assertInstanceOf('\\Ratchet\\Socket', $socket); } public function testCreationFromConfigOutputMatchesInput() { - $protocol = new Protocol(); + $protocol = new Protocol(new TestApp); $socket = Socket::createFromConfig($protocol); $config = $protocol::getDefaultConfig();