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 * Guacamole protocol client. Given a {@link Guacamole.Tunnel}, 27 * automatically handles incoming and outgoing Guacamole instructions via the 28 * provided tunnel, updating its display using one or more canvas elements. 29 * 30 * @constructor 31 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive 32 * Guacamole instructions. 33 */ 34 Guacamole.Client = function(tunnel) { 35 36 var guac_client = this; 37 38 var STATE_IDLE = 0; 39 var STATE_CONNECTING = 1; 40 var STATE_WAITING = 2; 41 var STATE_CONNECTED = 3; 42 var STATE_DISCONNECTING = 4; 43 var STATE_DISCONNECTED = 5; 44 45 var currentState = STATE_IDLE; 46 47 var currentTimestamp = 0; 48 var pingInterval = null; 49 50 /** 51 * Translation from Guacamole protocol line caps to Layer line caps. 52 * @private 53 */ 54 var lineCap = { 55 0: "butt", 56 1: "round", 57 2: "square" 58 }; 59 60 /** 61 * Translation from Guacamole protocol line caps to Layer line caps. 62 * @private 63 */ 64 var lineJoin = { 65 0: "bevel", 66 1: "miter", 67 2: "round" 68 }; 69 70 /** 71 * The underlying Guacamole display. 72 */ 73 var display = new Guacamole.Display(); 74 75 /** 76 * All available layers and buffers 77 */ 78 var layers = {}; 79 80 // No initial parsers 81 var parsers = []; 82 83 // No initial audio channels 84 var audio_channels = []; 85 86 // No initial streams 87 var streams = []; 88 89 // Pool of available stream indices 90 var stream_indices = new Guacamole.IntegerPool(); 91 92 // Array of allocated output streams by index 93 var output_streams = []; 94 95 function setState(state) { 96 if (state != currentState) { 97 currentState = state; 98 if (guac_client.onstatechange) 99 guac_client.onstatechange(currentState); 100 } 101 } 102 103 function isConnected() { 104 return currentState == STATE_CONNECTED 105 || currentState == STATE_WAITING; 106 } 107 108 /** 109 * Returns the underlying display of this Guacamole.Client. The display 110 * contains an Element which can be added to the DOM, causing the 111 * display to become visible. 112 * 113 * @return {Guacamole.Display} The underlying display of this 114 * Guacamole.Client. 115 */ 116 this.getDisplay = function() { 117 return display; 118 }; 119 120 /** 121 * Sends the current size of the screen. 122 * 123 * @param {Number} width The width of the screen. 124 * @param {Number} height The height of the screen. 125 */ 126 this.sendSize = function(width, height) { 127 128 // Do not send requests if not connected 129 if (!isConnected()) 130 return; 131 132 tunnel.sendMessage("size", width, height); 133 134 }; 135 136 /** 137 * Sends a key event having the given properties as if the user 138 * pressed or released a key. 139 * 140 * @param {Boolean} pressed Whether the key is pressed (true) or released 141 * (false). 142 * @param {Number} keysym The keysym of the key being pressed or released. 143 */ 144 this.sendKeyEvent = function(pressed, keysym) { 145 // Do not send requests if not connected 146 if (!isConnected()) 147 return; 148 149 tunnel.sendMessage("key", keysym, pressed); 150 }; 151 152 /** 153 * Sends a mouse event having the properties provided by the given mouse 154 * state. 155 * 156 * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send 157 * in the mouse event. 158 */ 159 this.sendMouseState = function(mouseState) { 160 161 // Do not send requests if not connected 162 if (!isConnected()) 163 return; 164 165 // Update client-side cursor 166 display.moveCursor( 167 Math.floor(mouseState.x), 168 Math.floor(mouseState.y) 169 ); 170 171 // Build mask 172 var buttonMask = 0; 173 if (mouseState.left) buttonMask |= 1; 174 if (mouseState.middle) buttonMask |= 2; 175 if (mouseState.right) buttonMask |= 4; 176 if (mouseState.up) buttonMask |= 8; 177 if (mouseState.down) buttonMask |= 16; 178 179 // Send message 180 tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask); 181 }; 182 183 /** 184 * Sets the clipboard of the remote client to the given text data. 185 * 186 * @deprecated Use createClipboardStream() instead. 187 * @param {String} data The data to send as the clipboard contents. 188 */ 189 this.setClipboard = function(data) { 190 191 // Do not send requests if not connected 192 if (!isConnected()) 193 return; 194 195 // Open stream 196 var stream = guac_client.createClipboardStream("text/plain"); 197 var writer = new Guacamole.StringWriter(stream); 198 199 // Send text chunks 200 for (var i=0; i<data.length; i += 4096) 201 writer.sendText(data.substring(i, i+4096)); 202 203 // Close stream 204 writer.sendEnd(); 205 206 }; 207 208 /** 209 * Opens a new file for writing, having the given index, mimetype and 210 * filename. 211 * 212 * @param {String} mimetype The mimetype of the file being sent. 213 * @param {String} filename The filename of the file being sent. 214 * @return {Guacamole.OutputStream} The created file stream. 215 */ 216 this.createFileStream = function(mimetype, filename) { 217 218 // Allocate index 219 var index = stream_indices.next(); 220 221 // Create new stream 222 tunnel.sendMessage("file", index, mimetype, filename); 223 var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); 224 225 // Override sendEnd() of stream to automatically free index 226 var old_end = stream.sendEnd; 227 stream.sendEnd = function() { 228 old_end(); 229 stream_indices.free(index); 230 delete output_streams[index]; 231 }; 232 233 // Return new, overridden stream 234 return stream; 235 236 }; 237 238 /** 239 * Opens a new pipe for writing, having the given name and mimetype. 240 * 241 * @param {String} mimetype The mimetype of the data being sent. 242 * @param {String} name The name of the pipe. 243 * @return {Guacamole.OutputStream} The created file stream. 244 */ 245 this.createPipeStream = function(mimetype, name) { 246 247 // Allocate index 248 var index = stream_indices.next(); 249 250 // Create new stream 251 tunnel.sendMessage("pipe", index, mimetype, name); 252 var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); 253 254 // Override sendEnd() of stream to automatically free index 255 var old_end = stream.sendEnd; 256 stream.sendEnd = function() { 257 old_end(); 258 stream_indices.free(index); 259 delete output_streams[index]; 260 }; 261 262 // Return new, overridden stream 263 return stream; 264 265 }; 266 267 /** 268 * Opens a new clipboard object for writing, having the given mimetype. 269 * 270 * @param {String} mimetype The mimetype of the data being sent. 271 * @param {String} name The name of the pipe. 272 * @return {Guacamole.OutputStream} The created file stream. 273 */ 274 this.createClipboardStream = function(mimetype) { 275 276 // Allocate index 277 var index = stream_indices.next(); 278 279 // Create new stream 280 tunnel.sendMessage("clipboard", index, mimetype); 281 var stream = output_streams[index] = new Guacamole.OutputStream(guac_client, index); 282 283 // Override sendEnd() of stream to automatically free index 284 var old_end = stream.sendEnd; 285 stream.sendEnd = function() { 286 old_end(); 287 stream_indices.free(index); 288 delete output_streams[index]; 289 }; 290 291 // Return new, overridden stream 292 return stream; 293 294 }; 295 296 /** 297 * Acknowledge receipt of a blob on the stream with the given index. 298 * 299 * @param {Number} index The index of the stream associated with the 300 * received blob. 301 * @param {String} message A human-readable message describing the error 302 * or status. 303 * @param {Number} code The error code, if any, or 0 for success. 304 */ 305 this.sendAck = function(index, message, code) { 306 307 // Do not send requests if not connected 308 if (!isConnected()) 309 return; 310 311 tunnel.sendMessage("ack", index, message, code); 312 }; 313 314 /** 315 * Given the index of a file, writes a blob of data to that file. 316 * 317 * @param {Number} index The index of the file to write to. 318 * @param {String} data Base64-encoded data to write to the file. 319 */ 320 this.sendBlob = function(index, data) { 321 322 // Do not send requests if not connected 323 if (!isConnected()) 324 return; 325 326 tunnel.sendMessage("blob", index, data); 327 }; 328 329 /** 330 * Marks a currently-open stream as complete. 331 * 332 * @param {Number} index The index of the stream to end. 333 */ 334 this.endStream = function(index) { 335 336 // Do not send requests if not connected 337 if (!isConnected()) 338 return; 339 340 tunnel.sendMessage("end", index); 341 }; 342 343 /** 344 * Fired whenever the state of this Guacamole.Client changes. 345 * 346 * @event 347 * @param {Number} state The new state of the client. 348 */ 349 this.onstatechange = null; 350 351 /** 352 * Fired when the remote client sends a name update. 353 * 354 * @event 355 * @param {String} name The new name of this client. 356 */ 357 this.onname = null; 358 359 /** 360 * Fired when an error is reported by the remote client, and the connection 361 * is being closed. 362 * 363 * @event 364 * @param {Guacamole.Status} status A status object which describes the 365 * error. 366 */ 367 this.onerror = null; 368 369 /** 370 * Fired when the clipboard of the remote client is changing. 371 * 372 * @event 373 * @param {Guacamole.InputStream} stream The stream that will receive 374 * clipboard data from the server. 375 * @param {String} mimetype The mimetype of the data which will be received. 376 */ 377 this.onclipboard = null; 378 379 /** 380 * Fired when a file stream is created. The stream provided to this event 381 * handler will contain its own event handlers for received data. 382 * 383 * @event 384 * @param {Guacamole.InputStream} stream The stream that will receive data 385 * from the server. 386 * @param {String} mimetype The mimetype of the file received. 387 * @param {String} filename The name of the file received. 388 */ 389 this.onfile = null; 390 391 /** 392 * Fired when a pipe stream is created. The stream provided to this event 393 * handler will contain its own event handlers for received data; 394 * 395 * @event 396 * @param {Guacamole.InputStream} stream The stream that will receive data 397 * from the server. 398 * @param {String} mimetype The mimetype of the data which will be received. 399 * @param {String} name The name of the pipe. 400 */ 401 this.onpipe = null; 402 403 /** 404 * Fired whenever a sync instruction is received from the server, indicating 405 * that the server is finished processing any input from the client and 406 * has sent any results. 407 * 408 * @event 409 * @param {Number} timestamp The timestamp associated with the sync 410 * instruction. 411 */ 412 this.onsync = null; 413 414 /** 415 * Returns the layer with the given index, creating it if necessary. 416 * Positive indices refer to visible layers, an index of zero refers to 417 * the default layer, and negative indices refer to buffers. 418 * 419 * @param {Number} index The index of the layer to retrieve. 420 * @return {Guacamole.Display.VisibleLayer|Guacamole.Layer} The layer having the given index. 421 */ 422 function getLayer(index) { 423 424 // Get layer, create if necessary 425 var layer = layers[index]; 426 if (!layer) { 427 428 // Create layer based on index 429 if (index === 0) 430 layer = display.getDefaultLayer(); 431 else if (index > 0) 432 layer = display.createLayer(); 433 else 434 layer = display.createBuffer(); 435 436 // Add new layer 437 layers[index] = layer; 438 439 } 440 441 return layer; 442 443 } 444 445 function getParser(index) { 446 447 var parser = parsers[index]; 448 449 // If parser not yet created, create it, and tie to the 450 // oninstruction handler of the tunnel. 451 if (parser == null) { 452 parser = parsers[index] = new Guacamole.Parser(); 453 parser.oninstruction = tunnel.oninstruction; 454 } 455 456 return parser; 457 458 } 459 460 function getAudioChannel(index) { 461 462 var audio_channel = audio_channels[index]; 463 464 // If audio channel not yet created, create it 465 if (audio_channel == null) 466 audio_channel = audio_channels[index] = new Guacamole.AudioChannel(); 467 468 return audio_channel; 469 470 } 471 472 /** 473 * Handlers for all defined layer properties. 474 * @private 475 */ 476 var layerPropertyHandlers = { 477 478 "miter-limit": function(layer, value) { 479 display.setMiterLimit(layer, parseFloat(value)); 480 } 481 482 }; 483 484 /** 485 * Handlers for all instruction opcodes receivable by a Guacamole protocol 486 * client. 487 * @private 488 */ 489 var instructionHandlers = { 490 491 "ack": function(parameters) { 492 493 var stream_index = parseInt(parameters[0]); 494 var reason = parameters[1]; 495 var code = parseInt(parameters[2]); 496 497 // Get stream 498 var stream = output_streams[stream_index]; 499 if (stream) { 500 501 // Signal ack if handler defined 502 if (stream.onack) 503 stream.onack(new Guacamole.Status(code, reason)); 504 505 // If code is an error, invalidate stream 506 if (code >= 0x0100) { 507 stream_indices.free(stream_index); 508 delete output_streams[stream_index]; 509 } 510 511 } 512 513 }, 514 515 "arc": function(parameters) { 516 517 var layer = getLayer(parseInt(parameters[0])); 518 var x = parseInt(parameters[1]); 519 var y = parseInt(parameters[2]); 520 var radius = parseInt(parameters[3]); 521 var startAngle = parseFloat(parameters[4]); 522 var endAngle = parseFloat(parameters[5]); 523 var negative = parseInt(parameters[6]); 524 525 display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0); 526 527 }, 528 529 "audio": function(parameters) { 530 531 var stream_index = parseInt(parameters[0]); 532 var channel = getAudioChannel(parseInt(parameters[1])); 533 var mimetype = parameters[2]; 534 var duration = parseFloat(parameters[3]); 535 536 // Create stream 537 var stream = streams[stream_index] = 538 new Guacamole.InputStream(guac_client, stream_index); 539 540 // Assemble entire stream as a blob 541 var blob_reader = new Guacamole.BlobReader(stream, mimetype); 542 543 // Play blob as audio 544 blob_reader.onend = function() { 545 channel.play(mimetype, duration, blob_reader.getBlob()); 546 }; 547 548 // Send success response 549 guac_client.sendAck(stream_index, "OK", 0x0000); 550 551 }, 552 553 "blob": function(parameters) { 554 555 // Get stream 556 var stream_index = parseInt(parameters[0]); 557 var data = parameters[1]; 558 var stream = streams[stream_index]; 559 560 // Write data 561 stream.onblob(data); 562 563 }, 564 565 "cfill": function(parameters) { 566 567 var channelMask = parseInt(parameters[0]); 568 var layer = getLayer(parseInt(parameters[1])); 569 var r = parseInt(parameters[2]); 570 var g = parseInt(parameters[3]); 571 var b = parseInt(parameters[4]); 572 var a = parseInt(parameters[5]); 573 574 display.setChannelMask(layer, channelMask); 575 display.fillColor(layer, r, g, b, a); 576 577 }, 578 579 "clip": function(parameters) { 580 581 var layer = getLayer(parseInt(parameters[0])); 582 583 display.clip(layer); 584 585 }, 586 587 "clipboard": function(parameters) { 588 589 var stream_index = parseInt(parameters[0]); 590 var mimetype = parameters[1]; 591 592 // Create stream 593 if (guac_client.onclipboard) { 594 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 595 guac_client.onclipboard(stream, mimetype); 596 } 597 598 // Otherwise, unsupported 599 else 600 guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100); 601 602 }, 603 604 "close": function(parameters) { 605 606 var layer = getLayer(parseInt(parameters[0])); 607 608 display.close(layer); 609 610 }, 611 612 "copy": function(parameters) { 613 614 var srcL = getLayer(parseInt(parameters[0])); 615 var srcX = parseInt(parameters[1]); 616 var srcY = parseInt(parameters[2]); 617 var srcWidth = parseInt(parameters[3]); 618 var srcHeight = parseInt(parameters[4]); 619 var channelMask = parseInt(parameters[5]); 620 var dstL = getLayer(parseInt(parameters[6])); 621 var dstX = parseInt(parameters[7]); 622 var dstY = parseInt(parameters[8]); 623 624 display.setChannelMask(dstL, channelMask); 625 display.copy(srcL, srcX, srcY, srcWidth, srcHeight, 626 dstL, dstX, dstY); 627 628 }, 629 630 "cstroke": function(parameters) { 631 632 var channelMask = parseInt(parameters[0]); 633 var layer = getLayer(parseInt(parameters[1])); 634 var cap = lineCap[parseInt(parameters[2])]; 635 var join = lineJoin[parseInt(parameters[3])]; 636 var thickness = parseInt(parameters[4]); 637 var r = parseInt(parameters[5]); 638 var g = parseInt(parameters[6]); 639 var b = parseInt(parameters[7]); 640 var a = parseInt(parameters[8]); 641 642 display.setChannelMask(layer, channelMask); 643 display.strokeColor(layer, cap, join, thickness, r, g, b, a); 644 645 }, 646 647 "cursor": function(parameters) { 648 649 var cursorHotspotX = parseInt(parameters[0]); 650 var cursorHotspotY = parseInt(parameters[1]); 651 var srcL = getLayer(parseInt(parameters[2])); 652 var srcX = parseInt(parameters[3]); 653 var srcY = parseInt(parameters[4]); 654 var srcWidth = parseInt(parameters[5]); 655 var srcHeight = parseInt(parameters[6]); 656 657 display.setCursor(cursorHotspotX, cursorHotspotY, 658 srcL, srcX, srcY, srcWidth, srcHeight); 659 660 }, 661 662 "curve": function(parameters) { 663 664 var layer = getLayer(parseInt(parameters[0])); 665 var cp1x = parseInt(parameters[1]); 666 var cp1y = parseInt(parameters[2]); 667 var cp2x = parseInt(parameters[3]); 668 var cp2y = parseInt(parameters[4]); 669 var x = parseInt(parameters[5]); 670 var y = parseInt(parameters[6]); 671 672 display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y); 673 674 }, 675 676 "dispose": function(parameters) { 677 678 var layer_index = parseInt(parameters[0]); 679 680 // If visible layer, remove from parent 681 if (layer_index > 0) { 682 683 // Remove from parent 684 var layer = getLayer(layer_index); 685 layer.dispose(); 686 687 // Delete reference 688 delete layers[layer_index]; 689 690 } 691 692 // If buffer, just delete reference 693 else if (layer_index < 0) 694 delete layers[layer_index]; 695 696 // Attempting to dispose the root layer currently has no effect. 697 698 }, 699 700 "distort": function(parameters) { 701 702 var layer_index = parseInt(parameters[0]); 703 var a = parseFloat(parameters[1]); 704 var b = parseFloat(parameters[2]); 705 var c = parseFloat(parameters[3]); 706 var d = parseFloat(parameters[4]); 707 var e = parseFloat(parameters[5]); 708 var f = parseFloat(parameters[6]); 709 710 // Only valid for visible layers (not buffers) 711 if (layer_index >= 0) { 712 var layer = getLayer(layer_index); 713 layer.distort(a, b, c, d, e, f); 714 } 715 716 }, 717 718 "error": function(parameters) { 719 720 var reason = parameters[0]; 721 var code = parseInt(parameters[1]); 722 723 // Call handler if defined 724 if (guac_client.onerror) 725 guac_client.onerror(new Guacamole.Status(code, reason)); 726 727 guac_client.disconnect(); 728 729 }, 730 731 "end": function(parameters) { 732 733 // Get stream 734 var stream_index = parseInt(parameters[0]); 735 var stream = streams[stream_index]; 736 737 // Signal end of stream 738 if (stream.onend) 739 stream.onend(); 740 741 }, 742 743 "file": function(parameters) { 744 745 var stream_index = parseInt(parameters[0]); 746 var mimetype = parameters[1]; 747 var filename = parameters[2]; 748 749 // Create stream 750 if (guac_client.onfile) { 751 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 752 guac_client.onfile(stream, mimetype, filename); 753 } 754 755 // Otherwise, unsupported 756 else 757 guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100); 758 759 }, 760 761 "identity": function(parameters) { 762 763 var layer = getLayer(parseInt(parameters[0])); 764 765 display.setTransform(layer, 1, 0, 0, 1, 0, 0); 766 767 }, 768 769 "lfill": function(parameters) { 770 771 var channelMask = parseInt(parameters[0]); 772 var layer = getLayer(parseInt(parameters[1])); 773 var srcLayer = getLayer(parseInt(parameters[2])); 774 775 display.setChannelMask(layer, channelMask); 776 display.fillLayer(layer, srcLayer); 777 778 }, 779 780 "line": function(parameters) { 781 782 var layer = getLayer(parseInt(parameters[0])); 783 var x = parseInt(parameters[1]); 784 var y = parseInt(parameters[2]); 785 786 display.lineTo(layer, x, y); 787 788 }, 789 790 "lstroke": function(parameters) { 791 792 var channelMask = parseInt(parameters[0]); 793 var layer = getLayer(parseInt(parameters[1])); 794 var srcLayer = getLayer(parseInt(parameters[2])); 795 796 display.setChannelMask(layer, channelMask); 797 display.strokeLayer(layer, srcLayer); 798 799 }, 800 801 "move": function(parameters) { 802 803 var layer_index = parseInt(parameters[0]); 804 var parent_index = parseInt(parameters[1]); 805 var x = parseInt(parameters[2]); 806 var y = parseInt(parameters[3]); 807 var z = parseInt(parameters[4]); 808 809 // Only valid for non-default layers 810 if (layer_index > 0 && parent_index >= 0) { 811 var layer = getLayer(layer_index); 812 var parent = getLayer(parent_index); 813 layer.move(parent, x, y, z); 814 } 815 816 }, 817 818 "name": function(parameters) { 819 if (guac_client.onname) guac_client.onname(parameters[0]); 820 }, 821 822 "nest": function(parameters) { 823 var parser = getParser(parseInt(parameters[0])); 824 parser.receive(parameters[1]); 825 }, 826 827 "pipe": function(parameters) { 828 829 var stream_index = parseInt(parameters[0]); 830 var mimetype = parameters[1]; 831 var name = parameters[2]; 832 833 // Create stream 834 if (guac_client.onpipe) { 835 var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); 836 guac_client.onpipe(stream, mimetype, name); 837 } 838 839 // Otherwise, unsupported 840 else 841 guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100); 842 843 }, 844 845 "png": function(parameters) { 846 847 var channelMask = parseInt(parameters[0]); 848 var layer = getLayer(parseInt(parameters[1])); 849 var x = parseInt(parameters[2]); 850 var y = parseInt(parameters[3]); 851 var data = parameters[4]; 852 853 display.setChannelMask(layer, channelMask); 854 display.draw(layer, x, y, "data:image/png;base64," + data); 855 856 }, 857 858 "pop": function(parameters) { 859 860 var layer = getLayer(parseInt(parameters[0])); 861 862 display.pop(layer); 863 864 }, 865 866 "push": function(parameters) { 867 868 var layer = getLayer(parseInt(parameters[0])); 869 870 display.push(layer); 871 872 }, 873 874 "rect": function(parameters) { 875 876 var layer = getLayer(parseInt(parameters[0])); 877 var x = parseInt(parameters[1]); 878 var y = parseInt(parameters[2]); 879 var w = parseInt(parameters[3]); 880 var h = parseInt(parameters[4]); 881 882 display.rect(layer, x, y, w, h); 883 884 }, 885 886 "reset": function(parameters) { 887 888 var layer = getLayer(parseInt(parameters[0])); 889 890 display.reset(layer); 891 892 }, 893 894 "set": function(parameters) { 895 896 var layer = getLayer(parseInt(parameters[0])); 897 var name = parameters[1]; 898 var value = parameters[2]; 899 900 // Call property handler if defined 901 var handler = layerPropertyHandlers[name]; 902 if (handler) 903 handler(layer, value); 904 905 }, 906 907 "shade": function(parameters) { 908 909 var layer_index = parseInt(parameters[0]); 910 var a = parseInt(parameters[1]); 911 912 // Only valid for visible layers (not buffers) 913 if (layer_index >= 0) { 914 var layer = getLayer(layer_index); 915 layer.shade(a); 916 } 917 918 }, 919 920 "size": function(parameters) { 921 922 var layer_index = parseInt(parameters[0]); 923 var layer = getLayer(layer_index); 924 var width = parseInt(parameters[1]); 925 var height = parseInt(parameters[2]); 926 927 display.resize(layer, width, height); 928 929 }, 930 931 "start": function(parameters) { 932 933 var layer = getLayer(parseInt(parameters[0])); 934 var x = parseInt(parameters[1]); 935 var y = parseInt(parameters[2]); 936 937 display.moveTo(layer, x, y); 938 939 }, 940 941 "sync": function(parameters) { 942 943 var timestamp = parseInt(parameters[0]); 944 945 // Flush display, send sync when done 946 display.flush(function __send_sync_response() { 947 if (timestamp !== currentTimestamp) { 948 tunnel.sendMessage("sync", timestamp); 949 currentTimestamp = timestamp; 950 } 951 }); 952 953 // If received first update, no longer waiting. 954 if (currentState === STATE_WAITING) 955 setState(STATE_CONNECTED); 956 957 // Call sync handler if defined 958 if (guac_client.onsync) 959 guac_client.onsync(timestamp); 960 961 }, 962 963 "transfer": function(parameters) { 964 965 var srcL = getLayer(parseInt(parameters[0])); 966 var srcX = parseInt(parameters[1]); 967 var srcY = parseInt(parameters[2]); 968 var srcWidth = parseInt(parameters[3]); 969 var srcHeight = parseInt(parameters[4]); 970 var function_index = parseInt(parameters[5]); 971 var dstL = getLayer(parseInt(parameters[6])); 972 var dstX = parseInt(parameters[7]); 973 var dstY = parseInt(parameters[8]); 974 975 /* SRC */ 976 if (function_index === 0x3) 977 display.put(srcL, srcX, srcY, srcWidth, srcHeight, 978 dstL, dstX, dstY); 979 980 /* Anything else that isn't a NO-OP */ 981 else if (function_index !== 0x5) 982 display.transfer(srcL, srcX, srcY, srcWidth, srcHeight, 983 dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]); 984 985 }, 986 987 "transform": function(parameters) { 988 989 var layer = getLayer(parseInt(parameters[0])); 990 var a = parseFloat(parameters[1]); 991 var b = parseFloat(parameters[2]); 992 var c = parseFloat(parameters[3]); 993 var d = parseFloat(parameters[4]); 994 var e = parseFloat(parameters[5]); 995 var f = parseFloat(parameters[6]); 996 997 display.transform(layer, a, b, c, d, e, f); 998 999 }, 1000 1001 "video": function(parameters) { 1002 1003 var stream_index = parseInt(parameters[0]); 1004 var layer = getLayer(parseInt(parameters[1])); 1005 var mimetype = parameters[2]; 1006 var duration = parseFloat(parameters[3]); 1007 1008 // Create stream 1009 var stream = streams[stream_index] = 1010 new Guacamole.InputStream(guac_client, stream_index); 1011 1012 // Assemble entire stream as a blob 1013 var blob_reader = new Guacamole.BlobReader(stream, mimetype); 1014 1015 // Play video once finished 1016 blob_reader.onend = function() { 1017 1018 // Read data from blob from stream 1019 var reader = new FileReader(); 1020 reader.onload = function() { 1021 1022 var binary = ""; 1023 var bytes = new Uint8Array(reader.result); 1024 1025 // Produce binary string from bytes in buffer 1026 for (var i=0; i<bytes.byteLength; i++) 1027 binary += String.fromCharCode(bytes[i]); 1028 1029 // Play video 1030 layer.play(mimetype, duration, "data:" + mimetype + ";base64," + window.btoa(binary)); 1031 1032 }; 1033 reader.readAsArrayBuffer(blob_reader.getBlob()); 1034 1035 }; 1036 1037 // Send success response 1038 tunnel.sendMessage("ack", stream_index, "OK", 0x0000); 1039 1040 } 1041 1042 }; 1043 1044 tunnel.oninstruction = function(opcode, parameters) { 1045 1046 var handler = instructionHandlers[opcode]; 1047 if (handler) 1048 handler(parameters); 1049 1050 }; 1051 1052 /** 1053 * Sends a disconnect instruction to the server and closes the tunnel. 1054 */ 1055 this.disconnect = function() { 1056 1057 // Only attempt disconnection not disconnected. 1058 if (currentState != STATE_DISCONNECTED 1059 && currentState != STATE_DISCONNECTING) { 1060 1061 setState(STATE_DISCONNECTING); 1062 1063 // Stop ping 1064 if (pingInterval) 1065 window.clearInterval(pingInterval); 1066 1067 // Send disconnect message and disconnect 1068 tunnel.sendMessage("disconnect"); 1069 tunnel.disconnect(); 1070 setState(STATE_DISCONNECTED); 1071 1072 } 1073 1074 }; 1075 1076 /** 1077 * Connects the underlying tunnel of this Guacamole.Client, passing the 1078 * given arbitrary data to the tunnel during the connection process. 1079 * 1080 * @param data Arbitrary connection data to be sent to the underlying 1081 * tunnel during the connection process. 1082 * @throws {Guacamole.Status} If an error occurs during connection. 1083 */ 1084 this.connect = function(data) { 1085 1086 setState(STATE_CONNECTING); 1087 1088 try { 1089 tunnel.connect(data); 1090 } 1091 catch (status) { 1092 setState(STATE_IDLE); 1093 throw status; 1094 } 1095 1096 // Ping every 5 seconds (ensure connection alive) 1097 pingInterval = window.setInterval(function() { 1098 tunnel.sendMessage("sync", currentTimestamp); 1099 }, 5000); 1100 1101 setState(STATE_WAITING); 1102 }; 1103 1104 }; 1105 1106 /** 1107 * Map of all Guacamole binary raster operations to transfer functions. 1108 * @private 1109 */ 1110 Guacamole.Client.DefaultTransferFunction = { 1111 1112 /* BLACK */ 1113 0x0: function (src, dst) { 1114 dst.red = dst.green = dst.blue = 0x00; 1115 }, 1116 1117 /* WHITE */ 1118 0xF: function (src, dst) { 1119 dst.red = dst.green = dst.blue = 0xFF; 1120 }, 1121 1122 /* SRC */ 1123 0x3: function (src, dst) { 1124 dst.red = src.red; 1125 dst.green = src.green; 1126 dst.blue = src.blue; 1127 dst.alpha = src.alpha; 1128 }, 1129 1130 /* DEST (no-op) */ 1131 0x5: function (src, dst) { 1132 // Do nothing 1133 }, 1134 1135 /* Invert SRC */ 1136 0xC: function (src, dst) { 1137 dst.red = 0xFF & ~src.red; 1138 dst.green = 0xFF & ~src.green; 1139 dst.blue = 0xFF & ~src.blue; 1140 dst.alpha = src.alpha; 1141 }, 1142 1143 /* Invert DEST */ 1144 0xA: function (src, dst) { 1145 dst.red = 0xFF & ~dst.red; 1146 dst.green = 0xFF & ~dst.green; 1147 dst.blue = 0xFF & ~dst.blue; 1148 }, 1149 1150 /* AND */ 1151 0x1: function (src, dst) { 1152 dst.red = ( src.red & dst.red); 1153 dst.green = ( src.green & dst.green); 1154 dst.blue = ( src.blue & dst.blue); 1155 }, 1156 1157 /* NAND */ 1158 0xE: function (src, dst) { 1159 dst.red = 0xFF & ~( src.red & dst.red); 1160 dst.green = 0xFF & ~( src.green & dst.green); 1161 dst.blue = 0xFF & ~( src.blue & dst.blue); 1162 }, 1163 1164 /* OR */ 1165 0x7: function (src, dst) { 1166 dst.red = ( src.red | dst.red); 1167 dst.green = ( src.green | dst.green); 1168 dst.blue = ( src.blue | dst.blue); 1169 }, 1170 1171 /* NOR */ 1172 0x8: function (src, dst) { 1173 dst.red = 0xFF & ~( src.red | dst.red); 1174 dst.green = 0xFF & ~( src.green | dst.green); 1175 dst.blue = 0xFF & ~( src.blue | dst.blue); 1176 }, 1177 1178 /* XOR */ 1179 0x6: function (src, dst) { 1180 dst.red = ( src.red ^ dst.red); 1181 dst.green = ( src.green ^ dst.green); 1182 dst.blue = ( src.blue ^ dst.blue); 1183 }, 1184 1185 /* XNOR */ 1186 0x9: function (src, dst) { 1187 dst.red = 0xFF & ~( src.red ^ dst.red); 1188 dst.green = 0xFF & ~( src.green ^ dst.green); 1189 dst.blue = 0xFF & ~( src.blue ^ dst.blue); 1190 }, 1191 1192 /* AND inverted source */ 1193 0x4: function (src, dst) { 1194 dst.red = 0xFF & (~src.red & dst.red); 1195 dst.green = 0xFF & (~src.green & dst.green); 1196 dst.blue = 0xFF & (~src.blue & dst.blue); 1197 }, 1198 1199 /* OR inverted source */ 1200 0xD: function (src, dst) { 1201 dst.red = 0xFF & (~src.red | dst.red); 1202 dst.green = 0xFF & (~src.green | dst.green); 1203 dst.blue = 0xFF & (~src.blue | dst.blue); 1204 }, 1205 1206 /* AND inverted destination */ 1207 0x2: function (src, dst) { 1208 dst.red = 0xFF & ( src.red & ~dst.red); 1209 dst.green = 0xFF & ( src.green & ~dst.green); 1210 dst.blue = 0xFF & ( src.blue & ~dst.blue); 1211 }, 1212 1213 /* OR inverted destination */ 1214 0xB: function (src, dst) { 1215 dst.red = 0xFF & ( src.red | ~dst.red); 1216 dst.green = 0xFF & ( src.green | ~dst.green); 1217 dst.blue = 0xFF & ( src.blue | ~dst.blue); 1218 } 1219 1220 }; 1221