| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="utf-8"> |
| <title>JSDoc: Source: main/webapp/modules/Tunnel.js</title> |
| |
| <script src="scripts/prettify/prettify.js"> </script> |
| <script src="scripts/prettify/lang-css.js"> </script> |
| <!--[if lt IE 9]> |
| <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> |
| <![endif]--> |
| <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> |
| <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> |
| </head> |
| |
| <body> |
| |
| <div id="main"> |
| |
| <h1 class="page-title">Source: main/webapp/modules/Tunnel.js</h1> |
| |
| |
| |
| |
| |
| |
| <section> |
| <article> |
| <pre class="prettyprint source linenums"><code>/* |
| * 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); |
| } |
| |
| }; |
| |
| /** |
| * Changes the stored UUID that uniquely identifies this tunnel, firing the |
| * onuuid event if a handler has been defined. |
| * |
| * @private |
| * @param {string} uuid |
| * The new state of this tunnel. |
| */ |
| this.setUUID = function setUUID(uuid) { |
| this.uuid = uuid; |
| if (this.onuuid) |
| this.onuuid(uuid); |
| }; |
| |
| /** |
| * Returns whether this tunnel is currently connected. |
| * |
| * @returns {!boolean} |
| * true if this tunnel is currently connected, false otherwise. |
| */ |
| this.isConnected = function isConnected() { |
| return this.state === Guacamole.Tunnel.State.OPEN |
| || this.state === Guacamole.Tunnel.State.UNSTABLE; |
| }; |
| |
| /** |
| * The current state of this tunnel. |
| * |
| * @type {!number} |
| */ |
| this.state = Guacamole.Tunnel.State.CLOSED; |
| |
| /** |
| * 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 when the UUID that uniquely identifies this tunnel is known. |
| * |
| * @event |
| * @param {!string} |
| * The UUID uniquely identifying this tunnel. |
| */ |
| this.onuuid = 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 {!string[]} 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 and send |
| * connection stability test pings/responses. |
| * |
| * @constant |
| * @type {!string} |
| */ |
| Guacamole.Tunnel.INTERNAL_DATA_OPCODE = ''; |
| |
| /** |
| * All possible tunnel states. |
| * |
| * @type {!Object.<string, number>} |
| */ |
| 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 |
| * @type {!Guacamole.HTTPTunnel} |
| */ |
| 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 |
| * @type {number} |
| */ |
| var receive_timeout = null; |
| |
| /** |
| * The current connection stability timeout ID, if any. |
| * |
| * @private |
| * @type {number} |
| */ |
| var unstableTimeout = null; |
| |
| /** |
| * The current connection stability test ping interval ID, if any. This |
| * will only be set upon successful connection. |
| * |
| * @private |
| * @type {number} |
| */ |
| var pingInterval = null; |
| |
| /** |
| * The number of milliseconds to wait between connection stability test |
| * pings. |
| * |
| * @private |
| * @constant |
| * @type {!number} |
| */ |
| var PING_FREQUENCY = 500; |
| |
| /** |
| * 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 |
| * @type {!object} |
| */ |
| var extraHeaders = extraTunnelHeaders || {}; |
| |
| /** |
| * The name of the HTTP header containing the session token specific to the |
| * HTTP tunnel implementation. |
| * |
| * @private |
| * @constant |
| * @type {!string} |
| */ |
| var TUNNEL_TOKEN_HEADER = 'Guacamole-Tunnel-Token'; |
| |
| /** |
| * The session token currently assigned to this HTTP tunnel. All distinct |
| * HTTP tunnel connections will have their own dedicated session token. |
| * |
| * @private |
| * @type {string} |
| */ |
| var tunnelSessionToken = null; |
| |
| /** |
| * Adds the configured additional headers to the given request. |
| * |
| * @private |
| * @param {!XMLHttpRequest} request |
| * The request where the configured extra headers will be added. |
| * |
| * @param {!object} headers |
| * The headers to be added to the request. |
| */ |
| function addExtraHeaders(request, headers) { |
| for (var name in headers) { |
| request.setRequestHeader(name, headers[name]); |
| } |
| } |
| |
| /** |
| * Resets the state of timers tracking network activity and stability. If |
| * those timers are not yet started, invoking this function starts them. |
| * This function should be invoked when the tunnel is established and every |
| * time there is network activity on the tunnel, such that the timers can |
| * safely assume the network and/or server are not responding if this |
| * function has not been invoked for a significant period of time. |
| * |
| * @private |
| */ |
| var resetTimers = function resetTimers() { |
| |
| // 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); |
| |
| // Cease connection test pings |
| window.clearInterval(pingInterval); |
| |
| // 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.isConnected()) |
| return; |
| |
| // Do not attempt to send empty messages |
| if (!arguments.length) |
| return; |
| |
| // Add message to buffer |
| outputMessageBuffer += Guacamole.Parser.toInstruction(arguments); |
| |
| // Send if not currently sending |
| if (!sendingMessages) |
| sendPendingMessages(); |
| |
| }; |
| |
| function sendPendingMessages() { |
| |
| // Do not attempt to send messages if not connected |
| if (!tunnel.isConnected()) |
| 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"); |
| message_xmlhttprequest.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken); |
| |
| // Once response received, send next queued event. |
| message_xmlhttprequest.onreadystatechange = function() { |
| if (message_xmlhttprequest.readyState === 4) { |
| |
| resetTimers(); |
| |
| // 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; |
| |
| var parser = new Guacamole.Parser(); |
| parser.oninstruction = function instructionReceived(opcode, args) { |
| |
| // Switch to next request if end-of-stream is signalled |
| if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && args.length === 0) { |
| |
| // Reset parser state by simply switching to an entirely new |
| // parser |
| parser = new Guacamole.Parser(); |
| parser.oninstruction = instructionReceived; |
| |
| // 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); |
| |
| } |
| |
| // Call instruction handler. |
| else if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction) |
| tunnel.oninstruction(opcode, args); |
| |
| }; |
| |
| function parseResponse() { |
| |
| // Do not handle responses if not connected |
| if (!tunnel.isConnected()) { |
| |
| // 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) { |
| |
| resetTimers(); |
| |
| // 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; } |
| |
| try { |
| parser.receive(current, true); |
| } |
| catch (e) { |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message)); |
| return; |
| } |
| |
| } |
| |
| } |
| |
| // 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.setRequestHeader(TUNNEL_TOKEN_HEADER, tunnelSessionToken); |
| xmlhttprequest.withCredentials = withCredentials; |
| addExtraHeaders(xmlhttprequest, extraHeaders); |
| xmlhttprequest.send(null); |
| |
| return xmlhttprequest; |
| |
| } |
| |
| this.connect = function(data) { |
| |
| // Start waiting for connect |
| resetTimers(); |
| |
| // 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; |
| } |
| |
| resetTimers(); |
| |
| // Get UUID and HTTP-specific tunnel session token from response |
| tunnel.setUUID(connect_xmlhttprequest.responseText); |
| tunnelSessionToken = connect_xmlhttprequest.getResponseHeader(TUNNEL_TOKEN_HEADER); |
| |
| // Fail connect attempt if token is not successfully assigned |
| if (!tunnelSessionToken) { |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND)); |
| return; |
| } |
| |
| // Mark as open |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| // Ping tunnel endpoint regularly to test connection stability |
| pingInterval = setInterval(function sendPing() { |
| tunnel.sendMessage("nop"); |
| }, PING_FREQUENCY); |
| |
| // 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 |
| * @type {Guacamole.WebSocketTunnel} |
| */ |
| var tunnel = this; |
| |
| /** |
| * The parser that this tunnel will use to parse received Guacamole |
| * instructions. The parser is created when the tunnel is (re-)connected. |
| * Initially, this will be null. |
| * |
| * @private |
| * @type {Guacamole.Parser} |
| */ |
| var parser = null; |
| |
| /** |
| * The WebSocket used by this tunnel. |
| * |
| * @private |
| * @type {WebSocket} |
| */ |
| var socket = null; |
| |
| /** |
| * The current receive timeout ID, if any. |
| * |
| * @private |
| * @type {number} |
| */ |
| var receive_timeout = null; |
| |
| /** |
| * The current connection stability timeout ID, if any. |
| * |
| * @private |
| * @type {number} |
| */ |
| var unstableTimeout = null; |
| |
| /** |
| * The current connection stability test ping timeout ID, if any. This |
| * will only be set upon successful connection. |
| * |
| * @private |
| * @type {number} |
| */ |
| var pingTimeout = null; |
| |
| /** |
| * The WebSocket protocol corresponding to the protocol used for the current |
| * location. |
| * |
| * @private |
| * @type {!Object.<string, string>} |
| */ |
| var ws_protocol = { |
| "http:": "ws:", |
| "https:": "wss:" |
| }; |
| |
| /** |
| * The number of milliseconds to wait between connection stability test |
| * pings. |
| * |
| * @private |
| * @constant |
| * @type {!number} |
| */ |
| var PING_FREQUENCY = 500; |
| |
| /** |
| * The timestamp of the point in time that the last connection stability |
| * test ping was sent, in milliseconds elapsed since midnight of January 1, |
| * 1970 UTC. |
| * |
| * @private |
| * @type {!number} |
| */ |
| var lastSentPing = 0; |
| |
| // 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; |
| |
| } |
| |
| } |
| |
| /** |
| * Sends an internal "ping" instruction to the Guacamole WebSocket |
| * endpoint, verifying network connection stability. If the network is |
| * stable, the Guacamole server will receive this instruction and respond |
| * with an identical ping. |
| * |
| * @private |
| */ |
| var sendPing = function sendPing() { |
| var currentTime = new Date().getTime(); |
| tunnel.sendMessage(Guacamole.Tunnel.INTERNAL_DATA_OPCODE, 'ping', currentTime); |
| lastSentPing = currentTime; |
| }; |
| |
| /** |
| * Resets the state of timers tracking network activity and stability. If |
| * those timers are not yet started, invoking this function starts them. |
| * This function should be invoked when the tunnel is established and every |
| * time there is network activity on the tunnel, such that the timers can |
| * safely assume the network and/or server are not responding if this |
| * function has not been invoked for a significant period of time. |
| * |
| * @private |
| */ |
| var resetTimers = function resetTimers() { |
| |
| // Get rid of old timeouts (if any) |
| window.clearTimeout(receive_timeout); |
| window.clearTimeout(unstableTimeout); |
| window.clearTimeout(pingTimeout); |
| |
| // 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); |
| |
| var currentTime = new Date().getTime(); |
| var pingDelay = Math.max(lastSentPing + PING_FREQUENCY - currentTime, 0); |
| |
| // Ping tunnel endpoint regularly to test connection stability, sending |
| // the ping immediately if enough time has already elapsed |
| if (pingDelay > 0) |
| pingTimeout = window.setTimeout(sendPing, pingDelay); |
| else |
| sendPing(); |
| |
| }; |
| |
| /** |
| * 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); |
| window.clearTimeout(pingTimeout); |
| |
| // 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.isConnected()) |
| return; |
| |
| // Do not attempt to send empty messages |
| if (!arguments.length) |
| return; |
| |
| socket.send(Guacamole.Parser.toInstruction(arguments)); |
| |
| }; |
| |
| this.connect = function(data) { |
| |
| resetTimers(); |
| |
| // Mark the tunnel as connecting |
| tunnel.setState(Guacamole.Tunnel.State.CONNECTING); |
| |
| parser = new Guacamole.Parser(); |
| parser.oninstruction = function instructionReceived(opcode, args) { |
| |
| // Update state and UUID when first instruction received |
| if (tunnel.uuid === null) { |
| |
| // Associate tunnel UUID if received |
| if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE && args.length === 1) |
| tunnel.setUUID(args[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, args); |
| |
| }; |
| |
| // Connect socket |
| socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); |
| |
| socket.onopen = function(event) { |
| resetTimers(); |
| }; |
| |
| 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) { |
| |
| resetTimers(); |
| |
| try { |
| parser.receive(event.data); |
| } |
| catch (e) { |
| close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, e.message)); |
| } |
| |
| }; |
| |
| }; |
| |
| 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 {...Guacamole.Tunnel} 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; |
| |
| // Assign UUID if already known |
| if (tunnel.uuid) |
| chained_tunnel.setUUID(tunnel.uuid); |
| |
| // Assign any future received UUIDs such that they are |
| // accessible from the main uuid property of the chained tunnel |
| tunnel.onuuid = function uuidReceived(uuid) { |
| chained_tunnel.setUUID(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; |
| |
| /** |
| * AbortController instance which allows the current, in-progress HTTP |
| * request to be aborted. If no request is currently in progress, this will |
| * be null. |
| * |
| * @private |
| * @type {AbortController} |
| */ |
| var abortController = 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 |
| * @type {!object} |
| */ |
| var extraHeaders = extraTunnelHeaders || {}; |
| |
| /** |
| * The number of bytes in the file being downloaded, or null if this is not |
| * known. |
| * |
| * @type {number} |
| */ |
| this.size = null; |
| |
| 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); |
| |
| // Create Guacamole protocol and UTF-8 parsers specifically for this |
| // connection |
| var parser = new Guacamole.Parser(); |
| var utf8Parser = new Guacamole.UTF8Parser(); |
| |
| // Invoke tunnel's oninstruction handler for each parsed instruction |
| parser.oninstruction = function instructionReceived(opcode, args) { |
| if (tunnel.oninstruction) |
| tunnel.oninstruction(opcode, args); |
| }; |
| |
| // Allow new request to be aborted |
| abortController = new AbortController(); |
| |
| // Stream using the Fetch API |
| fetch(url, { |
| headers : extraHeaders, |
| credentials : crossDomain ? 'include' : 'same-origin', |
| signal : abortController.signal |
| }) |
| .then(function gotResponse(response) { |
| |
| // Reset state and close upon error |
| if (!response.ok) { |
| |
| if (tunnel.onerror) |
| tunnel.onerror(new Guacamole.Status( |
| Guacamole.Status.Code.fromHTTPCode(response.status), response.statusText)); |
| |
| tunnel.disconnect(); |
| return; |
| |
| } |
| |
| // Report overall size of stream in bytes, if known |
| tunnel.size = response.headers.get('Content-Length'); |
| |
| // Connection is open |
| tunnel.setState(Guacamole.Tunnel.State.OPEN); |
| |
| var reader = response.body.getReader(); |
| var processReceivedText = function processReceivedText(result) { |
| |
| // Clean up and close when done |
| if (result.done) { |
| tunnel.disconnect(); |
| return; |
| } |
| |
| // Parse only the portion of data which is newly received |
| parser.receive(utf8Parser.decode(result.value)); |
| |
| // Continue parsing when next chunk is received |
| reader.read().then(processReceivedText); |
| |
| }; |
| |
| // Schedule parse of first chunk |
| reader.read().then(processReceivedText); |
| |
| }); |
| |
| }; |
| |
| this.disconnect = function disconnect() { |
| |
| // Abort any in-progress request |
| if (abortController) { |
| abortController.abort(); |
| abortController = null; |
| } |
| |
| // Connection is now closed |
| tunnel.setState(Guacamole.Tunnel.State.CLOSED); |
| |
| }; |
| |
| }; |
| |
| Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel(); |
| </code></pre> |
| </article> |
| </section> |
| |
| |
| |
| |
| </div> |
| |
| <nav> |
| <h2><a href="index.html">Home</a></h2><h3>Namespaces</h3><ul><li><a href="Guacamole.html">Guacamole</a></li><li><a href="Guacamole.AudioContextFactory.html">Guacamole.AudioContextFactory</a></li></ul><h3>Classes</h3><ul><li><a href="Guacamole.ArrayBufferReader.html">Guacamole.ArrayBufferReader</a></li><li><a href="Guacamole.ArrayBufferWriter.html">Guacamole.ArrayBufferWriter</a></li><li><a href="Guacamole.AudioPlayer.html">Guacamole.AudioPlayer</a></li><li><a href="Guacamole.AudioRecorder.html">Guacamole.AudioRecorder</a></li><li><a href="Guacamole.BlobReader.html">Guacamole.BlobReader</a></li><li><a href="Guacamole.BlobWriter.html">Guacamole.BlobWriter</a></li><li><a href="Guacamole.ChainedTunnel.html">Guacamole.ChainedTunnel</a></li><li><a href="Guacamole.Client.html">Guacamole.Client</a></li><li><a href="Guacamole.DataURIReader.html">Guacamole.DataURIReader</a></li><li><a href="Guacamole.Display.html">Guacamole.Display</a></li><li><a href="Guacamole.Display.Statistics.html">Guacamole.Display.Statistics</a></li><li><a href="Guacamole.Display.VisibleLayer.html">Guacamole.Display.VisibleLayer</a></li><li><a href="Guacamole.Event.html">Guacamole.Event</a></li><li><a href="Guacamole.Event.DOMEvent.html">Guacamole.Event.DOMEvent</a></li><li><a href="Guacamole.Event.Target.html">Guacamole.Event.Target</a></li><li><a href="Guacamole.HTTPTunnel.html">Guacamole.HTTPTunnel</a></li><li><a href="Guacamole.InputSink.html">Guacamole.InputSink</a></li><li><a href="Guacamole.InputStream.html">Guacamole.InputStream</a></li><li><a href="Guacamole.IntegerPool.html">Guacamole.IntegerPool</a></li><li><a href="Guacamole.JSONReader.html">Guacamole.JSONReader</a></li><li><a href="Guacamole.KeyEventInterpreter.html">Guacamole.KeyEventInterpreter</a></li><li><a href="Guacamole.KeyEventInterpreter.KeyDefinition.html">Guacamole.KeyEventInterpreter.KeyDefinition</a></li><li><a href="Guacamole.KeyEventInterpreter.KeyEvent.html">Guacamole.KeyEventInterpreter.KeyEvent</a></li><li><a href="Guacamole.Keyboard.html">Guacamole.Keyboard</a></li><li><a href="Guacamole.Keyboard.ModifierState.html">Guacamole.Keyboard.ModifierState</a></li><li><a href="Guacamole.Layer.html">Guacamole.Layer</a></li><li><a href="Guacamole.Layer.Pixel.html">Guacamole.Layer.Pixel</a></li><li><a href="Guacamole.Mouse.html">Guacamole.Mouse</a></li><li><a href="Guacamole.Mouse.Event.html">Guacamole.Mouse.Event</a></li><li><a href="Guacamole.Mouse.Event.Target.html">Guacamole.Mouse.Event.Target</a></li><li><a href="Guacamole.Mouse.State.html">Guacamole.Mouse.State</a></li><li><a href="Guacamole.Mouse.Touchpad.html">Guacamole.Mouse.Touchpad</a></li><li><a href="Guacamole.Mouse.Touchscreen.html">Guacamole.Mouse.Touchscreen</a></li><li><a href="Guacamole.Object.html">Guacamole.Object</a></li><li><a href="Guacamole.OnScreenKeyboard.html">Guacamole.OnScreenKeyboard</a></li><li><a href="Guacamole.OnScreenKeyboard.Key.html">Guacamole.OnScreenKeyboard.Key</a></li><li><a href="Guacamole.OnScreenKeyboard.Layout.html">Guacamole.OnScreenKeyboard.Layout</a></li><li><a href="Guacamole.OutputStream.html">Guacamole.OutputStream</a></li><li><a href="Guacamole.Parser.html">Guacamole.Parser</a></li><li><a href="Guacamole.Position.html">Guacamole.Position</a></li><li><a href="Guacamole.RawAudioFormat.html">Guacamole.RawAudioFormat</a></li><li><a href="Guacamole.RawAudioPlayer.html">Guacamole.RawAudioPlayer</a></li><li><a href="Guacamole.RawAudioRecorder.html">Guacamole.RawAudioRecorder</a></li><li><a href="Guacamole.SessionRecording.html">Guacamole.SessionRecording</a></li><li><a href="Guacamole.StaticHTTPTunnel.html">Guacamole.StaticHTTPTunnel</a></li><li><a href="Guacamole.Status.html">Guacamole.Status</a></li><li><a href="Guacamole.StringReader.html">Guacamole.StringReader</a></li><li><a href="Guacamole.StringWriter.html">Guacamole.StringWriter</a></li><li><a href="Guacamole.Touch.html">Guacamole.Touch</a></li><li><a href="Guacamole.Touch.Event.html">Guacamole.Touch.Event</a></li><li><a href="Guacamole.Touch.State.html">Guacamole.Touch.State</a></li><li><a href="Guacamole.Tunnel.html">Guacamole.Tunnel</a></li><li><a href="Guacamole.UTF8Parser.html">Guacamole.UTF8Parser</a></li><li><a href="Guacamole.VideoPlayer.html">Guacamole.VideoPlayer</a></li><li><a href="Guacamole.WebSocketTunnel.html">Guacamole.WebSocketTunnel</a></li></ul><h3>Events</h3><ul><li><a href="Guacamole.ArrayBufferReader.html#event:ondata">Guacamole.ArrayBufferReader#ondata</a></li><li><a href="Guacamole.ArrayBufferReader.html#event:onend">Guacamole.ArrayBufferReader#onend</a></li><li><a href="Guacamole.ArrayBufferWriter.html#event:onack">Guacamole.ArrayBufferWriter#onack</a></li><li><a href="Guacamole.AudioRecorder.html#event:onclose">Guacamole.AudioRecorder#onclose</a></li><li><a href="Guacamole.AudioRecorder.html#event:onerror">Guacamole.AudioRecorder#onerror</a></li><li><a href="Guacamole.BlobReader.html#event:onend">Guacamole.BlobReader#onend</a></li><li><a href="Guacamole.BlobReader.html#event:onprogress">Guacamole.BlobReader#onprogress</a></li><li><a href="Guacamole.BlobWriter.html#event:onack">Guacamole.BlobWriter#onack</a></li><li><a href="Guacamole.BlobWriter.html#event:oncomplete">Guacamole.BlobWriter#oncomplete</a></li><li><a href="Guacamole.BlobWriter.html#event:onerror">Guacamole.BlobWriter#onerror</a></li><li><a href="Guacamole.BlobWriter.html#event:onprogress">Guacamole.BlobWriter#onprogress</a></li><li><a href="Guacamole.ChainedTunnel.html#event:onerror">Guacamole.ChainedTunnel#onerror</a></li><li><a href="Guacamole.ChainedTunnel.html#event:oninstruction">Guacamole.ChainedTunnel#oninstruction</a></li><li><a href="Guacamole.ChainedTunnel.html#event:onstatechange">Guacamole.ChainedTunnel#onstatechange</a></li><li><a href="Guacamole.ChainedTunnel.html#event:onuuid">Guacamole.ChainedTunnel#onuuid</a></li><li><a href="Guacamole.Client.html#event:onargv">Guacamole.Client#onargv</a></li><li><a href="Guacamole.Client.html#event:onaudio">Guacamole.Client#onaudio</a></li><li><a href="Guacamole.Client.html#event:onclipboard">Guacamole.Client#onclipboard</a></li><li><a href="Guacamole.Client.html#event:onerror">Guacamole.Client#onerror</a></li><li><a href="Guacamole.Client.html#event:onfile">Guacamole.Client#onfile</a></li><li><a href="Guacamole.Client.html#event:onfilesystem">Guacamole.Client#onfilesystem</a></li><li><a href="Guacamole.Client.html#event:onjoin">Guacamole.Client#onjoin</a></li><li><a href="Guacamole.Client.html#event:onleave">Guacamole.Client#onleave</a></li><li><a href="Guacamole.Client.html#event:onmsg">Guacamole.Client#onmsg</a></li><li><a href="Guacamole.Client.html#event:onmultitouch">Guacamole.Client#onmultitouch</a></li><li><a href="Guacamole.Client.html#event:onname">Guacamole.Client#onname</a></li><li><a href="Guacamole.Client.html#event:onpipe">Guacamole.Client#onpipe</a></li><li><a href="Guacamole.Client.html#event:onrequired">Guacamole.Client#onrequired</a></li><li><a href="Guacamole.Client.html#event:onstatechange">Guacamole.Client#onstatechange</a></li><li><a href="Guacamole.Client.html#event:onsync">Guacamole.Client#onsync</a></li><li><a href="Guacamole.Client.html#event:onvideo">Guacamole.Client#onvideo</a></li><li><a href="Guacamole.DataURIReader.html#event:onend">Guacamole.DataURIReader#onend</a></li><li><a href="Guacamole.Display.html#event:oncursor">Guacamole.Display#oncursor</a></li><li><a href="Guacamole.Display.html#event:onresize">Guacamole.Display#onresize</a></li><li><a href="Guacamole.Display.html#event:onstatistics">Guacamole.Display#onstatistics</a></li><li><a href="Guacamole.HTTPTunnel.html#event:onerror">Guacamole.HTTPTunnel#onerror</a></li><li><a href="Guacamole.HTTPTunnel.html#event:oninstruction">Guacamole.HTTPTunnel#oninstruction</a></li><li><a href="Guacamole.HTTPTunnel.html#event:onstatechange">Guacamole.HTTPTunnel#onstatechange</a></li><li><a href="Guacamole.HTTPTunnel.html#event:onuuid">Guacamole.HTTPTunnel#onuuid</a></li><li><a href="Guacamole.InputStream.html#event:onblob">Guacamole.InputStream#onblob</a></li><li><a href="Guacamole.InputStream.html#event:onend">Guacamole.InputStream#onend</a></li><li><a href="Guacamole.JSONReader.html#event:onend">Guacamole.JSONReader#onend</a></li><li><a href="Guacamole.JSONReader.html#event:onprogress">Guacamole.JSONReader#onprogress</a></li><li><a href="Guacamole.Keyboard.html#event:onkeydown">Guacamole.Keyboard#onkeydown</a></li><li><a href="Guacamole.Keyboard.html#event:onkeyup">Guacamole.Keyboard#onkeyup</a></li><li><a href="Guacamole.Mouse.html#event:mousedown">Guacamole.Mouse#mousedown</a></li><li><a href="Guacamole.Mouse.html#event:mousemove">Guacamole.Mouse#mousemove</a></li><li><a href="Guacamole.Mouse.html#event:mouseout">Guacamole.Mouse#mouseout</a></li><li><a href="Guacamole.Mouse.html#event:mouseup">Guacamole.Mouse#mouseup</a></li><li><a href="Guacamole.Mouse.Event.Target.html#event:mousedown">Guacamole.Mouse.Event.Target#mousedown</a></li><li><a href="Guacamole.Mouse.Event.Target.html#event:mousemove">Guacamole.Mouse.Event.Target#mousemove</a></li><li><a href="Guacamole.Mouse.Event.Target.html#event:mouseout">Guacamole.Mouse.Event.Target#mouseout</a></li><li><a href="Guacamole.Mouse.Event.Target.html#event:mouseup">Guacamole.Mouse.Event.Target#mouseup</a></li><li><a href="Guacamole.Mouse.Touchpad.html#event:mousedown">Guacamole.Mouse.Touchpad#mousedown</a></li><li><a href="Guacamole.Mouse.Touchpad.html#event:mousemove">Guacamole.Mouse.Touchpad#mousemove</a></li><li><a href="Guacamole.Mouse.Touchpad.html#event:mouseup">Guacamole.Mouse.Touchpad#mouseup</a></li><li><a href="Guacamole.Mouse.Touchscreen.html#event:mousedown">Guacamole.Mouse.Touchscreen#mousedown</a></li><li><a href="Guacamole.Mouse.Touchscreen.html#event:mousemove">Guacamole.Mouse.Touchscreen#mousemove</a></li><li><a href="Guacamole.Mouse.Touchscreen.html#event:mouseup">Guacamole.Mouse.Touchscreen#mouseup</a></li><li><a href="Guacamole.Object.html#event:onbody">Guacamole.Object#onbody</a></li><li><a href="Guacamole.Object.html#event:onundefine">Guacamole.Object#onundefine</a></li><li><a href="Guacamole.OnScreenKeyboard.html#event:onkeydown">Guacamole.OnScreenKeyboard#onkeydown</a></li><li><a href="Guacamole.OnScreenKeyboard.html#event:onkeyup">Guacamole.OnScreenKeyboard#onkeyup</a></li><li><a href="Guacamole.OutputStream.html#event:onack">Guacamole.OutputStream#onack</a></li><li><a href="Guacamole.Parser.html#event:oninstruction">Guacamole.Parser#oninstruction</a></li><li><a href="Guacamole.RawAudioRecorder.html#event:onclose">Guacamole.RawAudioRecorder#onclose</a></li><li><a href="Guacamole.RawAudioRecorder.html#event:onerror">Guacamole.RawAudioRecorder#onerror</a></li><li><a href="Guacamole.SessionRecording.html#event:onabort">Guacamole.SessionRecording#onabort</a></li><li><a href="Guacamole.SessionRecording.html#event:onerror">Guacamole.SessionRecording#onerror</a></li><li><a href="Guacamole.SessionRecording.html#event:onkeyevents">Guacamole.SessionRecording#onkeyevents</a></li><li><a href="Guacamole.SessionRecording.html#event:onload">Guacamole.SessionRecording#onload</a></li><li><a href="Guacamole.SessionRecording.html#event:onpause">Guacamole.SessionRecording#onpause</a></li><li><a href="Guacamole.SessionRecording.html#event:onplay">Guacamole.SessionRecording#onplay</a></li><li><a href="Guacamole.SessionRecording.html#event:onprogress">Guacamole.SessionRecording#onprogress</a></li><li><a href="Guacamole.SessionRecording.html#event:onseek">Guacamole.SessionRecording#onseek</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:onerror">Guacamole.SessionRecording._PlaybackTunnel#onerror</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:oninstruction">Guacamole.SessionRecording._PlaybackTunnel#oninstruction</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:onstatechange">Guacamole.SessionRecording._PlaybackTunnel#onstatechange</a></li><li><a href="Guacamole.SessionRecording._PlaybackTunnel.html#event:onuuid">Guacamole.SessionRecording._PlaybackTunnel#onuuid</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:onerror">Guacamole.StaticHTTPTunnel#onerror</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:oninstruction">Guacamole.StaticHTTPTunnel#oninstruction</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:onstatechange">Guacamole.StaticHTTPTunnel#onstatechange</a></li><li><a href="Guacamole.StaticHTTPTunnel.html#event:onuuid">Guacamole.StaticHTTPTunnel#onuuid</a></li><li><a href="Guacamole.StringReader.html#event:onend">Guacamole.StringReader#onend</a></li><li><a href="Guacamole.StringReader.html#event:ontext">Guacamole.StringReader#ontext</a></li><li><a href="Guacamole.StringWriter.html#event:onack">Guacamole.StringWriter#onack</a></li><li><a href="Guacamole.Touch.html#event:touchend">Guacamole.Touch#touchend</a></li><li><a href="Guacamole.Touch.html#event:touchmove">Guacamole.Touch#touchmove</a></li><li><a href="Guacamole.Touch.html#event:touchstart">Guacamole.Touch#touchstart</a></li><li><a href="Guacamole.Tunnel.html#event:onerror">Guacamole.Tunnel#onerror</a></li><li><a href="Guacamole.Tunnel.html#event:oninstruction">Guacamole.Tunnel#oninstruction</a></li><li><a href="Guacamole.Tunnel.html#event:onstatechange">Guacamole.Tunnel#onstatechange</a></li><li><a href="Guacamole.Tunnel.html#event:onuuid">Guacamole.Tunnel#onuuid</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:onerror">Guacamole.WebSocketTunnel#onerror</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:oninstruction">Guacamole.WebSocketTunnel#oninstruction</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:onstatechange">Guacamole.WebSocketTunnel#onstatechange</a></li><li><a href="Guacamole.WebSocketTunnel.html#event:onuuid">Guacamole.WebSocketTunnel#onuuid</a></li></ul> |
| </nav> |
| |
| <br class="clear"> |
| |
| <footer> |
| Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.2</a> on Mon Jun 16 2025 15:53:22 GMT-0700 (Pacific Daylight Time) |
| </footer> |
| |
| <script> prettyPrint(); </script> |
| <script src="scripts/linenumber.js"> </script> |
| </body> |
| </html> |