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 || {};
22 Guacamole.Client = function(display, tunnel) {
24 var guac_client = this;
27 var STATE_CONNECTING = 1;
28 var STATE_WAITING = 2;
29 var STATE_CONNECTED = 3;
30 var STATE_DISCONNECTING = 4;
31 var STATE_DISCONNECTED = 5;
33 var currentState = STATE_IDLE;
35 tunnel.oninstruction = doInstruction;
37 // Display must be relatively positioned for mouse to be handled properly
38 display.style.position = "relative";
40 function setState(state) {
41 if (state != currentState) {
43 if (guac_client.onstatechange)
44 guac_client.onstatechange(currentState);
48 function isConnected() {
49 return currentState == STATE_CONNECTED
50 || currentState == STATE_WAITING;
53 var cursorImage = null;
54 var cursorHotspotX = 0;
55 var cursorHotspotY = 0;
64 function redrawCursor(x, y) {
66 // Hide hardware cursor
67 if (cursorHidden == 0) {
68 display.className += " guac-hide-cursor";
73 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
76 cursorRectX = x - cursorHotspotX;
77 cursorRectY = y - cursorHotspotY;
78 cursorRectW = cursorImage.width;
79 cursorRectH = cursorImage.height;
82 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
85 guac_client.sendKeyEvent = function(pressed, keysym) {
86 // Do not send requests if not connected
90 tunnel.sendMessage("key:" + keysym + "," + pressed + ";");
93 guac_client.sendMouseState = function(mouseState) {
95 // Do not send requests if not connected
99 // Draw client-side cursor
100 if (cursorImage != null) {
109 if (mouseState.getLeft()) buttonMask |= 1;
110 if (mouseState.getMiddle()) buttonMask |= 2;
111 if (mouseState.getRight()) buttonMask |= 4;
112 if (mouseState.getUp()) buttonMask |= 8;
113 if (mouseState.getDown()) buttonMask |= 16;
116 tunnel.sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
119 guac_client.setClipboard = function(data) {
121 // Do not send requests if not connected
125 tunnel.sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
129 guac_client.onstatechange = null;
130 guac_client.onname = null;
131 guac_client.onerror = null;
132 guac_client.onclipboard = null;
135 var displayWidth = 0;
136 var displayHeight = 0;
138 var layers = new Array();
139 var buffers = new Array();
142 guac_client.getLayers = function() {
146 function getLayer(index) {
148 // If negative index, use buffer
152 var buffer = buffers[index];
154 // Create buffer if necessary
155 if (buffer == null) {
156 buffer = new Guacamole.Layer(0, 0);
157 buffer.setAutosize(1);
158 buffers[index] = buffer;
164 // If non-negative, use visible layer
167 var layer = layers[index];
171 layer = new Guacamole.Layer(displayWidth, displayHeight);
172 layers[index] = layer;
174 // (Re)-add existing layers in order
175 for (var i=0; i<layers.length; i++) {
178 // If already present, remove
179 if (layers[i].parentNode === display)
180 display.removeChild(layers[i].getCanvas());
183 display.appendChild(layers[i].getCanvas());
187 // Add cursor layer last
188 if (cursor != null) {
189 if (cursor.parentNode === display)
190 display.removeChild(cursor.getCanvas());
191 display.appendChild(cursor.getCanvas());
197 layer.resize(displayWidth, displayHeight);
205 var instructionHandlers = {
207 "error": function(parameters) {
208 if (guac_client.onerror) guac_client.onerror(unescapeGuacamoleString(parameters[0]));
212 "name": function(parameters) {
213 if (guac_client.onname) guac_client.onname(unescapeGuacamoleString(parameters[0]));
216 "clipboard": function(parameters) {
217 if (guac_client.onclipboard) guac_client.onclipboard(unescapeGuacamoleString(parameters[0]));
220 "size": function(parameters) {
222 displayWidth = parseInt(parameters[0]);
223 displayHeight = parseInt(parameters[1]);
225 // Update (set) display size
226 display.style.width = displayWidth + "px";
227 display.style.height = displayHeight + "px";
229 // Set cursor layer width/height
231 cursor.resize(displayWidth, displayHeight);
235 "png": function(parameters) {
237 var channelMask = parseInt(parameters[0]);
238 var layer = getLayer(parseInt(parameters[1]));
239 var x = parseInt(parameters[2]);
240 var y = parseInt(parameters[3]);
241 var data = parameters[4];
243 layer.setChannelMask(channelMask);
248 "data:image/png;base64," + data
251 // If received first update, no longer waiting.
252 if (currentState == STATE_WAITING)
253 setState(STATE_CONNECTED);
257 "copy": function(parameters) {
259 var srcL = getLayer(parseInt(parameters[0]));
260 var srcX = parseInt(parameters[1]);
261 var srcY = parseInt(parameters[2]);
262 var srcWidth = parseInt(parameters[3]);
263 var srcHeight = parseInt(parameters[4]);
264 var channelMask = parseInt(parameters[5]);
265 var dstL = getLayer(parseInt(parameters[6]));
266 var dstX = parseInt(parameters[7]);
267 var dstY = parseInt(parameters[8]);
269 dstL.setChannelMask(channelMask);
283 "cursor": function(parameters) {
285 var x = parseInt(parameters[0]);
286 var y = parseInt(parameters[1]);
287 var data = parameters[2];
289 if (cursor == null) {
290 cursor = new Guacamole.Layer(displayWidth, displayHeight);
291 display.appendChild(cursor.getCanvas());
294 // Start cursor image load
295 var image = new Image();
296 image.onload = function() {
299 var cursorX = cursorRectX + cursorHotspotX;
300 var cursorY = cursorRectY + cursorHotspotY;
305 redrawCursor(cursorX, cursorY);
307 image.src = "data:image/png;base64," + data
311 "sync": function(parameters) {
313 var timestamp = parameters[0];
315 // When all layers have finished rendering all instructions
316 // UP TO THIS POINT IN TIME, send sync response.
318 var layersToSync = 0;
319 function syncLayer() {
323 // Send sync response when layers are finished
324 if (layersToSync == 0)
325 tunnel.sendMessage("sync:" + timestamp + ";");
329 // Count active, not-ready layers and install sync tracking hooks
330 for (var i=0; i<layers.length; i++) {
332 var layer = layers[i];
333 if (layer && !layer.isReady()) {
335 layer.sync(syncLayer);
340 // If all layers are ready, then we didn't install any hooks.
341 // Send sync message now,
342 if (layersToSync == 0)
343 tunnel.sendMessage("sync:" + timestamp + ";");
350 function doInstruction(opcode, parameters) {
352 var handler = instructionHandlers[opcode];
359 function disconnect() {
361 // Only attempt disconnection not disconnected.
362 if (currentState != STATE_DISCONNECTED
363 && currentState != STATE_DISCONNECTING) {
365 setState(STATE_DISCONNECTING);
366 tunnel.sendMessage("disconnect;");
368 setState(STATE_DISCONNECTED);
373 function escapeGuacamoleString(str) {
375 var escapedString = "";
377 for (var i=0; i<str.length; i++) {
379 var c = str.charAt(i);
381 escapedString += "\\c";
383 escapedString += "\\s";
385 escapedString += "\\\\";
391 return escapedString;
395 function unescapeGuacamoleString(str) {
397 var unescapedString = "";
399 for (var i=0; i<str.length; i++) {
401 var c = str.charAt(i);
402 if (c == "\\" && i<str.length-1) {
404 var escapeChar = str.charAt(++i);
405 if (escapeChar == "c")
406 unescapedString += ",";
407 else if (escapeChar == "s")
408 unescapedString += ";";
409 else if (escapeChar == "\\")
410 unescapedString += "\\";
412 unescapedString += "\\" + escapeChar;
416 unescapedString += c;
420 return unescapedString;
424 guac_client.disconnect = disconnect;
425 guac_client.connect = function(data) {
427 setState(STATE_CONNECTING);
430 tunnel.connect(data);
433 setState(STATE_IDLE);
437 setState(STATE_WAITING);
440 guac_client.escapeGuacamoleString = escapeGuacamoleString;
441 guac_client.unescapeGuacamoleString = unescapeGuacamoleString;