3 * sac-a-push - Obj/transports.phh
5 * Copyright (C) 2012 Matteo Nastasi
6 * mailto: nastasi@alternativeoutput.it
7 * matteo.nastasi@milug.org
8 * web: http://www.alternativeoutput.it
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
15 * This program is distributed in the hope that it will be useful, but
16 * WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABLILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18 * General Public License for more details. You should have received a
19 * copy of the GNU General Public License along with this program; if
20 * not, write to the Free Software Foundation, Inc, 59 Temple Place -
21 * Suite 330, Boston, MA 02111-1307, USA.
27 * Values: Y: works, N: not works, @: continuous download,
28 * D: continuous download after first reload
32 * Iframe| IW | FF | Ch | Op | Ko | IE
33 * ------+----+----+----+----+----+----
34 * Lnx | D | | @ | | @ | x
35 * Win | x | D | @ | @ | | D
39 * WS | IW | FF | Ch | Op | Ko | IE
40 * ------+----+----+----+----+----+----
46 * XHR | IW | FF | Ch | Op | Ko | IE
47 * ------+----+----+----+----+----+----
48 * Lnx | Y | | ^D | | Y | x
49 * Win | x | Y | Y | | | N
53 * HtmlFl| IW | FF | Ch | Op | Ko | IE
54 * ------+----+----+----+----+----+----
56 * Win | x | N | N | | | Y* (* seems delay between click and load of a new page)
62 class Transport_template {
64 function Transport_template() {
67 // return string value is appended to the content of the returned page
68 // return FALSE if fails
69 // check with '===' operator to disambiguation between "" and FALSE return value
70 function init($enc, $header, &$header_out, $init_string, $base, $step)
78 function postclose_get($sock, $curtime)
83 function chunk($step, $cont)
91 // return string to add to the stream to perform something to the engine
92 static function fini($init_string, $base, $blockerr)
98 define("TRANSP_WS_CLOSE_TOUT", 5);
100 class Transport_websocket_postclose {
101 function Transport_websocket_postclose($transp_ws, $sock, $curtime) {
102 printf("POSTCLOSE: Creation\n");
103 $this->transp_ws = $transp_ws;
105 $this->start = $curtime;
106 // status not required, currently
107 // $this->status = "begin";
110 function read($payload, $curtime) {
111 if ($this->start + TRANSP_WS_CLOSE_TOUT < $curtime) {
112 printf("POSTCLOSE: Closing ws (%d) force close by timeout\n", $this->sock);
115 if (mb_strlen($payload, "ASCII") > 1) {
116 $this->transp_ws->unchunk($payload, $this->sock);
118 if ($this->transp_ws->hasSentClose) {
119 printf("POSTCLOSE: Closing ws gracefully\n");
123 printf("POSTCLOSE: not yet finished\n");
130 class Transport_websocket {
131 protected $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
133 function Transport_websocket($secure = FALSE) {
134 $this->type = ($secure == FALSE ? "websocket" : "websocketsec");
135 $this->headerOriginRequired = false;
136 $this->headerSecWebSocketProtocolRequired = false;
137 $this->headerSecWebSocketExtensionsRequired = false;
139 $this->sendingContinuous = false;
141 $this->handlingPartialPacket = false;
142 $this->partialMessage = "";
144 $this->hasSentClose = false;
147 protected function extractHeaders($message) {
148 $header = array('fin' => $message[0] & chr(128),
149 'rsv1' => $message[0] & chr(64),
150 'rsv2' => $message[0] & chr(32),
151 'rsv3' => $message[0] & chr(16),
152 'opcode' => ord($message[0]) & 15,
153 'hasmask' => $message[1] & chr(128),
156 $header['length'] = (ord($message[1]) >= 128) ? ord($message[1]) - 128 : ord($message[1]);
158 if ($header['length'] == 126) {
159 if ($header['hasmask']) {
160 $header['mask'] = $message[4] . $message[5] . $message[6] . $message[7];
162 $header['length'] = ord($message[2]) * 256
164 } elseif ($header['length'] == 127) {
165 if ($header['hasmask']) {
166 $header['mask'] = $message[10] . $message[11] . $message[12] . $message[13];
168 $header['length'] = ord($message[2]) * 65536 * 65536 * 65536 * 256
169 + ord($message[3]) * 65536 * 65536 * 65536
170 + ord($message[4]) * 65536 * 65536 * 256
171 + ord($message[5]) * 65536 * 65536
172 + ord($message[6]) * 65536 * 256
173 + ord($message[7]) * 65536
174 + ord($message[8]) * 256
176 } elseif ($header['hasmask']) {
177 $header['mask'] = $message[2] . $message[3] . $message[4] . $message[5];
179 //echo $this->strtohex($message);
180 //$this->printHeaders($header);
184 protected function extractPayload($message,$headers) {
186 if ($headers['hasmask']) {
189 if ($headers['length'] > 65535) {
191 } elseif ($headers['length'] > 125) {
194 return substr($message,$offset);
197 protected function applyMask($headers,$payload) {
199 if ($headers['hasmask']) {
200 $mask = $headers['mask'];
205 while (mb_strlen($effectiveMask, "ASCII") < mb_strlen($payload, "ASCII")) {
206 $effectiveMask .= $mask;
208 while (mb_strlen($effectiveMask, "ASCII") > mb_strlen($payload, "ASCII")) {
209 $effectiveMask = substr($effectiveMask,0,-1);
211 return $effectiveMask ^ $payload;
214 protected function checkRSVBits($headers,$user) { // override this method if you are using an extension where the RSV bits are used.
215 if (ord($headers['rsv1']) + ord($headers['rsv2']) + ord($headers['rsv3']) > 0) {
216 //$this->disconnect($user); // todo: fail connection
222 protected function strtohex($str) {
224 for ($i = 0; $i < mb_strlen($str, "ASCII"); $i++) {
225 $strout .= (ord($str[$i])<16) ? "0" . dechex(ord($str[$i])) : dechex(ord($str[$i]));
240 return $strout . "\n";
243 function unchunk($cont, $sock)
245 // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
246 return $this->deframe($cont, $sock);
249 function chunk($step, $cont)
251 // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
252 return $this->frame('@BEGIN@'.$cont.'@END@'); // , 'text', TRUE);
255 protected function frame($message, $messageType='text', $messageContinues=false) {
256 switch ($messageType) {
261 $b1 = ($this->sendingContinuous) ? 0 : 1;
264 $b1 = ($this->sendingContinuous) ? 0 : 2;
276 if ($messageContinues) {
277 $this->sendingContinuous = true;
280 $this->sendingContinuous = false;
283 $length = mb_strlen($message, "ASCII");
287 } elseif ($length <= 65536) {
289 $hexLength = dechex($length);
290 //$this->stdout("Hex Length: $hexLength");
291 if (mb_strlen($hexLength, "ASCII")%2 == 1) {
292 $hexLength = '0' . $hexLength;
294 $n = mb_strlen($hexLength, "ASCII") - 2;
296 for ($i = $n; $i >= 0; $i=$i-2) {
297 $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
299 while (mb_strlen($lengthField, "ASCII") < 2) {
300 $lengthField = chr(0) . $lengthField;
304 $hexLength = dechex($length);
305 if (mb_strlen($hexLength, "ASCII")%2 == 1) {
306 $hexLength = '0' . $hexLength;
308 $n = mb_strlen($hexLength, "ASCII") - 2;
310 for ($i = $n; $i >= 0; $i=$i-2) {
311 $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
313 while (mb_strlen($lengthField, "ASCII") < 8) {
314 $lengthField = chr(0) . $lengthField;
318 return chr($b1) . chr($b2) . $lengthField . $message;
321 protected function deframe($message, $socket) {
322 //echo $this->strtohex($message);
323 $headers = $this->extractHeaders($message);
326 switch($headers['opcode']) {
332 // todo: close the connection
333 $this->hasSentClose = true;
340 //$this->disconnect($user); // todo: fail connection
345 if ($this->handlingPartialPacket) {
346 $message = $this->partialBuffer . $message;
347 $this->handlingPartialPacket = false;
348 return $this->deframe($message);
351 if ($this->checkRSVBits($headers,$this)) {
356 // todo: fail the connection
360 $payload = $this->partialMessage . $this->extractPayload($message,$headers);
363 $reply = $this->frame($payload,$this,'pong');
364 // TODO FIXME ALL socket_write management
365 // socket_write($user->socket,$reply,mb_strlen($reply, "ASCII"));
366 @fwrite($socket, $reply, mb_strlen($reply, "ASCII"));
369 if (extension_loaded('mbstring')) {
370 if ($headers['length'] > mb_strlen($payload, "ASCII")) {
371 $this->handlingPartialPacket = true;
372 $this->partialBuffer = $message;
376 if ($headers['length'] > mb_strlen($payload, "ASCII")) {
377 $this->handlingPartialPacket = true;
378 $this->partialBuffer = $message;
383 $payload = $this->applyMask($headers,$payload);
385 if ($headers['fin']) {
386 $this->partialMessage = "";
389 $this->partialMessage = $payload;
394 protected function checkHost($hostName) {
395 return true; // Override and return false if the host is not one that you would expect.
396 // Ex: You only want to accept hosts from the my-domain.com domain,
397 // but you receive a host from malicious-site.com instead.
400 protected function checkOrigin($origin) {
401 return true; // Override and return false if the origin is not one that you would expect.
404 protected function checkWebsocProtocol($protocol) {
405 return true; // Override and return false if a protocol is not found that you would expect.
408 protected function checkWebsocExtensions($extensions) {
409 return true; // Override and return false if an extension is not found that you would expect.
412 protected function processProtocol($protocol) {
413 return ""; // return either "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" or return an empty string.
414 // The carriage return/newline combo must appear at the end of a non-empty string, and must not
415 // appear at the beginning of the string nor in an otherwise empty string, or it will be considered part of
416 // the response body, which will trigger an error in the client as it will not be formatted correctly.
419 protected function processExtensions($extensions) {
420 return ""; // return either "Sec-WebSocket-Extensions: SelectedExtensions\r\n" or return an empty string.
423 function init($enc, $headers, &$headers_out, $init_string, $base, $step)
425 if (0) { // TODO: what is ?
426 if (isset($headers['get'])) {
427 $this->requestedResource = $headers['get'];
429 // todo: fail the connection
430 $headers_out['HTTP-Response'] = "405 Method Not Allowed";
434 if (!isset($headers['Host']) || !$this->checkHost($headers['Host'])) {
435 // error_log('bad 1');
436 $headers_out['HTTP-Response'] = "400 Bad Request";
438 if (!isset($headers['Upgrade']) || strtolower($headers['Upgrade']) != 'websocket') {
439 // error_log('bad 2 ' . $headers['Upgrade']);
440 $headers_out['HTTP-Response'] = "400 Bad Request";
442 if (!isset($headers['Connection']) || strpos(strtolower($headers['Connection']), 'upgrade') === FALSE) {
443 // error_log('bad 3');
444 $headers_out['HTTP-Response'] = "400 Bad Request";
446 if (!isset($headers['Sec-Websocket-Key'])) {
447 // error_log('bad 4');
448 $headers_out['HTTP-Response'] = "400 Bad Request";
452 if (!isset($headers['Sec-Websocket-Version']) || strtolower($headers['Sec-Websocket-Version']) != 13) {
453 $headers_out['HTTP-Response'] = "426 Upgrade Required";
454 $headers_out['Sec-WebSocketVersion'] = "13";
456 if ( ($this->headerOriginRequired && !isset($headers['Origin']) )
457 || ($this->headerOriginRequired && !$this->checkOrigin($headers['Origin'])) ) {
458 $headers_out['HTTP-Response'] = "403 Forbidden";
460 if ( ($this->headerSecWebSocketProtocolRequired && !isset($headers['Sec-Websocket-Protocol']))
461 || ($this->headerSecWebSocketProtocolRequired &&
462 !$this->checkWebsocProtocol($headers['Sec-Websocket-Protocol']))) {
463 // error_log('bad 5');
464 $headers_out['HTTP-Response'] = "400 Bad Request";
466 if ( ($this->headerSecWebSocketExtensionsRequired && !isset($headers['Sec-Websocket-Extensions']))
467 || ($this->headerSecWebSocketExtensionsRequired &&
468 !$this->checkWebsocExtensions($headers['Sec-Websocket-Extensions'])) ) {
469 // error_log('bad 6');
470 $headers_out['HTTP-Response'] = "400 Bad Request";
473 if (isset($headers_out['HTTP-Response'])) {
474 // TODO: check return management
478 // TODO: verify both variables
479 // here there is a change of the socket status from start to handshaked
480 // th headers are saved too but without any further access so we skip it
484 $inno = 'x3JJHMbDL1EzLkh9GBhXDw==';
485 $outo = sha1($inno . $this->magicGUID);
487 for ($i = 0; $i < 20; $i++) {
488 $rawToken .= chr(hexdec(substr($outo,$i*2, 2)));
491 $outo = base64_encode($rawToken);
493 $webSocketKeyHash = sha1($headers['Sec-Websocket-Key'] . $this->magicGUID);
495 for ($i = 0; $i < 20; $i++) {
496 $rawToken .= chr(hexdec(substr($webSocketKeyHash,$i*2, 2)));
498 $handshakeToken = base64_encode($rawToken);
499 $subProtocol = (isset($headers['Sec-Websocket-Protocol'])) ?
500 $this->processProtocol($headers['Sec-Websocket-Protocol']) : "";
501 $extensions = (isset($headers['Sec-Websocket-Extensions'])) ?
502 $this->processExtensions($headers['Sec-Websocket-Extensions']) : "";
504 $headers_out['HTTP-Response'] = "101 Switching Protocols";
505 $headers_out['Upgrade'] = 'websocket';
506 $headers_out['Connection'] = 'Upgrade';
507 $headers_out['Sec-WebSocket-Accept'] = "$handshakeToken$subProtocol$extensions";
512 static function close()
514 return(chr(0x88).chr(0x02).chr(0xe8).chr(0x03));
517 function postclose_get($sock, $curtime)
519 return new Transport_websocket_postclose($this, $sock, $curtime);
522 static function fini($init_string, $base, $blockerr)
524 return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@', ($blockerr ? 'xstm.stop(); ' : ''), $base).self::close());
527 function is_chunked()
534 class Transport_xhr {
536 function Transport_xhr() {
540 function init($enc, $header, &$header_out, $init_string, $base, $step)
542 $ret = sprintf("@BEGIN@ /* %s */ @END@", $init_string);
544 $header_out['Content-Encoding'] = $enc;
545 $header_out['Cache-Control'] = 'no-cache, must-revalidate'; // HTTP/1.1
546 $header_out['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; // Date in the past
547 $header_out['Content-type'] = 'application/xml; charset="utf-8"';
557 function postclose_get($sock, $curtime)
562 static function fini($init_string, $base, $blockerr)
564 return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@', ($blockerr ? 'xstm.stop(); ' : ''), $base));
568 function chunk($step, $cont)
570 // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
571 return ("@BEGIN@".$cont."@END@");
574 function is_chunked()
580 class Transport_iframe {
582 function Transport_iframe() {
583 $this->type = 'iframe';
586 function init($enc, $header, &$header_out, $init_string, $base, $step)
591 $header_out['Content-Encoding'] = $enc;
592 $header_out['Cache-Control'] = 'no-cache, must-revalidate'; // HTTP/1.1
593 $header_out['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; // Date in the past
594 $header_out['Content-type'] = 'text/html; charset="utf-8"';
596 $ret .= sprintf("<html>
598 <script type=\"text/javascript\" src=\"%scommons.js?v=%s\"></script>
599 <script type=\"text/javascript\" src=\"%sxynt-streaming-ifra.js?v=%s\"></script>
600 <script type=\"text/javascript\">
601 var xynt_streaming = \"ready\";", $base, BSK_BUSTING, $base, BSK_BUSTING);
603 $ret .= sprintf("last_clean = %d;\n", ($step-1));
605 window.onload = function () { try { if (xynt_streaming != \"ready\") { xynt_streaming.transp.stopped = true; } } catch(e) { /* console.log(\"catcha\"); */ } };
609 $ret .= sprintf("<!-- \n%s -->\n", $init_string);
619 function postclose_get($sock, $curtime)
624 static function fini($init_string, $base, $blockerr)
627 $ret .= sprintf("<html>
629 <script type=\"text/javascript\" src=\"%scommons.js?v=%s\"></script>
630 <script type=\"text/javascript\" src=\"%sxynt-streaming-ifra.js?v=%s\"></script>
631 <script type=\"text/javascript\">
632 var xynt_streaming = \"ready\";", $base, BSK_BUSTING, $base, BSK_BUSTING);
634 window.onload = function () { try { if (xynt_streaming != \"ready\") { xynt_streaming.reload(); } } catch(e) { /* console.log(\"catcha\"); */ } };
638 $ret .= sprintf("<!-- \n%s -->\n", $init_string);
639 $ret .= sprintf("<script id='hs%d' type='text/javascript'><!--
642 </script>", 0, escpush($blockerr) );
646 function chunk($step, $cont)
648 // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
650 return sprintf("<script id='hs%d' type='text/javascript'><!--
651 push(null);\n// -->\n</script>", $step);
654 return sprintf("<script id='hs%d' type='text/javascript'><!--
655 push(\"%s\");\n// -->\n</script>", $step, escpush($cont) );
659 function is_chunked()
665 class Transport_htmlfile extends Transport_iframe {
666 function Transport_htmlfile() {
667 $this->type = 'htmlfile';
670 function postclose_get($sock, $curtime)
681 static function create($transp)
683 if ($transp == 'websocket' || $transp == 'websocketsec') {
684 return new Transport_websocket($transp == 'websocketsec');
686 else if ($transp == 'xhr') {
687 return new Transport_xhr();
689 else if ($transp == 'htmlfile') {
690 return new Transport_htmlfile();
693 return new Transport_iframe();
696 static function gettype($transp)
698 if ($transp == 'websocket' || $transp == 'xhr' || $transp == 'htmlfile') {
699 return "Transport_".$transp;
702 return 'Transport_iframe';