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