1 /* 2 * Copyright (C) 2013 Glyptodon LLC 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 */ 22 23 var Guacamole = Guacamole || {}; 24 25 /** 26 * Core object providing abstract communication for Guacamole. This object 27 * is a null implementation whose functions do nothing. Guacamole applications 28 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based 29 * on this one. 30 * 31 * @constructor 32 * @see Guacamole.HTTPTunnel 33 */ 34 Guacamole.Tunnel = function() { 35 36 /** 37 * Connect to the tunnel with the given optional data. This data is 38 * typically used for authentication. The format of data accepted is 39 * up to the tunnel implementation. 40 * 41 * @param {String} data The data to send to the tunnel when connecting. 42 */ 43 this.connect = function(data) {}; 44 45 /** 46 * Disconnect from the tunnel. 47 */ 48 this.disconnect = function() {}; 49 50 /** 51 * Send the given message through the tunnel to the service on the other 52 * side. All messages are guaranteed to be received in the order sent. 53 * 54 * @param {...} elements The elements of the message to send to the 55 * service on the other side of the tunnel. 56 */ 57 this.sendMessage = function(elements) {}; 58 59 /** 60 * The current state of this tunnel. 61 * 62 * @type Number 63 */ 64 this.state = Guacamole.Tunnel.State.CONNECTING; 65 66 /** 67 * The maximum amount of time to wait for data to be received, in 68 * milliseconds. If data is not received within this amount of time, 69 * the tunnel is closed with an error. The default value is 15000. 70 * 71 * @type Number 72 */ 73 this.receiveTimeout = 15000; 74 75 /** 76 * Fired whenever an error is encountered by the tunnel. 77 * 78 * @event 79 * @param {Guacamole.Status} status A status object which describes the 80 * error. 81 */ 82 this.onerror = null; 83 84 /** 85 * Fired whenever the state of the tunnel changes. 86 * 87 * @event 88 * @param {Number} state The new state of the client. 89 */ 90 this.onstatechange = null; 91 92 /** 93 * Fired once for every complete Guacamole instruction received, in order. 94 * 95 * @event 96 * @param {String} opcode The Guacamole instruction opcode. 97 * @param {Array} parameters The parameters provided for the instruction, 98 * if any. 99 */ 100 this.oninstruction = null; 101 102 }; 103 104 /** 105 * All possible tunnel states. 106 */ 107 Guacamole.Tunnel.State = { 108 109 /** 110 * A connection is in pending. It is not yet known whether connection was 111 * successful. 112 * 113 * @type Number 114 */ 115 "CONNECTING": 0, 116 117 /** 118 * Connection was successful, and data is being received. 119 * 120 * @type Number 121 */ 122 "OPEN": 1, 123 124 /** 125 * The connection is closed. Connection may not have been successful, the 126 * tunnel may have been explicitly closed by either side, or an error may 127 * have occurred. 128 * 129 * @type Number 130 */ 131 "CLOSED": 2 132 133 }; 134 135 /** 136 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. 137 * 138 * @constructor 139 * @augments Guacamole.Tunnel 140 * 141 * @param {String} tunnelURL 142 * The URL of the HTTP tunneling service. 143 * 144 * @param {Boolean} [crossDomain=false] 145 * Whether tunnel requests will be cross-domain, and thus must use CORS 146 * mechanisms and headers. By default, it is assumed that tunnel requests 147 * will be made to the same domain. 148 */ 149 Guacamole.HTTPTunnel = function(tunnelURL, crossDomain) { 150 151 /** 152 * Reference to this HTTP tunnel. 153 * @private 154 */ 155 var tunnel = this; 156 157 var tunnel_uuid; 158 159 var TUNNEL_CONNECT = tunnelURL + "?connect"; 160 var TUNNEL_READ = tunnelURL + "?read:"; 161 var TUNNEL_WRITE = tunnelURL + "?write:"; 162 163 var POLLING_ENABLED = 1; 164 var POLLING_DISABLED = 0; 165 166 // Default to polling - will be turned off automatically if not needed 167 var pollingMode = POLLING_ENABLED; 168 169 var sendingMessages = false; 170 var outputMessageBuffer = ""; 171 172 // If requests are expected to be cross-domain, the cookie that the HTTP 173 // tunnel depends on will only be sent if withCredentials is true 174 var withCredentials = !!crossDomain; 175 176 /** 177 * The current receive timeout ID, if any. 178 * @private 179 */ 180 var receive_timeout = null; 181 182 /** 183 * Initiates a timeout which, if data is not received, causes the tunnel 184 * to close with an error. 185 * 186 * @private 187 */ 188 function reset_timeout() { 189 190 // Get rid of old timeout (if any) 191 window.clearTimeout(receive_timeout); 192 193 // Set new timeout 194 receive_timeout = window.setTimeout(function () { 195 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); 196 }, tunnel.receiveTimeout); 197 198 } 199 200 /** 201 * Closes this tunnel, signaling the given status and corresponding 202 * message, which will be sent to the onerror handler if the status is 203 * an error status. 204 * 205 * @private 206 * @param {Guacamole.Status} status The status causing the connection to 207 * close; 208 */ 209 function close_tunnel(status) { 210 211 // Ignore if already closed 212 if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 213 return; 214 215 // If connection closed abnormally, signal error. 216 if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) { 217 218 // Ignore RESOURCE_NOT_FOUND if we've already connected, as that 219 // only signals end-of-stream for the HTTP tunnel. 220 if (tunnel.state === Guacamole.Tunnel.State.CONNECTING 221 || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND) 222 tunnel.onerror(status); 223 224 } 225 226 // Mark as closed 227 tunnel.state = Guacamole.Tunnel.State.CLOSED; 228 229 // Reset output message buffer 230 sendingMessages = false; 231 232 if (tunnel.onstatechange) 233 tunnel.onstatechange(tunnel.state); 234 235 } 236 237 238 this.sendMessage = function() { 239 240 // Do not attempt to send messages if not connected 241 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 242 return; 243 244 // Do not attempt to send empty messages 245 if (arguments.length === 0) 246 return; 247 248 /** 249 * Converts the given value to a length/string pair for use as an 250 * element in a Guacamole instruction. 251 * 252 * @private 253 * @param value The value to convert. 254 * @return {String} The converted value. 255 */ 256 function getElement(value) { 257 var string = new String(value); 258 return string.length + "." + string; 259 } 260 261 // Initialized message with first element 262 var message = getElement(arguments[0]); 263 264 // Append remaining elements 265 for (var i=1; i<arguments.length; i++) 266 message += "," + getElement(arguments[i]); 267 268 // Final terminator 269 message += ";"; 270 271 // Add message to buffer 272 outputMessageBuffer += message; 273 274 // Send if not currently sending 275 if (!sendingMessages) 276 sendPendingMessages(); 277 278 }; 279 280 function sendPendingMessages() { 281 282 // Do not attempt to send messages if not connected 283 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 284 return; 285 286 if (outputMessageBuffer.length > 0) { 287 288 sendingMessages = true; 289 290 var message_xmlhttprequest = new XMLHttpRequest(); 291 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); 292 message_xmlhttprequest.withCredentials = withCredentials; 293 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 294 295 // Once response received, send next queued event. 296 message_xmlhttprequest.onreadystatechange = function() { 297 if (message_xmlhttprequest.readyState === 4) { 298 299 // If an error occurs during send, handle it 300 if (message_xmlhttprequest.status !== 200) 301 handleHTTPTunnelError(message_xmlhttprequest); 302 303 // Otherwise, continue the send loop 304 else 305 sendPendingMessages(); 306 307 } 308 }; 309 310 message_xmlhttprequest.send(outputMessageBuffer); 311 outputMessageBuffer = ""; // Clear buffer 312 313 } 314 else 315 sendingMessages = false; 316 317 } 318 319 function handleHTTPTunnelError(xmlhttprequest) { 320 321 var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); 322 var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); 323 324 close_tunnel(new Guacamole.Status(code, message)); 325 326 } 327 328 function handleResponse(xmlhttprequest) { 329 330 var interval = null; 331 var nextRequest = null; 332 333 var dataUpdateEvents = 0; 334 335 // The location of the last element's terminator 336 var elementEnd = -1; 337 338 // Where to start the next length search or the next element 339 var startIndex = 0; 340 341 // Parsed elements 342 var elements = new Array(); 343 344 function parseResponse() { 345 346 // Do not handle responses if not connected 347 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { 348 349 // Clean up interval if polling 350 if (interval !== null) 351 clearInterval(interval); 352 353 return; 354 } 355 356 // Do not parse response yet if not ready 357 if (xmlhttprequest.readyState < 2) return; 358 359 // Attempt to read status 360 var status; 361 try { status = xmlhttprequest.status; } 362 363 // If status could not be read, assume successful. 364 catch (e) { status = 200; } 365 366 // Start next request as soon as possible IF request was successful 367 if (!nextRequest && status === 200) 368 nextRequest = makeRequest(); 369 370 // Parse stream when data is received and when complete. 371 if (xmlhttprequest.readyState === 3 || 372 xmlhttprequest.readyState === 4) { 373 374 reset_timeout(); 375 376 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) 377 if (pollingMode === POLLING_ENABLED) { 378 if (xmlhttprequest.readyState === 3 && !interval) 379 interval = setInterval(parseResponse, 30); 380 else if (xmlhttprequest.readyState === 4 && !interval) 381 clearInterval(interval); 382 } 383 384 // If canceled, stop transfer 385 if (xmlhttprequest.status === 0) { 386 tunnel.disconnect(); 387 return; 388 } 389 390 // Halt on error during request 391 else if (xmlhttprequest.status !== 200) { 392 handleHTTPTunnelError(xmlhttprequest); 393 return; 394 } 395 396 // Attempt to read in-progress data 397 var current; 398 try { current = xmlhttprequest.responseText; } 399 400 // Do not attempt to parse if data could not be read 401 catch (e) { return; } 402 403 // While search is within currently received data 404 while (elementEnd < current.length) { 405 406 // If we are waiting for element data 407 if (elementEnd >= startIndex) { 408 409 // We now have enough data for the element. Parse. 410 var element = current.substring(startIndex, elementEnd); 411 var terminator = current.substring(elementEnd, elementEnd+1); 412 413 // Add element to array 414 elements.push(element); 415 416 // If last element, handle instruction 417 if (terminator === ";") { 418 419 // Get opcode 420 var opcode = elements.shift(); 421 422 // Call instruction handler. 423 if (tunnel.oninstruction) 424 tunnel.oninstruction(opcode, elements); 425 426 // Clear elements 427 elements.length = 0; 428 429 } 430 431 // Start searching for length at character after 432 // element terminator 433 startIndex = elementEnd + 1; 434 435 } 436 437 // Search for end of length 438 var lengthEnd = current.indexOf(".", startIndex); 439 if (lengthEnd !== -1) { 440 441 // Parse length 442 var length = parseInt(current.substring(elementEnd+1, lengthEnd)); 443 444 // If we're done parsing, handle the next response. 445 if (length === 0) { 446 447 // Clean up interval if polling 448 if (!interval) 449 clearInterval(interval); 450 451 // Clean up object 452 xmlhttprequest.onreadystatechange = null; 453 xmlhttprequest.abort(); 454 455 // Start handling next request 456 if (nextRequest) 457 handleResponse(nextRequest); 458 459 // Done parsing 460 break; 461 462 } 463 464 // Calculate start of element 465 startIndex = lengthEnd + 1; 466 467 // Calculate location of element terminator 468 elementEnd = startIndex + length; 469 470 } 471 472 // If no period yet, continue search when more data 473 // is received 474 else { 475 startIndex = current.length; 476 break; 477 } 478 479 } // end parse loop 480 481 } 482 483 } 484 485 // If response polling enabled, attempt to detect if still 486 // necessary (via wrapping parseResponse()) 487 if (pollingMode === POLLING_ENABLED) { 488 xmlhttprequest.onreadystatechange = function() { 489 490 // If we receive two or more readyState==3 events, 491 // there is no need to poll. 492 if (xmlhttprequest.readyState === 3) { 493 dataUpdateEvents++; 494 if (dataUpdateEvents >= 2) { 495 pollingMode = POLLING_DISABLED; 496 xmlhttprequest.onreadystatechange = parseResponse; 497 } 498 } 499 500 parseResponse(); 501 }; 502 } 503 504 // Otherwise, just parse 505 else 506 xmlhttprequest.onreadystatechange = parseResponse; 507 508 parseResponse(); 509 510 } 511 512 /** 513 * Arbitrary integer, unique for each tunnel read request. 514 * @private 515 */ 516 var request_id = 0; 517 518 function makeRequest() { 519 520 // Make request, increment request ID 521 var xmlhttprequest = new XMLHttpRequest(); 522 xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); 523 xmlhttprequest.withCredentials = withCredentials; 524 xmlhttprequest.send(null); 525 526 return xmlhttprequest; 527 528 } 529 530 this.connect = function(data) { 531 532 // Start waiting for connect 533 reset_timeout(); 534 535 // Start tunnel and connect 536 var connect_xmlhttprequest = new XMLHttpRequest(); 537 connect_xmlhttprequest.onreadystatechange = function() { 538 539 if (connect_xmlhttprequest.readyState !== 4) 540 return; 541 542 // If failure, throw error 543 if (connect_xmlhttprequest.status !== 200) { 544 handleHTTPTunnelError(connect_xmlhttprequest); 545 return; 546 } 547 548 reset_timeout(); 549 550 // Get UUID from response 551 tunnel_uuid = connect_xmlhttprequest.responseText; 552 553 tunnel.state = Guacamole.Tunnel.State.OPEN; 554 if (tunnel.onstatechange) 555 tunnel.onstatechange(tunnel.state); 556 557 // Start reading data 558 handleResponse(makeRequest()); 559 560 }; 561 562 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); 563 connect_xmlhttprequest.withCredentials = withCredentials; 564 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 565 connect_xmlhttprequest.send(data); 566 567 }; 568 569 this.disconnect = function() { 570 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 571 }; 572 573 }; 574 575 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); 576 577 /** 578 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. 579 * 580 * @constructor 581 * @augments Guacamole.Tunnel 582 * @param {String} tunnelURL The URL of the WebSocket tunneling service. 583 */ 584 Guacamole.WebSocketTunnel = function(tunnelURL) { 585 586 /** 587 * Reference to this WebSocket tunnel. 588 * @private 589 */ 590 var tunnel = this; 591 592 /** 593 * The WebSocket used by this tunnel. 594 * @private 595 */ 596 var socket = null; 597 598 /** 599 * The current receive timeout ID, if any. 600 * @private 601 */ 602 var receive_timeout = null; 603 604 /** 605 * The WebSocket protocol corresponding to the protocol used for the current 606 * location. 607 * @private 608 */ 609 var ws_protocol = { 610 "http:": "ws:", 611 "https:": "wss:" 612 }; 613 614 // Transform current URL to WebSocket URL 615 616 // If not already a websocket URL 617 if ( tunnelURL.substring(0, 3) !== "ws:" 618 && tunnelURL.substring(0, 4) !== "wss:") { 619 620 var protocol = ws_protocol[window.location.protocol]; 621 622 // If absolute URL, convert to absolute WS URL 623 if (tunnelURL.substring(0, 1) === "/") 624 tunnelURL = 625 protocol 626 + "//" + window.location.host 627 + tunnelURL; 628 629 // Otherwise, construct absolute from relative URL 630 else { 631 632 // Get path from pathname 633 var slash = window.location.pathname.lastIndexOf("/"); 634 var path = window.location.pathname.substring(0, slash + 1); 635 636 // Construct absolute URL 637 tunnelURL = 638 protocol 639 + "//" + window.location.host 640 + path 641 + tunnelURL; 642 643 } 644 645 } 646 647 /** 648 * Initiates a timeout which, if data is not received, causes the tunnel 649 * to close with an error. 650 * 651 * @private 652 */ 653 function reset_timeout() { 654 655 // Get rid of old timeout (if any) 656 window.clearTimeout(receive_timeout); 657 658 // Set new timeout 659 receive_timeout = window.setTimeout(function () { 660 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); 661 }, tunnel.receiveTimeout); 662 663 } 664 665 /** 666 * Closes this tunnel, signaling the given status and corresponding 667 * message, which will be sent to the onerror handler if the status is 668 * an error status. 669 * 670 * @private 671 * @param {Guacamole.Status} status The status causing the connection to 672 * close; 673 */ 674 function close_tunnel(status) { 675 676 // Ignore if already closed 677 if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 678 return; 679 680 // If connection closed abnormally, signal error. 681 if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) 682 tunnel.onerror(status); 683 684 // Mark as closed 685 tunnel.state = Guacamole.Tunnel.State.CLOSED; 686 if (tunnel.onstatechange) 687 tunnel.onstatechange(tunnel.state); 688 689 socket.close(); 690 691 } 692 693 this.sendMessage = function(elements) { 694 695 // Do not attempt to send messages if not connected 696 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 697 return; 698 699 // Do not attempt to send empty messages 700 if (arguments.length === 0) 701 return; 702 703 /** 704 * Converts the given value to a length/string pair for use as an 705 * element in a Guacamole instruction. 706 * 707 * @private 708 * @param value The value to convert. 709 * @return {String} The converted value. 710 */ 711 function getElement(value) { 712 var string = new String(value); 713 return string.length + "." + string; 714 } 715 716 // Initialized message with first element 717 var message = getElement(arguments[0]); 718 719 // Append remaining elements 720 for (var i=1; i<arguments.length; i++) 721 message += "," + getElement(arguments[i]); 722 723 // Final terminator 724 message += ";"; 725 726 socket.send(message); 727 728 }; 729 730 this.connect = function(data) { 731 732 reset_timeout(); 733 734 // Connect socket 735 socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); 736 737 socket.onopen = function(event) { 738 739 reset_timeout(); 740 741 tunnel.state = Guacamole.Tunnel.State.OPEN; 742 if (tunnel.onstatechange) 743 tunnel.onstatechange(tunnel.state); 744 745 }; 746 747 socket.onclose = function(event) { 748 close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason)); 749 }; 750 751 socket.onerror = function(event) { 752 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data)); 753 }; 754 755 socket.onmessage = function(event) { 756 757 reset_timeout(); 758 759 var message = event.data; 760 var startIndex = 0; 761 var elementEnd; 762 763 var elements = []; 764 765 do { 766 767 // Search for end of length 768 var lengthEnd = message.indexOf(".", startIndex); 769 if (lengthEnd !== -1) { 770 771 // Parse length 772 var length = parseInt(message.substring(elementEnd+1, lengthEnd)); 773 774 // Calculate start of element 775 startIndex = lengthEnd + 1; 776 777 // Calculate location of element terminator 778 elementEnd = startIndex + length; 779 780 } 781 782 // If no period, incomplete instruction. 783 else 784 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction.")); 785 786 // We now have enough data for the element. Parse. 787 var element = message.substring(startIndex, elementEnd); 788 var terminator = message.substring(elementEnd, elementEnd+1); 789 790 // Add element to array 791 elements.push(element); 792 793 // If last element, handle instruction 794 if (terminator === ";") { 795 796 // Get opcode 797 var opcode = elements.shift(); 798 799 // Call instruction handler. 800 if (tunnel.oninstruction) 801 tunnel.oninstruction(opcode, elements); 802 803 // Clear elements 804 elements.length = 0; 805 806 } 807 808 // Start searching for length at character after 809 // element terminator 810 startIndex = elementEnd + 1; 811 812 } while (startIndex < message.length); 813 814 }; 815 816 }; 817 818 this.disconnect = function() { 819 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 820 }; 821 822 }; 823 824 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel(); 825 826 /** 827 * Guacamole Tunnel which cycles between all specified tunnels until 828 * no tunnels are left. Another tunnel is used if an error occurs but 829 * no instructions have been received. If an instruction has been 830 * received, or no tunnels remain, the error is passed directly out 831 * through the onerror handler (if defined). 832 * 833 * @constructor 834 * @augments Guacamole.Tunnel 835 * @param {...} tunnel_chain The tunnels to use, in order of priority. 836 */ 837 Guacamole.ChainedTunnel = function(tunnel_chain) { 838 839 /** 840 * Reference to this chained tunnel. 841 * @private 842 */ 843 var chained_tunnel = this; 844 845 /** 846 * Data passed in via connect(), to be used for 847 * wrapped calls to other tunnels' connect() functions. 848 * @private 849 */ 850 var connect_data; 851 852 /** 853 * Array of all tunnels passed to this ChainedTunnel through the 854 * constructor arguments. 855 * @private 856 */ 857 var tunnels = []; 858 859 /** 860 * The tunnel committed via commit_tunnel(), if any, or null if no tunnel 861 * has yet been committed. 862 * 863 * @private 864 * @type Guacamole.Tunnel 865 */ 866 var committedTunnel = null; 867 868 // Load all tunnels into array 869 for (var i=0; i<arguments.length; i++) 870 tunnels.push(arguments[i]); 871 872 /** 873 * Sets the current tunnel. 874 * 875 * @private 876 * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel. 877 */ 878 function attach(tunnel) { 879 880 // Set own functions to tunnel's functions 881 chained_tunnel.disconnect = tunnel.disconnect; 882 chained_tunnel.sendMessage = tunnel.sendMessage; 883 884 /** 885 * Fails the currently-attached tunnel, attaching a new tunnel if 886 * possible. 887 * 888 * @private 889 * @return {Guacamole.Tunnel} The next tunnel, or null if there are no 890 * more tunnels to try. 891 */ 892 function fail_tunnel() { 893 894 // Get next tunnel 895 var next_tunnel = tunnels.shift(); 896 897 // If there IS a next tunnel, try using it. 898 if (next_tunnel) { 899 tunnel.onerror = null; 900 tunnel.oninstruction = null; 901 tunnel.onstatechange = null; 902 attach(next_tunnel); 903 } 904 905 return next_tunnel; 906 907 } 908 909 /** 910 * Use the current tunnel from this point forward. Do not try any more 911 * tunnels, even if the current tunnel fails. 912 * 913 * @private 914 */ 915 function commit_tunnel() { 916 tunnel.onstatechange = chained_tunnel.onstatechange; 917 tunnel.oninstruction = chained_tunnel.oninstruction; 918 tunnel.onerror = chained_tunnel.onerror; 919 committedTunnel = tunnel; 920 } 921 922 // Wrap own onstatechange within current tunnel 923 tunnel.onstatechange = function(state) { 924 925 switch (state) { 926 927 // If open, use this tunnel from this point forward. 928 case Guacamole.Tunnel.State.OPEN: 929 commit_tunnel(); 930 if (chained_tunnel.onstatechange) 931 chained_tunnel.onstatechange(state); 932 break; 933 934 // If closed, mark failure, attempt next tunnel 935 case Guacamole.Tunnel.State.CLOSED: 936 if (!fail_tunnel() && chained_tunnel.onstatechange) 937 chained_tunnel.onstatechange(state); 938 break; 939 940 } 941 942 }; 943 944 // Wrap own oninstruction within current tunnel 945 tunnel.oninstruction = function(opcode, elements) { 946 947 // Accept current tunnel 948 commit_tunnel(); 949 950 // Invoke handler 951 if (chained_tunnel.oninstruction) 952 chained_tunnel.oninstruction(opcode, elements); 953 954 }; 955 956 // Attach next tunnel on error 957 tunnel.onerror = function(status) { 958 959 // Mark failure, attempt next tunnel 960 if (!fail_tunnel() && chained_tunnel.onerror) 961 chained_tunnel.onerror(status); 962 963 }; 964 965 // Attempt connection 966 tunnel.connect(connect_data); 967 968 } 969 970 this.connect = function(data) { 971 972 // Remember connect data 973 connect_data = data; 974 975 // Get committed tunnel if exists or the first tunnel on the list 976 var next_tunnel = committedTunnel ? committedTunnel : tunnels.shift(); 977 978 // Attach first tunnel 979 if (next_tunnel) 980 attach(next_tunnel); 981 982 // If there IS no first tunnel, error 983 else if (chained_tunnel.onerror) 984 chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try."); 985 986 }; 987 988 }; 989 990 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel(); 991