3 * Guacamole - Clientless Remote Desktop
4 * Copyright (C) 2010 Michael Jumper
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Affero General Public License for more details.
16 * You should have received a copy of the GNU Affero General Public License
19 // Guacamole namespace
20 var Guacamole = Guacamole || {};
23 * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
24 * automatically handles incoming and outgoing Guacamole instructions via the
25 * provided tunnel, updating the display using one or more canvas elements.
28 * @param {Element} display The display element to add canvas elements to.
29 * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
30 * Guacamole instructions.
32 Guacamole.Client = function(display, tunnel) {
34 var guac_client = this;
37 var STATE_CONNECTING = 1;
38 var STATE_WAITING = 2;
39 var STATE_CONNECTED = 3;
40 var STATE_DISCONNECTING = 4;
41 var STATE_DISCONNECTED = 5;
43 var currentState = STATE_IDLE;
45 tunnel.oninstruction = doInstruction;
47 tunnel.onerror = function(message) {
48 if (guac_client.onerror)
49 guac_client.onerror(message);
52 // Display must be relatively positioned for mouse to be handled properly
53 display.style.position = "relative";
55 function setState(state) {
56 if (state != currentState) {
58 if (guac_client.onstatechange)
59 guac_client.onstatechange(currentState);
63 function isConnected() {
64 return currentState == STATE_CONNECTED
65 || currentState == STATE_WAITING;
68 var cursorImage = null;
69 var cursorHotspotX = 0;
70 var cursorHotspotY = 0;
79 function redrawCursor(x, y) {
81 // Hide hardware cursor
82 if (cursorHidden == 0) {
83 display.className += " guac-hide-cursor";
88 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
91 cursorRectX = x - cursorHotspotX;
92 cursorRectY = y - cursorHotspotY;
93 cursorRectW = cursorImage.width;
94 cursorRectH = cursorImage.height;
97 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
100 guac_client.sendKeyEvent = function(pressed, keysym) {
101 // Do not send requests if not connected
105 tunnel.sendMessage("key:" + keysym + "," + pressed + ";");
108 guac_client.sendMouseState = function(mouseState) {
110 // Do not send requests if not connected
114 // Draw client-side cursor
115 if (cursorImage != null) {
124 if (mouseState.left) buttonMask |= 1;
125 if (mouseState.middle) buttonMask |= 2;
126 if (mouseState.right) buttonMask |= 4;
127 if (mouseState.up) buttonMask |= 8;
128 if (mouseState.down) buttonMask |= 16;
131 tunnel.sendMessage("mouse:" + mouseState.x + "," + mouseState.y + "," + buttonMask + ";");
134 guac_client.setClipboard = function(data) {
136 // Do not send requests if not connected
140 tunnel.sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
144 guac_client.onstatechange = null;
145 guac_client.onname = null;
146 guac_client.onerror = null;
147 guac_client.onclipboard = null;
150 var displayWidth = 0;
151 var displayHeight = 0;
153 var layers = new Array();
154 var buffers = new Array();
157 guac_client.getLayers = function() {
161 function getLayer(index) {
163 // If negative index, use buffer
167 var buffer = buffers[index];
169 // Create buffer if necessary
170 if (buffer == null) {
171 buffer = new Guacamole.Layer(0, 0);
173 buffers[index] = buffer;
179 // If non-negative, use visible layer
182 var layer = layers[index];
186 layer = new Guacamole.Layer(displayWidth, displayHeight);
188 // Set layer position
189 var canvas = layer.getCanvas();
190 canvas.style.position = "absolute";
191 canvas.style.left = "0px";
192 canvas.style.top = "0px";
194 layers[index] = layer;
196 // (Re)-add existing layers in order
197 for (var i=0; i<layers.length; i++) {
200 // If already present, remove
201 if (layers[i].parentNode === display)
202 display.removeChild(layers[i].getCanvas());
205 display.appendChild(layers[i].getCanvas());
209 // Add cursor layer last
210 if (cursor != null) {
211 if (cursor.parentNode === display)
212 display.removeChild(cursor.getCanvas());
213 display.appendChild(cursor.getCanvas());
219 layer.resize(displayWidth, displayHeight);
227 var instructionHandlers = {
229 "error": function(parameters) {
230 if (guac_client.onerror) guac_client.onerror(parameters[0]);
234 "name": function(parameters) {
235 if (guac_client.onname) guac_client.onname(parameters[0]);
238 "clipboard": function(parameters) {
239 if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
242 "size": function(parameters) {
244 displayWidth = parseInt(parameters[0]);
245 displayHeight = parseInt(parameters[1]);
247 // Update (set) display size
248 display.style.width = displayWidth + "px";
249 display.style.height = displayHeight + "px";
251 // Set cursor layer width/height
253 cursor.resize(displayWidth, displayHeight);
257 "png": function(parameters) {
259 var channelMask = parseInt(parameters[0]);
260 var layer = getLayer(parseInt(parameters[1]));
261 var x = parseInt(parameters[2]);
262 var y = parseInt(parameters[3]);
263 var data = parameters[4];
265 layer.setChannelMask(channelMask);
270 "data:image/png;base64," + data
273 // If received first update, no longer waiting.
274 if (currentState == STATE_WAITING)
275 setState(STATE_CONNECTED);
279 "copy": function(parameters) {
281 var srcL = getLayer(parseInt(parameters[0]));
282 var srcX = parseInt(parameters[1]);
283 var srcY = parseInt(parameters[2]);
284 var srcWidth = parseInt(parameters[3]);
285 var srcHeight = parseInt(parameters[4]);
286 var channelMask = parseInt(parameters[5]);
287 var dstL = getLayer(parseInt(parameters[6]));
288 var dstX = parseInt(parameters[7]);
289 var dstY = parseInt(parameters[8]);
291 dstL.setChannelMask(channelMask);
305 "rect": function(parameters) {
307 var channelMask = parseInt(parameters[0]);
308 var layer = getLayer(parseInt(parameters[1]));
309 var x = parseInt(parameters[2]);
310 var y = parseInt(parameters[3]);
311 var w = parseInt(parameters[4]);
312 var h = parseInt(parameters[5]);
313 var r = parseInt(parameters[6]);
314 var g = parseInt(parameters[7]);
315 var b = parseInt(parameters[8]);
316 var a = parseInt(parameters[9]);
318 layer.setChannelMask(channelMask);
327 "clip": function(parameters) {
329 var layer = getLayer(parseInt(parameters[0]));
330 var x = parseInt(parameters[1]);
331 var y = parseInt(parameters[2]);
332 var w = parseInt(parameters[3]);
333 var h = parseInt(parameters[4]);
335 layer.clipRect(x, y, w, h);
339 "cursor": function(parameters) {
341 var x = parseInt(parameters[0]);
342 var y = parseInt(parameters[1]);
343 var data = parameters[2];
345 if (cursor == null) {
346 cursor = new Guacamole.Layer(displayWidth, displayHeight);
348 var canvas = cursor.getCanvas();
349 canvas.style.position = "absolute";
350 canvas.style.left = "0px";
351 canvas.style.top = "0px";
353 display.appendChild(canvas);
356 // Start cursor image load
357 var image = new Image();
358 image.onload = function() {
361 var cursorX = cursorRectX + cursorHotspotX;
362 var cursorY = cursorRectY + cursorHotspotY;
367 redrawCursor(cursorX, cursorY);
369 image.src = "data:image/png;base64," + data
373 "sync": function(parameters) {
375 var timestamp = parameters[0];
377 // When all layers have finished rendering all instructions
378 // UP TO THIS POINT IN TIME, send sync response.
380 var layersToSync = 0;
381 function syncLayer() {
385 // Send sync response when layers are finished
386 if (layersToSync == 0)
387 tunnel.sendMessage("sync:" + timestamp + ";");
391 // Count active, not-ready layers and install sync tracking hooks
392 for (var i=0; i<layers.length; i++) {
394 var layer = layers[i];
395 if (layer && !layer.isReady()) {
397 layer.sync(syncLayer);
402 // If all layers are ready, then we didn't install any hooks.
403 // Send sync message now,
404 if (layersToSync == 0)
405 tunnel.sendMessage("sync:" + timestamp + ";");
412 function doInstruction(opcode, parameters) {
414 var handler = instructionHandlers[opcode];
421 function disconnect() {
423 // Only attempt disconnection not disconnected.
424 if (currentState != STATE_DISCONNECTED
425 && currentState != STATE_DISCONNECTING) {
427 setState(STATE_DISCONNECTING);
428 tunnel.sendMessage("disconnect;");
430 setState(STATE_DISCONNECTED);
435 function escapeGuacamoleString(str) {
437 var escapedString = "";
439 for (var i=0; i<str.length; i++) {
441 var c = str.charAt(i);
443 escapedString += "\\c";
445 escapedString += "\\s";
447 escapedString += "\\\\";
453 return escapedString;
457 guac_client.disconnect = disconnect;
458 guac_client.connect = function(data) {
460 setState(STATE_CONNECTING);
463 tunnel.connect(data);
466 setState(STATE_IDLE);
470 setState(STATE_WAITING);