Updated comments
[guacamole-common-js.git] / src / main / resources / tunnel.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  * Core object providing abstract communication for Guacamole. This object
24  * is a null implementation whose functions do nothing. Guacamole applications
25  * should use {@link Guacamole.HTTPTunnel} instead, or implement their own tunnel based
26  * on this one.
27  * 
28  * @constructor
29  * @see Guacamole.HTTPTunnel
30  */
31 Guacamole.Tunnel = function() {
32
33     /**
34      * Connect to the tunnel with the given optional data. This data is
35      * typically used for authentication. The format of data accepted is
36      * up to the tunnel implementation.
37      * 
38      * @param {String} data The data to send to the tunnel when connecting.
39      */
40     this.connect = function(data) {};
41     
42     /**
43      * Disconnect from the tunnel.
44      */
45     this.disconnect = function() {};
46     
47     /**
48      * Send the given message through the tunnel to the service on the other
49      * side. All messages are guaranteed to be received in the order sent.
50      * 
51      * @param {...} elements The elements of the message to send to the
52      *                       service on the other side of the tunnel.
53      */
54     this.sendMessage = function(elements) {};
55     
56     /**
57      * Fired whenever an error is encountered by the tunnel.
58      * 
59      * @event
60      * @param {String} message A human-readable description of the error that
61      *                         occurred.
62      */
63     this.onerror = null;
64
65     /**
66      * Fired once for every complete Guacamole instruction received, in order.
67      * 
68      * @event
69      * @param {String} opcode The Guacamole instruction opcode.
70      * @param {Array} parameters The parameters provided for the instruction,
71      *                           if any.
72      */
73     this.oninstruction = null;
74
75 };
76
77 /**
78  * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
79  * 
80  * @constructor
81  * @augments Guacamole.Tunnel
82  * @param {String} tunnelURL The URL of the HTTP tunneling service.
83  */
84 Guacamole.HTTPTunnel = function(tunnelURL) {
85
86     /**
87      * Reference to this HTTP tunnel.
88      */
89     var tunnel = this;
90
91     var tunnel_uuid;
92
93     var TUNNEL_CONNECT = tunnelURL + "?connect";
94     var TUNNEL_READ    = tunnelURL + "?read:";
95     var TUNNEL_WRITE   = tunnelURL + "?write:";
96
97     var STATE_IDLE          = 0;
98     var STATE_CONNECTED     = 1;
99     var STATE_DISCONNECTED  = 2;
100
101     var currentState = STATE_IDLE;
102
103     var POLLING_ENABLED     = 1;
104     var POLLING_DISABLED    = 0;
105
106     // Default to polling - will be turned off automatically if not needed
107     var pollingMode = POLLING_ENABLED;
108
109     var sendingMessages = false;
110     var outputMessageBuffer = "";
111
112     this.sendMessage = function() {
113
114         // Do not attempt to send messages if not connected
115         if (currentState != STATE_CONNECTED)
116             return;
117
118         // Do not attempt to send empty messages
119         if (arguments.length == 0)
120             return;
121
122         /**
123          * Converts the given value to a length/string pair for use as an
124          * element in a Guacamole instruction.
125          * 
126          * @param value The value to convert.
127          * @return {String} The converted value. 
128          */
129         function getElement(value) {
130             var string = new String(value);
131             return string.length + "." + string; 
132         }
133
134         // Initialized message with first element
135         var message = getElement(arguments[0]);
136
137         // Append remaining elements
138         for (var i=1; i<arguments.length; i++)
139             message += "," + getElement(arguments[i]);
140
141         // Final terminator
142         message += ";";
143
144         // Add message to buffer
145         outputMessageBuffer += message;
146
147         // Send if not currently sending
148         if (!sendingMessages)
149             sendPendingMessages();
150
151     };
152
153     function sendPendingMessages() {
154
155         if (outputMessageBuffer.length > 0) {
156
157             sendingMessages = true;
158
159             var message_xmlhttprequest = new XMLHttpRequest();
160             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
161             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
162
163             // Once response received, send next queued event.
164             message_xmlhttprequest.onreadystatechange = function() {
165                 if (message_xmlhttprequest.readyState == 4)
166                     sendPendingMessages();
167             }
168
169             message_xmlhttprequest.send(outputMessageBuffer);
170             outputMessageBuffer = ""; // Clear buffer
171
172         }
173         else
174             sendingMessages = false;
175
176     }
177
178
179     function handleResponse(xmlhttprequest) {
180
181         var interval = null;
182         var nextRequest = null;
183
184         var dataUpdateEvents = 0;
185
186         // The location of the last element's terminator
187         var elementEnd = -1;
188
189         // Where to start the next length search or the next element
190         var startIndex = 0;
191
192         // Parsed elements
193         var elements = new Array();
194
195         function parseResponse() {
196
197             // Do not handle responses if not connected
198             if (currentState != STATE_CONNECTED) {
199                 
200                 // Clean up interval if polling
201                 if (interval != null)
202                     clearInterval(interval);
203                 
204                 return;
205             }
206
207             // Start next request as soon as possible IF request was successful
208             if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
209                 nextRequest = makeRequest();
210
211             // Parse stream when data is received and when complete.
212             if (xmlhttprequest.readyState == 3 ||
213                 xmlhttprequest.readyState == 4) {
214
215                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
216                 if (pollingMode == POLLING_ENABLED) {
217                     if (xmlhttprequest.readyState == 3 && interval == null)
218                         interval = setInterval(parseResponse, 30);
219                     else if (xmlhttprequest.readyState == 4 && interval != null)
220                         clearInterval(interval);
221                 }
222
223                 // If canceled, stop transfer
224                 if (xmlhttprequest.status == 0) {
225                     tunnel.disconnect();
226                     return;
227                 }
228
229                 // Halt on error during request
230                 else if (xmlhttprequest.status != 200) {
231
232                     // Get error message (if any)
233                     var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
234                     if (!message)
235                         message = "Internal server error";
236
237                     // Call error handler
238                     if (tunnel.onerror) tunnel.onerror(message);
239
240                     // Finish
241                     tunnel.disconnect();
242                     return;
243                 }
244
245                 var current = xmlhttprequest.responseText;
246
247                 // While search is within currently received data
248                 while (elementEnd < current.length) {
249
250                     // If we are waiting for element data
251                     if (elementEnd >= startIndex) {
252
253                         // We now have enough data for the element. Parse.
254                         var element = current.substring(startIndex, elementEnd);
255                         var terminator = current.substring(elementEnd, elementEnd+1);
256
257                         // Add element to array
258                         elements.push(element);
259
260                         // If last element, handle instruction
261                         if (terminator == ";") {
262
263                             // Get opcode
264                             var opcode = elements.shift();
265
266                             // Call instruction handler.
267                             if (tunnel.oninstruction != null)
268                                 tunnel.oninstruction(opcode, elements);
269
270                             // Clear elements
271                             elements.length = 0;
272
273                         }
274
275                         // Start searching for length at character after
276                         // element terminator
277                         startIndex = elementEnd + 1;
278
279                     }
280
281                     // Search for end of length
282                     var lengthEnd = current.indexOf(".", startIndex);
283                     if (lengthEnd != -1) {
284
285                         // Parse length
286                         var length = parseInt(current.substring(elementEnd+1, lengthEnd));
287
288                         // If we're done parsing, handle the next response.
289                         if (length == 0) {
290
291                             // Clean up interval if polling
292                             if (interval != null)
293                                 clearInterval(interval);
294                            
295                             // Clean up object
296                             xmlhttprequest.onreadystatechange = null;
297                             xmlhttprequest.abort();
298
299                             // Start handling next request
300                             if (nextRequest)
301                                 handleResponse(nextRequest);
302
303                             // Done parsing
304                             break;
305
306                         }
307
308                         // Calculate start of element
309                         startIndex = lengthEnd + 1;
310
311                         // Calculate location of element terminator
312                         elementEnd = startIndex + length;
313
314                     }
315                     
316                     // If no period yet, continue search when more data
317                     // is received
318                     else {
319                         startIndex = current.length;
320                         break;
321                     }
322
323                 } // end parse loop
324
325             }
326
327         }
328
329         // If response polling enabled, attempt to detect if still
330         // necessary (via wrapping parseResponse())
331         if (pollingMode == POLLING_ENABLED) {
332             xmlhttprequest.onreadystatechange = function() {
333
334                 // If we receive two or more readyState==3 events,
335                 // there is no need to poll.
336                 if (xmlhttprequest.readyState == 3) {
337                     dataUpdateEvents++;
338                     if (dataUpdateEvents >= 2) {
339                         pollingMode = POLLING_DISABLED;
340                         xmlhttprequest.onreadystatechange = parseResponse;
341                     }
342                 }
343
344                 parseResponse();
345             }
346         }
347
348         // Otherwise, just parse
349         else
350             xmlhttprequest.onreadystatechange = parseResponse;
351
352         parseResponse();
353
354     }
355
356
357     function makeRequest() {
358
359         // Download self
360         var xmlhttprequest = new XMLHttpRequest();
361         xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
362         xmlhttprequest.send(null);
363
364         return xmlhttprequest;
365
366     }
367
368     this.connect = function(data) {
369
370         // Start tunnel and connect synchronously
371         var connect_xmlhttprequest = new XMLHttpRequest();
372         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
373         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
374         connect_xmlhttprequest.send(data);
375
376         // If failure, throw error
377         if (connect_xmlhttprequest.status != 200) {
378
379             var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
380             if (!message)
381                 message = "Internal error";
382
383             throw new Error(message);
384
385         }
386
387         // Get UUID from response
388         tunnel_uuid = connect_xmlhttprequest.responseText;
389
390         // Start reading data
391         currentState = STATE_CONNECTED;
392         handleResponse(makeRequest());
393
394     };
395
396     this.disconnect = function() {
397         currentState = STATE_DISCONNECTED;
398     };
399
400 };
401
402 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
403
404
405 /**
406  * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
407  * 
408  * @constructor
409  * @augments Guacamole.Tunnel
410  * @param {String} tunnelURL The URL of the WebSocket tunneling service.
411  */
412 Guacamole.WebSocketTunnel = function(tunnelURL) {
413
414     /**
415      * Reference to this WebSocket tunnel.
416      */
417     var tunnel = this;
418
419     /**
420      * The WebSocket used by this tunnel.
421      */
422     var socket = null;
423
424     /**
425      * The WebSocket protocol corresponding to the protocol used for the current
426      * location.
427      */
428     var ws_protocol = {
429         "http:":  "ws:",
430         "https:": "wss:"
431     };
432
433     var STATE_IDLE          = 0;
434     var STATE_CONNECTED     = 1;
435     var STATE_DISCONNECTED  = 2;
436
437     var currentState = STATE_IDLE;
438     
439     // Transform current URL to WebSocket URL
440
441     // If not already a websocket URL
442     if (   tunnelURL.substring(0, 3) != "ws:"
443         && tunnelURL.substring(0, 4) != "wss:") {
444
445         var protocol = ws_protocol[window.location.protocol];
446
447         // If absolute URL, convert to absolute WS URL
448         if (tunnelURL.substring(0, 1) == "/")
449             tunnelURL =
450                 protocol
451                 + "//" + window.location.host
452                 + tunnelURL;
453
454         // Otherwise, construct absolute from relative URL
455         else {
456
457             // Get path from pathname
458             var slash = window.location.pathname.lastIndexOf("/");
459             var path  = window.location.pathname.substring(0, slash + 1);
460
461             // Construct absolute URL
462             tunnelURL =
463                 protocol
464                 + "//" + window.location.host
465                 + path
466                 + tunnelURL;
467
468         }
469
470     }
471
472     this.sendMessage = function(message) {
473
474         // Do not attempt to send messages if not connected
475         if (currentState != STATE_CONNECTED)
476             return;
477
478         socket.send(message);
479
480     };
481
482     this.connect = function(data) {
483
484         // Connect socket
485         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
486
487         socket.onopen = function(event) {
488             currentState = STATE_CONNECTED;
489         };
490
491         socket.onmessage = function(event) {
492
493             var message = event.data;
494
495             var instructions = message.split(";");
496             for (var i=0; i<instructions.length; i++) {
497
498                 var instruction = instructions[i];
499
500                 var opcodeEnd = instruction.indexOf(":");
501                 if (opcodeEnd == -1)
502                     opcodeEnd = instruction.length;
503
504                 var opcode = instruction.substring(0, opcodeEnd);
505                 var parameters = instruction.substring(opcodeEnd+1).split(",");
506
507                 if (tunnel.oninstruction)
508                     tunnel.oninstruction(opcode, parameters);
509
510             }
511
512         };
513
514     };
515
516     this.disconnect = function() {
517         currentState = STATE_DISCONNECTED;
518         socket.close();
519     };
520
521 };
522
523 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
524