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