From 51d0516aa3c44c7fc448e10f1344e5c809d46f78 Mon Sep 17 00:00:00 2001
From: Chris Boden <cboden@gmail.com>
Date: Fri, 28 Oct 2011 14:12:39 -0400
Subject: [PATCH] Cleanup

Application stack working!
Existing unit tests fixed
Implemented HyBi-10 unframing
---
 .../{Server => }/Command/CloseConnection.php  |   2 +-
 .../{Server => }/Command/CommandInterface.php |   2 +-
 lib/Ratchet/{Server => }/Command/Null.php     |   2 +-
 lib/Ratchet/{Server => }/Command/Ping.php     |   2 +-
 lib/Ratchet/{Server => }/Command/Pong.php     |   2 +-
 .../{Server => }/Command/SendMessage.php      |   2 +-
 lib/Ratchet/Protocol/WebSocket.php            |  50 ++++++++-
 .../Protocol/WebSocket/AppInterface.php       |  14 +++
 lib/Ratchet/Protocol/WebSocket/Client.php     |  25 ++++-
 .../Protocol/WebSocket/Version/Hixie76.php    |  21 ++--
 .../Protocol/WebSocket/Version/Hybi10.php     | 106 ++++++++++++++++--
 .../WebSocket/Version/VersionInterface.php    |  22 +++-
 lib/Ratchet/ReceiverInterface.php             |   3 +
 lib/Ratchet/Server.php                        |   2 +-
 .../Ratchet/Tests/Protocol/WebSocketTest.php  |   5 +-
 tests/Ratchet/Tests/ServerTest.php            |   9 +-
 tests/Ratchet/Tests/SocketTest.php            |   5 +-
 17 files changed, 230 insertions(+), 44 deletions(-)
 rename lib/Ratchet/{Server => }/Command/CloseConnection.php (89%)
 rename lib/Ratchet/{Server => }/Command/CommandInterface.php (80%)
 rename lib/Ratchet/{Server => }/Command/Null.php (84%)
 rename lib/Ratchet/{Server => }/Command/Ping.php (87%)
 rename lib/Ratchet/{Server => }/Command/Pong.php (87%)
 rename lib/Ratchet/{Server => }/Command/SendMessage.php (95%)
 create mode 100644 lib/Ratchet/Protocol/WebSocket/AppInterface.php

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 @@
 <?php
-namespace Ratchet\Server\Command;
+namespace Ratchet\Command;
 use Ratchet\SocketCollection;
 
 class CloseConnection implements CommandInterface {
diff --git a/lib/Ratchet/Server/Command/CommandInterface.php b/lib/Ratchet/Command/CommandInterface.php
similarity index 80%
rename from lib/Ratchet/Server/Command/CommandInterface.php
rename to lib/Ratchet/Command/CommandInterface.php
index 30c9b16..ca45e57 100644
--- a/lib/Ratchet/Server/Command/CommandInterface.php
+++ b/lib/Ratchet/Command/CommandInterface.php
@@ -1,5 +1,5 @@
 <?php
-namespace Ratchet\Server\Command;
+namespace Ratchet\Command;
 use Ratchet\SocketCollection;
 
 interface CommandInterface {
diff --git a/lib/Ratchet/Server/Command/Null.php b/lib/Ratchet/Command/Null.php
similarity index 84%
rename from lib/Ratchet/Server/Command/Null.php
rename to lib/Ratchet/Command/Null.php
index ce0365c..67865d1 100644
--- a/lib/Ratchet/Server/Command/Null.php
+++ b/lib/Ratchet/Command/Null.php
@@ -1,5 +1,5 @@
 <?php
-namespace Ratchet\Server\Command;
+namespace Ratchet\Command;
 use Ratchet\SocketCollection;
 
 class Null implements CommandInterface {
diff --git a/lib/Ratchet/Server/Command/Ping.php b/lib/Ratchet/Command/Ping.php
similarity index 87%
rename from lib/Ratchet/Server/Command/Ping.php
rename to lib/Ratchet/Command/Ping.php
index e993c1d..1e8b481 100644
--- a/lib/Ratchet/Server/Command/Ping.php
+++ b/lib/Ratchet/Command/Ping.php
@@ -1,5 +1,5 @@
 <?php
-namespace Ratchet\Server\Command;
+namespace Ratchet\Command;
 use Ratchet\SocketCollection;
 
 /**
diff --git a/lib/Ratchet/Server/Command/Pong.php b/lib/Ratchet/Command/Pong.php
similarity index 87%
rename from lib/Ratchet/Server/Command/Pong.php
rename to lib/Ratchet/Command/Pong.php
index 1720bf3..fb22d2f 100644
--- a/lib/Ratchet/Server/Command/Pong.php
+++ b/lib/Ratchet/Command/Pong.php
@@ -1,5 +1,5 @@
 <?php
-namespace Ratchet\Server\Command;
+namespace Ratchet\Command;
 use Ratchet\SocketCollection;
 
 /**
diff --git a/lib/Ratchet/Server/Command/SendMessage.php b/lib/Ratchet/Command/SendMessage.php
similarity index 95%
rename from lib/Ratchet/Server/Command/SendMessage.php
rename to lib/Ratchet/Command/SendMessage.php
index fefc258..298d132 100644
--- a/lib/Ratchet/Server/Command/SendMessage.php
+++ b/lib/Ratchet/Command/SendMessage.php
@@ -1,5 +1,5 @@
 <?php
-namespace Ratchet\Server\Command;
+namespace Ratchet\Command;
 use Ratchet\SocketCollection;
 
 class SendMessage implements CommandInterface {
diff --git a/lib/Ratchet/Protocol/WebSocket.php b/lib/Ratchet/Protocol/WebSocket.php
index 62a0e68..94b6efe 100644
--- a/lib/Ratchet/Protocol/WebSocket.php
+++ b/lib/Ratchet/Protocol/WebSocket.php
@@ -8,6 +8,8 @@ use Ratchet\ReceiverInterface;
 
 /**
  * @link http://ca.php.net/manual/en/ref.http.php
+ * @todo Make sure this works both ways (client/server) as stack needs to exist on client for framing
+ * @todo Clean up Client/Version stuff.  This should be a factory making single instances of Version classes, implement chain of reponsibility for version - client should implement an interface?
  */
 class WebSocket implements ProtocolInterface {
     /**
@@ -25,6 +27,11 @@ class WebSocket implements ProtocolInterface {
      */
     protected $_app;
 
+    protected $_versions = array(
+        'HyBi10'  => 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 @@
+<?php
+namespace Ratchet\Protocol\Websocket;
+use Ratchet\ReceiverInterface;
+
+/**
+ * @todo App interfaces this (optionally) if is meant for WebSocket
+ * @todo WebSocket checks if instanceof AppInterface, if so uses getSubProtocol() when doing handshake
+ */
+interface AppInterface extends ReceiverInterface {
+    /**
+     * @return string
+     */
+    function getSubProtocol();
+}
\ No newline at end of file
diff --git a/lib/Ratchet/Protocol/WebSocket/Client.php b/lib/Ratchet/Protocol/WebSocket/Client.php
index ec0760b..51608d5 100644
--- a/lib/Ratchet/Protocol/WebSocket/Client.php
+++ b/lib/Ratchet/Protocol/WebSocket/Client.php
@@ -13,13 +13,30 @@ class Client {
      */
     protected $_hands_shook = false;
 
-    public function doHandshake(VersionInterface $version) {
-        $key = $version->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();