Added autosize to layer
[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                 // Remove all children
480                 display.innerHTML = "";
481
482                 // Add existing layers in order
483                 for (var i=0; i<layers.length; i++)
484                     display.appendChild(layers[i]);
485
486                 // Add cursor layer last
487                 if (cursor != null)
488                     display.appendChild(cursor);
489
490             }
491             else {
492                 // Reset size
493                 layer.resize(displayWidth, displayHeight);
494             }
495
496             return layer;
497         }
498
499     }
500
501     var instructionHandlers = {
502
503         "error": function(parameters) {
504             showError(unescapeGuacamoleString(parameters[0]));
505         },
506
507         "name": function(parameters) {
508             nameHandler(unescapeGuacamoleString(parameters[0]));
509         },
510
511         "clipboard": function(parameters) {
512             clipboardHandler(unescapeGuacamoleString(parameters[0]));
513         },
514
515         "size": function(parameters) {
516
517             displayWidth = parseInt(parameters[0]);
518             displayHeight = parseInt(parameters[1]);
519
520             // Update (set) display size
521             if (display) {
522                 display.style.width = displayWidth + "px";
523                 display.style.height = displayHeight + "px";
524             }
525
526             // Set cursor layer width/height
527             if (cursor != null)
528                 cursor.resize(displayWidth, displayHeight);
529
530         },
531
532         "png": function(parameters) {
533
534             var layer = parseInt(parameters[0]);
535             var x = parseInt(parameters[1]);
536             var y = parseInt(parameters[2]);
537             var data = parameters[3];
538
539             getLayer(layer).draw(
540                 x,
541                 y,
542                 "data:image/png;base64," + data
543             );
544
545             // If received first update, no longer waiting.
546             if (currentState == STATE_WAITING)
547                 setState(STATE_CONNECTED);
548
549         },
550
551         "copy": function(parameters) {
552
553             var srcL = parseInt(parameters[0]);
554             var srcX = parseInt(parameters[1]);
555             var srcY = parseInt(parameters[2]);
556             var srcWidth = parseInt(parameters[3]);
557             var srcHeight = parseInt(parameters[4]);
558             var dstL = parseInt(parameters[5]);
559             var dstX = parseInt(parameters[6]);
560             var dstY = parseInt(parameters[7]);
561
562             getLayer(dstL).copyRect(
563                 getLayer(srcL),
564                 srcX,
565                 srcY,
566                 srcWidth, 
567                 srcHeight, 
568                 dstX,
569                 dstY 
570             );
571
572         },
573
574         "cursor": function(parameters) {
575
576             var x = parseInt(parameters[0]);
577             var y = parseInt(parameters[1]);
578             var data = parameters[2];
579
580             if (cursor == null) {
581                 cursor = new Layer(displayWidth, displayHeight);
582                 display.appendChild(cursor);
583             }
584
585             // Start cursor image load
586             var image = new Image();
587             image.onload = function() {
588                 cursorImage = image;
589                 cursorHotspotX = x;
590                 cursorHotspotY = y;
591                 redrawCursor();
592             };
593             image.src = "data:image/png;base64," + data
594
595         }
596       
597     };
598
599
600     function doInstruction(opcode, parameters) {
601
602         var handler = instructionHandlers[opcode];
603         if (handler)
604             handler(parameters);
605
606     }
607         
608
609     this.connect = function() {
610
611         setState(STATE_CONNECTING);
612
613         // Start tunnel and connect synchronously
614         var connect_xmlhttprequest = new XMLHttpRequest();
615         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
616         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
617         connect_xmlhttprequest.setRequestHeader("Content-length", 0);
618         connect_xmlhttprequest.send(null);
619
620         // Start reading data
621         setState(STATE_WAITING);
622         handleResponse(makeRequest());
623
624     };
625
626     
627     function disconnect() {
628
629         // Only attempt disconnection not disconnected.
630         if (currentState != STATE_DISCONNECTED
631                 && currentState != STATE_DISCONNECTING) {
632
633             var message = "disconnect;";
634             setState(STATE_DISCONNECTING);
635
636             // Send disconnect message (synchronously... as necessary until handoff is implemented)
637             var disconnect_xmlhttprequest = new XMLHttpRequest();
638             disconnect_xmlhttprequest.open("POST", TUNNEL_WRITE, false);
639             disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
640             disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
641             disconnect_xmlhttprequest.send(message);
642
643             setState(STATE_DISCONNECTED);
644         }
645
646     }
647
648     this.disconnect = disconnect;
649
650 }