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 == 200) return "Success";
212 if (status == 403) return "Unauthorized";
213 if (status == 404) return "Connection does not exist";
215 // Internal server errors
216 if (status >= 500 && status <= 599) return "Server error";
218 // Otherwise, unknown
219 return "Unknown error";
223 function handleHTTPTunnelError(xmlhttprequest) {
226 var message = getHTTPTunnelErrorMessage(xmlhttprequest);
228 // Call error handler
229 if (tunnel.onerror) tunnel.onerror(message);
237 function handleResponse(xmlhttprequest) {
240 var nextRequest = null;
242 var dataUpdateEvents = 0;
244 // The location of the last element's terminator
247 // Where to start the next length search or the next element
251 var elements = new Array();
253 function parseResponse() {
255 // Do not handle responses if not connected
256 if (currentState != STATE_CONNECTED) {
258 // Clean up interval if polling
259 if (interval != null)
260 clearInterval(interval);
265 // Do not parse response yet if not ready
266 if (xmlhttprequest.readyState < 2) return;
268 // Attempt to read status
270 try { status = xmlhttprequest.status; }
272 // If status could not be read, assume successful.
273 catch (e) { status = 200; }
275 // Start next request as soon as possible IF request was successful
276 if (nextRequest == null && status == 200)
277 nextRequest = makeRequest();
279 // Parse stream when data is received and when complete.
280 if (xmlhttprequest.readyState == 3 ||
281 xmlhttprequest.readyState == 4) {
283 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
284 if (pollingMode == POLLING_ENABLED) {
285 if (xmlhttprequest.readyState == 3 && interval == null)
286 interval = setInterval(parseResponse, 30);
287 else if (xmlhttprequest.readyState == 4 && interval != null)
288 clearInterval(interval);
291 // If canceled, stop transfer
292 if (xmlhttprequest.status == 0) {
297 // Halt on error during request
298 else if (xmlhttprequest.status != 200) {
299 handleHTTPTunnelError(xmlhttprequest);
303 // Attempt to read in-progress data
305 try { current = xmlhttprequest.responseText; }
307 // Do not attempt to parse if data could not be read
308 catch (e) { return; }
310 // While search is within currently received data
311 while (elementEnd < current.length) {
313 // If we are waiting for element data
314 if (elementEnd >= startIndex) {
316 // We now have enough data for the element. Parse.
317 var element = current.substring(startIndex, elementEnd);
318 var terminator = current.substring(elementEnd, elementEnd+1);
320 // Add element to array
321 elements.push(element);
323 // If last element, handle instruction
324 if (terminator == ";") {
327 var opcode = elements.shift();
329 // Call instruction handler.
330 if (tunnel.oninstruction != null)
331 tunnel.oninstruction(opcode, elements);
338 // Start searching for length at character after
339 // element terminator
340 startIndex = elementEnd + 1;
344 // Search for end of length
345 var lengthEnd = current.indexOf(".", startIndex);
346 if (lengthEnd != -1) {
349 var length = parseInt(current.substring(elementEnd+1, lengthEnd));
351 // If we're done parsing, handle the next response.
354 // Clean up interval if polling
355 if (interval != null)
356 clearInterval(interval);
359 xmlhttprequest.onreadystatechange = null;
360 xmlhttprequest.abort();
362 // Start handling next request
364 handleResponse(nextRequest);
371 // Calculate start of element
372 startIndex = lengthEnd + 1;
374 // Calculate location of element terminator
375 elementEnd = startIndex + length;
379 // If no period yet, continue search when more data
382 startIndex = current.length;
392 // If response polling enabled, attempt to detect if still
393 // necessary (via wrapping parseResponse())
394 if (pollingMode == POLLING_ENABLED) {
395 xmlhttprequest.onreadystatechange = function() {
397 // If we receive two or more readyState==3 events,
398 // there is no need to poll.
399 if (xmlhttprequest.readyState == 3) {
401 if (dataUpdateEvents >= 2) {
402 pollingMode = POLLING_DISABLED;
403 xmlhttprequest.onreadystatechange = parseResponse;
411 // Otherwise, just parse
413 xmlhttprequest.onreadystatechange = parseResponse;
420 function makeRequest() {
423 var xmlhttprequest = new XMLHttpRequest();
424 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
425 xmlhttprequest.send(null);
427 return xmlhttprequest;
431 this.connect = function(data) {
433 // Start tunnel and connect synchronously
434 var connect_xmlhttprequest = new XMLHttpRequest();
435 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
436 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
437 connect_xmlhttprequest.send(data);
439 // If failure, throw error
440 if (connect_xmlhttprequest.status != 200) {
441 var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest);
442 throw new Error(message);
445 // Get UUID from response
446 tunnel_uuid = connect_xmlhttprequest.responseText;
448 // Start reading data
449 currentState = STATE_CONNECTED;
450 handleResponse(makeRequest());
454 this.disconnect = function() {
455 currentState = STATE_DISCONNECTED;
460 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
464 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
467 * @augments Guacamole.Tunnel
468 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
470 Guacamole.WebSocketTunnel = function(tunnelURL) {
473 * Reference to this WebSocket tunnel.
478 * The WebSocket used by this tunnel.
483 * The WebSocket protocol corresponding to the protocol used for the current
492 1000: "Connection closed normally.",
493 1001: "Connection shut down.",
494 1002: "Protocol error.",
495 1003: "Invalid data.",
496 1004: "[UNKNOWN, RESERVED]",
497 1005: "No status code present.",
498 1006: "Connection closed abnormally.",
499 1007: "Inconsistent data type.",
500 1008: "Policy violation.",
501 1009: "Message too large.",
502 1010: "Extension negotiation failed."
506 var STATE_CONNECTED = 1;
507 var STATE_DISCONNECTED = 2;
509 var currentState = STATE_IDLE;
511 // Transform current URL to WebSocket URL
513 // If not already a websocket URL
514 if ( tunnelURL.substring(0, 3) != "ws:"
515 && tunnelURL.substring(0, 4) != "wss:") {
517 var protocol = ws_protocol[window.location.protocol];
519 // If absolute URL, convert to absolute WS URL
520 if (tunnelURL.substring(0, 1) == "/")
523 + "//" + window.location.host
526 // Otherwise, construct absolute from relative URL
529 // Get path from pathname
530 var slash = window.location.pathname.lastIndexOf("/");
531 var path = window.location.pathname.substring(0, slash + 1);
533 // Construct absolute URL
536 + "//" + window.location.host
544 this.sendMessage = function(elements) {
546 // Do not attempt to send messages if not connected
547 if (currentState != STATE_CONNECTED)
550 // Do not attempt to send empty messages
551 if (arguments.length == 0)
555 * Converts the given value to a length/string pair for use as an
556 * element in a Guacamole instruction.
558 * @param value The value to convert.
559 * @return {String} The converted value.
561 function getElement(value) {
562 var string = new String(value);
563 return string.length + "." + string;
566 // Initialized message with first element
567 var message = getElement(arguments[0]);
569 // Append remaining elements
570 for (var i=1; i<arguments.length; i++)
571 message += "," + getElement(arguments[i]);
576 socket.send(message);
580 this.connect = function(data) {
583 socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
585 socket.onopen = function(event) {
586 currentState = STATE_CONNECTED;
589 socket.onclose = function(event) {
591 // If connection closed abnormally, signal error.
592 if (event.code != 1000 && tunnel.onerror)
593 tunnel.onerror(status_code[event.code]);
597 socket.onerror = function(event) {
599 // Call error handler
600 if (tunnel.onerror) tunnel.onerror(event.data);
604 socket.onmessage = function(event) {
606 var message = event.data;
614 // Search for end of length
615 var lengthEnd = message.indexOf(".", startIndex);
616 if (lengthEnd != -1) {
619 var length = parseInt(message.substring(elementEnd+1, lengthEnd));
621 // Calculate start of element
622 startIndex = lengthEnd + 1;
624 // Calculate location of element terminator
625 elementEnd = startIndex + length;
629 // If no period, incomplete instruction.
631 throw new Error("Incomplete instruction.");
633 // We now have enough data for the element. Parse.
634 var element = message.substring(startIndex, elementEnd);
635 var terminator = message.substring(elementEnd, elementEnd+1);
637 // Add element to array
638 elements.push(element);
640 // If last element, handle instruction
641 if (terminator == ";") {
644 var opcode = elements.shift();
646 // Call instruction handler.
647 if (tunnel.oninstruction != null)
648 tunnel.oninstruction(opcode, elements);
655 // Start searching for length at character after
656 // element terminator
657 startIndex = elementEnd + 1;
659 } while (startIndex < message.length);
665 this.disconnect = function() {
666 currentState = STATE_DISCONNECTED;
672 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
676 * Guacamole Tunnel which cycles between all specified tunnels until
677 * no tunnels are left. Another tunnel is used if an error occurs but
678 * no instructions have been received. If an instruction has been
679 * received, or no tunnels remain, the error is passed directly out
680 * through the onerror handler (if defined).
683 * @augments Guacamole.Tunnel
684 * @param {...} tunnel_chain The tunnels to use, in order of priority.
686 Guacamole.ChainedTunnel = function(tunnel_chain) {
689 * Reference to this chained tunnel.
691 var chained_tunnel = this;
694 * The currently wrapped tunnel, if any.
696 var current_tunnel = null;
699 * Data passed in via connect(), to be used for
700 * wrapped calls to other tunnels' connect() functions.
705 * Array of all tunnels passed to this ChainedTunnel through the
706 * constructor arguments.
710 // Load all tunnels into array
711 for (var i=0; i<arguments.length; i++)
712 tunnels.push(arguments[i]);
715 * Sets the current tunnel
717 function attach(tunnel) {
719 // Clear handlers of current tunnel, if any
720 if (current_tunnel) {
721 current_tunnel.onerror = null;
722 current_tunnel.oninstruction = null;
725 // Set own functions to tunnel's functions
726 chained_tunnel.disconnect = tunnel.disconnect;
727 chained_tunnel.sendMessage = tunnel.sendMessage;
729 // Record current tunnel
730 current_tunnel = tunnel;
732 // Wrap own oninstruction within current tunnel
733 current_tunnel.oninstruction = function(opcode, elements) {
736 chained_tunnel.oninstruction(opcode, elements);
738 // Use handler permanently from now on
739 current_tunnel.oninstruction = chained_tunnel.oninstruction;
741 // Pass through errors (without trying other tunnels)
742 current_tunnel.onerror = chained_tunnel.onerror;
746 // Attach next tunnel on error
747 current_tunnel.onerror = function(message) {
750 var next_tunnel = tunnels.shift();
752 // If there IS a next tunnel, try using it.
756 // Otherwise, call error handler
757 else if (chained_tunnel.onerror)
758 chained_tunnel.onerror(message);
764 // Attempt connection
765 current_tunnel.connect(connect_data);
770 // Call error handler of current tunnel on error
771 current_tunnel.onerror(e.message);
778 this.connect = function(data) {
780 // Remember connect data
784 var next_tunnel = tunnels.shift();
786 // Attach first tunnel
790 // If there IS no first tunnel, error
791 else if (chained_tunnel.onerror)
792 chained_tunnel.onerror("No tunnels to try.");
798 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();