Skip to content
This repository
Browse code

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...
commit a8e4ec05b183dd610fc7137274a1b0a3244c0d93 1 parent 7485d24
Erika31 Erika31 authored
99 app-examples/ExampleGenericWebSocket.php
... ... @@ -0,0 +1,99 @@
  1 +<?php
  2 +
  3 +define("EXTERNAL_APP_CLASSES_DIR", dirname(__FILE__) . "/../../../app/") ;
  4 +
  5 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Base_Class.class.php") ;
  6 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/DB_Connection.class.php") ;
  7 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/DB_GenericTable.class.php") ;
  8 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Tools.class.php") ;
  9 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Crypto.class.php") ;
  10 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/Session.class.php") ;
  11 +require_once(EXTERNAL_APP_CLASSES_DIR . "/modules/app/classes/wsServer.class.php") ;
  12 +
  13 +class ExampleGenericWebSocket extends AppInstance {
  14 +
  15 + /**
  16 + * Called when the worker is ready to go.
  17 + * @return void
  18 + */
  19 + public function onReady() {
  20 + if ($this->WS = Daemon::$appResolver->getInstanceByAppName('genericWebSocketServer')) {
  21 + $this->WS->addRoute('exampleApp', function ($client) {
  22 + return new ExampleGenericWebSocketRoute($client);
  23 + }
  24 + );
  25 +
  26 + // If you want to manage timeout events :
  27 + $appInstance = $this ;
  28 + $this->WS->registerEventTimeout(function() use ($appInstance) {
  29 + return $appInstance->onEventTimeout() ;
  30 + }
  31 + ) ;
  32 + }
  33 + }
  34 +
  35 + /**
  36 + * Called when a timeout event is raised
  37 + * @return void
  38 + */
  39 + public function onEventTimeout() {
  40 +
  41 + }
  42 +
  43 + /**
  44 + * Called when application instance is going to shutdown
  45 + * @todo protected?
  46 + * @return boolean Ready to shutdown?
  47 + */
  48 + public function onShutdown() {
  49 + return TRUE;
  50 + }
  51 +
  52 +}
  53 +
  54 +class ExampleGenericWebSocketRoute extends WebSocketRoute {
  55 +
  56 + /**
  57 + * Called when new frame received.
  58 + * @param string Frame's contents.
  59 + * @param string Frame's type. ("STRING" or "BINARY")
  60 + * @return void
  61 + */
  62 + public function onFrame($data, $type) {
  63 + if ($data === 'ping') {
  64 + $this->client->sendFrame('pong', "STRING", function($client) {
  65 + Daemon::log($client->clientAddr . ' : SEND pong');
  66 + }
  67 + );
  68 + return ;
  69 + }
  70 + }
  71 +
  72 + /**
  73 + * Called when the connection is handshaked.
  74 + * @return void
  75 + */
  76 + public function onHandshake() {
  77 +
  78 + Daemon::log($this->client->clientAddr . ' : Handshake success') ;
  79 + }
  80 +
  81 + /**
  82 + * Called when session finished.
  83 + * @return void
  84 + */
  85 + public function onFinish() {
  86 +
  87 + Daemon::log($this->client->clientAddr . ' : Disconnected');
  88 + }
  89 +
  90 + /**
  91 + * Called when the worker is going to shutdown.
  92 + * @return boolean Ready to shutdown?
  93 + */
  94 + public function gracefulShutdown() {
  95 +
  96 + Daemon::log($this->client->clientAddr . ' : Gracefully disconnecting (as requested by server)') ;
  97 + return TRUE ;
  98 + }
  99 +}
267 app-servers/genericWebSocketServer.php
... ... @@ -0,0 +1,267 @@
  1 +<?php
  2 +
  3 +class genericWebSocketServer extends AsyncServer
  4 +{
  5 + public $sessions = array();
  6 + public $routes = array();
  7 +
  8 + protected $timeout_cb;
  9 +
  10 + const BINARY = 0x80;
  11 + const STRING = 0x00;
  12 +
  13 + /**
  14 + * Registering event timeout callback function
  15 + * @param Closure Callback function
  16 + * @return void
  17 + */
  18 +
  19 + public function registerEventTimeout($cb)
  20 + {
  21 + if ($cb === NULL || is_callable($cb))
  22 + {
  23 + $this->timeout_cb = $cb ;
  24 + }
  25 + }
  26 +
  27 + /**
  28 + * Setting default config options
  29 + * Overriden from AppInstance::getConfigDefaults
  30 + * @return array|false
  31 + */
  32 +
  33 + protected function getConfigDefaults()
  34 + {
  35 + return array(
  36 + // listen to
  37 + 'listen' => 'tcp://0.0.0.0',
  38 + // listen port
  39 + 'listenport' => 55556,
  40 + // max allowed packet size
  41 + 'maxallowedpacket' => new Daemon_ConfigEntrySize('16k'),
  42 + // disabled by default
  43 + 'enable' => 0,
  44 + // no event_timeout by default
  45 + 'ev_timeout' => -1
  46 + );
  47 + }
  48 +
  49 + /**
  50 + * Event of appInstance. Adds default settings and binds sockets.
  51 + * @return void
  52 + */
  53 +
  54 + public function init()
  55 + {
  56 + $this->update();
  57 +
  58 + if ($this->config->enable->value)
  59 + {
  60 + $this->bindSockets(
  61 + $this->config->listen->value,
  62 + $this->config->listenport->value
  63 + );
  64 + }
  65 + }
  66 +
  67 + /**
  68 + * Enable all events of sockets
  69 + * @return void
  70 + */
  71 +
  72 + public function enableSocketEvents()
  73 + {
  74 + foreach ($this->socketEvents as $ev)
  75 + {
  76 + event_base_set($ev, Daemon::$process->eventBase);
  77 + event_add($ev, $this->config->ev_timeout->value); // With specified timeout
  78 + }
  79 + }
  80 +
  81 + /**
  82 + * Called when a request to HTTP-server looks like WebSocket handshake query.
  83 + * @return void
  84 + */
  85 +/*
  86 + public function inheritFromRequest($req, $appInstance)
  87 + {
  88 + $connId = $req->attrs->connId;
  89 +
  90 + unset(Daemon::$process->queue[$connId . '-' . $req->attrs->id]);
  91 +
  92 + $this->buf[$connId] = $appInstance->buf[$connId];
  93 +
  94 + unset($appInstance->buf[$connId]);
  95 + unset($appInstance->poolState[$connId]);
  96 +
  97 + $set = event_buffer_set_callback(
  98 + $this->buf[$connId],
  99 + $this->directReads ? NULL : array($this, 'onReadEvent'),
  100 + array($this, 'onWriteEvent'),
  101 + array($this, 'onFailureEvent'),
  102 + array($connId)
  103 + );
  104 +
  105 + unset(Daemon::$process->readPoolState[$connId]);
  106 +
  107 + $this->poolState[$connId] = array();
  108 +
  109 + $this->sessions[$connId] = new genericWebSocketSession($connId, $this);
  110 + $this->sessions[$connId]->clientAddr = $req->attrs->server['REMOTE_ADDR'];
  111 + $this->sessions[$connId]->server = $req->attrs->server;
  112 + $this->sessions[$connId]->firstline = TRUE;
  113 + $this->sessions[$connId]->stdin("\r\n" . $req->attrs->inbuf);
  114 + }
  115 +*/
  116 + /**
  117 + * Adds a route if it doesn't exist already.
  118 + * @param string Route name.
  119 + * @param mixed Route's callback.
  120 + * @return boolean Success.
  121 + */
  122 +
  123 + public function addRoute($route, $cb)
  124 + {
  125 + if (isset($this->routes[$route]))
  126 + {
  127 + Daemon::log(__METHOD__ . ' Route \'' . $route . '\' is already taken.');
  128 + return FALSE;
  129 + }
  130 +
  131 + $this->routes[$route] = $cb;
  132 +
  133 + return TRUE;
  134 + }
  135 +
  136 + /**
  137 + * Force add/replace a route.
  138 + * @param string Route name.
  139 + * @param mixed Route's callback.
  140 + * @return boolean Success.
  141 + */
  142 +
  143 + public function setRoute($route, $cb)
  144 + {
  145 + $this->routes[$route] = $cb;
  146 +
  147 + return TRUE;
  148 + }
  149 +
  150 + /**
  151 + * Removes a route.
  152 + * @param string Route name.
  153 + * @return boolean Success.
  154 + */
  155 +
  156 + public function removeRoute($route)
  157 + {
  158 + if (!isset($this->routes[$route]))
  159 + {
  160 + return FALSE;
  161 + }
  162 +
  163 + unset($this->routes[$route]);
  164 +
  165 + return TRUE;
  166 + }
  167 +
  168 + /**
  169 + * Event of appInstance.
  170 + * @return void
  171 + */
  172 +
  173 + public function onReady()
  174 + {
  175 + if ($this->config->enable->value)
  176 + {
  177 + $this->enableSocketEvents();
  178 + }
  179 + }
  180 +
  181 + /**
  182 + * Called when remote host is trying to establish the connection
  183 + * @param resource Descriptor
  184 + * @param integer Events
  185 + * @param mixed Attached variable
  186 + * @return boolean If true then we can accept new connections, else we can't
  187 + */
  188 +
  189 + public function checkAccept($stream, $events, $arg)
  190 + {
  191 + if (!parent::checkAccept($stream, $events, $arg))
  192 + {
  193 + return FALSE;
  194 + }
  195 +
  196 + $sockId = $arg[0];
  197 +
  198 + event_add($this->socketEvents[$sockId], $this->config->ev_timeout->value) ; // With specified timeout
  199 +
  200 + // Always return FALSE to skip adding event without timeout in "parent::onAcceptEvent"...
  201 + return FALSE ;
  202 + }
  203 +
  204 + /**
  205 + * Called when remote host is trying to establish the connection
  206 + * @param integer Connection's ID
  207 + * @param string Address
  208 + * @return boolean Accept/Drop the connection
  209 + */
  210 +
  211 + public function onAccept($connId, $addr)
  212 + {
  213 + if (parent::onAccept($connId, $addr))
  214 + {
  215 + Daemon::log("New client : " . $addr) ;
  216 +
  217 + return TRUE ;
  218 + }
  219 +
  220 + return FALSE ;
  221 + }
  222 +
  223 + /**
  224 + * Event of asyncServer
  225 + * @param integer Connection's ID
  226 + * @param string Peer's address
  227 + * @return void
  228 + */
  229 + protected function onAccepted($connId, $addr)
  230 + {
  231 + $this->sessions[$connId] = new genericWebSocketSession($connId, $this);
  232 + $this->sessions[$connId]->clientAddr = $addr;
  233 + }
  234 +
  235 + /**
  236 + * Called when new connections is waiting for accept
  237 + * @param resource Descriptor
  238 + * @param integer Events
  239 + * @param mixed Attached variable
  240 + * @return void
  241 + */
  242 +
  243 + public function onAcceptEvent($stream, $events, $arg)
  244 + {
  245 + if ($events & EV_TIMEOUT)
  246 + {
  247 + $sockId = $arg[0];
  248 +
  249 + if ($this->timeout_cb !== NULL)
  250 + {
  251 + call_user_func($this->timeout_cb) ;
  252 + }
  253 +
  254 + event_add($this->socketEvents[$sockId], $this->config->ev_timeout->value) ;
  255 + return ;
  256 + }
  257 +
  258 + parent::onAcceptEvent($stream, $events, $arg);
  259 + }
  260 +/*
  261 + public function onTimeout()
  262 + {
  263 +
  264 + }
  265 +*/
  266 +}
  267 +
7 lib/AsyncServer.php
@@ -329,9 +329,12 @@ public function onAccept($connId, $addr) {
329 329
330 330 /**
331 331 * Called when remote host is trying to establish the connection
  332 + * @param resource Descriptor
  333 + * @param integer Events
  334 + * @param mixed Attached variable
332 335 * @return boolean If true then we can accept new connections, else we can't
333 336 */
334   - public function checkAccept() {
  337 + public function checkAccept($stream, $events, $arg) {
335 338 if (Daemon::$process->reload) {
336 339 return FALSE;
337 340 }
@@ -505,7 +508,7 @@ public function onAcceptEvent($stream, $events, $arg) {
505 508 Daemon::$process->log(get_class($this) . '::' . __METHOD__ . '(' . $sockId . ') invoked.');
506 509 }
507 510
508   - if ($this->checkAccept()) {
  511 + if ($this->checkAccept($stream, $events, $arg)) {
509 512 event_add($this->socketEvents[$sockId]);
510 513 }
511 514
76 lib/WebSocketProtocol.php
... ... @@ -0,0 +1,76 @@
  1 +<?php
  2 +
  3 +/**
  4 + * Websocket protocol abstract class
  5 + */
  6 +
  7 +abstract class WebSocketProtocol
  8 +{
  9 + public $description ;
  10 + protected $session ;
  11 +
  12 + const STRING = NULL;
  13 + const BINARY = NULL;
  14 +
  15 + public function __construct($session)
  16 + {
  17 + $this->session = $session ;
  18 + }
  19 +
  20 + public function getFrameType($type)
  21 + {
  22 + $frametype = @constant(get_class($this) .'::' . $type) ;
  23 +
  24 + if ($frametype === NULL)
  25 + {
  26 + Daemon::log(__METHOD__ . ' : Undefined frametype "' . $type . '"') ;
  27 + }
  28 +
  29 + return $frametype ;
  30 + }
  31 +
  32 + public function onHandshake()
  33 + {
  34 + return TRUE ;
  35 + }
  36 +
  37 + public function sendFrame($data, $type)
  38 + {
  39 + $this->session->write($this->_dataEncode($data, $type)) ;
  40 + }
  41 +
  42 + public function recvFrame($data, $type)
  43 + {
  44 + $this->session->onFrame($this->_dataDecode($data), $type) ;
  45 + $this->session->buf = "" ;
  46 + }
  47 +
  48 + /**
  49 + * Returns handshaked data for reply
  50 + * @param string Received data (no use in this class)
  51 + * @return string Handshaked data
  52 + */
  53 +
  54 + public function getHandshakeReply($data)
  55 + {
  56 + return FALSE ;
  57 + }
  58 +
  59 + /**
  60 + * Data encoding
  61 + */
  62 +
  63 + protected function _dataEncode($decodedData, $type = NULL)
  64 + {
  65 + return NULL ;
  66 + }
  67 +
  68 + /**
  69 + * Data decoding
  70 + */
  71 +
  72 + protected function _dataDecode($encodedData)
  73 + {
  74 + return NULL ;
  75 + }
  76 +}
235 lib/WebSocketProtocolV0.php
... ... @@ -0,0 +1,235 @@
  1 +<?php
  2 +
  3 +/**
  4 + * Websocket protocol hixie-76
  5 + * @see http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
  6 + */
  7 +
  8 +class WebSocketProtocolV0 extends WebSocketProtocol
  9 +{
  10 + const STRING = 0x00;
  11 + const BINARY = 0x80;
  12 +
  13 + public function __construct($session)
  14 + {
  15 + parent::__construct($session) ;
  16 +
  17 + $this->description = "Deprecated websocket protocol (IETF drafts 'hixie-76' or 'hybi-00')" ;
  18 + }
  19 +
  20 + public function onHandshake()
  21 + {
  22 + if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_KEY1']) || !isset($this->session->server['HTTP_SEC_WEBSOCKET_KEY2']))
  23 + {
  24 + return FALSE ;
  25 + }
  26 +
  27 + return TRUE ;
  28 + }
  29 +
  30 + /**
  31 + * Returns handshaked data for reply
  32 + * @param string Received data (no use in this class)
  33 + * @return string Handshaked data
  34 + */
  35 +
  36 + public function getHandshakeReply($data)
  37 + {
  38 + if ($this->onHandshake())
  39 + {
  40 + $final_key = $this->_computeFinalKey($this->session->server['HTTP_SEC_WEBSOCKET_KEY1'], $this->session->server['HTTP_SEC_WEBSOCKET_KEY2'], $data) ;
  41 +
  42 + if (!$final_key)
  43 + {
  44 + return FALSE ;
  45 + }
  46 +
  47 + if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN']))
  48 + {
  49 + $this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = '' ;
  50 + }
  51 +
  52 + $reply = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
  53 + . "Upgrade: WebSocket\r\n"
  54 + . "Connection: Upgrade\r\n"
  55 + . "Sec-WebSocket-Origin: " . $this->session->server['HTTP_ORIGIN'] . "\r\n"
  56 + . "Sec-WebSocket-Location: ws://" . $this->session->server['HTTP_HOST'] . $this->session->server['REQUEST_URI'] . "\r\n" ;
  57 +
  58 + if (isset($this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL']))
  59 + {
  60 + $reply .= "Sec-WebSocket-Protocol: " . $this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL'] . "\r\n" ;
  61 + }
  62 +
  63 + $reply .= "\r\n" ;
  64 + $reply .= $final_key ;
  65 +
  66 + return $reply ;
  67 + }
  68 +
  69 + return FALSE ;
  70 + }
  71 +
  72 + /**
  73 + * Computes final key for Sec-WebSocket.
  74 + * @param string Key1
  75 + * @param string Key2
  76 + * @param string Data
  77 + * @return string Result
  78 + */
  79 +
  80 + protected function _computeFinalKey($key1, $key2, $data)
  81 + {
  82 + if (strlen($data) < 8)
  83 + {
  84 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Invalid handshake data for client "' . $this->session->clientAddr . '"') ;
  85 + return FALSE ;
  86 + }
  87 +
  88 + $bodyData = binarySubstr($data, 0, 8) ;
  89 +
  90 + return md5($this->_computeKey($key1) . $this->_computeKey($key2) . binarySubstr($data, 0, 8), TRUE) ;
  91 + }
  92 +
  93 + /**
  94 + * Computes key for Sec-WebSocket.
  95 + * @param string Key
  96 + * @return string Result
  97 + */
  98 +
  99 + protected function _computeKey($key)
  100 + {
  101 + $spaces = 0;
  102 + $digits = '';
  103 +
  104 + for ($i = 0, $s = strlen($key); $i < $s; ++$i) {
  105 + $c = binarySubstr($key, $i, 1);
  106 +
  107 + if ($c === "\x20") {
  108 + ++$spaces;
  109 + }
  110 + elseif (ctype_digit($c)) {
  111 + $digits .= $c;
  112 + }
  113 + }
  114 +
  115 + if ($spaces > 0) {
  116 + $result = (float) floor($digits / $spaces);
  117 + } else {
  118 + $result = (float) $digits;
  119 + }
  120 +
  121 + return pack('N', $result);
  122 + }
  123 +
  124 + protected function _dataEncode($data, $type)
  125 + {
  126 + // Binary
  127 + if (($type & self::BINARY) === self::BINARY)
  128 + {
  129 + $n = strlen($data);
  130 + $len = '';
  131 + $pos = 0;
  132 +
  133 + char:
  134 +
  135 + ++$pos;
  136 + $c = $n >> 0 & 0x7F;
  137 + $n = $n >> 7;
  138 +
  139 + if ($pos != 1)
  140 + {
  141 + $c += 0x80;
  142 + }
  143 +
  144 + if ($c != 0x80)
  145 + {
  146 + $len = chr($c) . $len;
  147 + goto char;
  148 + };
  149 +
  150 + return chr(self::BINARY) . $len . $data ;
  151 + }
  152 + // String
  153 + else
  154 + {
  155 + return chr(self::STRING) . $data . "\xFF" ;
  156 + }
  157 + }
  158 +
  159 + protected function _dataDecode($data)
  160 + {
  161 + $decodedData = '' ;
  162 +
  163 + while (($buflen = strlen($data)) >= 2)
  164 + {
  165 + $frametype = ord(binarySubstr($data, 0, 1)) ;
  166 +
  167 + if (($frametype & 0x80) === 0x80)
  168 + {
  169 + $len = 0 ;
  170 + $i = 0 ;
  171 +
  172 + do {
  173 + $b = ord(binarySubstr($data, ++$i, 1)) ;
  174 + $n = $b & 0x7F ;
  175 + $len *= 0x80 ;
  176 + $len += $n ;
  177 + } while ($b > 0x80) ;
  178 +
  179 + if ($this->session->appInstance->config->maxallowedpacket->value <= $len)
  180 + {
  181 + // Too big packet
  182 + $this->session->finish() ;
  183 + return FALSE ;
  184 + }
  185 +
  186 + if ($buflen < $len + 2)
  187 + {
  188 + // not enough data yet
  189 + return FALSE ;
  190 + }
  191 +
  192 + $decodedData .= binarySubstr($data, 2, $len) ;
  193 + $data = binarySubstr($data, 2 + $len) ;
  194 +// $this->onFrame($decodedData, $frametype);
  195 + }
  196 + else
  197 + {
  198 + if (($p = strpos($data, "\xFF")) !== FALSE)
  199 + {
  200 + if ($this->session->appInstance->config->maxallowedpacket->value <= $p - 1)
  201 + {
  202 + // Too big packet
  203 + $this->session->finish() ;
  204 + return FALSE ;
  205 + }
  206 +
  207 + $decodedData .= binarySubstr($data, 1, $p - 1) ;
  208 + $data = binarySubstr($data, $p + 1) ;
  209 +// $this->onFrame($decodedData, $frametype);
  210 + }
  211 + else
  212 + {
  213 + // not enough data yet
  214 + if ($this->session->appInstance->config->maxallowedpacket->value <= strlen($data))
  215 + {
  216 + // Too big packet
  217 + $this->session->finish() ;
  218 + return FALSE ;
  219 + }
  220 +
  221 + return $decodedData ;
  222 + }
  223 + }
  224 + }
  225 +
  226 + if ($this->session->appInstance->config->maxallowedpacket->value <= strlen($decodedData))
  227 + {
  228 + // Too big packet
  229 + $this->session->finish() ;
  230 + return FALSE ;
  231 + }
  232 +
  233 + return $decodedData ;
  234 + }
  235 +}
184 lib/WebSocketProtocolV8.php
... ... @@ -0,0 +1,184 @@
  1 +<?php
  2 +
  3 +/**
  4 + * Websocket protocol hybi-10
  5 + * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10
  6 + */
  7 +
  8 +class WebSocketProtocolV8 extends WebSocketProtocol
  9 +{
  10 + // @todo manage only the 4 last bits (opcode), as described in the draft
  11 + const STRING = 0x81;
  12 + const BINARY = 0x82;
  13 +
  14 + public function __construct($session)
  15 + {
  16 + parent::__construct($session) ;
  17 +
  18 + $this->description = "Websocket protocol version " . $this->session->server['HTTP_SEC_WEBSOCKET_VERSION'] . " (IETF draft 'hybi-10')" ;
  19 + }
  20 +
  21 + public function onHandshake()
  22 + {
  23 + 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))
  24 + {
  25 + return FALSE ;
  26 + }
  27 +
  28 + return TRUE ;
  29 + }
  30 +
  31 + /**
  32 + * Returns handshaked data for reply
  33 + * @param string Received data (no use in this class)
  34 + * @return string Handshaked data
  35 + */
  36 +
  37 + public function getHandshakeReply($data)
  38 + {
  39 + if ($this->onHandshake())
  40 + {
  41 + if (!isset($this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN']))
  42 + {
  43 + $this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN'] = '' ;
  44 + }
  45 +
  46 + $reply = "HTTP/1.1 101 Switching Protocols\r\n"
  47 + . "Upgrade: websocket\r\n"
  48 + . "Connection: Upgrade\r\n"
  49 + . "Sec-WebSocket-Origin: " . $this->session->server['HTTP_SEC_WEBSOCKET_ORIGIN'] . "\r\n"
  50 + . "Sec-WebSocket-Location: ws://" . $this->session->server['HTTP_HOST'] . $this->session->server['REQUEST_URI'] . "\r\n"
  51 + . "Sec-WebSocket-Accept: " . base64_encode(sha1(trim($this->session->server['HTTP_SEC_WEBSOCKET_KEY']) . "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", true)) . "\r\n" ;
  52 +
  53 + if (isset($this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL']))
  54 + {
  55 + $reply .= "Sec-WebSocket-Protocol: " . $this->session->server['HTTP_SEC_WEBSOCKET_PROTOCOL'] . "\r\n" ;
  56 + }
  57 +
  58 + $reply .= "\r\n" ;
  59 +
  60 + return $reply ;
  61 + }
  62 +
  63 + return FALSE ;
  64 + }
  65 +
  66 + /**
  67 + * Data encoding, according to related IETF draft
  68 + *
  69 + * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#page-16
  70 + */
  71 +
  72 + protected function _dataEncode($decodedData, $type = NULL)
  73 + {
  74 + $frames = array() ;
  75 + $maskingKeys = chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) . chr(rand(0, 255)) ;
  76 + $frames[0] = ($type === NULL) ? $this->getFrameType("STRING") : $this->getFrameType($type) ;
  77 + $dataLength = strlen($decodedData) ;
  78 +
  79 + if ($dataLength <= 125)
  80 + {
  81 + $frames[1] = $dataLength + 128 ;
  82 + }
  83 + elseif ($dataLength <= 65535)
  84 + {
  85 + $frames[1] = 254 ; // 126 + 128
  86 + $frames[2] = $dataLength >> 8 ;
  87 + $frames[3] = $dataLength & 0xFF ;
  88 + }
  89 + else
  90 + {
  91 + $frames[1] = 255 ; // 127 + 128
  92 + $frames[2] = $dataLength >> 56 ;
  93 + $frames[3] = $dataLength >> 48 ;
  94 + $frames[4] = $dataLength >> 40 ;
  95 + $frames[5] = $dataLength >> 32 ;
  96 + $frames[6] = $dataLength >> 24 ;
  97 + $frames[7] = $dataLength >> 16 ;
  98 + $frames[8] = $dataLength >> 8 ;
  99 + $frames[9] = $dataLength & 0xFF ;
  100 + }
  101 +
  102 + $maskingFunc = function($data, $mask)
  103 + {
  104 + for ($i = 0, $l = strlen($data); $i < $l; $i++)
  105 + {
  106 + // Avoid storing a new copy of $data...
  107 + $data[$i] = $data[$i] ^ $mask[$i % 4] ;
  108 + }
  109 +
  110 + return $data ;
  111 + } ;
  112 +
  113 + return implode('', array_map('chr', $frames)) . $maskingKeys . $maskingFunc($decodedData, $maskingKeys) ;
  114 + }
  115 +
  116 + /**
  117 + * Data decoding, according to related IETF draft
  118 + *
  119 + * @see http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10#page-16
  120 + */
  121 +
  122 + protected function _dataDecode($encodedData)
  123 + {
  124 + $isMasked = (bool) (ord($encodedData[1]) >> 7) ;
  125 + $dataLength = ord($encodedData[1]) & 127 ;
  126 +
  127 + if ($isMasked)
  128 + {
  129 + $unmaskingFunc = function($data, $mask)
  130 + {
  131 + for ($i = 0, $l = strlen($data); $i < $l; $i++)
  132 + {
  133 + // Avoid storing a new copy of $data...
  134 + $data[$i] = $data[$i] ^ $mask[$i % 4] ;
  135 + }
  136 +
  137 + return $data ;
  138 + } ;
  139 +
  140 + if ($dataLength === 126)
  141 + {
  142 + $maskingKey = binarySubstr($encodedData, 4, 4) ;
  143 + $extDataLength = hexdec(sprintf('%02x%02x', ord($encodedData[2]), ord($encodedData[3]))) ;
  144 + $offsetStart = 8 ;
  145 + }
  146 + elseif ($dataLength === 127)
  147 + {
  148 + $maskingKey = binarySubstr($encodedData, 10, 4) ;
  149 + $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]))) ;
  150 + $offsetStart = 14 ;
  151 + }
  152 + else
  153 + {
  154 + $maskingKey = binarySubstr($encodedData, 2, 4) ;
  155 + $extDataLength = $dataLength ;
  156 + $offsetStart = 6 ;
  157 + }
  158 +
  159 + if (strlen($encodedData) < $offsetStart + $extDataLength)
  160 + {
  161 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Incorrect data size in frame decoding for client "' . $this->session->clientAddr . '"') ;
  162 + }
  163 +
  164 + return $unmaskingFunc(binarySubstr($encodedData, $offsetStart, $extDataLength), $maskingKey) ;
  165 + }
  166 + else
  167 + {
  168 + if ($dataLength === 126)
  169 + {
  170 + return binarySubstr($encodedData, 4) ;
  171 + }
  172 + elseif ($dataLength === 127)
  173 + {
  174 + return binarySubstr($encodedData, 10) ;
  175 + }
  176 + else
  177 + {
  178 + return binarySubstr($encodedData, 2) ;
  179 + }
  180 + }
  181 +
  182 + return NULL ;
  183 + }
  184 +}
365 lib/genericWebSocketSession.php
... ... @@ -0,0 +1,365 @@
  1 +<?php
  2 +/*
  3 +require_once("./lib/SocketSession.class.php") ;
  4 +require_once("./applications/WebSocketProtocolV0.php") ;
  5 +require_once("./applications/WebSocketProtocolV8.php") ;
  6 +*/
  7 +class genericWebSocketSession extends SocketSession {
  8 +
  9 + public $secprotocol;
  10 + public $resultKey;
  11 + public $handshaked = FALSE;
  12 + public $upstream;
  13 + public $server = array();
  14 + public $cookie = array();
  15 + public $firstline = FALSE;
  16 + public $writeReady = TRUE;
  17 + public $callbacks = array();
  18 +
  19 + public $protocol; // Related WebSocket protocol
  20 +
  21 + public function init()
  22 + {
  23 + }
  24 +
  25 + /**
  26 + * Sends a frame.
  27 + * @param string Frame's data.
  28 + * @param string Frame's type. ("STRING" OR "BINARY")
  29 + * @param callback Optional. Callback called when the frame is received by client.
  30 + * @return boolean Success.
  31 + */
  32 +
  33 + public function sendFrame($data, $type = NULL, $callback = NULL)
  34 + {
  35 + if (!$this->handshaked)
  36 + {
  37 + return FALSE;
  38 + }
  39 +
  40 + if (!isset($this->protocol))
  41 + {
  42 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client ' . $this->clientAddr) ;
  43 + return FALSE ;
  44 + }
  45 +
  46 +// $this->write($this->protocol->dataEncode($data, $type)) ;
  47 + $this->protocol->sendFrame($data, $type) ;
  48 + $this->writeReady = FALSE;
  49 +
  50 + if ($callback)
  51 + {
  52 + $this->callbacks[] = $callback;
  53 + }
  54 +
  55 + return TRUE;
  56 + }
  57 +
  58 + /**
  59 + * Event of SocketSession (asyncServer).
  60 + * @return void
  61 + */
  62 +
  63 + public function onFinish()
  64 + {
  65 + if (Daemon::$config->logevents->value)
  66 + {
  67 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
  68 + }
  69 +
  70 + if (isset($this->upstream))
  71 + {
  72 + $this->upstream->onFinish();
  73 + }
  74 +
  75 + unset($this->upstream);
  76 + unset($this->appInstance->sessions[$this->connId]);
  77 + }
  78 +
  79 + /**
  80 + * Called when new frame received.
  81 + * @param string Frame's data.
  82 + * @param string Frame's type ("STRING" OR "BINARY").
  83 + * @return boolean Success.
  84 + */
  85 +
  86 + public function onFrame($data, $type)
  87 + {
  88 + if (Daemon::$config->logevents->value)
  89 + {
  90 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
  91 + }
  92 +
  93 + if (!isset($this->upstream))
  94 + {
  95 + return FALSE;
  96 + }
  97 +
  98 + $this->upstream->onFrame($data, $type);
  99 +
  100 + return TRUE;
  101 + }
  102 +
  103 + /**
  104 + * Called when the connection is ready to accept new data.
  105 + * @return void
  106 + */
  107 +
  108 + public function onWrite()
  109 + {
  110 + if (Daemon::$config->logevents->value)
  111 + {
  112 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
  113 + }
  114 +
  115 + $this->writeReady = TRUE;
  116 +
  117 + for ($i = 0, $s = sizeof($this->callbacks); $i < $s; ++$i)
  118 + {
  119 + call_user_func(array_shift($this->callbacks), $this);
  120 + }
  121 +
  122 + if (is_callable(array($this->upstream, 'onWrite')))
  123 + {
  124 + $this->upstream->onWrite();
  125 + }
  126 + }
  127 +
  128 + /**
  129 + * Called when the connection is handshaked.
  130 + * @return boolean Ready to handshake ?
  131 + */
  132 +
  133 + public function onHandshake()
  134 + {
  135 + if (Daemon::$config->logevents->value)
  136 + {
  137 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' invoked');
  138 + }
  139 +
  140 + $e = explode('/', $this->server['DOCUMENT_URI']);
  141 + $appName = isset($e[1])?$e[1]:'';
  142 +
  143 + if (!isset($this->appInstance->routes[$appName]))
  144 + {
  145 + if (Daemon::$config->logerrors->value)
  146 + {
  147 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : undefined route "' . $appName . '" for client "' . $this->clientAddr . '"');
  148 + }
  149 +
  150 + return FALSE;
  151 + }
  152 +
  153 + if (!$this->upstream = call_user_func($this->appInstance->routes[$appName], $this))
  154 + {
  155 + return FALSE;
  156 + }
  157 +
  158 + if (!isset($this->protocol))
  159 + {
  160 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client "' . $this->clientAddr . '"') ;
  161 + return FALSE ;
  162 + }
  163 +
  164 + if ($this->protocol->onHandshake() === FALSE)
  165 + {
  166 + return FALSE ;
  167 + }
  168 +
  169 + return TRUE;
  170 + }
  171 +
  172 + /**
  173 + * Event of SocketSession (AsyncServer). Called when the worker is going to shutdown.
  174 + * @return boolean Ready to shutdown ?
  175 + */
  176 +
  177 + public function gracefulShutdown()
  178 + {
  179 + if ((!$this->upstream) || $this->upstream->gracefulShutdown())
  180 + {
  181 + $this->finish();
  182 +
  183 + return TRUE;
  184 + }
  185 +
  186 + return FALSE;
  187 + }
  188 +
  189 + /**
  190 + * Called when we're going to handshake.
  191 + * @return boolean Handshake status
  192 + */
  193 +
  194 + public function handshake($data)
  195 + {
  196 + $this->handshaked = TRUE;
  197 +
  198 + if (!$this->onHandshake())
  199 + {
  200 + $this->finish() ;
  201 + return FALSE ;
  202 + }
  203 +
  204 + if (!isset($this->protocol))
  205 + {
  206 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Cannot find session-related websocket protocol for client "' . $this->clientAddr . '"') ;
  207 + $this->finish() ;
  208 + }
  209 +
  210 + // Handshaking...
  211 + $handshake = $this->protocol->getHandshakeReply($data) ;
  212 +
  213 + if (!$handshake)
  214 + {
  215 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Handshake protocol failure for client "' . $this->clientAddr . '"') ;
  216 + $this->finish() ;
  217 + return FALSE ;
  218 + }
  219 +
  220 + if ($this->write($handshake))
  221 + {
  222 + if (is_callable(array($this->upstream, 'onHandshake')))
  223 + {
  224 + $this->upstream->onHandshake();
  225 + }
  226 + }
  227 + else
  228 + {
  229 + Daemon::$process->log(get_class($this) . '::' . __METHOD__ . ' : Handshake send failure for client "' . $this->clientAddr . '"') ;
  230 + $this->finish() ;
  231 + return FALSE ;
  232 + }
  233 +
  234 + return TRUE ;
  235 + }
  236 +
  237 + /**
  238 + * Event of SocketSession (AsyncServer). Called when new data received.
  239 + * @param string New received data.
  240 + * @return void
  241 + */
  242 +
  243 + public function stdin($buf)
  244 + {
  245 + $this->buf .= $buf;
  246 +
  247 + if (!$this->handshaked)
  248 + {
  249 +/*
  250 + if (Daemon::$appResolver->checkAppEnabled('FlashPolicy'))
  251 + {
  252 + if (strpos($this->buf, '<policy-file-request/>') !== FALSE) {
  253 + if (
  254 + ($FP = Daemon::$appResolver->getInstanceByAppName('FlashPolicy'))
  255 + && $FP->policyData
  256 + ) {
  257 + $this->write($FP->policyData . "\x00");
  258 + }
  259 +
  260 + $this->finish();
  261 +
  262 + return;
  263 + }
  264 + }
  265 +*/
  266 + $i = 0;
  267 +
  268 + while (($l = $this->gets()) !== FALSE)
  269 + {
  270 + if ($i++ > 100)
  271 + {
  272 + break;
  273 + }
  274 +
  275 + if ($l === "\r\n")
  276 + {
  277 + if (
  278 + !isset($this->server['HTTP_CONNECTION'])
  279 + || (!preg_match('/upgrade/i', $this->server['HTTP_CONNECTION'])) // "Upgrade" is not always alone (ie. "Connection: Keep-alive, Upgrade")
  280 + || !isset($this->server['HTTP_UPGRADE'])
  281 + || (strtolower($this->server['HTTP_UPGRADE']) !== 'websocket') // Lowercase compare important
  282 + ) {
  283 + $this->finish();
  284 + return;
  285 + }
  286 +