ws.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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. $agents=[],
  43. $events=[];
  44. /**
  45. * Allocate stream socket
  46. * @return NULL
  47. * @param $socket resource
  48. **/
  49. function alloc($socket) {
  50. if (is_bool($str=$this->read($socket))) {
  51. $this->close($socket);
  52. return;
  53. }
  54. // Get WebSocket headers
  55. $hdrs=[];
  56. $CRLF="\r\n";
  57. $verb=NULL;
  58. $uri=NULL;
  59. foreach (explode($CRLF,trim($str)) as $line)
  60. if (preg_match('/^(\w+)\s(.+)\sHTTP\/1\.\d$/',
  61. trim($line),$match)) {
  62. $verb=$match[1];
  63. $uri=$match[2];
  64. }
  65. else
  66. if (preg_match('/^(.+): (.+)/',trim($line),$match))
  67. // Standardize header
  68. $hdrs[
  69. strtr(
  70. ucwords(
  71. strtolower(
  72. strtr($match[1],'-',' ')
  73. )
  74. ),' ','-'
  75. )
  76. ]=$match[2];
  77. else {
  78. $this->close($socket);
  79. return;
  80. }
  81. if (empty($hdrs['Upgrade']) &&
  82. empty($hdrs['Sec-Websocket-Key'])) {
  83. // Not a WebSocket request
  84. if ($verb && $uri)
  85. $this->write(
  86. $socket,
  87. $str='HTTP/1.1 400 Bad Request'.$CRLF.
  88. 'Connection: close'.$CRLF.$CRLF
  89. );
  90. $this->close($socket);
  91. return;
  92. }
  93. // Handshake
  94. $bytes=$this->write(
  95. $socket,
  96. $str='HTTP/1.1 101 Switching Protocols'.$CRLF.
  97. 'Upgrade: websocket'.$CRLF.
  98. 'Connection: Upgrade'.$CRLF.
  99. 'Sec-WebSocket-Accept: '.
  100. base64_encode(
  101. sha1(
  102. $hdrs['Sec-Websocket-Key'].
  103. self::Magic,
  104. TRUE
  105. )
  106. ).$CRLF.$CRLF
  107. );
  108. if ($bytes) {
  109. // Connect agent to server
  110. $this->sockets[]=$socket;
  111. $this->agents[(int)$socket]=
  112. new Agent($this,$socket,$verb,$uri,$hdrs);
  113. }
  114. else
  115. $this->close($socket);
  116. }
  117. /**
  118. * Close stream socket
  119. * @return NULL
  120. * @param $socket resource
  121. **/
  122. function close($socket) {
  123. stream_socket_shutdown($socket,STREAM_SHUT_WR);
  124. @fclose($socket);
  125. }
  126. /**
  127. * Free stream socket
  128. * @return bool
  129. * @param $socket resource
  130. **/
  131. function free($socket) {
  132. unset($this->sockets[array_search($socket,$this->sockets)]);
  133. unset($this->agents[(int)$socket]);
  134. $this->close($socket);
  135. }
  136. /**
  137. * Read from stream socket
  138. * @return string|FALSE
  139. * @param $socket resource
  140. **/
  141. function read($socket) {
  142. if (is_string($str=@fread($socket,self::Packet)) &&
  143. strlen($str) &&
  144. strlen($str)<self::Packet)
  145. return $str;
  146. if (isset($this->events['error']) &&
  147. is_callable($func=$this->events['error']))
  148. $func($this);
  149. return FALSE;
  150. }
  151. /**
  152. * Write to stream socket
  153. * @return int|FALSE
  154. * @param $socket resource
  155. * @param $str string
  156. **/
  157. function write($socket,$str) {
  158. for ($i=0,$bytes=0;$i<strlen($str);$i+=$bytes) {
  159. if (($bytes=@fwrite($socket,substr($str,$i))) &&
  160. @fflush($socket))
  161. continue;
  162. if (isset($this->events['error']) &&
  163. is_callable($func=$this->events['error']))
  164. $func($this);
  165. return FALSE;
  166. }
  167. return $bytes;
  168. }
  169. /**
  170. * Return socket agents
  171. * @return array
  172. * @param $uri string
  173. ***/
  174. function agents($uri=NULL) {
  175. return array_filter(
  176. $this->agents,
  177. function($val) use($uri) {
  178. return $uri?($val->uri()==$uri):TRUE;
  179. }
  180. );
  181. }
  182. /**
  183. * Return event handlers
  184. * @return array
  185. **/
  186. function events() {
  187. return $this->events;
  188. }
  189. /**
  190. * Bind function to event handler
  191. * @return object
  192. * @param $event string
  193. * @param $func callable
  194. **/
  195. function on($event,$func) {
  196. $this->events[$event]=$func;
  197. return $this;
  198. }
  199. /**
  200. * Terminate server
  201. * @return NULL
  202. * @param $signal int
  203. **/
  204. function kill($signal) {
  205. die;
  206. }
  207. /**
  208. * Execute the server process
  209. * @return object
  210. **/
  211. function run() {
  212. $fw=\Base::instance();
  213. // Assign signal handlers
  214. declare(ticks=1);
  215. pcntl_signal(SIGINT,[$this,'kill']);
  216. pcntl_signal(SIGTERM,[$this,'kill']);
  217. gc_enable();
  218. // Activate WebSocket listener
  219. $listen=stream_socket_server(
  220. $this->addr,$errno,$errstr,
  221. STREAM_SERVER_BIND|STREAM_SERVER_LISTEN,
  222. $this->ctx
  223. );
  224. $socket=socket_import_stream($listen);
  225. register_shutdown_function(function() use($listen) {
  226. foreach ($this->sockets as $socket)
  227. if ($socket!=$listen)
  228. $this->free($socket);
  229. $this->close($listen);
  230. if (isset($this->events['stop']) &&
  231. is_callable($func=$this->events['stop']))
  232. $func($this);
  233. });
  234. if ($errstr)
  235. user_error($errstr,E_USER_ERROR);
  236. if (isset($this->events['start']) &&
  237. is_callable($func=$this->events['start']))
  238. $func($this);
  239. $this->sockets=[$listen];
  240. $empty=[];
  241. $wait=$this->wait;
  242. while (TRUE) {
  243. $active=$this->sockets;
  244. $mark=microtime(TRUE);
  245. $count=@stream_select(
  246. $active,$empty,$empty,(int)$wait,round(1e6*($wait-(int)$wait))
  247. );
  248. if (is_bool($count) && $wait) {
  249. if (isset($this->events['error']) &&
  250. is_callable($func=$this->events['error']))
  251. $func($this);
  252. die;
  253. }
  254. if ($count) {
  255. // Process active connections
  256. foreach ($active as $socket) {
  257. if (!is_resource($socket))
  258. continue;
  259. if ($socket==$listen) {
  260. if ($socket=@stream_socket_accept($listen,0))
  261. $this->alloc($socket);
  262. else
  263. if (isset($this->events['error']) &&
  264. is_callable($func=$this->events['error']))
  265. $func($this);
  266. }
  267. else {
  268. $id=(int)$socket;
  269. if (isset($this->agents[$id]) &&
  270. $raw=$this->agents[$id]->fetch()) {
  271. list($op,$data)=$raw;
  272. // Dispatch
  273. switch ($op & self::OpCode) {
  274. case self::Ping:
  275. $this->agents[$id]->send(self::Pong);
  276. break;
  277. case self::Close:
  278. $this->free($socket);
  279. break;
  280. case self::Text:
  281. $data=trim($data);
  282. case self::Binary:
  283. if (isset($this->events['receive']) &&
  284. is_callable($func=$this->events['receive']))
  285. $func($this->agents[$id],$op,$data);
  286. break;
  287. }
  288. }
  289. }
  290. }
  291. $wait-=microtime(TRUE)-$mark;
  292. while ($wait<1e-6) {
  293. $wait+=$this->wait;
  294. $count=0;
  295. }
  296. }
  297. if (!$count) {
  298. $mark=microtime(TRUE);
  299. foreach ($this->sockets as $socket) {
  300. if (!is_resource($socket))
  301. continue;
  302. $id=(int)$socket;
  303. if ($socket!=$listen &&
  304. isset($this->agents[$id]) &&
  305. isset($this->events['idle']) &&
  306. is_callable($func=$this->events['idle']))
  307. $func($this->agents[$id]);
  308. }
  309. $wait=$this->wait-microtime(TRUE)+$mark;
  310. }
  311. gc_collect_cycles();
  312. }
  313. }
  314. /**
  315. * Instantiate object
  316. * @return object
  317. * @param $addr string
  318. * @param $ctx resource
  319. * @param $wait int
  320. **/
  321. function __construct($addr,$ctx=NULL,$wait=60) {
  322. $this->addr=$addr;
  323. $this->ctx=$ctx?:stream_context_create();
  324. $this->wait=$wait;
  325. $this->events=[];
  326. }
  327. }
  328. //! RFC6455 remote socket
  329. class Agent {
  330. protected
  331. $server,
  332. $id,
  333. $socket,
  334. $flag,
  335. $verb,
  336. $uri,
  337. $headers,
  338. $events,
  339. $buffer;
  340. /**
  341. * Return server instance
  342. * @return object
  343. **/
  344. function server() {
  345. return $this->server;
  346. }
  347. /**
  348. * Return socket ID
  349. * @return string
  350. **/
  351. function id() {
  352. return $this->id;
  353. }
  354. /**
  355. * Return request method
  356. * @return string
  357. **/
  358. function verb() {
  359. return $this->verb;
  360. }
  361. /**
  362. * Return request URI
  363. * @return string
  364. **/
  365. function uri() {
  366. return $this->uri;
  367. }
  368. /**
  369. * Return socket headers
  370. * @return string
  371. **/
  372. function headers() {
  373. return $this->headers;
  374. }
  375. /**
  376. * Frame and transmit payload
  377. * @return string|FALSE
  378. * @param $socket resource
  379. * @param $op int
  380. * @param $payload string
  381. **/
  382. function send($op,$data='') {
  383. $mask=WS::Finale | $op & WS::OpCode;
  384. $len=strlen($data);
  385. $str='';
  386. if ($len>0xffff)
  387. $str=pack('CCNN',$mask,0x7f,$len);
  388. else
  389. if ($len>0x7d)
  390. $str=pack('CCn',$mask,0x7e,$len);
  391. else
  392. $str=pack('CC',$mask,$len);
  393. $str.=$data;
  394. $server=$this->server();
  395. if (is_bool($server->write($this->socket,$str))) {
  396. $this->free();
  397. return FALSE;
  398. }
  399. if (!in_array($op,[WS::Pong,WS::Close]) &&
  400. isset($this->events['send']) &&
  401. is_callable($func=$this->events['send']))
  402. $func($this,$op,$data);
  403. return $data;
  404. }
  405. /**
  406. * Retrieve and unmask payload
  407. * @return array|FALSE
  408. **/
  409. function fetch() {
  410. // Unmask payload
  411. $server=$this->server();
  412. if (is_bool($buf=$server->read($this->socket))) {
  413. $this->free();
  414. return FALSE;
  415. }
  416. $buf=($this->buffer.=$buf);
  417. $op=ord($buf[0]) & WS::OpCode;
  418. $len=ord($buf[1]) & WS::Length;
  419. $pos=2;
  420. if ($len==0x7e) {
  421. $len=ord($buf[2])*256+ord($buf[3]);
  422. $pos+=2;
  423. }
  424. else
  425. if ($len==0x7f) {
  426. for ($i=0,$len=0;$i<8;$i++)
  427. $len=$len*256+ord($buf[$i+2]);
  428. $pos+=8;
  429. }
  430. for ($i=0,$mask=[];$i<4;$i++)
  431. $mask[$i]=ord($buf[$pos+$i]);
  432. $pos+=4;
  433. if (strlen($buf)<$len+$pos)
  434. return FALSE;
  435. for ($i=0,$data='';$i<$len;$i++)
  436. $data.=chr(ord($buf[$pos+$i])^$mask[$i%4]);
  437. $this->buffer='';
  438. return [$op,$data];
  439. }
  440. /**
  441. * Free stream socket
  442. * @return NULL
  443. **/
  444. function free() {
  445. $this->server->free($this->socket);
  446. }
  447. /**
  448. * Destroy object
  449. * @return NULL
  450. **/
  451. function __destruct() {
  452. if (isset($this->events['disconnect']) &&
  453. is_callable($func=$this->events['disconnect']))
  454. $func($this);
  455. }
  456. /**
  457. * Instantiate object
  458. * @return object
  459. * @param $server object
  460. * @param $socket resource
  461. * @param $verb string
  462. * @param $uri string
  463. * @param $hdrs array
  464. **/
  465. function __construct($server,$socket,$verb,$uri,array $hdrs) {
  466. $this->server=$server;
  467. $this->id=stream_socket_get_name($socket,TRUE);
  468. $this->socket=$socket;
  469. $this->verb=$verb;
  470. $this->uri=$uri;
  471. $this->headers=$hdrs;
  472. $this->events=$server->events();
  473. $this->buffer='';
  474. if (isset($this->events['connect']) &&
  475. is_callable($func=$this->events['connect']))
  476. $func($this);
  477. }
  478. }