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;
54 var background = null;
57 var cursorImage = null;
58 var cursorHotspotX = 0;
59 var cursorHotspotY = 0;
61 // FIXME: Make object. Clean up.
69 function redrawCursor() {
71 // Hide hardware cursor
72 if (cursorHidden == 0) {
73 display.className += " guac-hide-cursor";
78 cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
81 cursorRectX = mouse.getX() - cursorHotspotX;
82 cursorRectY = mouse.getY() - cursorHotspotY;
83 cursorRectW = cursorImage.width;
84 cursorRectH = cursorImage.height;
87 cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
93 /*****************************************/
95 /*****************************************/
97 var keyboard = new GuacamoleKeyboard(document);
99 this.disableKeyboard = function() {
100 keyboard.setKeyPressedHandler(null);
101 keyboard.setKeyReleasedHandler(null);
104 this.enableKeyboard = function() {
105 keyboard.setKeyPressedHandler(
107 sendKeyEvent(1, keysym);
111 keyboard.setKeyReleasedHandler(
113 sendKeyEvent(0, keysym);
118 // Enable keyboard by default
119 this.enableKeyboard();
121 function sendKeyEvent(pressed, keysym) {
122 // Do not send requests if not connected
126 sendMessage("key:" + keysym + "," + pressed + ";");
129 this.pressKey = function(keysym) {
130 sendKeyEvent(1, keysym);
133 this.releaseKey = function(keysym) {
134 sendKeyEvent(0, keysym);
138 /*****************************************/
140 /*****************************************/
142 var mouse = new GuacamoleMouse(display);
143 mouse.setButtonPressedHandler(
144 function(mouseState) {
145 sendMouseState(mouseState);
149 mouse.setButtonReleasedHandler(
150 function(mouseState) {
151 sendMouseState(mouseState);
155 mouse.setMovementHandler(
156 function(mouseState) {
158 // Draw client-side cursor
159 if (cursorImage != null) {
163 sendMouseState(mouseState);
168 function sendMouseState(mouseState) {
170 // Do not send requests if not connected
176 if (mouseState.getLeft()) buttonMask |= 1;
177 if (mouseState.getMiddle()) buttonMask |= 2;
178 if (mouseState.getRight()) buttonMask |= 4;
179 if (mouseState.getUp()) buttonMask |= 8;
180 if (mouseState.getDown()) buttonMask |= 16;
183 sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
186 var sendingMessages = 0;
187 var outputMessageBuffer = "";
189 function sendMessage(message) {
191 // Add event to queue, restart send loop if finished.
192 outputMessageBuffer += message;
193 if (sendingMessages == 0)
194 sendPendingMessages();
198 function sendPendingMessages() {
200 if (outputMessageBuffer.length > 0) {
204 var message_xmlhttprequest = new XMLHttpRequest();
205 message_xmlhttprequest.open("POST", TUNNEL_WRITE);
206 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
207 message_xmlhttprequest.setRequestHeader("Content-length", outputMessageBuffer.length);
209 // Once response received, send next queued event.
210 message_xmlhttprequest.onreadystatechange = function() {
211 if (message_xmlhttprequest.readyState == 4)
212 sendPendingMessages();
215 message_xmlhttprequest.send(outputMessageBuffer);
216 outputMessageBuffer = ""; // Clear buffer
225 /*****************************************/
227 /*****************************************/
229 this.setClipboard = function(data) {
231 // Do not send requests if not connected
235 sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
239 function desaturateFilter(data, width, height) {
241 for (var i=0; i<data.length; i+=4) {
249 var v = Math.max(r, g, b) / 2;
258 var nameHandler = null;
259 this.setNameHandler = function(handler) {
260 nameHandler = handler;
263 var errorHandler = null;
264 this.setErrorHandler = function(handler) {
265 errorHandler = handler;
268 var errorEncountered = 0;
269 function showError(error) {
270 // Only display first error (avoid infinite error loops)
271 if (errorEncountered == 0) {
272 errorEncountered = 1;
276 // Show error by desaturating display
278 background.filter(desaturateFilter);
285 function handleErrors(message) {
286 var errors = message.getErrors();
287 for (var errorIndex=0; errorIndex<errors.length; errorIndex++)
288 showError(errors[errorIndex].getMessage());
291 var clipboardHandler = null;
294 this.setClipboardHandler = function(handler) {
295 clipboardHandler = handler;
299 function handleResponse(xmlhttprequest) {
301 var nextRequest = null;
303 var instructionStart = 0;
306 function parseResponse() {
308 // Start next request as soon as possible
309 if (xmlhttprequest.readyState >= 2 && nextRequest == null)
310 nextRequest = makeRequest();
312 // Parse stream when data is received and when complete.
313 if (xmlhttprequest.readyState == 3 ||
314 xmlhttprequest.readyState == 4) {
316 // Halt on error during request
317 if (xmlhttprequest.status == 0) {
318 showError("Request canceled by browser.");
321 else if (xmlhttprequest.status != 200) {
322 showError("Error during request (HTTP " + xmlhttprequest.status + "): " + xmlhttprequest.statusText);
326 var current = xmlhttprequest.responseText;
329 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
331 // Start next search at next instruction
332 startIndex = instructionEnd+1;
334 var instruction = current.substr(instructionStart,
335 instructionEnd - instructionStart);
337 instructionStart = startIndex;
339 var opcodeEnd = instruction.indexOf(":");
343 if (opcodeEnd == -1) {
344 opcode = instruction;
345 parameters = new Array();
348 opcode = instruction.substr(0, opcodeEnd);
349 parameters = instruction.substr(opcodeEnd+1).split(",");
352 // If we're done parsing, handle the next response.
353 if (opcode.length == 0) {
356 delete xmlhttprequest;
358 handleResponse(nextRequest);
364 // Call instruction handler.
365 doInstruction(opcode, parameters);
368 // Start search at end of string.
369 startIndex = current.length;
378 xmlhttprequest.onreadystatechange = parseResponse;
384 function makeRequest() {
387 var xmlhttprequest = new XMLHttpRequest();
388 xmlhttprequest.open("POST", TUNNEL_READ);
389 xmlhttprequest.send(null);
391 return xmlhttprequest;
395 function escapeGuacamoleString(str) {
397 var escapedString = "";
399 for (var i=0; i<str.length; i++) {
401 var c = str.charAt(i);
403 escapedString += "\\c";
405 escapedString += "\\s";
407 escapedString += "\\\\";
413 return escapedString;
417 function unescapeGuacamoleString(str) {
419 var unescapedString = "";
421 for (var i=0; i<str.length; i++) {
423 var c = str.charAt(i);
424 if (c == "\\" && i<str.length-1) {
426 var escapeChar = str.charAt(++i);
427 if (escapeChar == "c")
428 unescapedString += ",";
429 else if (escapeChar == "s")
430 unescapedString += ";";
431 else if (escapeChar == "\\")
432 unescapedString += "\\";
434 unescapedString += "\\" + escapeChar;
438 unescapedString += c;
442 return unescapedString;
446 var instructionHandlers = {
448 "error": function(parameters) {
449 showError(unescapeGuacamoleString(parameters[0]));
452 "name": function(parameters) {
453 nameHandler(unescapeGuacamoleString(parameters[0]));
456 "clipboard": function(parameters) {
457 clipboardHandler(unescapeGuacamoleString(parameters[0]));
460 "size": function(parameters) {
462 var width = parseInt(parameters[0]);
463 var height = parseInt(parameters[1]);
465 // Update (set) display size
466 if (display && (background == null || cursor == null)) {
467 display.style.width = width + "px";
468 display.style.height = height + "px";
470 background = new Layer(width, height);
471 cursor = new Layer(width, height);
473 display.appendChild(background);
474 display.appendChild(cursor);
479 "rect": function(parameters) {
481 var x = parseInt(parameters[0]);
482 var y = parseInt(parameters[1]);
483 var w = parseInt(parameters[2]);
484 var h = parseInt(parameters[3]);
485 var color = parameters[4];
497 "png": function(parameters) {
499 var x = parseInt(parameters[0]);
500 var y = parseInt(parameters[1]);
501 var data = parameters[2];
506 "data:image/png;base64," + data
509 // If received first update, no longer waiting.
510 if (currentState == STATE_WAITING)
511 setState(STATE_CONNECTED);
515 "copy": function(parameters) {
517 var srcX = parseInt(parameters[0]);
518 var srcY = parseInt(parameters[1]);
519 var srcWidth = parseInt(parameters[2]);
520 var srcHeight = parseInt(parameters[3]);
521 var dstX = parseInt(parameters[4]);
522 var dstY = parseInt(parameters[5]);
535 "cursor": function(parameters) {
537 var x = parseInt(parameters[0]);
538 var y = parseInt(parameters[1]);
539 var data = parameters[2];
541 // Start cursor image load
542 var image = new Image();
543 image.onload = function() {
549 image.src = "data:image/png;base64," + data
556 function doInstruction(opcode, parameters) {
558 var handler = instructionHandlers[opcode];
565 this.connect = function() {
567 setState(STATE_CONNECTING);
569 // Start tunnel and connect synchronously
570 var connect_xmlhttprequest = new XMLHttpRequest();
571 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
572 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
573 connect_xmlhttprequest.setRequestHeader("Content-length", 0);
574 connect_xmlhttprequest.send(null);
576 // Start reading data
577 setState(STATE_WAITING);
578 handleResponse(makeRequest());
583 function disconnect() {
585 // Only attempt disconnection not disconnected.
586 if (currentState != STATE_DISCONNECTED
587 && currentState != STATE_DISCONNECTING) {
589 var message = "disconnect;";
590 setState(STATE_DISCONNECTING);
592 // Send disconnect message (synchronously... as necessary until handoff is implemented)
593 var disconnect_xmlhttprequest = new XMLHttpRequest();
594 disconnect_xmlhttprequest.open("POST", TUNNEL_WRITE, false);
595 disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
596 disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
597 disconnect_xmlhttprequest.send(message);
599 setState(STATE_DISCONNECTED);
604 this.disconnect = disconnect;