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