fix typo
[brisk.git] / web / Obj / transports.phh
index 3b6e79c..0cc5f54 100644 (file)
@@ -3,7 +3,7 @@
  *  sac-a-push - Obj/transports.phh
  *
  *  Copyright (C) 2012 Matteo Nastasi
- *                          mailto: nastasi@alternativeoutput.it 
+ *                          mailto: nastasi@alternativeoutput.it
  *                                  matteo.nastasi@milug.org
  *                          web: http://www.alternativeoutput.it
  *
  *   Mac | x  |    |    |    |    |
  *
  *
+ *   WS  | IW | FF | Ch | Op | Ko | IE
+ * ------+----+----+----+----+----+----
+ *   Lnx |    |    |    |    |    |
+ *   Win |    |    |    |    |    |
+ *   Mac |    |    |    |    |    |
+ *
+ *
  *   XHR | IW | FF | Ch | Op | Ko | IE
  * ------+----+----+----+----+----+----
  *   Lnx | Y  |    | ^D |    | Y  | x
  *
  */
 
-
 class Transport_template {
 
     function Transport_template() {
     }
 
     // return string value is appended to the content of the returned page
+    // return FALSE if fails
+    // check with '===' operator to disambiguation between "" and FALSE return value
     function init($enc, $header, &$header_out, $init_string, $base, $step)
     {
     }
 
-    static function fini($init_string, $base, $blockerr)
+    function close()
     {
     }
 
@@ -74,119 +82,416 @@ class Transport_template {
     function is_chunked()
     {
     }
+
+    // return string to add to the stream to perform something to the engine
+    static function fini($init_string, $base, $blockerr)
+    {
+        return "";
+    }
 }
 
 class Transport_websocket {
-    $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-
-    function Transport_websocket() {
-    }
-
-    protected function doHandshake($user, $buffer) {
-        $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-        $headers = array();
-        $lines = explode("\n",$buffer);
-        foreach ($lines as $line) {
-            if (strpos($line,":") !== false) {
-                $header = explode(":",$line,2);
-                $headers[strtolower(trim($header[0]))] = trim($header[1]);
-            } else if (stripos($line,"get ") !== false) {
-                preg_match("/GET (.*) HTTP/i", $buffer, $reqResource);
-                $headers['get'] = trim($reqResource[1]);
+    protected $magicGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+    function Transport_websocket($secure = FALSE) {
+        $this->type = ($secure == FALSE ? "websocket" : "websocketsec");
+        $this->headerOriginRequired                 = false;
+        $this->headerSecWebSocketProtocolRequired   = false;
+        $this->headerSecWebSocketExtensionsRequired = false;
+
+        $this->sendingContinuous = false;
+
+        $this->handlingPartialPacket = false;
+        $this->partialMessage = "";
+
+        $this->hasSentClose = false;
+    }
+
+    protected function extractHeaders($message) {
+        $header = array('fin'     => $message[0] & chr(128),
+                        'rsv1'    => $message[0] & chr(64),
+                        'rsv2'    => $message[0] & chr(32),
+                        'rsv3'    => $message[0] & chr(16),
+                        'opcode'  => ord($message[0]) & 15,
+                        'hasmask' => $message[1] & chr(128),
+                        'length'  => 0,
+                        'mask'    => "");
+        $header['length'] = (ord($message[1]) >= 128) ? ord($message[1]) - 128 : ord($message[1]);
+
+        if ($header['length'] == 126) {
+            if ($header['hasmask']) {
+                $header['mask'] = $message[4] . $message[5] . $message[6] . $message[7];
+            }
+            $header['length'] = ord($message[2]) * 256
+                + ord($message[3]);
+        } elseif ($header['length'] == 127) {
+            if ($header['hasmask']) {
+                $header['mask'] = $message[10] . $message[11] . $message[12] . $message[13];
             }
+            $header['length'] = ord($message[2]) * 65536 * 65536 * 65536 * 256
+                + ord($message[3]) * 65536 * 65536 * 65536
+                + ord($message[4]) * 65536 * 65536 * 256
+                + ord($message[5]) * 65536 * 65536
+                + ord($message[6]) * 65536 * 256
+                + ord($message[7]) * 65536
+                + ord($message[8]) * 256
+                + ord($message[9]);
+        } elseif ($header['hasmask']) {
+            $header['mask'] = $message[2] . $message[3] . $message[4] . $message[5];
+        }
+        //echo $this->strtohex($message);
+        //$this->printHeaders($header);
+        return $header;
+    }
+
+    protected function extractPayload($message,$headers) {
+        $offset = 2;
+        if ($headers['hasmask']) {
+            $offset += 4;
+        }
+        if ($headers['length'] > 65535) {
+            $offset += 8;
+        } elseif ($headers['length'] > 125) {
+            $offset += 2;
         }
-        if (isset($headers['get'])) {
-            $user->requestedResource = $headers['get'];
+        return substr($message,$offset);
+    }
+
+    protected function applyMask($headers,$payload) {
+        $effectiveMask = "";
+        if ($headers['hasmask']) {
+            $mask = $headers['mask'];
         } else {
-            // todo: fail the connection
-            $handshakeResponse = "HTTP/1.1 405 Method Not Allowed\r\n\r\n";                    
+            return $payload;
         }
-        if (!isset($headers['host']) || !$this->checkHost($headers['host'])) {
-            $handshakeResponse = "HTTP/1.1 400 Bad Request";
+
+        while (mb_strlen($effectiveMask, "ASCII") < mb_strlen($payload, "ASCII")) {
+            $effectiveMask .= $mask;
+        }
+        while (mb_strlen($effectiveMask, "ASCII") > mb_strlen($payload, "ASCII")) {
+            $effectiveMask = substr($effectiveMask,0,-1);
+        }
+        return $effectiveMask ^ $payload;
+    }
+
+    protected function checkRSVBits($headers,$user) { // override this method if you are using an extension where the RSV bits are used.
+        if (ord($headers['rsv1']) + ord($headers['rsv2']) + ord($headers['rsv3']) > 0) {
+            //$this->disconnect($user); // todo: fail connection
+            return true;
         }
-        if (!isset($headers['upgrade']) || strtolower($headers['upgrade']) != 'websocket') {
-            $handshakeResponse = "HTTP/1.1 400 Bad Request";
-        } 
-        if (!isset($headers['connection']) || strpos(strtolower($headers['connection']), 'upgrade') === FALSE) {
-            $handshakeResponse = "HTTP/1.1 400 Bad Request";
+        return false;
+    }
+
+    protected function strtohex($str) {
+        $strout = "";
+        for ($i = 0; $i < mb_strlen($str, "ASCII"); $i++) {
+            $strout .= (ord($str[$i])<16) ? "0" . dechex(ord($str[$i])) : dechex(ord($str[$i]));
+            $strout .= " ";
+            if ($i%32 == 7) {
+                $strout .= ": ";
+            }
+            if ($i%32 == 15) {
+                $strout .= ": ";
+            }
+            if ($i%32 == 23) {
+                $strout .= ": ";
+            }
+            if ($i%32 == 31) {
+                $strout .= "\n";
+            }
         }
-        if (!isset($headers['sec-websocket-key'])) {
-            $handshakeResponse = "HTTP/1.1 400 Bad Request";
+        return $strout . "\n";
+    }
+
+    function unchunk($cont)
+    {
+        // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
+        return $this->deframe($cont);
+    }
+
+    function chunk($step, $cont)
+    {
+        // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
+        return $this->frame('@BEGIN@'.$cont.'@END@'); // , 'text', TRUE);
+    }
+
+    protected function frame($message, $messageType='text', $messageContinues=false) {
+        switch ($messageType) {
+        case 'continuous':
+            $b1 = 0;
+            break;
+        case 'text':
+            $b1 = ($this->sendingContinuous) ? 0 : 1;
+            break;
+        case 'binary':
+            $b1 = ($this->sendingContinuous) ? 0 : 2;
+            break;
+        case 'close':
+            $b1 = 8;
+            break;
+        case 'ping':
+            $b1 = 9;
+            break;
+        case 'pong':
+            $b1 = 10;
+            break;
+        }
+        if ($messageContinues) {
+            $this->sendingContinuous = true;
         } else {
-            
+            $b1 += 128;
+            $this->sendingContinuous = false;
         }
-        if (!isset($headers['sec-websocket-version']) || strtolower($headers['sec-websocket-version']) != 13) {
-            $handshakeResponse = "HTTP/1.1 426 Upgrade Required\r\nSec-WebSocketVersion: 13";
+
+        $length = mb_strlen($message, "ASCII");
+        $lengthField = "";
+        if ($length < 126) {
+            $b2 = $length;
+        } elseif ($length <= 65536) {
+            $b2 = 126;
+            $hexLength = dechex($length);
+            //$this->stdout("Hex Length: $hexLength");
+            if (mb_strlen($hexLength, "ASCII")%2 == 1) {
+                $hexLength = '0' . $hexLength;
+            }
+            $n = mb_strlen($hexLength, "ASCII") - 2;
+
+            for ($i = $n; $i >= 0; $i=$i-2) {
+                $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
+            }
+            while (mb_strlen($lengthField, "ASCII") < 2) {
+                $lengthField = chr(0) . $lengthField;
+            }
+        } else {
+            $b2 = 127;
+            $hexLength = dechex($length);
+            if (mb_strlen($hexLength, "ASCII")%2 == 1) {
+                $hexLength = '0' . $hexLength;
+            }
+            $n = mb_strlen($hexLength, "ASCII") - 2;
+
+            for ($i = $n; $i >= 0; $i=$i-2) {
+                $lengthField = chr(hexdec(substr($hexLength, $i, 2))) . $lengthField;
+            }
+            while (mb_strlen($lengthField, "ASCII") < 8) {
+                $lengthField = chr(0) . $lengthField;
+            }
         }
-        if (($this->headerOriginRequired && !isset($headers['origin']) ) || ($this->headerOriginRequired && !$this->checkOrigin($headers['origin']))) {
-            $handshakeResponse = "HTTP/1.1 403 Forbidden";
+
+        return chr($b1) . chr($b2) . $lengthField . $message;
+    }
+
+    protected function deframe($message) {
+        //echo $this->strtohex($message);
+        $headers = $this->extractHeaders($message);
+        $pongReply = false;
+        $willClose = false;
+        switch($headers['opcode']) {
+        case 0:
+        case 1:
+        case 2:
+            break;
+        case 8:
+            // todo: close the connection
+            $this->hasSentClose = true;
+            return "";
+        case 9:
+            $pongReply = true;
+        case 10:
+            break;
+        default:
+            //$this->disconnect($user); // todo: fail connection
+            $willClose = true;
+            break;
         }
-        if (($this->headerSecWebSocketProtocolRequired && !isset($headers['sec-websocket-protocol'])) || ($this->headerSecWebSocketProtocolRequired && !$this->checkWebsocProtocol($header['sec-websocket-protocol']))) {
-            $handshakeResponse = "HTTP/1.1 400 Bad Request";
+
+        if ($this->handlingPartialPacket) {
+            $message = $this->partialBuffer . $message;
+            $this->handlingPartialPacket = false;
+            return $this->deframe($message);
         }
-        if (($this->headerSecWebSocketExtensionsRequired && !isset($headers['sec-websocket-extensions'])) || ($this->headerSecWebSocketExtensionsRequired && !$this->checkWebsocExtensions($header['sec-websocket-extensions']))) {
-            $handshakeResponse = "HTTP/1.1 400 Bad Request";
+
+        if ($this->checkRSVBits($headers,$this)) {
+            return false;
         }
-        
-        // Done verifying the _required_ headers and optionally required headers.
-        
-        if (isset($handshakeResponse)) {
-            socket_write($user->socket,$handshakeResponse,strlen($handshakeResponse));
-            $this->disconnect($user->socket);
+
+        if ($willClose) {
+            // todo: fail the connection
             return false;
         }
-        
-        $user->headers = $headers;
-        $user->handshake = $buffer;
-        
-        $webSocketKeyHash = sha1($headers['sec-websocket-key'] . $magicGUID);
-        
-        $rawToken = "";
-        for ($i = 0; $i < 20; $i++) {
-            $rawToken .= chr(hexdec(substr($webSocketKeyHash,$i*2, 2)));
+
+        $payload = $this->partialMessage . $this->extractPayload($message,$headers);
+
+        if ($pongReply) {
+            $reply = $this->frame($payload,$this,'pong');
+            // TODO FIXME ALL socket_write management
+            socket_write($user->socket,$reply,mb_strlen($reply, "ASCII"));
+            return false;
+        }
+        if (extension_loaded('mbstring')) {
+            if ($headers['length'] > mb_strlen($payload, "ASCII")) {
+                $this->handlingPartialPacket = true;
+                $this->partialBuffer = $message;
+                return false;
+            }
+        } else {
+            if ($headers['length'] > mb_strlen($payload, "ASCII")) {
+                $this->handlingPartialPacket = true;
+                $this->partialBuffer = $message;
+                return false;
+            }
         }
-        $handshakeToken = base64_encode($rawToken) . "\r\n";
-        
-        $subProtocol = (isset($headers['sec-websocket-protocol'])) ? $this->processProtocol($headers['sec-websocket-protocol']) : "";
-        $extensions = (isset($headers['sec-websocket-extensions'])) ? $this->processExtensions($headers['sec-websocket-extensions']) : "";
-        
-        $handshakeResponse = "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: $handshakeToken$subProtocol$extensions\r\n";
-        socket_write($user->socket,$handshakeResponse,strlen($handshakeResponse));
-        $this->connected($user);
+
+        $payload = $this->applyMask($headers,$payload);
+
+        if ($headers['fin']) {
+            $this->partialMessage = "";
+            return $payload;
+        }
+        $this->partialMessage = $payload;
+        return false;
     }
-    
 
-    function init($enc, $header, &$header_out, $init_string, $base, $step)
+
+    protected function checkHost($hostName) {
+        return true; // Override and return false if the host is not one that you would expect.
+        // Ex: You only want to accept hosts from the my-domain.com domain,
+        // but you receive a host from malicious-site.com instead.
+    }
+
+    protected function checkOrigin($origin) {
+        return true; // Override and return false if the origin is not one that you would expect.
+    }
+
+    protected function checkWebsocProtocol($protocol) {
+        return true; // Override and return false if a protocol is not found that you would expect.
+    }
+
+    protected function checkWebsocExtensions($extensions) {
+        return true; // Override and return false if an extension is not found that you would expect.
+    }
+
+    protected function processProtocol($protocol) {
+        return ""; // return either "Sec-WebSocket-Protocol: SelectedProtocolFromClientList\r\n" or return an empty string.
+        // The carriage return/newline combo must appear at the end of a non-empty string, and must not
+        // appear at the beginning of the string nor in an otherwise empty string, or it will be considered part of
+        // the response body, which will trigger an error in the client as it will not be formatted correctly.
+    }
+
+    protected function processExtensions($extensions) {
+        return ""; // return either "Sec-WebSocket-Extensions: SelectedExtensions\r\n" or return an empty string.
+    }
+
+    function init($enc, $headers, &$headers_out, $init_string, $base, $step)
     {
-        
+        if (0) { // TODO: what is ?
+            if (isset($headers['get'])) {
+                $this->requestedResource = $headers['get'];
+            } else {
+                // todo: fail the connection
+                $headers_out['HTTP-Response'] = "405 Method Not Allowed";
+            }
+        }
 
+        if (!isset($headers['Host']) || !$this->checkHost($headers['Host'])) {
+            // error_log('bad 1');
+            $headers_out['HTTP-Response'] = "400 Bad Request";
+        }
+        if (!isset($headers['Upgrade']) || strtolower($headers['Upgrade']) != 'websocket') {
+            // error_log('bad 2 ' . $headers['Upgrade']);
+            $headers_out['HTTP-Response'] = "400 Bad Request";
+        }
+        if (!isset($headers['Connection']) || strpos(strtolower($headers['Connection']), 'upgrade') === FALSE) {
+            // error_log('bad 3');
+            $headers_out['HTTP-Response'] = "400 Bad Request";
+        }
+        if (!isset($headers['Sec-Websocket-Key'])) {
+            // error_log('bad 4');
+            $headers_out['HTTP-Response'] = "400 Bad Request";
+        } else {
+        }
 
+        if (!isset($headers['Sec-Websocket-Version']) || strtolower($headers['Sec-Websocket-Version']) != 13) {
+            $headers_out['HTTP-Response'] = "426 Upgrade Required";
+            $headers_out['Sec-WebSocketVersion'] = "13";
+        }
+        if ( ($this->headerOriginRequired && !isset($headers['Origin']) )
+             || ($this->headerOriginRequired && !$this->checkOrigin($headers['Origin'])) ) {
+            $headers_out['HTTP-Response'] = "403 Forbidden";
+        }
+        if ( ($this->headerSecWebSocketProtocolRequired && !isset($headers['Sec-Websocket-Protocol']))
+             || ($this->headerSecWebSocketProtocolRequired &&
+                 !$this->checkWebsocProtocol($headers['Sec-Websocket-Protocol']))) {
+            // error_log('bad 5');
+            $headers_out['HTTP-Response'] = "400 Bad Request";
+        }
+        if ( ($this->headerSecWebSocketExtensionsRequired  && !isset($headers['Sec-Websocket-Extensions']))
+             || ($this->headerSecWebSocketExtensionsRequired &&
+                 !$this->checkWebsocExtensions($headers['Sec-Websocket-Extensions'])) ) {
+            // error_log('bad 6');
+            $headers_out['HTTP-Response'] = "400 Bad Request";
+        }
 
-        $ret = sprintf("@BEGIN@ /* %s */ @END@", $init_string);
-        if ($enc != 'plain')
-            $header_out['Content-Encoding'] = $enc;
-        $header_out['Cache-Control'] = 'no-cache, must-revalidate';     // HTTP/1.1
-        $header_out['Expires']       = 'Mon, 26 Jul 1997 05:00:00 GMT'; // Date in the past
-        $header_out['Content-type']  = 'application/xml; charset="utf-8"';
+        if (isset($headers_out['HTTP-Response'])) {
+            // TODO: check return management
+            return (FALSE);
+        }
 
-        return ($ret);
+        // TODO: verify both variables
+        // here there is a change of the socket status from start to handshaked
+        // th headers are saved too but without any further access so we skip it
+
+
+
+        $inno = 'x3JJHMbDL1EzLkh9GBhXDw==';
+        $outo = sha1($inno . $this->magicGUID);
+        $rawToken = "";
+        for ($i = 0; $i < 20; $i++) {
+            $rawToken .= chr(hexdec(substr($outo,$i*2, 2)));
+        }
+
+        $outo = base64_encode($rawToken);
+
+        $webSocketKeyHash = sha1($headers['Sec-Websocket-Key'] . $this->magicGUID);
+        $rawToken = "";
+        for ($i = 0; $i < 20; $i++) {
+            $rawToken .= chr(hexdec(substr($webSocketKeyHash,$i*2, 2)));
+        }
+        $handshakeToken = base64_encode($rawToken);
+        $subProtocol = (isset($headers['Sec-Websocket-Protocol'])) ?
+            $this->processProtocol($headers['Sec-Websocket-Protocol']) : "";
+        $extensions = (isset($headers['Sec-Websocket-Extensions'])) ?
+            $this->processExtensions($headers['Sec-Websocket-Extensions']) : "";
+
+        $headers_out['HTTP-Response'] = "101 Switching Protocols";
+        $headers_out['Upgrade']       = 'websocket';
+        $headers_out['Connection']    = 'Upgrade';
+        $headers_out['Sec-WebSocket-Accept'] = "$handshakeToken$subProtocol$extensions";
+
+        return ("");
+    }
+
+    static function close()
+    {
+        return(chr(0x88).chr(0x02).chr(0xe8).chr(0x03));
     }
 
     static function fini($init_string, $base, $blockerr)
     {
-        return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@',  ($blockerr ? 'xstm.stop(); ' : ''), $base));
-        return ("");
+        return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@',  ($blockerr ? 'xstm.stop(); ' : ''), $base).self::close());
     }
 
-    function chunk($step, $cont)
+    function is_chunked()
     {
-        return ("@BEGIN@".$cont."@END@");
+        return FALSE;
     }
+
 }
 
 class Transport_xhr {
 
     function Transport_xhr() {
+        $this->type = 'xhr';
     }
 
     function init($enc, $header, &$header_out, $init_string, $base, $step)
@@ -201,6 +506,11 @@ class Transport_xhr {
         return ($ret);
     }
 
+    function close()
+    {
+        return "";
+    }
+
     static function fini($init_string, $base, $blockerr)
     {
         return (sprintf('@BEGIN@ %s window.onbeforeunload = null; window.onunload = null; document.location.assign("%sindex.php"); @END@',  ($blockerr ? 'xstm.stop(); ' : ''), $base));
@@ -209,6 +519,7 @@ class Transport_xhr {
 
     function chunk($step, $cont)
     {
+        // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
         return ("@BEGIN@".$cont."@END@");
     }
 
@@ -221,6 +532,7 @@ class Transport_xhr {
 class Transport_iframe {
 
     function Transport_iframe() {
+        $this->type = 'iframe';
     }
 
     function init($enc, $header, &$header_out, $init_string, $base, $step)
@@ -232,7 +544,7 @@ class Transport_iframe {
         $header_out['Cache-Control'] = 'no-cache, must-revalidate';     // HTTP/1.1
         $header_out['Expires']       = 'Mon, 26 Jul 1997 05:00:00 GMT'; // Date in the past
         $header_out['Content-type']  = 'text/html; charset="utf-8"';
-        
+
         $ret .= sprintf("<html>
 <head>
 <script type=\"text/javascript\" src=\"%scommons.js\"></script>
@@ -243,7 +555,7 @@ var xynt_streaming = \"ready\";", $base, $base);
             $ret .= sprintf("last_clean = %d;\n", ($step-1));
         $ret .= sprintf("
 window.onload = function () { try { if (xynt_streaming != \"ready\") { xynt_streaming.transp.stopped = true; } } catch(e) { /* console.log(\"catcha\"); */ } };
-</script> 
+</script>
 </head>
 <body>");
         $ret .= sprintf("<!-- \n%s -->\n", $init_string);
@@ -251,6 +563,11 @@ window.onload = function () { try { if (xynt_streaming != \"ready\") { xynt_stre
         return ($ret);
     }
 
+    function close()
+    {
+        return "";
+    }
+
     static function fini($init_string, $base, $blockerr)
     {
         $ret = "";
@@ -275,6 +592,7 @@ push(\"%s\");
 
     function chunk($step, $cont)
     {
+        // fprintf(STDERR, "CHUNK: [%s]\n", $cont);
         if ($cont == NULL) {
             return sprintf("<script id='hs%d' type='text/javascript'><!--
 push(null);\n// -->\n</script>", $step);
@@ -292,6 +610,9 @@ push(\"%s\");\n// -->\n</script>", $step, escpush($cont) );
 }
 
 class Transport_htmlfile extends Transport_iframe {
+    function Transport_htmlfile() {
+        $this->type = 'htmlfile';
+    }
 }
 
 class Transport {
@@ -301,7 +622,10 @@ class Transport {
 
     static function create($transp)
     {
-        if ($transp == 'xhr') {
+        if ($transp == 'websocket' || $transp == 'websocketsec') {
+            return new Transport_websocket($transp == 'websocketsec');
+        }
+        else if ($transp == 'xhr') {
             return new Transport_xhr();
         }
         else if ($transp == 'htmlfile') {
@@ -313,7 +637,7 @@ class Transport {
     }
     static function gettype($transp)
     {
-        if ($transp == 'xhr' || $transp == 'htmlfile') {
+        if ($transp == 'websocket' || $transp == 'xhr' || $transp == 'htmlfile') {
             return "Transport_".$transp;
         }
         else {
@@ -321,4 +645,4 @@ class Transport {
         }
     }
 }
-?>
\ No newline at end of file
+?>