Stub implementation of layer/buffer support (initial compat for new version of protocol)
[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             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     // Layers
443     var displayWidth = null;
444     var displayHeight = null;
445     var background = null;
446     var cursor = null;
447
448     function getLayer(index) {
449
450         // FIXME: Stub - does not actually handle layers or the layer index
451
452         if (background == null) {
453             background = new Layer(displayWidth, displayHeight);
454
455             if (cursor != null)
456                 display.insertBefore(background, cursor);
457             else
458                 display.appendChild(background);
459         }
460
461         return background;
462
463     }
464
465     var instructionHandlers = {
466
467         "error": function(parameters) {
468             showError(unescapeGuacamoleString(parameters[0]));
469         },
470
471         "name": function(parameters) {
472             nameHandler(unescapeGuacamoleString(parameters[0]));
473         },
474
475         "clipboard": function(parameters) {
476             clipboardHandler(unescapeGuacamoleString(parameters[0]));
477         },
478
479         "size": function(parameters) {
480
481             displayWidth = parseInt(parameters[0]);
482             displayHeight = parseInt(parameters[1]);
483
484             // Update (set) display size
485             if (display) {
486                 display.style.width = displayWidth + "px";
487                 display.style.height = displayHeight + "px";
488             }
489
490             // Set cursor layer width/height here
491
492         },
493
494         "png": function(parameters) {
495
496             var layer = parseInt(parameters[0]);
497             var x = parseInt(parameters[1]);
498             var y = parseInt(parameters[2]);
499             var data = parameters[3];
500
501             getLayer(layer).draw(
502                 x,
503                 y,
504                 "data:image/png;base64," + data
505             );
506
507             // If received first update, no longer waiting.
508             if (currentState == STATE_WAITING)
509                 setState(STATE_CONNECTED);
510
511         },
512
513         "copy": function(parameters) {
514
515             var srcL = parseInt(parameters[0]);
516             var srcX = parseInt(parameters[1]);
517             var srcY = parseInt(parameters[2]);
518             var srcWidth = parseInt(parameters[3]);
519             var srcHeight = parseInt(parameters[4]);
520             var dstL = parseInt(parameters[5]);
521             var dstX = parseInt(parameters[6]);
522             var dstY = parseInt(parameters[7]);
523
524             getLayer(dstL).copyRect(
525                 getLayer(srcL),
526                 srcX,
527                 srcY,
528                 srcWidth, 
529                 srcHeight, 
530                 dstX,
531                 dstY 
532             );
533
534         },
535
536         "cursor": function(parameters) {
537
538             var x = parseInt(parameters[0]);
539             var y = parseInt(parameters[1]);
540             var data = parameters[2];
541
542             if (cursor == null) {
543                 cursor = new Layer(displayWidth, displayHeight);
544                 display.appendChild(cursor);
545             }
546
547             // Start cursor image load
548             var image = new Image();
549             image.onload = function() {
550                 cursorImage = image;
551                 cursorHotspotX = x;
552                 cursorHotspotY = y;
553                 redrawCursor();
554             };
555             image.src = "data:image/png;base64," + data
556
557         }
558       
559     };
560
561
562     function doInstruction(opcode, parameters) {
563
564         var handler = instructionHandlers[opcode];
565         if (handler)
566             handler(parameters);
567
568     }
569         
570
571     this.connect = function() {
572
573         setState(STATE_CONNECTING);
574
575         // Start tunnel and connect synchronously
576         var connect_xmlhttprequest = new XMLHttpRequest();
577         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
578         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
579         connect_xmlhttprequest.setRequestHeader("Content-length", 0);
580         connect_xmlhttprequest.send(null);
581
582         // Start reading data
583         setState(STATE_WAITING);
584         handleResponse(makeRequest());
585
586     };
587
588     
589     function disconnect() {
590
591         // Only attempt disconnection not disconnected.
592         if (currentState != STATE_DISCONNECTED
593                 && currentState != STATE_DISCONNECTING) {
594
595             var message = "disconnect;";
596             setState(STATE_DISCONNECTING);
597
598             // Send disconnect message (synchronously... as necessary until handoff is implemented)
599             var disconnect_xmlhttprequest = new XMLHttpRequest();
600             disconnect_xmlhttprequest.open("POST", TUNNEL_WRITE, false);
601             disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
602             disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
603             disconnect_xmlhttprequest.send(message);
604
605             setState(STATE_DISCONNECTED);
606         }
607
608     }
609
610     this.disconnect = disconnect;
611
612 }