/* * Copyright (C) 2013 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ var Guacamole = Guacamole || {}; /** * Guacamole protocol client. Given a {@link Guacamole.Tunnel}, * automatically handles incoming and outgoing Guacamole instructions via the * provided tunnel, updating its display using one or more canvas elements. * * @constructor * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive * Guacamole instructions. */ Guacamole.Client = function(tunnel) { var guac_client = this; var STATE_IDLE = 0; var STATE_CONNECTING = 1; var STATE_WAITING = 2; var STATE_CONNECTED = 3; var STATE_DISCONNECTING = 4; var STATE_DISCONNECTED = 5; var currentState = STATE_IDLE; var currentTimestamp = 0; var pingInterval = null; /** * Translation from Guacamole protocol line caps to Layer line caps. * @private */ var lineCap = { 0: "butt", 1: "round", 2: "square" }; /** * Translation from Guacamole protocol line caps to Layer line caps. * @private */ var lineJoin = { 0: "bevel", 1: "miter", 2: "round" }; /** * The underlying Guacamole display. */ var display = new Guacamole.Display(); /** * All available layers and buffers */ var layers = {}; // No initial parsers var parsers = []; // No initial audio channels var audio_channels = []; // No initial streams var streams = []; /** * All current objects. The index of each object is dictated by the * Guacamole server. * * @type Guacamole.Object[] */ var objects = []; // Pool of available stream indices var stream_indices = new Guacamole.IntegerPool(); // Array of allocated output streams by index var output_streams = []; function setState(state) { if (state != currentState) { currentState = state; if (guac_client.onstatechange) guac_client.onstatechange(currentState); } } function isConnected() { return currentState == STATE_CONNECTED || currentState == STATE_WAITING; } /** * Returns the underlying display of this Guacamole.Client. The display * contains an Element which can be added to the DOM, causing the * display to become visible. * * @return {Guacamole.Display} The underlying display of this * Guacamole.Client. */ this.getDisplay = function() { return display; }; /** * Sends the current size of the screen. * * @param {Number} width The width of the screen. * @param {Number} height The height of the screen. */ this.sendSize = function(width, height) { // Do not send requests if not connected if (!isConnected()) return; tunnel.sendMessage("size", width, height); }; /** * Sends a key event having the given properties as if the user * pressed or released a key. * * @param {Boolean} pressed Whether the key is pressed (true) or released * (false). * @param {Number} keysym The keysym of the key being pressed or released. */ this.sendKeyEvent = function(pressed, keysym) { // Do not send requests if not connected if (!isConnected()) return; tunnel.sendMessage("key", keysym, pressed); }; /** * Sends a mouse event having the properties provided by the given mouse * state. * * @param {Guacamole.Mouse.State} mouseState The state of the mouse to send * in the mouse event. */ this.sendMouseState = function(mouseState) { // Do not send requests if not connected if (!isConnected()) return; // Update client-side cursor display.moveCursor( Math.floor(mouseState.x), Math.floor(mouseState.y) ); // Build mask var buttonMask = 0; if (mouseState.left) buttonMask |= 1; if (mouseState.middle) buttonMask |= 2; if (mouseState.right) buttonMask |= 4; if (mouseState.up) buttonMask |= 8; if (mouseState.down) buttonMask |= 16; // Send message tunnel.sendMessage("mouse", Math.floor(mouseState.x), Math.floor(mouseState.y), buttonMask); }; /** * Sets the clipboard of the remote client to the given text data. * * @deprecated Use createClipboardStream() instead. * @param {String} data The data to send as the clipboard contents. */ this.setClipboard = function(data) { // Do not send requests if not connected if (!isConnected()) return; // Open stream var stream = guac_client.createClipboardStream("text/plain"); var writer = new Guacamole.StringWriter(stream); // Send text chunks for (var i=0; i 0) layer = display.createLayer(); else layer = display.createBuffer(); // Add new layer layers[index] = layer; } return layer; } function getParser(index) { var parser = parsers[index]; // If parser not yet created, create it, and tie to the // oninstruction handler of the tunnel. if (parser == null) { parser = parsers[index] = new Guacamole.Parser(); parser.oninstruction = tunnel.oninstruction; } return parser; } function getAudioChannel(index) { var audio_channel = audio_channels[index]; // If audio channel not yet created, create it if (audio_channel == null) audio_channel = audio_channels[index] = new Guacamole.AudioChannel(); return audio_channel; } /** * Handlers for all defined layer properties. * @private */ var layerPropertyHandlers = { "miter-limit": function(layer, value) { display.setMiterLimit(layer, parseFloat(value)); } }; /** * Handlers for all instruction opcodes receivable by a Guacamole protocol * client. * @private */ var instructionHandlers = { "ack": function(parameters) { var stream_index = parseInt(parameters[0]); var reason = parameters[1]; var code = parseInt(parameters[2]); // Get stream var stream = output_streams[stream_index]; if (stream) { // Signal ack if handler defined if (stream.onack) stream.onack(new Guacamole.Status(code, reason)); // If code is an error, invalidate stream if (code >= 0x0100) { stream_indices.free(stream_index); delete output_streams[stream_index]; } } }, "arc": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); var radius = parseInt(parameters[3]); var startAngle = parseFloat(parameters[4]); var endAngle = parseFloat(parameters[5]); var negative = parseInt(parameters[6]); display.arc(layer, x, y, radius, startAngle, endAngle, negative != 0); }, "audio": function(parameters) { var stream_index = parseInt(parameters[0]); var channel = getAudioChannel(parseInt(parameters[1])); var mimetype = parameters[2]; var duration = parseFloat(parameters[3]); // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); // Assemble entire stream as a blob var blob_reader = new Guacamole.BlobReader(stream, mimetype); // Play blob as audio blob_reader.onend = function() { channel.play(mimetype, duration, blob_reader.getBlob()); }; // Send success response guac_client.sendAck(stream_index, "OK", 0x0000); }, "blob": function(parameters) { // Get stream var stream_index = parseInt(parameters[0]); var data = parameters[1]; var stream = streams[stream_index]; // Write data stream.onblob(data); }, "body" : function handleBody(parameters) { // Get object var objectIndex = parseInt(parameters[0]); var object = objects[objectIndex]; var streamIndex = parseInt(parameters[1]); var mimetype = parameters[2]; var name = parameters[3]; // Create stream if handler defined if (object && object.onbody) { var stream = streams[streamIndex] = new Guacamole.InputStream(guac_client, streamIndex); object.onbody(stream, mimetype, name); } // Otherwise, unsupported else guac_client.sendAck(streamIndex, "Receipt of body unsupported", 0x0100); }, "cfill": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var r = parseInt(parameters[2]); var g = parseInt(parameters[3]); var b = parseInt(parameters[4]); var a = parseInt(parameters[5]); display.setChannelMask(layer, channelMask); display.fillColor(layer, r, g, b, a); }, "clip": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.clip(layer); }, "clipboard": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; // Create stream if (guac_client.onclipboard) { var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); guac_client.onclipboard(stream, mimetype); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "Clipboard unsupported", 0x0100); }, "close": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.close(layer); }, "copy": function(parameters) { var srcL = getLayer(parseInt(parameters[0])); var srcX = parseInt(parameters[1]); var srcY = parseInt(parameters[2]); var srcWidth = parseInt(parameters[3]); var srcHeight = parseInt(parameters[4]); var channelMask = parseInt(parameters[5]); var dstL = getLayer(parseInt(parameters[6])); var dstX = parseInt(parameters[7]); var dstY = parseInt(parameters[8]); display.setChannelMask(dstL, channelMask); display.copy(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY); }, "cstroke": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var cap = lineCap[parseInt(parameters[2])]; var join = lineJoin[parseInt(parameters[3])]; var thickness = parseInt(parameters[4]); var r = parseInt(parameters[5]); var g = parseInt(parameters[6]); var b = parseInt(parameters[7]); var a = parseInt(parameters[8]); display.setChannelMask(layer, channelMask); display.strokeColor(layer, cap, join, thickness, r, g, b, a); }, "cursor": function(parameters) { var cursorHotspotX = parseInt(parameters[0]); var cursorHotspotY = parseInt(parameters[1]); var srcL = getLayer(parseInt(parameters[2])); var srcX = parseInt(parameters[3]); var srcY = parseInt(parameters[4]); var srcWidth = parseInt(parameters[5]); var srcHeight = parseInt(parameters[6]); display.setCursor(cursorHotspotX, cursorHotspotY, srcL, srcX, srcY, srcWidth, srcHeight); }, "curve": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var cp1x = parseInt(parameters[1]); var cp1y = parseInt(parameters[2]); var cp2x = parseInt(parameters[3]); var cp2y = parseInt(parameters[4]); var x = parseInt(parameters[5]); var y = parseInt(parameters[6]); display.curveTo(layer, cp1x, cp1y, cp2x, cp2y, x, y); }, "dispose": function(parameters) { var layer_index = parseInt(parameters[0]); // If visible layer, remove from parent if (layer_index > 0) { // Remove from parent var layer = getLayer(layer_index); layer.dispose(); // Delete reference delete layers[layer_index]; } // If buffer, just delete reference else if (layer_index < 0) delete layers[layer_index]; // Attempting to dispose the root layer currently has no effect. }, "distort": function(parameters) { var layer_index = parseInt(parameters[0]); var a = parseFloat(parameters[1]); var b = parseFloat(parameters[2]); var c = parseFloat(parameters[3]); var d = parseFloat(parameters[4]); var e = parseFloat(parameters[5]); var f = parseFloat(parameters[6]); // Only valid for visible layers (not buffers) if (layer_index >= 0) { var layer = getLayer(layer_index); layer.distort(a, b, c, d, e, f); } }, "error": function(parameters) { var reason = parameters[0]; var code = parseInt(parameters[1]); // Call handler if defined if (guac_client.onerror) guac_client.onerror(new Guacamole.Status(code, reason)); guac_client.disconnect(); }, "end": function(parameters) { // Get stream var stream_index = parseInt(parameters[0]); var stream = streams[stream_index]; // Signal end of stream if (stream.onend) stream.onend(); }, "file": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; var filename = parameters[2]; // Create stream if (guac_client.onfile) { var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); guac_client.onfile(stream, mimetype, filename); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "File transfer unsupported", 0x0100); }, "filesystem" : function handleFilesystem(parameters) { var objectIndex = parseInt(parameters[0]); var name = parameters[1]; // Create object, if supported if (guac_client.onfilesystem) { var object = objects[objectIndex] = new Guacamole.Object(guac_client, objectIndex); guac_client.onfilesystem(object, name); } // If unsupported, simply ignore the availability of the filesystem }, "identity": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.setTransform(layer, 1, 0, 0, 1, 0, 0); }, "img": function(parameters) { var stream_index = parseInt(parameters[0]); var channelMask = parseInt(parameters[1]); var layer = getLayer(parseInt(parameters[2])); var mimetype = parameters[3]; var x = parseInt(parameters[4]); var y = parseInt(parameters[5]); // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); var reader = new Guacamole.BlobReader(stream, mimetype); // Draw blob when stream is complete reader.onend = function drawImageBlob() { display.setChannelMask(layer, channelMask); display.drawBlob(layer, x, y, reader.getBlob()); }; }, "jpeg": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var x = parseInt(parameters[2]); var y = parseInt(parameters[3]); var data = parameters[4]; display.setChannelMask(layer, channelMask); display.draw(layer, x, y, "data:image/jpeg;base64," + data); }, "lfill": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var srcLayer = getLayer(parseInt(parameters[2])); display.setChannelMask(layer, channelMask); display.fillLayer(layer, srcLayer); }, "line": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); display.lineTo(layer, x, y); }, "lstroke": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var srcLayer = getLayer(parseInt(parameters[2])); display.setChannelMask(layer, channelMask); display.strokeLayer(layer, srcLayer); }, "move": function(parameters) { var layer_index = parseInt(parameters[0]); var parent_index = parseInt(parameters[1]); var x = parseInt(parameters[2]); var y = parseInt(parameters[3]); var z = parseInt(parameters[4]); // Only valid for non-default layers if (layer_index > 0 && parent_index >= 0) { var layer = getLayer(layer_index); var parent = getLayer(parent_index); layer.move(parent, x, y, z); } }, "name": function(parameters) { if (guac_client.onname) guac_client.onname(parameters[0]); }, "nest": function(parameters) { var parser = getParser(parseInt(parameters[0])); parser.receive(parameters[1]); }, "pipe": function(parameters) { var stream_index = parseInt(parameters[0]); var mimetype = parameters[1]; var name = parameters[2]; // Create stream if (guac_client.onpipe) { var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); guac_client.onpipe(stream, mimetype, name); } // Otherwise, unsupported else guac_client.sendAck(stream_index, "Named pipes unsupported", 0x0100); }, "png": function(parameters) { var channelMask = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var x = parseInt(parameters[2]); var y = parseInt(parameters[3]); var data = parameters[4]; display.setChannelMask(layer, channelMask); display.draw(layer, x, y, "data:image/png;base64," + data); }, "pop": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.pop(layer); }, "push": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.push(layer); }, "rect": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); var w = parseInt(parameters[3]); var h = parseInt(parameters[4]); display.rect(layer, x, y, w, h); }, "reset": function(parameters) { var layer = getLayer(parseInt(parameters[0])); display.reset(layer); }, "set": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var name = parameters[1]; var value = parameters[2]; // Call property handler if defined var handler = layerPropertyHandlers[name]; if (handler) handler(layer, value); }, "shade": function(parameters) { var layer_index = parseInt(parameters[0]); var a = parseInt(parameters[1]); // Only valid for visible layers (not buffers) if (layer_index >= 0) { var layer = getLayer(layer_index); layer.shade(a); } }, "size": function(parameters) { var layer_index = parseInt(parameters[0]); var layer = getLayer(layer_index); var width = parseInt(parameters[1]); var height = parseInt(parameters[2]); display.resize(layer, width, height); }, "start": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var x = parseInt(parameters[1]); var y = parseInt(parameters[2]); display.moveTo(layer, x, y); }, "sync": function(parameters) { var timestamp = parseInt(parameters[0]); // Flush display, send sync when done display.flush(function __send_sync_response() { if (timestamp !== currentTimestamp) { tunnel.sendMessage("sync", timestamp); currentTimestamp = timestamp; } }); // If received first update, no longer waiting. if (currentState === STATE_WAITING) setState(STATE_CONNECTED); // Call sync handler if defined if (guac_client.onsync) guac_client.onsync(timestamp); }, "transfer": function(parameters) { var srcL = getLayer(parseInt(parameters[0])); var srcX = parseInt(parameters[1]); var srcY = parseInt(parameters[2]); var srcWidth = parseInt(parameters[3]); var srcHeight = parseInt(parameters[4]); var function_index = parseInt(parameters[5]); var dstL = getLayer(parseInt(parameters[6])); var dstX = parseInt(parameters[7]); var dstY = parseInt(parameters[8]); /* SRC */ if (function_index === 0x3) display.put(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY); /* Anything else that isn't a NO-OP */ else if (function_index !== 0x5) display.transfer(srcL, srcX, srcY, srcWidth, srcHeight, dstL, dstX, dstY, Guacamole.Client.DefaultTransferFunction[function_index]); }, "transform": function(parameters) { var layer = getLayer(parseInt(parameters[0])); var a = parseFloat(parameters[1]); var b = parseFloat(parameters[2]); var c = parseFloat(parameters[3]); var d = parseFloat(parameters[4]); var e = parseFloat(parameters[5]); var f = parseFloat(parameters[6]); display.transform(layer, a, b, c, d, e, f); }, "undefine" : function handleUndefine(parameters) { // Get object var objectIndex = parseInt(parameters[0]); var object = objects[objectIndex]; // Signal end of object definition if (object && object.onundefine) object.onundefine(); }, "video": function(parameters) { var stream_index = parseInt(parameters[0]); var layer = getLayer(parseInt(parameters[1])); var mimetype = parameters[2]; var duration = parseFloat(parameters[3]); // Create stream var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index); // Assemble entire stream as a blob var blob_reader = new Guacamole.BlobReader(stream, mimetype); // Play video once finished blob_reader.onend = function() { // Read data from blob from stream var reader = new FileReader(); reader.onload = function() { var binary = ""; var bytes = new Uint8Array(reader.result); // Produce binary string from bytes in buffer for (var i=0; i