/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ 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) {}; /** * The current state of this tunnel. * * @type {Number} */ this.state = Guacamole.Tunnel.State.CONNECTING; /** * The maximum amount of time to wait for data to be received, in * milliseconds. If data is not received within this amount of time, * the tunnel is closed with an error. The default value is 15000. * * @type {Number} */ this.receiveTimeout = 15000; /** * The UUID uniquely identifying this tunnel. If not yet known, this will * be null. * * @type {String} */ this.uuid = null; /** * Fired whenever an error is encountered by the tunnel. * * @event * @param {Guacamole.Status} status A status object which describes the * error. */ this.onerror = null; /** * Fired whenever the state of the tunnel changes. * * @event * @param {Number} state The new state of the client. */ this.onstatechange = 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; }; /** * The Guacamole protocol instruction opcode reserved for arbitrary internal * use by tunnel implementations. The value of this opcode is guaranteed to be * the empty string (""). Tunnel implementations may use this opcode for any * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP * response, and by the WebSocket tunnel to transmit the tunnel UUID. * * @constant * @type {String} */ Guacamole.Tunnel.INTERNAL_DATA_OPCODE = ''; /** * All possible tunnel states. */ Guacamole.Tunnel.State = { /** * A connection is in pending. It is not yet known whether connection was * successful. * * @type {Number} */ "CONNECTING": 0, /** * Connection was successful, and data is being received. * * @type {Number} */ "OPEN": 1, /** * The connection is closed. Connection may not have been successful, the * tunnel may have been explicitly closed by either side, or an error may * have occurred. * * @type {Number} */ "CLOSED": 2 }; /** * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. * * @constructor * @augments Guacamole.Tunnel * * @param {String} tunnelURL * The URL of the HTTP tunneling service. * * @param {Boolean} [crossDomain=false] * Whether tunnel requests will be cross-domain, and thus must use CORS * mechanisms and headers. By default, it is assumed that tunnel requests * will be made to the same domain. */ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { /** * Reference to this HTTP tunnel. * @private */ var tunnel = this; var TUNNEL_CONNECT = tunnelURL + "?connect"; var TUNNEL_READ = tunnelURL + "?read:"; var TUNNEL_WRITE = tunnelURL + "?write:"; var POLLING_ENABLED = 1; var POLLING_DISABLED = 0; // Default to polling - will be turned off automatically if not needed var pollingMode = POLLING_ENABLED; var sendingMessages = false; var outputMessageBuffer = ""; // If requests are expected to be cross-domain, the cookie that the HTTP // tunnel depends on will only be sent if withCredentials is true var withCredentials = !!crossDomain; /** * The current receive timeout ID, if any. * @private */ var receive_timeout = null; /** * Initiates a timeout which, if data is not received, causes the tunnel * to close with an error. * * @private */ function reset_timeout() { // Get rid of old timeout (if any) window.clearTimeout(receive_timeout); // Set new timeout receive_timeout = window.setTimeout(function () { close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); }, tunnel.receiveTimeout); } /** * Closes this tunnel, signaling the given status and corresponding * message, which will be sent to the onerror handler if the status is * an error status. * * @private * @param {Guacamole.Status} status The status causing the connection to * close; */ function close_tunnel(status) { // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) return; // If connection closed abnormally, signal error. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) { // Ignore RESOURCE_NOT_FOUND if we've already connected, as that // only signals end-of-stream for the HTTP tunnel. if (tunnel.state === Guacamole.Tunnel.State.CONNECTING || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND) tunnel.onerror(status); } // Mark as closed tunnel.state = Guacamole.Tunnel.State.CLOSED; // Reset output message buffer sendingMessages = false; if (tunnel.onstatechange) tunnel.onstatechange(tunnel.state); } this.sendMessage = function() { // Do not attempt to send messages if not connected if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 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 0) { sendingMessages = true; var message_xmlhttprequest = new XMLHttpRequest(); message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid); message_xmlhttprequest.withCredentials = withCredentials; message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream"); // Once response received, send next queued event. message_xmlhttprequest.onreadystatechange = function() { 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); outputMessageBuffer = ""; // Clear buffer } else sendingMessages = false; } function handleHTTPTunnelError(xmlhttprequest) { var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); close_tunnel(new Guacamole.Status(code, message)); } function handleResponse(xmlhttprequest) { var interval = null; var nextRequest = null; var dataUpdateEvents = 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 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { // Clean up interval if polling if (interval !== null) clearInterval(interval); 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 (!nextRequest && status === 200) nextRequest = makeRequest(); // Parse stream when data is received and when complete. if (xmlhttprequest.readyState === 3 || xmlhttprequest.readyState === 4) { reset_timeout(); // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) if (pollingMode === POLLING_ENABLED) { if (xmlhttprequest.readyState === 3 && !interval) interval = setInterval(parseResponse, 30); else if (xmlhttprequest.readyState === 4 && interval) clearInterval(interval); } // If canceled, stop transfer if (xmlhttprequest.status === 0) { tunnel.disconnect(); return; } // Halt on error during request else if (xmlhttprequest.status !== 200) { handleHTTPTunnelError(xmlhttprequest); return; } // 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) { // If we are waiting for element data if (elementEnd >= startIndex) { // We now have enough data for the element. Parse. var element = current.substring(startIndex, elementEnd); var terminator = current.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) tunnel.oninstruction(opcode, elements); // Clear elements elements.length = 0; } // Start searching for length at character after // element terminator startIndex = elementEnd + 1; } // 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) { // Clean up interval if polling if (interval) 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 } } // If response polling enabled, attempt to detect if still // necessary (via wrapping parseResponse()) if (pollingMode === POLLING_ENABLED) { xmlhttprequest.onreadystatechange = function() { // If we receive two or more readyState==3 events, // there is no need to poll. if (xmlhttprequest.readyState === 3) { dataUpdateEvents++; if (dataUpdateEvents >= 2) { pollingMode = POLLING_DISABLED; xmlhttprequest.onreadystatechange = parseResponse; } } parseResponse(); }; } // Otherwise, just parse else xmlhttprequest.onreadystatechange = parseResponse; parseResponse(); } /** * Arbitrary integer, unique for each tunnel read request. * @private */ var request_id = 0; function makeRequest() { // Make request, increment request ID var xmlhttprequest = new XMLHttpRequest(); xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++)); xmlhttprequest.withCredentials = withCredentials; xmlhttprequest.send(null); return xmlhttprequest; } this.connect = function(data) { // Start waiting for connect reset_timeout(); // Start tunnel and connect var connect_xmlhttprequest = new XMLHttpRequest(); connect_xmlhttprequest.onreadystatechange = function() { if (connect_xmlhttprequest.readyState !== 4) return; // If failure, throw error if (connect_xmlhttprequest.status !== 200) { handleHTTPTunnelError(connect_xmlhttprequest); return; } reset_timeout(); // Get UUID from response tunnel.uuid = connect_xmlhttprequest.responseText; tunnel.state = Guacamole.Tunnel.State.OPEN; if (tunnel.onstatechange) tunnel.onstatechange(tunnel.state); // Start reading data handleResponse(makeRequest()); }; connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); connect_xmlhttprequest.withCredentials = withCredentials; connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); connect_xmlhttprequest.send(data); }; this.disconnect = function() { close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); }; }; 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 current receive timeout ID, if any. * @private */ var receive_timeout = null; /** * The WebSocket protocol corresponding to the protocol used for the current * location. * @private */ var ws_protocol = { "http:": "ws:", "https:": "wss:" }; // 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; } } /** * Initiates a timeout which, if data is not received, causes the tunnel * to close with an error. * * @private */ function reset_timeout() { // Get rid of old timeout (if any) window.clearTimeout(receive_timeout); // Set new timeout receive_timeout = window.setTimeout(function () { close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); }, tunnel.receiveTimeout); } /** * Closes this tunnel, signaling the given status and corresponding * message, which will be sent to the onerror handler if the status is * an error status. * * @private * @param {Guacamole.Status} status The status causing the connection to * close; */ function close_tunnel(status) { // Ignore if already closed if (tunnel.state === Guacamole.Tunnel.State.CLOSED) return; // If connection closed abnormally, signal error. if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) tunnel.onerror(status); // Mark as closed tunnel.state = Guacamole.Tunnel.State.CLOSED; if (tunnel.onstatechange) tunnel.onstatechange(tunnel.state); socket.close(); } this.sendMessage = function(elements) { // Do not attempt to send messages if not connected if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 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