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; charset=UTF-8");
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      * Arbitrary integer, unique for each tunnel read request.
439      * @private
440      */
441     var request_id = 0;
442 
443     function makeRequest() {
444 
445         // Make request, increment request ID
446         var xmlhttprequest = new XMLHttpRequest();
447         xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++));
448         xmlhttprequest.send(null);
449 
450         return xmlhttprequest;
451 
452     }
453 
454     this.connect = function(data) {
455 
456         // Start tunnel and connect synchronously
457         var connect_xmlhttprequest = new XMLHttpRequest();
458         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
459         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
460         connect_xmlhttprequest.send(data);
461 
462         // If failure, throw error
463         if (connect_xmlhttprequest.status != 200) {
464             var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest);
465             throw new Error(message);
466         }
467 
468         // Get UUID from response
469         tunnel_uuid = connect_xmlhttprequest.responseText;
470 
471         // Start reading data
472         currentState = STATE_CONNECTED;
473         handleResponse(makeRequest());
474 
475     };
476 
477     this.disconnect = function() {
478         currentState = STATE_DISCONNECTED;
479     };
480 
481 };
482 
483 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
484 
485 
486 /**
487  * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
488  * 
489  * @constructor
490  * @augments Guacamole.Tunnel
491  * @param {String} tunnelURL The URL of the WebSocket tunneling service.
492  */
493 Guacamole.WebSocketTunnel = function(tunnelURL) {
494 
495     /**
496      * Reference to this WebSocket tunnel.
497      * @private
498      */
499     var tunnel = this;
500 
501     /**
502      * The WebSocket used by this tunnel.
503      * @private
504      */
505     var socket = null;
506 
507     /**
508      * The WebSocket protocol corresponding to the protocol used for the current
509      * location.
510      * @private
511      */
512     var ws_protocol = {
513         "http:":  "ws:",
514         "https:": "wss:"
515     };
516 
517     var status_code = {
518         1000: "Connection closed normally.",
519         1001: "Connection shut down.",
520         1002: "Protocol error.",
521         1003: "Invalid data.",
522         1004: "[UNKNOWN, RESERVED]",
523         1005: "No status code present.",
524         1006: "Connection closed abnormally.",
525         1007: "Inconsistent data type.",
526         1008: "Policy violation.",
527         1009: "Message too large.",
528         1010: "Extension negotiation failed."
529     };
530 
531     var STATE_IDLE          = 0;
532     var STATE_CONNECTED     = 1;
533     var STATE_DISCONNECTED  = 2;
534 
535     var currentState = STATE_IDLE;
536     
537     // Transform current URL to WebSocket URL
538 
539     // If not already a websocket URL
540     if (   tunnelURL.substring(0, 3) != "ws:"
541         && tunnelURL.substring(0, 4) != "wss:") {
542 
543         var protocol = ws_protocol[window.location.protocol];
544 
545         // If absolute URL, convert to absolute WS URL
546         if (tunnelURL.substring(0, 1) == "/")
547             tunnelURL =
548                 protocol
549                 + "//" + window.location.host
550                 + tunnelURL;
551 
552         // Otherwise, construct absolute from relative URL
553         else {
554 
555             // Get path from pathname
556             var slash = window.location.pathname.lastIndexOf("/");
557             var path  = window.location.pathname.substring(0, slash + 1);
558 
559             // Construct absolute URL
560             tunnelURL =
561                 protocol
562                 + "//" + window.location.host
563                 + path
564                 + tunnelURL;
565 
566         }
567 
568     }
569 
570     this.sendMessage = function(elements) {
571 
572         // Do not attempt to send messages if not connected
573         if (currentState != STATE_CONNECTED)
574             return;
575 
576         // Do not attempt to send empty messages
577         if (arguments.length == 0)
578             return;
579 
580         /**
581          * Converts the given value to a length/string pair for use as an
582          * element in a Guacamole instruction.
583          * 
584          * @private
585          * @param value The value to convert.
586          * @return {String} The converted value. 
587          */
588         function getElement(value) {
589             var string = new String(value);
590             return string.length + "." + string; 
591         }
592 
593         // Initialized message with first element
594         var message = getElement(arguments[0]);
595 
596         // Append remaining elements
597         for (var i=1; i<arguments.length; i++)
598             message += "," + getElement(arguments[i]);
599 
600         // Final terminator
601         message += ";";
602 
603         socket.send(message);
604 
605     };
606 
607     this.connect = function(data) {
608 
609         // Connect socket
610         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
611 
612         socket.onopen = function(event) {
613             currentState = STATE_CONNECTED;
614         };
615 
616         socket.onclose = function(event) {
617 
618             // If connection closed abnormally, signal error.
619             if (event.code != 1000 && tunnel.onerror)
620                 tunnel.onerror(status_code[event.code]);
621 
622         };
623         
624         socket.onerror = function(event) {
625 
626             // Call error handler
627             if (tunnel.onerror) tunnel.onerror(event.data);
628 
629         };
630 
631         socket.onmessage = function(event) {
632 
633             var message = event.data;
634             var startIndex = 0;
635             var elementEnd;
636 
637             var elements = [];
638 
639             do {
640 
641                 // Search for end of length
642                 var lengthEnd = message.indexOf(".", startIndex);
643                 if (lengthEnd != -1) {
644 
645                     // Parse length
646                     var length = parseInt(message.substring(elementEnd+1, lengthEnd));
647 
648                     // Calculate start of element
649                     startIndex = lengthEnd + 1;
650 
651                     // Calculate location of element terminator
652                     elementEnd = startIndex + length;
653 
654                 }
655                 
656                 // If no period, incomplete instruction.
657                 else
658                     throw new Error("Incomplete instruction.");
659 
660                 // We now have enough data for the element. Parse.
661                 var element = message.substring(startIndex, elementEnd);
662                 var terminator = message.substring(elementEnd, elementEnd+1);
663 
664                 // Add element to array
665                 elements.push(element);
666 
667                 // If last element, handle instruction
668                 if (terminator == ";") {
669 
670                     // Get opcode
671                     var opcode = elements.shift();
672 
673                     // Call instruction handler.
674                     if (tunnel.oninstruction != null)
675                         tunnel.oninstruction(opcode, elements);
676 
677                     // Clear elements
678                     elements.length = 0;
679 
680                 }
681 
682                 // Start searching for length at character after
683                 // element terminator
684                 startIndex = elementEnd + 1;
685 
686             } while (startIndex < message.length);
687 
688         };
689 
690     };
691 
692     this.disconnect = function() {
693         currentState = STATE_DISCONNECTED;
694         socket.close();
695     };
696 
697 };
698 
699 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
700 
701 
702 /**
703  * Guacamole Tunnel which cycles between all specified tunnels until
704  * no tunnels are left. Another tunnel is used if an error occurs but
705  * no instructions have been received. If an instruction has been
706  * received, or no tunnels remain, the error is passed directly out
707  * through the onerror handler (if defined).
708  * 
709  * @constructor
710  * @augments Guacamole.Tunnel
711  * @param {...} tunnel_chain The tunnels to use, in order of priority.
712  */
713 Guacamole.ChainedTunnel = function(tunnel_chain) {
714 
715     /**
716      * Reference to this chained tunnel.
717      * @private
718      */
719     var chained_tunnel = this;
720 
721     /**
722      * The currently wrapped tunnel, if any.
723      * @private
724      */
725     var current_tunnel = null;
726 
727     /**
728      * Data passed in via connect(), to be used for
729      * wrapped calls to other tunnels' connect() functions.
730      * @private
731      */
732     var connect_data;
733 
734     /**
735      * Array of all tunnels passed to this ChainedTunnel through the
736      * constructor arguments.
737      * @private
738      */
739     var tunnels = [];
740 
741     // Load all tunnels into array
742     for (var i=0; i<arguments.length; i++)
743         tunnels.push(arguments[i]);
744 
745     /**
746      * Sets the current tunnel.
747      * 
748      * @private
749      * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel.
750      */
751     function attach(tunnel) {
752 
753         // Clear handlers of current tunnel, if any
754         if (current_tunnel) {
755             current_tunnel.onerror = null;
756             current_tunnel.oninstruction = null;
757         }
758 
759         // Set own functions to tunnel's functions
760         chained_tunnel.disconnect    = tunnel.disconnect;
761         chained_tunnel.sendMessage   = tunnel.sendMessage;
762         
763         // Record current tunnel
764         current_tunnel = tunnel;
765 
766         // Wrap own oninstruction within current tunnel
767         current_tunnel.oninstruction = function(opcode, elements) {
768             
769             // Invoke handler
770             chained_tunnel.oninstruction(opcode, elements);
771 
772             // Use handler permanently from now on
773             current_tunnel.oninstruction = chained_tunnel.oninstruction;
774 
775             // Pass through errors (without trying other tunnels)
776             current_tunnel.onerror = chained_tunnel.onerror;
777             
778         }
779 
780         // Attach next tunnel on error
781         current_tunnel.onerror = function(message) {
782 
783             // Get next tunnel
784             var next_tunnel = tunnels.shift();
785 
786             // If there IS a next tunnel, try using it.
787             if (next_tunnel)
788                 attach(next_tunnel);
789 
790             // Otherwise, call error handler
791             else if (chained_tunnel.onerror)
792                 chained_tunnel.onerror(message);
793 
794         };
795 
796         try {
797             
798             // Attempt connection
799             current_tunnel.connect(connect_data);
800             
801         }
802         catch (e) {
803             
804             // Call error handler of current tunnel on error
805             current_tunnel.onerror(e.message);
806             
807         }
808 
809 
810     }
811 
812     this.connect = function(data) {
813        
814         // Remember connect data
815         connect_data = data;
816 
817         // Get first tunnel
818         var next_tunnel = tunnels.shift();
819 
820         // Attach first tunnel
821         if (next_tunnel)
822             attach(next_tunnel);
823 
824         // If there IS no first tunnel, error
825         else if (chained_tunnel.onerror)
826             chained_tunnel.onerror("No tunnels to try.");
827 
828     };
829     
830 };
831 
832 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
833