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 ***** */
39 * Namespace for all Guacamole JavaScript objects.
42 var Guacamole = Guacamole || {};
45 * Core object providing abstract communication for Guacamole. This object
46 * is a null implementation whose functions do nothing. Guacamole applications
47 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
51 * @see Guacamole.HTTPTunnel
53 Guacamole.Tunnel = function() {
56 * Connect to the tunnel with the given optional data. This data is
57 * typically used for authentication. The format of data accepted is
58 * up to the tunnel implementation.
60 * @param {String} data The data to send to the tunnel when connecting.
62 this.connect = function(data) {};
65 * Disconnect from the tunnel.
67 this.disconnect = function() {};
70 * Send the given message through the tunnel to the service on the other
71 * side. All messages are guaranteed to be received in the order sent.
73 * @param {...} elements The elements of the message to send to the
74 * service on the other side of the tunnel.
76 this.sendMessage = function(elements) {};
79 * Fired whenever an error is encountered by the tunnel.
82 * @param {String} message A human-readable description of the error that
88 * Fired once for every complete Guacamole instruction received, in order.
91 * @param {String} opcode The Guacamole instruction opcode.
92 * @param {Array} parameters The parameters provided for the instruction,
95 this.oninstruction = null;
100 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
103 * @augments Guacamole.Tunnel
104 * @param {String} tunnelURL The URL of the HTTP tunneling service.
106 Guacamole.HTTPTunnel = function(tunnelURL) {
109 * Reference to this HTTP tunnel.
116 var TUNNEL_CONNECT = tunnelURL + "?connect";
117 var TUNNEL_READ = tunnelURL + "?read:";
118 var TUNNEL_WRITE = tunnelURL + "?write:";
121 var STATE_CONNECTED = 1;
122 var STATE_DISCONNECTED = 2;
124 var currentState = STATE_IDLE;
126 var POLLING_ENABLED = 1;
127 var POLLING_DISABLED = 0;
129 // Default to polling - will be turned off automatically if not needed
130 var pollingMode = POLLING_ENABLED;
132 var sendingMessages = false;
133 var outputMessageBuffer = "";
135 this.sendMessage = function() {
137 // Do not attempt to send messages if not connected
138 if (currentState != STATE_CONNECTED)
141 // Do not attempt to send empty messages
142 if (arguments.length == 0)
146 * Converts the given value to a length/string pair for use as an
147 * element in a Guacamole instruction.
150 * @param value The value to convert.
151 * @return {String} The converted value.
153 function getElement(value) {
154 var string = new String(value);
155 return string.length + "." + string;
158 // Initialized message with first element
159 var message = getElement(arguments[0]);
161 // Append remaining elements
162 for (var i=1; i<arguments.length; i++)
163 message += "," + getElement(arguments[i]);
168 // Add message to buffer
169 outputMessageBuffer += message;
171 // Send if not currently sending
172 if (!sendingMessages)
173 sendPendingMessages();
177 function sendPendingMessages() {
179 if (outputMessageBuffer.length > 0) {
181 sendingMessages = true;
183 var message_xmlhttprequest = new XMLHttpRequest();
184 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
185 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
187 // Once response received, send next queued event.
188 message_xmlhttprequest.onreadystatechange = function() {
189 if (message_xmlhttprequest.readyState == 4) {
191 // If an error occurs during send, handle it
192 if (message_xmlhttprequest.status != 200)
193 handleHTTPTunnelError(message_xmlhttprequest);
195 // Otherwise, continue the send loop
197 sendPendingMessages();
202 message_xmlhttprequest.send(outputMessageBuffer);
203 outputMessageBuffer = ""; // Clear buffer
207 sendingMessages = false;
211 function getHTTPTunnelErrorMessage(xmlhttprequest) {
213 var status = xmlhttprequest.status;
216 if (status == 0) return "Disconnected";
217 if (status == 200) return "Success";
218 if (status == 403) return "Unauthorized";
219 if (status == 404) return "Connection closed"; /* While it may be more
220 * accurate to say the
221 * connection does not
222 * exist, it is confusing
225 * In general, this error
226 * will only happen when
227 * the tunnel does not
228 * exist, which happens
229 * after the connection
231 * tunnel is detached.
233 // Internal server errors
234 if (status >= 500 && status <= 599) return "Server error";
236 // Otherwise, unknown
237 return "Unknown error";
241 function handleHTTPTunnelError(xmlhttprequest) {
244 var message = getHTTPTunnelErrorMessage(xmlhttprequest);
246 // Call error handler
247 if (tunnel.onerror) tunnel.onerror(message);
255 function handleResponse(xmlhttprequest) {
258 var nextRequest = null;
260 var dataUpdateEvents = 0;
262 // The location of the last element's terminator
265 // Where to start the next length search or the next element
269 var elements = new Array();
271 function parseResponse() {
273 // Do not handle responses if not connected
274 if (currentState != STATE_CONNECTED) {
276 // Clean up interval if polling
277 if (interval != null)
278 clearInterval(interval);
283 // Do not parse response yet if not ready
284 if (xmlhttprequest.readyState < 2) return;
286 // Attempt to read status
288 try { status = xmlhttprequest.status; }
290 // If status could not be read, assume successful.
291 catch (e) { status = 200; }
293 // Start next request as soon as possible IF request was successful
294 if (nextRequest == null && status == 200)
295 nextRequest = makeRequest();
297 // Parse stream when data is received and when complete.
298 if (xmlhttprequest.readyState == 3 ||
299 xmlhttprequest.readyState == 4) {
301 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
302 if (pollingMode == POLLING_ENABLED) {
303 if (xmlhttprequest.readyState == 3 && interval == null)
304 interval = setInterval(parseResponse, 30);
305 else if (xmlhttprequest.readyState == 4 && interval != null)
306 clearInterval(interval);
309 // If canceled, stop transfer
310 if (xmlhttprequest.status == 0) {
315 // Halt on error during request
316 else if (xmlhttprequest.status != 200) {
317 handleHTTPTunnelError(xmlhttprequest);
321 // Attempt to read in-progress data
323 try { current = xmlhttprequest.responseText; }
325 // Do not attempt to parse if data could not be read
326 catch (e) { return; }
328 // While search is within currently received data
329 while (elementEnd < current.length) {
331 // If we are waiting for element data
332 if (elementEnd >= startIndex) {
334 // We now have enough data for the element. Parse.
335 var element = current.substring(startIndex, elementEnd);
336 var terminator = current.substring(elementEnd, elementEnd+1);
338 // Add element to array
339 elements.push(element);
341 // If last element, handle instruction
342 if (terminator == ";") {
345 var opcode = elements.shift();
347 // Call instruction handler.
348 if (tunnel.oninstruction != null)
349 tunnel.oninstruction(opcode, elements);
356 // Start searching for length at character after
357 // element terminator
358 startIndex = elementEnd + 1;
362 // Search for end of length
363 var lengthEnd = current.indexOf(".", startIndex);
364 if (lengthEnd != -1) {
367 var length = parseInt(current.substring(elementEnd+1, lengthEnd));
369 // If we're done parsing, handle the next response.
372 // Clean up interval if polling
373 if (interval != null)
374 clearInterval(interval);
377 xmlhttprequest.onreadystatechange = null;
378 xmlhttprequest.abort();
380 // Start handling next request
382 handleResponse(nextRequest);
389 // Calculate start of element
390 startIndex = lengthEnd + 1;
392 // Calculate location of element terminator
393 elementEnd = startIndex + length;
397 // If no period yet, continue search when more data
400 startIndex = current.length;
410 // If response polling enabled, attempt to detect if still
411 // necessary (via wrapping parseResponse())
412 if (pollingMode == POLLING_ENABLED) {
413 xmlhttprequest.onreadystatechange = function() {
415 // If we receive two or more readyState==3 events,
416 // there is no need to poll.
417 if (xmlhttprequest.readyState == 3) {
419 if (dataUpdateEvents >= 2) {
420 pollingMode = POLLING_DISABLED;
421 xmlhttprequest.onreadystatechange = parseResponse;
429 // Otherwise, just parse
431 xmlhttprequest.onreadystatechange = parseResponse;
438 function makeRequest() {
441 var xmlhttprequest = new XMLHttpRequest();
442 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
443 xmlhttprequest.send(null);
445 return xmlhttprequest;
449 this.connect = function(data) {
451 // Start tunnel and connect synchronously
452 var connect_xmlhttprequest = new XMLHttpRequest();
453 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
454 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
455 connect_xmlhttprequest.send(data);
457 // If failure, throw error
458 if (connect_xmlhttprequest.status != 200) {
459 var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest);
460 throw new Error(message);
463 // Get UUID from response
464 tunnel_uuid = connect_xmlhttprequest.responseText;
466 // Start reading data
467 currentState = STATE_CONNECTED;
468 handleResponse(makeRequest());
472 this.disconnect = function() {
473 currentState = STATE_DISCONNECTED;
478 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
482 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
485 * @augments Guacamole.Tunnel
486 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
488 Guacamole.WebSocketTunnel = function(tunnelURL) {
491 * Reference to this WebSocket tunnel.
497 * The WebSocket used by this tunnel.
503 * The WebSocket protocol corresponding to the protocol used for the current
513 1000: "Connection closed normally.",
514 1001: "Connection shut down.",
515 1002: "Protocol error.",
516 1003: "Invalid data.",
517 1004: "[UNKNOWN, RESERVED]",
518 1005: "No status code present.",
519 1006: "Connection closed abnormally.",
520 1007: "Inconsistent data type.",
521 1008: "Policy violation.",
522 1009: "Message too large.",
523 1010: "Extension negotiation failed."
527 var STATE_CONNECTED = 1;
528 var STATE_DISCONNECTED = 2;
530 var currentState = STATE_IDLE;
532 // Transform current URL to WebSocket URL
534 // If not already a websocket URL
535 if ( tunnelURL.substring(0, 3) != "ws:"
536 && tunnelURL.substring(0, 4) != "wss:") {
538 var protocol = ws_protocol[window.location.protocol];
540 // If absolute URL, convert to absolute WS URL
541 if (tunnelURL.substring(0, 1) == "/")
544 + "//" + window.location.host
547 // Otherwise, construct absolute from relative URL
550 // Get path from pathname
551 var slash = window.location.pathname.lastIndexOf("/");
552 var path = window.location.pathname.substring(0, slash + 1);
554 // Construct absolute URL
557 + "//" + window.location.host
565 this.sendMessage = function(elements) {
567 // Do not attempt to send messages if not connected
568 if (currentState != STATE_CONNECTED)
571 // Do not attempt to send empty messages
572 if (arguments.length == 0)
576 * Converts the given value to a length/string pair for use as an
577 * element in a Guacamole instruction.
580 * @param value The value to convert.
581 * @return {String} The converted value.
583 function getElement(value) {
584 var string = new String(value);
585 return string.length + "." + string;
588 // Initialized message with first element
589 var message = getElement(arguments[0]);
591 // Append remaining elements
592 for (var i=1; i<arguments.length; i++)
593 message += "," + getElement(arguments[i]);
598 socket.send(message);
602 this.connect = function(data) {
605 socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
607 socket.onopen = function(event) {
608 currentState = STATE_CONNECTED;
611 socket.onclose = function(event) {
613 // If connection closed abnormally, signal error.
614 if (event.code != 1000 && tunnel.onerror)
615 tunnel.onerror(status_code[event.code]);
619 socket.onerror = function(event) {
621 // Call error handler
622 if (tunnel.onerror) tunnel.onerror(event.data);
626 socket.onmessage = function(event) {
628 var message = event.data;
636 // Search for end of length
637 var lengthEnd = message.indexOf(".", startIndex);
638 if (lengthEnd != -1) {
641 var length = parseInt(message.substring(elementEnd+1, lengthEnd));
643 // Calculate start of element
644 startIndex = lengthEnd + 1;
646 // Calculate location of element terminator
647 elementEnd = startIndex + length;
651 // If no period, incomplete instruction.
653 throw new Error("Incomplete instruction.");
655 // We now have enough data for the element. Parse.
656 var element = message.substring(startIndex, elementEnd);
657 var terminator = message.substring(elementEnd, elementEnd+1);
659 // Add element to array
660 elements.push(element);
662 // If last element, handle instruction
663 if (terminator == ";") {
666 var opcode = elements.shift();
668 // Call instruction handler.
669 if (tunnel.oninstruction != null)
670 tunnel.oninstruction(opcode, elements);
677 // Start searching for length at character after
678 // element terminator
679 startIndex = elementEnd + 1;
681 } while (startIndex < message.length);
687 this.disconnect = function() {
688 currentState = STATE_DISCONNECTED;
694 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
698 * Guacamole Tunnel which cycles between all specified tunnels until
699 * no tunnels are left. Another tunnel is used if an error occurs but
700 * no instructions have been received. If an instruction has been
701 * received, or no tunnels remain, the error is passed directly out
702 * through the onerror handler (if defined).
705 * @augments Guacamole.Tunnel
706 * @param {...} tunnel_chain The tunnels to use, in order of priority.
708 Guacamole.ChainedTunnel = function(tunnel_chain) {
711 * Reference to this chained tunnel.
714 var chained_tunnel = this;
717 * The currently wrapped tunnel, if any.
720 var current_tunnel = null;
723 * Data passed in via connect(), to be used for
724 * wrapped calls to other tunnels' connect() functions.
730 * Array of all tunnels passed to this ChainedTunnel through the
731 * constructor arguments.
736 // Load all tunnels into array
737 for (var i=0; i<arguments.length; i++)
738 tunnels.push(arguments[i]);
741 * Sets the current tunnel.
744 * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel.
746 function attach(tunnel) {
748 // Clear handlers of current tunnel, if any
749 if (current_tunnel) {
750 current_tunnel.onerror = null;
751 current_tunnel.oninstruction = null;
754 // Set own functions to tunnel's functions
755 chained_tunnel.disconnect = tunnel.disconnect;
756 chained_tunnel.sendMessage = tunnel.sendMessage;
758 // Record current tunnel
759 current_tunnel = tunnel;
761 // Wrap own oninstruction within current tunnel
762 current_tunnel.oninstruction = function(opcode, elements) {
765 chained_tunnel.oninstruction(opcode, elements);
767 // Use handler permanently from now on
768 current_tunnel.oninstruction = chained_tunnel.oninstruction;
770 // Pass through errors (without trying other tunnels)
771 current_tunnel.onerror = chained_tunnel.onerror;
775 // Attach next tunnel on error
776 current_tunnel.onerror = function(message) {
779 var next_tunnel = tunnels.shift();
781 // If there IS a next tunnel, try using it.
785 // Otherwise, call error handler
786 else if (chained_tunnel.onerror)
787 chained_tunnel.onerror(message);
793 // Attempt connection
794 current_tunnel.connect(connect_data);
799 // Call error handler of current tunnel on error
800 current_tunnel.onerror(e.message);
807 this.connect = function(data) {
809 // Remember connect data
813 var next_tunnel = tunnels.shift();
815 // Attach first tunnel
819 // If there IS no first tunnel, error
820 else if (chained_tunnel.onerror)
821 chained_tunnel.onerror("No tunnels to try.");
827 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();