In chained tunnel, clear handlers in old tunnel when new tunnel is taking over.
[guacamole-common-js.git] / src / main / resources / tunnel.js
index 618734e..a35d007 100644 (file)
@@ -181,8 +181,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);
@@ -195,6 +204,22 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
     }
 
 
+    function handleHTTPTunnelError(xmlhttprequest) {
+
+        // 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();
+
+    }
+
+
     function handleResponse(xmlhttprequest) {
 
         var interval = null;
@@ -223,6 +248,9 @@ 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; }
@@ -231,7 +259,7 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
             catch (e) { status = 200; }
 
             // Start next request as soon as possible IF request was successful
-            if (xmlhttprequest.readyState >= 2 && nextRequest == null && status == 200)
+            if (nextRequest == null && status == 200)
                 nextRequest = makeRequest();
 
             // Parse stream when data is received and when complete.
@@ -254,17 +282,7 @@ 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;
                 }
 
@@ -431,3 +449,341 @@ Guacamole.HTTPTunnel = function(tunnelURL) {
 };
 
 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
+
+
+/**
+ * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
+ * 
+ * @constructor
+ * @augments Guacamole.Tunnel
+ * @param {String} tunnelURL The URL of the WebSocket tunneling service.
+ */
+Guacamole.WebSocketTunnel = function(tunnelURL) {
+
+    /**
+     * Reference to this WebSocket tunnel.
+     */
+    var tunnel = this;
+
+    /**
+     * The WebSocket used by this tunnel.
+     */
+    var socket = null;
+
+    /**
+     * The WebSocket protocol corresponding to the protocol used for the current
+     * location.
+     */
+    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;
+
+    var currentState = STATE_IDLE;
+    
+    // Transform current URL to WebSocket URL
+
+    // If not already a websocket URL
+    if (   tunnelURL.substring(0, 3) != "ws:"
+        && tunnelURL.substring(0, 4) != "wss:") {
+
+        var protocol = ws_protocol[window.location.protocol];
+
+        // If absolute URL, convert to absolute WS URL
+        if (tunnelURL.substring(0, 1) == "/")
+            tunnelURL =
+                protocol
+                + "//" + window.location.host
+                + tunnelURL;
+
+        // Otherwise, construct absolute from relative URL
+        else {
+
+            // Get path from pathname
+            var slash = window.location.pathname.lastIndexOf("/");
+            var path  = window.location.pathname.substring(0, slash + 1);
+
+            // Construct absolute URL
+            tunnelURL =
+                protocol
+                + "//" + window.location.host
+                + path
+                + tunnelURL;
+
+        }
+
+    }
+
+    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.
+         * 
+         * @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);
+
+    };
+
+    this.connect = function(data) {
+
+        // Connect socket
+        socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
+
+        socket.onopen = function(event) {
+            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 elements = [];
+
+            do {
+
+                // Search for end of length
+                var lengthEnd = message.indexOf(".", startIndex);
+                if (lengthEnd != -1) {
+
+                    // Parse length
+                    var length = parseInt(message.substring(elementEnd+1, lengthEnd));
+
+                    // 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);
+
+        };
+
+    };
+
+    this.disconnect = function() {
+        currentState = STATE_DISCONNECTED;
+        socket.close();
+    };
+
+};
+
+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.
+     */
+    var chained_tunnel = this;
+
+    /**
+     * The currently wrapped tunnel, if any.
+     */
+    var current_tunnel = null;
+
+    /**
+     * Data passed in via connect(), to be used for
+     * wrapped calls to other tunnels' connect() functions.
+     */
+    var connect_data;
+
+    /**
+     * Array of all tunnels passed to this ChainedTunnel through the
+     * constructor arguments.
+     */
+    var tunnels = [];
+
+    // Load all tunnels into array
+    for (var i=0; i<arguments.length; i++)
+        tunnels.push(arguments[i]);
+
+    /**
+     * Sets 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();