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
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 function GuacamoleClient(display, tunnelURL) {
22 var TUNNEL_CONNECT = tunnelURL + "?connect";
23 var TUNNEL_READ = tunnelURL + "?read";
24 var TUNNEL_WRITE = tunnelURL + "?write";
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;
34 var stateChangeHandler = null;
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() {
67 // Hide hardware cursor
68 if (cursorHidden == 0) {
69 display.className += " guac-hide-cursor";
74 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
77 cursorRectX = mouse.getX() - cursorHotspotX;
78 cursorRectY = mouse.getY() - cursorHotspotY;
79 cursorRectW = cursorImage.width;
80 cursorRectH = cursorImage.height;
83 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
89 /*****************************************/
91 /*****************************************/
93 var keyboard = new GuacamoleKeyboard(document);
95 this.disableKeyboard = function() {
96 keyboard.setKeyPressedHandler(null);
97 keyboard.setKeyReleasedHandler(null);
100 this.enableKeyboard = function() {
101 keyboard.setKeyPressedHandler(
103 sendKeyEvent(1, keysym);
107 keyboard.setKeyReleasedHandler(
109 sendKeyEvent(0, keysym);
114 // Enable keyboard by default
115 this.enableKeyboard();
117 function sendKeyEvent(pressed, keysym) {
118 // Do not send requests if not connected
122 sendMessage("key:" + keysym + "," + pressed + ";");
125 this.pressKey = function(keysym) {
126 sendKeyEvent(1, keysym);
129 this.releaseKey = function(keysym) {
130 sendKeyEvent(0, keysym);
134 /*****************************************/
136 /*****************************************/
138 var mouse = new GuacamoleMouse(display);
139 mouse.setButtonPressedHandler(
140 function(mouseState) {
141 sendMouseState(mouseState);
145 mouse.setButtonReleasedHandler(
146 function(mouseState) {
147 sendMouseState(mouseState);
151 mouse.setMovementHandler(
152 function(mouseState) {
154 // Draw client-side cursor
155 if (cursorImage != null) {
159 sendMouseState(mouseState);
164 function sendMouseState(mouseState) {
166 // Do not send requests if not connected
172 if (mouseState.getLeft()) buttonMask |= 1;
173 if (mouseState.getMiddle()) buttonMask |= 2;
174 if (mouseState.getRight()) buttonMask |= 4;
175 if (mouseState.getUp()) buttonMask |= 8;
176 if (mouseState.getDown()) buttonMask |= 16;
179 sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
182 var sendingMessages = 0;
183 var outputMessageBuffer = "";
185 function sendMessage(message) {
187 // Add event to queue, restart send loop if finished.
188 outputMessageBuffer += message;
189 if (sendingMessages == 0)
190 sendPendingMessages();
194 function sendPendingMessages() {
196 if (outputMessageBuffer.length > 0) {
200 var message_xmlhttprequest = new XMLHttpRequest();
201 message_xmlhttprequest.open("POST", TUNNEL_WRITE);
202 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
203 message_xmlhttprequest.setRequestHeader("Content-length", outputMessageBuffer.length);
205 // Once response received, send next queued event.
206 message_xmlhttprequest.onreadystatechange = function() {
207 if (message_xmlhttprequest.readyState == 4)
208 sendPendingMessages();
211 message_xmlhttprequest.send(outputMessageBuffer);
212 outputMessageBuffer = ""; // Clear buffer
221 /*****************************************/
223 /*****************************************/
225 this.setClipboard = function(data) {
227 // Do not send requests if not connected
231 sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
235 function desaturateFilter(data, width, height) {
237 for (var i=0; i<data.length; i+=4) {
245 var v = Math.max(r, g, b) / 2;
254 var nameHandler = null;
255 this.setNameHandler = function(handler) {
256 nameHandler = handler;
259 var errorHandler = null;
260 this.setErrorHandler = function(handler) {
261 errorHandler = handler;
264 var errorEncountered = 0;
265 function showError(error) {
266 // Only display first error (avoid infinite error loops)
267 if (errorEncountered == 0) {
268 errorEncountered = 1;
272 // Show error by desaturating display
273 for (var i=0; i<layers.length; i++) {
274 layers[i].filter(desaturateFilter);
282 function handleErrors(message) {
283 var errors = message.getErrors();
284 for (var errorIndex=0; errorIndex<errors.length; errorIndex++)
285 showError(errors[errorIndex].getMessage());
288 var clipboardHandler = null;
291 this.setClipboardHandler = function(handler) {
292 clipboardHandler = handler;
296 function handleResponse(xmlhttprequest) {
299 var nextRequest = null;
301 var instructionStart = 0;
304 function parseResponse() {
306 // Start next request as soon as possible
307 if (xmlhttprequest.readyState >= 2 && nextRequest == null)
308 nextRequest = makeRequest();
310 // Parse stream when data is received and when complete.
311 if (xmlhttprequest.readyState == 3 ||
312 xmlhttprequest.readyState == 4) {
314 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
315 if (xmlhttprequest.readyState == 3 && interval == null)
316 interval = setInterval(parseResponse, 30);
317 else if (xmlhttprequest.readyState == 4 && interval != null)
318 clearInterval(interval);
320 // Halt on error during request
321 if (xmlhttprequest.status == 0) {
322 showError("Request canceled by browser.");
325 else if (xmlhttprequest.status != 200) {
326 showError("Error during request (HTTP " + xmlhttprequest.status + "): " + xmlhttprequest.statusText);
330 var current = xmlhttprequest.responseText;
333 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
335 // Start next search at next instruction
336 startIndex = instructionEnd+1;
338 var instruction = current.substr(instructionStart,
339 instructionEnd - instructionStart);
341 instructionStart = startIndex;
343 var opcodeEnd = instruction.indexOf(":");
347 if (opcodeEnd == -1) {
348 opcode = instruction;
349 parameters = new Array();
352 opcode = instruction.substr(0, opcodeEnd);
353 parameters = instruction.substr(opcodeEnd+1).split(",");
356 // If we're done parsing, handle the next response.
357 if (opcode.length == 0) {
360 delete xmlhttprequest;
362 handleResponse(nextRequest);
368 // Call instruction handler.
369 doInstruction(opcode, parameters);
372 // Start search at end of string.
373 startIndex = current.length;
382 xmlhttprequest.onreadystatechange = parseResponse;
388 function makeRequest() {
391 var xmlhttprequest = new XMLHttpRequest();
392 xmlhttprequest.open("POST", TUNNEL_READ);
393 xmlhttprequest.send(null);
395 return xmlhttprequest;
399 function escapeGuacamoleString(str) {
401 var escapedString = "";
403 for (var i=0; i<str.length; i++) {
405 var c = str.charAt(i);
407 escapedString += "\\c";
409 escapedString += "\\s";
411 escapedString += "\\\\";
417 return escapedString;
421 function unescapeGuacamoleString(str) {
423 var unescapedString = "";
425 for (var i=0; i<str.length; i++) {
427 var c = str.charAt(i);
428 if (c == "\\" && i<str.length-1) {
430 var escapeChar = str.charAt(++i);
431 if (escapeChar == "c")
432 unescapedString += ",";
433 else if (escapeChar == "s")
434 unescapedString += ";";
435 else if (escapeChar == "\\")
436 unescapedString += "\\";
438 unescapedString += "\\" + escapeChar;
442 unescapedString += c;
446 return unescapedString;
451 var displayWidth = 0;
452 var displayHeight = 0;
454 var layers = new Array();
455 var buffers = new Array();
458 function getLayer(index) {
460 // If negative index, use buffer
464 var buffer = buffers[index];
466 // Create buffer if necessary
467 if (buffer == null) {
468 buffer = new Layer(0, 0);
469 buffer.setAutosize(1);
470 buffers[index] = buffer;
476 // If non-negative, use visible layer
479 var layer = layers[index];
483 layer = new Layer(displayWidth, displayHeight);
484 layers[index] = layer;
486 // (Re)-add existing layers in order
487 for (var i=0; i<layers.length; i++) {
490 // If already present, remove
491 if (layers[i].parentNode === display)
492 display.removeChild(layers[i]);
495 display.appendChild(layers[i]);
499 // Add cursor layer last
500 if (cursor != null) {
501 if (cursor.parentNode === display)
502 display.removeChild(cursor);
503 display.appendChild(cursor);
509 layer.resize(displayWidth, displayHeight);
517 var instructionHandlers = {
519 "error": function(parameters) {
520 showError(unescapeGuacamoleString(parameters[0]));
523 "name": function(parameters) {
524 nameHandler(unescapeGuacamoleString(parameters[0]));
527 "clipboard": function(parameters) {
528 clipboardHandler(unescapeGuacamoleString(parameters[0]));
531 "size": function(parameters) {
533 displayWidth = parseInt(parameters[0]);
534 displayHeight = parseInt(parameters[1]);
536 // Update (set) display size
538 display.style.width = displayWidth + "px";
539 display.style.height = displayHeight + "px";
542 // Set cursor layer width/height
544 cursor.resize(displayWidth, displayHeight);
548 "png": function(parameters) {
550 var layer = parseInt(parameters[0]);
551 var x = parseInt(parameters[1]);
552 var y = parseInt(parameters[2]);
553 var data = parameters[3];
555 getLayer(layer).draw(
558 "data:image/png;base64," + data
561 // If received first update, no longer waiting.
562 if (currentState == STATE_WAITING)
563 setState(STATE_CONNECTED);
567 "copy": function(parameters) {
569 var srcL = parseInt(parameters[0]);
570 var srcX = parseInt(parameters[1]);
571 var srcY = parseInt(parameters[2]);
572 var srcWidth = parseInt(parameters[3]);
573 var srcHeight = parseInt(parameters[4]);
574 var dstL = parseInt(parameters[5]);
575 var dstX = parseInt(parameters[6]);
576 var dstY = parseInt(parameters[7]);
578 getLayer(dstL).copyRect(
590 "cursor": function(parameters) {
592 var x = parseInt(parameters[0]);
593 var y = parseInt(parameters[1]);
594 var data = parameters[2];
596 if (cursor == null) {
597 cursor = new Layer(displayWidth, displayHeight);
598 display.appendChild(cursor);
601 // Start cursor image load
602 var image = new Image();
603 image.onload = function() {
609 image.src = "data:image/png;base64," + data
616 function doInstruction(opcode, parameters) {
618 var handler = instructionHandlers[opcode];
625 this.connect = function() {
627 setState(STATE_CONNECTING);
629 // Start tunnel and connect synchronously
630 var connect_xmlhttprequest = new XMLHttpRequest();
631 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
632 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
633 connect_xmlhttprequest.setRequestHeader("Content-length", 0);
634 connect_xmlhttprequest.send(null);
636 // Start reading data
637 setState(STATE_WAITING);
638 handleResponse(makeRequest());
643 function disconnect() {
645 // Only attempt disconnection not disconnected.
646 if (currentState != STATE_DISCONNECTED
647 && currentState != STATE_DISCONNECTING) {
649 var message = "disconnect;";
650 setState(STATE_DISCONNECTING);
652 // Send disconnect message (synchronously... as necessary until handoff is implemented)
653 var disconnect_xmlhttprequest = new XMLHttpRequest();
654 disconnect_xmlhttprequest.open("POST", TUNNEL_WRITE, false);
655 disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
656 disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
657 disconnect_xmlhttprequest.send(message);
659 setState(STATE_DISCONNECTED);
664 this.disconnect = disconnect;