Support for new instruction format.
[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  */
18
19 // Guacamole namespace
20 var Guacamole = Guacamole || {};
21
22 /**
23  * Guacamole protocol client. Given a display element and {@link Guacamole.Tunnel},
24  * automatically handles incoming and outgoing Guacamole instructions via the
25  * provided tunnel, updating the display using one or more canvas elements.
26  * 
27  * @constructor
28  * @param {Element} display The display element to add canvas elements to.
29  * @param {Guacamole.Tunnel} tunnel The tunnel to use to send and receive
30  *                                  Guacamole instructions.
31  */
32 Guacamole.Client = function(display, tunnel) {
33
34     var guac_client = this;
35
36     var STATE_IDLE          = 0;
37     var STATE_CONNECTING    = 1;
38     var STATE_WAITING       = 2;
39     var STATE_CONNECTED     = 3;
40     var STATE_DISCONNECTING = 4;
41     var STATE_DISCONNECTED  = 5;
42
43     var currentState = STATE_IDLE;
44
45     tunnel.oninstruction = doInstruction;
46
47     tunnel.onerror = function(message) {
48         if (guac_client.onerror)
49             guac_client.onerror(message);
50     };
51
52     // Display must be relatively positioned for mouse to be handled properly
53     display.style.position = "relative";
54
55     function setState(state) {
56         if (state != currentState) {
57             currentState = state;
58             if (guac_client.onstatechange)
59                 guac_client.onstatechange(currentState);
60         }
61     }
62
63     function isConnected() {
64         return currentState == STATE_CONNECTED
65             || currentState == STATE_WAITING;
66     }
67
68     var cursorImage = null;
69     var cursorHotspotX = 0;
70     var cursorHotspotY = 0;
71
72     var cursorRectX = 0;
73     var cursorRectY = 0;
74     var cursorRectW = 0;
75     var cursorRectH = 0;
76
77     var cursorHidden = 0;
78
79     function redrawCursor(x, y) {
80
81         // Hide hardware cursor
82         if (cursorHidden == 0) {
83             display.className += " guac-hide-cursor";
84             cursorHidden = 1;
85         }
86
87         // Erase old cursor
88         cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
89
90         // Update rect
91         cursorRectX = x - cursorHotspotX;
92         cursorRectY = y - cursorHotspotY;
93         cursorRectW = cursorImage.width;
94         cursorRectH = cursorImage.height;
95
96         // Draw new cursor
97         cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
98     }
99
100     guac_client.sendKeyEvent = function(pressed, keysym) {
101         // Do not send requests if not connected
102         if (!isConnected())
103             return;
104
105         tunnel.sendMessage("key:" +  keysym + "," + pressed + ";");
106     };
107
108     guac_client.sendMouseState = function(mouseState) {
109
110         // Do not send requests if not connected
111         if (!isConnected())
112             return;
113
114         // Draw client-side cursor
115         if (cursorImage != null) {
116             redrawCursor(
117                 mouseState.x,
118                 mouseState.y
119             );
120         }
121
122         // Build mask
123         var buttonMask = 0;
124         if (mouseState.left)   buttonMask |= 1;
125         if (mouseState.middle) buttonMask |= 2;
126         if (mouseState.right)  buttonMask |= 4;
127         if (mouseState.up)     buttonMask |= 8;
128         if (mouseState.down)   buttonMask |= 16;
129
130         // Send message
131         tunnel.sendMessage("mouse:" + mouseState.x + "," + mouseState.y + "," + buttonMask + ";");
132     };
133
134     guac_client.setClipboard = function(data) {
135
136         // Do not send requests if not connected
137         if (!isConnected())
138             return;
139
140         tunnel.sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
141     };
142
143     // Handlers
144     guac_client.onstatechange = null;
145     guac_client.onname = null;
146     guac_client.onerror = null;
147     guac_client.onclipboard = null;
148
149     // Layers
150     var displayWidth = 0;
151     var displayHeight = 0;
152
153     var layers = new Array();
154     var buffers = new Array();
155     var cursor = null;
156
157     guac_client.getLayers = function() {
158         return layers;
159     };
160
161     function getLayer(index) {
162
163         // If negative index, use buffer
164         if (index < 0) {
165
166             index = -1 - index;
167             var buffer = buffers[index];
168
169             // Create buffer if necessary
170             if (buffer == null) {
171                 buffer = new Guacamole.Layer(0, 0);
172                 buffer.autosize = 1;
173                 buffers[index] = buffer;
174             }
175
176             return buffer;
177         }
178
179         // If non-negative, use visible layer
180         else {
181
182             var layer = layers[index];
183             if (layer == null) {
184
185                 // Add new layer
186                 layer = new Guacamole.Layer(displayWidth, displayHeight);
187                 
188                 // Set layer position
189                 var canvas = layer.getCanvas();
190                 canvas.style.position = "absolute";
191                 canvas.style.left = "0px";
192                 canvas.style.top = "0px";
193
194                 layers[index] = layer;
195
196                 // (Re)-add existing layers in order
197                 for (var i=0; i<layers.length; i++) {
198                     if (layers[i]) {
199
200                         // If already present, remove
201                         if (layers[i].parentNode === display)
202                             display.removeChild(layers[i].getCanvas());
203
204                         // Add to end
205                         display.appendChild(layers[i].getCanvas());
206                     }
207                 }
208
209                 // Add cursor layer last
210                 if (cursor != null) {
211                     if (cursor.parentNode === display)
212                         display.removeChild(cursor.getCanvas());
213                     display.appendChild(cursor.getCanvas());
214                 }
215
216             }
217             else {
218                 // Reset size
219                 layer.resize(displayWidth, displayHeight);
220             }
221
222             return layer;
223         }
224
225     }
226
227     var instructionHandlers = {
228
229         "error": function(parameters) {
230             if (guac_client.onerror) guac_client.onerror(parameters[0]);
231             disconnect();
232         },
233
234         "name": function(parameters) {
235             if (guac_client.onname) guac_client.onname(parameters[0]);
236         },
237
238         "clipboard": function(parameters) {
239             if (guac_client.onclipboard) guac_client.onclipboard(parameters[0]);
240         },
241
242         "size": function(parameters) {
243
244             displayWidth = parseInt(parameters[0]);
245             displayHeight = parseInt(parameters[1]);
246
247             // Update (set) display size
248             display.style.width = displayWidth + "px";
249             display.style.height = displayHeight + "px";
250
251             // Set cursor layer width/height
252             if (cursor != null)
253                 cursor.resize(displayWidth, displayHeight);
254
255         },
256
257         "png": function(parameters) {
258
259             var channelMask = parseInt(parameters[0]);
260             var layer = getLayer(parseInt(parameters[1]));
261             var x = parseInt(parameters[2]);
262             var y = parseInt(parameters[3]);
263             var data = parameters[4];
264
265             layer.setChannelMask(channelMask);
266
267             layer.draw(
268                 x,
269                 y,
270                 "data:image/png;base64," + data
271             );
272
273             // If received first update, no longer waiting.
274             if (currentState == STATE_WAITING)
275                 setState(STATE_CONNECTED);
276
277         },
278
279         "copy": function(parameters) {
280
281             var srcL = getLayer(parseInt(parameters[0]));
282             var srcX = parseInt(parameters[1]);
283             var srcY = parseInt(parameters[2]);
284             var srcWidth = parseInt(parameters[3]);
285             var srcHeight = parseInt(parameters[4]);
286             var channelMask = parseInt(parameters[5]);
287             var dstL = getLayer(parseInt(parameters[6]));
288             var dstX = parseInt(parameters[7]);
289             var dstY = parseInt(parameters[8]);
290
291             dstL.setChannelMask(channelMask);
292
293             dstL.copyRect(
294                 srcL,
295                 srcX,
296                 srcY,
297                 srcWidth, 
298                 srcHeight, 
299                 dstX,
300                 dstY 
301             );
302
303         },
304
305         "rect": function(parameters) {
306
307             var channelMask = parseInt(parameters[0]);
308             var layer = getLayer(parseInt(parameters[1]));
309             var x = parseInt(parameters[2]);
310             var y = parseInt(parameters[3]);
311             var w = parseInt(parameters[4]);
312             var h = parseInt(parameters[5]);
313             var r = parseInt(parameters[6]);
314             var g = parseInt(parameters[7]);
315             var b = parseInt(parameters[8]);
316             var a = parseInt(parameters[9]);
317
318             layer.setChannelMask(channelMask);
319
320             layer.drawRect(
321                 x, y, w, h,
322                 r, g, b, a
323             );
324
325         },
326
327         "clip": function(parameters) {
328
329             var layer = getLayer(parseInt(parameters[0]));
330             var x = parseInt(parameters[1]);
331             var y = parseInt(parameters[2]);
332             var w = parseInt(parameters[3]);
333             var h = parseInt(parameters[4]);
334
335             layer.clipRect(x, y, w, h);
336
337         },
338
339         "cursor": function(parameters) {
340
341             var x = parseInt(parameters[0]);
342             var y = parseInt(parameters[1]);
343             var data = parameters[2];
344
345             if (cursor == null) {
346                 cursor = new Guacamole.Layer(displayWidth, displayHeight);
347                 
348                 var canvas = cursor.getCanvas();
349                 canvas.style.position = "absolute";
350                 canvas.style.left = "0px";
351                 canvas.style.top = "0px";
352
353                 display.appendChild(canvas);
354             }
355
356             // Start cursor image load
357             var image = new Image();
358             image.onload = function() {
359                 cursorImage = image;
360
361                 var cursorX = cursorRectX + cursorHotspotX;
362                 var cursorY = cursorRectY + cursorHotspotY;
363
364                 cursorHotspotX = x;
365                 cursorHotspotY = y;
366
367                 redrawCursor(cursorX, cursorY);
368             };
369             image.src = "data:image/png;base64," + data
370
371         },
372
373         "sync": function(parameters) {
374
375             var timestamp = parameters[0];
376
377             // When all layers have finished rendering all instructions
378             // UP TO THIS POINT IN TIME, send sync response.
379
380             var layersToSync = 0;
381             function syncLayer() {
382
383                 layersToSync--;
384
385                 // Send sync response when layers are finished
386                 if (layersToSync == 0)
387                     tunnel.sendMessage("sync:" + timestamp + ";");
388
389             }
390
391             // Count active, not-ready layers and install sync tracking hooks
392             for (var i=0; i<layers.length; i++) {
393
394                 var layer = layers[i];
395                 if (layer && !layer.isReady()) {
396                     layersToSync++;
397                     layer.sync(syncLayer);
398                 }
399
400             }
401
402             // If all layers are ready, then we didn't install any hooks.
403             // Send sync message now,
404             if (layersToSync == 0)
405                 tunnel.sendMessage("sync:" + timestamp + ";");
406
407         }
408       
409     };
410
411
412     function doInstruction(opcode, parameters) {
413
414         var handler = instructionHandlers[opcode];
415         if (handler)
416             handler(parameters);
417
418     }
419
420
421     function disconnect() {
422
423         // Only attempt disconnection not disconnected.
424         if (currentState != STATE_DISCONNECTED
425                 && currentState != STATE_DISCONNECTING) {
426
427             setState(STATE_DISCONNECTING);
428             tunnel.sendMessage("disconnect;");
429             tunnel.disconnect();
430             setState(STATE_DISCONNECTED);
431         }
432
433     }
434
435     function escapeGuacamoleString(str) {
436
437         var escapedString = "";
438
439         for (var i=0; i<str.length; i++) {
440
441             var c = str.charAt(i);
442             if (c == ",")
443                 escapedString += "\\c";
444             else if (c == ";")
445                 escapedString += "\\s";
446             else if (c == "\\")
447                 escapedString += "\\\\";
448             else
449                 escapedString += c;
450
451         }
452
453         return escapedString;
454
455     }
456
457     guac_client.disconnect = disconnect;
458     guac_client.connect = function(data) {
459
460         setState(STATE_CONNECTING);
461
462         try {
463             tunnel.connect(data);
464         }
465         catch (e) {
466             setState(STATE_IDLE);
467             throw e;
468         }
469
470         setState(STATE_WAITING);
471     };
472
473 }