X-Git-Url: http://git.alex.org.uk diff --git a/src/main/resources/tunnel.js b/src/main/resources/tunnel.js index 3ab4bf3..4f75ea3 100644 --- a/src/main/resources/tunnel.js +++ b/src/main/resources/tunnel.js @@ -1,26 +1,121 @@ -/* - * 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 ***** */ + +/** + * Namespace for all Guacamole JavaScript objects. + * @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. + * @private + */ + var tunnel = this; -function GuacamoleHTTPTunnel(tunnelURL) { + var tunnel_uuid; var TUNNEL_CONNECT = tunnelURL + "?connect"; - var TUNNEL_READ = tunnelURL + "?read"; - var TUNNEL_WRITE = tunnelURL + "?write"; + var TUNNEL_READ = tunnelURL + "?read:"; + var TUNNEL_WRITE = tunnelURL + "?write:"; var STATE_IDLE = 0; var STATE_CONNECTED = 1; @@ -34,38 +129,74 @@ function GuacamoleHTTPTunnel(tunnelURL) { // Default to polling - will be turned off automatically if not needed var pollingMode = POLLING_ENABLED; - var instructionHandler = null; - - var sendingMessages = 0; + var sendingMessages = false; var outputMessageBuffer = ""; - 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. + * + * @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 0) { - sendingMessages = 1; + sendingMessages = true; var message_xmlhttprequest = new XMLHttpRequest(); - message_xmlhttprequest.open("POST", TUNNEL_WRITE); + message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); // 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); @@ -73,7 +204,50 @@ function GuacamoleHTTPTunnel(tunnelURL) { } else - sendingMessages = 0; + sendingMessages = false; + + } + + 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(); } @@ -84,9 +258,16 @@ function GuacamoleHTTPTunnel(tunnelURL) { 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 @@ -99,8 +280,18 @@ function GuacamoleHTTPTunnel(tunnelURL) { 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. @@ -115,58 +306,102 @@ function GuacamoleHTTPTunnel(tunnelURL) { 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) { - disconnect(); + else if (xmlhttprequest.status != 200) { + handleHTTPTunnelError(xmlhttprequest); 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 (instructionHandler != null) - instructionHandler(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)); + + // If we're done parsing, handle the next response. + if (length == 0) { - // Start search at end of string. - startIndex = current.length; + // 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; + } - delete instruction; - delete parameters; + } // end parse loop } @@ -204,37 +439,389 @@ function GuacamoleHTTPTunnel(tunnelURL) { // Download self var xmlhttprequest = new XMLHttpRequest(); - xmlhttprequest.open("POST", TUNNEL_READ); + xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid); xmlhttprequest.send(null); return xmlhttprequest; } - function connect() { + this.connect = function(data) { // Start tunnel and connect synchronously var connect_xmlhttprequest = new XMLHttpRequest(); connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false); connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - connect_xmlhttprequest.send(null); + connect_xmlhttprequest.send(data); + + // If failure, throw error + if (connect_xmlhttprequest.status != 200) { + var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest); + throw new Error(message); + } + + // Get UUID from response + tunnel_uuid = connect_xmlhttprequest.responseText; // Start reading data 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. + * @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; + + 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. + * + * @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