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 * Matt Hortman 24 * 25 * Alternatively, the contents of this file may be used under the terms of 26 * either the GNU General Public License Version 2 or later (the "GPL"), or 27 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 28 * in which case the provisions of the GPL or the LGPL are applicable instead 29 * of those above. If you wish to allow use of your version of this file only 30 * under the terms of either the GPL or the LGPL, and not to allow others to 31 * use your version of this file under the terms of the MPL, indicate your 32 * decision by deleting the provisions above and replace them with the notice 33 * and other provisions required by the GPL or the LGPL. If you do not delete 34 * the provisions above, a recipient may use your version of this file under 35 * the terms of any one of the MPL, the GPL or the LGPL. 36 * 37 * ***** END LICENSE BLOCK ***** */ 38 39 /** 40 * Namespace for all Guacamole JavaScript objects. 41 * @namespace 42 */ 43 var Guacamole = Guacamole || {}; 44 45 /** 46 * Simple Guacamole protocol parser that invokes an oninstruction event when 47 * full instructions are available from data received via receive(). 48 * 49 * @constructor 50 */ 51 Guacamole.Parser = function() { 52 53 /** 54 * Reference to this parser. 55 * @private 56 */ 57 var parser = this; 58 59 /** 60 * Current buffer of received data. This buffer grows until a full 61 * element is available. After a full element is available, that element 62 * is flushed into the element buffer. 63 * 64 * @private 65 */ 66 var buffer = ""; 67 68 /** 69 * Buffer of all received, complete elements. After an entire instruction 70 * is read, this buffer is flushed, and a new instruction begins. 71 * 72 * @private 73 */ 74 var element_buffer = []; 75 76 // The location of the last element's terminator 77 var element_end = -1; 78 79 // Where to start the next length search or the next element 80 var start_index = 0; 81 82 /** 83 * Appends the given instruction data packet to the internal buffer of 84 * this Guacamole.Parser, executing all completed instructions at 85 * the beginning of this buffer, if any. 86 * 87 * @param {String} packet The instruction data to append. 88 */ 89 this.receive = function(packet) { 90 91 // Truncate buffer as necessary 92 if (start_index > 4096 && element_end >= start_index) { 93 94 buffer = buffer.substring(start_index); 95 96 // Reset parse relative to truncation 97 element_end -= start_index; 98 start_index = 0; 99 100 } 101 102 // Append data to buffer 103 buffer += packet; 104 105 // While search is within currently received data 106 while (element_end < buffer.length) { 107 108 // If we are waiting for element data 109 if (element_end >= start_index) { 110 111 // We now have enough data for the element. Parse. 112 var element = buffer.substring(start_index, element_end); 113 var terminator = buffer.substring(element_end, element_end+1); 114 115 // Add element to array 116 element_buffer.push(element); 117 118 // If last element, handle instruction 119 if (terminator == ";") { 120 121 // Get opcode 122 var opcode = element_buffer.shift(); 123 124 // Call instruction handler. 125 if (parser.oninstruction != null) 126 parser.oninstruction(opcode, element_buffer); 127 128 // Clear elements 129 element_buffer.length = 0; 130 131 } 132 else if (terminator != ',') 133 throw new Error("Illegal terminator."); 134 135 // Start searching for length at character after 136 // element terminator 137 start_index = element_end + 1; 138 139 } 140 141 // Search for end of length 142 var length_end = buffer.indexOf(".", start_index); 143 if (length_end != -1) { 144 145 // Parse length 146 var length = parseInt(buffer.substring(element_end+1, length_end)); 147 if (length == NaN) 148 throw new Error("Non-numeric character in element length."); 149 150 // Calculate start of element 151 start_index = length_end + 1; 152 153 // Calculate location of element terminator 154 element_end = start_index + length; 155 156 } 157 158 // If no period yet, continue search when more data 159 // is received 160 else { 161 start_index = buffer.length; 162 break; 163 } 164 165 } // end parse loop 166 167 }; 168 169 /** 170 * Fired once for every complete Guacamole instruction received, in order. 171 * 172 * @event 173 * @param {String} opcode The Guacamole instruction opcode. 174 * @param {Array} parameters The parameters provided for the instruction, 175 * if any. 176 */ 177 this.oninstruction = null; 178 179 }; 180 181 182 /** 183 * A blob abstraction used by the Guacamole client to facilitate transfer of 184 * files or other binary data. 185 * 186 * @constructor 187 * @param {String} mimetype The mimetype of the data this blob will contain. 188 * @param {String} name An arbitrary name for this blob. 189 */ 190 Guacamole.Blob = function(mimetype, name) { 191 192 /** 193 * Reference to this Guacamole.Blob. 194 * @private 195 */ 196 var guac_blob = this; 197 198 /** 199 * The length of this Guacamole.Blob in bytes. 200 * @private 201 */ 202 var length = 0; 203 204 /** 205 * The mimetype of the data contained within this blob. 206 */ 207 this.mimetype = mimetype; 208 209 /** 210 * The name of this blob. In general, this should be an appropriate 211 * filename. 212 */ 213 this.name = name; 214 215 // Get blob builder 216 var blob_builder; 217 if (window.BlobBuilder) blob_builder = new BlobBuilder(); 218 else if (window.WebKitBlobBuilder) blob_builder = new WebKitBlobBuilder(); 219 else if (window.MozBlobBuilder) blob_builder = new MozBlobBuilder(); 220 else 221 blob_builder = new (function() { 222 223 var blobs = []; 224 225 this.append = function(data) { 226 blobs.push(new Blob([data], {"type": mimetype})); 227 }; 228 229 this.getBlob = function() { 230 return new Blob(blobs, {"type": mimetype}); 231 }; 232 233 })(); 234 235 /** 236 * Appends the given ArrayBuffer to this Guacamole.Blob. 237 * 238 * @param {ArrayBuffer} buffer An ArrayBuffer containing the data to be 239 * appended. 240 */ 241 this.append = function(buffer) { 242 243 blob_builder.append(buffer); 244 length += buffer.byteLength; 245 246 // Call handler, if present 247 if (guac_blob.ondata) 248 guac_blob.ondata(buffer.byteLength); 249 250 }; 251 252 /** 253 * Closes this Guacamole.Blob such that no further data will be written. 254 */ 255 this.close = function() { 256 257 // Call handler, if present 258 if (guac_blob.oncomplete) 259 guac_blob.oncomplete(); 260 261 // NOTE: Currently not enforced. 262 263 }; 264 265 /** 266 * Returns the current length of this Guacamole.Blob, in bytes. 267 * @return {Number} The current length of this Guacamole.Blob. 268 */ 269 this.getLength = function() { 270 return length; 271 }; 272 273 /** 274 * Returns the contents of this Guacamole.Blob as a Blob. 275 * @return {Blob} The contents of this Guacamole.Blob. 276 */ 277 this.getBlob = function() { 278 return blob_builder.getBlob(); 279 }; 280 281 /** 282 * Fired once for every blob of data received. 283 * 284 * @event 285 * @param {Number} length The number of bytes received. 286 */ 287 this.ondata = null; 288 289 /** 290 * Fired once this blob is finished and no further data will be written. 291 * @event 292 */ 293 this.oncomplete = null; 294 295 }; 296 297 298 /** 299 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel}, 300 * automatically handles incoming and outgoing Guacamole instructions via the 301 * provided tunnel, updating the display using one or more canvas elements. 302 * 303 * @constructor 304 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive 305 * Guacamole instructions. 306 */ 307 Guacamole.Client = function(tunnel) { 308 309 var guac_client = this; 310 311 var STATE_IDLE = 0; 312 var STATE_CONNECTING = 1; 313 var STATE_WAITING = 2; 314 var STATE_CONNECTED = 3; 315 var STATE_DISCONNECTING = 4; 316 var STATE_DISCONNECTED = 5; 317 318 var currentState = STATE_IDLE; 319 320 var currentTimestamp = 0; 321 var pingInterval = null; 322 323 var displayWidth = 0; 324 var displayHeight = 0; 325 var displayScale = 1; 326 327 /** 328 * Translation from Guacamole protocol line caps to Layer line caps. 329 * @private 330 */ 331 var lineCap = { 332 0: "butt", 333 1: "round", 334 2: "square" 335 }; 336 337 /** 338 * Translation from Guacamole protocol line caps to Layer line caps. 339 * @private 340 */ 341 var lineJoin = { 342 0: "bevel", 343 1: "miter", 344 2: "round" 345 }; 346 347 // Create bounding div 348 var bounds = document.createElement("div"); 349 bounds.style.position = "relative"; 350 bounds.style.width = (displayWidth*displayScale) + "px"; 351 bounds.style.height = (displayHeight*displayScale) + "px"; 352 353 // Create display 354 var display = document.createElement("div"); 355 display.style.position = "relative"; 356 display.style.width = displayWidth + "px"; 357 display.style.height = displayHeight + "px"; 358 359 // Ensure transformations on display originate at 0,0 360 display.style.transformOrigin = 361 display.style.webkitTransformOrigin = 362 display.style.MozTransformOrigin = 363 display.style.OTransformOrigin = 364 display.style.msTransformOrigin = 365 "0 0"; 366 367 // Create default layer 368 var default_layer_container = new Guacamole.Client.LayerContainer(displayWidth, displayHeight); 369 370 // Position default layer 371 var default_layer_container_element = default_layer_container.getElement(); 372 default_layer_container_element.style.position = "absolute"; 373 default_layer_container_element.style.left = "0px"; 374 default_layer_container_element.style.top = "0px"; 375 default_layer_container_element.style.overflow = "hidden"; 376 377 // Create cursor layer 378 var cursor = new Guacamole.Client.LayerContainer(0, 0); 379 cursor.getLayer().setChannelMask(Guacamole.Layer.SRC); 380 cursor.getLayer().autoflush = true; 381 382 // Position cursor layer 383 var cursor_element = cursor.getElement(); 384 cursor_element.style.position = "absolute"; 385 cursor_element.style.left = "0px"; 386 cursor_element.style.top = "0px"; 387 388 // Add default layer and cursor to display 389 display.appendChild(default_layer_container.getElement()); 390 display.appendChild(cursor.getElement()); 391 392 // Add display to bounds 393 bounds.appendChild(display); 394 395 // Initially, only default layer exists 396 var layers = [default_layer_container]; 397 398 // No initial buffers 399 var buffers = []; 400 401 // No initial parsers 402 var parsers = []; 403 404 // No initial audio channels 405 var audio_channels = []; 406 407 // No initial blobs 408 var blobs = []; 409 410 tunnel.onerror = function(message) { 411 if (guac_client.onerror) 412 guac_client.onerror(message); 413 }; 414 415 function setState(state) { 416 if (state != currentState) { 417 currentState = state; 418 if (guac_client.onstatechange) 419 guac_client.onstatechange(currentState); 420 } 421 } 422 423 function isConnected() { 424 return currentState == STATE_CONNECTED 425 || currentState == STATE_WAITING; 426 } 427 428 var cursorHotspotX = 0; 429 var cursorHotspotY = 0; 430 431 var cursorX = 0; 432 var cursorY = 0; 433 434 function moveCursor(x, y) { 435 436 // Move cursor layer 437 cursor.translate(x - cursorHotspotX, y - cursorHotspotY); 438 439 // Update stored position 440 cursorX = x; 441 cursorY = y; 442 443 } 444 445 /** 446 * Returns an element containing the display of this Guacamole.Client. 447 * Adding the element returned by this function to an element in the body 448 * of a document will cause the client's display to be visible. 449 * 450 * @return {Element} An element containing ths display of this 451 * Guacamole.Client. 452 */ 453 this.getDisplay = function() { 454 return bounds; 455 }; 456 457 /** 458 * Sends the current size of the screen. 459 * 460 * @param {Number} width The width of the screen. 461 * @param {Number} height The height of the screen. 462 */ 463 this.sendSize = function(width, height) { 464 465 // Do not send requests if not connected 466 if (!isConnected()) 467 return; 468 469 tunnel.sendMessage("size", width, height); 470 471 }; 472 473 /** 474 * Sends a key event having the given properties as if the user 475 * pressed or released a key. 476 * 477 * @param {Boolean} pressed Whether the key is pressed (true) or released 478 * (false). 479 * @param {Number} keysym The keysym of the key being pressed or released. 480 */ 481 this.sendKeyEvent = function(pressed, keysym) { 482 // Do not send requests if not connected 483 if (!isConnected()) 484 return; 485 486 tunnel.sendMessage("key", keysym, pressed); 487 }; 488 489 /** 490 * Sends a mouse event having the properties provided by the given mouse 491 * state. 492 * 493 * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send 494 * in the mouse event. 495 */ 496 this.sendMouseState = function(mouseState) { 497 498 // Do not send requests if not connected 499 if (!isConnected()) 500 return; 501 502 // Update client-side cursor 503 moveCursor( 504 Math.floor(mouseState.x), 505 Math.floor(mouseState.y) 506 ); 507 508 // Build mask 509 var buttonMask = 0; 510 if (mouseState.left) buttonMask |= 1; 511 if (mouseState.middle) buttonMask |= 2; 512 if (mouseState.right) buttonMask |= 4; 513 if (mouseState.up) buttonMask |= 8; 514 if (mouseState.down) buttonMask |= 16; 515 516 // Send message 517 tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask); 518 }; 519 520 /** 521 * Sets the clipboard of the remote client to the given text data. 522 * 523 * @param {String} data The data to send as the clipboard contents. 524 */ 525 this.setClipboard = function(data) { 526 527 // Do not send requests if not connected 528 if (!isConnected()) 529 return; 530 531 tunnel.sendMessage("clipboard", data); 532 }; 533 534 /** 535 * Fired whenever the state of this Guacamole.Client changes. 536 * 537 * @event 538 * @param {Number} state The new state of the client. 539 */ 540 this.onstatechange = null; 541 542 /** 543 * Fired when the remote client sends a name update. 544 * 545 * @event 546 * @param {String} name The new name of this client. 547 */ 548 this.onname = null; 549 550 /** 551 * Fired when an error is reported by the remote client, and the connection 552 * is being closed. 553 * 554 * @event 555 * @param {String} error A human-readable description of the error. 556 */ 557 this.onerror = null; 558 559 /** 560 * Fired when the clipboard of the remote client is changing. 561 * 562 * @event 563 * @param {String} data The new text data of the remote clipboard. 564 */ 565 this.onclipboard = null; 566 567 /** 568 * Fired when the default layer (and thus the entire Guacamole display) 569 * is resized. 570 * 571 * @event 572 * @param {Number} width The new width of the Guacamole display. 573 * @param {Number} height The new height of the Guacamole display. 574 */ 575 this.onresize = null; 576 577 /** 578 * Fired when a blob is created. The blob provided to this event handler 579 * will contain its own event handlers for received data and the close 580 * event. 581 * 582 * @event 583 * @param {Guacamole.Blob} blob A container for blob data that will receive 584 * data from the server. 585 */ 586 this.onblob = null; 587 588 // Layers 589 function getBufferLayer(index) { 590 591 index = -1 - index; 592 var buffer = buffers[index]; 593 594 // Create buffer if necessary 595 if (buffer == null) { 596 buffer = new Guacamole.Layer(0, 0); 597 buffer.autoflush = 1; 598 buffer.autosize = 1; 599 buffers[index] = buffer; 600 } 601 602 return buffer; 603 604 } 605 606 function getLayerContainer(index) { 607 608 var layer = layers[index]; 609 if (layer == null) { 610 611 // Add new layer 612 layer = new Guacamole.Client.LayerContainer(displayWidth, displayHeight); 613 layers[index] = layer; 614 615 // Get and position layer 616 var layer_element = layer.getElement(); 617 layer_element.style.position = "absolute"; 618 layer_element.style.left = "0px"; 619 layer_element.style.top = "0px"; 620 layer_element.style.overflow = "hidden"; 621 622 // Add to default layer container 623 default_layer_container.getElement().appendChild(layer_element); 624 625 } 626 627 return layer; 628 629 } 630 631 function getLayer(index) { 632 633 // If buffer, just get layer 634 if (index < 0) 635 return getBufferLayer(index); 636 637 // Otherwise, retrieve layer from layer container 638 return getLayerContainer(index).getLayer(); 639 640 } 641 642 function getParser(index) { 643 644 var parser = parsers[index]; 645 646 // If parser not yet created, create it, and tie to the 647 // oninstruction handler of the tunnel. 648 if (parser == null) { 649 parser = parsers[index] = new Guacamole.Parser(); 650 parser.oninstruction = tunnel.oninstruction; 651 } 652 653 return parser; 654 655 } 656 657 function getAudioChannel(index) { 658 659 var audio_channel = audio_channels[index]; 660 661 // If audio channel not yet created, create it 662 if (audio_channel == null) 663 audio_channel = audio_channels[index] = new Guacamole.AudioChannel(); 664 665 return audio_channel; 666 667 } 668 669 /** 670 * Handlers for all defined layer properties. 671 * @private 672 */ 673 var layerPropertyHandlers = { 674 675 "miter-limit": function(layer, value) { 676 layer.setMiterLimit(parseFloat(value)); 677 } 678 679 }; 680 681 /** 682 * Handlers for all instruction opcodes receivable by a Guacamole protocol 683 * client. 684 * @private 685 */ 686 var instructionHandlers = { 687 688 "arc": function(parameters) { 689 690 var layer = getLayer(parseInt(parameters[0])); 691 var x = parseInt(parameters[1]); 692 var y = parseInt(parameters[2]); 693 var radius = parseInt(parameters[3]); 694 var startAngle = parseFloat(parameters[4]); 695 var endAngle = parseFloat(parameters[5]); 696 var negative = parseInt(parameters[6]); 697 698 layer.arc(x, y, radius, startAngle, endAngle, negative != 0); 699 700 }, 701 702 "audio": function(parameters) { 703 704 var channel = getAudioChannel(parseInt(parameters[0])); 705 var mimetype = parameters[1]; 706 var duration = parseFloat(parameters[2]); 707 var data = parameters[3]; 708 709 channel.play(mimetype, duration, data); 710 711 }, 712 713 "blob": function(parameters) { 714 715 // Get blob 716 var blob_index = parseInt(parameters[0]); 717 var data = parameters[1]; 718 var blob = blobs[blob_index]; 719 720 // Convert to ArrayBuffer 721 var binary = window.atob(data); 722 var arrayBuffer = new ArrayBuffer(binary.length); 723 var bufferView = new Uint8Array(arrayBuffer); 724 725 for (var i=0; i<binary.length; i++) 726 bufferView[i] = binary.charCodeAt(i); 727 728 // Write data 729 blob.append(arrayBuffer); 730 731 }, 732 733 "cfill": function(parameters) { 734 735 var channelMask = parseInt(parameters[0]); 736 var layer = getLayer(parseInt(parameters[1])); 737 var r = parseInt(parameters[2]); 738 var g = parseInt(parameters[3]); 739 var b = parseInt(parameters[4]); 740 var a = parseInt(parameters[5]); 741 742 layer.setChannelMask(channelMask); 743 744 layer.fillColor(r, g, b, a); 745 746 }, 747 748 "clip": function(parameters) { 749 750 var layer = getLayer(parseInt(parameters[0])); 751 752 layer.clip(); 753 754 }, 755 756 "clipboard": function(parameters) { 757 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]); 758 }, 759 760 "close": function(parameters) { 761 762 var layer = getLayer(parseInt(parameters[0])); 763 764 layer.close(); 765 766 }, 767 768 "copy": function(parameters) { 769 770 var srcL = getLayer(parseInt(parameters[0])); 771 var srcX = parseInt(parameters[1]); 772 var srcY = parseInt(parameters[2]); 773 var srcWidth = parseInt(parameters[3]); 774 var srcHeight = parseInt(parameters[4]); 775 var channelMask = parseInt(parameters[5]); 776 var dstL = getLayer(parseInt(parameters[6])); 777 var dstX = parseInt(parameters[7]); 778 var dstY = parseInt(parameters[8]); 779 780 dstL.setChannelMask(channelMask); 781 782 dstL.copy( 783 srcL, 784 srcX, 785 srcY, 786 srcWidth, 787 srcHeight, 788 dstX, 789 dstY 790 ); 791 792 }, 793 794 "cstroke": function(parameters) { 795 796 var channelMask = parseInt(parameters[0]); 797 var layer = getLayer(parseInt(parameters[1])); 798 var cap = lineCap[parseInt(parameters[2])]; 799 var join = lineJoin[parseInt(parameters[3])]; 800 var thickness = parseInt(parameters[4]); 801 var r = parseInt(parameters[5]); 802 var g = parseInt(parameters[6]); 803 var b = parseInt(parameters[7]); 804 var a = parseInt(parameters[8]); 805 806 layer.setChannelMask(channelMask); 807 808 layer.strokeColor(cap, join, thickness, r, g, b, a); 809 810 }, 811 812 "cursor": function(parameters) { 813 814 cursorHotspotX = parseInt(parameters[0]); 815 cursorHotspotY = parseInt(parameters[1]); 816 var srcL = getLayer(parseInt(parameters[2])); 817 var srcX = parseInt(parameters[3]); 818 var srcY = parseInt(parameters[4]); 819 var srcWidth = parseInt(parameters[5]); 820 var srcHeight = parseInt(parameters[6]); 821 822 // Reset cursor size 823 cursor.resize(srcWidth, srcHeight); 824 825 // Draw cursor to cursor layer 826 cursor.getLayer().copy( 827 srcL, 828 srcX, 829 srcY, 830 srcWidth, 831 srcHeight, 832 0, 833 0 834 ); 835 836 // Update cursor position (hotspot may have changed) 837 moveCursor(cursorX, cursorY); 838 839 }, 840 841 "curve": function(parameters) { 842 843 var layer = getLayer(parseInt(parameters[0])); 844 var cp1x = parseInt(parameters[1]); 845 var cp1y = parseInt(parameters[2]); 846 var cp2x = parseInt(parameters[3]); 847 var cp2y = parseInt(parameters[4]); 848 var x = parseInt(parameters[5]); 849 var y = parseInt(parameters[6]); 850 851 layer.curveTo(cp1x, cp1y, cp2x, cp2y, x, y); 852 853 }, 854 855 "dispose": function(parameters) { 856 857 var layer_index = parseInt(parameters[0]); 858 859 // If visible layer, remove from parent 860 if (layer_index > 0) { 861 862 // Get container element 863 var layer_container = getLayerContainer(layer_index).getElement(); 864 865 // Remove from parent 866 layer_container.parentNode.removeChild(layer_container); 867 868 // Delete reference 869 delete layers[layer_index]; 870 871 } 872 873 // If buffer, just delete reference 874 else if (layer_index < 0) 875 delete buffers[-1 - layer_index]; 876 877 // Attempting to dispose the root layer currently has no effect. 878 879 }, 880 881 "distort": function(parameters) { 882 883 var layer_index = parseInt(parameters[0]); 884 var a = parseFloat(parameters[1]); 885 var b = parseFloat(parameters[2]); 886 var c = parseFloat(parameters[3]); 887 var d = parseFloat(parameters[4]); 888 var e = parseFloat(parameters[5]); 889 var f = parseFloat(parameters[6]); 890 891 // Only valid for visible layers (not buffers) 892 if (layer_index >= 0) { 893 894 // Get container element 895 var layer_container = getLayerContainer(layer_index).getElement(); 896 897 // Set layer transform 898 layer_container.transform(a, b, c, d, e, f); 899 900 } 901 902 }, 903 904 "error": function(parameters) { 905 if (guac_client.onerror) guac_client.onerror(parameters[0]); 906 guac_client.disconnect(); 907 }, 908 909 "end": function(parameters) { 910 911 // Get blob 912 var blob_index = parseInt(parameters[0]); 913 var blob = blobs[blob_index]; 914 915 // Close blob 916 blob.close(); 917 918 }, 919 920 "file": function(parameters) { 921 922 var blob_index = parseInt(parameters[0]); 923 var mimetype = parameters[1]; 924 var filename = parameters[2]; 925 926 // Create blob 927 var blob = blobs[blob_index] = new Guacamole.Blob(mimetype, filename); 928 929 // Call handler now that blob is created 930 if (guac_client.onblob) 931 guac_client.onblob(blob); 932 933 }, 934 935 "identity": function(parameters) { 936 937 var layer = getLayer(parseInt(parameters[0])); 938 939 layer.setTransform(1, 0, 0, 1, 0, 0); 940 941 }, 942 943 "lfill": function(parameters) { 944 945 var channelMask = parseInt(parameters[0]); 946 var layer = getLayer(parseInt(parameters[1])); 947 var srcLayer = getLayer(parseInt(parameters[2])); 948 949 layer.setChannelMask(channelMask); 950 951 layer.fillLayer(srcLayer); 952 953 }, 954 955 "line": function(parameters) { 956 957 var layer = getLayer(parseInt(parameters[0])); 958 var x = parseInt(parameters[1]); 959 var y = parseInt(parameters[2]); 960 961 layer.lineTo(x, y); 962 963 }, 964 965 "lstroke": function(parameters) { 966 967 var channelMask = parseInt(parameters[0]); 968 var layer = getLayer(parseInt(parameters[1])); 969 var srcLayer = getLayer(parseInt(parameters[2])); 970 971 layer.setChannelMask(channelMask); 972 973 layer.strokeLayer(srcLayer); 974 975 }, 976 977 "move": function(parameters) { 978 979 var layer_index = parseInt(parameters[0]); 980 var parent_index = parseInt(parameters[1]); 981 var x = parseInt(parameters[2]); 982 var y = parseInt(parameters[3]); 983 var z = parseInt(parameters[4]); 984 985 // Only valid for non-default layers 986 if (layer_index > 0 && parent_index >= 0) { 987 988 // Get container element 989 var layer_container = getLayerContainer(layer_index); 990 var layer_container_element = layer_container.getElement(); 991 var parent = getLayerContainer(parent_index).getElement(); 992 993 // Set parent if necessary 994 if (!(layer_container_element.parentNode === parent)) 995 parent.appendChild(layer_container_element); 996 997 // Move layer 998 layer_container.translate(x, y); 999 layer_container_element.style.zIndex = z; 1000 1001 } 1002 1003 }, 1004 1005 "name": function(parameters) { 1006 if (guac_client.onname) guac_client.onname(parameters[0]); 1007 }, 1008 1009 "nest": function(parameters) { 1010 var parser = getParser(parseInt(parameters[0])); 1011 parser.receive(parameters[1]); 1012 }, 1013 1014 "png": function(parameters) { 1015 1016 var channelMask = parseInt(parameters[0]); 1017 var layer = getLayer(parseInt(parameters[1])); 1018 var x = parseInt(parameters[2]); 1019 var y = parseInt(parameters[3]); 1020 var data = parameters[4]; 1021 1022 layer.setChannelMask(channelMask); 1023 1024 layer.draw( 1025 x, 1026 y, 1027 "data:image/png;base64," + data 1028 ); 1029 1030 }, 1031 1032 "pop": function(parameters) { 1033 1034 var layer = getLayer(parseInt(parameters[0])); 1035 1036 layer.pop(); 1037 1038 }, 1039 1040 "push": function(parameters) { 1041 1042 var layer = getLayer(parseInt(parameters[0])); 1043 1044 layer.push(); 1045 1046 }, 1047 1048 "rect": function(parameters) { 1049 1050 var layer = getLayer(parseInt(parameters[0])); 1051 var x = parseInt(parameters[1]); 1052 var y = parseInt(parameters[2]); 1053 var w = parseInt(parameters[3]); 1054 var h = parseInt(parameters[4]); 1055 1056 layer.rect(x, y, w, h); 1057 1058 }, 1059 1060 "reset": function(parameters) { 1061 1062 var layer = getLayer(parseInt(parameters[0])); 1063 1064 layer.reset(); 1065 1066 }, 1067 1068 "set": function(parameters) { 1069 1070 var layer = getLayer(parseInt(parameters[0])); 1071 var name = parameters[1]; 1072 var value = parameters[2]; 1073 1074 // Call property handler if defined 1075 var handler = layerPropertyHandlers[name]; 1076 if (handler) 1077 handler(layer, value); 1078 1079 }, 1080 1081 "shade": function(parameters) { 1082 1083 var layer_index = parseInt(parameters[0]); 1084 var a = parseInt(parameters[1]); 1085 1086 // Only valid for visible layers (not buffers) 1087 if (layer_index >= 0) { 1088 1089 // Get container element 1090 var layer_container = getLayerContainer(layer_index).getElement(); 1091 1092 // Set layer opacity 1093 layer_container.style.opacity = a/255.0; 1094 1095 } 1096 1097 }, 1098 1099 "size": function(parameters) { 1100 1101 var layer_index = parseInt(parameters[0]); 1102 var width = parseInt(parameters[1]); 1103 var height = parseInt(parameters[2]); 1104 1105 // If not buffer, resize layer and container 1106 if (layer_index >= 0) { 1107 1108 // Resize layer 1109 var layer_container = getLayerContainer(layer_index); 1110 layer_container.resize(width, height); 1111 1112 // If layer is default, resize display 1113 if (layer_index == 0) { 1114 1115 displayWidth = width; 1116 displayHeight = height; 1117 1118 // Update (set) display size 1119 display.style.width = displayWidth + "px"; 1120 display.style.height = displayHeight + "px"; 1121 1122 // Update bounds size 1123 bounds.style.width = (displayWidth*displayScale) + "px"; 1124 bounds.style.height = (displayHeight*displayScale) + "px"; 1125 1126 // Call resize event handler if defined 1127 if (guac_client.onresize) 1128 guac_client.onresize(width, height); 1129 1130 } 1131 1132 } 1133 1134 // If buffer, resize layer only 1135 else { 1136 var layer = getBufferLayer(parseInt(parameters[0])); 1137 layer.resize(width, height); 1138 } 1139 1140 }, 1141 1142 "start": function(parameters) { 1143 1144 var layer = getLayer(parseInt(parameters[0])); 1145 var x = parseInt(parameters[1]); 1146 var y = parseInt(parameters[2]); 1147 1148 layer.moveTo(x, y); 1149 1150 }, 1151 1152 "sync": function(parameters) { 1153 1154 var timestamp = parameters[0]; 1155 1156 // When all layers have finished rendering all instructions 1157 // UP TO THIS POINT IN TIME, send sync response. 1158 1159 var layersToSync = 0; 1160 function syncLayer() { 1161 1162 layersToSync--; 1163 1164 // Send sync response when layers are finished 1165 if (layersToSync == 0) { 1166 if (timestamp != currentTimestamp) { 1167 tunnel.sendMessage("sync", timestamp); 1168 currentTimestamp = timestamp; 1169 } 1170 } 1171 1172 } 1173 1174 // Count active, not-ready layers and install sync tracking hooks 1175 for (var i=0; i<layers.length; i++) { 1176 1177 var layer = layers[i].getLayer(); 1178 if (layer) { 1179 1180 // Flush layer 1181 layer.flush(); 1182 1183 // If still not ready, sync later 1184 if (!layer.isReady()) { 1185 layersToSync++; 1186 layer.sync(syncLayer); 1187 } 1188 1189 } 1190 1191 } 1192 1193 // If all layers are ready, then we didn't install any hooks. 1194 // Send sync message now, 1195 if (layersToSync == 0) { 1196 if (timestamp != currentTimestamp) { 1197 tunnel.sendMessage("sync", timestamp); 1198 currentTimestamp = timestamp; 1199 } 1200 } 1201 1202 // If received first update, no longer waiting. 1203 if (currentState == STATE_WAITING) 1204 setState(STATE_CONNECTED); 1205 1206 }, 1207 1208 "transfer": function(parameters) { 1209 1210 var srcL = getLayer(parseInt(parameters[0])); 1211 var srcX = parseInt(parameters[1]); 1212 var srcY = parseInt(parameters[2]); 1213 var srcWidth = parseInt(parameters[3]); 1214 var srcHeight = parseInt(parameters[4]); 1215 var transferFunction = Guacamole.Client.DefaultTransferFunction[parameters[5]]; 1216 var dstL = getLayer(parseInt(parameters[6])); 1217 var dstX = parseInt(parameters[7]); 1218 var dstY = parseInt(parameters[8]); 1219 1220 dstL.transfer( 1221 srcL, 1222 srcX, 1223 srcY, 1224 srcWidth, 1225 srcHeight, 1226 dstX, 1227 dstY, 1228 transferFunction 1229 ); 1230 1231 }, 1232 1233 "transform": function(parameters) { 1234 1235 var layer = getLayer(parseInt(parameters[0])); 1236 var a = parseFloat(parameters[1]); 1237 var b = parseFloat(parameters[2]); 1238 var c = parseFloat(parameters[3]); 1239 var d = parseFloat(parameters[4]); 1240 var e = parseFloat(parameters[5]); 1241 var f = parseFloat(parameters[6]); 1242 1243 layer.transform(a, b, c, d, e, f); 1244 1245 }, 1246 1247 "video": function(parameters) { 1248 1249 var layer = getLayer(parseInt(parameters[0])); 1250 var mimetype = parameters[1]; 1251 var duration = parseFloat(parameters[2]); 1252 var data = parameters[3]; 1253 1254 layer.play(mimetype, duration, "data:" + mimetype + ";base64," + data); 1255 1256 } 1257 1258 1259 }; 1260 1261 1262 tunnel.oninstruction = function(opcode, parameters) { 1263 1264 var handler = instructionHandlers[opcode]; 1265 if (handler) 1266 handler(parameters); 1267 1268 }; 1269 1270 1271 /** 1272 * Sends a disconnect instruction to the server and closes the tunnel. 1273 */ 1274 this.disconnect = function() { 1275 1276 // Only attempt disconnection not disconnected. 1277 if (currentState != STATE_DISCONNECTED 1278 && currentState != STATE_DISCONNECTING) { 1279 1280 setState(STATE_DISCONNECTING); 1281 1282 // Stop ping 1283 if (pingInterval) 1284 window.clearInterval(pingInterval); 1285 1286 // Send disconnect message and disconnect 1287 tunnel.sendMessage("disconnect"); 1288 tunnel.disconnect(); 1289 setState(STATE_DISCONNECTED); 1290 1291 } 1292 1293 }; 1294 1295 /** 1296 * Connects the underlying tunnel of this Guacamole.Client, passing the 1297 * given arbitrary data to the tunnel during the connection process. 1298 * 1299 * @param data Arbitrary connection data to be sent to the underlying 1300 * tunnel during the connection process. 1301 */ 1302 this.connect = function(data) { 1303 1304 setState(STATE_CONNECTING); 1305 1306 try { 1307 tunnel.connect(data); 1308 } 1309 catch (e) { 1310 setState(STATE_IDLE); 1311 throw e; 1312 } 1313 1314 // Ping every 5 seconds (ensure connection alive) 1315 pingInterval = window.setInterval(function() { 1316 tunnel.sendMessage("sync", currentTimestamp); 1317 }, 5000); 1318 1319 setState(STATE_WAITING); 1320 }; 1321 1322 /** 1323 * Sets the scale of the client display element such that it renders at 1324 * a relatively smaller or larger size, without affecting the true 1325 * resolution of the display. 1326 * 1327 * @param {Number} scale The scale to resize to, where 1.0 is normal 1328 * size (1:1 scale). 1329 */ 1330 this.scale = function(scale) { 1331 1332 display.style.transform = 1333 display.style.WebkitTransform = 1334 display.style.MozTransform = 1335 display.style.OTransform = 1336 display.style.msTransform = 1337 1338 "scale(" + scale + "," + scale + ")"; 1339 1340 displayScale = scale; 1341 1342 // Update bounds size 1343 bounds.style.width = (displayWidth*displayScale) + "px"; 1344 bounds.style.height = (displayHeight*displayScale) + "px"; 1345 1346 }; 1347 1348 /** 1349 * Returns the width of the display. 1350 * 1351 * @return {Number} The width of the display. 1352 */ 1353 this.getWidth = function() { 1354 return displayWidth; 1355 }; 1356 1357 /** 1358 * Returns the height of the display. 1359 * 1360 * @return {Number} The height of the display. 1361 */ 1362 this.getHeight = function() { 1363 return displayHeight; 1364 }; 1365 1366 /** 1367 * Returns the scale of the display. 1368 * 1369 * @return {Number} The scale of the display. 1370 */ 1371 this.getScale = function() { 1372 return displayScale; 1373 }; 1374 1375 /** 1376 * Returns a canvas element containing the entire display, with all child 1377 * layers composited within. 1378 * 1379 * @return {HTMLCanvasElement} A new canvas element containing a copy of 1380 * the display. 1381 */ 1382 this.flatten = function() { 1383 1384 // Get source and destination canvases 1385 var source = getLayer(0).getCanvas(); 1386 var canvas = document.createElement("canvas"); 1387 1388 // Set dimensions 1389 canvas.width = source.width; 1390 canvas.height = source.height; 1391 1392 // Copy image from source 1393 var context = canvas.getContext("2d"); 1394 context.drawImage(source, 0, 0); 1395 1396 // Return new canvas copy 1397 return canvas; 1398 1399 }; 1400 1401 }; 1402 1403 /** 1404 * Simple container for Guacamole.Layer, allowing layers to be easily 1405 * repositioned and nested. This allows certain operations to be accelerated 1406 * through DOM manipulation, rather than raster operations. 1407 * 1408 * @constructor 1409 * 1410 * @param {Number} width The width of the Layer, in pixels. The canvas element 1411 * backing this Layer will be given this width. 1412 * 1413 * @param {Number} height The height of the Layer, in pixels. The canvas element 1414 * backing this Layer will be given this height. 1415 */ 1416 Guacamole.Client.LayerContainer = function(width, height) { 1417 1418 /** 1419 * Reference to this LayerContainer. 1420 * @private 1421 */ 1422 var layer_container = this; 1423 1424 // Create layer with given size 1425 var layer = new Guacamole.Layer(width, height); 1426 1427 // Set layer position 1428 var canvas = layer.getCanvas(); 1429 canvas.style.position = "absolute"; 1430 canvas.style.left = "0px"; 1431 canvas.style.top = "0px"; 1432 1433 // Create div with given size 1434 var div = document.createElement("div"); 1435 div.appendChild(canvas); 1436 div.style.width = width + "px"; 1437 div.style.height = height + "px"; 1438 1439 /** 1440 * Changes the size of this LayerContainer and the contained Layer to the 1441 * given width and height. 1442 * 1443 * @param {Number} width The new width to assign to this Layer. 1444 * @param {Number} height The new height to assign to this Layer. 1445 */ 1446 this.resize = function(width, height) { 1447 1448 // Resize layer 1449 layer.resize(width, height); 1450 1451 // Resize containing div 1452 div.style.width = width + "px"; 1453 div.style.height = height + "px"; 1454 1455 }; 1456 1457 /** 1458 * Returns the Layer contained within this LayerContainer. 1459 * @returns {Guacamole.Layer} The Layer contained within this 1460 * LayerContainer. 1461 */ 1462 this.getLayer = function() { 1463 return layer; 1464 }; 1465 1466 /** 1467 * Returns the element containing the Layer within this LayerContainer. 1468 * @returns {Element} The element containing the Layer within this 1469 * LayerContainer. 1470 */ 1471 this.getElement = function() { 1472 return div; 1473 }; 1474 1475 /** 1476 * The translation component of this LayerContainer's transform. 1477 * @private 1478 */ 1479 var translate = "translate(0px, 0px)"; // (0, 0) 1480 1481 /** 1482 * The arbitrary matrix component of this LayerContainer's transform. 1483 * @private 1484 */ 1485 var matrix = "matrix(1, 0, 0, 1, 0, 0)"; // Identity 1486 1487 /** 1488 * Moves the upper-left corner of this LayerContainer to the given X and Y 1489 * coordinate. 1490 * 1491 * @param {Number} x The X coordinate to move to. 1492 * @param {Number} y The Y coordinate to move to. 1493 */ 1494 this.translate = function(x, y) { 1495 1496 // Generate translation 1497 translate = "translate(" 1498 + x + "px," 1499 + y + "px)"; 1500 1501 // Set layer transform 1502 div.style.transform = 1503 div.style.WebkitTransform = 1504 div.style.MozTransform = 1505 div.style.OTransform = 1506 div.style.msTransform = 1507 1508 translate + " " + matrix; 1509 1510 }; 1511 1512 /** 1513 * Applies the given affine transform (defined with six values from the 1514 * transform's matrix). 1515 * 1516 * @param {Number} a The first value in the affine transform's matrix. 1517 * @param {Number} b The second value in the affine transform's matrix. 1518 * @param {Number} c The third value in the affine transform's matrix. 1519 * @param {Number} d The fourth value in the affine transform's matrix. 1520 * @param {Number} e The fifth value in the affine transform's matrix. 1521 * @param {Number} f The sixth value in the affine transform's matrix. 1522 */ 1523 this.transform = function(a, b, c, d, e, f) { 1524 1525 // Generate matrix transformation 1526 matrix = 1527 1528 /* a c e 1529 * b d f 1530 * 0 0 1 1531 */ 1532 1533 "matrix(" + a + "," + b + "," + c + "," + d + "," + e + "," + f + ")"; 1534 1535 // Set layer transform 1536 div.style.transform = 1537 div.style.WebkitTransform = 1538 div.style.MozTransform = 1539 div.style.OTransform = 1540 div.style.msTransform = 1541 1542 translate + " " + matrix; 1543 1544 }; 1545 1546 }; 1547 1548 /** 1549 * Map of all Guacamole binary raster operations to transfer functions. 1550 * @private 1551 */ 1552 Guacamole.Client.DefaultTransferFunction = { 1553 1554 /* BLACK */ 1555 0x0: function (src, dst) { 1556 dst.red = dst.green = dst.blue = 0x00; 1557 }, 1558 1559 /* WHITE */ 1560 0xF: function (src, dst) { 1561 dst.red = dst.green = dst.blue = 0xFF; 1562 }, 1563 1564 /* SRC */ 1565 0x3: function (src, dst) { 1566 dst.red = src.red; 1567 dst.green = src.green; 1568 dst.blue = src.blue; 1569 dst.alpha = src.alpha; 1570 }, 1571 1572 /* DEST (no-op) */ 1573 0x5: function (src, dst) { 1574 // Do nothing 1575 }, 1576 1577 /* Invert SRC */ 1578 0xC: function (src, dst) { 1579 dst.red = 0xFF & ~src.red; 1580 dst.green = 0xFF & ~src.green; 1581 dst.blue = 0xFF & ~src.blue; 1582 dst.alpha = src.alpha; 1583 }, 1584 1585 /* Invert DEST */ 1586 0xA: function (src, dst) { 1587 dst.red = 0xFF & ~dst.red; 1588 dst.green = 0xFF & ~dst.green; 1589 dst.blue = 0xFF & ~dst.blue; 1590 }, 1591 1592 /* AND */ 1593 0x1: function (src, dst) { 1594 dst.red = ( src.red & dst.red); 1595 dst.green = ( src.green & dst.green); 1596 dst.blue = ( src.blue & dst.blue); 1597 }, 1598 1599 /* NAND */ 1600 0xE: function (src, dst) { 1601 dst.red = 0xFF & ~( src.red & dst.red); 1602 dst.green = 0xFF & ~( src.green & dst.green); 1603 dst.blue = 0xFF & ~( src.blue & dst.blue); 1604 }, 1605 1606 /* OR */ 1607 0x7: function (src, dst) { 1608 dst.red = ( src.red | dst.red); 1609 dst.green = ( src.green | dst.green); 1610 dst.blue = ( src.blue | dst.blue); 1611 }, 1612 1613 /* NOR */ 1614 0x8: function (src, dst) { 1615 dst.red = 0xFF & ~( src.red | dst.red); 1616 dst.green = 0xFF & ~( src.green | dst.green); 1617 dst.blue = 0xFF & ~( src.blue | dst.blue); 1618 }, 1619 1620 /* XOR */ 1621 0x6: function (src, dst) { 1622 dst.red = ( src.red ^ dst.red); 1623 dst.green = ( src.green ^ dst.green); 1624 dst.blue = ( src.blue ^ dst.blue); 1625 }, 1626 1627 /* XNOR */ 1628 0x9: function (src, dst) { 1629 dst.red = 0xFF & ~( src.red ^ dst.red); 1630 dst.green = 0xFF & ~( src.green ^ dst.green); 1631 dst.blue = 0xFF & ~( src.blue ^ dst.blue); 1632 }, 1633 1634 /* AND inverted source */ 1635 0x4: function (src, dst) { 1636 dst.red = 0xFF & (~src.red & dst.red); 1637 dst.green = 0xFF & (~src.green & dst.green); 1638 dst.blue = 0xFF & (~src.blue & dst.blue); 1639 }, 1640 1641 /* OR inverted source */ 1642 0xD: function (src, dst) { 1643 dst.red = 0xFF & (~src.red | dst.red); 1644 dst.green = 0xFF & (~src.green | dst.green); 1645 dst.blue = 0xFF & (~src.blue | dst.blue); 1646 }, 1647 1648 /* AND inverted destination */ 1649 0x2: function (src, dst) { 1650 dst.red = 0xFF & ( src.red & ~dst.red); 1651 dst.green = 0xFF & ( src.green & ~dst.green); 1652 dst.blue = 0xFF & ( src.blue & ~dst.blue); 1653 }, 1654 1655 /* OR inverted destination */ 1656 0xB: function (src, dst) { 1657 dst.red = 0xFF & ( src.red | ~dst.red); 1658 dst.green = 0xFF & ( src.green | ~dst.green); 1659 dst.blue = 0xFF & ( src.blue | ~dst.blue); 1660 } 1661 1662 }; 1663