PageRenderTime 23ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/cli/ws.php

http://github.com/bcosca/fatfree
PHP | 487 lines | 322 code | 32 blank | 133 comment | 50 complexity | 323e61a415a0e9003118463f9f0ae1d5 MD5 | raw file
Possible License(s): GPL-3.0
  1. <?php
  2. /*
  3. Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.
  4. This file is part of the Fat-Free Framework (http://fatfreeframework.com).
  5. This is free software: you can redistribute it and/or modify it under the
  6. terms of the GNU General Public License as published by the Free Software
  7. Foundation, either version 3 of the License, or later.
  8. Fat-Free Framework is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. General Public License for more details.
  12. You should have received a copy of the GNU General Public License along
  13. with Fat-Free Framework. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. namespace CLI;
  16. //! RFC6455 WebSocket server
  17. class WS {
  18. const
  19. //! UUID magic string
  20. Magic='258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
  21. //! Max packet size
  22. Packet=65536;
  23. //@{ Mask bits for first byte of header
  24. const
  25. Text=0x01,
  26. Binary=0x02,
  27. Close=0x08,
  28. Ping=0x09,
  29. Pong=0x0a,
  30. OpCode=0x0f,
  31. Finale=0x80;
  32. //@}
  33. //@{ Mask bits for second byte of header
  34. const
  35. Length=0x7f;
  36. //@}
  37. protected
  38. $addr,
  39. $ctx,
  40. $wait,
  41. $sockets,
  42. $protocol,
  43. $agents=[],
  44. $events=[];
  45. /**
  46. * Allocate stream socket
  47. * @return NULL
  48. * @param $socket resource
  49. **/
  50. function alloc($socket) {
  51. if (is_bool($buf=$this->read($socket)))
  52. return;
  53. // Get WebSocket headers
  54. $hdrs=[];
  55. $EOL="\r\n";
  56. $verb=NULL;
  57. $uri=NULL;
  58. foreach (explode($EOL,trim($buf)) as $line)
  59. if (preg_match('/^(\w+)\s(.+)\sHTTP\/[\d.]{1,3}$/',
  60. trim($line),$match)) {
  61. $verb=$match[1];
  62. $uri=$match[2];
  63. }
  64. else
  65. if (preg_match('/^(.+): (.+)/',trim($line),$match))
  66. // Standardize header
  67. $hdrs[
  68. strtr(
  69. ucwords(
  70. strtolower(
  71. strtr($match[1],'-',' ')
  72. )
  73. ),' ','-'
  74. )
  75. ]=$match[2];
  76. else {
  77. $this->close($socket);
  78. return;
  79. }
  80. if (empty($hdrs['Upgrade']) &&
  81. empty($hdrs['Sec-Websocket-Key'])) {
  82. // Not a WebSocket request
  83. if ($verb && $uri)
  84. $this->write(
  85. $socket,
  86. 'HTTP/1.1 400 Bad Request'.$EOL.
  87. 'Connection: close'.$EOL.$EOL
  88. );
  89. $this->close($socket);
  90. return;
  91. }
  92. // Handshake
  93. $buf='HTTP/1.1 101 Switching Protocols'.$EOL.
  94. 'Upgrade: websocket'.$EOL.
  95. 'Connection: Upgrade'.$EOL;
  96. if (isset($hdrs['Sec-Websocket-Protocol']))
  97. $buf.='Sec-WebSocket-Protocol: '.
  98. $hdrs['Sec-Websocket-Protocol'].$EOL;
  99. $buf.='Sec-WebSocket-Accept: '.
  100. base64_encode(
  101. sha1($hdrs['Sec-Websocket-Key'].WS::Magic,TRUE)
  102. ).$EOL.$EOL;
  103. if ($this->write($socket,$buf)) {
  104. // Connect agent to server
  105. $this->sockets[(int)$socket]=$socket;
  106. $this->agents[(int)$socket]=
  107. new Agent($this,$socket,$verb,$uri,$hdrs);
  108. }
  109. }
  110. /**
  111. * Close stream socket
  112. * @return NULL
  113. * @param $socket resource
  114. **/
  115. function close($socket) {
  116. if (isset($this->agents[(int)$socket]))
  117. unset($this->sockets[(int)$socket],$this->agents[(int)$socket]);
  118. stream_socket_shutdown($socket,STREAM_SHUT_WR);
  119. @fclose($socket);
  120. }
  121. /**
  122. * Read from stream socket
  123. * @return string|FALSE
  124. * @param $socket resource
  125. * @param $len int
  126. **/
  127. function read($socket,$len=0) {
  128. if (!$len)
  129. $len=WS::Packet;
  130. if (is_string($buf=@fread($socket,$len)) &&
  131. strlen($buf) && strlen($buf)<$len)
  132. return $buf;
  133. if (isset($this->events['error']) &&
  134. is_callable($func=$this->events['error']))
  135. $func($this);
  136. $this->close($socket);
  137. return FALSE;
  138. }
  139. /**
  140. * Write to stream socket
  141. * @return int|FALSE
  142. * @param $socket resource
  143. * @param $buf string
  144. **/
  145. function write($socket,$buf) {
  146. for ($i=0,$bytes=0;$i<strlen($buf);$i+=$bytes) {
  147. if (($bytes=@fwrite($socket,substr($buf,$i))) &&
  148. @fflush($socket))
  149. continue;
  150. if (isset($this->events['error']) &&
  151. is_callable($func=$this->events['error']))
  152. $func($this);
  153. $this->close($socket);
  154. return FALSE;
  155. }
  156. return $bytes;
  157. }
  158. /**
  159. * Return socket agents
  160. * @return array
  161. * @param $uri string
  162. ***/
  163. function agents($uri=NULL) {
  164. return array_filter(
  165. $this->agents,
  166. /**
  167. * @var $val Agent
  168. * @return bool
  169. */
  170. function($val) use($uri) {
  171. return $uri?($val->uri()==$uri):TRUE;
  172. }
  173. );
  174. }
  175. /**
  176. * Return event handlers
  177. * @return array
  178. **/
  179. function events() {
  180. return $this->events;
  181. }
  182. /**
  183. * Bind function to event handler
  184. * @return object
  185. * @param $event string
  186. * @param $func callable
  187. **/
  188. function on($event,$func) {
  189. $this->events[$event]=$func;
  190. return $this;
  191. }
  192. /**
  193. * Terminate server
  194. **/
  195. function kill() {
  196. die;
  197. }
  198. /**
  199. * Execute the server process
  200. **/
  201. function run() {
  202. // Assign signal handlers
  203. declare(ticks=1);
  204. pcntl_signal(SIGINT,[$this,'kill']);
  205. pcntl_signal(SIGTERM,[$this,'kill']);
  206. gc_enable();
  207. // Activate WebSocket listener
  208. $listen=stream_socket_server(
  209. $this->addr,$errno,$errstr,
  210. STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
  211. $this->ctx
  212. );
  213. $socket=socket_import_stream($listen);
  214. register_shutdown_function(function() use($listen) {
  215. foreach ($this->sockets as $socket)
  216. if ($socket!=$listen)
  217. $this->close($socket);
  218. $this->close($listen);
  219. if (isset($this->events['stop']) &&
  220. is_callable($func=$this->events['stop']))
  221. $func($this);
  222. });
  223. if ($errstr)
  224. user_error($errstr,E_USER_ERROR);
  225. if (isset($this->events['start']) &&
  226. is_callable($func=$this->events['start']))
  227. $func($this);
  228. $this->sockets=[(int)$listen=>$listen];
  229. $empty=[];
  230. $wait=$this->wait;
  231. while (TRUE) {
  232. $active=$this->sockets;
  233. $mark=microtime(TRUE);
  234. $count=@stream_select(
  235. $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait))
  236. );
  237. if (is_bool($count) && $wait) {
  238. if (isset($this->events['error']) &&
  239. is_callable($func=$this->events['error']))
  240. $func($this);
  241. die;
  242. }
  243. if ($count) {
  244. // Process active connections
  245. foreach ($active as $socket) {
  246. if (!is_resource($socket))
  247. continue;
  248. if ($socket==$listen) {
  249. if ($socket=@stream_socket_accept($listen,0))
  250. $this->alloc($socket);
  251. else
  252. if (isset($this->events['error']) &&
  253. is_callable($func=$this->events['error']))
  254. $func($this);
  255. }
  256. else {
  257. $id=(int)$socket;
  258. if (isset($this->agents[$id]))
  259. $this->agents[$id]->fetch();
  260. }
  261. }
  262. $wait-=microtime(TRUE)-$mark;
  263. while ($wait<1e-6) {
  264. $wait+=$this->wait;
  265. $count=0;
  266. }
  267. }
  268. if (!$count) {
  269. $mark=microtime(TRUE);
  270. foreach ($this->sockets as $id=>$socket) {
  271. if (!is_resource($socket))
  272. continue;
  273. if ($socket!=$listen &&
  274. isset($this->agents[$id]) &&
  275. isset($this->events['idle']) &&
  276. is_callable($func=$this->events['idle']))
  277. $func($this->agents[$id]);
  278. }
  279. $wait=$this->wait-microtime(TRUE)+$mark;
  280. }
  281. gc_collect_cycles();
  282. }
  283. }
  284. /**
  285. * @param $addr string
  286. * @param $ctx resource
  287. * @param $wait int
  288. **/
  289. function __construct($addr,$ctx=NULL,$wait=60) {
  290. $this->addr=$addr;
  291. $this->ctx=$ctx?:stream_context_create();
  292. $this->wait=$wait;
  293. $this->events=[];
  294. }
  295. }
  296. //! RFC6455 remote socket
  297. class Agent {
  298. protected
  299. $server,
  300. $id,
  301. $socket,
  302. $flag,
  303. $verb,
  304. $uri,
  305. $headers;
  306. /**
  307. * Return server instance
  308. * @return WS
  309. **/
  310. function server() {
  311. return $this->server;
  312. }
  313. /**
  314. * Return socket ID
  315. * @return string
  316. **/
  317. function id() {
  318. return $this->id;
  319. }
  320. /**
  321. * Return socket
  322. * @return resource
  323. **/
  324. function socket() {
  325. return $this->socket;
  326. }
  327. /**
  328. * Return request method
  329. * @return string
  330. **/
  331. function verb() {
  332. return $this->verb;
  333. }
  334. /**
  335. * Return request URI
  336. * @return string
  337. **/
  338. function uri() {
  339. return $this->uri;
  340. }
  341. /**
  342. * Return socket headers
  343. * @return array
  344. **/
  345. function headers() {
  346. return $this->headers;
  347. }
  348. /**
  349. * Frame and transmit payload
  350. * @return string|FALSE
  351. * @param $op int
  352. * @param $data string
  353. **/
  354. function send($op,$data='') {
  355. $server=$this->server;
  356. $mask=WS::Finale | $op & WS::OpCode;
  357. $len=strlen($data);
  358. $buf='';
  359. if ($len>0xffff)
  360. $buf=pack('CCNN',$mask,0x7f,$len);
  361. elseif ($len>0x7d)
  362. $buf=pack('CCn',$mask,0x7e,$len);
  363. else
  364. $buf=pack('CC',$mask,$len);
  365. $buf.=$data;
  366. if (is_bool($server->write($this->socket,$buf)))
  367. return FALSE;
  368. if (!in_array($op,[WS::Pong,WS::Close]) &&
  369. isset($this->server->events['send']) &&
  370. is_callable($func=$this->server->events['send']))
  371. $func($this,$op,$data);
  372. return $data;
  373. }
  374. /**
  375. * Retrieve and unmask payload
  376. * @return bool|NULL
  377. **/
  378. function fetch() {
  379. // Unmask payload
  380. $server=$this->server;
  381. if (is_bool($buf=$server->read($this->socket)))
  382. return FALSE;
  383. while($buf) {
  384. $op=ord($buf[0]) & WS::OpCode;
  385. $len=ord($buf[1]) & WS::Length;
  386. $pos=2;
  387. if ($len==0x7e) {
  388. $len=ord($buf[2])*256+ord($buf[3]);
  389. $pos+=2;
  390. }
  391. else
  392. if ($len==0x7f) {
  393. for ($i=0,$len=0;$i<8;++$i)
  394. $len=$len*256+ord($buf[$i+2]);
  395. $pos+=8;
  396. }
  397. for ($i=0,$mask=[];$i<4;++$i)
  398. $mask[$i]=ord($buf[$pos+$i]);
  399. $pos+=4;
  400. if (strlen($buf)<$len+$pos)
  401. return FALSE;
  402. for ($i=0,$data='';$i<$len;++$i)
  403. $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]);
  404. // Dispatch
  405. switch ($op & WS::OpCode) {
  406. case WS::Ping:
  407. $this->send(WS::Pong);
  408. break;
  409. case WS::Close:
  410. $server->close($this->socket);
  411. break;
  412. case WS::Text:
  413. $data=trim($data);
  414. case WS::Binary:
  415. if (isset($this->server->events['receive']) &&
  416. is_callable($func=$this->server->events['receive']))
  417. $func($this,$op,$data);
  418. break;
  419. }
  420. $buf = substr($buf, $len+$pos);
  421. }
  422. }
  423. /**
  424. * Destroy object
  425. **/
  426. function __destruct() {
  427. if (isset($this->server->events['disconnect']) &&
  428. is_callable($func=$this->server->events['disconnect']))
  429. $func($this);
  430. }
  431. /**
  432. * @param $server WS
  433. * @param $socket resource
  434. * @param $verb string
  435. * @param $uri string
  436. * @param $hdrs array
  437. **/
  438. function __construct($server,$socket,$verb,$uri,array $hdrs) {
  439. $this->server=$server;
  440. $this->id=stream_socket_get_name($socket,TRUE);
  441. $this->socket=$socket;
  442. $this->verb=$verb;
  443. $this->uri=$uri;
  444. $this->headers=$hdrs;
  445. if (isset($server->events['connect']) &&
  446. is_callable($func=$server->events['connect']))
  447. $func($this);
  448. }
  449. }