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  * @param {String} tunnelURL The URL of the HTTP tunneling service.
141  */
142 Guacamole.HTTPTunnel = function(tunnelURL) {
143 
144     /**
145      * Reference to this HTTP tunnel.
146      * @private
147      */
148     var tunnel = this;
149 
150     var tunnel_uuid;
151 
152     var TUNNEL_CONNECT = tunnelURL + "?connect";
153     var TUNNEL_READ    = tunnelURL + "?read:";
154     var TUNNEL_WRITE   = tunnelURL + "?write:";
155 
156     var POLLING_ENABLED     = 1;
157     var POLLING_DISABLED    = 0;
158 
159     // Default to polling - will be turned off automatically if not needed
160     var pollingMode = POLLING_ENABLED;
161 
162     var sendingMessages = false;
163     var outputMessageBuffer = "";
164 
165     /**
166      * The current receive timeout ID, if any.
167      * @private
168      */
169     var receive_timeout = null;
170 
171     /**
172      * Initiates a timeout which, if data is not received, causes the tunnel
173      * to close with an error.
174      * 
175      * @private
176      */
177     function reset_timeout() {
178 
179         // Get rid of old timeout (if any)
180         window.clearTimeout(receive_timeout);
181 
182         // Set new timeout
183         receive_timeout = window.setTimeout(function () {
184             close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
185         }, tunnel.receiveTimeout);
186 
187     }
188 
189     /**
190      * Closes this tunnel, signaling the given status and corresponding
191      * message, which will be sent to the onerror handler if the status is
192      * an error status.
193      * 
194      * @private
195      * @param {Guacamole.Status} status The status causing the connection to
196      *                                  close;
197      */
198     function close_tunnel(status) {
199 
200         // Ignore if already closed
201         if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
202             return;
203 
204         // If connection closed abnormally, signal error.
205         if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {
206 
207             // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
208             // only signals end-of-stream for the HTTP tunnel.
209             if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
210                     || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
211                 tunnel.onerror(status);
212 
213         }
214 
215         // Mark as closed
216         tunnel.state = Guacamole.Tunnel.State.CLOSED;
217 
218         // Reset output message buffer
219         sendingMessages = false;
220 
221         if (tunnel.onstatechange)
222             tunnel.onstatechange(tunnel.state);
223 
224     }
225 
226 
227     this.sendMessage = function() {
228 
229         // Do not attempt to send messages if not connected
230         if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
231             return;
232 
233         // Do not attempt to send empty messages
234         if (arguments.length === 0)
235             return;
236 
237         /**
238          * Converts the given value to a length/string pair for use as an
239          * element in a Guacamole instruction.
240          * 
241          * @private
242          * @param value The value to convert.
243          * @return {String} The converted value. 
244          */
245         function getElement(value) {
246             var string = new String(value);
247             return string.length + "." + string; 
248         }
249 
250         // Initialized message with first element
251         var message = getElement(arguments[0]);
252 
253         // Append remaining elements
254         for (var i=1; i<arguments.length; i++)
255             message += "," + getElement(arguments[i]);
256 
257         // Final terminator
258         message += ";";
259 
260         // Add message to buffer
261         outputMessageBuffer += message;
262 
263         // Send if not currently sending
264         if (!sendingMessages)
265             sendPendingMessages();
266 
267     };
268 
269     function sendPendingMessages() {
270 
271         // Do not attempt to send messages if not connected
272         if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
273             return;
274 
275         if (outputMessageBuffer.length > 0) {
276 
277             sendingMessages = true;
278 
279             var message_xmlhttprequest = new XMLHttpRequest();
280             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
281             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
282 
283             // Once response received, send next queued event.
284             message_xmlhttprequest.onreadystatechange = function() {
285                 if (message_xmlhttprequest.readyState === 4) {
286 
287                     // If an error occurs during send, handle it
288                     if (message_xmlhttprequest.status !== 200)
289                         handleHTTPTunnelError(message_xmlhttprequest);
290 
291                     // Otherwise, continue the send loop
292                     else
293                         sendPendingMessages();
294 
295                 }
296             };
297 
298             message_xmlhttprequest.send(outputMessageBuffer);
299             outputMessageBuffer = ""; // Clear buffer
300 
301         }
302         else
303             sendingMessages = false;
304 
305     }
306 
307     function handleHTTPTunnelError(xmlhttprequest) {
308 
309         var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
310         var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
311 
312         close_tunnel(new Guacamole.Status(code, message));
313 
314     }
315 
316     function handleResponse(xmlhttprequest) {
317 
318         var interval = null;
319         var nextRequest = null;
320 
321         var dataUpdateEvents = 0;
322 
323         // The location of the last element's terminator
324         var elementEnd = -1;
325 
326         // Where to start the next length search or the next element
327         var startIndex = 0;
328 
329         // Parsed elements
330         var elements = new Array();
331 
332         function parseResponse() {
333 
334             // Do not handle responses if not connected
335             if (tunnel.state !== Guacamole.Tunnel.State.OPEN) {
336                 
337                 // Clean up interval if polling
338                 if (interval !== null)
339                     clearInterval(interval);
340                 
341                 return;
342             }
343 
344             // Do not parse response yet if not ready
345             if (xmlhttprequest.readyState < 2) return;
346 
347             // Attempt to read status
348             var status;
349             try { status = xmlhttprequest.status; }
350 
351             // If status could not be read, assume successful.
352             catch (e) { status = 200; }
353 
354             // Start next request as soon as possible IF request was successful
355             if (!nextRequest && status === 200)
356                 nextRequest = makeRequest();
357 
358             // Parse stream when data is received and when complete.
359             if (xmlhttprequest.readyState === 3 ||
360                 xmlhttprequest.readyState === 4) {
361 
362                 reset_timeout();
363 
364                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
365                 if (pollingMode === POLLING_ENABLED) {
366                     if (xmlhttprequest.readyState === 3 && !interval)
367                         interval = setInterval(parseResponse, 30);
368                     else if (xmlhttprequest.readyState === 4 && !interval)
369                         clearInterval(interval);
370                 }
371 
372                 // If canceled, stop transfer
373                 if (xmlhttprequest.status === 0) {
374                     tunnel.disconnect();
375                     return;
376                 }
377 
378                 // Halt on error during request
379                 else if (xmlhttprequest.status !== 200) {
380                     handleHTTPTunnelError(xmlhttprequest);
381                     return;
382                 }
383 
384                 // Attempt to read in-progress data
385                 var current;
386                 try { current = xmlhttprequest.responseText; }
387 
388                 // Do not attempt to parse if data could not be read
389                 catch (e) { return; }
390 
391                 // While search is within currently received data
392                 while (elementEnd < current.length) {
393 
394                     // If we are waiting for element data
395                     if (elementEnd >= startIndex) {
396 
397                         // We now have enough data for the element. Parse.
398                         var element = current.substring(startIndex, elementEnd);
399                         var terminator = current.substring(elementEnd, elementEnd+1);
400 
401                         // Add element to array
402                         elements.push(element);
403 
404                         // If last element, handle instruction
405                         if (terminator === ";") {
406 
407                             // Get opcode
408                             var opcode = elements.shift();
409 
410                             // Call instruction handler.
411                             if (tunnel.oninstruction)
412                                 tunnel.oninstruction(opcode, elements);
413 
414                             // Clear elements
415                             elements.length = 0;
416 
417                         }
418 
419                         // Start searching for length at character after
420                         // element terminator
421                         startIndex = elementEnd + 1;
422 
423                     }
424 
425                     // Search for end of length
426                     var lengthEnd = current.indexOf(".", startIndex);
427                     if (lengthEnd !== -1) {
428 
429                         // Parse length
430                         var length = parseInt(current.substring(elementEnd+1, lengthEnd));
431 
432                         // If we're done parsing, handle the next response.
433                         if (length === 0) {
434 
435                             // Clean up interval if polling
436                             if (!interval)
437                                 clearInterval(interval);
438                            
439                             // Clean up object
440                             xmlhttprequest.onreadystatechange = null;
441                             xmlhttprequest.abort();
442 
443                             // Start handling next request
444                             if (nextRequest)
445                                 handleResponse(nextRequest);
446 
447                             // Done parsing
448                             break;
449 
450                         }
451 
452                         // Calculate start of element
453                         startIndex = lengthEnd + 1;
454 
455                         // Calculate location of element terminator
456                         elementEnd = startIndex + length;
457 
458                     }
459                     
460                     // If no period yet, continue search when more data
461                     // is received
462                     else {
463                         startIndex = current.length;
464                         break;
465                     }
466 
467                 } // end parse loop
468 
469             }
470 
471         }
472 
473         // If response polling enabled, attempt to detect if still
474         // necessary (via wrapping parseResponse())
475         if (pollingMode === POLLING_ENABLED) {
476             xmlhttprequest.onreadystatechange = function() {
477 
478                 // If we receive two or more readyState==3 events,
479                 // there is no need to poll.
480                 if (xmlhttprequest.readyState === 3) {
481                     dataUpdateEvents++;
482                     if (dataUpdateEvents >= 2) {
483                         pollingMode = POLLING_DISABLED;
484                         xmlhttprequest.onreadystatechange = parseResponse;
485                     }
486                 }
487 
488                 parseResponse();
489             };
490         }
491 
492         // Otherwise, just parse
493         else
494             xmlhttprequest.onreadystatechange = parseResponse;
495 
496         parseResponse();
497 
498     }
499 
500     /**
501      * Arbitrary integer, unique for each tunnel read request.
502      * @private
503      */
504     var request_id = 0;
505 
506     function makeRequest() {
507 
508         // Make request, increment request ID
509         var xmlhttprequest = new XMLHttpRequest();
510         xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++));
511         xmlhttprequest.send(null);
512 
513         return xmlhttprequest;
514 
515     }
516 
517     this.connect = function(data) {
518 
519         // Start waiting for connect
520         reset_timeout();
521 
522         // Start tunnel and connect
523         var connect_xmlhttprequest = new XMLHttpRequest();
524         connect_xmlhttprequest.onreadystatechange = function() {
525 
526             if (connect_xmlhttprequest.readyState !== 4)
527                 return;
528 
529             // If failure, throw error
530             if (connect_xmlhttprequest.status !== 200) {
531                 handleHTTPTunnelError(connect_xmlhttprequest);
532                 return;
533             }
534 
535             reset_timeout();
536 
537             // Get UUID from response
538             tunnel_uuid = connect_xmlhttprequest.responseText;
539 
540             tunnel.state = Guacamole.Tunnel.State.OPEN;
541             if (tunnel.onstatechange)
542                 tunnel.onstatechange(tunnel.state);
543 
544             // Start reading data
545             handleResponse(makeRequest());
546 
547         };
548 
549         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
550         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
551         connect_xmlhttprequest.send(data);
552 
553     };
554 
555     this.disconnect = function() {
556         close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
557     };
558 
559 };
560 
561 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
562 
563 /**
564  * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
565  * 
566  * @constructor
567  * @augments Guacamole.Tunnel
568  * @param {String} tunnelURL The URL of the WebSocket tunneling service.
569  */
570 Guacamole.WebSocketTunnel = function(tunnelURL) {
571 
572     /**
573      * Reference to this WebSocket tunnel.
574      * @private
575      */
576     var tunnel = this;
577 
578     /**
579      * The WebSocket used by this tunnel.
580      * @private
581      */
582     var socket = null;
583 
584     /**
585      * The current receive timeout ID, if any.
586      * @private
587      */
588     var receive_timeout = null;
589 
590     /**
591      * The WebSocket protocol corresponding to the protocol used for the current
592      * location.
593      * @private
594      */
595     var ws_protocol = {
596         "http:":  "ws:",
597         "https:": "wss:"
598     };
599 
600     // Transform current URL to WebSocket URL
601 
602     // If not already a websocket URL
603     if (   tunnelURL.substring(0, 3) !== "ws:"
604         && tunnelURL.substring(0, 4) !== "wss:") {
605 
606         var protocol = ws_protocol[window.location.protocol];
607 
608         // If absolute URL, convert to absolute WS URL
609         if (tunnelURL.substring(0, 1) === "/")
610             tunnelURL =
611                 protocol
612                 + "//" + window.location.host
613                 + tunnelURL;
614 
615         // Otherwise, construct absolute from relative URL
616         else {
617 
618             // Get path from pathname
619             var slash = window.location.pathname.lastIndexOf("/");
620             var path  = window.location.pathname.substring(0, slash + 1);
621 
622             // Construct absolute URL
623             tunnelURL =
624                 protocol
625                 + "//" + window.location.host
626                 + path
627                 + tunnelURL;
628 
629         }
630 
631     }
632 
633     /**
634      * Initiates a timeout which, if data is not received, causes the tunnel
635      * to close with an error.
636      * 
637      * @private
638      */
639     function reset_timeout() {
640 
641         // Get rid of old timeout (if any)
642         window.clearTimeout(receive_timeout);
643 
644         // Set new timeout
645         receive_timeout = window.setTimeout(function () {
646             close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
647         }, tunnel.receiveTimeout);
648 
649     }
650 
651     /**
652      * Closes this tunnel, signaling the given status and corresponding
653      * message, which will be sent to the onerror handler if the status is
654      * an error status.
655      * 
656      * @private
657      * @param {Guacamole.Status} status The status causing the connection to
658      *                                  close;
659      */
660     function close_tunnel(status) {
661 
662         // Ignore if already closed
663         if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
664             return;
665 
666         // If connection closed abnormally, signal error.
667         if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
668             tunnel.onerror(status);
669 
670         // Mark as closed
671         tunnel.state = Guacamole.Tunnel.State.CLOSED;
672         if (tunnel.onstatechange)
673             tunnel.onstatechange(tunnel.state);
674 
675         socket.close();
676 
677     }
678 
679     this.sendMessage = function(elements) {
680 
681         // Do not attempt to send messages if not connected
682         if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
683             return;
684 
685         // Do not attempt to send empty messages
686         if (arguments.length === 0)
687             return;
688 
689         /**
690          * Converts the given value to a length/string pair for use as an
691          * element in a Guacamole instruction.
692          * 
693          * @private
694          * @param value The value to convert.
695          * @return {String} The converted value. 
696          */
697         function getElement(value) {
698             var string = new String(value);
699             return string.length + "." + string; 
700         }
701 
702         // Initialized message with first element
703         var message = getElement(arguments[0]);
704 
705         // Append remaining elements
706         for (var i=1; i<arguments.length; i++)
707             message += "," + getElement(arguments[i]);
708 
709         // Final terminator
710         message += ";";
711 
712         socket.send(message);
713 
714     };
715 
716     this.connect = function(data) {
717 
718         reset_timeout();
719 
720         // Connect socket
721         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
722 
723         socket.onopen = function(event) {
724 
725             reset_timeout();
726 
727             tunnel.state = Guacamole.Tunnel.State.OPEN;
728             if (tunnel.onstatechange)
729                 tunnel.onstatechange(tunnel.state);
730 
731         };
732 
733         socket.onclose = function(event) {
734             close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
735         };
736         
737         socket.onerror = function(event) {
738             close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data));
739         };
740 
741         socket.onmessage = function(event) {
742 
743             reset_timeout();
744 
745             var message = event.data;
746             var startIndex = 0;
747             var elementEnd;
748 
749             var elements = [];
750 
751             do {
752 
753                 // Search for end of length
754                 var lengthEnd = message.indexOf(".", startIndex);
755                 if (lengthEnd !== -1) {
756 
757                     // Parse length
758                     var length = parseInt(message.substring(elementEnd+1, lengthEnd));
759 
760                     // Calculate start of element
761                     startIndex = lengthEnd + 1;
762 
763                     // Calculate location of element terminator
764                     elementEnd = startIndex + length;
765 
766                 }
767                 
768                 // If no period, incomplete instruction.
769                 else
770                     close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction."));
771 
772                 // We now have enough data for the element. Parse.
773                 var element = message.substring(startIndex, elementEnd);
774                 var terminator = message.substring(elementEnd, elementEnd+1);
775 
776                 // Add element to array
777                 elements.push(element);
778 
779                 // If last element, handle instruction
780                 if (terminator === ";") {
781 
782                     // Get opcode
783                     var opcode = elements.shift();
784 
785                     // Call instruction handler.
786                     if (tunnel.oninstruction)
787                         tunnel.oninstruction(opcode, elements);
788 
789                     // Clear elements
790                     elements.length = 0;
791 
792                 }
793 
794                 // Start searching for length at character after
795                 // element terminator
796                 startIndex = elementEnd + 1;
797 
798             } while (startIndex < message.length);
799 
800         };
801 
802     };
803 
804     this.disconnect = function() {
805         close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
806     };
807 
808 };
809 
810 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
811 
812 /**
813  * Guacamole Tunnel which cycles between all specified tunnels until
814  * no tunnels are left. Another tunnel is used if an error occurs but
815  * no instructions have been received. If an instruction has been
816  * received, or no tunnels remain, the error is passed directly out
817  * through the onerror handler (if defined).
818  * 
819  * @constructor
820  * @augments Guacamole.Tunnel
821  * @param {...} tunnel_chain The tunnels to use, in order of priority.
822  */
823 Guacamole.ChainedTunnel = function(tunnel_chain) {
824 
825     /**
826      * Reference to this chained tunnel.
827      * @private
828      */
829     var chained_tunnel = this;
830 
831     /**
832      * Data passed in via connect(), to be used for
833      * wrapped calls to other tunnels' connect() functions.
834      * @private
835      */
836     var connect_data;
837 
838     /**
839      * Array of all tunnels passed to this ChainedTunnel through the
840      * constructor arguments.
841      * @private
842      */
843     var tunnels = [];
844 
845     // Load all tunnels into array
846     for (var i=0; i<arguments.length; i++)
847         tunnels.push(arguments[i]);
848 
849     /**
850      * Sets the current tunnel.
851      * 
852      * @private
853      * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel.
854      */
855     function attach(tunnel) {
856 
857         // Set own functions to tunnel's functions
858         chained_tunnel.disconnect  = tunnel.disconnect;
859         chained_tunnel.sendMessage = tunnel.sendMessage;
860 
861         /**
862          * Fails the currently-attached tunnel, attaching a new tunnel if
863          * possible.
864          * 
865          * @private
866          * @return {Guacamole.Tunnel} The next tunnel, or null if there are no
867          *                            more tunnels to try.
868          */
869         function fail_tunnel() {
870 
871             // Get next tunnel
872             var next_tunnel = tunnels.shift();
873 
874             // If there IS a next tunnel, try using it.
875             if (next_tunnel) {
876                 tunnel.onerror = null;
877                 tunnel.oninstruction = null;
878                 tunnel.onstatechange = null;
879                 attach(next_tunnel);
880             }
881 
882             return next_tunnel;
883 
884         }
885 
886         /**
887          * Use the current tunnel from this point forward. Do not try any more
888          * tunnels, even if the current tunnel fails.
889          * 
890          * @private
891          */
892         function commit_tunnel() {
893             tunnel.onstatechange = chained_tunnel.onstatechange;
894             tunnel.oninstruction = chained_tunnel.oninstruction;
895             tunnel.onerror = chained_tunnel.onerror;
896         }
897 
898         // Wrap own onstatechange within current tunnel
899         tunnel.onstatechange = function(state) {
900 
901             switch (state) {
902 
903                 // If open, use this tunnel from this point forward.
904                 case Guacamole.Tunnel.State.OPEN:
905                     commit_tunnel();
906                     if (chained_tunnel.onstatechange)
907                         chained_tunnel.onstatechange(state);
908                     break;
909 
910                 // If closed, mark failure, attempt next tunnel
911                 case Guacamole.Tunnel.State.CLOSED:
912                     if (!fail_tunnel() && chained_tunnel.onstatechange)
913                         chained_tunnel.onstatechange(state);
914                     break;
915                 
916             }
917 
918         };
919 
920         // Wrap own oninstruction within current tunnel
921         tunnel.oninstruction = function(opcode, elements) {
922 
923             // Accept current tunnel
924             commit_tunnel();
925 
926             // Invoke handler
927             if (chained_tunnel.oninstruction)
928                 chained_tunnel.oninstruction(opcode, elements);
929 
930         };
931 
932         // Attach next tunnel on error
933         tunnel.onerror = function(status) {
934 
935             // Mark failure, attempt next tunnel
936             if (!fail_tunnel() && chained_tunnel.onerror)
937                 chained_tunnel.onerror(status);
938 
939         };
940 
941         // Attempt connection
942         tunnel.connect(connect_data);
943         
944     }
945 
946     this.connect = function(data) {
947        
948         // Remember connect data
949         connect_data = data;
950 
951         // Get first tunnel
952         var next_tunnel = tunnels.shift();
953 
954         // Attach first tunnel
955         if (next_tunnel)
956             attach(next_tunnel);
957 
958         // If there IS no first tunnel, error
959         else if (chained_tunnel.onerror)
960             chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");
961 
962     };
963     
964 };
965 
966 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
967