Using new tunnel servlet API
[guacamole-common-js.git] / src / main / resources / guacamole.js
1
2 /*
3  *  Guacamole - Clientless Remote Desktop
4  *  Copyright (C) 2010  Michael Jumper
5  *
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.
10  *
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.
15  *
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/>.
18  */
19
20 function GuacamoleClient(display, tunnelURL) {
21
22     var TUNNEL_CONNECT = tunnelURL + "?connect";
23     var TUNNEL_READ    = tunnelURL + "?read";
24     var TUNNEL_WRITE   = tunnelURL + "?write";
25
26     var STATE_IDLE          = 0;
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;
32
33     var currentState = STATE_IDLE;
34     var stateChangeHandler = null;
35
36     function setState(state) {
37         if (state != currentState) {
38             currentState = state;
39             if (stateChangeHandler)
40                 stateChangeHandler(currentState);
41         }
42     }
43
44     this.setOnStateChangeHandler = function(handler) {
45         stateChangeHandler = handler;
46     }
47
48     function isConnected() {
49         return currentState == STATE_CONNECTED
50             || currentState == STATE_WAITING;
51     }
52
53     // Layers
54     var background = null;
55     var cursor = null;
56
57     var cursorImage = null;
58     var cursorHotspotX = 0;
59     var cursorHotspotY = 0;
60
61     // FIXME: Make object. Clean up.
62     var cursorRectX = 0;
63     var cursorRectY = 0;
64     var cursorRectW = 0;
65     var cursorRectH = 0;
66
67     var cursorHidden = 0;
68
69     function redrawCursor() {
70
71         // Hide hardware cursor
72         if (cursorHidden == 0) {
73             display.className += " guac-hide-cursor";
74             cursorHidden = 1;
75         }
76
77         // Erase old cursor
78         cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
79
80         // Update rect
81         cursorRectX = mouse.getX() - cursorHotspotX;
82         cursorRectY = mouse.getY() - cursorHotspotY;
83         cursorRectW = cursorImage.width;
84         cursorRectH = cursorImage.height;
85
86         // Draw new cursor
87         cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
88     }
89
90
91
92
93         /*****************************************/
94         /*** Keyboard                          ***/
95         /*****************************************/
96
97     var keyboard = new GuacamoleKeyboard(document);
98
99     this.disableKeyboard = function() {
100         keyboard.setKeyPressedHandler(null);
101         keyboard.setKeyReleasedHandler(null);
102     };
103
104     this.enableKeyboard = function() {
105         keyboard.setKeyPressedHandler(
106             function (keysym) {
107                 sendKeyEvent(1, keysym);
108             }
109         );
110
111         keyboard.setKeyReleasedHandler(
112             function (keysym) {
113                 sendKeyEvent(0, keysym);
114             }
115         );
116     };
117
118     // Enable keyboard by default
119     this.enableKeyboard();
120
121     function sendKeyEvent(pressed, keysym) {
122         // Do not send requests if not connected
123         if (!isConnected())
124             return;
125
126         sendMessage("key:" +  keysym + "," + pressed + ";");
127     }
128
129     this.pressKey = function(keysym) {
130         sendKeyEvent(1, keysym);
131     };
132
133     this.releaseKey = function(keysym) {
134         sendKeyEvent(0, keysym);
135     };
136
137
138         /*****************************************/
139         /*** Mouse                             ***/
140         /*****************************************/
141
142     var mouse = new GuacamoleMouse(display);
143     mouse.setButtonPressedHandler(
144         function(mouseState) {
145             sendMouseState(mouseState);
146         }
147     );
148
149     mouse.setButtonReleasedHandler(
150         function(mouseState) {
151             sendMouseState(mouseState);
152         }
153     );
154
155     mouse.setMovementHandler(
156         function(mouseState) {
157
158             // Draw client-side cursor
159             if (cursorImage != null) {
160                 redrawCursor();
161             }
162
163             sendMouseState(mouseState);
164         }
165     );
166
167
168     function sendMouseState(mouseState) {
169
170         // Do not send requests if not connected
171         if (!isConnected())
172             return;
173
174         // Build mask
175         var buttonMask = 0;
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;
181
182         // Send message
183         sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
184     }
185
186     var sendingMessages = 0;
187     var outputMessageBuffer = "";
188
189     function sendMessage(message) {
190
191         // Add event to queue, restart send loop if finished.
192         outputMessageBuffer += message;
193         if (sendingMessages == 0)
194             sendPendingMessages();
195
196     }
197
198     function sendPendingMessages() {
199
200         if (outputMessageBuffer.length > 0) {
201
202             sendingMessages = 1;
203
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);
208
209             // Once response received, send next queued event.
210             message_xmlhttprequest.onreadystatechange = function() {
211                 if (message_xmlhttprequest.readyState == 4)
212                     sendPendingMessages();
213             }
214
215             message_xmlhttprequest.send(outputMessageBuffer);
216             outputMessageBuffer = ""; // Clear buffer
217
218         }
219         else
220             sendingMessages = 0;
221
222     }
223
224
225         /*****************************************/
226         /*** Clipboard                         ***/
227         /*****************************************/
228
229     this.setClipboard = function(data) {
230
231         // Do not send requests if not connected
232         if (!isConnected())
233             return;
234
235         sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
236     }
237
238
239     function desaturateFilter(data, width, height) {
240
241         for (var i=0; i<data.length; i+=4) {
242
243             // Get RGB values
244             var r = data[i];
245             var g = data[i+1];
246             var b = data[i+2];
247
248             // Desaturate
249             var v = Math.max(r, g, b) / 2;
250             data[i]   = v;
251             data[i+1] = v;
252             data[i+2] = v;
253
254         }
255
256     }
257
258
259     var errorHandler = null;
260     this.setErrorHandler = function(handler) {
261         errorHandler = handler;
262     };
263
264     var errorEncountered = 0;
265     function showError(error) {
266         // Only display first error (avoid infinite error loops)
267         if (errorEncountered == 0) {
268             errorEncountered = 1;
269
270             disconnect();
271
272             // Show error by desaturating display
273             if (background)
274                 background.filter(desaturateFilter);
275
276             if (errorHandler)
277                 errorHandler(error);
278         }
279     }
280
281     function handleErrors(message) {
282         var errors = message.getErrors();
283         for (var errorIndex=0; errorIndex<errors.length; errorIndex++)
284             showError(errors[errorIndex].getMessage());
285     }
286
287     var clipboardHandler = null;
288     var requests = 0;
289
290     this.setClipboardHandler = function(handler) {
291         clipboardHandler = handler;
292     };
293
294
295     function handleResponse(xmlhttprequest) {
296
297         var nextRequest = null;
298
299         var instructionStart = 0;
300         var startIndex = 0;
301
302         function parseResponse() {
303
304             // Start next request as soon as possible
305             if (xmlhttprequest.readyState >= 2 && nextRequest == null)
306                 nextRequest = makeRequest();
307
308             // Parse stream when data is received and when complete.
309             if (xmlhttprequest.readyState == 3 ||
310                 xmlhttprequest.readyState == 4) {
311
312                 // Halt on error during request
313                 if (xmlhttprequest.status == 0) {
314                     showError("Request canceled by browser.");
315                     return;
316                 }
317                 else if (xmlhttprequest.status != 200) {
318                     showError("Error during request (HTTP " + xmlhttprequest.status + "): " + xmlhttprequest.statusText);
319                     return;
320                 }
321
322                 var current = xmlhttprequest.responseText;
323                 var instructionEnd;
324                 
325                 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
326
327                     // Start next search at next instruction
328                     startIndex = instructionEnd+1;
329
330                     var instruction = current.substr(instructionStart,
331                             instructionEnd - instructionStart);
332
333                     instructionStart = startIndex;
334
335                     var opcodeEnd = instruction.indexOf(":");
336
337                     var opcode;
338                     var parameters;
339                     if (opcodeEnd == -1) {
340                         opcode = instruction;
341                         parameters = new Array();
342                     }
343                     else {
344                         opcode = instruction.substr(0, opcodeEnd);
345                         parameters = instruction.substr(opcodeEnd+1).split(",");
346                     }
347
348                     // If we're done parsing, handle the next response.
349                     if (opcode.length == 0) {
350
351                         if (isConnected()) {
352                             delete xmlhttprequest;
353                             if (nextRequest)
354                                 handleResponse(nextRequest);
355                         }
356
357                         break;
358                     }
359
360                     // Call instruction handler.
361                     doInstruction(opcode, parameters);
362                 }
363
364                 // Start search at end of string.
365                 startIndex = current.length;
366
367                 delete instruction;
368                 delete parameters;
369
370             }
371
372         }
373
374         xmlhttprequest.onreadystatechange = parseResponse;
375         parseResponse();
376
377     }
378
379
380     function makeRequest() {
381
382         // Download self
383         var xmlhttprequest = new XMLHttpRequest();
384         xmlhttprequest.open("POST", TUNNEL_READ);
385         xmlhttprequest.send(null); 
386
387         return xmlhttprequest;
388
389     }
390
391     function escapeGuacamoleString(str) {
392
393         var escapedString = "";
394
395         for (var i=0; i<str.length; i++) {
396
397             var c = str.charAt(i);
398             if (c == ",")
399                 escapedString += "\\c";
400             else if (c == ";")
401                 escapedString += "\\s";
402             else if (c == "\\")
403                 escapedString += "\\\\";
404             else
405                 escapedString += c;
406
407         }
408
409         return escapedString;
410
411     }
412
413     function unescapeGuacamoleString(str) {
414
415         var unescapedString = "";
416
417         for (var i=0; i<str.length; i++) {
418
419             var c = str.charAt(i);
420             if (c == "\\" && i<str.length-1) {
421
422                 var escapeChar = str.charAt(++i);
423                 if (escapeChar == "c")
424                     unescapedString += ",";
425                 else if (escapeChar == "s")
426                     unescapedString += ";";
427                 else if (escapeChar == "\\")
428                     unescapedString += "\\";
429                 else
430                     unescapedString += "\\" + escapeChar;
431
432             }
433             else
434                 unescapedString += c;
435
436         }
437
438         return unescapedString;
439
440     }
441
442     var instructionHandlers = {
443
444         "error": function(parameters) {
445             showError(unescapeGuacamoleString(parameters[0]));
446         },
447
448         "name": function(parameters) {
449             document.title = unescapeGuacamoleString(parameters[0]);
450         },
451
452         "clipboard": function(parameters) {
453             clipboardHandler(unescapeGuacamoleString(parameters[0]));
454         },
455
456         "size": function(parameters) {
457
458             var width = parseInt(parameters[0]);
459             var height = parseInt(parameters[1]);
460
461             // Update (set) display size
462             if (display && (background == null || cursor == null)) {
463                 display.style.width = width + "px";
464                 display.style.height = height + "px";
465
466                 background = new Layer(width, height);
467                 cursor = new Layer(width, height);
468
469                 display.appendChild(background);
470                 display.appendChild(cursor);
471             }
472
473         },
474
475         "rect": function(parameters) {
476
477             var x = parseInt(parameters[0]);
478             var y = parseInt(parameters[1]);
479             var w = parseInt(parameters[2]);
480             var h = parseInt(parameters[3]);
481             var color = parameters[4];
482
483             background.drawRect(
484                 x,
485                 y,
486                 w,
487                 h,
488                 color
489             );
490
491         },
492
493         "png": function(parameters) {
494
495             var x = parseInt(parameters[0]);
496             var y = parseInt(parameters[1]);
497             var data = parameters[2];
498
499             background.draw(
500                 x,
501                 y,
502                 "data:image/png;base64," + data
503             );
504
505             // If received first update, no longer waiting.
506             if (currentState == STATE_WAITING)
507                 setState(STATE_CONNECTED);
508
509         },
510
511         "copy": function(parameters) {
512
513             var srcX = parseInt(parameters[0]);
514             var srcY = parseInt(parameters[1]);
515             var srcWidth = parseInt(parameters[2]);
516             var srcHeight = parseInt(parameters[3]);
517             var dstX = parseInt(parameters[4]);
518             var dstY = parseInt(parameters[5]);
519
520             background.copyRect(
521                 srcX,
522                 srcY,
523                 srcWidth, 
524                 srcHeight, 
525                 dstX,
526                 dstY 
527             );
528
529         },
530
531         "cursor": function(parameters) {
532
533             var x = parseInt(parameters[0]);
534             var y = parseInt(parameters[1]);
535             var data = parameters[2];
536
537             // Start cursor image load
538             var image = new Image();
539             image.onload = function() {
540                 cursorImage = image;
541                 cursorHotspotX = x;
542                 cursorHotspotY = y;
543                 redrawCursor();
544             };
545             image.src = "data:image/png;base64," + data
546
547         }
548       
549     };
550
551
552     function doInstruction(opcode, parameters) {
553
554         var handler = instructionHandlers[opcode];
555         if (handler)
556             handler(parameters);
557
558     }
559         
560
561     this.connect = function() {
562
563         setState(STATE_CONNECTING);
564
565         // Start tunnel and connect synchronously
566         var connect_xmlhttprequest = new XMLHttpRequest();
567         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
568         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
569         connect_xmlhttprequest.setRequestHeader("Content-length", 0);
570         connect_xmlhttprequest.send(null);
571
572         // Start reading data
573         setState(STATE_WAITING);
574         handleResponse(makeRequest());
575
576     };
577
578     
579     function disconnect() {
580
581         // Only attempt disconnection not disconnected.
582         if (currentState != STATE_DISCONNECTED
583                 && currentState != STATE_DISCONNECTING) {
584
585             var message = "disconnect;";
586             setState(STATE_DISCONNECTING);
587
588             // Send disconnect message (synchronously... as necessary until handoff is implemented)
589             var disconnect_xmlhttprequest = new XMLHttpRequest();
590             disconnect_xmlhttprequest.open("POST", TUNNEL_WRITE, false);
591             disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
592             disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
593             disconnect_xmlhttprequest.send(message);
594
595             setState(STATE_DISCONNECTED);
596         }
597
598     }
599
600     this.disconnect = disconnect;
601
602 }