-/*
- * 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
var Guacamole = Guacamole || {};
+/**
+ * Core object providing abstract communication for Guacamole. This object
+ * is a null implementation whose functions do nothing. Guacamole applications
+ * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
+ * on this one.
+ *
+ * @constructor
+ * @see Guacamole.HTTPTunnel
+ */
+Guacamole.Tunnel = function() {
+
+ /**
+ * Connect to the tunnel with the given optional data. This data is
+ * typically used for authentication. The format of data accepted is
+ * up to the tunnel implementation.
+ *
+ * @param {String} data The data to send to the tunnel when connecting.
+ */
+ this.connect = function(data) {};
+
+ /**
+ * Disconnect from the tunnel.
+ */
+ this.disconnect = function() {};
+
+ /**
+ * Send the given message through the tunnel to the service on the other
+ * side. All messages are guaranteed to be received in the order sent.
+ *
+ * @param {...} elements The elements of the message to send to the
+ * service on the other side of the tunnel.
+ */
+ this.sendMessage = function(elements) {};
+
+ /**
+ * Fired whenever an error is encountered by the tunnel.
+ *
+ * @event
+ * @param {String} message A human-readable description of the error that
+ * occurred.
+ */
+ this.onerror = null;
+
+ /**
+ * Fired once for every complete Guacamole instruction received, in order.
+ *
+ * @event
+ * @param {String} opcode The Guacamole instruction opcode.
+ * @param {Array} parameters The parameters provided for the instruction,
+ * if any.
+ */
+ this.oninstruction = null;
+
+};
+
+/**
+ * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
+ *
+ * @constructor
+ * @augments Guacamole.Tunnel
+ * @param {String} tunnelURL The URL of the HTTP tunneling service.
+ */
Guacamole.HTTPTunnel = function(tunnelURL) {
+ /**
+ * Reference to this HTTP tunnel.
+ */
var tunnel = this;
var tunnel_uuid;
// Default to polling - will be turned off automatically if not needed
var pollingMode = POLLING_ENABLED;
- var sendingMessages = 0;
+ var sendingMessages = false;
var outputMessageBuffer = "";
- // Handlers
- tunnel.onerror = null;
- tunnel.oninstruction = null;
-
- function sendMessage(message) {
+ this.sendMessage = function() {
// Do not attempt to send messages if not connected
if (currentState != STATE_CONNECTED)
return;
- // Add event to queue, restart send loop if finished.
+ // 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 += ";";
+
+ // Add message to buffer
outputMessageBuffer += message;
- if (sendingMessages == 0)
+
+ // Send if not currently sending
+ if (!sendingMessages)
sendPendingMessages();
- }
+ };
function sendPendingMessages() {
if (outputMessageBuffer.length > 0) {
- sendingMessages = 1;
+ sendingMessages = true;
var message_xmlhttprequest = new XMLHttpRequest();
message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
}
else
- sendingMessages = 0;
+ sendingMessages = false;
}
var nextRequest = null;
var dataUpdateEvents = 0;
- var instructionStart = 0;
+
+ // The location of the last element's terminator
+ var elementEnd = -1;
+
+ // Where to start the next length search or the next element
var startIndex = 0;
+ // Parsed elements
+ var elements = new Array();
+
function parseResponse() {
// Do not handle responses if not connected
return;
}
- // Start next request as soon as possible
- if (xmlhttprequest.readyState >= 2 && nextRequest == null)
+ // 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 (nextRequest == null && status == 200)
nextRequest = makeRequest();
// Parse stream when data is received and when complete.
clearInterval(interval);
}
+ // If canceled, stop transfer
+ if (xmlhttprequest.status == 0) {
+ tunnel.disconnect();
+ return;
+ }
+
// Halt on error during request
- if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) {
+ else if (xmlhttprequest.status != 200) {
// Get error message (if any)
var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
- if (message)
+ if (!message)
message = "Internal server error";
// Call error handler
if (tunnel.onerror) tunnel.onerror(message);
// Finish
- disconnect();
+ tunnel.disconnect();
return;
}
- var current = xmlhttprequest.responseText;
- var instructionEnd;
+ // Attempt to read in-progress data
+ var current;
+ try { current = xmlhttprequest.responseText; }
- while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
+ // Do not attempt to parse if data could not be read
+ catch (e) { return; }
- // Start next search at next instruction
- startIndex = instructionEnd+1;
+ // While search is within currently received data
+ while (elementEnd < current.length) {
- var instruction = current.substr(instructionStart,
- instructionEnd - instructionStart);
+ // If we are waiting for element data
+ if (elementEnd >= startIndex) {
- instructionStart = startIndex;
+ // We now have enough data for the element. Parse.
+ var element = current.substring(startIndex, elementEnd);
+ var terminator = current.substring(elementEnd, elementEnd+1);
- var opcodeEnd = instruction.indexOf(":");
+ // Add element to array
+ elements.push(element);
- var opcode;
- var parameters;
- if (opcodeEnd == -1) {
- opcode = instruction;
- parameters = new Array();
- }
- else {
- opcode = instruction.substr(0, opcodeEnd);
- parameters = instruction.substr(opcodeEnd+1).split(",");
- }
+ // If last element, handle instruction
+ if (terminator == ";") {
- // If we're done parsing, handle the next response.
- if (opcode.length == 0) {
+ // Get opcode
+ var opcode = elements.shift();
- delete xmlhttprequest;
- if (nextRequest)
- handleResponse(nextRequest);
+ // 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;
- break;
}
- // Call instruction handler.
- if (tunnel.oninstruction != null)
- tunnel.oninstruction(opcode, parameters);
- }
+ // Search for end of length
+ var lengthEnd = current.indexOf(".", startIndex);
+ if (lengthEnd != -1) {
+
+ // Parse length
+ var length = parseInt(current.substring(elementEnd+1, lengthEnd));
- // Start search at end of string.
- startIndex = current.length;
+ // If we're done parsing, handle the next response.
+ if (length == 0) {
- delete instruction;
- delete parameters;
+ // Clean up interval if polling
+ if (interval != null)
+ clearInterval(interval);
+
+ // Clean up object
+ xmlhttprequest.onreadystatechange = null;
+ xmlhttprequest.abort();
+
+ // Start handling next request
+ if (nextRequest)
+ handleResponse(nextRequest);
+
+ // Done parsing
+ break;
+
+ }
+
+ // Calculate start of element
+ startIndex = lengthEnd + 1;
+
+ // Calculate location of element terminator
+ elementEnd = startIndex + length;
+
+ }
+
+ // If no period yet, continue search when more data
+ // is received
+ else {
+ startIndex = current.length;
+ break;
+ }
+
+ } // end parse loop
}
}
- function connect(data) {
+ this.connect = function(data) {
// Start tunnel and connect synchronously
var connect_xmlhttprequest = new XMLHttpRequest();
currentState = STATE_CONNECTED;
handleResponse(makeRequest());
+ };
+
+ this.disconnect = function() {
+ currentState = STATE_DISCONNECTED;
+ };
+
+};
+
+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;
+
+ }
+
}
- function disconnect() {
+ 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.ChainedTunnel = function() {
+
+ var chained_tunnel = this;
+ var current_tunnel = null;
+
+ var connect_data;
+ 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) {
+
+ // 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);
+
+ };
+
+ // Attempt connection
+ current_tunnel.connect(connect_data);
+
}
- // External API
- tunnel.connect = connect;
- tunnel.disconnect = disconnect;
- tunnel.sendMessage = sendMessage;
+ 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.");
+
+ };
+
+ this.onerror = null;
+
+};
-}
+Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();