ws.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. <?php
  2. /*
  3. Copyright (c) 2009-2017 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\/1\.\d$/',
  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. **/
  126. function read($socket) {
  127. if (is_string($buf=@fread($socket,WS::Packet)) &&
  128. strlen($buf) &&
  129. strlen($buf)<WS::Packet)
  130. return $buf;
  131. if (isset($this->events['error']) &&
  132. is_callable($func=$this->events['error']))
  133. $func($this);
  134. $this->close($socket);
  135. return FALSE;
  136. }
  137. /**
  138. * Write to stream socket
  139. * @return int|FALSE
  140. * @param $socket resource
  141. * @param $buf string
  142. **/
  143. function write($socket,$buf) {
  144. for ($i=0,$bytes=0;$i<strlen($buf);$i+=$bytes) {
  145. if (($bytes=@fwrite($socket,substr($buf,$i))) &&
  146. @fflush($socket))
  147. continue;
  148. if (isset($this->events['error']) &&
  149. is_callable($func=$this->events['error']))
  150. $func($this);
  151. $this->close($socket);
  152. return FALSE;
  153. }
  154. return $bytes;
  155. }
  156. /**
  157. * Return socket agents
  158. * @return array
  159. * @param $uri string
  160. ***/
  161. function agents($uri=NULL) {
  162. return array_filter(
  163. $this->agents,
  164. function($val) use($uri) {
  165. return $uri?($val->uri()==$uri):TRUE;
  166. }
  167. );
  168. }
  169. /**
  170. * Return event handlers
  171. * @return array
  172. **/
  173. function events() {
  174. return $this->events;
  175. }
  176. /**
  177. * Bind function to event handler
  178. * @return object
  179. * @param $event string
  180. * @param $func callable
  181. **/
  182. function on($event,$func) {
  183. $this->events[$event]=$func;
  184. return $this;
  185. }
  186. /**
  187. * Terminate server
  188. * @return NULL
  189. * @param $signal int
  190. **/
  191. function kill($signal) {
  192. die;
  193. }
  194. /**
  195. * Execute the server process
  196. * @return object
  197. **/
  198. function run() {
  199. $fw=\Base::instance();
  200. // Assign signal handlers
  201. declare(ticks=1);
  202. pcntl_signal(SIGINT,[$this,'kill']);
  203. pcntl_signal(SIGTERM,[$this,'kill']);
  204. gc_enable();
  205. // Activate WebSocket listener
  206. $listen=stream_socket_server(
  207. $this->addr,$errno,$errstr,
  208. STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
  209. $this->ctx
  210. );
  211. $socket=socket_import_stream($listen);
  212. register_shutdown_function(function() use($listen) {
  213. foreach ($this->sockets as $socket)
  214. if ($socket!=$listen)
  215. $this->close($socket);
  216. $this->close($listen);
  217. if (isset($this->events['stop']) &&
  218. is_callable($func=$this->events['stop']))
  219. $func($this);
  220. });
  221. if ($errstr)
  222. user_error($errstr,E_USER_ERROR);
  223. if (isset($this->events['start']) &&
  224. is_callable($func=$this->events['start']))
  225. $func($this);
  226. $this->sockets=[(int)$listen=>$listen];
  227. $empty=[];
  228. $wait=$this->wait;
  229. while (TRUE) {
  230. $active=$this->sockets;
  231. $mark=microtime(TRUE);
  232. $count=@stream_select(
  233. $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait))
  234. );
  235. if (is_bool($count) && $wait) {
  236. if (isset($this->events['error']) &&
  237. is_callable($func=$this->events['error']))
  238. $func($this);
  239. die;
  240. }
  241. if ($count) {
  242. // Process active connections
  243. foreach ($active as $socket) {
  244. if (!is_resource($socket))
  245. continue;
  246. if ($socket==$listen) {
  247. if ($socket=@stream_socket_accept($listen,0))
  248. $this->alloc($socket);
  249. else
  250. if (isset($this->events['error']) &&
  251. is_callable($func=$this->events['error']))
  252. $func($this);
  253. }
  254. else {
  255. $id=(int)$socket;
  256. if (isset($this->agents[$id]))
  257. $this->agents[$id]->fetch();
  258. }
  259. }
  260. $wait-=microtime(TRUE)-$mark;
  261. while ($wait<1e-6) {
  262. $wait+=$this->wait;
  263. $count=0;
  264. }
  265. }
  266. if (!$count) {
  267. $mark=microtime(TRUE);
  268. foreach ($this->sockets as $id=>$socket) {
  269. if (!is_resource($socket))
  270. continue;
  271. if ($socket!=$listen &&
  272. isset($this->agents[$id]) &&
  273. isset($this->events['idle']) &&
  274. is_callable($func=$this->events['idle']))
  275. $func($this->agents[$id]);
  276. }
  277. $wait=$this->wait-microtime(TRUE)+$mark;
  278. }
  279. gc_collect_cycles();
  280. }
  281. }
  282. /**
  283. * Instantiate object
  284. * @return object
  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. $events;
  307. /**
  308. * Return server instance
  309. * @return object
  310. **/
  311. function server() {
  312. return $this->server;
  313. }
  314. /**
  315. * Return socket ID
  316. * @return string
  317. **/
  318. function id() {
  319. return $this->id;
  320. }
  321. /**
  322. * Return socket
  323. * @return object
  324. **/
  325. function socket() {
  326. return $this->socket;
  327. }
  328. /**
  329. * Return request method
  330. * @return string
  331. **/
  332. function verb() {
  333. return $this->verb;
  334. }
  335. /**
  336. * Return request URI
  337. * @return string
  338. **/
  339. function uri() {
  340. return $this->uri;
  341. }
  342. /**
  343. * Return socket headers
  344. * @return string
  345. **/
  346. function headers() {
  347. return $this->headers;
  348. }
  349. /**
  350. * Frame and transmit payload
  351. * @return string|FALSE
  352. * @param $socket resource
  353. * @param $op int
  354. * @param $payload string
  355. **/
  356. function send($op,$data='') {
  357. $server=$this->server;
  358. $mask=WS::Finale | $op & WS::OpCode;
  359. $len=strlen($data);
  360. $buf='';
  361. if ($len>0xffff)
  362. $buf=pack('CCNN',$mask,0x7f,$len);
  363. else
  364. if ($len>0x7d)
  365. $buf=pack('CCn',$mask,0x7e,$len);
  366. else
  367. $buf=pack('CC',$mask,$len);
  368. $buf.=$data;
  369. if (is_bool($server->write($this->socket,$buf)))
  370. return FALSE;
  371. if (!in_array($op,[WS::Pong,WS::Close]) &&
  372. isset($this->events['send']) &&
  373. is_callable($func=$this->events['send']))
  374. $func($this,$op,$data);
  375. return $data;
  376. }
  377. /**
  378. * Retrieve and unmask payload
  379. * @return array|FALSE
  380. **/
  381. function fetch() {
  382. // Unmask payload
  383. $server=$this->server;
  384. if (is_bool($buf=$server->read($this->socket)))
  385. return FALSE;
  386. $op=ord($buf[0]) & WS::OpCode;
  387. $len=ord($buf[1]) & WS::Length;
  388. $pos=2;
  389. if ($len==0x7e) {
  390. $len=ord($buf[2])*256+ord($buf[3]);
  391. $pos+=2;
  392. }
  393. else
  394. if ($len==0x7f) {
  395. for ($i=0,$len=0;$i<8;$i++)
  396. $len=$len*256+ord($buf[$i+2]);
  397. $pos+=8;
  398. }
  399. for ($i=0,$mask=[];$i<4;$i++)
  400. $mask[$i]=ord($buf[$pos+$i]);
  401. $pos+=4;
  402. if (strlen($buf)<$len+$pos)
  403. return FALSE;
  404. for ($i=0,$data='';$i<$len;$i++)
  405. $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]);
  406. // Dispatch
  407. switch ($op & WS::OpCode) {
  408. case WS::Ping:
  409. $this->send(WS::Pong);
  410. break;
  411. case WS::Close:
  412. $server->close($this->socket);
  413. break;
  414. case WS::Text:
  415. $data=trim($data);
  416. case WS::Binary:
  417. if (isset($this->events['receive']) &&
  418. is_callable($func=$this->events['receive']))
  419. $func($this,$op,$data);
  420. break;
  421. }
  422. return [$op,$data];
  423. }
  424. /**
  425. * Destroy object
  426. * @return NULL
  427. **/
  428. function __destruct() {
  429. if (isset($this->events['disconnect']) &&
  430. is_callable($func=$this->events['disconnect']))
  431. $func($this);
  432. }
  433. /**
  434. * Instantiate object
  435. * @return object
  436. * @param $server object
  437. * @param $socket resource
  438. * @param $verb string
  439. * @param $uri string
  440. * @param $hdrs array
  441. **/
  442. function __construct($server,$socket,$verb,$uri,array $hdrs) {
  443. $this->server=$server;
  444. $this->id=stream_socket_get_name($socket,TRUE);
  445. $this->socket=$socket;
  446. $this->verb=$verb;
  447. $this->uri=$uri;
  448. $this->headers=$hdrs;
  449. $this->events=$server->events();
  450. if (isset($this->events['connect']) &&
  451. is_callable($func=$this->events['connect']))
  452. $func($this);
  453. }
  454. }