removeChild(), not removeNode()
[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     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", TUNNEL_WRITE);
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     var nameHandler = null;
255     this.setNameHandler = function(handler) {
256         nameHandler = handler;
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             for (var i=0; i<layers.length; i++) {
274                 layers[i].filter(desaturateFilter);
275             }
276
277             if (errorHandler)
278                 errorHandler(error);
279         }
280     }
281
282     function handleErrors(message) {
283         var errors = message.getErrors();
284         for (var errorIndex=0; errorIndex<errors.length; errorIndex++)
285             showError(errors[errorIndex].getMessage());
286     }
287
288     var clipboardHandler = null;
289     var requests = 0;
290
291     this.setClipboardHandler = function(handler) {
292         clipboardHandler = handler;
293     };
294
295
296     function handleResponse(xmlhttprequest) {
297
298         var nextRequest = null;
299
300         var instructionStart = 0;
301         var startIndex = 0;
302
303         function parseResponse() {
304
305             // Start next request as soon as possible
306             if (xmlhttprequest.readyState >= 2 && nextRequest == null)
307                 nextRequest = makeRequest();
308
309             // Parse stream when data is received and when complete.
310             if (xmlhttprequest.readyState == 3 ||
311                 xmlhttprequest.readyState == 4) {
312
313                 // Halt on error during request
314                 if (xmlhttprequest.status == 0) {
315                     showError("Request canceled by browser.");
316                     return;
317                 }
318                 else if (xmlhttprequest.status != 200) {
319                     showError("Error during request (HTTP " + xmlhttprequest.status + "): " + xmlhttprequest.statusText);
320                     return;
321                 }
322
323                 var current = xmlhttprequest.responseText;
324                 var instructionEnd;
325                 
326                 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
327
328                     // Start next search at next instruction
329                     startIndex = instructionEnd+1;
330
331                     var instruction = current.substr(instructionStart,
332                             instructionEnd - instructionStart);
333
334                     instructionStart = startIndex;
335
336                     var opcodeEnd = instruction.indexOf(":");
337
338                     var opcode;
339                     var parameters;
340                     if (opcodeEnd == -1) {
341                         opcode = instruction;
342                         parameters = new Array();
343                     }
344                     else {
345                         opcode = instruction.substr(0, opcodeEnd);
346                         parameters = instruction.substr(opcodeEnd+1).split(",");
347                     }
348
349                     // If we're done parsing, handle the next response.
350                     if (opcode.length == 0) {
351
352                         if (isConnected()) {
353                             delete xmlhttprequest;
354                             if (nextRequest)
355                                 handleResponse(nextRequest);
356                         }
357
358                         break;
359                     }
360
361                     // Call instruction handler.
362                     doInstruction(opcode, parameters);
363                 }
364
365                 // Start search at end of string.
366                 startIndex = current.length;
367
368                 delete instruction;
369                 delete parameters;
370
371             }
372
373         }
374
375         xmlhttprequest.onreadystatechange = parseResponse;
376         parseResponse();
377
378     }
379
380
381     function makeRequest() {
382
383         // Download self
384         var xmlhttprequest = new XMLHttpRequest();
385         xmlhttprequest.open("POST", TUNNEL_READ);
386         xmlhttprequest.send(null); 
387
388         return xmlhttprequest;
389
390     }
391
392     function escapeGuacamoleString(str) {
393
394         var escapedString = "";
395
396         for (var i=0; i<str.length; i++) {
397
398             var c = str.charAt(i);
399             if (c == ",")
400                 escapedString += "\\c";
401             else if (c == ";")
402                 escapedString += "\\s";
403             else if (c == "\\")
404                 escapedString += "\\\\";
405             else
406                 escapedString += c;
407
408         }
409
410         return escapedString;
411
412     }
413
414     function unescapeGuacamoleString(str) {
415
416         var unescapedString = "";
417
418         for (var i=0; i<str.length; i++) {
419
420             var c = str.charAt(i);
421             if (c == "\\" && i<str.length-1) {
422
423                 var escapeChar = str.charAt(++i);
424                 if (escapeChar == "c")
425                     unescapedString += ",";
426                 else if (escapeChar == "s")
427                     unescapedString += ";";
428                 else if (escapeChar == "\\")
429                     unescapedString += "\\";
430                 else
431                     unescapedString += "\\" + escapeChar;
432
433             }
434             else
435                 unescapedString += c;
436
437         }
438
439         return unescapedString;
440
441     }
442
443     // Layers
444     var displayWidth = 0;
445     var displayHeight = 0;
446
447     var layers = new Array();
448     var buffers = new Array();
449     var cursor = null;
450
451     function getLayer(index) {
452
453         // If negative index, use buffer
454         if (index < 0) {
455
456             index = -1 - index;
457             var buffer = buffers[index];
458
459             // Create buffer if necessary
460             if (buffer == null) {
461                 buffer = new Layer(0, 0);
462                 buffer.setAutosize(1);
463                 buffers[index] = buffer;
464             }
465
466             return buffer;
467         }
468
469         // If non-negative, use visible layer
470         else {
471
472             var layer = layers[index];
473             if (layer == null) {
474
475                 // Add new layer
476                 layer = new Layer(displayWidth, displayHeight);
477                 layers[index] = layer;
478
479                 // (Re)-add existing layers in order
480                 for (var i=0; i<layers.length; i++) {
481                     if (layers[i]) {
482
483                         // If already present, remove
484                         if (layers[i].parentNode === display)
485                             display.removeChild(layers[i]);
486
487                         // Add to end
488                         display.appendChild(layers[i]);
489                     }
490                 }
491
492                 // Add cursor layer last
493                 if (cursor != null) {
494                     if (cursor.parentNode === display)
495                         display.removeChild(cursor);
496                     display.appendChild(cursor);
497                 }
498
499             }
500             else {
501                 // Reset size
502                 layer.resize(displayWidth, displayHeight);
503             }
504
505             return layer;
506         }
507
508     }
509
510     var instructionHandlers = {
511
512         "error": function(parameters) {
513             showError(unescapeGuacamoleString(parameters[0]));
514         },
515
516         "name": function(parameters) {
517             nameHandler(unescapeGuacamoleString(parameters[0]));
518         },
519
520         "clipboard": function(parameters) {
521             clipboardHandler(unescapeGuacamoleString(parameters[0]));
522         },
523
524         "size": function(parameters) {
525
526             displayWidth = parseInt(parameters[0]);
527             displayHeight = parseInt(parameters[1]);
528
529             // Update (set) display size
530             if (display) {
531                 display.style.width = displayWidth + "px";
532                 display.style.height = displayHeight + "px";
533             }
534
535             // Set cursor layer width/height
536             if (cursor != null)
537                 cursor.resize(displayWidth, displayHeight);
538
539         },
540
541         "png": function(parameters) {
542
543             var layer = parseInt(parameters[0]);
544             var x = parseInt(parameters[1]);
545             var y = parseInt(parameters[2]);
546             var data = parameters[3];
547
548             getLayer(layer).draw(
549                 x,
550                 y,
551                 "data:image/png;base64," + data
552             );
553
554             // If received first update, no longer waiting.
555             if (currentState == STATE_WAITING)
556                 setState(STATE_CONNECTED);
557
558         },
559
560         "copy": function(parameters) {
561
562             var srcL = parseInt(parameters[0]);
563             var srcX = parseInt(parameters[1]);
564             var srcY = parseInt(parameters[2]);
565             var srcWidth = parseInt(parameters[3]);
566             var srcHeight = parseInt(parameters[4]);
567             var dstL = parseInt(parameters[5]);
568             var dstX = parseInt(parameters[6]);
569             var dstY = parseInt(parameters[7]);
570
571             getLayer(dstL).copyRect(
572                 getLayer(srcL),
573                 srcX,
574                 srcY,
575                 srcWidth, 
576                 srcHeight, 
577                 dstX,
578                 dstY 
579             );
580
581         },
582
583         "cursor": function(parameters) {
584
585             var x = parseInt(parameters[0]);
586             var y = parseInt(parameters[1]);
587             var data = parameters[2];
588
589             if (cursor == null) {
590                 cursor = new Layer(displayWidth, displayHeight);
591                 display.appendChild(cursor);
592             }
593
594             // Start cursor image load
595             var image = new Image();
596             image.onload = function() {
597                 cursorImage = image;
598                 cursorHotspotX = x;
599                 cursorHotspotY = y;
600                 redrawCursor();
601             };
602             image.src = "data:image/png;base64," + data
603
604         }
605       
606     };
607
608
609     function doInstruction(opcode, parameters) {
610
611         var handler = instructionHandlers[opcode];
612         if (handler)
613             handler(parameters);
614
615     }
616         
617
618     this.connect = function() {
619
620         setState(STATE_CONNECTING);
621
622         // Start tunnel and connect synchronously
623         var connect_xmlhttprequest = new XMLHttpRequest();
624         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
625         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
626         connect_xmlhttprequest.setRequestHeader("Content-length", 0);
627         connect_xmlhttprequest.send(null);
628
629         // Start reading data
630         setState(STATE_WAITING);
631         handleResponse(makeRequest());
632
633     };
634
635     
636     function disconnect() {
637
638         // Only attempt disconnection not disconnected.
639         if (currentState != STATE_DISCONNECTED
640                 && currentState != STATE_DISCONNECTING) {
641
642             var message = "disconnect;";
643             setState(STATE_DISCONNECTING);
644
645             // Send disconnect message (synchronously... as necessary until handoff is implemented)
646             var disconnect_xmlhttprequest = new XMLHttpRequest();
647             disconnect_xmlhttprequest.open("POST", TUNNEL_WRITE, false);
648             disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
649             disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
650             disconnect_xmlhttprequest.send(message);
651
652             setState(STATE_DISCONNECTED);
653         }
654
655     }
656
657     this.disconnect = disconnect;
658
659 }