Fix jsdoc, add missing documentation.
[guacamole-common-js.git] / src / main / resources / tunnel.js
index ae7a85c..4f75ea3 100644 (file)
@@ -1,22 +1,44 @@
 
-/*
- *  Guacamole - Clientless Remote Desktop
- *  Copyright (C) 2010  Michael Jumper
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  *
- *  This program is free software: you can redistribute it and/or modify
- *  it under the terms of the GNU Affero General Public License as published by
- *  the Free Software Foundation, either version 3 of the License, or
- *  (at your option) any later version.
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
  *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU Affero General Public License for more details.
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
  *
- *  You should have received a copy of the GNU Affero General Public License
- */
+ * The Original Code is guacamole-common-js.
+ *
+ * The Initial Developer of the Original Code is
+ * Michael Jumper.
+ * Portions created by the Initial Developer are Copyright (C) 2010
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
 
-// Guacamole namespace
+/**
+ * Namespace for all Guacamole JavaScript objects.
+ * @namespace
+ */
 var Guacamole = Guacamole || {};
 
 /**
@@ -85,6 +107,7 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
 
     /**
      * Reference to this HTTP tunnel.
+     * @private
      */
     var tunnel = this;
 
@@ -119,6 +142,14 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
         if (arguments.length == 0)
             return;
 
+        /**
+         * Converts the given value to a length/string pair for use as an
+         * element in a Guacamole instruction.
+         * 
+         * @private
+         * @param value The value to convert.
+         * @return {String} The converted value. 
+         */
         function getElement(value) {
             var string = new String(value);
             return string.length + "." + string; 
@@ -134,8 +165,10 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
         // Final terminator
         message += ";";
 
-        // Add message to buffer, restart send loop if finished.
+        // Add message to buffer
         outputMessageBuffer += message;
+
+        // Send if not currently sending
         if (!sendingMessages)
             sendPendingMessages();
 
@@ -153,8 +186,17 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
 
             // Once response received, send next queued event.
             message_xmlhttprequest.onreadystatechange = function() {
-                if (message_xmlhttprequest.readyState == 4)
-                    sendPendingMessages();
+                if (message_xmlhttprequest.readyState == 4) {
+
+                    // If an error occurs during send, handle it
+                    if (message_xmlhttprequest.status != 200)
+                        handleHTTPTunnelError(message_xmlhttprequest);
+
+                    // Otherwise, continue the send loop
+                    else
+                        sendPendingMessages();
+
+                }
             }
 
             message_xmlhttprequest.send(outputMessageBuffer);
@@ -166,6 +208,49 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
 
     }
 
+    function getHTTPTunnelErrorMessage(xmlhttprequest) {
+
+        var status = xmlhttprequest.status;
+
+        // Special cases
+        if (status == 0)   return "Disconnected";
+        if (status == 200) return "Success";
+        if (status == 403) return "Unauthorized";
+        if (status == 404) return "Connection closed"; /* While it may be more
+                                                        * accurate to say the
+                                                        * connection does not
+                                                        * exist, it is confusing
+                                                        * to the user.
+                                                        * 
+                                                        * In general, this error
+                                                        * will only happen when
+                                                        * the tunnel does not
+                                                        * exist, which happens
+                                                        * after the connection
+                                                        * is closed and the
+                                                        * tunnel is detached.
+                                                        */
+        // Internal server errors
+        if (status >= 500 && status <= 599) return "Server error";
+
+        // Otherwise, unknown
+        return "Unknown error";
+
+    }
+
+    function handleHTTPTunnelError(xmlhttprequest) {
+
+        // Get error message
+        var message = getHTTPTunnelErrorMessage(xmlhttprequest);
+
+        // Call error handler
+        if (tunnel.onerror) tunnel.onerror(message);
+
+        // Finish
+        tunnel.disconnect();
+
+    }
+
 
     function handleResponse(xmlhttprequest) {
 
@@ -195,8 +280,18 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
                 return;
             }
 
+            // Do not parse response yet if not ready
+            if (xmlhttprequest.readyState < 2) return;
+
+            // Attempt to read status
+            var status;
+            try { status = xmlhttprequest.status; }
+
+            // If status could not be read, assume successful.
+            catch (e) { status = 200; }
+
             // Start next request as soon as possible IF request was successful
-            if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
+            if (nextRequest == null && status == 200)
                 nextRequest = makeRequest();
 
             // Parse stream when data is received and when complete.
@@ -219,21 +314,16 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
 
                 // Halt on error during request
                 else if (xmlhttprequest.status != 200) {
-
-                    // Get error message (if any)
-                    var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
-                    if (!message)
-                        message = "Internal server error";
-
-                    // Call error handler
-                    if (tunnel.onerror) tunnel.onerror(message);
-
-                    // Finish
-                    tunnel.disconnect();
+                    handleHTTPTunnelError(xmlhttprequest);
                     return;
                 }
 
-                var current = xmlhttprequest.responseText;
+                // Attempt to read in-progress data
+                var current;
+                try { current = xmlhttprequest.responseText; }
+
+                // Do not attempt to parse if data could not be read
+                catch (e) { return; }
 
                 // While search is within currently received data
                 while (elementEnd < current.length) {
@@ -366,13 +456,8 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
 
         // If failure, throw error
         if (connect_xmlhttprequest.status != 200) {
-
-            var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
-            if (!message)
-                message = "Internal error";
-
+            var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest);
             throw new Error(message);
-
         }
 
         // Get UUID from response
@@ -404,23 +489,40 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
 
     /**
      * Reference to this WebSocket tunnel.
+     * @private
      */
     var tunnel = this;
 
     /**
      * The WebSocket used by this tunnel.
+     * @private
      */
     var socket = null;
 
     /**
      * The WebSocket protocol corresponding to the protocol used for the current
      * location.
+     * @private
      */
     var ws_protocol = {
         "http:":  "ws:",
         "https:": "wss:"
     };
 
+    var status_code = {
+        1000: "Connection closed normally.",
+        1001: "Connection shut down.",
+        1002: "Protocol error.",
+        1003: "Invalid data.",
+        1004: "[UNKNOWN, RESERVED]",
+        1005: "No status code present.",
+        1006: "Connection closed abnormally.",
+        1007: "Inconsistent data type.",
+        1008: "Policy violation.",
+        1009: "Message too large.",
+        1010: "Extension negotiation failed."
+    };
+
     var STATE_IDLE          = 0;
     var STATE_CONNECTED     = 1;
     var STATE_DISCONNECTED  = 2;
@@ -460,12 +562,39 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
 
     }
 
-    this.sendMessage = function(message) {
+    this.sendMessage = function(elements) {
 
         // Do not attempt to send messages if not connected
         if (currentState != STATE_CONNECTED)
             return;
 
+        // Do not attempt to send empty messages
+        if (arguments.length == 0)
+            return;
+
+        /**
+         * Converts the given value to a length/string pair for use as an
+         * element in a Guacamole instruction.
+         * 
+         * @private
+         * @param value The value to convert.
+         * @return {String} The converted value. 
+         */
+        function getElement(value) {
+            var string = new String(value);
+            return string.length + "." + string; 
+        }
+
+        // Initialized message with first element
+        var message = getElement(arguments[0]);
+
+        // Append remaining elements
+        for (var i=1; i<arguments.length; i++)
+            message += "," + getElement(arguments[i]);
+
+        // Final terminator
+        message += ";";
+
         socket.send(message);
 
     };
@@ -479,26 +608,77 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
             currentState = STATE_CONNECTED;
         };
 
+        socket.onclose = function(event) {
+
+            // If connection closed abnormally, signal error.
+            if (event.code != 1000 && tunnel.onerror)
+                tunnel.onerror(status_code[event.code]);
+
+        };
+        
+        socket.onerror = function(event) {
+
+            // Call error handler
+            if (tunnel.onerror) tunnel.onerror(event.data);
+
+        };
+
         socket.onmessage = function(event) {
 
             var message = event.data;
+            var startIndex = 0;
+            var elementEnd;
 
-            var instructions = message.split(";");
-            for (var i=0; i<instructions.length; i++) {
+            var elements = [];
 
-                var instruction = instructions[i];
+            do {
 
-                var opcodeEnd = instruction.indexOf(":");
-                if (opcodeEnd == -1)
-                    opcodeEnd = instruction.length;
+                // Search for end of length
+                var lengthEnd = message.indexOf(".", startIndex);
+                if (lengthEnd != -1) {
 
-                var opcode = instruction.substring(0, opcodeEnd);
-                var parameters = instruction.substring(opcodeEnd+1).split(",");
+                    // Parse length
+                    var length = parseInt(message.substring(elementEnd+1, lengthEnd));
 
-                if (tunnel.oninstruction)
-                    tunnel.oninstruction(opcode, parameters);
+                    // Calculate start of element
+                    startIndex = lengthEnd + 1;
 
-            }
+                    // Calculate location of element terminator
+                    elementEnd = startIndex + length;
+
+                }
+                
+                // If no period, incomplete instruction.
+                else
+                    throw new Error("Incomplete instruction.");
+
+                // We now have enough data for the element. Parse.
+                var element = message.substring(startIndex, elementEnd);
+                var terminator = message.substring(elementEnd, elementEnd+1);
+
+                // Add element to array
+                elements.push(element);
+
+                // If last element, handle instruction
+                if (terminator == ";") {
+
+                    // Get opcode
+                    var opcode = elements.shift();
+
+                    // Call instruction handler.
+                    if (tunnel.oninstruction != null)
+                        tunnel.oninstruction(opcode, elements);
+
+                    // Clear elements
+                    elements.length = 0;
+
+                }
+
+                // Start searching for length at character after
+                // element terminator
+                startIndex = elementEnd + 1;
+
+            } while (startIndex < message.length);
 
         };
 
@@ -513,3 +693,135 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
 
 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
 
+
+/**
+ * Guacamole Tunnel which cycles between all specified tunnels until
+ * no tunnels are left. Another tunnel is used if an error occurs but
+ * no instructions have been received. If an instruction has been
+ * received, or no tunnels remain, the error is passed directly out
+ * through the onerror handler (if defined).
+ * 
+ * @constructor
+ * @augments Guacamole.Tunnel
+ * @param {...} tunnel_chain The tunnels to use, in order of priority.
+ */
+Guacamole.ChainedTunnel = function(tunnel_chain) {
+
+    /**
+     * Reference to this chained tunnel.
+     * @private
+     */
+    var chained_tunnel = this;
+
+    /**
+     * The currently wrapped tunnel, if any.
+     * @private
+     */
+    var current_tunnel = null;
+
+    /**
+     * Data passed in via connect(), to be used for
+     * wrapped calls to other tunnels' connect() functions.
+     * @private
+     */
+    var connect_data;
+
+    /**
+     * Array of all tunnels passed to this ChainedTunnel through the
+     * constructor arguments.
+     * @private
+     */
+    var tunnels = [];
+
+    // Load all tunnels into array
+    for (var i=0; i<arguments.length; i++)
+        tunnels.push(arguments[i]);
+
+    /**
+     * Sets the current tunnel.
+     * 
+     * @private
+     * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel.
+     */
+    function attach(tunnel) {
+
+        // Clear handlers of current tunnel, if any
+        if (current_tunnel) {
+            current_tunnel.onerror = null;
+            current_tunnel.oninstruction = null;
+        }
+
+        // Set own functions to tunnel's functions
+        chained_tunnel.disconnect    = tunnel.disconnect;
+        chained_tunnel.sendMessage   = tunnel.sendMessage;
+        
+        // Record current tunnel
+        current_tunnel = tunnel;
+
+        // Wrap own oninstruction within current tunnel
+        current_tunnel.oninstruction = function(opcode, elements) {
+            
+            // Invoke handler
+            chained_tunnel.oninstruction(opcode, elements);
+
+            // Use handler permanently from now on
+            current_tunnel.oninstruction = chained_tunnel.oninstruction;
+
+            // Pass through errors (without trying other tunnels)
+            current_tunnel.onerror = chained_tunnel.onerror;
+            
+        }
+
+        // Attach next tunnel on error
+        current_tunnel.onerror = function(message) {
+
+            // Get next tunnel
+            var next_tunnel = tunnels.shift();
+
+            // If there IS a next tunnel, try using it.
+            if (next_tunnel)
+                attach(next_tunnel);
+
+            // Otherwise, call error handler
+            else if (chained_tunnel.onerror)
+                chained_tunnel.onerror(message);
+
+        };
+
+        try {
+            
+            // Attempt connection
+            current_tunnel.connect(connect_data);
+            
+        }
+        catch (e) {
+            
+            // Call error handler of current tunnel on error
+            current_tunnel.onerror(e.message);
+            
+        }
+
+
+    }
+
+    this.connect = function(data) {
+       
+        // Remember connect data
+        connect_data = data;
+
+        // Get first tunnel
+        var next_tunnel = tunnels.shift();
+
+        // Attach first tunnel
+        if (next_tunnel)
+            attach(next_tunnel);
+
+        // If there IS no first tunnel, error
+        else if (chained_tunnel.onerror)
+            chained_tunnel.onerror("No tunnels to try.");
+
+    };
+    
+};
+
+Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();