// 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);
}
+ 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;
return;
}
+ // Do not parse response yet if not ready
+ if (xmlhttprequest.readyState < 2) return;
+
// Attempt to read status
var status;
try { status = xmlhttprequest.status; }
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.
// 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;
}
};
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();