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 function GuacamoleClient(display, tunnel) {
22 var STATE_CONNECTING = 1;
23 var STATE_WAITING = 2;
24 var STATE_CONNECTED = 3;
25 var STATE_DISCONNECTING = 4;
26 var STATE_DISCONNECTED = 5;
28 var currentState = STATE_IDLE;
29 var stateChangeHandler = null;
31 tunnel.setInstructionHandler(doInstruction);
33 // Display must be relatively positioned for mouse to be handled properly
34 display.style.position = "relative";
36 function setState(state) {
37 if (state != currentState) {
39 if (stateChangeHandler)
40 stateChangeHandler(currentState);
44 this.setOnStateChangeHandler = function(handler) {
45 stateChangeHandler = handler;
48 function isConnected() {
49 return currentState == STATE_CONNECTED
50 || currentState == STATE_WAITING;
53 var cursorImage = null;
54 var cursorHotspotX = 0;
55 var cursorHotspotY = 0;
57 // FIXME: Make object. Clean up.
65 function redrawCursor(x, y) {
67 // Hide hardware cursor
68 if (cursorHidden == 0) {
69 display.className += " guac-hide-cursor";
74 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
77 cursorRectX = x - cursorHotspotX;
78 cursorRectY = y - cursorHotspotY;
79 cursorRectW = cursorImage.width;
80 cursorRectH = cursorImage.height;
83 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
86 this.sendKeyEvent = function(pressed, keysym) {
87 // Do not send requests if not connected
91 tunnel.sendMessage("key:" + keysym + "," + pressed + ";");
94 this.sendMouseState = function(mouseState) {
96 // Do not send requests if not connected
100 // Draw client-side cursor
101 if (cursorImage != null) {
110 if (mouseState.getLeft()) buttonMask |= 1;
111 if (mouseState.getMiddle()) buttonMask |= 2;
112 if (mouseState.getRight()) buttonMask |= 4;
113 if (mouseState.getUp()) buttonMask |= 8;
114 if (mouseState.getDown()) buttonMask |= 16;
117 tunnel.sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
120 this.setClipboard = function(data) {
122 // Do not send requests if not connected
126 tunnel.sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
131 var nameHandler = null;
132 this.setNameHandler = function(handler) {
133 nameHandler = handler;
136 var errorHandler = null;
137 this.setErrorHandler = function(handler) {
138 errorHandler = handler;
141 var clipboardHandler = null;
142 this.setClipboardHandler = function(handler) {
143 clipboardHandler = handler;
147 var displayWidth = 0;
148 var displayHeight = 0;
150 var layers = new Array();
151 var buffers = new Array();
154 this.getLayers = function() {
158 function getLayer(index) {
160 // If negative index, use buffer
164 var buffer = buffers[index];
166 // Create buffer if necessary
167 if (buffer == null) {
168 buffer = new Layer(0, 0);
169 buffer.setAutosize(1);
170 buffers[index] = buffer;
176 // If non-negative, use visible layer
179 var layer = layers[index];
183 layer = new Layer(displayWidth, displayHeight);
184 layers[index] = layer;
186 // (Re)-add existing layers in order
187 for (var i=0; i<layers.length; i++) {
190 // If already present, remove
191 if (layers[i].parentNode === display)
192 display.removeChild(layers[i]);
195 display.appendChild(layers[i]);
199 // Add cursor layer last
200 if (cursor != null) {
201 if (cursor.parentNode === display)
202 display.removeChild(cursor);
203 display.appendChild(cursor);
209 layer.resize(displayWidth, displayHeight);
217 var instructionHandlers = {
219 "error": function(parameters) {
220 if (errorHandler) errorHandler(unescapeGuacamoleString(parameters[0]));
224 "name": function(parameters) {
225 if (nameHandler) nameHandler(unescapeGuacamoleString(parameters[0]));
228 "clipboard": function(parameters) {
229 if (clipboardHandler) clipboardHandler(unescapeGuacamoleString(parameters[0]));
232 "size": function(parameters) {
234 displayWidth = parseInt(parameters[0]);
235 displayHeight = parseInt(parameters[1]);
237 // Update (set) display size
238 display.style.width = displayWidth + "px";
239 display.style.height = displayHeight + "px";
241 // Set cursor layer width/height
243 cursor.resize(displayWidth, displayHeight);
247 "png": function(parameters) {
249 var channelMask = parseInt(parameters[0]);
250 var layer = getLayer(parseInt(parameters[1]));
251 var x = parseInt(parameters[2]);
252 var y = parseInt(parameters[3]);
253 var data = parameters[4];
255 layer.setChannelMask(channelMask);
260 "data:image/png;base64," + data
263 // If received first update, no longer waiting.
264 if (currentState == STATE_WAITING)
265 setState(STATE_CONNECTED);
269 "copy": function(parameters) {
271 var srcL = getLayer(parseInt(parameters[0]));
272 var srcX = parseInt(parameters[1]);
273 var srcY = parseInt(parameters[2]);
274 var srcWidth = parseInt(parameters[3]);
275 var srcHeight = parseInt(parameters[4]);
276 var channelMask = parseInt(parameters[5]);
277 var dstL = getLayer(parseInt(parameters[6]));
278 var dstX = parseInt(parameters[7]);
279 var dstY = parseInt(parameters[8]);
281 dstL.setChannelMask(channelMask);
295 "cursor": function(parameters) {
297 var x = parseInt(parameters[0]);
298 var y = parseInt(parameters[1]);
299 var data = parameters[2];
301 if (cursor == null) {
302 cursor = new Layer(displayWidth, displayHeight);
303 display.appendChild(cursor);
306 // Start cursor image load
307 var image = new Image();
308 image.onload = function() {
313 cursorRectX + cursorHotspotX,
314 cursorRectY + cursorHotspotY
317 image.src = "data:image/png;base64," + data
321 "sync": function(parameters) {
323 var timestamp = parameters[0];
325 // When all layers have finished rendering all instructions
326 // UP TO THIS POINT IN TIME, send sync response.
328 var layersToSync = 0;
329 function syncLayer() {
333 // Send sync response when layers are finished
334 if (layersToSync == 0)
335 tunnel.sendMessage("sync:" + timestamp + ";");
339 // Count active, not-ready layers and install sync tracking hooks
340 for (var i=0; i<layers.length; i++) {
342 var layer = layers[i];
343 if (layer && !layer.isReady()) {
345 layer.sync(syncLayer);
350 // If all layers are ready, then we didn't install any hooks.
351 // Send sync message now,
352 if (layersToSync == 0)
353 tunnel.sendMessage("sync:" + timestamp + ";");
360 function doInstruction(opcode, parameters) {
362 var handler = instructionHandlers[opcode];
369 function disconnect() {
371 // Only attempt disconnection not disconnected.
372 if (currentState != STATE_DISCONNECTED
373 && currentState != STATE_DISCONNECTING) {
375 setState(STATE_DISCONNECTING);
376 tunnel.sendMessage("disconnect;");
378 setState(STATE_DISCONNECTED);
383 function escapeGuacamoleString(str) {
385 var escapedString = "";
387 for (var i=0; i<str.length; i++) {
389 var c = str.charAt(i);
391 escapedString += "\\c";
393 escapedString += "\\s";
395 escapedString += "\\\\";
401 return escapedString;
405 function unescapeGuacamoleString(str) {
407 var unescapedString = "";
409 for (var i=0; i<str.length; i++) {
411 var c = str.charAt(i);
412 if (c == "\\" && i<str.length-1) {
414 var escapeChar = str.charAt(++i);
415 if (escapeChar == "c")
416 unescapedString += ",";
417 else if (escapeChar == "s")
418 unescapedString += ";";
419 else if (escapeChar == "\\")
420 unescapedString += "\\";
422 unescapedString += "\\" + escapeChar;
426 unescapedString += c;
430 return unescapedString;
434 this.disconnect = disconnect;
435 this.connect = function(data) {
437 setState(STATE_CONNECTING);
440 tunnel.connect(data);
443 setState(STATE_IDLE);
447 setState(STATE_WAITING);
450 this.escapeGuacamoleString = escapeGuacamoleString;
451 this.unescapeGuacamoleString = unescapeGuacamoleString;