source: OpenRLabs-Git/web2py/applications/rlabs/static/js/guacamole-common-js/modules/Tunnel.js

main
Last change on this file was 42bd667, checked in by David Fuertes <dfuertes@…>, 4 years ago

Historial Limpio

  • Property mode set to 100644
File size: 35.6 KB
Line 
1/*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements.  See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership.  The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License.  You may obtain a copy of the License at
9 *
10 *   http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied.  See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20var Guacamole = Guacamole || {};
21
22/**
23 * Core object providing abstract communication for Guacamole. This object
24 * is a null implementation whose functions do nothing. Guacamole applications
25 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
26 * on this one.
27 *
28 * @constructor
29 * @see Guacamole.HTTPTunnel
30 */
31Guacamole.Tunnel = function() {
32
33    /**
34     * Connect to the tunnel with the given optional data. This data is
35     * typically used for authentication. The format of data accepted is
36     * up to the tunnel implementation.
37     *
38     * @param {String} data The data to send to the tunnel when connecting.
39     */
40    this.connect = function(data) {};
41   
42    /**
43     * Disconnect from the tunnel.
44     */
45    this.disconnect = function() {};
46   
47    /**
48     * Send the given message through the tunnel to the service on the other
49     * side. All messages are guaranteed to be received in the order sent.
50     *
51     * @param {...*} elements
52     *     The elements of the message to send to the service on the other side
53     *     of the tunnel.
54     */
55    this.sendMessage = function(elements) {};
56
57    /**
58     * The current state of this tunnel.
59     *
60     * @type {Number}
61     */
62    this.state = Guacamole.Tunnel.State.CONNECTING;
63
64    /**
65     * The maximum amount of time to wait for data to be received, in
66     * milliseconds. If data is not received within this amount of time,
67     * the tunnel is closed with an error. The default value is 15000.
68     *
69     * @type {Number}
70     */
71    this.receiveTimeout = 15000;
72
73    /**
74     * The UUID uniquely identifying this tunnel. If not yet known, this will
75     * be null.
76     *
77     * @type {String}
78     */
79    this.uuid = null;
80
81    /**
82     * Fired whenever an error is encountered by the tunnel.
83     *
84     * @event
85     * @param {Guacamole.Status} status A status object which describes the
86     *                                  error.
87     */
88    this.onerror = null;
89
90    /**
91     * Fired whenever the state of the tunnel changes.
92     *
93     * @event
94     * @param {Number} state The new state of the client.
95     */
96    this.onstatechange = null;
97
98    /**
99     * Fired once for every complete Guacamole instruction received, in order.
100     *
101     * @event
102     * @param {String} opcode The Guacamole instruction opcode.
103     * @param {Array} parameters The parameters provided for the instruction,
104     *                           if any.
105     */
106    this.oninstruction = null;
107
108};
109
110/**
111 * The Guacamole protocol instruction opcode reserved for arbitrary internal
112 * use by tunnel implementations. The value of this opcode is guaranteed to be
113 * the empty string (""). Tunnel implementations may use this opcode for any
114 * purpose. It is currently used by the HTTP tunnel to mark the end of the HTTP
115 * response, and by the WebSocket tunnel to transmit the tunnel UUID.
116 *
117 * @constant
118 * @type {String}
119 */
120Guacamole.Tunnel.INTERNAL_DATA_OPCODE = '';
121
122/**
123 * All possible tunnel states.
124 */
125Guacamole.Tunnel.State = {
126
127    /**
128     * A connection is in pending. It is not yet known whether connection was
129     * successful.
130     *
131     * @type {Number}
132     */
133    "CONNECTING": 0,
134
135    /**
136     * Connection was successful, and data is being received.
137     *
138     * @type {Number}
139     */
140    "OPEN": 1,
141
142    /**
143     * The connection is closed. Connection may not have been successful, the
144     * tunnel may have been explicitly closed by either side, or an error may
145     * have occurred.
146     *
147     * @type {Number}
148     */
149    "CLOSED": 2
150
151};
152
153/**
154 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
155 *
156 * @constructor
157 * @augments Guacamole.Tunnel
158 *
159 * @param {String} tunnelURL
160 *     The URL of the HTTP tunneling service.
161 *
162 * @param {Boolean} [crossDomain=false]
163 *     Whether tunnel requests will be cross-domain, and thus must use CORS
164 *     mechanisms and headers. By default, it is assumed that tunnel requests
165 *     will be made to the same domain.
166 */
167Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) {
168
169    /**
170     * Reference to this HTTP tunnel.
171     * @private
172     */
173    var tunnel = this;
174
175    var TUNNEL_CONNECT = tunnelURL + "?connect";
176    var TUNNEL_READ    = tunnelURL + "?read:";
177    var TUNNEL_WRITE   = tunnelURL + "?write:";
178
179    var POLLING_ENABLED     = 1;
180    var POLLING_DISABLED    = 0;
181
182    // Default to polling - will be turned off automatically if not needed
183    var pollingMode = POLLING_ENABLED;
184
185    var sendingMessages = false;
186    var outputMessageBuffer = "";
187
188    // If requests are expected to be cross-domain, the cookie that the HTTP
189    // tunnel depends on will only be sent if withCredentials is true
190    var withCredentials = !!crossDomain;
191
192    /**
193     * The current receive timeout ID, if any.
194     * @private
195     */
196    var receive_timeout = null;
197
198    /**
199     * Initiates a timeout which, if data is not received, causes the tunnel
200     * to close with an error.
201     *
202     * @private
203     */
204    function reset_timeout() {
205
206        // Get rid of old timeout (if any)
207        window.clearTimeout(receive_timeout);
208
209        // Set new timeout
210        receive_timeout = window.setTimeout(function () {
211            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
212        }, tunnel.receiveTimeout);
213
214    }
215
216    /**
217     * Closes this tunnel, signaling the given status and corresponding
218     * message, which will be sent to the onerror handler if the status is
219     * an error status.
220     *
221     * @private
222     * @param {Guacamole.Status} status The status causing the connection to
223     *                                  close;
224     */
225    function close_tunnel(status) {
226
227        // Ignore if already closed
228        if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
229            return;
230
231        // If connection closed abnormally, signal error.
232        if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) {
233
234            // Ignore RESOURCE_NOT_FOUND if we've already connected, as that
235            // only signals end-of-stream for the HTTP tunnel.
236            if (tunnel.state === Guacamole.Tunnel.State.CONNECTING
237                    || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND)
238                tunnel.onerror(status);
239
240        }
241
242        // Mark as closed
243        tunnel.state = Guacamole.Tunnel.State.CLOSED;
244
245        // Reset output message buffer
246        sendingMessages = false;
247
248        if (tunnel.onstatechange)
249            tunnel.onstatechange(tunnel.state);
250
251    }
252
253
254    this.sendMessage = function() {
255
256        // Do not attempt to send messages if not connected
257        if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
258            return;
259
260        // Do not attempt to send empty messages
261        if (arguments.length === 0)
262            return;
263
264        /**
265         * Converts the given value to a length/string pair for use as an
266         * element in a Guacamole instruction.
267         *
268         * @private
269         * @param value The value to convert.
270         * @return {String} The converted value.
271         */
272        function getElement(value) {
273            var string = new String(value);
274            return string.length + "." + string;
275        }
276
277        // Initialized message with first element
278        var message = getElement(arguments[0]);
279
280        // Append remaining elements
281        for (var i=1; i<arguments.length; i++)
282            message += "," + getElement(arguments[i]);
283
284        // Final terminator
285        message += ";";
286
287        // Add message to buffer
288        outputMessageBuffer += message;
289
290        // Send if not currently sending
291        if (!sendingMessages)
292            sendPendingMessages();
293
294    };
295
296    function sendPendingMessages() {
297
298        // Do not attempt to send messages if not connected
299        if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
300            return;
301
302        if (outputMessageBuffer.length > 0) {
303
304            sendingMessages = true;
305
306            var message_xmlhttprequest = new XMLHttpRequest();
307            message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel.uuid);
308            message_xmlhttprequest.withCredentials = withCredentials;
309            message_xmlhttprequest.setRequestHeader("Content-type", "application/octet-stream");
310
311            // Once response received, send next queued event.
312            message_xmlhttprequest.onreadystatechange = function() {
313                if (message_xmlhttprequest.readyState === 4) {
314
315                    // If an error occurs during send, handle it
316                    if (message_xmlhttprequest.status !== 200)
317                        handleHTTPTunnelError(message_xmlhttprequest);
318
319                    // Otherwise, continue the send loop
320                    else
321                        sendPendingMessages();
322
323                }
324            };
325
326            message_xmlhttprequest.send(outputMessageBuffer);
327            outputMessageBuffer = ""; // Clear buffer
328
329        }
330        else
331            sendingMessages = false;
332
333    }
334
335    function handleHTTPTunnelError(xmlhttprequest) {
336
337        var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
338        var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
339
340        close_tunnel(new Guacamole.Status(code, message));
341
342    }
343
344    function handleResponse(xmlhttprequest) {
345
346        var interval = null;
347        var nextRequest = null;
348
349        var dataUpdateEvents = 0;
350
351        // The location of the last element's terminator
352        var elementEnd = -1;
353
354        // Where to start the next length search or the next element
355        var startIndex = 0;
356
357        // Parsed elements
358        var elements = new Array();
359
360        function parseResponse() {
361
362            // Do not handle responses if not connected
363            if (tunnel.state !== Guacamole.Tunnel.State.OPEN) {
364               
365                // Clean up interval if polling
366                if (interval !== null)
367                    clearInterval(interval);
368               
369                return;
370            }
371
372            // Do not parse response yet if not ready
373            if (xmlhttprequest.readyState < 2) return;
374
375            // Attempt to read status
376            var status;
377            try { status = xmlhttprequest.status; }
378
379            // If status could not be read, assume successful.
380            catch (e) { status = 200; }
381
382            // Start next request as soon as possible IF request was successful
383            if (!nextRequest && status === 200)
384                nextRequest = makeRequest();
385
386            // Parse stream when data is received and when complete.
387            if (xmlhttprequest.readyState === 3 ||
388                xmlhttprequest.readyState === 4) {
389
390                reset_timeout();
391
392                // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
393                if (pollingMode === POLLING_ENABLED) {
394                    if (xmlhttprequest.readyState === 3 && !interval)
395                        interval = setInterval(parseResponse, 30);
396                    else if (xmlhttprequest.readyState === 4 && interval)
397                        clearInterval(interval);
398                }
399
400                // If canceled, stop transfer
401                if (xmlhttprequest.status === 0) {
402                    tunnel.disconnect();
403                    return;
404                }
405
406                // Halt on error during request
407                else if (xmlhttprequest.status !== 200) {
408                    handleHTTPTunnelError(xmlhttprequest);
409                    return;
410                }
411
412                // Attempt to read in-progress data
413                var current;
414                try { current = xmlhttprequest.responseText; }
415
416                // Do not attempt to parse if data could not be read
417                catch (e) { return; }
418
419                // While search is within currently received data
420                while (elementEnd < current.length) {
421
422                    // If we are waiting for element data
423                    if (elementEnd >= startIndex) {
424
425                        // We now have enough data for the element. Parse.
426                        var element = current.substring(startIndex, elementEnd);
427                        var terminator = current.substring(elementEnd, elementEnd+1);
428
429                        // Add element to array
430                        elements.push(element);
431
432                        // If last element, handle instruction
433                        if (terminator === ";") {
434
435                            // Get opcode
436                            var opcode = elements.shift();
437
438                            // Call instruction handler.
439                            if (tunnel.oninstruction)
440                                tunnel.oninstruction(opcode, elements);
441
442                            // Clear elements
443                            elements.length = 0;
444
445                        }
446
447                        // Start searching for length at character after
448                        // element terminator
449                        startIndex = elementEnd + 1;
450
451                    }
452
453                    // Search for end of length
454                    var lengthEnd = current.indexOf(".", startIndex);
455                    if (lengthEnd !== -1) {
456
457                        // Parse length
458                        var length = parseInt(current.substring(elementEnd+1, lengthEnd));
459
460                        // If we're done parsing, handle the next response.
461                        if (length === 0) {
462
463                            // Clean up interval if polling
464                            if (interval)
465                                clearInterval(interval);
466                           
467                            // Clean up object
468                            xmlhttprequest.onreadystatechange = null;
469                            xmlhttprequest.abort();
470
471                            // Start handling next request
472                            if (nextRequest)
473                                handleResponse(nextRequest);
474
475                            // Done parsing
476                            break;
477
478                        }
479
480                        // Calculate start of element
481                        startIndex = lengthEnd + 1;
482
483                        // Calculate location of element terminator
484                        elementEnd = startIndex + length;
485
486                    }
487                   
488                    // If no period yet, continue search when more data
489                    // is received
490                    else {
491                        startIndex = current.length;
492                        break;
493                    }
494
495                } // end parse loop
496
497            }
498
499        }
500
501        // If response polling enabled, attempt to detect if still
502        // necessary (via wrapping parseResponse())
503        if (pollingMode === POLLING_ENABLED) {
504            xmlhttprequest.onreadystatechange = function() {
505
506                // If we receive two or more readyState==3 events,
507                // there is no need to poll.
508                if (xmlhttprequest.readyState === 3) {
509                    dataUpdateEvents++;
510                    if (dataUpdateEvents >= 2) {
511                        pollingMode = POLLING_DISABLED;
512                        xmlhttprequest.onreadystatechange = parseResponse;
513                    }
514                }
515
516                parseResponse();
517            };
518        }
519
520        // Otherwise, just parse
521        else
522            xmlhttprequest.onreadystatechange = parseResponse;
523
524        parseResponse();
525
526    }
527
528    /**
529     * Arbitrary integer, unique for each tunnel read request.
530     * @private
531     */
532    var request_id = 0;
533
534    function makeRequest() {
535
536        // Make request, increment request ID
537        var xmlhttprequest = new XMLHttpRequest();
538        xmlhttprequest.open("GET", TUNNEL_READ + tunnel.uuid + ":" + (request_id++));
539        xmlhttprequest.withCredentials = withCredentials;
540        xmlhttprequest.send(null);
541
542        return xmlhttprequest;
543
544    }
545
546    this.connect = function(data) {
547
548        // Start waiting for connect
549        reset_timeout();
550
551        // Start tunnel and connect
552        var connect_xmlhttprequest = new XMLHttpRequest();
553        connect_xmlhttprequest.onreadystatechange = function() {
554
555            if (connect_xmlhttprequest.readyState !== 4)
556                return;
557
558            // If failure, throw error
559            if (connect_xmlhttprequest.status !== 200) {
560                handleHTTPTunnelError(connect_xmlhttprequest);
561                return;
562            }
563
564            reset_timeout();
565
566            // Get UUID from response
567            tunnel.uuid = connect_xmlhttprequest.responseText;
568
569            tunnel.state = Guacamole.Tunnel.State.OPEN;
570            if (tunnel.onstatechange)
571                tunnel.onstatechange(tunnel.state);
572
573            // Start reading data
574            handleResponse(makeRequest());
575
576        };
577
578        connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true);
579        connect_xmlhttprequest.withCredentials = withCredentials;
580        connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
581        connect_xmlhttprequest.send(data);
582
583    };
584
585    this.disconnect = function() {
586        close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
587    };
588
589};
590
591Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
592
593/**
594 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
595 *
596 * @constructor
597 * @augments Guacamole.Tunnel
598 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
599 */
600Guacamole.WebSocketTunnel = function(tunnelURL) {
601
602    /**
603     * Reference to this WebSocket tunnel.
604     * @private
605     */
606    var tunnel = this;
607
608    /**
609     * The WebSocket used by this tunnel.
610     * @private
611     */
612    var socket = null;
613
614    /**
615     * The current receive timeout ID, if any.
616     * @private
617     */
618    var receive_timeout = null;
619
620    /**
621     * The WebSocket protocol corresponding to the protocol used for the current
622     * location.
623     * @private
624     */
625    var ws_protocol = {
626        "http:":  "ws:",
627        "https:": "wss:"
628    };
629
630    // Transform current URL to WebSocket URL
631
632    // If not already a websocket URL
633    if (   tunnelURL.substring(0, 3) !== "ws:"
634        && tunnelURL.substring(0, 4) !== "wss:") {
635
636        var protocol = ws_protocol[window.location.protocol];
637
638        // If absolute URL, convert to absolute WS URL
639        if (tunnelURL.substring(0, 1) === "/")
640            tunnelURL =
641                protocol
642                + "//" + window.location.host
643                + tunnelURL;
644
645        // Otherwise, construct absolute from relative URL
646        else {
647
648            // Get path from pathname
649            var slash = window.location.pathname.lastIndexOf("/");
650            var path  = window.location.pathname.substring(0, slash + 1);
651
652            // Construct absolute URL
653            tunnelURL =
654                protocol
655                + "//" + window.location.host
656                + path
657                + tunnelURL;
658
659        }
660
661    }
662
663    /**
664     * Initiates a timeout which, if data is not received, causes the tunnel
665     * to close with an error.
666     *
667     * @private
668     */
669    function reset_timeout() {
670
671        // Get rid of old timeout (if any)
672        window.clearTimeout(receive_timeout);
673
674        // Set new timeout
675        receive_timeout = window.setTimeout(function () {
676            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
677        }, tunnel.receiveTimeout);
678
679    }
680
681    /**
682     * Closes this tunnel, signaling the given status and corresponding
683     * message, which will be sent to the onerror handler if the status is
684     * an error status.
685     *
686     * @private
687     * @param {Guacamole.Status} status The status causing the connection to
688     *                                  close;
689     */
690    function close_tunnel(status) {
691
692        // Ignore if already closed
693        if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
694            return;
695
696        // If connection closed abnormally, signal error.
697        if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror)
698            tunnel.onerror(status);
699
700        // Mark as closed
701        tunnel.state = Guacamole.Tunnel.State.CLOSED;
702        if (tunnel.onstatechange)
703            tunnel.onstatechange(tunnel.state);
704
705        socket.close();
706
707    }
708
709    this.sendMessage = function(elements) {
710
711        // Do not attempt to send messages if not connected
712        if (tunnel.state !== Guacamole.Tunnel.State.OPEN)
713            return;
714
715        // Do not attempt to send empty messages
716        if (arguments.length === 0)
717            return;
718
719        /**
720         * Converts the given value to a length/string pair for use as an
721         * element in a Guacamole instruction.
722         *
723         * @private
724         * @param value The value to convert.
725         * @return {String} The converted value.
726         */
727        function getElement(value) {
728            var string = new String(value);
729            return string.length + "." + string;
730        }
731
732        // Initialized message with first element
733        var message = getElement(arguments[0]);
734
735        // Append remaining elements
736        for (var i=1; i<arguments.length; i++)
737            message += "," + getElement(arguments[i]);
738
739        // Final terminator
740        message += ";";
741
742        socket.send(message);
743
744    };
745
746    this.connect = function(data) {
747
748        reset_timeout();
749
750        // Connect socket
751        socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
752
753        socket.onopen = function(event) {
754            reset_timeout();
755        };
756
757        socket.onclose = function(event) {
758            close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
759        };
760       
761        socket.onerror = function(event) {
762            close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data));
763        };
764
765        socket.onmessage = function(event) {
766
767            reset_timeout();
768
769            var message = event.data;
770            var startIndex = 0;
771            var elementEnd;
772
773            var elements = [];
774
775            do {
776
777                // Search for end of length
778                var lengthEnd = message.indexOf(".", startIndex);
779                if (lengthEnd !== -1) {
780
781                    // Parse length
782                    var length = parseInt(message.substring(elementEnd+1, lengthEnd));
783
784                    // Calculate start of element
785                    startIndex = lengthEnd + 1;
786
787                    // Calculate location of element terminator
788                    elementEnd = startIndex + length;
789
790                }
791               
792                // If no period, incomplete instruction.
793                else
794                    close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction."));
795
796                // We now have enough data for the element. Parse.
797                var element = message.substring(startIndex, elementEnd);
798                var terminator = message.substring(elementEnd, elementEnd+1);
799
800                // Add element to array
801                elements.push(element);
802
803                // If last element, handle instruction
804                if (terminator === ";") {
805
806                    // Get opcode
807                    var opcode = elements.shift();
808
809                    // Update state and UUID when first instruction received
810                    if (tunnel.state !== Guacamole.Tunnel.State.OPEN) {
811
812                        // Associate tunnel UUID if received
813                        if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE)
814                            tunnel.uuid = elements[0];
815
816                        // Tunnel is now open and UUID is available
817                        tunnel.state = Guacamole.Tunnel.State.OPEN;
818                        if (tunnel.onstatechange)
819                            tunnel.onstatechange(tunnel.state);
820
821                    }
822
823                    // Call instruction handler.
824                    if (opcode !== Guacamole.Tunnel.INTERNAL_DATA_OPCODE && tunnel.oninstruction)
825                        tunnel.oninstruction(opcode, elements);
826
827                    // Clear elements
828                    elements.length = 0;
829
830                }
831
832                // Start searching for length at character after
833                // element terminator
834                startIndex = elementEnd + 1;
835
836            } while (startIndex < message.length);
837
838        };
839
840    };
841
842    this.disconnect = function() {
843        close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed."));
844    };
845
846};
847
848Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
849
850/**
851 * Guacamole Tunnel which cycles between all specified tunnels until
852 * no tunnels are left. Another tunnel is used if an error occurs but
853 * no instructions have been received. If an instruction has been
854 * received, or no tunnels remain, the error is passed directly out
855 * through the onerror handler (if defined).
856 *
857 * @constructor
858 * @augments Guacamole.Tunnel
859 * @param {...*} tunnelChain
860 *     The tunnels to use, in order of priority.
861 */
862Guacamole.ChainedTunnel = function(tunnelChain) {
863
864    /**
865     * Reference to this chained tunnel.
866     * @private
867     */
868    var chained_tunnel = this;
869
870    /**
871     * Data passed in via connect(), to be used for
872     * wrapped calls to other tunnels' connect() functions.
873     * @private
874     */
875    var connect_data;
876
877    /**
878     * Array of all tunnels passed to this ChainedTunnel through the
879     * constructor arguments.
880     * @private
881     */
882    var tunnels = [];
883
884    /**
885     * The tunnel committed via commit_tunnel(), if any, or null if no tunnel
886     * has yet been committed.
887     *
888     * @private
889     * @type {Guacamole.Tunnel}
890     */
891    var committedTunnel = null;
892
893    // Load all tunnels into array
894    for (var i=0; i<arguments.length; i++)
895        tunnels.push(arguments[i]);
896
897    /**
898     * Sets the current tunnel.
899     *
900     * @private
901     * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel.
902     */
903    function attach(tunnel) {
904
905        // Set own functions to tunnel's functions
906        chained_tunnel.disconnect  = tunnel.disconnect;
907        chained_tunnel.sendMessage = tunnel.sendMessage;
908
909        /**
910         * Fails the currently-attached tunnel, attaching a new tunnel if
911         * possible.
912         *
913         * @private
914         * @param {Guacamole.Status} [status]
915         *     An object representing the failure that occured in the
916         *     currently-attached tunnel, if known.
917         *
918         * @return {Guacamole.Tunnel}
919         *     The next tunnel, or null if there are no more tunnels to try or
920         *     if no more tunnels should be tried.
921         */
922        var failTunnel = function failTunnel(status) {
923
924            // Do not attempt to continue using next tunnel on server timeout
925            if (status && status.code === Guacamole.Status.Code.UPSTREAM_TIMEOUT) {
926                tunnels = [];
927                return null;
928            }
929
930            // Get next tunnel
931            var next_tunnel = tunnels.shift();
932
933            // If there IS a next tunnel, try using it.
934            if (next_tunnel) {
935                tunnel.onerror = null;
936                tunnel.oninstruction = null;
937                tunnel.onstatechange = null;
938                attach(next_tunnel);
939            }
940
941            return next_tunnel;
942
943        };
944
945        /**
946         * Use the current tunnel from this point forward. Do not try any more
947         * tunnels, even if the current tunnel fails.
948         *
949         * @private
950         */
951        function commit_tunnel() {
952            tunnel.onstatechange = chained_tunnel.onstatechange;
953            tunnel.oninstruction = chained_tunnel.oninstruction;
954            tunnel.onerror = chained_tunnel.onerror;
955            chained_tunnel.uuid = tunnel.uuid;
956            committedTunnel = tunnel;
957        }
958
959        // Wrap own onstatechange within current tunnel
960        tunnel.onstatechange = function(state) {
961
962            switch (state) {
963
964                // If open, use this tunnel from this point forward.
965                case Guacamole.Tunnel.State.OPEN:
966                    commit_tunnel();
967                    if (chained_tunnel.onstatechange)
968                        chained_tunnel.onstatechange(state);
969                    break;
970
971                // If closed, mark failure, attempt next tunnel
972                case Guacamole.Tunnel.State.CLOSED:
973                    if (!failTunnel() && chained_tunnel.onstatechange)
974                        chained_tunnel.onstatechange(state);
975                    break;
976               
977            }
978
979        };
980
981        // Wrap own oninstruction within current tunnel
982        tunnel.oninstruction = function(opcode, elements) {
983
984            // Accept current tunnel
985            commit_tunnel();
986
987            // Invoke handler
988            if (chained_tunnel.oninstruction)
989                chained_tunnel.oninstruction(opcode, elements);
990
991        };
992
993        // Attach next tunnel on error
994        tunnel.onerror = function(status) {
995
996            // Mark failure, attempt next tunnel
997            if (!failTunnel(status) && chained_tunnel.onerror)
998                chained_tunnel.onerror(status);
999
1000        };
1001
1002        // Attempt connection
1003        tunnel.connect(connect_data);
1004       
1005    }
1006
1007    this.connect = function(data) {
1008       
1009        // Remember connect data
1010        connect_data = data;
1011
1012        // Get committed tunnel if exists or the first tunnel on the list
1013        var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift();
1014
1015        // Attach first tunnel
1016        if (next_tunnel)
1017            attach(next_tunnel);
1018
1019        // If there IS no first tunnel, error
1020        else if (chained_tunnel.onerror)
1021            chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try.");
1022
1023    };
1024   
1025};
1026
1027Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel();
1028
1029/**
1030 * Guacamole Tunnel which replays a Guacamole protocol dump from a static file
1031 * received via HTTP. Instructions within the file are parsed and handled as
1032 * quickly as possible, while the file is being downloaded.
1033 *
1034 * @constructor
1035 * @augments Guacamole.Tunnel
1036 * @param {String} url
1037 *     The URL of a Guacamole protocol dump.
1038 *
1039 * @param {Boolean} [crossDomain=false]
1040 *     Whether tunnel requests will be cross-domain, and thus must use CORS
1041 *     mechanisms and headers. By default, it is assumed that tunnel requests
1042 *     will be made to the same domain.
1043 */
1044Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain) {
1045
1046    /**
1047     * Reference to this Guacamole.StaticHTTPTunnel.
1048     *
1049     * @private
1050     */
1051    var tunnel = this;
1052
1053    /**
1054     * The current, in-progress HTTP request. If no request is currently in
1055     * progress, this will be null.
1056     *
1057     * @private
1058     * @type {XMLHttpRequest}
1059     */
1060    var xhr = null;
1061
1062    /**
1063     * Changes the stored numeric state of this tunnel, firing the onstatechange
1064     * event if the new state is different and a handler has been defined.
1065     *
1066     * @private
1067     * @param {Number} state
1068     *     The new state of this tunnel.
1069     */
1070    var setState = function setState(state) {
1071
1072        // Notify only if state changes
1073        if (state !== tunnel.state) {
1074            tunnel.state = state;
1075            if (tunnel.onstatechange)
1076                tunnel.onstatechange(state);
1077        }
1078
1079    };
1080
1081    /**
1082     * Returns the Guacamole protocol status code which most closely
1083     * represents the given HTTP status code.
1084     *
1085     * @private
1086     * @param {Number} httpStatus
1087     *     The HTTP status code to translate into a Guacamole protocol status
1088     *     code.
1089     *
1090     * @returns {Number}
1091     *     The Guacamole protocol status code which most closely represents the
1092     *     given HTTP status code.
1093     */
1094    var getGuacamoleStatusCode = function getGuacamoleStatusCode(httpStatus) {
1095
1096        // Translate status codes with known equivalents
1097        switch (httpStatus) {
1098
1099            // HTTP 400 - Bad request
1100            case 400:
1101                return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
1102
1103            // HTTP 403 - Forbidden
1104            case 403:
1105                return Guacamole.Status.Code.CLIENT_FORBIDDEN;
1106
1107            // HTTP 404 - Resource not found
1108            case 404:
1109                return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
1110
1111            // HTTP 429 - Too many requests
1112            case 429:
1113                return Guacamole.Status.Code.CLIENT_TOO_MANY;
1114
1115            // HTTP 503 - Server unavailable
1116            case 503:
1117                return Guacamole.Status.Code.SERVER_BUSY;
1118
1119        }
1120
1121        // Default all other codes to generic internal error
1122        return Guacamole.Status.Code.SERVER_ERROR;
1123
1124    };
1125
1126    this.sendMessage = function sendMessage(elements) {
1127        // Do nothing
1128    };
1129
1130    this.connect = function connect(data) {
1131
1132        // Ensure any existing connection is killed
1133        tunnel.disconnect();
1134
1135        // Connection is now starting
1136        setState(Guacamole.Tunnel.State.CONNECTING);
1137
1138        // Start a new connection
1139        xhr = new XMLHttpRequest();
1140        xhr.open('GET', url);
1141        xhr.withCredentials = !!crossDomain;
1142        xhr.responseType = 'text';
1143        xhr.send(null);
1144
1145        var offset = 0;
1146
1147        // Create Guacamole protocol parser specifically for this connection
1148        var parser = new Guacamole.Parser();
1149
1150        // Invoke tunnel's oninstruction handler for each parsed instruction
1151        parser.oninstruction = function instructionReceived(opcode, args) {
1152            if (tunnel.oninstruction)
1153                tunnel.oninstruction(opcode, args);
1154        };
1155
1156        // Continuously parse received data
1157        xhr.onreadystatechange = function readyStateChanged() {
1158
1159            // Parse while data is being received
1160            if (xhr.readyState === 3 || xhr.readyState === 4) {
1161
1162                // Connection is open
1163                setState(Guacamole.Tunnel.State.OPEN);
1164
1165                var buffer = xhr.responseText;
1166                var length = buffer.length;
1167
1168                // Parse only the portion of data which is newly received
1169                if (offset < length) {
1170                    parser.receive(buffer.substring(offset));
1171                    offset = length;
1172                }
1173
1174            }
1175
1176            // Clean up and close when done
1177            if (xhr.readyState === 4)
1178                tunnel.disconnect();
1179
1180        };
1181
1182        // Reset state and close upon error
1183        xhr.onerror = function httpError() {
1184
1185            // Fail if file could not be downloaded via HTTP
1186            if (tunnel.onerror)
1187                tunnel.onerror(new Guacamole.Status(getGuacamoleStatusCode(xhr.status), xhr.statusText));
1188
1189            tunnel.disconnect();
1190        };
1191
1192    };
1193
1194    this.disconnect = function disconnect() {
1195
1196        // Abort and dispose of XHR if a request is in progress
1197        if (xhr) {
1198            xhr.abort();
1199            xhr = null;
1200        }
1201
1202        // Connection is now closed
1203        setState(Guacamole.Tunnel.State.CLOSED);
1204
1205    };
1206
1207};
1208
1209Guacamole.StaticHTTPTunnel.prototype = new Guacamole.Tunnel();
Note: See TracBrowser for help on using the repository browser.