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