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