2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
5 * The contents of this file are subject to the Mozilla Public License Version
6 * 1.1 (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 * http://www.mozilla.org/MPL/
10 * Software distributed under the License is distributed on an "AS IS" basis,
11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12 * for the specific language governing rights and limitations under the
15 * The Original Code is guacamole-common-js.
17 * The Initial Developer of the Original Code is
19 * Portions created by the Initial Developer are Copyright (C) 2010
20 * the Initial Developer. All Rights Reserved.
24 * Alternatively, the contents of this file may be used under the terms of
25 * either the GNU General Public License Version 2 or later (the "GPL"), or
26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27 * in which case the provisions of the GPL or the LGPL are applicable instead
28 * of those above. If you wish to allow use of your version of this file only
29 * under the terms of either the GPL or the LGPL, and not to allow others to
30 * use your version of this file under the terms of the MPL, indicate your
31 * decision by deleting the provisions above and replace them with the notice
32 * and other provisions required by the GPL or the LGPL. If you do not delete
33 * the provisions above, a recipient may use your version of this file under
34 * the terms of any one of the MPL, the GPL or the LGPL.
36 * ***** END LICENSE BLOCK ***** */
38 // Guacamole namespace
39 var Guacamole = Guacamole || {};
42 * Core object providing abstract communication for Guacamole. This object
43 * is a null implementation whose functions do nothing. Guacamole applications
44 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
48 * @see Guacamole.HTTPTunnel
50 Guacamole.Tunnel = function() {
53 * Connect to the tunnel with the given optional data. This data is
54 * typically used for authentication. The format of data accepted is
55 * up to the tunnel implementation.
57 * @param {String} data The data to send to the tunnel when connecting.
59 this.connect = function(data) {};
62 * Disconnect from the tunnel.
64 this.disconnect = function() {};
67 * Send the given message through the tunnel to the service on the other
68 * side. All messages are guaranteed to be received in the order sent.
70 * @param {...} elements The elements of the message to send to the
71 * service on the other side of the tunnel.
73 this.sendMessage = function(elements) {};
76 * Fired whenever an error is encountered by the tunnel.
79 * @param {String} message A human-readable description of the error that
85 * Fired once for every complete Guacamole instruction received, in order.
88 * @param {String} opcode The Guacamole instruction opcode.
89 * @param {Array} parameters The parameters provided for the instruction,
92 this.oninstruction = null;
97 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
100 * @augments Guacamole.Tunnel
101 * @param {String} tunnelURL The URL of the HTTP tunneling service.
103 Guacamole.HTTPTunnel = function(tunnelURL) {
106 * Reference to this HTTP tunnel.
112 var TUNNEL_CONNECT = tunnelURL + "?connect";
113 var TUNNEL_READ = tunnelURL + "?read:";
114 var TUNNEL_WRITE = tunnelURL + "?write:";
117 var STATE_CONNECTED = 1;
118 var STATE_DISCONNECTED = 2;
120 var currentState = STATE_IDLE;
122 var POLLING_ENABLED = 1;
123 var POLLING_DISABLED = 0;
125 // Default to polling - will be turned off automatically if not needed
126 var pollingMode = POLLING_ENABLED;
128 var sendingMessages = false;
129 var outputMessageBuffer = "";
131 this.sendMessage = function() {
133 // Do not attempt to send messages if not connected
134 if (currentState != STATE_CONNECTED)
137 // Do not attempt to send empty messages
138 if (arguments.length == 0)
142 * Converts the given value to a length/string pair for use as an
143 * element in a Guacamole instruction.
145 * @param value The value to convert.
146 * @return {String} The converted value.
148 function getElement(value) {
149 var string = new String(value);
150 return string.length + "." + string;
153 // Initialized message with first element
154 var message = getElement(arguments[0]);
156 // Append remaining elements
157 for (var i=1; i<arguments.length; i++)
158 message += "," + getElement(arguments[i]);
163 // Add message to buffer
164 outputMessageBuffer += message;
166 // Send if not currently sending
167 if (!sendingMessages)
168 sendPendingMessages();
172 function sendPendingMessages() {
174 if (outputMessageBuffer.length > 0) {
176 sendingMessages = true;
178 var message_xmlhttprequest = new XMLHttpRequest();
179 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
180 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
182 // Once response received, send next queued event.
183 message_xmlhttprequest.onreadystatechange = function() {
184 if (message_xmlhttprequest.readyState == 4) {
186 // If an error occurs during send, handle it
187 if (message_xmlhttprequest.status != 200)
188 handleHTTPTunnelError(message_xmlhttprequest);
190 // Otherwise, continue the send loop
192 sendPendingMessages();
197 message_xmlhttprequest.send(outputMessageBuffer);
198 outputMessageBuffer = ""; // Clear buffer
202 sendingMessages = false;
206 function getHTTPTunnelErrorMessage(xmlhttprequest) {
208 var status = xmlhttprequest.status;
211 if (status == 0) return "Disconnected";
212 if (status == 200) return "Success";
213 if (status == 403) return "Unauthorized";
214 if (status == 404) return "Connection does not exist";
216 // Internal server errors
217 if (status >= 500 && status <= 599) return "Server error";
219 // Otherwise, unknown
220 return "Unknown error";
224 function handleHTTPTunnelError(xmlhttprequest) {
227 var message = getHTTPTunnelErrorMessage(xmlhttprequest);
229 // Call error handler
230 if (tunnel.onerror) tunnel.onerror(message);
238 function handleResponse(xmlhttprequest) {
241 var nextRequest = null;
243 var dataUpdateEvents = 0;
245 // The location of the last element's terminator
248 // Where to start the next length search or the next element
252 var elements = new Array();
254 function parseResponse() {
256 // Do not handle responses if not connected
257 if (currentState != STATE_CONNECTED) {
259 // Clean up interval if polling
260 if (interval != null)
261 clearInterval(interval);
266 // Do not parse response yet if not ready
267 if (xmlhttprequest.readyState < 2) return;
269 // Attempt to read status
271 try { status = xmlhttprequest.status; }
273 // If status could not be read, assume successful.
274 catch (e) { status = 200; }
276 // Start next request as soon as possible IF request was successful
277 if (nextRequest == null && status == 200)
278 nextRequest = makeRequest();
280 // Parse stream when data is received and when complete.
281 if (xmlhttprequest.readyState == 3 ||
282 xmlhttprequest.readyState == 4) {
284 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
285 if (pollingMode == POLLING_ENABLED) {
286 if (xmlhttprequest.readyState == 3 && interval == null)
287 interval = setInterval(parseResponse, 30);
288 else if (xmlhttprequest.readyState == 4 && interval != null)
289 clearInterval(interval);
292 // If canceled, stop transfer
293 if (xmlhttprequest.status == 0) {
298 // Halt on error during request
299 else if (xmlhttprequest.status != 200) {
300 handleHTTPTunnelError(xmlhttprequest);
304 // Attempt to read in-progress data
306 try { current = xmlhttprequest.responseText; }
308 // Do not attempt to parse if data could not be read
309 catch (e) { return; }
311 // While search is within currently received data
312 while (elementEnd < current.length) {
314 // If we are waiting for element data
315 if (elementEnd >= startIndex) {
317 // We now have enough data for the element. Parse.
318 var element = current.substring(startIndex, elementEnd);
319 var terminator = current.substring(elementEnd, elementEnd+1);
321 // Add element to array
322 elements.push(element);
324 // If last element, handle instruction
325 if (terminator == ";") {
328 var opcode = elements.shift();
330 // Call instruction handler.
331 if (tunnel.oninstruction != null)
332 tunnel.oninstruction(opcode, elements);
339 // Start searching for length at character after
340 // element terminator
341 startIndex = elementEnd + 1;
345 // Search for end of length
346 var lengthEnd = current.indexOf(".", startIndex);
347 if (lengthEnd != -1) {
350 var length = parseInt(current.substring(elementEnd+1, lengthEnd));
352 // If we're done parsing, handle the next response.
355 // Clean up interval if polling
356 if (interval != null)
357 clearInterval(interval);
360 xmlhttprequest.onreadystatechange = null;
361 xmlhttprequest.abort();
363 // Start handling next request
365 handleResponse(nextRequest);
372 // Calculate start of element
373 startIndex = lengthEnd + 1;
375 // Calculate location of element terminator
376 elementEnd = startIndex + length;
380 // If no period yet, continue search when more data
383 startIndex = current.length;
393 // If response polling enabled, attempt to detect if still
394 // necessary (via wrapping parseResponse())
395 if (pollingMode == POLLING_ENABLED) {
396 xmlhttprequest.onreadystatechange = function() {
398 // If we receive two or more readyState==3 events,
399 // there is no need to poll.
400 if (xmlhttprequest.readyState == 3) {
402 if (dataUpdateEvents >= 2) {
403 pollingMode = POLLING_DISABLED;
404 xmlhttprequest.onreadystatechange = parseResponse;
412 // Otherwise, just parse
414 xmlhttprequest.onreadystatechange = parseResponse;
421 function makeRequest() {
424 var xmlhttprequest = new XMLHttpRequest();
425 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
426 xmlhttprequest.send(null);
428 return xmlhttprequest;
432 this.connect = function(data) {
434 // Start tunnel and connect synchronously
435 var connect_xmlhttprequest = new XMLHttpRequest();
436 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
437 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
438 connect_xmlhttprequest.send(data);
440 // If failure, throw error
441 if (connect_xmlhttprequest.status != 200) {
442 var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest);
443 throw new Error(message);
446 // Get UUID from response
447 tunnel_uuid = connect_xmlhttprequest.responseText;
449 // Start reading data
450 currentState = STATE_CONNECTED;
451 handleResponse(makeRequest());
455 this.disconnect = function() {
456 currentState = STATE_DISCONNECTED;
461 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
465 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
468 * @augments Guacamole.Tunnel
469 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
471 Guacamole.WebSocketTunnel = function(tunnelURL) {
474 * Reference to this WebSocket tunnel.
479 * The WebSocket used by this tunnel.
484 * The WebSocket protocol corresponding to the protocol used for the current
493 1000: "Connection closed normally.",
494 1001: "Connection shut down.",
495 1002: "Protocol error.",
496 1003: "Invalid data.",
497 1004: "[UNKNOWN, RESERVED]",
498 1005: "No status code present.",
499 1006: "Connection closed abnormally.",
500 1007: "Inconsistent data type.",
501 1008: "Policy violation.",
502 1009: "Message too large.",
503 1010: "Extension negotiation failed."
507 var STATE_CONNECTED = 1;
508 var STATE_DISCONNECTED = 2;
510 var currentState = STATE_IDLE;
512 // Transform current URL to WebSocket URL
514 // If not already a websocket URL
515 if ( tunnelURL.substring(0, 3) != "ws:"
516 && tunnelURL.substring(0, 4) != "wss:") {
518 var protocol = ws_protocol[window.location.protocol];
520 // If absolute URL, convert to absolute WS URL
521 if (tunnelURL.substring(0, 1) == "/")
524 + "//" + window.location.host
527 // Otherwise, construct absolute from relative URL
530 // Get path from pathname
531 var slash = window.location.pathname.lastIndexOf("/");
532 var path = window.location.pathname.substring(0, slash + 1);
534 // Construct absolute URL
537 + "//" + window.location.host
545 this.sendMessage = function(elements) {
547 // Do not attempt to send messages if not connected
548 if (currentState != STATE_CONNECTED)
551 // Do not attempt to send empty messages
552 if (arguments.length == 0)
556 * Converts the given value to a length/string pair for use as an
557 * element in a Guacamole instruction.
559 * @param value The value to convert.
560 * @return {String} The converted value.
562 function getElement(value) {
563 var string = new String(value);
564 return string.length + "." + string;
567 // Initialized message with first element
568 var message = getElement(arguments[0]);
570 // Append remaining elements
571 for (var i=1; i<arguments.length; i++)
572 message += "," + getElement(arguments[i]);
577 socket.send(message);
581 this.connect = function(data) {
584 socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
586 socket.onopen = function(event) {
587 currentState = STATE_CONNECTED;
590 socket.onclose = function(event) {
592 // If connection closed abnormally, signal error.
593 if (event.code != 1000 && tunnel.onerror)
594 tunnel.onerror(status_code[event.code]);
598 socket.onerror = function(event) {
600 // Call error handler
601 if (tunnel.onerror) tunnel.onerror(event.data);
605 socket.onmessage = function(event) {
607 var message = event.data;
615 // Search for end of length
616 var lengthEnd = message.indexOf(".", startIndex);
617 if (lengthEnd != -1) {
620 var length = parseInt(message.substring(elementEnd+1, lengthEnd));
622 // Calculate start of element
623 startIndex = lengthEnd + 1;
625 // Calculate location of element terminator
626 elementEnd = startIndex + length;
630 // If no period, incomplete instruction.
632 throw new Error("Incomplete instruction.");
634 // We now have enough data for the element. Parse.
635 var element = message.substring(startIndex, elementEnd);
636 var terminator = message.substring(elementEnd, elementEnd+1);
638 // Add element to array
639 elements.push(element);
641 // If last element, handle instruction
642 if (terminator == ";") {
645 var opcode = elements.shift();
647 // Call instruction handler.
648 if (tunnel.oninstruction != null)
649 tunnel.oninstruction(opcode, elements);
656 // Start searching for length at character after
657 // element terminator
658 startIndex = elementEnd + 1;
660 } while (startIndex < message.length);
666 this.disconnect = function() {
667 currentState = STATE_DISCONNECTED;
673 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
677 * Guacamole Tunnel which cycles between all specified tunnels until
678 * no tunnels are left. Another tunnel is used if an error occurs but
679 * no instructions have been received. If an instruction has been
680 * received, or no tunnels remain, the error is passed directly out
681 * through the onerror handler (if defined).
684 * @augments Guacamole.Tunnel
685 * @param {...} tunnel_chain The tunnels to use, in order of priority.
687 Guacamole.ChainedTunnel = function(tunnel_chain) {
690 * Reference to this chained tunnel.
692 var chained_tunnel = this;
695 * The currently wrapped tunnel, if any.
697 var current_tunnel = null;
700 * Data passed in via connect(), to be used for
701 * wrapped calls to other tunnels' connect() functions.
706 * Array of all tunnels passed to this ChainedTunnel through the
707 * constructor arguments.
711 // Load all tunnels into array
712 for (var i=0; i<arguments.length; i++)
713 tunnels.push(arguments[i]);
716 * Sets the current tunnel
718 function attach(tunnel) {
720 // Clear handlers of current tunnel, if any
721 if (current_tunnel) {
722 current_tunnel.onerror = null;
723 current_tunnel.oninstruction = null;
726 // Set own functions to tunnel's functions
727 chained_tunnel.disconnect = tunnel.disconnect;
728 chained_tunnel.sendMessage = tunnel.sendMessage;
730 // Record current tunnel
731 current_tunnel = tunnel;
733 // Wrap own oninstruction within current tunnel
734 current_tunnel.oninstruction = function(opcode, elements) {
737 chained_tunnel.oninstruction(opcode, elements);
739 // Use handler permanently from now on
740 current_tunnel.oninstruction = chained_tunnel.oninstruction;
742 // Pass through errors (without trying other tunnels)
743 current_tunnel.onerror = chained_tunnel.onerror;
747 // Attach next tunnel on error
748 current_tunnel.onerror = function(message) {
751 var next_tunnel = tunnels.shift();
753 // If there IS a next tunnel, try using it.
757 // Otherwise, call error handler
758 else if (chained_tunnel.onerror)
759 chained_tunnel.onerror(message);
765 // Attempt connection
766 current_tunnel.connect(connect_data);
771 // Call error handler of current tunnel on error
772 current_tunnel.onerror(e.message);
779 this.connect = function(data) {
781 // Remember connect data
785 var next_tunnel = tunnels.shift();
787 // Attach first tunnel
791 // If there IS no first tunnel, error
792 else if (chained_tunnel.onerror)
793 chained_tunnel.onerror("No tunnels to try.");
799 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();