Browse files

Improved "AsyncServer::checkAccept" to pass the same arguments as

"AsyncServer::onAcceptEvent"
Created a new "genericWebSocketServer", able to deal with old (aka
IETF hixie-76) and new (aka IETF hybi-10) websocket protocol, and being
scalable with future protocol improvements
Created the "ExampleGenericWebSocket" class as...  ...an example ;-)
Does not deal yet with BINARY frame type ;-(
  • Loading branch information...
1 parent 7485d24 commit a8e4ec05b183dd610fc7137274a1b0a3244c0d93 @Erika31 Erika31 committed Oct 18, 2011
View
99 app-examples/ExampleGenericWebSocket.php
@@ -0,0 +1,99 @@
+<?php
+
+define("EXTERNAL_APP_CLASSES_DIR", dirname(__FILE__) . "/../../../app/") ;
+
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Base_Class.class.php") ;
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/DB_Connection.class.php") ;
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/DB_GenericTable.class.php") ;
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Tools.class.php") ;
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Crypto.class.php") ;
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Session.class.php") ;
+require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/wsServer.class.php") ;
+
+class ExampleGenericWebSocket extends AppInstance {
+
+ /**
+ * Called when the worker is ready to go.
+ * @return void
+ */
+ public function onReady() {
+ if ($this->WS = Daemon::$appResolver->getInstanceByAppName('genericWebSocketServer')) {
+ $this->WS->addRoute('exampleApp', function ($client) {
+ return new ExampleGenericWebSocketRoute($client);
+ }
+ );
+
+ // If you want to manage timeout events :
+ $appInstance = $this ;
+ $this->WS->registerEventTimeout(function() use ($appInstance) {
+ return $appInstance->onEventTimeout() ;
+ }
+ ) ;
+ }
+ }
+
+ /**
+ * Called when a timeout event is raised
+ * @return void
+ */
+ public function onEventTimeout() {
+
+ }
+
+ /**
+ * Called when application instance is going to shutdown
+ * @todo protected?
+ * @return boolean Ready to shutdown?
+ */
+ public function onShutdown() {
+ return TRUE;
+ }
+
+}
+
+class ExampleGenericWebSocketRoute extends WebSocketRoute {
+
+ /**
+ * Called when new frame received.
+ * @param string Frame's contents.
+ * @param string Frame's type. ("STRING" or "BINARY")
+ * @return void
+ */
+ public function onFrame($data, $type) {
+ if ($data === 'ping') {
+ $this->client->sendFrame('pong', "STRING", function($client) {
+ Daemon::log($client->clientAddr . ' : SEND pong');
+ }
+ );
+ return ;
+ }
+ }
+
+ /**
+ * Called when the connection is handshaked.
+ * @return void
+ */
+ public function onHandshake() {
+
+ Daemon::log($this->client->clientAddr . ' : Handshake success') ;
+ }
+
+ /**
+ * Called when session finished.
+ * @return void
+ */
+ public function onFinish() {
+
+ Daemon::log($this->client->clientAddr . ' : Disconnected');
+ }
+
+ /**
+ * Called when the worker is going to shutdown.
+ * @return boolean Ready to shutdown?
+ */
+ public function gracefulShutdown() {
+
+ Daemon::log($this->client->clientAddr . ' : Gracefully disconnecting (as requested by server)') ;
+ return TRUE ;
+ }
+}
View
267 app-servers/genericWebSocketServer.php
@@ -0,0 +1,267 @@
+<?php
+
+class genericWebSocketServer extends AsyncServer
+{
+ public $sessions = array();
+ public $routes = array();
+
+ protected $timeout_cb;
+
+ const BINARY = 0x80;
+ const STRING = 0x00;
+
+ /**
+ * Registering event timeout callback function
+ * @param Closure Callback function
+ * @return void
+ */
+
+ public function registerEventTimeout($cb)
+ {
+ if ($cb === NULL || is_callable($cb))
+ {
+ $this->timeout_cb = $cb ;
+ }
+ }
+
+ /**
+ * Setting default config options
+ * Overriden from AppInstance::getConfigDefaults
+ * @return array|false
+ */
+
+ protected function getConfigDefaults()
+ {
+ return array(
+ // listen to
+ 'listen' => 'tcp://0.0.0.0',
+ // listen port
+ 'listenport' => 55556,
+ // max allowed packet size
+ 'maxallowedpacket' => new Daemon_ConfigEntrySize('16k'),
+ // disabled by default
+ 'enable' => 0,
+ // no event_timeout by default
+ 'ev_timeout' => -1
+ );
+ }
+
+ /**
+ * Event of appInstance. Adds default settings and binds sockets.
+ * @return void
+ */
+
+ public function init()
+ {
+ $this->update();
+
+ if ($this->config->enable->value)
+ {
+ $this->bindSockets(
+ $this->config->listen->value,
+ $this->config->listenport->value
+ );
+ }
+ }
+
+ /**
+ * Enable all events of sockets
+ * @return void
+ */
+
+ public function enableSocketEvents()
+ {
+ foreach ($this->socketEvents as $ev)
+ {
+ event_base_set($ev, Daemon::$process->eventBase);
+ event_add($ev, $this->config->ev_timeout->value); // With specified timeout
+ }
+ }
+
+ /**
+ * Called when a request to HTTP-server looks like WebSocket handshake query.
+ * @return void
+ */
+/*
+ public function inheritFromRequest($req, $appInstance)
+ {
+ $connId = $req->attrs->connId;
+
+ unset(Daemon::$process->queue[$connId . '-' . $req->attrs->id]);
+
+ $this->buf[$connId] = $appInstance->buf[$connId];
+
+ unset($appInstance->buf[$connId]);
+ unset($appInstance->poolState[$connId]);
+
+ $set = event_buffer_set_callback(
+ $this->buf[$connId],
+ $this->directReads ? NULL : array($this, 'onReadEvent'),
+ array($this, 'onWriteEvent'),
+ array($this, 'onFailureEvent'),
+ array($connId)
+ );
+
+ unset(Daemon::$process->readPoolState[$connId]);
+
+ $this->poolState[$connId] = array();
+
+ $this->sessions[$connId] = new genericWebSocketSession($connId, $this);
+ $this->sessions[$connId]->clientAddr = $req->attrs->server['REMOTE_ADDR'];
+ $this->sessions[$connId]->server = $req->attrs->server;
+ $this->sessions[$connId]->firstline = TRUE;
+ $this->sessions[$connId]->stdin("\r\n" . $req->attrs->inbuf);
+ }
+*/
+ /**
+ * Adds a route if it doesn't exist already.
+ * @param string Route name.
+ * @param mixed Route's callback.
+ * @return boolean Success.
+ */
+
+ public function addRoute($route, $cb)
+ {
+ if (isset($this->routes[$route]))
+ {
+ Daemon::log(__METHOD__ . ' Route \'' . $route . '\' is already taken.');
+ return FALSE;
+ }
+
+ $this->routes[$route] = $cb;
+
+ return TRUE;
+ }
+
+ /**
+ * Force add/replace a route.
+ * @param string Route name.
+ * @param mixed Route's callback.
+ * @return boolean Success.
+ */
+
+ public function setRoute($route, $cb)
+ {
+ $this->routes[$route] = $cb;
+
+ return TRUE;
+ }
+
+ /**
+ * Removes a route.
+ * @param string Route name.
+ * @return boolean Success.
+ */
+
+ public function removeRoute($route)
+ {
+ if (!isset($this->routes[$route]))
+ {
+ return FALSE;
+ }
+
+ unset($this->routes[$route]);
+
+ return TRUE;
+ }
+
+ /**
+ * Event of appInstance.
+ * @return void
+ */
+
+ public function onReady()
+ {
+ if ($this->config->enable->value)
+ {
+ $this->enableSocketEvents();
+ }
+ }
+
+ /**
+ * Called when remote host is trying to establish the connection
+ * @param resource Descriptor
+ * @param integer Events
+ * @param mixed Attached variable
+ * @return boolean If true then we can accept new connections, else we can't
+ */
+
+ public function checkAccept($stream, $events, $arg)
+ {
+ if (!parent::checkAccept($stream, $events, $arg))
+ {
+ return FALSE;
+ }
+
+ $sockId = $arg[0];
+
+ event_add($this->socketEvents[$sockId], $this->config->ev_timeout->value) ; // With specified timeout
+
+ // Always return FALSE to skip adding event without timeout in "parent::onAcceptEvent"...
+ return FALSE ;
+ }
+
+ /**
+ * Called when remote host is trying to establish the connection
+ * @param integer Connection's ID
+ * @param string Address
+ * @return boolean Accept/Drop the connection
+ */
+
+ public function onAccept($connId, $addr)
+ {
+ if (parent::onAccept($connId, $addr))
+ {
+ Daemon::log("New client : " . $addr) ;
+
+ return TRUE ;
+ }
+
+ return FALSE ;
+ }
+
+ /**
+ * Event of asyncServer
+ * @param integer Connection's ID
+ * @param string Peer's address
+ * @return void
+ */
+ protected function onAccepted($connId, $addr)
+ {
+ $this->sessions[$connId] = new genericWebSocketSession($connId, $this);
+ $this->sessions[$connId]->clientAddr = $addr;
+ }
+
+ /**
+ * Called when new connections is waiting for accept
+ * @param resource Descriptor
+ * @param integer Events
+ * @param mixed Attached variable
+ * @return void
+ */
+
+ public function onAcceptEvent($stream, $events, $arg)
+ {
+ if ($events & EV_TIMEOUT)
+ {
+ $sockId = $arg[0];
+
+ if ($this->timeout_cb !== NULL)
+ {
+ call_user_func($this->timeout_cb) ;
+ }
+
+ event_add($this->socketEvents[$sockId], $this->config->ev_timeout->value) ;
+ return ;
+ }
+
+ parent::onAcceptEvent($stream, $events, $arg);
+ }
+/*
+ public function onTimeout()
+ {
+
+ }
+*/
+}
+
View
7 lib/AsyncServer.php
@@ -329,9 +329,12 @@ public function onAccept($connId, $addr) {
/**
* Called when remote host is trying to establish the connection
+ * @param resource Descriptor
+ * @param integer Events
+ * @param mixed Attached variable
* @return boolean If true then we can accept new connections, else we can't
*/
- public function checkAccept() {
+ public function checkAccept($stream, $events, $arg) {
if (Daemon::$process->reload) {
return FALSE;
}
@@ -505,7 +508,7 @@ public function onAcceptEvent($stream, $events, $arg) {
Daemon::$process->log(get_class($this) . '::' . __METHOD__ . '(' . $sockId . ') invoked.');
}
- if ($this->checkAccept()) {
+ if ($this->checkAccept($stream, $events, $arg)) {
event_add($this->socketEvents[$sockId]);
}
View
76 lib/WebSocketProtocol.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * Websocket protocol abstract class
+ */
+
+abstract class WebSocketProtocol
+{
+ public $description ;
+ protected $session ;
+
+ const STRING = NULL;
+ const BINARY = NULL;
+
+ public function __construct($session)
+ {
+ $this->session = $session ;
+ }
+
+ public function getFrameType($type)
+ {
+ $frametype = @constant(get_class($this) .'::' . $type) ;
+
+ if ($frametype === NULL)
+ {
+ Daemon::log(__METHOD__ . ' : Undefined frametype "' . $type . '"') ;
+ }
+
+ return $frametype ;
+ }
+
+ public function onHandshake()
+ {
+ return TRUE ;
+ }
+
+ public function sendFrame($data, $type)
+ {
+ $this->session->write($this->_dataEncode($data, $type)) ;
+ }
+
+ public function recvFrame($data, $type)
+ {
+ $this->session->onFrame($this->_dataDecode($data), $type) ;
+ $this->session->buf = "" ;
+ }
+
+ /**
+ * Returns handshaked data for reply
+ * @param string Received data (no use in this class)
+ * @return string Handshaked data
+ */
+
+ public function getHandshakeReply($data)
+ {
+ return FALSE ;
+ }
+
+ /**
+ * Data encoding
+ */
+
+ protected function _dataEncode($decodedData, $type = NULL)
+ {
+ return NULL ;
+ }
+
+ /**
+ * Data decoding
+ */
+
+ protected function _dataDecode($encodedData)
+ {
+ return NULL ;
+ }
+}
View
235 lib/WebSocketProtocolV0.php
@@ -0,0 +1,235 @@
+<?php
+
+/**
+ * Websocket protocol hixie-76
+ * @see http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
+ */
+
+class WebSocketProtocolV0 extends WebSocketProtocol
+{
+ const STRING = 0x00;
+ const BINARY = 0x80;
+
+ public function __construct($session)
+ {
+ parent::__construct($session) ;
+
+ $this->description = "Deprecated websocket protocol (IETF drafts 'hixie-76' or 'hybi-00')" ;
+ }
+
+ public function onHandshake()
+ {
+ if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_KEY1']) || !isset($this->session->server['HTTP_SEC_WEBSOCKET_KEY2']))
+ {
+ return FALSE ;
+ }
+
+ return TRUE ;
+ }
+
+ /**
+ * Returns handshaked data for reply
+ * @param string Received data (no use in this class)
+ * @return string Handshaked data
+ */
+
+ public function getHandshakeReply($data)
+ {
+ if ($this->onHandshake())
+ {
+ $final_key = $this->_computeFinalKey($this->session->server['HTTP_SEC_WEBSOCKET_KEY1'], $this->session->server['HTTP_SEC_WEBSOCKET_KEY2'], $data) ;
+
+ if (!$final_key)
+ {
+ return FALSE ;
+ }
+
+ if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN']))
+ {
+ $this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = '' ;
+ }
+
+ $reply = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
+ . "Upgrade: WebSocket\r\n"
+ . "Connection: Upgrade\r\n"
+ . "Sec-WebSocket-Origin: " . $this->session->server['HTTP_ORIGIN'] . "\r\n"
+ . "Sec-WebSocket-Location: ws://" . $this->session->server['HTTP_HOST'] . $this->session->server['REQUEST_URI'] . "\r\n" ;
+
+ if (isset($this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL']))
+ {
+ $reply .= "Sec-WebSocket-Protocol: " . $this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL'] . "\r\n" ;
+ }
+
+ $reply .= "\r\n" ;
+ $reply .= $final_key ;
+
+ return $reply ;
+ }
+
+ return FALSE ;
+ }
+
+ /**
+ * Computes final key for Sec-WebSocket.
+ * @param string Key1
+ * @param string Key2
+ * @param string Data
+ * @return string Result
+ */
+
+ protected function _computeFinalKey($key1, $key2, $data)
+ {
+ if (strlen($data) < 8)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Invalid handshake data for client "' . $this->session->clientAddr . '"') ;
+ return FALSE ;
+ }
+
+ $bodyData = binarySubstr($data, 0, 8) ;
+
+ return md5($this->_computeKey($key1) . $this->_computeKey($key2) . binarySubstr($data, 0, 8), TRUE) ;
+ }
+
+ /**
+ * Computes key for Sec-WebSocket.
+ * @param string Key
+ * @return string Result
+ */
+
+ protected function _computeKey($key)
+ {
+ $spaces = 0;
+ $digits = '';
+
+ for ($i = 0, $s = strlen($key); $i < $s; ++$i) {
+ $c = binarySubstr($key, $i, 1);
+
+ if ($c === "\x20") {
+ ++$spaces;
+ }
+ elseif (ctype_digit($c)) {
+ $digits .= $c;
+ }
+ }
+
+ if ($spaces > 0) {
+ $result = (float) floor($digits / $spaces);
+ } else {
+ $result = (float) $digits;
+ }
+
+ return pack('N', $result);
+ }
+
+ protected function _dataEncode($data, $type)
+ {
+ // Binary
+ if (($type & self::BINARY) === self::BINARY)
+ {
+ $n = strlen($data);
+ $len = '';
+ $pos = 0;
+
+ char:
+
+ ++$pos;
+ $c = $n >> 0 & 0x7F;
+ $n = $n >> 7;
+
+ if ($pos != 1)
+ {
+ $c += 0x80;
+ }
+
+ if ($c != 0x80)
+ {
+ $len = chr($c) . $len;
+ goto char;
+ };
+
+ return chr(self::BINARY) . $len . $data ;
+ }
+ // String
+ else
+ {
+ return chr(self::STRING) . $data . "\xFF" ;
+ }
+ }
+
+ protected function _dataDecode($data)
+ {
+ $decodedData = '' ;
+
+ while (($buflen = strlen($data)) >= 2)
+ {
+ $frametype = ord(binarySubstr($data, 0, 1)) ;
+
+ if (($frametype & 0x80) === 0x80)
+ {
+ $len = 0 ;
+ $i = 0 ;
+
+ do {
+ $b = ord(binarySubstr($data, ++$i, 1)) ;
+ $n = $b & 0x7F ;
+ $len *= 0x80 ;
+ $len += $n ;
+ } while ($b > 0x80) ;
+
+ if ($this->session->appInstance->config->maxallowedpacket->value <= $len)
+ {
+ // Too big packet
+ $this->session->finish() ;
+ return FALSE ;
+ }
+
+ if ($buflen < $len + 2)
+ {
+ // not enough data yet
+ return FALSE ;
+ }
+
+ $decodedData .= binarySubstr($data, 2, $len) ;
+ $data = binarySubstr($data, 2 + $len) ;
+// $this->onFrame($decodedData, $frametype);
+ }
+ else
+ {
+ if (($p = strpos($data, "\xFF")) !== FALSE)
+ {
+ if ($this->session->appInstance->config->maxallowedpacket->value <= $p - 1)
+ {
+ // Too big packet
+ $this->session->finish() ;
+ return FALSE ;
+ }
+
+ $decodedData .= binarySubstr($data, 1, $p - 1) ;
+ $data = binarySubstr($data, $p + 1) ;
+// $this->onFrame($decodedData, $frametype);
+ }
+ else
+ {
+ // not enough data yet
+ if ($this->session->appInstance->config->maxallowedpacket->value <= strlen($data))
+ {
+ // Too big packet
+ $this->session->finish() ;
+ return FALSE ;
+ }
+
+ return $decodedData ;
+ }
+ }
+ }
+
+ if ($this->session->appInstance->config->maxallowedpacket->value <= strlen($decodedData))
+ {
+ // Too big packet
+ $this->session->finish() ;
+ return FALSE ;
+ }
+
+ return $decodedData ;
+ }
+}
View
184 lib/WebSocketProtocolV8.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * Websocket protocol hybi-10
+ * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
+ */
+
+class WebSocketProtocolV8 extends WebSocketProtocol
+{
+ // @todo manage only the 4 last bits (opcode), as described in the draft
+ const STRING = 0x81;
+ const BINARY = 0x82;
+
+ public function __construct($session)
+ {
+ parent::__construct($session) ;
+
+ $this->description = "Websocket protocol version " . $this->session->server['HTTP_SEC_WEBSOCKET_VERSION'] . " (IETF draft 'hybi-10')" ;
+ }
+
+ public function onHandshake()
+ {
+ if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_KEY']) || !isset($this->session->server['HTTP_SEC_WEBSOCKET_VERSION']) || ($this->session->server['HTTP_SEC_WEBSOCKET_VERSION'] != 8))
+ {
+ return FALSE ;
+ }
+
+ return TRUE ;
+ }
+
+ /**
+ * Returns handshaked data for reply
+ * @param string Received data (no use in this class)
+ * @return string Handshaked data
+ */
+
+ public function getHandshakeReply($data)
+ {
+ if ($this->onHandshake())
+ {
+ if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN']))
+ {
+ $this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = '' ;
+ }
+
+ $reply = "HTTP/1.1 101 Switching Protocols\r\n"
+ . "Upgrade: websocket\r\n"
+ . "Connection: Upgrade\r\n"
+ . "Sec-WebSocket-Origin: " . $this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN'] . "\r\n"
+ . "Sec-WebSocket-Location: ws://" . $this->session->server['HTTP_HOST'] . $this->session->server['REQUEST_URI'] . "\r\n"
+ . "Sec-WebSocket-Accept: " . base64_encode(sha1(trim($this->session->server['HTTP_SEC_WEBSOCKET_KEY']) . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)) . "\r\n" ;
+
+ if (isset($this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL']))
+ {
+ $reply .= "Sec-WebSocket-Protocol: " . $this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL'] . "\r\n" ;
+ }
+
+ $reply .= "\r\n" ;
+
+ return $reply ;
+ }
+
+ return FALSE ;
+ }
+
+ /**
+ * Data encoding, according to related IETF draft
+ *
+ * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#page-16
+ */
+
+ protected function _dataEncode($decodedData, $type = NULL)
+ {
+ $frames = array() ;
+ $maskingKeys = chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) ;
+ $frames[0] = ($type === NULL) ? $this->getFrameType("STRING") : $this->getFrameType($type) ;
+ $dataLength = strlen($decodedData) ;
+
+ if ($dataLength <= 125)
+ {
+ $frames[1] = $dataLength + 128 ;
+ }
+ elseif ($dataLength <= 65535)
+ {
+ $frames[1] = 254 ; // 126 + 128
+ $frames[2] = $dataLength >> 8 ;
+ $frames[3] = $dataLength & 0xFF ;
+ }
+ else
+ {
+ $frames[1] = 255 ; // 127 + 128
+ $frames[2] = $dataLength >> 56 ;
+ $frames[3] = $dataLength >> 48 ;
+ $frames[4] = $dataLength >> 40 ;
+ $frames[5] = $dataLength >> 32 ;
+ $frames[6] = $dataLength >> 24 ;
+ $frames[7] = $dataLength >> 16 ;
+ $frames[8] = $dataLength >> 8 ;
+ $frames[9] = $dataLength & 0xFF ;
+ }
+
+ $maskingFunc = function($data, $mask)
+ {
+ for ($i = 0, $l = strlen($data); $i < $l; $i++)
+ {
+ // Avoid storing a new copy of $data...
+ $data[$i] = $data[$i] ^ $mask[$i % 4] ;
+ }
+
+ return $data ;
+ } ;
+
+ return implode('', array_map('chr', $frames)) . $maskingKeys . $maskingFunc($decodedData, $maskingKeys) ;
+ }
+
+ /**
+ * Data decoding, according to related IETF draft
+ *
+ * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#page-16
+ */
+
+ protected function _dataDecode($encodedData)
+ {
+ $isMasked = (bool) (ord($encodedData[1]) >> 7) ;
+ $dataLength = ord($encodedData[1]) & 127 ;
+
+ if ($isMasked)
+ {
+ $unmaskingFunc = function($data, $mask)
+ {
+ for ($i = 0, $l = strlen($data); $i < $l; $i++)
+ {
+ // Avoid storing a new copy of $data...
+ $data[$i] = $data[$i] ^ $mask[$i % 4] ;
+ }
+
+ return $data ;
+ } ;
+
+ if ($dataLength === 126)
+ {
+ $maskingKey = binarySubstr($encodedData, 4, 4) ;
+ $extDataLength = hexdec(sprintf('%02x%02x', ord($encodedData[2]), ord($encodedData[3]))) ;
+ $offsetStart = 8 ;
+ }
+ elseif ($dataLength === 127)
+ {
+ $maskingKey = binarySubstr($encodedData, 10, 4) ;
+ $extDataLength = hexdec(sprintf('%02x%02x%02x%02x%02x%02x%02x%02x', ord($encodedData[2]), ord($encodedData[3]), ord($encodedData[4]), ord($encodedData[5]), ord($encodedData[6]), ord($encodedData[7]), ord($encodedData[8]), ord($encodedData[9]))) ;
+ $offsetStart = 14 ;
+ }
+ else
+ {
+ $maskingKey = binarySubstr($encodedData, 2, 4) ;
+ $extDataLength = $dataLength ;
+ $offsetStart = 6 ;
+ }
+
+ if (strlen($encodedData) < $offsetStart + $extDataLength)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Incorrect data size in frame decoding for client "' . $this->session->clientAddr . '"') ;
+ }
+
+ return $unmaskingFunc(binarySubstr($encodedData, $offsetStart, $extDataLength), $maskingKey) ;
+ }
+ else
+ {
+ if ($dataLength === 126)
+ {
+ return binarySubstr($encodedData, 4) ;
+ }
+ elseif ($dataLength === 127)
+ {
+ return binarySubstr($encodedData, 10) ;
+ }
+ else
+ {
+ return binarySubstr($encodedData, 2) ;
+ }
+ }
+
+ return NULL ;
+ }
+}
View
365 lib/genericWebSocketSession.php
@@ -0,0 +1,365 @@
+<?php
+/*
+require_once("./lib/SocketSession.class.php") ;
+require_once("./applications/WebSocketProtocolV0.php") ;
+require_once("./applications/WebSocketProtocolV8.php") ;
+*/
+class genericWebSocketSession extends SocketSession {
+
+ public $secprotocol;
+ public $resultKey;
+ public $handshaked = FALSE;
+ public $upstream;
+ public $server = array();
+ public $cookie = array();
+ public $firstline = FALSE;
+ public $writeReady = TRUE;
+ public $callbacks = array();
+
+ public $protocol; // Related WebSocket protocol
+
+ public function init()
+ {
+ }
+
+ /**
+ * Sends a frame.
+ * @param string Frame's data.
+ * @param string Frame's type. ("STRING" OR "BINARY")
+ * @param callback Optional. Callback called when the frame is received by client.
+ * @return boolean Success.
+ */
+
+ public function sendFrame($data, $type = NULL, $callback = NULL)
+ {
+ if (!$this->handshaked)
+ {
+ return FALSE;
+ }
+
+ if (!isset($this->protocol))
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client ' . $this->clientAddr) ;
+ return FALSE ;
+ }
+
+// $this->write($this->protocol->dataEncode($data, $type)) ;
+ $this->protocol->sendFrame($data, $type) ;
+ $this->writeReady = FALSE;
+
+ if ($callback)
+ {
+ $this->callbacks[] = $callback;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Event of SocketSession (asyncServer).
+ * @return void
+ */
+
+ public function onFinish()
+ {
+ if (Daemon::$config->logevents->value)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
+ }
+
+ if (isset($this->upstream))
+ {
+ $this->upstream->onFinish();
+ }
+
+ unset($this->upstream);
+ unset($this->appInstance->sessions[$this->connId]);
+ }
+
+ /**
+ * Called when new frame received.
+ * @param string Frame's data.
+ * @param string Frame's type ("STRING" OR "BINARY").
+ * @return boolean Success.
+ */
+
+ public function onFrame($data, $type)
+ {
+ if (Daemon::$config->logevents->value)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
+ }
+
+ if (!isset($this->upstream))
+ {
+ return FALSE;
+ }
+
+ $this->upstream->onFrame($data, $type);
+
+ return TRUE;
+ }
+
+ /**
+ * Called when the connection is ready to accept new data.
+ * @return void
+ */
+
+ public function onWrite()
+ {
+ if (Daemon::$config->logevents->value)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
+ }
+
+ $this->writeReady = TRUE;
+
+ for ($i = 0, $s = sizeof($this->callbacks); $i < $s; ++$i)
+ {
+ call_user_func(array_shift($this->callbacks), $this);
+ }
+
+ if (is_callable(array($this->upstream, 'onWrite')))
+ {
+ $this->upstream->onWrite();
+ }
+ }
+
+ /**
+ * Called when the connection is handshaked.
+ * @return boolean Ready to handshake ?
+ */
+
+ public function onHandshake()
+ {
+ if (Daemon::$config->logevents->value)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
+ }
+
+ $e = explode('/', $this->server['DOCUMENT_URI']);
+ $appName = isset($e[1])?$e[1]:'';
+
+ if (!isset($this->appInstance->routes[$appName]))
+ {
+ if (Daemon::$config->logerrors->value)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : undefined route "' . $appName . '" for client "' . $this->clientAddr . '"');
+ }
+
+ return FALSE;
+ }
+
+ if (!$this->upstream = call_user_func($this->appInstance->routes[$appName], $this))
+ {
+ return FALSE;
+ }
+
+ if (!isset($this->protocol))
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client "' . $this->clientAddr . '"') ;
+ return FALSE ;
+ }
+
+ if ($this->protocol->onHandshake() === FALSE)
+ {
+ return FALSE ;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Event of SocketSession (AsyncServer). Called when the worker is going to shutdown.
+ * @return boolean Ready to shutdown ?
+ */
+
+ public function gracefulShutdown()
+ {
+ if ((!$this->upstream) || $this->upstream->gracefulShutdown())
+ {
+ $this->finish();
+
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * Called when we're going to handshake.
+ * @return boolean Handshake status
+ */
+
+ public function handshake($data)
+ {
+ $this->handshaked = TRUE;
+
+ if (!$this->onHandshake())
+ {
+ $this->finish() ;
+ return FALSE ;
+ }
+
+ if (!isset($this->protocol))
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client "' . $this->clientAddr . '"') ;
+ $this->finish() ;
+ }
+
+ // Handshaking...
+ $handshake = $this->protocol->getHandshakeReply($data) ;
+
+ if (!$handshake)
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Handshake protocol failure for client "' . $this->clientAddr . '"') ;
+ $this->finish() ;
+ return FALSE ;
+ }
+
+ if ($this->write($handshake))
+ {
+ if (is_callable(array($this->upstream, 'onHandshake')))
+ {
+ $this->upstream->onHandshake();
+ }
+ }
+ else
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Handshake send failure for client "' . $this->clientAddr . '"') ;
+ $this->finish() ;
+ return FALSE ;
+ }
+
+ return TRUE ;
+ }
+
+ /**
+ * Event of SocketSession (AsyncServer). Called when new data received.
+ * @param string New received data.
+ * @return void
+ */
+
+ public function stdin($buf)
+ {
+ $this->buf .= $buf;
+
+ if (!$this->handshaked)
+ {
+/*
+ if (Daemon::$appResolver->checkAppEnabled('FlashPolicy'))
+ {
+ if (strpos($this->buf, '<policy-file-request/>') !== FALSE) {
+ if (
+ ($FP = Daemon::$appResolver->getInstanceByAppName('FlashPolicy'))
+ && $FP->policyData
+ ) {
+ $this->write($FP->policyData . "\x00");
+ }
+
+ $this->finish();
+
+ return;
+ }
+ }
+*/
+ $i = 0;
+
+ while (($l = $this->gets()) !== FALSE)
+ {
+ if ($i++ > 100)
+ {
+ break;
+ }
+
+ if ($l === "\r\n")
+ {
+ if (
+ !isset($this->server['HTTP_CONNECTION'])
+ || (!preg_match('/upgrade/i', $this->server['HTTP_CONNECTION'])) // "Upgrade" is not always alone (ie. "Connection: Keep-alive, Upgrade")
+ || !isset($this->server['HTTP_UPGRADE'])
+ || (strtolower($this->server['HTTP_UPGRADE']) !== 'websocket') // Lowercase compare important
+ ) {
+ $this->finish();
+ return;
+ }
+
+ if (isset($this->server['HTTP_COOKIE']))
+ {
+ HTTPRequest::parse_str(strtr($this->server['HTTP_COOKIE'], HTTPRequest::$hvaltr), $this->cookie);
+ }
+
+ // ----------------------------------------------------------
+ // Protocol discovery, based on HTTP headers...
+ // ----------------------------------------------------------
+ if (isset($this->server['HTTP_SEC_WEBSOCKET_VERSION'])) // HYBI
+ {
+ if ($this->server['HTTP_SEC_WEBSOCKET_VERSION'] == 8) // At the moment, managing only version 8 (FF7, Chrome14)
+ {
+ $this->protocol = new WebSocketProtocolV8($this) ;
+ }
+ else
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . " : Websocket protocol version " . $this->server['HTTP_SEC_WEBSOCKET_VERSION'] . ' is not yet supported for client "' . $this->clientAddr . '"') ;
+
+ $this->finish() ;
+ }
+ }
+ else // Defaulting to HIXIE (Safari5 and many non-browser clients...)
+ {
+ $this->protocol = new WebSocketProtocolV0($this) ;
+ }
+ // ----------------------------------------------------------
+ // End of protocol discovery
+ // ----------------------------------------------------------
+
+ break;
+ }
+
+ if (!$this->firstline)
+ {
+ $this->firstline = TRUE;
+ $e = explode(' ',$l);
+ $u = parse_url(isset($e[1])?$e[1]:'');
+
+ $this->server['REQUEST_METHOD'] = $e[0];
+ $this->server['REQUEST_URI'] = $u['path'] . (isset($u['query']) ? '?' . $u['query'] : '');
+ $this->server['DOCUMENT_URI'] = $u['path'];
+ $this->server['PHP_SELF'] = $u['path'];
+ $this->server['QUERY_STRING'] = isset($u['query']) ? $u['query'] : NULL;
+ $this->server['SCRIPT_NAME'] = $this->server['DOCUMENT_URI'] = isset($u['path']) ? $u['path'] : '/';
+ $this->server['SERVER_PROTOCOL'] = isset($e[2]) ? trim($e[2]) : '';
+
+ list($this->server['REMOTE_ADDR'],$this->server['REMOTE_PORT']) = explode(':', $this->clientAddr);
+ }
+ else
+ {
+ $e = explode(': ', $l);
+
+ if (isset($e[1]))
+ {
+ $this->server['HTTP_' . strtoupper(strtr($e[0], HTTPRequest::$htr))] = rtrim($e[1], "\r\n");
+ }
+ }
+ }
+ }
+
+ if ($this->handshaked)
+ {
+ if (!isset($this->protocol))
+ {
+ Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client "' . $this->clientAddr . '"') ;
+ $this->finish() ;
+ return ;
+ }
+
+// $this->onFrame($this->protocol->dataDecode($this->buf), genericWebSocketServer::STRING) ;
+// $this->buf = "" ;
+ $this->protocol->recvFrame($this->buf, "STRING") ;
+ }
+ else
+ {
+ $this->handshake($this->buf);
+ }
+ }
+}

0 comments on commit a8e4ec0

Please sign in to comment.