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