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