1 /*
  2  * Copyright (C) 2013 Glyptodon LLC
  3  *
  4  * Permission is hereby granted, free of charge, to any person obtaining a copy
  5  * of this software and associated documentation files (the "Software"), to deal
  6  * in the Software without restriction, including without limitation the rights
  7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8  * copies of the Software, and to permit persons to whom the Software is
  9  * furnished to do so, subject to the following conditions:
 10  *
 11  * The above copyright notice and this permission notice shall be included in
 12  * all copies or substantial portions of the Software.
 13  *
 14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20  * THE SOFTWARE.
 21  */
 22 
 23 var Guacamole = Guacamole || {};
 24 
 25 /**
 26  * Core object providing abstract communication for Guacamole. This object
 27  * is a null implementation whose functions do nothing. Guacamole applications
 28  * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
 29  * on this one.
 30  * 
 31  * @constructor
 32  * @see Guacamole.HTTPTunnel
 33  */
 34 Guacamole.Tunnel = function() {
 35 
 36     /**
 37      * Connect to the tunnel with the given optional data. This data is
 38      * typically used for authentication. The format of data accepted is
 39      * up to the tunnel implementation.
 40      * 
 41      * @param {String} data The data to send to the tunnel when connecting.
 42      */
 43     this.connect = function(data) {};
 44     
 45     /**
 46      * Disconnect from the tunnel.
 47      */
 48     this.disconnect = function() {};
 49     
 50     /**
 51      * Send the given message through the tunnel to the service on the other
 52      * side. All messages are guaranteed to be received in the order sent.
 53      * 
 54      * @param {...} elements The elements of the message to send to the
 55      *                       service on the other side of the tunnel.
 56      */
 57     this.sendMessage = function(elements) {};
 58 
 59     /**
 60      * The current state of this tunnel.
 61      * 
 62      * @type Number
 63      */
 64     this.state = Guacamole.Tunnel.State.CONNECTING;
 65 
 66     /**
 67      * The maximum amount of time to wait for data to be received, in
 68      * milliseconds. If data is not received within this amount of time,
 69      * the tunnel is closed with an error. The default value is 15000.
 70      * 
 71      * @type Number
 72      */
 73     this.receiveTimeout = 15000;
 74 
 75     /**
 76      * Fired whenever an error is encountered by the tunnel.
 77      * 
 78      * @event
 79      * @param {Guacamole.Status} status A status object which describes the
 80      *                                  error.
 81      */
 82     this.onerror = null;
 83 
 84     /**
 85      * Fired whenever the state of the tunnel changes.
 86      * 
 87      * @event
 88      * @param {Number} state The new state of the client.
 89      */
 90     this.onstatechange = null;
 91 
 92     /**
 93      * Fired once for every complete Guacamole instruction received, in order.
 94      * 
 95      * @event
 96      * @param {String} opcode The Guacamole instruction opcode.
 97      * @param {Array} parameters The parameters provided for the instruction,
 98      *                           if any.
 99      */
100     this.oninstruction = null;
101 
102 };
103 
104 /**
105  * All possible tunnel states.
106  */
107 Guacamole.Tunnel.State = {
108 
109     /**
110      * A connection is in pending. It is not yet known whether connection was
111      * successful.
112      * 
113      * @type Number
114      */
115     "CONNECTING": 0,
116 
117     /**
118      * Connection was successful, and data is being received.
119      * 
120      * @type Number
121      */
122     "OPEN": 1,
123 
124     /**
125      * The connection is closed. Connection may not have been successful, the
126      * tunnel may have been explicitly closed by either side, or an error may
127      * have occurred.
128      * 
129      * @type Number
130      */
131     "CLOSED": 2
132 
133 };
134 
135 /**
136  * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
137  * 
138  * @constructor
139  * @augments Guacamole.Tunnel
140  *
141  * @param {String} tunnelURL
142  *     The URL of the HTTP tunneling service.
143  *
144  * @param {Boolean} [crossDomain=false]
145  *     Whether tunnel requests will be cross-domain, and thus must use CORS
146  *     mechanisms and headers. By default, it is assumed that tunnel requests
147  *     will be made to the same domain.
148  */
149 Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) {
150 
151     /**
152      * Reference to this HTTP tunnel.
153      * @private
154      */
155     var tunnel = this;
156 
157     var tunnel_uuid;
158 
159     var TUNNEL_CONNECT = tunnelURL + "?connect";
160     var TUNNEL_READ    = tunnelURL + "?read:";
161     var TUNNEL_WRITE   = tunnelURL + "?write:";
162 
163     var POLLING_ENABLED     = 1;
164     var POLLING_DISABLED    = 0;
165 
166     // Default to polling - will be turned off automatically if not needed
167     var pollingMode = POLLING_ENABLED;
168 
169     var sendingMessages = false;
170     var outputMessageBuffer = "";
171 
172     // If requests are expected to be cross-domain, the cookie that the HTTP
173     // tunnel depends on will only be sent if withCredentials is true
174     var withCredentials = !!crossDomain;
175 
176     /**
177      * The current receive timeout ID, if any.
178      * @private
179      */
180     var receive_timeout = null;
181 
182     /**
183      * Initiates a timeout which, if data is not received, causes the tunnel
184      * to close with an error.
185      * 
186      * @private
187      */
188     function reset_timeout() {
189 
190         // Get rid of old timeout (if any)
191         window.clearTimeout(receive_timeout);
192 
193         // Set new timeout
194         receive_timeout = window.setTimeout(function () {
195             close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
196         }, tunnel.receiveTimeout);
197 
198     }
199 
200     /**
201      * Closes this tunnel, signaling the given status and corresponding
202      * message, which will be sent to the onerror handler if the status is
203      * an error status.
204      * 
205      * @private
206      * @param {Guacamole.Status} status The status causing the connection to
207      *                                  close;
208      */
209     function close_tunnel(status) {
210 
211         // Ignore if already closed
212         if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
213             return;
214 
215         // If connection closed abnormally, signal error.
216         if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {
217 
218             // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
219             // only signals end-of-stream for the HTTP tunnel.
220             if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
221                     || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
222                 tunnel.onerror(status);
223 
224         }
225 
226         // Mark as closed
227         tunnel.state = Guacamole.Tunnel.State.CLOSED;
228 
229         // Reset output message buffer
230         sendingMessages = false;
231 
232         if (tunnel.onstatechange)
233             tunnel.onstatechange(tunnel.state);
234 
235     }
236 
237 
238     this.sendMessage = function() {
239 
240         // Do not attempt to send messages if not connected
241         if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
242             return;
243 
244         // Do not attempt to send empty messages
245         if (arguments.length === 0)
246             return;
247 
248         /**
249          * Converts the given value to a length/string pair for use as an
250          * element in a Guacamole instruction.
251          * 
252          * @private
253          * @param value The value to convert.
254          * @return {String} The converted value. 
255          */
256         function getElement(value) {
257             var string = new String(value);
258             return string.length + "." + string; 
259         }
260 
261         // Initialized message with first element
262         var message = getElement(arguments[0]);
263 
264         // Append remaining elements
265         for (var i=1; i<arguments.length; i++)
266             message += "," + getElement(arguments[i]);
267 
268         // Final terminator
269         message += ";";
270 
271         // Add message to buffer
272         outputMessageBuffer += message;
273 
274         // Send if not currently sending
275         if (!sendingMessages)
276             sendPendingMessages();
277 
278     };
279 
280     function sendPendingMessages() {
281 
282         // Do not attempt to send messages if not connected
283         if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
284             return;
285 
286         if (outputMessageBuffer.length > 0) {
287 
288             sendingMessages = true;
289 
290             var message_xmlhttprequest = new XMLHttpRequest();
291             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
292             message_xmlhttprequest.withCredentials = withCredentials;
293             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
294 
295             // Once response received, send next queued event.
296             message_xmlhttprequest.onreadystatechange = function() {
297                 if (message_xmlhttprequest.readyState === 4) {
298 
299                     // If an error occurs during send, handle it
300                     if (message_xmlhttprequest.status !== 200)
301                         handleHTTPTunnelError(message_xmlhttprequest);
302 
303                     // Otherwise, continue the send loop
304                     else
305                         sendPendingMessages();
306 
307                 }
308             };
309 
310             message_xmlhttprequest.send(outputMessageBuffer);
311             outputMessageBuffer = ""; // Clear buffer
312 
313         }
314         else
315             sendingMessages = false;
316 
317     }
318 
319     function handleHTTPTunnelError(xmlhttprequest) {
320 
321         var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
322         var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
323 
324         close_tunnel(new Guacamole.Status(code, message));
325 
326     }
327 
328     function handleResponse(xmlhttprequest) {
329 
330         var interval = null;
331         var nextRequest = null;
332 
333         var dataUpdateEvents = 0;
334 
335         // The location of the last element's terminator
336         var elementEnd = -1;
337 
338         // Where to start the next length search or the next element
339         var startIndex = 0;
340 
341         // Parsed elements
342         var elements = new Array();
343 
344         function parseResponse() {
345 
346             // Do not handle responses if not connected
347             if (tunnel.state !== Guacamole.Tunnel.State.OPEN) {
348                 
349                 // Clean up interval if polling
350                 if (interval !== null)
351                     clearInterval(interval);
352                 
353                 return;
354             }
355 
356             // Do not parse response yet if not ready
357             if (xmlhttprequest.readyState < 2) return;
358 
359             // Attempt to read status
360             var status;
361             try { status = xmlhttprequest.status; }
362 
363             // If status could not be read, assume successful.
364             catch (e) { status = 200; }
365 
366             // Start next request as soon as possible IF request was successful
367             if (!nextRequest && status === 200)
368                 nextRequest = makeRequest();
369 
370             // Parse stream when data is received and when complete.
371             if (xmlhttprequest.readyState === 3 ||
372                 xmlhttprequest.readyState === 4) {
373 
374                 reset_timeout();
375 
376                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
377                 if (pollingMode === POLLING_ENABLED) {
378                     if (xmlhttprequest.readyState === 3 && !interval)
379                         interval = setInterval(parseResponse, 30);
380                     else if (xmlhttprequest.readyState === 4 && !interval)
381                         clearInterval(interval);
382                 }
383 
384                 // If canceled, stop transfer
385                 if (xmlhttprequest.status === 0) {
386                     tunnel.disconnect();
387                     return;
388                 }
389 
390                 // Halt on error during request
391                 else if (xmlhttprequest.status !== 200) {
392                     handleHTTPTunnelError(xmlhttprequest);
393                     return;
394                 }
395 
396                 // Attempt to read in-progress data
397                 var current;
398                 try { current = xmlhttprequest.responseText; }
399 
400                 // Do not attempt to parse if data could not be read
401                 catch (e) { return; }
402 
403                 // While search is within currently received data
404                 while (elementEnd < current.length) {
405 
406                     // If we are waiting for element data
407                     if (elementEnd >= startIndex) {
408 
409                         // We now have enough data for the element. Parse.
410                         var element = current.substring(startIndex, elementEnd);
411                         var terminator = current.substring(elementEnd, elementEnd+1);
412 
413                         // Add element to array
414                         elements.push(element);
415 
416                         // If last element, handle instruction
417                         if (terminator === ";") {
418 
419                             // Get opcode
420                             var opcode = elements.shift();
421 
422                             // Call instruction handler.
423                             if (tunnel.oninstruction)
424                                 tunnel.oninstruction(opcode, elements);
425 
426                             // Clear elements
427                             elements.length = 0;
428 
429                         }
430 
431                         // Start searching for length at character after
432                         // element terminator
433                         startIndex = elementEnd + 1;
434 
435                     }
436 
437                     // Search for end of length
438                     var lengthEnd = current.indexOf(".", startIndex);
439                     if (lengthEnd !== -1) {
440 
441                         // Parse length
442                         var length = parseInt(current.substring(elementEnd+1, lengthEnd));
443 
444                         // If we're done parsing, handle the next response.
445                         if (length === 0) {
446 
447                             // Clean up interval if polling
448                             if (!interval)
449                                 clearInterval(interval);
450                            
451                             // Clean up object
452                             xmlhttprequest.onreadystatechange = null;
453                             xmlhttprequest.abort();
454 
455                             // Start handling next request
456                             if (nextRequest)
457                                 handleResponse(nextRequest);
458 
459                             // Done parsing
460                             break;
461 
462                         }
463 
464                         // Calculate start of element
465                         startIndex = lengthEnd + 1;
466 
467                         // Calculate location of element terminator
468                         elementEnd = startIndex + length;
469 
470                     }
471                     
472                     // If no period yet, continue search when more data
473                     // is received
474                     else {
475                         startIndex = current.length;
476                         break;
477                     }
478 
479                 } // end parse loop
480 
481             }
482 
483         }
484 
485         // If response polling enabled, attempt to detect if still
486         // necessary (via wrapping parseResponse())
487         if (pollingMode === POLLING_ENABLED) {
488             xmlhttprequest.onreadystatechange = function() {
489 
490                 // If we receive two or more readyState==3 events,
491                 // there is no need to poll.
492                 if (xmlhttprequest.readyState === 3) {
493                     dataUpdateEvents++;
494                     if (dataUpdateEvents >= 2) {
495                         pollingMode = POLLING_DISABLED;
496                         xmlhttprequest.onreadystatechange = parseResponse;
497                     }
498                 }
499 
500                 parseResponse();
501             };
502         }
503 
504         // Otherwise, just parse
505         else
506             xmlhttprequest.onreadystatechange = parseResponse;
507 
508         parseResponse();
509 
510     }
511 
512     /**
513      * Arbitrary integer, unique for each tunnel read request.
514      * @private
515      */
516     var request_id = 0;
517 
518     function makeRequest() {
519 
520         // Make request, increment request ID
521         var xmlhttprequest = new XMLHttpRequest();
522         xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++));
523         xmlhttprequest.withCredentials = withCredentials;
524         xmlhttprequest.send(null);
525 
526         return xmlhttprequest;
527 
528     }
529 
530     this.connect = function(data) {
531 
532         // Start waiting for connect
533         reset_timeout();
534 
535         // Start tunnel and connect
536         var connect_xmlhttprequest = new XMLHttpRequest();
537         connect_xmlhttprequest.onreadystatechange = function() {
538 
539             if (connect_xmlhttprequest.readyState !== 4)
540                 return;
541 
542             // If failure, throw error
543             if (connect_xmlhttprequest.status !== 200) {
544                 handleHTTPTunnelError(connect_xmlhttprequest);
545                 return;
546             }
547 
548             reset_timeout();
549 
550             // Get UUID from response
551             tunnel_uuid = connect_xmlhttprequest.responseText;
552 
553             tunnel.state = Guacamole.Tunnel.State.OPEN;
554             if (tunnel.onstatechange)
555                 tunnel.onstatechange(tunnel.state);
556 
557             // Start reading data
558             handleResponse(makeRequest());
559 
560         };
561 
562         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
563         connect_xmlhttprequest.withCredentials = withCredentials;
564         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
565         connect_xmlhttprequest.send(data);
566 
567     };
568 
569     this.disconnect = function() {
570         close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
571     };
572 
573 };
574 
575 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
576 
577 /**
578  * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
579  * 
580  * @constructor
581  * @augments Guacamole.Tunnel
582  * @param {String} tunnelURL The URL of the WebSocket tunneling service.
583  */
584 Guacamole.WebSocketTunnel = function(tunnelURL) {
585 
586     /**
587      * Reference to this WebSocket tunnel.
588      * @private
589      */
590     var tunnel = this;
591 
592     /**
593      * The WebSocket used by this tunnel.
594      * @private
595      */
596     var socket = null;
597 
598     /**
599      * The current receive timeout ID, if any.
600      * @private
601      */
602     var receive_timeout = null;
603 
604     /**
605      * The WebSocket protocol corresponding to the protocol used for the current
606      * location.
607      * @private
608      */
609     var ws_protocol = {
610         "http:":  "ws:",
611         "https:": "wss:"
612     };
613 
614     // Transform current URL to WebSocket URL
615 
616     // If not already a websocket URL
617     if (   tunnelURL.substring(0, 3) !== "ws:"
618         && tunnelURL.substring(0, 4) !== "wss:") {
619 
620         var protocol = ws_protocol[window.location.protocol];
621 
622         // If absolute URL, convert to absolute WS URL
623         if (tunnelURL.substring(0, 1) === "/")
624             tunnelURL =
625                 protocol
626                 + "//" + window.location.host
627                 + tunnelURL;
628 
629         // Otherwise, construct absolute from relative URL
630         else {
631 
632             // Get path from pathname
633             var slash = window.location.pathname.lastIndexOf("/");
634             var path  = window.location.pathname.substring(0, slash + 1);
635 
636             // Construct absolute URL
637             tunnelURL =
638                 protocol
639                 + "//" + window.location.host
640                 + path
641                 + tunnelURL;
642 
643         }
644 
645     }
646 
647     /**
648      * Initiates a timeout which, if data is not received, causes the tunnel
649      * to close with an error.
650      * 
651      * @private
652      */
653     function reset_timeout() {
654 
655         // Get rid of old timeout (if any)
656         window.clearTimeout(receive_timeout);
657 
658         // Set new timeout
659         receive_timeout = window.setTimeout(function () {
660             close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
661         }, tunnel.receiveTimeout);
662 
663     }
664 
665     /**
666      * Closes this tunnel, signaling the given status and corresponding
667      * message, which will be sent to the onerror handler if the status is
668      * an error status.
669      * 
670      * @private
671      * @param {Guacamole.Status} status The status causing the connection to
672      *                                  close;
673      */
674     function close_tunnel(status) {
675 
676         // Ignore if already closed
677         if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
678             return;
679 
680         // If connection closed abnormally, signal error.
681         if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
682             tunnel.onerror(status);
683 
684         // Mark as closed
685         tunnel.state = Guacamole.Tunnel.State.CLOSED;
686         if (tunnel.onstatechange)
687             tunnel.onstatechange(tunnel.state);
688 
689         socket.close();
690 
691     }
692 
693     this.sendMessage = function(elements) {
694 
695         // Do not attempt to send messages if not connected
696         if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
697             return;
698 
699         // Do not attempt to send empty messages
700         if (arguments.length === 0)
701             return;
702 
703         /**
704          * Converts the given value to a length/string pair for use as an
705          * element in a Guacamole instruction.
706          * 
707          * @private
708          * @param value The value to convert.
709          * @return {String} The converted value. 
710          */
711         function getElement(value) {
712             var string = new String(value);
713             return string.length + "." + string; 
714         }
715 
716         // Initialized message with first element
717         var message = getElement(arguments[0]);
718 
719         // Append remaining elements
720         for (var i=1; i<arguments.length; i++)
721             message += "," + getElement(arguments[i]);
722 
723         // Final terminator
724         message += ";";
725 
726         socket.send(message);
727 
728     };
729 
730     this.connect = function(data) {
731 
732         reset_timeout();
733 
734         // Connect socket
735         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
736 
737         socket.onopen = function(event) {
738 
739             reset_timeout();
740 
741             tunnel.state = Guacamole.Tunnel.State.OPEN;
742             if (tunnel.onstatechange)
743                 tunnel.onstatechange(tunnel.state);
744 
745         };
746 
747         socket.onclose = function(event) {
748             close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
749         };
750         
751         socket.onerror = function(event) {
752             close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data));
753         };
754 
755         socket.onmessage = function(event) {
756 
757             reset_timeout();
758 
759             var message = event.data;
760             var startIndex = 0;
761             var elementEnd;
762 
763             var elements = [];
764 
765             do {
766 
767                 // Search for end of length
768                 var lengthEnd = message.indexOf(".", startIndex);
769                 if (lengthEnd !== -1) {
770 
771                     // Parse length
772                     var length = parseInt(message.substring(elementEnd+1, lengthEnd));
773 
774                     // Calculate start of element
775                     startIndex = lengthEnd + 1;
776 
777                     // Calculate location of element terminator
778                     elementEnd = startIndex + length;
779 
780                 }
781                 
782                 // If no period, incomplete instruction.
783                 else
784                     close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction."));
785 
786                 // We now have enough data for the element. Parse.
787                 var element = message.substring(startIndex, elementEnd);
788                 var terminator = message.substring(elementEnd, elementEnd+1);
789 
790                 // Add element to array
791                 elements.push(element);
792 
793                 // If last element, handle instruction
794                 if (terminator === ";") {
795 
796                     // Get opcode
797                     var opcode = elements.shift();
798 
799                     // Call instruction handler.
800                     if (tunnel.oninstruction)
801                         tunnel.oninstruction(opcode, elements);
802 
803                     // Clear elements
804                     elements.length = 0;
805 
806                 }
807 
808                 // Start searching for length at character after
809                 // element terminator
810                 startIndex = elementEnd + 1;
811 
812             } while (startIndex < message.length);
813 
814         };
815 
816     };
817 
818     this.disconnect = function() {
819         close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
820     };
821 
822 };
823 
824 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
825 
826 /**
827  * Guacamole Tunnel which cycles between all specified tunnels until
828  * no tunnels are left. Another tunnel is used if an error occurs but
829  * no instructions have been received. If an instruction has been
830  * received, or no tunnels remain, the error is passed directly out
831  * through the onerror handler (if defined).
832  * 
833  * @constructor
834  * @augments Guacamole.Tunnel
835  * @param {...} tunnel_chain The tunnels to use, in order of priority.
836  */
837 Guacamole.ChainedTunnel = function(tunnel_chain) {
838 
839     /**
840      * Reference to this chained tunnel.
841      * @private
842      */
843     var chained_tunnel = this;
844 
845     /**
846      * Data passed in via connect(), to be used for
847      * wrapped calls to other tunnels' connect() functions.
848      * @private
849      */
850     var connect_data;
851 
852     /**
853      * Array of all tunnels passed to this ChainedTunnel through the
854      * constructor arguments.
855      * @private
856      */
857     var tunnels = [];
858 
859     /**
860      * The tunnel committed via commit_tunnel(), if any, or null if no tunnel
861      * has yet been committed.
862      *
863      * @private
864      * @type Guacamole.Tunnel
865      */
866     var committedTunnel = null;
867 
868     // Load all tunnels into array
869     for (var i=0; i<arguments.length; i++)
870         tunnels.push(arguments[i]);
871 
872     /**
873      * Sets the current tunnel.
874      * 
875      * @private
876      * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel.
877      */
878     function attach(tunnel) {
879 
880         // Set own functions to tunnel's functions
881         chained_tunnel.disconnect  = tunnel.disconnect;
882         chained_tunnel.sendMessage = tunnel.sendMessage;
883 
884         /**
885          * Fails the currently-attached tunnel, attaching a new tunnel if
886          * possible.
887          * 
888          * @private
889          * @return {Guacamole.Tunnel} The next tunnel, or null if there are no
890          *                            more tunnels to try.
891          */
892         function fail_tunnel() {
893 
894             // Get next tunnel
895             var next_tunnel = tunnels.shift();
896 
897             // If there IS a next tunnel, try using it.
898             if (next_tunnel) {
899                 tunnel.onerror = null;
900                 tunnel.oninstruction = null;
901                 tunnel.onstatechange = null;
902                 attach(next_tunnel);
903             }
904 
905             return next_tunnel;
906 
907         }
908 
909         /**
910          * Use the current tunnel from this point forward. Do not try any more
911          * tunnels, even if the current tunnel fails.
912          * 
913          * @private
914          */
915         function commit_tunnel() {
916             tunnel.onstatechange = chained_tunnel.onstatechange;
917             tunnel.oninstruction = chained_tunnel.oninstruction;
918             tunnel.onerror = chained_tunnel.onerror;
919             committedTunnel = tunnel;
920         }
921 
922         // Wrap own onstatechange within current tunnel
923         tunnel.onstatechange = function(state) {
924 
925             switch (state) {
926 
927                 // If open, use this tunnel from this point forward.
928                 case Guacamole.Tunnel.State.OPEN:
929                     commit_tunnel();
930                     if (chained_tunnel.onstatechange)
931                         chained_tunnel.onstatechange(state);
932                     break;
933 
934                 // If closed, mark failure, attempt next tunnel
935                 case Guacamole.Tunnel.State.CLOSED:
936                     if (!fail_tunnel() && chained_tunnel.onstatechange)
937                         chained_tunnel.onstatechange(state);
938                     break;
939                 
940             }
941 
942         };
943 
944         // Wrap own oninstruction within current tunnel
945         tunnel.oninstruction = function(opcode, elements) {
946 
947             // Accept current tunnel
948             commit_tunnel();
949 
950             // Invoke handler
951             if (chained_tunnel.oninstruction)
952                 chained_tunnel.oninstruction(opcode, elements);
953 
954         };
955 
956         // Attach next tunnel on error
957         tunnel.onerror = function(status) {
958 
959             // Mark failure, attempt next tunnel
960             if (!fail_tunnel() && chained_tunnel.onerror)
961                 chained_tunnel.onerror(status);
962 
963         };
964 
965         // Attempt connection
966         tunnel.connect(connect_data);
967         
968     }
969 
970     this.connect = function(data) {
971        
972         // Remember connect data
973         connect_data = data;
974 
975         // Get committed tunnel if exists or the first tunnel on the list
976         var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift();
977 
978         // Attach first tunnel
979         if (next_tunnel)
980             attach(next_tunnel);
981 
982         // If there IS no first tunnel, error
983         else if (chained_tunnel.onerror)
984             chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");
985 
986     };
987     
988 };
989 
990 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
991