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 * @param {String} tunnelURL The URL of the HTTP tunneling service. 141 */ 142 Guacamole.HTTPTunnel = function(tunnelURL) { 143 144 /** 145 * Reference to this HTTP tunnel. 146 * @private 147 */ 148 var tunnel = this; 149 150 var tunnel_uuid; 151 152 var TUNNEL_CONNECT = tunnelURL + "?connect"; 153 var TUNNEL_READ = tunnelURL + "?read:"; 154 var TUNNEL_WRITE = tunnelURL + "?write:"; 155 156 var POLLING_ENABLED = 1; 157 var POLLING_DISABLED = 0; 158 159 // Default to polling - will be turned off automatically if not needed 160 var pollingMode = POLLING_ENABLED; 161 162 var sendingMessages = false; 163 var outputMessageBuffer = ""; 164 165 /** 166 * The current receive timeout ID, if any. 167 * @private 168 */ 169 var receive_timeout = null; 170 171 /** 172 * Initiates a timeout which, if data is not received, causes the tunnel 173 * to close with an error. 174 * 175 * @private 176 */ 177 function reset_timeout() { 178 179 // Get rid of old timeout (if any) 180 window.clearTimeout(receive_timeout); 181 182 // Set new timeout 183 receive_timeout = window.setTimeout(function () { 184 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); 185 }, tunnel.receiveTimeout); 186 187 } 188 189 /** 190 * Closes this tunnel, signaling the given status and corresponding 191 * message, which will be sent to the onerror handler if the status is 192 * an error status. 193 * 194 * @private 195 * @param {Guacamole.Status} status The status causing the connection to 196 * close; 197 */ 198 function close_tunnel(status) { 199 200 // Ignore if already closed 201 if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 202 return; 203 204 // If connection closed abnormally, signal error. 205 if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) { 206 207 // Ignore RESOURCE_NOT_FOUND if we've already connected, as that 208 // only signals end-of-stream for the HTTP tunnel. 209 if (tunnel.state === Guacamole.Tunnel.State.CONNECTING 210 || status.code !== Guacamole.Status.Code.RESOURCE_NOT_FOUND) 211 tunnel.onerror(status); 212 213 } 214 215 // Mark as closed 216 tunnel.state = Guacamole.Tunnel.State.CLOSED; 217 218 // Reset output message buffer 219 sendingMessages = false; 220 221 if (tunnel.onstatechange) 222 tunnel.onstatechange(tunnel.state); 223 224 } 225 226 227 this.sendMessage = function() { 228 229 // Do not attempt to send messages if not connected 230 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 231 return; 232 233 // Do not attempt to send empty messages 234 if (arguments.length === 0) 235 return; 236 237 /** 238 * Converts the given value to a length/string pair for use as an 239 * element in a Guacamole instruction. 240 * 241 * @private 242 * @param value The value to convert. 243 * @return {String} The converted value. 244 */ 245 function getElement(value) { 246 var string = new String(value); 247 return string.length + "." + string; 248 } 249 250 // Initialized message with first element 251 var message = getElement(arguments[0]); 252 253 // Append remaining elements 254 for (var i=1; i<arguments.length; i++) 255 message += "," + getElement(arguments[i]); 256 257 // Final terminator 258 message += ";"; 259 260 // Add message to buffer 261 outputMessageBuffer += message; 262 263 // Send if not currently sending 264 if (!sendingMessages) 265 sendPendingMessages(); 266 267 }; 268 269 function sendPendingMessages() { 270 271 // Do not attempt to send messages if not connected 272 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 273 return; 274 275 if (outputMessageBuffer.length > 0) { 276 277 sendingMessages = true; 278 279 var message_xmlhttprequest = new XMLHttpRequest(); 280 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); 281 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 282 283 // Once response received, send next queued event. 284 message_xmlhttprequest.onreadystatechange = function() { 285 if (message_xmlhttprequest.readyState === 4) { 286 287 // If an error occurs during send, handle it 288 if (message_xmlhttprequest.status !== 200) 289 handleHTTPTunnelError(message_xmlhttprequest); 290 291 // Otherwise, continue the send loop 292 else 293 sendPendingMessages(); 294 295 } 296 }; 297 298 message_xmlhttprequest.send(outputMessageBuffer); 299 outputMessageBuffer = ""; // Clear buffer 300 301 } 302 else 303 sendingMessages = false; 304 305 } 306 307 function handleHTTPTunnelError(xmlhttprequest) { 308 309 var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code")); 310 var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message"); 311 312 close_tunnel(new Guacamole.Status(code, message)); 313 314 } 315 316 function handleResponse(xmlhttprequest) { 317 318 var interval = null; 319 var nextRequest = null; 320 321 var dataUpdateEvents = 0; 322 323 // The location of the last element's terminator 324 var elementEnd = -1; 325 326 // Where to start the next length search or the next element 327 var startIndex = 0; 328 329 // Parsed elements 330 var elements = new Array(); 331 332 function parseResponse() { 333 334 // Do not handle responses if not connected 335 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) { 336 337 // Clean up interval if polling 338 if (interval !== null) 339 clearInterval(interval); 340 341 return; 342 } 343 344 // Do not parse response yet if not ready 345 if (xmlhttprequest.readyState < 2) return; 346 347 // Attempt to read status 348 var status; 349 try { status = xmlhttprequest.status; } 350 351 // If status could not be read, assume successful. 352 catch (e) { status = 200; } 353 354 // Start next request as soon as possible IF request was successful 355 if (!nextRequest && status === 200) 356 nextRequest = makeRequest(); 357 358 // Parse stream when data is received and when complete. 359 if (xmlhttprequest.readyState === 3 || 360 xmlhttprequest.readyState === 4) { 361 362 reset_timeout(); 363 364 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) 365 if (pollingMode === POLLING_ENABLED) { 366 if (xmlhttprequest.readyState === 3 && !interval) 367 interval = setInterval(parseResponse, 30); 368 else if (xmlhttprequest.readyState === 4 && !interval) 369 clearInterval(interval); 370 } 371 372 // If canceled, stop transfer 373 if (xmlhttprequest.status === 0) { 374 tunnel.disconnect(); 375 return; 376 } 377 378 // Halt on error during request 379 else if (xmlhttprequest.status !== 200) { 380 handleHTTPTunnelError(xmlhttprequest); 381 return; 382 } 383 384 // Attempt to read in-progress data 385 var current; 386 try { current = xmlhttprequest.responseText; } 387 388 // Do not attempt to parse if data could not be read 389 catch (e) { return; } 390 391 // While search is within currently received data 392 while (elementEnd < current.length) { 393 394 // If we are waiting for element data 395 if (elementEnd >= startIndex) { 396 397 // We now have enough data for the element. Parse. 398 var element = current.substring(startIndex, elementEnd); 399 var terminator = current.substring(elementEnd, elementEnd+1); 400 401 // Add element to array 402 elements.push(element); 403 404 // If last element, handle instruction 405 if (terminator === ";") { 406 407 // Get opcode 408 var opcode = elements.shift(); 409 410 // Call instruction handler. 411 if (tunnel.oninstruction) 412 tunnel.oninstruction(opcode, elements); 413 414 // Clear elements 415 elements.length = 0; 416 417 } 418 419 // Start searching for length at character after 420 // element terminator 421 startIndex = elementEnd + 1; 422 423 } 424 425 // Search for end of length 426 var lengthEnd = current.indexOf(".", startIndex); 427 if (lengthEnd !== -1) { 428 429 // Parse length 430 var length = parseInt(current.substring(elementEnd+1, lengthEnd)); 431 432 // If we're done parsing, handle the next response. 433 if (length === 0) { 434 435 // Clean up interval if polling 436 if (!interval) 437 clearInterval(interval); 438 439 // Clean up object 440 xmlhttprequest.onreadystatechange = null; 441 xmlhttprequest.abort(); 442 443 // Start handling next request 444 if (nextRequest) 445 handleResponse(nextRequest); 446 447 // Done parsing 448 break; 449 450 } 451 452 // Calculate start of element 453 startIndex = lengthEnd + 1; 454 455 // Calculate location of element terminator 456 elementEnd = startIndex + length; 457 458 } 459 460 // If no period yet, continue search when more data 461 // is received 462 else { 463 startIndex = current.length; 464 break; 465 } 466 467 } // end parse loop 468 469 } 470 471 } 472 473 // If response polling enabled, attempt to detect if still 474 // necessary (via wrapping parseResponse()) 475 if (pollingMode === POLLING_ENABLED) { 476 xmlhttprequest.onreadystatechange = function() { 477 478 // If we receive two or more readyState==3 events, 479 // there is no need to poll. 480 if (xmlhttprequest.readyState === 3) { 481 dataUpdateEvents++; 482 if (dataUpdateEvents >= 2) { 483 pollingMode = POLLING_DISABLED; 484 xmlhttprequest.onreadystatechange = parseResponse; 485 } 486 } 487 488 parseResponse(); 489 }; 490 } 491 492 // Otherwise, just parse 493 else 494 xmlhttprequest.onreadystatechange = parseResponse; 495 496 parseResponse(); 497 498 } 499 500 /** 501 * Arbitrary integer, unique for each tunnel read request. 502 * @private 503 */ 504 var request_id = 0; 505 506 function makeRequest() { 507 508 // Make request, increment request ID 509 var xmlhttprequest = new XMLHttpRequest(); 510 xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); 511 xmlhttprequest.send(null); 512 513 return xmlhttprequest; 514 515 } 516 517 this.connect = function(data) { 518 519 // Start waiting for connect 520 reset_timeout(); 521 522 // Start tunnel and connect 523 var connect_xmlhttprequest = new XMLHttpRequest(); 524 connect_xmlhttprequest.onreadystatechange = function() { 525 526 if (connect_xmlhttprequest.readyState !== 4) 527 return; 528 529 // If failure, throw error 530 if (connect_xmlhttprequest.status !== 200) { 531 handleHTTPTunnelError(connect_xmlhttprequest); 532 return; 533 } 534 535 reset_timeout(); 536 537 // Get UUID from response 538 tunnel_uuid = connect_xmlhttprequest.responseText; 539 540 tunnel.state = Guacamole.Tunnel.State.OPEN; 541 if (tunnel.onstatechange) 542 tunnel.onstatechange(tunnel.state); 543 544 // Start reading data 545 handleResponse(makeRequest()); 546 547 }; 548 549 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, true); 550 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 551 connect_xmlhttprequest.send(data); 552 553 }; 554 555 this.disconnect = function() { 556 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 557 }; 558 559 }; 560 561 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); 562 563 /** 564 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. 565 * 566 * @constructor 567 * @augments Guacamole.Tunnel 568 * @param {String} tunnelURL The URL of the WebSocket tunneling service. 569 */ 570 Guacamole.WebSocketTunnel = function(tunnelURL) { 571 572 /** 573 * Reference to this WebSocket tunnel. 574 * @private 575 */ 576 var tunnel = this; 577 578 /** 579 * The WebSocket used by this tunnel. 580 * @private 581 */ 582 var socket = null; 583 584 /** 585 * The current receive timeout ID, if any. 586 * @private 587 */ 588 var receive_timeout = null; 589 590 /** 591 * The WebSocket protocol corresponding to the protocol used for the current 592 * location. 593 * @private 594 */ 595 var ws_protocol = { 596 "http:": "ws:", 597 "https:": "wss:" 598 }; 599 600 // Transform current URL to WebSocket URL 601 602 // If not already a websocket URL 603 if ( tunnelURL.substring(0, 3) !== "ws:" 604 && tunnelURL.substring(0, 4) !== "wss:") { 605 606 var protocol = ws_protocol[window.location.protocol]; 607 608 // If absolute URL, convert to absolute WS URL 609 if (tunnelURL.substring(0, 1) === "/") 610 tunnelURL = 611 protocol 612 + "//" + window.location.host 613 + tunnelURL; 614 615 // Otherwise, construct absolute from relative URL 616 else { 617 618 // Get path from pathname 619 var slash = window.location.pathname.lastIndexOf("/"); 620 var path = window.location.pathname.substring(0, slash + 1); 621 622 // Construct absolute URL 623 tunnelURL = 624 protocol 625 + "//" + window.location.host 626 + path 627 + tunnelURL; 628 629 } 630 631 } 632 633 /** 634 * Initiates a timeout which, if data is not received, causes the tunnel 635 * to close with an error. 636 * 637 * @private 638 */ 639 function reset_timeout() { 640 641 // Get rid of old timeout (if any) 642 window.clearTimeout(receive_timeout); 643 644 // Set new timeout 645 receive_timeout = window.setTimeout(function () { 646 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout.")); 647 }, tunnel.receiveTimeout); 648 649 } 650 651 /** 652 * Closes this tunnel, signaling the given status and corresponding 653 * message, which will be sent to the onerror handler if the status is 654 * an error status. 655 * 656 * @private 657 * @param {Guacamole.Status} status The status causing the connection to 658 * close; 659 */ 660 function close_tunnel(status) { 661 662 // Ignore if already closed 663 if (tunnel.state === Guacamole.Tunnel.State.CLOSED) 664 return; 665 666 // If connection closed abnormally, signal error. 667 if (status.code !== Guacamole.Status.Code.SUCCESS && tunnel.onerror) 668 tunnel.onerror(status); 669 670 // Mark as closed 671 tunnel.state = Guacamole.Tunnel.State.CLOSED; 672 if (tunnel.onstatechange) 673 tunnel.onstatechange(tunnel.state); 674 675 socket.close(); 676 677 } 678 679 this.sendMessage = function(elements) { 680 681 // Do not attempt to send messages if not connected 682 if (tunnel.state !== Guacamole.Tunnel.State.OPEN) 683 return; 684 685 // Do not attempt to send empty messages 686 if (arguments.length === 0) 687 return; 688 689 /** 690 * Converts the given value to a length/string pair for use as an 691 * element in a Guacamole instruction. 692 * 693 * @private 694 * @param value The value to convert. 695 * @return {String} The converted value. 696 */ 697 function getElement(value) { 698 var string = new String(value); 699 return string.length + "." + string; 700 } 701 702 // Initialized message with first element 703 var message = getElement(arguments[0]); 704 705 // Append remaining elements 706 for (var i=1; i<arguments.length; i++) 707 message += "," + getElement(arguments[i]); 708 709 // Final terminator 710 message += ";"; 711 712 socket.send(message); 713 714 }; 715 716 this.connect = function(data) { 717 718 reset_timeout(); 719 720 // Connect socket 721 socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); 722 723 socket.onopen = function(event) { 724 725 reset_timeout(); 726 727 tunnel.state = Guacamole.Tunnel.State.OPEN; 728 if (tunnel.onstatechange) 729 tunnel.onstatechange(tunnel.state); 730 731 }; 732 733 socket.onclose = function(event) { 734 close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason)); 735 }; 736 737 socket.onerror = function(event) { 738 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data)); 739 }; 740 741 socket.onmessage = function(event) { 742 743 reset_timeout(); 744 745 var message = event.data; 746 var startIndex = 0; 747 var elementEnd; 748 749 var elements = []; 750 751 do { 752 753 // Search for end of length 754 var lengthEnd = message.indexOf(".", startIndex); 755 if (lengthEnd !== -1) { 756 757 // Parse length 758 var length = parseInt(message.substring(elementEnd+1, lengthEnd)); 759 760 // Calculate start of element 761 startIndex = lengthEnd + 1; 762 763 // Calculate location of element terminator 764 elementEnd = startIndex + length; 765 766 } 767 768 // If no period, incomplete instruction. 769 else 770 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, "Incomplete instruction.")); 771 772 // We now have enough data for the element. Parse. 773 var element = message.substring(startIndex, elementEnd); 774 var terminator = message.substring(elementEnd, elementEnd+1); 775 776 // Add element to array 777 elements.push(element); 778 779 // If last element, handle instruction 780 if (terminator === ";") { 781 782 // Get opcode 783 var opcode = elements.shift(); 784 785 // Call instruction handler. 786 if (tunnel.oninstruction) 787 tunnel.oninstruction(opcode, elements); 788 789 // Clear elements 790 elements.length = 0; 791 792 } 793 794 // Start searching for length at character after 795 // element terminator 796 startIndex = elementEnd + 1; 797 798 } while (startIndex < message.length); 799 800 }; 801 802 }; 803 804 this.disconnect = function() { 805 close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SUCCESS, "Manually closed.")); 806 }; 807 808 }; 809 810 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel(); 811 812 /** 813 * Guacamole Tunnel which cycles between all specified tunnels until 814 * no tunnels are left. Another tunnel is used if an error occurs but 815 * no instructions have been received. If an instruction has been 816 * received, or no tunnels remain, the error is passed directly out 817 * through the onerror handler (if defined). 818 * 819 * @constructor 820 * @augments Guacamole.Tunnel 821 * @param {...} tunnel_chain The tunnels to use, in order of priority. 822 */ 823 Guacamole.ChainedTunnel = function(tunnel_chain) { 824 825 /** 826 * Reference to this chained tunnel. 827 * @private 828 */ 829 var chained_tunnel = this; 830 831 /** 832 * Data passed in via connect(), to be used for 833 * wrapped calls to other tunnels' connect() functions. 834 * @private 835 */ 836 var connect_data; 837 838 /** 839 * Array of all tunnels passed to this ChainedTunnel through the 840 * constructor arguments. 841 * @private 842 */ 843 var tunnels = []; 844 845 // Load all tunnels into array 846 for (var i=0; i<arguments.length; i++) 847 tunnels.push(arguments[i]); 848 849 /** 850 * Sets the current tunnel. 851 * 852 * @private 853 * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel. 854 */ 855 function attach(tunnel) { 856 857 // Set own functions to tunnel's functions 858 chained_tunnel.disconnect = tunnel.disconnect; 859 chained_tunnel.sendMessage = tunnel.sendMessage; 860 861 /** 862 * Fails the currently-attached tunnel, attaching a new tunnel if 863 * possible. 864 * 865 * @private 866 * @return {Guacamole.Tunnel} The next tunnel, or null if there are no 867 * more tunnels to try. 868 */ 869 function fail_tunnel() { 870 871 // Get next tunnel 872 var next_tunnel = tunnels.shift(); 873 874 // If there IS a next tunnel, try using it. 875 if (next_tunnel) { 876 tunnel.onerror = null; 877 tunnel.oninstruction = null; 878 tunnel.onstatechange = null; 879 attach(next_tunnel); 880 } 881 882 return next_tunnel; 883 884 } 885 886 /** 887 * Use the current tunnel from this point forward. Do not try any more 888 * tunnels, even if the current tunnel fails. 889 * 890 * @private 891 */ 892 function commit_tunnel() { 893 tunnel.onstatechange = chained_tunnel.onstatechange; 894 tunnel.oninstruction = chained_tunnel.oninstruction; 895 tunnel.onerror = chained_tunnel.onerror; 896 } 897 898 // Wrap own onstatechange within current tunnel 899 tunnel.onstatechange = function(state) { 900 901 switch (state) { 902 903 // If open, use this tunnel from this point forward. 904 case Guacamole.Tunnel.State.OPEN: 905 commit_tunnel(); 906 if (chained_tunnel.onstatechange) 907 chained_tunnel.onstatechange(state); 908 break; 909 910 // If closed, mark failure, attempt next tunnel 911 case Guacamole.Tunnel.State.CLOSED: 912 if (!fail_tunnel() && chained_tunnel.onstatechange) 913 chained_tunnel.onstatechange(state); 914 break; 915 916 } 917 918 }; 919 920 // Wrap own oninstruction within current tunnel 921 tunnel.oninstruction = function(opcode, elements) { 922 923 // Accept current tunnel 924 commit_tunnel(); 925 926 // Invoke handler 927 if (chained_tunnel.oninstruction) 928 chained_tunnel.oninstruction(opcode, elements); 929 930 }; 931 932 // Attach next tunnel on error 933 tunnel.onerror = function(status) { 934 935 // Mark failure, attempt next tunnel 936 if (!fail_tunnel() && chained_tunnel.onerror) 937 chained_tunnel.onerror(status); 938 939 }; 940 941 // Attempt connection 942 tunnel.connect(connect_data); 943 944 } 945 946 this.connect = function(data) { 947 948 // Remember connect data 949 connect_data = data; 950 951 // Get first tunnel 952 var next_tunnel = tunnels.shift(); 953 954 // Attach first tunnel 955 if (next_tunnel) 956 attach(next_tunnel); 957 958 // If there IS no first tunnel, error 959 else if (chained_tunnel.onerror) 960 chained_tunnel.onerror(Guacamole.Status.Code.SERVER_ERROR, "No tunnels to try."); 961 962 }; 963 964 }; 965 966 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel(); 967