In chained tunnel, clear handlers in old tunnel when new tunnel is taking over.
[guacamole-common-js.git] / src / main / resources / tunnel.js
1
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is guacamole-common-js.
16  *
17  * The Initial Developer of the Original Code is
18  * Michael Jumper.
19  * Portions created by the Initial Developer are Copyright (C) 2010
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  *
24  * Alternatively, the contents of this file may be used under the terms of
25  * either the GNU General Public License Version 2 or later (the "GPL"), or
26  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27  * in which case the provisions of the GPL or the LGPL are applicable instead
28  * of those above. If you wish to allow use of your version of this file only
29  * under the terms of either the GPL or the LGPL, and not to allow others to
30  * use your version of this file under the terms of the MPL, indicate your
31  * decision by deleting the provisions above and replace them with the notice
32  * and other provisions required by the GPL or the LGPL. If you do not delete
33  * the provisions above, a recipient may use your version of this file under
34  * the terms of any one of the MPL, the GPL or the LGPL.
35  *
36  * ***** END LICENSE BLOCK ***** */
37
38 // Guacamole namespace
39 var Guacamole = Guacamole || {};
40
41 /**
42  * Core object providing abstract communication for Guacamole. This object
43  * is a null implementation whose functions do nothing. Guacamole applications
44  * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
45  * on this one.
46  * 
47  * @constructor
48  * @see Guacamole.HTTPTunnel
49  */
50 Guacamole.Tunnel = function() {
51
52     /**
53      * Connect to the tunnel with the given optional data. This data is
54      * typically used for authentication. The format of data accepted is
55      * up to the tunnel implementation.
56      * 
57      * @param {String} data The data to send to the tunnel when connecting.
58      */
59     this.connect = function(data) {};
60     
61     /**
62      * Disconnect from the tunnel.
63      */
64     this.disconnect = function() {};
65     
66     /**
67      * Send the given message through the tunnel to the service on the other
68      * side. All messages are guaranteed to be received in the order sent.
69      * 
70      * @param {...} elements The elements of the message to send to the
71      *                       service on the other side of the tunnel.
72      */
73     this.sendMessage = function(elements) {};
74     
75     /**
76      * Fired whenever an error is encountered by the tunnel.
77      * 
78      * @event
79      * @param {String} message A human-readable description of the error that
80      *                         occurred.
81      */
82     this.onerror = null;
83
84     /**
85      * Fired once for every complete Guacamole instruction received, in order.
86      * 
87      * @event
88      * @param {String} opcode The Guacamole instruction opcode.
89      * @param {Array} parameters The parameters provided for the instruction,
90      *                           if any.
91      */
92     this.oninstruction = null;
93
94 };
95
96 /**
97  * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
98  * 
99  * @constructor
100  * @augments Guacamole.Tunnel
101  * @param {String} tunnelURL The URL of the HTTP tunneling service.
102  */
103 Guacamole.HTTPTunnel = function(tunnelURL) {
104
105     /**
106      * Reference to this HTTP tunnel.
107      */
108     var tunnel = this;
109
110     var tunnel_uuid;
111
112     var TUNNEL_CONNECT = tunnelURL + "?connect";
113     var TUNNEL_READ    = tunnelURL + "?read:";
114     var TUNNEL_WRITE   = tunnelURL + "?write:";
115
116     var STATE_IDLE          = 0;
117     var STATE_CONNECTED     = 1;
118     var STATE_DISCONNECTED  = 2;
119
120     var currentState = STATE_IDLE;
121
122     var POLLING_ENABLED     = 1;
123     var POLLING_DISABLED    = 0;
124
125     // Default to polling - will be turned off automatically if not needed
126     var pollingMode = POLLING_ENABLED;
127
128     var sendingMessages = false;
129     var outputMessageBuffer = "";
130
131     this.sendMessage = function() {
132
133         // Do not attempt to send messages if not connected
134         if (currentState != STATE_CONNECTED)
135             return;
136
137         // Do not attempt to send empty messages
138         if (arguments.length == 0)
139             return;
140
141         /**
142          * Converts the given value to a length/string pair for use as an
143          * element in a Guacamole instruction.
144          * 
145          * @param value The value to convert.
146          * @return {String} The converted value. 
147          */
148         function getElement(value) {
149             var string = new String(value);
150             return string.length + "." + string; 
151         }
152
153         // Initialized message with first element
154         var message = getElement(arguments[0]);
155
156         // Append remaining elements
157         for (var i=1; i<arguments.length; i++)
158             message += "," + getElement(arguments[i]);
159
160         // Final terminator
161         message += ";";
162
163         // Add message to buffer
164         outputMessageBuffer += message;
165
166         // Send if not currently sending
167         if (!sendingMessages)
168             sendPendingMessages();
169
170     };
171
172     function sendPendingMessages() {
173
174         if (outputMessageBuffer.length > 0) {
175
176             sendingMessages = true;
177
178             var message_xmlhttprequest = new XMLHttpRequest();
179             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
180             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
181
182             // Once response received, send next queued event.
183             message_xmlhttprequest.onreadystatechange = function() {
184                 if (message_xmlhttprequest.readyState == 4) {
185
186                     // If an error occurs during send, handle it
187                     if (message_xmlhttprequest.status != 200)
188                         handleHTTPTunnelError(message_xmlhttprequest);
189
190                     // Otherwise, continue the send loop
191                     else
192                         sendPendingMessages();
193
194                 }
195             }
196
197             message_xmlhttprequest.send(outputMessageBuffer);
198             outputMessageBuffer = ""; // Clear buffer
199
200         }
201         else
202             sendingMessages = false;
203
204     }
205
206
207     function handleHTTPTunnelError(xmlhttprequest) {
208
209         // Get error message (if any)
210         var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
211         if (!message)
212             message = "Internal server error";
213
214         // Call error handler
215         if (tunnel.onerror) tunnel.onerror(message);
216
217         // Finish
218         tunnel.disconnect();
219
220     }
221
222
223     function handleResponse(xmlhttprequest) {
224
225         var interval = null;
226         var nextRequest = null;
227
228         var dataUpdateEvents = 0;
229
230         // The location of the last element's terminator
231         var elementEnd = -1;
232
233         // Where to start the next length search or the next element
234         var startIndex = 0;
235
236         // Parsed elements
237         var elements = new Array();
238
239         function parseResponse() {
240
241             // Do not handle responses if not connected
242             if (currentState != STATE_CONNECTED) {
243                 
244                 // Clean up interval if polling
245                 if (interval != null)
246                     clearInterval(interval);
247                 
248                 return;
249             }
250
251             // Do not parse response yet if not ready
252             if (xmlhttprequest.readyState < 2) return;
253
254             // Attempt to read status
255             var status;
256             try { status = xmlhttprequest.status; }
257
258             // If status could not be read, assume successful.
259             catch (e) { status = 200; }
260
261             // Start next request as soon as possible IF request was successful
262             if (nextRequest == null && status == 200)
263                 nextRequest = makeRequest();
264
265             // Parse stream when data is received and when complete.
266             if (xmlhttprequest.readyState == 3 ||
267                 xmlhttprequest.readyState == 4) {
268
269                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
270                 if (pollingMode == POLLING_ENABLED) {
271                     if (xmlhttprequest.readyState == 3 && interval == null)
272                         interval = setInterval(parseResponse, 30);
273                     else if (xmlhttprequest.readyState == 4 && interval != null)
274                         clearInterval(interval);
275                 }
276
277                 // If canceled, stop transfer
278                 if (xmlhttprequest.status == 0) {
279                     tunnel.disconnect();
280                     return;
281                 }
282
283                 // Halt on error during request
284                 else if (xmlhttprequest.status != 200) {
285                     handleHTTPTunnelError(xmlhttprequest);
286                     return;
287                 }
288
289                 // Attempt to read in-progress data
290                 var current;
291                 try { current = xmlhttprequest.responseText; }
292
293                 // Do not attempt to parse if data could not be read
294                 catch (e) { return; }
295
296                 // While search is within currently received data
297                 while (elementEnd < current.length) {
298
299                     // If we are waiting for element data
300                     if (elementEnd >= startIndex) {
301
302                         // We now have enough data for the element. Parse.
303                         var element = current.substring(startIndex, elementEnd);
304                         var terminator = current.substring(elementEnd, elementEnd+1);
305
306                         // Add element to array
307                         elements.push(element);
308
309                         // If last element, handle instruction
310                         if (terminator == ";") {
311
312                             // Get opcode
313                             var opcode = elements.shift();
314
315                             // Call instruction handler.
316                             if (tunnel.oninstruction != null)
317                                 tunnel.oninstruction(opcode, elements);
318
319                             // Clear elements
320                             elements.length = 0;
321
322                         }
323
324                         // Start searching for length at character after
325                         // element terminator
326                         startIndex = elementEnd + 1;
327
328                     }
329
330                     // Search for end of length
331                     var lengthEnd = current.indexOf(".", startIndex);
332                     if (lengthEnd != -1) {
333
334                         // Parse length
335                         var length = parseInt(current.substring(elementEnd+1, lengthEnd));
336
337                         // If we're done parsing, handle the next response.
338                         if (length == 0) {
339
340                             // Clean up interval if polling
341                             if (interval != null)
342                                 clearInterval(interval);
343                            
344                             // Clean up object
345                             xmlhttprequest.onreadystatechange = null;
346                             xmlhttprequest.abort();
347
348                             // Start handling next request
349                             if (nextRequest)
350                                 handleResponse(nextRequest);
351
352                             // Done parsing
353                             break;
354
355                         }
356
357                         // Calculate start of element
358                         startIndex = lengthEnd + 1;
359
360                         // Calculate location of element terminator
361                         elementEnd = startIndex + length;
362
363                     }
364                     
365                     // If no period yet, continue search when more data
366                     // is received
367                     else {
368                         startIndex = current.length;
369                         break;
370                     }
371
372                 } // end parse loop
373
374             }
375
376         }
377
378         // If response polling enabled, attempt to detect if still
379         // necessary (via wrapping parseResponse())
380         if (pollingMode == POLLING_ENABLED) {
381             xmlhttprequest.onreadystatechange = function() {
382
383                 // If we receive two or more readyState==3 events,
384                 // there is no need to poll.
385                 if (xmlhttprequest.readyState == 3) {
386                     dataUpdateEvents++;
387                     if (dataUpdateEvents >= 2) {
388                         pollingMode = POLLING_DISABLED;
389                         xmlhttprequest.onreadystatechange = parseResponse;
390                     }
391                 }
392
393                 parseResponse();
394             }
395         }
396
397         // Otherwise, just parse
398         else
399             xmlhttprequest.onreadystatechange = parseResponse;
400
401         parseResponse();
402
403     }
404
405
406     function makeRequest() {
407
408         // Download self
409         var xmlhttprequest = new XMLHttpRequest();
410         xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
411         xmlhttprequest.send(null);
412
413         return xmlhttprequest;
414
415     }
416
417     this.connect = function(data) {
418
419         // Start tunnel and connect synchronously
420         var connect_xmlhttprequest = new XMLHttpRequest();
421         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
422         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
423         connect_xmlhttprequest.send(data);
424
425         // If failure, throw error
426         if (connect_xmlhttprequest.status != 200) {
427
428             var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
429             if (!message)
430                 message = "Internal error";
431
432             throw new Error(message);
433
434         }
435
436         // Get UUID from response
437         tunnel_uuid = connect_xmlhttprequest.responseText;
438
439         // Start reading data
440         currentState = STATE_CONNECTED;
441         handleResponse(makeRequest());
442
443     };
444
445     this.disconnect = function() {
446         currentState = STATE_DISCONNECTED;
447     };
448
449 };
450
451 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
452
453
454 /**
455  * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
456  * 
457  * @constructor
458  * @augments Guacamole.Tunnel
459  * @param {String} tunnelURL The URL of the WebSocket tunneling service.
460  */
461 Guacamole.WebSocketTunnel = function(tunnelURL) {
462
463     /**
464      * Reference to this WebSocket tunnel.
465      */
466     var tunnel = this;
467
468     /**
469      * The WebSocket used by this tunnel.
470      */
471     var socket = null;
472
473     /**
474      * The WebSocket protocol corresponding to the protocol used for the current
475      * location.
476      */
477     var ws_protocol = {
478         "http:":  "ws:",
479         "https:": "wss:"
480     };
481
482     var status_code = {
483         1000: "Connection closed normally.",
484         1001: "Connection shut down.",
485         1002: "Protocol error.",
486         1003: "Invalid data.",
487         1004: "[UNKNOWN, RESERVED]",
488         1005: "No status code present.",
489         1006: "Connection closed abnormally.",
490         1007: "Inconsistent data type.",
491         1008: "Policy violation.",
492         1009: "Message too large.",
493         1010: "Extension negotiation failed."
494     };
495
496     var STATE_IDLE          = 0;
497     var STATE_CONNECTED     = 1;
498     var STATE_DISCONNECTED  = 2;
499
500     var currentState = STATE_IDLE;
501     
502     // Transform current URL to WebSocket URL
503
504     // If not already a websocket URL
505     if (   tunnelURL.substring(0, 3) != "ws:"
506         && tunnelURL.substring(0, 4) != "wss:") {
507
508         var protocol = ws_protocol[window.location.protocol];
509
510         // If absolute URL, convert to absolute WS URL
511         if (tunnelURL.substring(0, 1) == "/")
512             tunnelURL =
513                 protocol
514                 + "//" + window.location.host
515                 + tunnelURL;
516
517         // Otherwise, construct absolute from relative URL
518         else {
519
520             // Get path from pathname
521             var slash = window.location.pathname.lastIndexOf("/");
522             var path  = window.location.pathname.substring(0, slash + 1);
523
524             // Construct absolute URL
525             tunnelURL =
526                 protocol
527                 + "//" + window.location.host
528                 + path
529                 + tunnelURL;
530
531         }
532
533     }
534
535     this.sendMessage = function(elements) {
536
537         // Do not attempt to send messages if not connected
538         if (currentState != STATE_CONNECTED)
539             return;
540
541         // Do not attempt to send empty messages
542         if (arguments.length == 0)
543             return;
544
545         /**
546          * Converts the given value to a length/string pair for use as an
547          * element in a Guacamole instruction.
548          * 
549          * @param value The value to convert.
550          * @return {String} The converted value. 
551          */
552         function getElement(value) {
553             var string = new String(value);
554             return string.length + "." + string; 
555         }
556
557         // Initialized message with first element
558         var message = getElement(arguments[0]);
559
560         // Append remaining elements
561         for (var i=1; i<arguments.length; i++)
562             message += "," + getElement(arguments[i]);
563
564         // Final terminator
565         message += ";";
566
567         socket.send(message);
568
569     };
570
571     this.connect = function(data) {
572
573         // Connect socket
574         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
575
576         socket.onopen = function(event) {
577             currentState = STATE_CONNECTED;
578         };
579
580         socket.onclose = function(event) {
581
582             // If connection closed abnormally, signal error.
583             if (event.code != 1000 && tunnel.onerror)
584                 tunnel.onerror(status_code[event.code]);
585
586         };
587         
588         socket.onerror = function(event) {
589
590             // Call error handler
591             if (tunnel.onerror) tunnel.onerror(event.data);
592
593         };
594
595         socket.onmessage = function(event) {
596
597             var message = event.data;
598             var startIndex = 0;
599             var elementEnd;
600
601             var elements = [];
602
603             do {
604
605                 // Search for end of length
606                 var lengthEnd = message.indexOf(".", startIndex);
607                 if (lengthEnd != -1) {
608
609                     // Parse length
610                     var length = parseInt(message.substring(elementEnd+1, lengthEnd));
611
612                     // Calculate start of element
613                     startIndex = lengthEnd + 1;
614
615                     // Calculate location of element terminator
616                     elementEnd = startIndex + length;
617
618                 }
619                 
620                 // If no period, incomplete instruction.
621                 else
622                     throw new Error("Incomplete instruction.");
623
624                 // We now have enough data for the element. Parse.
625                 var element = message.substring(startIndex, elementEnd);
626                 var terminator = message.substring(elementEnd, elementEnd+1);
627
628                 // Add element to array
629                 elements.push(element);
630
631                 // If last element, handle instruction
632                 if (terminator == ";") {
633
634                     // Get opcode
635                     var opcode = elements.shift();
636
637                     // Call instruction handler.
638                     if (tunnel.oninstruction != null)
639                         tunnel.oninstruction(opcode, elements);
640
641                     // Clear elements
642                     elements.length = 0;
643
644                 }
645
646                 // Start searching for length at character after
647                 // element terminator
648                 startIndex = elementEnd + 1;
649
650             } while (startIndex < message.length);
651
652         };
653
654     };
655
656     this.disconnect = function() {
657         currentState = STATE_DISCONNECTED;
658         socket.close();
659     };
660
661 };
662
663 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
664
665
666 /**
667  * Guacamole Tunnel which cycles between all specified tunnels until
668  * no tunnels are left. Another tunnel is used if an error occurs but
669  * no instructions have been received. If an instruction has been
670  * received, or no tunnels remain, the error is passed directly out
671  * through the onerror handler (if defined).
672  * 
673  * @constructor
674  * @augments Guacamole.Tunnel
675  * @param {...} tunnel_chain The tunnels to use, in order of priority.
676  */
677 Guacamole.ChainedTunnel = function(tunnel_chain) {
678
679     /**
680      * Reference to this chained tunnel.
681      */
682     var chained_tunnel = this;
683
684     /**
685      * The currently wrapped tunnel, if any.
686      */
687     var current_tunnel = null;
688
689     /**
690      * Data passed in via connect(), to be used for
691      * wrapped calls to other tunnels' connect() functions.
692      */
693     var connect_data;
694
695     /**
696      * Array of all tunnels passed to this ChainedTunnel through the
697      * constructor arguments.
698      */
699     var tunnels = [];
700
701     // Load all tunnels into array
702     for (var i=0; i<arguments.length; i++)
703         tunnels.push(arguments[i]);
704
705     /**
706      * Sets the current tunnel
707      */
708     function attach(tunnel) {
709
710         // Clear handlers of current tunnel, if any
711         if (current_tunnel) {
712             current_tunnel.onerror = null;
713             current_tunnel.oninstruction = null;
714         }
715
716         // Set own functions to tunnel's functions
717         chained_tunnel.disconnect    = tunnel.disconnect;
718         chained_tunnel.sendMessage   = tunnel.sendMessage;
719         
720         // Record current tunnel
721         current_tunnel = tunnel;
722
723         // Wrap own oninstruction within current tunnel
724         current_tunnel.oninstruction = function(opcode, elements) {
725             
726             // Invoke handler
727             chained_tunnel.oninstruction(opcode, elements);
728
729             // Use handler permanently from now on
730             current_tunnel.oninstruction = chained_tunnel.oninstruction;
731
732             // Pass through errors (without trying other tunnels)
733             current_tunnel.onerror = chained_tunnel.onerror;
734             
735         }
736
737         // Attach next tunnel on error
738         current_tunnel.onerror = function(message) {
739
740             // Get next tunnel
741             var next_tunnel = tunnels.shift();
742
743             // If there IS a next tunnel, try using it.
744             if (next_tunnel)
745                 attach(next_tunnel);
746
747             // Otherwise, call error handler
748             else if (chained_tunnel.onerror)
749                 chained_tunnel.onerror(message);
750
751         };
752
753         try {
754             
755             // Attempt connection
756             current_tunnel.connect(connect_data);
757             
758         }
759         catch (e) {
760             
761             // Call error handler of current tunnel on error
762             current_tunnel.onerror(e.message);
763             
764         }
765
766
767     }
768
769     this.connect = function(data) {
770        
771         // Remember connect data
772         connect_data = data;
773
774         // Get first tunnel
775         var next_tunnel = tunnels.shift();
776
777         // Attach first tunnel
778         if (next_tunnel)
779             attach(next_tunnel);
780
781         // If there IS no first tunnel, error
782         else if (chained_tunnel.onerror)
783             chained_tunnel.onerror("No tunnels to try.");
784
785     };
786     
787 };
788
789 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();