clockwerk-guacamole – Blame information for rev 1

Subversion Repositories:
Rev:
Rev Author Line No. Line
1 eva 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();