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