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