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