version updated
[brisk.git] / web / Obj / transports.phh
1 <?php
2 /*
3  *  sac-a-push - Obj/transports.phh
4  *
5  *  Copyright (C) 2012 Matteo Nastasi
6  *                          mailto: nastasi@alternativeoutput.it
7  *                                  matteo.nastasi@milug.org
8  *                          web: http://www.alternativeoutput.it
9  *
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.
14  *
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.
22  *
23  */
24
25 /*
26  *  test: SO x Browser
27  *  Values: Y: works, N: not works, @: continuous download,
28  *          D: continuous download after first reload
29  *
30  *  Stream IFRAME:
31  *
32  * Iframe| IW | FF | Ch | Op | Ko | IE
33  * ------+----+----+----+----+----+----
34  *   Lnx | D  |    | @  |    | @  | x
35  *   Win | x  | D  | @  | @  |    | D
36  *   Mac | x  |    |    |    |    |
37  *
38  *
39  *   WS  | IW | FF | Ch | Op | Ko | IE
40  * ------+----+----+----+----+----+----
41  *   Lnx |    |    |    |    |    |
42  *   Win |    |    |    |    |    |
43  *   Mac |    |    |    |    |    |
44  *
45  *
46  *   XHR | IW | FF | Ch | Op | Ko | IE
47  * ------+----+----+----+----+----+----
48  *   Lnx | Y  |    | ^D |    | Y  | x
49  *   Win | x  | Y  | Y  |    |    | N
50  *   Mac | x  |    |    |    |    |
51  *
52  *
53  * HtmlFl| IW | FF | Ch | Op | Ko | IE
54  * ------+----+----+----+----+----+----
55  *   Lnx | N  |    |    |    | N  |
56  *   Win | x  | N  | N  |    |    | Y* (* seems delay between click and load of a new page)
57  *   Mac | x  |    |    |    |    |
58  *
59  *
60  */
61
62 class Transport_template {
63
64     function Transport_template() {
65     }
66
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)
71     {
72     }
73
74     function close()
75     {
76     }
77
78     function chunk($step, $cont)
79     {
80     }
81
82     function is_chunked()
83     {
84     }
85
86     // return string to add to the stream to perform something to the engine
87     static function fini($init_string, $base, $blockerr)
88     {
89         return "";
90     }
91 }
92
93 class Transport_websocket {
94     protected $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
95
96     function Transport_websocket() {
97         $this->headerOriginRequired                 = false;
98         $this->headerSecWebSocketProtocolRequired   = false;
99         $this->headerSecWebSocketExtensionsRequired = false;
100
101         $this->sendingContinuous = false;
102         $this->sendingContinuous = false;
103         $this->partialMessage = "";
104
105         $this->hasSentClose = false;
106     }
107
108     protected function extractHeaders($message) {
109         $header = array('fin'     => $message[0] & chr(128),
110                         'rsv1'    => $message[0] & chr(64),
111                         'rsv2'    => $message[0] & chr(32),
112                         'rsv3'    => $message[0] & chr(16),
113                         'opcode'  => ord($message[0]) & 15,
114                         'hasmask' => $message[1] & chr(128),
115                         'length'  => 0,
116                         'mask'    => "");
117         $header['length'] = (ord($message[1]) >= 128) ? ord($message[1]) - 128 : ord($message[1]);
118
119         if ($header['length'] == 126) {
120             if ($header['hasmask']) {
121                 $header['mask'] = $message[4] . $message[5] . $message[6] . $message[7];
122             }
123             $header['length'] = ord($message[2]) * 256
124                 + ord($message[3]);
125         } elseif ($header['length'] == 127) {
126             if ($header['hasmask']) {
127                 $header['mask'] = $message[10] . $message[11] . $message[12] . $message[13];
128             }
129             $header['length'] = ord($message[2]) * 65536 * 65536 * 65536 * 256
130                 + ord($message[3]) * 65536 * 65536 * 65536
131                 + ord($message[4]) * 65536 * 65536 * 256
132                 + ord($message[5]) * 65536 * 65536
133                 + ord($message[6]) * 65536 * 256
134                 + ord($message[7]) * 65536
135                 + ord($message[8]) * 256
136                 + ord($message[9]);
137         } elseif ($header['hasmask']) {
138             $header['mask'] = $message[2] . $message[3] . $message[4] . $message[5];
139         }
140         //echo $this->strtohex($message);
141         //$this->printHeaders($header);
142         return $header;
143     }
144
145     protected function extractPayload($message,$headers) {
146         $offset = 2;
147         if ($headers['hasmask']) {
148             $offset += 4;
149         }
150         if ($headers['length'] > 65535) {
151             $offset += 8;
152         } elseif ($headers['length'] > 125) {
153             $offset += 2;
154         }
155         return substr($message,$offset);
156     }
157
158     protected function applyMask($headers,$payload) {
159         $effectiveMask = "";
160         if ($headers['hasmask']) {
161             $mask = $headers['mask'];
162         } else {
163             return $payload;
164         }
165
166         while (mb_strlen($effectiveMask, "ASCII") < mb_strlen($payload, "ASCII")) {
167             $effectiveMask .= $mask;
168         }
169         while (mb_strlen($effectiveMask, "ASCII") > mb_strlen($payload, "ASCII")) {
170             $effectiveMask = substr($effectiveMask,0,-1);
171         }
172         return $effectiveMask ^ $payload;
173     }
174
175     protected function checkRSVBits($headers,$user) { // override this method if you are using an extension where the RSV bits are used.
176         if (ord($headers['rsv1']) + ord($headers['rsv2']) + ord($headers['rsv3']) > 0) {
177             //$this->disconnect($user); // todo: fail connection
178             return true;
179         }
180         return false;
181     }
182
183     protected function strtohex($str) {
184         $strout = "";
185         for ($i = 0; $i < mb_strlen($str, "ASCII"); $i++) {
186             $strout .= (ord($str[$i])<16) ? "0" . dechex(ord($str[$i])) : dechex(ord($str[$i]));
187             $strout .= " ";
188             if ($i%32 == 7) {
189                 $strout .= ": ";
190             }
191             if ($i%32 == 15) {
192                 $strout .= ": ";
193             }
194             if ($i%32 == 23) {
195                 $strout .= ": ";
196             }
197             if ($i%32 == 31) {
198                 $strout .= "\n";
199             }
200         }
201         return $strout . "\n";
202     }
203
204     function chunk($step, $cont)
205     {
206         // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
207         return $this->frame('@BEGIN@'.$cont.'@END@'); // , 'text', TRUE);
208     }
209
210     protected function frame($message, $messageType='text', $messageContinues=false) {
211         switch ($messageType) {
212         case 'continuous':
213             $b1 = 0;
214             break;
215         case 'text':
216             $b1 = ($this->sendingContinuous) ? 0 : 1;
217             break;
218         case 'binary':
219             $b1 = ($this->sendingContinuous) ? 0 : 2;
220             break;
221         case 'close':
222             $b1 = 8;
223             break;
224         case 'ping':
225             $b1 = 9;
226             break;
227         case 'pong':
228             $b1 = 10;
229             break;
230         }
231         if ($messageContinues) {
232             $this->sendingContinuous = true;
233         } else {
234             $b1 += 128;
235             $this->sendingContinuous = false;
236         }
237
238         $length = mb_strlen($message, "ASCII");
239         $lengthField = "";
240         if ($length < 126) {
241             $b2 = $length;
242         } elseif ($length <= 65536) {
243             $b2 = 126;
244             $hexLength = dechex($length);
245             //$this->stdout("Hex Length: $hexLength");
246             if (mb_strlen($hexLength, "ASCII")%2 == 1) {
247                 $hexLength = '0' . $hexLength;
248             }
249             $n = mb_strlen($hexLength, "ASCII") - 2;
250
251             for ($i = $n; $i >= 0; $i=$i-2) {
252                 $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
253             }
254             while (mb_strlen($lengthField, "ASCII") < 2) {
255                 $lengthField = chr(0) . $lengthField;
256             }
257         } else {
258             $b2 = 127;
259             $hexLength = dechex($length);
260             if (mb_strlen($hexLength, "ASCII")%2 == 1) {
261                 $hexLength = '0' . $hexLength;
262             }
263             $n = mb_strlen($hexLength, "ASCII") - 2;
264
265             for ($i = $n; $i >= 0; $i=$i-2) {
266                 $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
267             }
268             while (mb_strlen($lengthField, "ASCII") < 8) {
269                 $lengthField = chr(0) . $lengthField;
270             }
271         }
272
273         return chr($b1) . chr($b2) . $lengthField . $message;
274     }
275
276     protected function deframe($message) {
277         //echo $this->strtohex($message);
278         $headers = $this->extractHeaders($message);
279         $pongReply = false;
280         $willClose = false;
281         switch($headers['opcode']) {
282         case 0:
283         case 1:
284         case 2:
285             break;
286         case 8:
287             // todo: close the connection
288             $this->hasSentClose = true;
289             return "";
290         case 9:
291             $pongReply = true;
292         case 10:
293             break;
294         default:
295             //$this->disconnect($user); // todo: fail connection
296             $willClose = true;
297             break;
298         }
299
300         if ($this->handlingPartialPacket) {
301             $message = $this->partialBuffer . $message;
302             $this->handlingPartialPacket = false;
303             return $this->deframe($message);
304         }
305
306         if ($this->checkRSVBits($headers,$this)) {
307             return false;
308         }
309
310         if ($willClose) {
311             // todo: fail the connection
312             return false;
313         }
314
315         $payload = $this->partialMessage . $this->extractPayload($message,$headers);
316
317         if ($pongReply) {
318             $reply = $this->frame($payload,$this,'pong');
319             // TODO FIXME ALL socket_write management
320             socket_write($user->socket,$reply,mb_strlen($reply, "ASCII"));
321             return false;
322         }
323         if (extension_loaded('mbstring')) {
324             if ($headers['length'] > mb_strlen($payload, "ASCII")) {
325                 $this->handlingPartialPacket = true;
326                 $this->partialBuffer = $message;
327                 return false;
328             }
329         } else {
330             if ($headers['length'] > mb_strlen($payload, "ASCII")) {
331                 $this->handlingPartialPacket = true;
332                 $this->partialBuffer = $message;
333                 return false;
334             }
335         }
336
337         $payload = $this->applyMask($headers,$payload);
338
339         if ($headers['fin']) {
340             $this->partialMessage = "";
341             return $payload;
342         }
343         $this->partialMessage = $payload;
344         return false;
345     }
346
347
348     protected function checkHost($hostName) {
349         return true; // Override and return false if the host is not one that you would expect.
350         // Ex: You only want to accept hosts from the my-domain.com domain,
351         // but you receive a host from malicious-site.com instead.
352     }
353
354     protected function checkOrigin($origin) {
355         return true; // Override and return false if the origin is not one that you would expect.
356     }
357
358     protected function checkWebsocProtocol($protocol) {
359         return true; // Override and return false if a protocol is not found that you would expect.
360     }
361
362     protected function checkWebsocExtensions($extensions) {
363         return true; // Override and return false if an extension is not found that you would expect.
364     }
365
366     protected function processProtocol($protocol) {
367         return ""; // return either "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" or return an empty string.
368         // The carriage return/newline combo must appear at the end of a non-empty string, and must not
369         // appear at the beginning of the string nor in an otherwise empty string, or it will be considered part of
370         // the response body, which will trigger an error in the client as it will not be formatted correctly.
371     }
372
373     protected function processExtensions($extensions) {
374         return ""; // return either "Sec-WebSocket-Extensions: SelectedExtensions\r\n" or return an empty string.
375     }
376
377     function init($enc, $headers, &$headers_out, $init_string, $base, $step)
378     {
379         if (0) { // TODO: what is ?
380             if (isset($headers['get'])) {
381                 $this->requestedResource = $headers['get'];
382             } else {
383                 // todo: fail the connection
384                 $headers_out['HTTP-Response'] = "405 Method Not Allowed";
385             }
386         }
387
388         if (!isset($headers['Host']) || !$this->checkHost($headers['Host'])) {
389             $headers_out['HTTP-Response'] = "400 Bad Request";
390         }
391         if (!isset($headers['Upgrade']) || strtolower($headers['Upgrade']) != 'websocket') {
392             $headers_out['HTTP-Response'] = "400 Bad Request";
393         }
394         if (!isset($headers['Connection']) || strpos(strtolower($headers['Connection']), 'upgrade') === FALSE) {
395             $headers_out['HTTP-Response'] = "400 Bad Request";
396         }
397         if (!isset($headers['Sec-Websocket-Key'])) {
398             $headers_out['HTTP-Response'] = "400 Bad Request";
399         } else {
400         }
401
402         if (!isset($headers['Sec-Websocket-Version']) || strtolower($headers['Sec-Websocket-Version']) != 13) {
403             $headers_out['HTTP-Response'] = "426 Upgrade Required";
404             $headers_out['Sec-WebSocketVersion'] = "13";
405         }
406         if ( ($this->headerOriginRequired && !isset($headers['Origin']) )
407              || ($this->headerOriginRequired && !$this->checkOrigin($headers['Origin'])) ) {
408             $headers_out['HTTP-Response'] = "403 Forbidden";
409         }
410         if ( ($this->headerSecWebSocketProtocolRequired && !isset($headers['Sec-Websocket-Protocol']))
411              || ($this->headerSecWebSocketProtocolRequired &&
412                  !$this->checkWebsocProtocol($headers['Sec-Websocket-Protocol']))) {
413             $headers_out['HTTP-Response'] = "400 Bad Request";
414         }
415         if ( ($this->headerSecWebSocketExtensionsRequired  && !isset($headers['Sec-Websocket-Extensions']))
416              || ($this->headerSecWebSocketExtensionsRequired &&
417                  !$this->checkWebsocExtensions($headers['Sec-Websocket-Extensions'])) ) {
418             $headers_out['HTTP-Response'] = "400 Bad Request";
419         }
420
421         if (isset($headers_out['HTTP-Response'])) {
422             // TODO: check return management
423             return (FALSE);
424         }
425
426         // TODO: verify both variables
427         // here there is a change of the socket status from start to handshaked
428         // th headers are saved too but without any further access so we skip it
429
430
431
432         $inno = 'x3JJHMbDL1EzLkh9GBhXDw==';
433         $outo = sha1($inno . $this->magicGUID);
434         $rawToken = "";
435         for ($i = 0; $i < 20; $i++) {
436             $rawToken .= chr(hexdec(substr($outo,$i*2, 2)));
437         }
438
439         $outo = base64_encode($rawToken);
440
441         $webSocketKeyHash = sha1($headers['Sec-Websocket-Key'] . $this->magicGUID);
442         $rawToken = "";
443         for ($i = 0; $i < 20; $i++) {
444             $rawToken .= chr(hexdec(substr($webSocketKeyHash,$i*2, 2)));
445         }
446         $handshakeToken = base64_encode($rawToken);
447         $subProtocol = (isset($headers['Sec-Websocket-Protocol'])) ?
448             $this->processProtocol($headers['Sec-Websocket-Protocol']) : "";
449         $extensions = (isset($headers['Sec-Websocket-Extensions'])) ?
450             $this->processExtensions($headers['Sec-Websocket-Extensions']) : "";
451
452         $headers_out['HTTP-Response'] = "101 Switching Protocols";
453         $headers_out['Upgrade']       = 'websocket';
454         $headers_out['Connection']    = 'Upgrade';
455         $headers_out['Sec-WebSocket-Accept'] = "$handshakeToken$subProtocol$extensions";
456
457         return ("");
458     }
459
460     static function close()
461     {
462         return(chr(0x88).chr(0x02).chr(0xe8).chr(0x03));
463     }
464
465     static function fini($init_string, $base, $blockerr)
466     {
467         return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@',  ($blockerr ? 'xstm.stop(); ' : ''), $base).self::close());
468     }
469
470     function is_chunked()
471     {
472         return FALSE;
473     }
474
475 }
476
477 class Transport_xhr {
478
479     function Transport_xhr() {
480     }
481
482     function init($enc, $header, &$header_out, $init_string, $base, $step)
483     {
484         $ret = sprintf("@BEGIN@ /* %s */ @END@", $init_string);
485         if ($enc != 'plain')
486             $header_out['Content-Encoding'] = $enc;
487         $header_out['Cache-Control'] = 'no-cache, must-revalidate';     // HTTP/1.1
488         $header_out['Expires']       = 'Mon, 26 Jul 1997 05:00:00 GMT'; // Date in the past
489         $header_out['Content-type']  = 'application/xml; charset="utf-8"';
490
491         return ($ret);
492     }
493
494     function close()
495     {
496         return "";
497     }
498
499     static function fini($init_string, $base, $blockerr)
500     {
501         return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@',  ($blockerr ? 'xstm.stop(); ' : ''), $base));
502         return ("");
503     }
504
505     function chunk($step, $cont)
506     {
507         // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
508         return ("@BEGIN@".$cont."@END@");
509     }
510
511     function is_chunked()
512     {
513         return TRUE;
514     }
515 }
516
517 class Transport_iframe {
518
519     function Transport_iframe() {
520     }
521
522     function init($enc, $header, &$header_out, $init_string, $base, $step)
523     {
524         $ret = "";
525
526         if ($enc != 'plain')
527             $header_out['Content-Encoding'] = $enc;
528         $header_out['Cache-Control'] = 'no-cache, must-revalidate';     // HTTP/1.1
529         $header_out['Expires']       = 'Mon, 26 Jul 1997 05:00:00 GMT'; // Date in the past
530         $header_out['Content-type']  = 'text/html; charset="utf-8"';
531
532         $ret .= sprintf("<html>
533 <head>
534 <script type=\"text/javascript\" src=\"%scommons.js\"></script>
535 <script type=\"text/javascript\" src=\"%sxynt-streaming-ifra.js\"></script>
536 <script type=\"text/javascript\">
537 var xynt_streaming = \"ready\";", $base, $base);
538         if ($step > 0)
539             $ret .= sprintf("last_clean = %d;\n", ($step-1));
540         $ret .= sprintf("
541 window.onload = function () { try { if (xynt_streaming != \"ready\") { xynt_streaming.transp.stopped = true; } } catch(e) { /* console.log(\"catcha\"); */ } };
542 </script>
543 </head>
544 <body>");
545         $ret .= sprintf("<!-- \n%s -->\n", $init_string);
546
547         return ($ret);
548     }
549
550     function close()
551     {
552         return "";
553     }
554
555     static function fini($init_string, $base, $blockerr)
556     {
557         $ret = "";
558         $ret .= sprintf("<html>
559 <head>
560 <script type=\"text/javascript\" src=\"%scommons.js\"></script>
561 <script type=\"text/javascript\" src=\"%sxynt-streaming-ifra.js\"></script>
562 <script type=\"text/javascript\">
563 var xynt_streaming = \"ready\";", $base, $base);
564         $ret .= sprintf("
565 window.onload = function () { try { if (xynt_streaming != \"ready\") { xynt_streaming.reload(); } } catch(e) { /* console.log(\"catcha\"); */ } };
566 </script>
567 </head>
568 <body>");
569         $ret .= sprintf("<!-- \n%s -->\n", $init_string);
570         $ret .= sprintf("<script id='hs%d' type='text/javascript'><!--
571 push(\"%s\");
572 // -->
573 </script>", 0, escpush($blockerr) );
574         return ($ret);
575     }
576
577     function chunk($step, $cont)
578     {
579         // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
580         if ($cont == NULL) {
581             return sprintf("<script id='hs%d' type='text/javascript'><!--
582 push(null);\n// -->\n</script>", $step);
583         }
584         else {
585             return sprintf("<script id='hs%d' type='text/javascript'><!--
586 push(\"%s\");\n// -->\n</script>", $step, escpush($cont) );
587         }
588     }
589
590     function is_chunked()
591     {
592         return TRUE;
593     }
594 }
595
596 class Transport_htmlfile extends Transport_iframe {
597 }
598
599 class Transport {
600     function Transport()
601     {
602     }
603
604     static function create($transp)
605     {
606         if ($transp == 'websocket') {
607             return new Transport_websocket();
608         }
609         else if ($transp == 'xhr') {
610             return new Transport_xhr();
611         }
612         else if ($transp == 'htmlfile') {
613             return new Transport_htmlfile();
614         }
615         else  {
616             return new Transport_iframe();
617         }
618     }
619     static function gettype($transp)
620     {
621         if ($transp == 'websocket' || $transp == 'xhr' || $transp == 'htmlfile') {
622             return "Transport_".$transp;
623         }
624         else {
625             return 'Transport_iframe';
626         }
627     }
628 }
629 ?>