Application stack working!
Existing unit tests fixed
Implemented HyBi-10 unframing
This commit is contained in:
Chris Boden 2011-10-28 14:12:39 -04:00
parent 7514ce8e85
commit 51d0516aa3
17 changed files with 230 additions and 44 deletions

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\Server\Command;
namespace Ratchet\Command;
use Ratchet\SocketCollection;
class CloseConnection implements CommandInterface {

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\Server\Command;
namespace Ratchet\Command;
use Ratchet\SocketCollection;
interface CommandInterface {

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\Server\Command;
namespace Ratchet\Command;
use Ratchet\SocketCollection;
class Null implements CommandInterface {

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\Server\Command;
namespace Ratchet\Command;
use Ratchet\SocketCollection;
/**

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\Server\Command;
namespace Ratchet\Command;
use Ratchet\SocketCollection;
/**

View File

@ -1,5 +1,5 @@
<?php
namespace Ratchet\Server\Command;
namespace Ratchet\Command;
use Ratchet\SocketCollection;
class SendMessage implements CommandInterface {

View File

@ -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
}

View File

@ -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();
}

View File

@ -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);
}
/**

View File

@ -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) {
}
}

View File

@ -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));
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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

View File

@ -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() {

View File

@ -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();

View File

@ -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();