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