1 2 /* ***** BEGIN LICENSE BLOCK ***** 3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 4 * 5 * The contents of this file are subject to the Mozilla Public License Version 6 * 1.1 (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * http://www.mozilla.org/MPL/ 9 * 10 * Software distributed under the License is distributed on an "AS IS" basis, 11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 12 * for the specific language governing rights and limitations under the 13 * License. 14 * 15 * The Original Code is guacamole-common-js. 16 * 17 * The Initial Developer of the Original Code is 18 * Michael Jumper. 19 * Portions created by the Initial Developer are Copyright (C) 2010 20 * the Initial Developer. All Rights Reserved. 21 * 22 * Contributor(s): 23 * 24 * Alternatively, the contents of this file may be used under the terms of 25 * either the GNU General Public License Version 2 or later (the "GPL"), or 26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 27 * in which case the provisions of the GPL or the LGPL are applicable instead 28 * of those above. If you wish to allow use of your version of this file only 29 * under the terms of either the GPL or the LGPL, and not to allow others to 30 * use your version of this file under the terms of the MPL, indicate your 31 * decision by deleting the provisions above and replace them with the notice 32 * and other provisions required by the GPL or the LGPL. If you do not delete 33 * the provisions above, a recipient may use your version of this file under 34 * the terms of any one of the MPL, the GPL or the LGPL. 35 * 36 * ***** END LICENSE BLOCK ***** */ 37 38 /** 39 * Namespace for all Guacamole JavaScript objects. 40 * @namespace 41 */ 42 var Guacamole = Guacamole || {}; 43 44 /** 45 * Core object providing abstract communication for Guacamole. This object 46 * is a null implementation whose functions do nothing. Guacamole applications 47 * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based 48 * on this one. 49 * 50 * @constructor 51 * @see Guacamole.HTTPTunnel 52 */ 53 Guacamole.Tunnel = function() { 54 55 /** 56 * Connect to the tunnel with the given optional data. This data is 57 * typically used for authentication. The format of data accepted is 58 * up to the tunnel implementation. 59 * 60 * @param {String} data The data to send to the tunnel when connecting. 61 */ 62 this.connect = function(data) {}; 63 64 /** 65 * Disconnect from the tunnel. 66 */ 67 this.disconnect = function() {}; 68 69 /** 70 * Send the given message through the tunnel to the service on the other 71 * side. All messages are guaranteed to be received in the order sent. 72 * 73 * @param {...} elements The elements of the message to send to the 74 * service on the other side of the tunnel. 75 */ 76 this.sendMessage = function(elements) {}; 77 78 /** 79 * Fired whenever an error is encountered by the tunnel. 80 * 81 * @event 82 * @param {String} message A human-readable description of the error that 83 * occurred. 84 */ 85 this.onerror = null; 86 87 /** 88 * Fired once for every complete Guacamole instruction received, in order. 89 * 90 * @event 91 * @param {String} opcode The Guacamole instruction opcode. 92 * @param {Array} parameters The parameters provided for the instruction, 93 * if any. 94 */ 95 this.oninstruction = null; 96 97 }; 98 99 /** 100 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest. 101 * 102 * @constructor 103 * @augments Guacamole.Tunnel 104 * @param {String} tunnelURL The URL of the HTTP tunneling service. 105 */ 106 Guacamole.HTTPTunnel = function(tunnelURL) { 107 108 /** 109 * Reference to this HTTP tunnel. 110 * @private 111 */ 112 var tunnel = this; 113 114 var tunnel_uuid; 115 116 var TUNNEL_CONNECT = tunnelURL + "?connect"; 117 var TUNNEL_READ = tunnelURL + "?read:"; 118 var TUNNEL_WRITE = tunnelURL + "?write:"; 119 120 var STATE_IDLE = 0; 121 var STATE_CONNECTED = 1; 122 var STATE_DISCONNECTED = 2; 123 124 var currentState = STATE_IDLE; 125 126 var POLLING_ENABLED = 1; 127 var POLLING_DISABLED = 0; 128 129 // Default to polling - will be turned off automatically if not needed 130 var pollingMode = POLLING_ENABLED; 131 132 var sendingMessages = false; 133 var outputMessageBuffer = ""; 134 135 this.sendMessage = function() { 136 137 // Do not attempt to send messages if not connected 138 if (currentState != STATE_CONNECTED) 139 return; 140 141 // Do not attempt to send empty messages 142 if (arguments.length == 0) 143 return; 144 145 /** 146 * Converts the given value to a length/string pair for use as an 147 * element in a Guacamole instruction. 148 * 149 * @private 150 * @param value The value to convert. 151 * @return {String} The converted value. 152 */ 153 function getElement(value) { 154 var string = new String(value); 155 return string.length + "." + string; 156 } 157 158 // Initialized message with first element 159 var message = getElement(arguments[0]); 160 161 // Append remaining elements 162 for (var i=1; i<arguments.length; i++) 163 message += "," + getElement(arguments[i]); 164 165 // Final terminator 166 message += ";"; 167 168 // Add message to buffer 169 outputMessageBuffer += message; 170 171 // Send if not currently sending 172 if (!sendingMessages) 173 sendPendingMessages(); 174 175 }; 176 177 function sendPendingMessages() { 178 179 if (outputMessageBuffer.length > 0) { 180 181 sendingMessages = true; 182 183 var message_xmlhttprequest = new XMLHttpRequest(); 184 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); 185 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 186 187 // Once response received, send next queued event. 188 message_xmlhttprequest.onreadystatechange = function() { 189 if (message_xmlhttprequest.readyState == 4) { 190 191 // If an error occurs during send, handle it 192 if (message_xmlhttprequest.status != 200) 193 handleHTTPTunnelError(message_xmlhttprequest); 194 195 // Otherwise, continue the send loop 196 else 197 sendPendingMessages(); 198 199 } 200 } 201 202 message_xmlhttprequest.send(outputMessageBuffer); 203 outputMessageBuffer = ""; // Clear buffer 204 205 } 206 else 207 sendingMessages = false; 208 209 } 210 211 function getHTTPTunnelErrorMessage(xmlhttprequest) { 212 213 var status = xmlhttprequest.status; 214 215 // Special cases 216 if (status == 0) return "Disconnected"; 217 if (status == 200) return "Success"; 218 if (status == 403) return "Unauthorized"; 219 if (status == 404) return "Connection closed"; /* While it may be more 220 * accurate to say the 221 * connection does not 222 * exist, it is confusing 223 * to the user. 224 * 225 * In general, this error 226 * will only happen when 227 * the tunnel does not 228 * exist, which happens 229 * after the connection 230 * is closed and the 231 * tunnel is detached. 232 */ 233 // Internal server errors 234 if (status >= 500 && status <= 599) return "Server error"; 235 236 // Otherwise, unknown 237 return "Unknown error"; 238 239 } 240 241 function handleHTTPTunnelError(xmlhttprequest) { 242 243 // Get error message 244 var message = getHTTPTunnelErrorMessage(xmlhttprequest); 245 246 // Call error handler 247 if (tunnel.onerror) tunnel.onerror(message); 248 249 // Finish 250 tunnel.disconnect(); 251 252 } 253 254 255 function handleResponse(xmlhttprequest) { 256 257 var interval = null; 258 var nextRequest = null; 259 260 var dataUpdateEvents = 0; 261 262 // The location of the last element's terminator 263 var elementEnd = -1; 264 265 // Where to start the next length search or the next element 266 var startIndex = 0; 267 268 // Parsed elements 269 var elements = new Array(); 270 271 function parseResponse() { 272 273 // Do not handle responses if not connected 274 if (currentState != STATE_CONNECTED) { 275 276 // Clean up interval if polling 277 if (interval != null) 278 clearInterval(interval); 279 280 return; 281 } 282 283 // Do not parse response yet if not ready 284 if (xmlhttprequest.readyState < 2) return; 285 286 // Attempt to read status 287 var status; 288 try { status = xmlhttprequest.status; } 289 290 // If status could not be read, assume successful. 291 catch (e) { status = 200; } 292 293 // Start next request as soon as possible IF request was successful 294 if (nextRequest == null && status == 200) 295 nextRequest = makeRequest(); 296 297 // Parse stream when data is received and when complete. 298 if (xmlhttprequest.readyState == 3 || 299 xmlhttprequest.readyState == 4) { 300 301 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) 302 if (pollingMode == POLLING_ENABLED) { 303 if (xmlhttprequest.readyState == 3 && interval == null) 304 interval = setInterval(parseResponse, 30); 305 else if (xmlhttprequest.readyState == 4 && interval != null) 306 clearInterval(interval); 307 } 308 309 // If canceled, stop transfer 310 if (xmlhttprequest.status == 0) { 311 tunnel.disconnect(); 312 return; 313 } 314 315 // Halt on error during request 316 else if (xmlhttprequest.status != 200) { 317 handleHTTPTunnelError(xmlhttprequest); 318 return; 319 } 320 321 // Attempt to read in-progress data 322 var current; 323 try { current = xmlhttprequest.responseText; } 324 325 // Do not attempt to parse if data could not be read 326 catch (e) { return; } 327 328 // While search is within currently received data 329 while (elementEnd < current.length) { 330 331 // If we are waiting for element data 332 if (elementEnd >= startIndex) { 333 334 // We now have enough data for the element. Parse. 335 var element = current.substring(startIndex, elementEnd); 336 var terminator = current.substring(elementEnd, elementEnd+1); 337 338 // Add element to array 339 elements.push(element); 340 341 // If last element, handle instruction 342 if (terminator == ";") { 343 344 // Get opcode 345 var opcode = elements.shift(); 346 347 // Call instruction handler. 348 if (tunnel.oninstruction != null) 349 tunnel.oninstruction(opcode, elements); 350 351 // Clear elements 352 elements.length = 0; 353 354 } 355 356 // Start searching for length at character after 357 // element terminator 358 startIndex = elementEnd + 1; 359 360 } 361 362 // Search for end of length 363 var lengthEnd = current.indexOf(".", startIndex); 364 if (lengthEnd != -1) { 365 366 // Parse length 367 var length = parseInt(current.substring(elementEnd+1, lengthEnd)); 368 369 // If we're done parsing, handle the next response. 370 if (length == 0) { 371 372 // Clean up interval if polling 373 if (interval != null) 374 clearInterval(interval); 375 376 // Clean up object 377 xmlhttprequest.onreadystatechange = null; 378 xmlhttprequest.abort(); 379 380 // Start handling next request 381 if (nextRequest) 382 handleResponse(nextRequest); 383 384 // Done parsing 385 break; 386 387 } 388 389 // Calculate start of element 390 startIndex = lengthEnd + 1; 391 392 // Calculate location of element terminator 393 elementEnd = startIndex + length; 394 395 } 396 397 // If no period yet, continue search when more data 398 // is received 399 else { 400 startIndex = current.length; 401 break; 402 } 403 404 } // end parse loop 405 406 } 407 408 } 409 410 // If response polling enabled, attempt to detect if still 411 // necessary (via wrapping parseResponse()) 412 if (pollingMode == POLLING_ENABLED) { 413 xmlhttprequest.onreadystatechange = function() { 414 415 // If we receive two or more readyState==3 events, 416 // there is no need to poll. 417 if (xmlhttprequest.readyState == 3) { 418 dataUpdateEvents++; 419 if (dataUpdateEvents >= 2) { 420 pollingMode = POLLING_DISABLED; 421 xmlhttprequest.onreadystatechange = parseResponse; 422 } 423 } 424 425 parseResponse(); 426 } 427 } 428 429 // Otherwise, just parse 430 else 431 xmlhttprequest.onreadystatechange = parseResponse; 432 433 parseResponse(); 434 435 } 436 437 /** 438 * Arbitrary integer, unique for each tunnel read request. 439 * @private 440 */ 441 var request_id = 0; 442 443 function makeRequest() { 444 445 // Make request, increment request ID 446 var xmlhttprequest = new XMLHttpRequest(); 447 xmlhttprequest.open("GET", TUNNEL_READ + tunnel_uuid + ":" + (request_id++)); 448 xmlhttprequest.send(null); 449 450 return xmlhttprequest; 451 452 } 453 454 this.connect = function(data) { 455 456 // Start tunnel and connect synchronously 457 var connect_xmlhttprequest = new XMLHttpRequest(); 458 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false); 459 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8"); 460 connect_xmlhttprequest.send(data); 461 462 // If failure, throw error 463 if (connect_xmlhttprequest.status != 200) { 464 var message = getHTTPTunnelErrorMessage(connect_xmlhttprequest); 465 throw new Error(message); 466 } 467 468 // Get UUID from response 469 tunnel_uuid = connect_xmlhttprequest.responseText; 470 471 // Start reading data 472 currentState = STATE_CONNECTED; 473 handleResponse(makeRequest()); 474 475 }; 476 477 this.disconnect = function() { 478 currentState = STATE_DISCONNECTED; 479 }; 480 481 }; 482 483 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel(); 484 485 486 /** 487 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest. 488 * 489 * @constructor 490 * @augments Guacamole.Tunnel 491 * @param {String} tunnelURL The URL of the WebSocket tunneling service. 492 */ 493 Guacamole.WebSocketTunnel = function(tunnelURL) { 494 495 /** 496 * Reference to this WebSocket tunnel. 497 * @private 498 */ 499 var tunnel = this; 500 501 /** 502 * The WebSocket used by this tunnel. 503 * @private 504 */ 505 var socket = null; 506 507 /** 508 * The WebSocket protocol corresponding to the protocol used for the current 509 * location. 510 * @private 511 */ 512 var ws_protocol = { 513 "http:": "ws:", 514 "https:": "wss:" 515 }; 516 517 var status_code = { 518 1000: "Connection closed normally.", 519 1001: "Connection shut down.", 520 1002: "Protocol error.", 521 1003: "Invalid data.", 522 1004: "[UNKNOWN, RESERVED]", 523 1005: "No status code present.", 524 1006: "Connection closed abnormally.", 525 1007: "Inconsistent data type.", 526 1008: "Policy violation.", 527 1009: "Message too large.", 528 1010: "Extension negotiation failed." 529 }; 530 531 var STATE_IDLE = 0; 532 var STATE_CONNECTED = 1; 533 var STATE_DISCONNECTED = 2; 534 535 var currentState = STATE_IDLE; 536 537 // Transform current URL to WebSocket URL 538 539 // If not already a websocket URL 540 if ( tunnelURL.substring(0, 3) != "ws:" 541 && tunnelURL.substring(0, 4) != "wss:") { 542 543 var protocol = ws_protocol[window.location.protocol]; 544 545 // If absolute URL, convert to absolute WS URL 546 if (tunnelURL.substring(0, 1) == "/") 547 tunnelURL = 548 protocol 549 + "//" + window.location.host 550 + tunnelURL; 551 552 // Otherwise, construct absolute from relative URL 553 else { 554 555 // Get path from pathname 556 var slash = window.location.pathname.lastIndexOf("/"); 557 var path = window.location.pathname.substring(0, slash + 1); 558 559 // Construct absolute URL 560 tunnelURL = 561 protocol 562 + "//" + window.location.host 563 + path 564 + tunnelURL; 565 566 } 567 568 } 569 570 this.sendMessage = function(elements) { 571 572 // Do not attempt to send messages if not connected 573 if (currentState != STATE_CONNECTED) 574 return; 575 576 // Do not attempt to send empty messages 577 if (arguments.length == 0) 578 return; 579 580 /** 581 * Converts the given value to a length/string pair for use as an 582 * element in a Guacamole instruction. 583 * 584 * @private 585 * @param value The value to convert. 586 * @return {String} The converted value. 587 */ 588 function getElement(value) { 589 var string = new String(value); 590 return string.length + "." + string; 591 } 592 593 // Initialized message with first element 594 var message = getElement(arguments[0]); 595 596 // Append remaining elements 597 for (var i=1; i<arguments.length; i++) 598 message += "," + getElement(arguments[i]); 599 600 // Final terminator 601 message += ";"; 602 603 socket.send(message); 604 605 }; 606 607 this.connect = function(data) { 608 609 // Connect socket 610 socket = new WebSocket(tunnelURL + "?" + data, "guacamole"); 611 612 socket.onopen = function(event) { 613 currentState = STATE_CONNECTED; 614 }; 615 616 socket.onclose = function(event) { 617 618 // If connection closed abnormally, signal error. 619 if (event.code != 1000 && tunnel.onerror) 620 tunnel.onerror(status_code[event.code]); 621 622 }; 623 624 socket.onerror = function(event) { 625 626 // Call error handler 627 if (tunnel.onerror) tunnel.onerror(event.data); 628 629 }; 630 631 socket.onmessage = function(event) { 632 633 var message = event.data; 634 var startIndex = 0; 635 var elementEnd; 636 637 var elements = []; 638 639 do { 640 641 // Search for end of length 642 var lengthEnd = message.indexOf(".", startIndex); 643 if (lengthEnd != -1) { 644 645 // Parse length 646 var length = parseInt(message.substring(elementEnd+1, lengthEnd)); 647 648 // Calculate start of element 649 startIndex = lengthEnd + 1; 650 651 // Calculate location of element terminator 652 elementEnd = startIndex + length; 653 654 } 655 656 // If no period, incomplete instruction. 657 else 658 throw new Error("Incomplete instruction."); 659 660 // We now have enough data for the element. Parse. 661 var element = message.substring(startIndex, elementEnd); 662 var terminator = message.substring(elementEnd, elementEnd+1); 663 664 // Add element to array 665 elements.push(element); 666 667 // If last element, handle instruction 668 if (terminator == ";") { 669 670 // Get opcode 671 var opcode = elements.shift(); 672 673 // Call instruction handler. 674 if (tunnel.oninstruction != null) 675 tunnel.oninstruction(opcode, elements); 676 677 // Clear elements 678 elements.length = 0; 679 680 } 681 682 // Start searching for length at character after 683 // element terminator 684 startIndex = elementEnd + 1; 685 686 } while (startIndex < message.length); 687 688 }; 689 690 }; 691 692 this.disconnect = function() { 693 currentState = STATE_DISCONNECTED; 694 socket.close(); 695 }; 696 697 }; 698 699 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel(); 700 701 702 /** 703 * Guacamole Tunnel which cycles between all specified tunnels until 704 * no tunnels are left. Another tunnel is used if an error occurs but 705 * no instructions have been received. If an instruction has been 706 * received, or no tunnels remain, the error is passed directly out 707 * through the onerror handler (if defined). 708 * 709 * @constructor 710 * @augments Guacamole.Tunnel 711 * @param {...} tunnel_chain The tunnels to use, in order of priority. 712 */ 713 Guacamole.ChainedTunnel = function(tunnel_chain) { 714 715 /** 716 * Reference to this chained tunnel. 717 * @private 718 */ 719 var chained_tunnel = this; 720 721 /** 722 * The currently wrapped tunnel, if any. 723 * @private 724 */ 725 var current_tunnel = null; 726 727 /** 728 * Data passed in via connect(), to be used for 729 * wrapped calls to other tunnels' connect() functions. 730 * @private 731 */ 732 var connect_data; 733 734 /** 735 * Array of all tunnels passed to this ChainedTunnel through the 736 * constructor arguments. 737 * @private 738 */ 739 var tunnels = []; 740 741 // Load all tunnels into array 742 for (var i=0; i<arguments.length; i++) 743 tunnels.push(arguments[i]); 744 745 /** 746 * Sets the current tunnel. 747 * 748 * @private 749 * @param {Guacamole.Tunnel} tunnel The tunnel to set as the current tunnel. 750 */ 751 function attach(tunnel) { 752 753 // Clear handlers of current tunnel, if any 754 if (current_tunnel) { 755 current_tunnel.onerror = null; 756 current_tunnel.oninstruction = null; 757 } 758 759 // Set own functions to tunnel's functions 760 chained_tunnel.disconnect = tunnel.disconnect; 761 chained_tunnel.sendMessage = tunnel.sendMessage; 762 763 // Record current tunnel 764 current_tunnel = tunnel; 765 766 // Wrap own oninstruction within current tunnel 767 current_tunnel.oninstruction = function(opcode, elements) { 768 769 // Invoke handler 770 chained_tunnel.oninstruction(opcode, elements); 771 772 // Use handler permanently from now on 773 current_tunnel.oninstruction = chained_tunnel.oninstruction; 774 775 // Pass through errors (without trying other tunnels) 776 current_tunnel.onerror = chained_tunnel.onerror; 777 778 } 779 780 // Attach next tunnel on error 781 current_tunnel.onerror = function(message) { 782 783 // Get next tunnel 784 var next_tunnel = tunnels.shift(); 785 786 // If there IS a next tunnel, try using it. 787 if (next_tunnel) 788 attach(next_tunnel); 789 790 // Otherwise, call error handler 791 else if (chained_tunnel.onerror) 792 chained_tunnel.onerror(message); 793 794 }; 795 796 try { 797 798 // Attempt connection 799 current_tunnel.connect(connect_data); 800 801 } 802 catch (e) { 803 804 // Call error handler of current tunnel on error 805 current_tunnel.onerror(e.message); 806 807 } 808 809 810 } 811 812 this.connect = function(data) { 813 814 // Remember connect data 815 connect_data = data; 816 817 // Get first tunnel 818 var next_tunnel = tunnels.shift(); 819 820 // Attach first tunnel 821 if (next_tunnel) 822 attach(next_tunnel); 823 824 // If there IS no first tunnel, error 825 else if (chained_tunnel.onerror) 826 chained_tunnel.onerror("No tunnels to try."); 827 828 }; 829 830 }; 831 832 Guacamole.ChainedTunnel.prototype = new Guacamole.Tunnel(); 833