| /* |
| * 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) {}; |
| |
| /** |
| * Changes the stored numeric state of this tunnel, firing the onstatechange |
| * event if the new state is different and a handler has been defined. |
| * |
| * @private |
| * @param {Number} state |
| * The new state of this tunnel. |
| */ |
| this.setState = function(state) { |
| |
| // Notify only if state changes |
| if (state !== this.state) { |
| this.state = state; |
| if (this.onstatechange) |
| this.onstatechange(state); |
| } |
| |
| }; |
| |
| /** |
| * 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 amount of time to wait for data to be received before considering |
| * the connection to be unstable, in milliseconds. If data is not received |
| * within this amount of time, the tunnel status is updated to warn that |
| * the connection appears unresponsive and may close. The default value is |
| * 1500. |
| * |
| * @type {Number} |
| */ |
| this.unstableThreshold = 1500; |
| |
| /** |
| * 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, |
| |
| /** |
| * The connection is open, but communication through the tunnel appears to |
| * be disrupted, and the connection may close as a result. |
| * |
| * @type {Number} |
| */ |
| "UNSTABLE" : 3 |
| |
| }; |
| |
| /** |
| * 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. |
| * |
| * @param {Object} [extraTunnelHeaders={}] |
| * Key value pairs containing the header names and values of any additional |
| * headers to be sent in tunnel requests. By default, no extra headers will |
| * be added. |
| */ |
| Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) { |
| |
| /** |
| * 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; |
| |
| /** |
| * The current connection stability timeout ID, if any. |
| * |
| * @private |
| * @type {Number} |
| */ |
| var unstableTimeout = null; |
| |
| /** |
| * Additional headers to be sent in tunnel requests. This dictionary can be |
| * populated with key/value header pairs to pass information such as authentication |
| * tokens, etc. |
| * |
| * @private |
| */ |
| var extraHeaders = extraTunnelHeaders || {}; |
| |
| /** |
| * Adds the configured additional headers to the given request. |
| * |
| * @param {XMLHttpRequest} request |
| * The request where the configured extra headers will be added. |
| * |
| * @param {Object} headers |
| * The headers to be added to the request. |
| * |
| * @private |
| */ |
| function addExtraHeaders(request, headers) { |
| for (var name in headers) { |
| request.setRequestHeader(name, headers[name]); |
| } |
| } |
| |
| /** |
| * 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 timeouts (if any) |
| window.clearTimeout(receive_timeout); |
| window.clearTimeout(unstableTimeout); |
| |
| // Clear unstable status |
| if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| // Set new timeout for tracking overall connection timeout |
| receive_timeout = window.setTimeout(function () { |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); |
| }, tunnel.receiveTimeout); |
| |
| // Set new timeout for tracking suspected connection instability |
| unstableTimeout = window.setTimeout(function() { |
| tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); |
| }, tunnel.unstableThreshold); |
| |
| } |
| |
| /** |
| * 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) { |
| |
| // Get rid of old timeouts (if any) |
| window.clearTimeout(receive_timeout); |
| window.clearTimeout(unstableTimeout); |
| |
| // 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); |
| |
| } |
| |
| // Reset output message buffer |
| sendingMessages = false; |
| |
| // Mark as closed |
| tunnel.setState(Guacamole.Tunnel.State.CLOSED); |
| |
| } |
| |
| |
| 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<arguments.length; i++) |
| message += "," + getElement(arguments[i]); |
| |
| // Final terminator |
| message += ";"; |
| |
| // Add message to buffer |
| outputMessageBuffer += message; |
| |
| // Send if not currently sending |
| if (!sendingMessages) |
| sendPendingMessages(); |
| |
| }; |
| |
| function sendPendingMessages() { |
| |
| // Do not attempt to send messages if not connected |
| if (tunnel.state !== Guacamole.Tunnel.State.OPEN) |
| return; |
| |
| if (outputMessageBuffer.length > 0) { |
| |
| sendingMessages = true; |
| |
| var message_xmlhttprequest = new XMLHttpRequest(); |
| message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid); |
| message_xmlhttprequest.withCredentials = withCredentials; |
| addExtraHeaders(message_xmlhttprequest, extraHeaders); |
| 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) { |
| |
| // Pull status code directly from headers provided by Guacamole |
| var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); |
| if (code) { |
| var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); |
| close_tunnel(new Guacamole.Status(code, message)); |
| } |
| |
| // Failing that, derive a Guacamole status code from the HTTP status |
| // code provided by the browser |
| else if (xmlhttprequest.status) |
| close_tunnel(new Guacamole.Status( |
| Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status), |
| xmlhttprequest.statusText)); |
| |
| // Otherwise, assume server is unreachable |
| else |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND)); |
| |
| } |
| |
| 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; |
| addExtraHeaders(xmlhttprequest, extraHeaders); |
| xmlhttprequest.send(null); |
| |
| return xmlhttprequest; |
| |
| } |
| |
| this.connect = function(data) { |
| |
| // Start waiting for connect |
| reset_timeout(); |
| |
| // Mark the tunnel as connecting |
| tunnel.setState(Guacamole.Tunnel.State.CONNECTING); |
| |
| // 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; |
| |
| // Mark as open |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| // Start reading data |
| handleResponse(makeRequest()); |
| |
| }; |
| |
| connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); |
| connect_xmlhttprequest.withCredentials = withCredentials; |
| addExtraHeaders(connect_xmlhttprequest, extraHeaders); |
| 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 current connection stability timeout ID, if any. |
| * |
| * @private |
| * @type {Number} |
| */ |
| var unstableTimeout = 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 timeouts (if any) |
| window.clearTimeout(receive_timeout); |
| window.clearTimeout(unstableTimeout); |
| |
| // Clear unstable status |
| if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE) |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| // Set new timeout for tracking overall connection timeout |
| receive_timeout = window.setTimeout(function () { |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); |
| }, tunnel.receiveTimeout); |
| |
| // Set new timeout for tracking suspected connection instability |
| unstableTimeout = window.setTimeout(function() { |
| tunnel.setState(Guacamole.Tunnel.State.UNSTABLE); |
| }, tunnel.unstableThreshold); |
| |
| } |
| |
| /** |
| * 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) { |
| |
| // Get rid of old timeouts (if any) |
| window.clearTimeout(receive_timeout); |
| window.clearTimeout(unstableTimeout); |
| |
| // 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.setState(Guacamole.Tunnel.State.CLOSED); |
| |
| 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<arguments.length; i++) |
| message += "," + getElement(arguments[i]); |
| |
| // Final terminator |
| message += ";"; |
| |
| socket.send(message); |
| |
| }; |
| |
| this.connect = function(data) { |
| |
| reset_timeout(); |
| |
| // Mark the tunnel as connecting |
| tunnel.setState(Guacamole.Tunnel.State.CONNECTING); |
| |
| // Connect socket |
| socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); |
| |
| socket.onopen = function(event) { |
| reset_timeout(); |
| }; |
| |
| socket.onclose = function(event) { |
| |
| // Pull status code directly from closure reason provided by Guacamole |
| if (event.reason) |
| close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason)); |
| |
| // Failing that, derive a Guacamole status code from the WebSocket |
| // status code provided by the browser |
| else if (event.code) |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code))); |
| |
| // Otherwise, assume server is unreachable |
| else |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND)); |
| |
| }; |
| |
| socket.onmessage = function(event) { |
| |
| reset_timeout(); |
| |
| 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 |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_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(); |
| |
| // Update state and UUID when first instruction received |
| if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { |
| |
| // Associate tunnel UUID if received |
| if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE) |
| tunnel.uuid = elements[0]; |
| |
| // Tunnel is now open and UUID is available |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| } |
| |
| // Call instruction handler. |
| if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction) |
| 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() { |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); |
| }; |
| |
| }; |
| |
| 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 {...*} tunnelChain |
| * The tunnels to use, in order of priority. |
| */ |
| Guacamole.ChainedTunnel = function(tunnelChain) { |
| |
| /** |
| * Reference to this chained tunnel. |
| * @private |
| */ |
| var chained_tunnel = this; |
| |
| /** |
| * Data passed in via connect(), to be used for |
| * wrapped calls to other tunnels' connect() functions. |
| * @private |
| */ |
| var connect_data; |
| |
| /** |
| * Array of all tunnels passed to this ChainedTunnel through the |
| * constructor arguments. |
| * @private |
| */ |
| var tunnels = []; |
| |
| /** |
| * The tunnel committed via commit_tunnel(), if any, or null if no tunnel |
| * has yet been committed. |
| * |
| * @private |
| * @type {Guacamole.Tunnel} |
| */ |
| var committedTunnel = null; |
| |
| // Load all tunnels into array |
| for (var i=0; i<arguments.length; i++) |
| tunnels.push(arguments[i]); |
| |
| /** |
| * Sets the current tunnel. |
| * |
| * @private |
| * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel. |
| */ |
| function attach(tunnel) { |
| |
| // Set own functions to tunnel's functions |
| chained_tunnel.disconnect = tunnel.disconnect; |
| chained_tunnel.sendMessage = tunnel.sendMessage; |
| |
| /** |
| * Fails the currently-attached tunnel, attaching a new tunnel if |
| * possible. |
| * |
| * @private |
| * @param {Guacamole.Status} [status] |
| * An object representing the failure that occured in the |
| * currently-attached tunnel, if known. |
| * |
| * @return {Guacamole.Tunnel} |
| * The next tunnel, or null if there are no more tunnels to try or |
| * if no more tunnels should be tried. |
| */ |
| var failTunnel = function failTunnel(status) { |
| |
| // Do not attempt to continue using next tunnel on server timeout |
| if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) { |
| tunnels = []; |
| return null; |
| } |
| |
| // Get next tunnel |
| var next_tunnel = tunnels.shift(); |
| |
| // If there IS a next tunnel, try using it. |
| if (next_tunnel) { |
| tunnel.onerror = null; |
| tunnel.oninstruction = null; |
| tunnel.onstatechange = null; |
| attach(next_tunnel); |
| } |
| |
| return next_tunnel; |
| |
| }; |
| |
| /** |
| * Use the current tunnel from this point forward. Do not try any more |
| * tunnels, even if the current tunnel fails. |
| * |
| * @private |
| */ |
| function commit_tunnel() { |
| tunnel.onstatechange = chained_tunnel.onstatechange; |
| tunnel.oninstruction = chained_tunnel.oninstruction; |
| tunnel.onerror = chained_tunnel.onerror; |
| chained_tunnel.uuid = tunnel.uuid; |
| committedTunnel = tunnel; |
| } |
| |
| // Wrap own onstatechange within current tunnel |
| tunnel.onstatechange = function(state) { |
| |
| switch (state) { |
| |
| // If open, use this tunnel from this point forward. |
| case Guacamole.Tunnel.State.OPEN: |
| commit_tunnel(); |
| if (chained_tunnel.onstatechange) |
| chained_tunnel.onstatechange(state); |
| break; |
| |
| // If closed, mark failure, attempt next tunnel |
| case Guacamole.Tunnel.State.CLOSED: |
| if (!failTunnel() && chained_tunnel.onstatechange) |
| chained_tunnel.onstatechange(state); |
| break; |
| |
| } |
| |
| }; |
| |
| // Wrap own oninstruction within current tunnel |
| tunnel.oninstruction = function(opcode, elements) { |
| |
| // Accept current tunnel |
| commit_tunnel(); |
| |
| // Invoke handler |
| if (chained_tunnel.oninstruction) |
| chained_tunnel.oninstruction(opcode, elements); |
| |
| }; |
| |
| // Attach next tunnel on error |
| tunnel.onerror = function(status) { |
| |
| // Mark failure, attempt next tunnel |
| if (!failTunnel(status) && chained_tunnel.onerror) |
| chained_tunnel.onerror(status); |
| |
| }; |
| |
| // Attempt connection |
| tunnel.connect(connect_data); |
| |
| } |
| |
| this.connect = function(data) { |
| |
| // Remember connect data |
| connect_data = data; |
| |
| // Get committed tunnel if exists or the first tunnel on the list |
| var next_tunnel = committedTunnel ? committedTunnel : 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(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try."); |
| |
| }; |
| |
| }; |
| |
| Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel(); |
| |
| /** |
| * Guacamole Tunnel which replays a Guacamole protocol dump from a static file |
| * received via HTTP. Instructions within the file are parsed and handled as |
| * quickly as possible, while the file is being downloaded. |
| * |
| * @constructor |
| * @augments Guacamole.Tunnel |
| * @param {String} url |
| * The URL of a Guacamole protocol dump. |
| * |
| * @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. |
| * |
| * @param {Object} [extraTunnelHeaders={}] |
| * Key value pairs containing the header names and values of any additional |
| * headers to be sent in tunnel requests. By default, no extra headers will |
| * be added. |
| */ |
| Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTunnelHeaders) { |
| |
| /** |
| * Reference to this Guacamole.StaticHTTPTunnel. |
| * |
| * @private |
| */ |
| var tunnel = this; |
| |
| /** |
| * The current, in-progress HTTP request. If no request is currently in |
| * progress, this will be null. |
| * |
| * @private |
| * @type {XMLHttpRequest} |
| */ |
| var xhr = null; |
| |
| /** |
| * Additional headers to be sent in tunnel requests. This dictionary can be |
| * populated with key/value header pairs to pass information such as authentication |
| * tokens, etc. |
| * |
| * @private |
| */ |
| var extraHeaders = extraTunnelHeaders || {}; |
| |
| /** |
| * Adds the configured additional headers to the given request. |
| * |
| * @param {XMLHttpRequest} request |
| * The request where the configured extra headers will be added. |
| * |
| * @param {Object} headers |
| * The headers to be added to the request. |
| * |
| * @private |
| */ |
| function addExtraHeaders(request, headers) { |
| for (var name in headers) { |
| request.setRequestHeader(name, headers[name]); |
| } |
| } |
| |
| this.sendMessage = function sendMessage(elements) { |
| // Do nothing |
| }; |
| |
| this.connect = function connect(data) { |
| |
| // Ensure any existing connection is killed |
| tunnel.disconnect(); |
| |
| // Connection is now starting |
| tunnel.setState(Guacamole.Tunnel.State.CONNECTING); |
| |
| // Start a new connection |
| xhr = new XMLHttpRequest(); |
| xhr.open('GET', url); |
| xhr.withCredentials = !!crossDomain; |
| addExtraHeaders(xhr, extraHeaders); |
| xhr.responseType = 'text'; |
| xhr.send(null); |
| |
| var offset = 0; |
| |
| // Create Guacamole protocol parser specifically for this connection |
| var parser = new Guacamole.Parser(); |
| |
| // Invoke tunnel's oninstruction handler for each parsed instruction |
| parser.oninstruction = function instructionReceived(opcode, args) { |
| if (tunnel.oninstruction) |
| tunnel.oninstruction(opcode, args); |
| }; |
| |
| // Continuously parse received data |
| xhr.onreadystatechange = function readyStateChanged() { |
| |
| // Parse while data is being received |
| if (xhr.readyState === 3 || xhr.readyState === 4) { |
| |
| // Connection is open |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| var buffer = xhr.responseText; |
| var length = buffer.length; |
| |
| // Parse only the portion of data which is newly received |
| if (offset < length) { |
| parser.receive(buffer.substring(offset)); |
| offset = length; |
| } |
| |
| } |
| |
| // Clean up and close when done |
| if (xhr.readyState === 4) |
| tunnel.disconnect(); |
| |
| }; |
| |
| // Reset state and close upon error |
| xhr.onerror = function httpError() { |
| |
| // Fail if file could not be downloaded via HTTP |
| if (tunnel.onerror) |
| tunnel.onerror(new Guacamole.Status( |
| Guacamole.Status.Code.fromHTTPCode(xhr.status), xhr.statusText)); |
| |
| tunnel.disconnect(); |
| }; |
| |
| }; |
| |
| this.disconnect = function disconnect() { |
| |
| // Abort and dispose of XHR if a request is in progress |
| if (xhr) { |
| xhr.abort(); |
| xhr = null; |
| } |
| |
| // Connection is now closed |
| tunnel.setState(Guacamole.Tunnel.State.CLOSED); |
| |
| }; |
| |
| }; |
| |
| Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel(); |