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