Engine.php 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <?php
  2. namespace PHPSocketIO\Engine;
  3. use \PHPSocketIO\Engine\Transports\Polling;
  4. use \PHPSocketIO\Engine\Transports\PollingXHR;
  5. use \PHPSocketIO\Engine\Transports\WebSocket;
  6. use \PHPSocketIO\Event\Emitter;
  7. use \PHPSocketIO\Debug;
  8. class Engine extends Emitter
  9. {
  10. public $pingTimeout = 60;
  11. public $pingInterval = 25;
  12. public $upgradeTimeout = 5;
  13. public $transports = array();
  14. public $allowUpgrades = array();
  15. public $allowRequest = array();
  16. public $clients = array();
  17. public $origins = '*:*';
  18. public static $allowTransports = array(
  19. 'polling' => 'polling',
  20. 'websocket' => 'websocket'
  21. );
  22. public static $errorMessages = array(
  23. 'Transport unknown',
  24. 'Session ID unknown',
  25. 'Bad handshake method',
  26. 'Bad request'
  27. );
  28. const ERROR_UNKNOWN_TRANSPORT = 0;
  29. const ERROR_UNKNOWN_SID = 1;
  30. const ERROR_BAD_HANDSHAKE_METHOD = 2;
  31. const ERROR_BAD_REQUEST = 3;
  32. public function __construct($opts = array())
  33. {
  34. $ops_map = array(
  35. 'pingTimeout',
  36. 'pingInterval',
  37. 'upgradeTimeout',
  38. 'transports',
  39. 'allowUpgrades',
  40. 'allowRequest'
  41. );
  42. foreach($ops_map as $key)
  43. {
  44. if(isset($opts[$key]))
  45. {
  46. $this->$key = $opts[$key];
  47. }
  48. }
  49. Debug::debug('Engine __construct');
  50. }
  51. public function __destruct()
  52. {
  53. Debug::debug('Engine __destruct');
  54. }
  55. public function handleRequest($req, $res)
  56. {
  57. $this->prepare($req);
  58. $req->res = $res;
  59. $this->verify($req, $res, false, array($this, 'dealRequest'));
  60. }
  61. public function dealRequest($err, $success, $req)
  62. {
  63. if (!$success)
  64. {
  65. self::sendErrorMessage($req, $req->res, $err);
  66. return;
  67. }
  68. if(isset($req->_query['sid']))
  69. {
  70. $this->clients[$req->_query['sid']]->transport->onRequest($req);
  71. }
  72. else
  73. {
  74. $this->handshake($req->_query['transport'], $req);
  75. }
  76. }
  77. protected function sendErrorMessage($req, $res, $code)
  78. {
  79. $headers = array('Content-Type'=> 'application/json');
  80. if(isset($req->headers['origin']))
  81. {
  82. $headers['Access-Control-Allow-Credentials'] = 'true';
  83. $headers['Access-Control-Allow-Origin'] = $req->headers['origin'];
  84. }
  85. else
  86. {
  87. $headers['Access-Control-Allow-Origin'] = '*';
  88. }
  89. $res->writeHead(403, '', $headers);
  90. $res->end(json_encode(array(
  91. 'code' => $code,
  92. 'message' => isset(self::$errorMessages[$code]) ? self::$errorMessages[$code] : $code
  93. )));
  94. }
  95. protected function verify($req, $res, $upgrade, $fn)
  96. {
  97. if(!isset($req->_query['transport']) || !isset(self::$allowTransports[$req->_query['transport']]))
  98. {
  99. return call_user_func($fn, self::ERROR_UNKNOWN_TRANSPORT, false, $req, $res);
  100. }
  101. $transport = $req->_query['transport'];
  102. $sid = isset($req->_query['sid']) ? $req->_query['sid'] : '';
  103. if ($transport === 'websocket' && empty($sid)) {
  104. return call_user_func($fn, self::ERROR_UNKNOWN_TRANSPORT, false, $req, $res);
  105. }
  106. if($sid)
  107. {
  108. if(!isset($this->clients[$sid]))
  109. {
  110. return call_user_func($fn, self::ERROR_UNKNOWN_SID, false, $req, $res);
  111. }
  112. if(!$upgrade && $this->clients[$sid]->transport->name !== $transport)
  113. {
  114. return call_user_func($fn, self::ERROR_BAD_REQUEST, false, $req, $res);
  115. }
  116. }
  117. else
  118. {
  119. if('GET' !== $req->method)
  120. {
  121. return call_user_func($fn, self::ERROR_BAD_HANDSHAKE_METHOD, false, $req, $res);
  122. }
  123. return $this->checkRequest($req, $res, $fn);
  124. }
  125. call_user_func($fn, null, true, $req, $res);
  126. }
  127. public function checkRequest($req, $res, $fn)
  128. {
  129. if ($this->origins === "*:*" || empty($this->origins))
  130. {
  131. return call_user_func($fn, null, true, $req, $res);
  132. }
  133. $origin = null;
  134. if (isset($req->headers['origin']))
  135. {
  136. $origin = $req->headers['origin'];
  137. }
  138. else if(isset($req->headers['referer']))
  139. {
  140. $origin = $req->headers['referer'];
  141. }
  142. // file:// URLs produce a null Origin which can't be authorized via echo-back
  143. if ('null' === $origin || null === $origin) {
  144. return call_user_func($fn, null, true, $req, $res);
  145. }
  146. if ($origin)
  147. {
  148. $parts = parse_url($origin);
  149. $defaultPort = 'https:' === $parts['scheme'] ? 443 : 80;
  150. $parts['port'] = isset($parts['port']) ? $parts['port'] : $defaultPort;
  151. $allowed_origins = explode(' ', $this->origins);
  152. foreach( $allowed_origins as $allow_origin ){
  153. $ok =
  154. $allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'] ||
  155. $allow_origin === $parts['scheme'] . '://' . $parts['host'] ||
  156. $allow_origin === $parts['scheme'] . '://' . $parts['host'] . ':*' ||
  157. $allow_origin === '*:' . $parts['port'];
  158. if($ok){
  159. # 只需要有一个白名单通过,则都通过
  160. return call_user_func($fn, null, $ok, $req, $res);
  161. }
  162. }
  163. }
  164. call_user_func($fn, null, false, $req, $res);
  165. }
  166. protected function prepare($req)
  167. {
  168. if(!isset($req->_query))
  169. {
  170. $info = parse_url($req->url);
  171. if(isset($info['query']))
  172. {
  173. parse_str($info['query'], $req->_query);
  174. }
  175. }
  176. }
  177. public function handshake($transport, $req)
  178. {
  179. $id = bin2hex(pack('d', microtime(true)).pack('N', function_exists('random_int') ? random_int(1, 100000000): rand(1, 100000000)));
  180. if ($transport == 'websocket') {
  181. $transport = '\\PHPSocketIO\\Engine\\Transports\\WebSocket';
  182. }
  183. elseif (isset($req->_query['j']))
  184. {
  185. $transport = '\\PHPSocketIO\\Engine\\Transports\\PollingJsonp';
  186. }
  187. else
  188. {
  189. $transport = '\\PHPSocketIO\\Engine\\Transports\\PollingXHR';
  190. }
  191. $transport = new $transport($req);
  192. $transport->supportsBinary = !isset($req->_query['b64']);
  193. $socket = new Socket($id, $this, $transport, $req);
  194. /* $transport->on('headers', function(&$headers)use($id)
  195. {
  196. $headers['Set-Cookie'] = "io=$id";
  197. }); */
  198. $transport->onRequest($req);
  199. $this->clients[$id] = $socket;
  200. $socket->once('close', array($this, 'onSocketClose'));
  201. $this->emit('connection', $socket);
  202. }
  203. public function onSocketClose($id)
  204. {
  205. unset($this->clients[$id]);
  206. }
  207. public function attach($worker)
  208. {
  209. $this->server = $worker;
  210. $worker->onConnect = array($this, 'onConnect');
  211. }
  212. public function onConnect($connection)
  213. {
  214. $connection->onRequest = array($this, 'handleRequest');
  215. $connection->onWebSocketConnect = array($this, 'onWebSocketConnect');
  216. // clean
  217. $connection->onClose = function($connection)
  218. {
  219. if(!empty($connection->httpRequest))
  220. {
  221. $connection->httpRequest->destroy();
  222. $connection->httpRequest = null;
  223. }
  224. if(!empty($connection->httpResponse))
  225. {
  226. $connection->httpResponse->destroy();
  227. $connection->httpResponse = null;
  228. }
  229. if(!empty($connection->onRequest))
  230. {
  231. $connection->onRequest = null;
  232. }
  233. if(!empty($connection->onWebSocketConnect))
  234. {
  235. $connection->onWebSocketConnect = null;
  236. }
  237. };
  238. }
  239. public function onWebSocketConnect($connection, $req, $res)
  240. {
  241. $this->prepare($req);
  242. $this->verify($req, $res, true, array($this, 'dealWebSocketConnect'));
  243. }
  244. public function dealWebSocketConnect($err, $success, $req, $res)
  245. {
  246. if (!$success)
  247. {
  248. self::sendErrorMessage($req, $res, $err);
  249. return;
  250. }
  251. if(isset($req->_query['sid']))
  252. {
  253. if(!isset($this->clients[$req->_query['sid']]))
  254. {
  255. self::sendErrorMessage($req, $res, 'upgrade attempt for closed client');
  256. return;
  257. }
  258. $client = $this->clients[$req->_query['sid']];
  259. if($client->upgrading)
  260. {
  261. self::sendErrorMessage($req, $res, 'transport has already been trying to upgrade');
  262. return;
  263. }
  264. if($client->upgraded)
  265. {
  266. self::sendErrorMessage($req, $res, 'transport had already been upgraded');
  267. return;
  268. }
  269. $transport = new WebSocket($req);
  270. $client->maybeUpgrade($transport);
  271. }
  272. else
  273. {
  274. $this->handshake($req->_query['transport'], $req);
  275. }
  276. }
  277. }