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