ae7a85c69b8fd79653e4ef21ee9bc9d527db681b
[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         function getElement(value) {
123             var string = new String(value);
124             return string.length + "." + string; 
125         }
126
127         // Initialized message with first element
128         var message = getElement(arguments[0]);
129
130         // Append remaining elements
131         for (var i=1; i<arguments.length; i++)
132             message += "," + getElement(arguments[i]);
133
134         // Final terminator
135         message += ";";
136
137         // Add message to buffer, restart send loop if finished.
138         outputMessageBuffer += message;
139         if (!sendingMessages)
140             sendPendingMessages();
141
142     };
143
144     function sendPendingMessages() {
145
146         if (outputMessageBuffer.length > 0) {
147
148             sendingMessages = true;
149
150             var message_xmlhttprequest = new XMLHttpRequest();
151             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
152             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
153
154             // Once response received, send next queued event.
155             message_xmlhttprequest.onreadystatechange = function() {
156                 if (message_xmlhttprequest.readyState == 4)
157                     sendPendingMessages();
158             }
159
160             message_xmlhttprequest.send(outputMessageBuffer);
161             outputMessageBuffer = ""; // Clear buffer
162
163         }
164         else
165             sendingMessages = false;
166
167     }
168
169
170     function handleResponse(xmlhttprequest) {
171
172         var interval = null;
173         var nextRequest = null;
174
175         var dataUpdateEvents = 0;
176
177         // The location of the last element's terminator
178         var elementEnd = -1;
179
180         // Where to start the next length search or the next element
181         var startIndex = 0;
182
183         // Parsed elements
184         var elements = new Array();
185
186         function parseResponse() {
187
188             // Do not handle responses if not connected
189             if (currentState != STATE_CONNECTED) {
190                 
191                 // Clean up interval if polling
192                 if (interval != null)
193                     clearInterval(interval);
194                 
195                 return;
196             }
197
198             // Start next request as soon as possible IF request was successful
199             if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
200                 nextRequest = makeRequest();
201
202             // Parse stream when data is received and when complete.
203             if (xmlhttprequest.readyState == 3 ||
204                 xmlhttprequest.readyState == 4) {
205
206                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
207                 if (pollingMode == POLLING_ENABLED) {
208                     if (xmlhttprequest.readyState == 3 && interval == null)
209                         interval = setInterval(parseResponse, 30);
210                     else if (xmlhttprequest.readyState == 4 && interval != null)
211                         clearInterval(interval);
212                 }
213
214                 // If canceled, stop transfer
215                 if (xmlhttprequest.status == 0) {
216                     tunnel.disconnect();
217                     return;
218                 }
219
220                 // Halt on error during request
221                 else if (xmlhttprequest.status != 200) {
222
223                     // Get error message (if any)
224                     var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
225                     if (!message)
226                         message = "Internal server error";
227
228                     // Call error handler
229                     if (tunnel.onerror) tunnel.onerror(message);
230
231                     // Finish
232                     tunnel.disconnect();
233                     return;
234                 }
235
236                 var current = xmlhttprequest.responseText;
237
238                 // While search is within currently received data
239                 while (elementEnd < current.length) {
240
241                     // If we are waiting for element data
242                     if (elementEnd >= startIndex) {
243
244                         // We now have enough data for the element. Parse.
245                         var element = current.substring(startIndex, elementEnd);
246                         var terminator = current.substring(elementEnd, elementEnd+1);
247
248                         // Add element to array
249                         elements.push(element);
250
251                         // If last element, handle instruction
252                         if (terminator == ";") {
253
254                             // Get opcode
255                             var opcode = elements.shift();
256
257                             // Call instruction handler.
258                             if (tunnel.oninstruction != null)
259                                 tunnel.oninstruction(opcode, elements);
260
261                             // Clear elements
262                             elements.length = 0;
263
264                         }
265
266                         // Start searching for length at character after
267                         // element terminator
268                         startIndex = elementEnd + 1;
269
270                     }
271
272                     // Search for end of length
273                     var lengthEnd = current.indexOf(".", startIndex);
274                     if (lengthEnd != -1) {
275
276                         // Parse length
277                         var length = parseInt(current.substring(elementEnd+1, lengthEnd));
278
279                         // If we're done parsing, handle the next response.
280                         if (length == 0) {
281
282                             // Clean up interval if polling
283                             if (interval != null)
284                                 clearInterval(interval);
285                            
286                             // Clean up object
287                             xmlhttprequest.onreadystatechange = null;
288                             xmlhttprequest.abort();
289
290                             // Start handling next request
291                             if (nextRequest)
292                                 handleResponse(nextRequest);
293
294                             // Done parsing
295                             break;
296
297                         }
298
299                         // Calculate start of element
300                         startIndex = lengthEnd + 1;
301
302                         // Calculate location of element terminator
303                         elementEnd = startIndex + length;
304
305                     }
306                     
307                     // If no period yet, continue search when more data
308                     // is received
309                     else {
310                         startIndex = current.length;
311                         break;
312                     }
313
314                 } // end parse loop
315
316             }
317
318         }
319
320         // If response polling enabled, attempt to detect if still
321         // necessary (via wrapping parseResponse())
322         if (pollingMode == POLLING_ENABLED) {
323             xmlhttprequest.onreadystatechange = function() {
324
325                 // If we receive two or more readyState==3 events,
326                 // there is no need to poll.
327                 if (xmlhttprequest.readyState == 3) {
328                     dataUpdateEvents++;
329                     if (dataUpdateEvents >= 2) {
330                         pollingMode = POLLING_DISABLED;
331                         xmlhttprequest.onreadystatechange = parseResponse;
332                     }
333                 }
334
335                 parseResponse();
336             }
337         }
338
339         // Otherwise, just parse
340         else
341             xmlhttprequest.onreadystatechange = parseResponse;
342
343         parseResponse();
344
345     }
346
347
348     function makeRequest() {
349
350         // Download self
351         var xmlhttprequest = new XMLHttpRequest();
352         xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
353         xmlhttprequest.send(null);
354
355         return xmlhttprequest;
356
357     }
358
359     this.connect = function(data) {
360
361         // Start tunnel and connect synchronously
362         var connect_xmlhttprequest = new XMLHttpRequest();
363         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
364         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
365         connect_xmlhttprequest.send(data);
366
367         // If failure, throw error
368         if (connect_xmlhttprequest.status != 200) {
369
370             var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
371             if (!message)
372                 message = "Internal error";
373
374             throw new Error(message);
375
376         }
377
378         // Get UUID from response
379         tunnel_uuid = connect_xmlhttprequest.responseText;
380
381         // Start reading data
382         currentState = STATE_CONNECTED;
383         handleResponse(makeRequest());
384
385     };
386
387     this.disconnect = function() {
388         currentState = STATE_DISCONNECTED;
389     };
390
391 };
392
393 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
394
395
396 /**
397  * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
398  * 
399  * @constructor
400  * @augments Guacamole.Tunnel
401  * @param {String} tunnelURL The URL of the WebSocket tunneling service.
402  */
403 Guacamole.WebSocketTunnel = function(tunnelURL) {
404
405     /**
406      * Reference to this WebSocket tunnel.
407      */
408     var tunnel = this;
409
410     /**
411      * The WebSocket used by this tunnel.
412      */
413     var socket = null;
414
415     /**
416      * The WebSocket protocol corresponding to the protocol used for the current
417      * location.
418      */
419     var ws_protocol = {
420         "http:":  "ws:",
421         "https:": "wss:"
422     };
423
424     var STATE_IDLE          = 0;
425     var STATE_CONNECTED     = 1;
426     var STATE_DISCONNECTED  = 2;
427
428     var currentState = STATE_IDLE;
429     
430     // Transform current URL to WebSocket URL
431
432     // If not already a websocket URL
433     if (   tunnelURL.substring(0, 3) != "ws:"
434         && tunnelURL.substring(0, 4) != "wss:") {
435
436         var protocol = ws_protocol[window.location.protocol];
437
438         // If absolute URL, convert to absolute WS URL
439         if (tunnelURL.substring(0, 1) == "/")
440             tunnelURL =
441                 protocol
442                 + "//" + window.location.host
443                 + tunnelURL;
444
445         // Otherwise, construct absolute from relative URL
446         else {
447
448             // Get path from pathname
449             var slash = window.location.pathname.lastIndexOf("/");
450             var path  = window.location.pathname.substring(0, slash + 1);
451
452             // Construct absolute URL
453             tunnelURL =
454                 protocol
455                 + "//" + window.location.host
456                 + path
457                 + tunnelURL;
458
459         }
460
461     }
462
463     this.sendMessage = function(message) {
464
465         // Do not attempt to send messages if not connected
466         if (currentState != STATE_CONNECTED)
467             return;
468
469         socket.send(message);
470
471     };
472
473     this.connect = function(data) {
474
475         // Connect socket
476         socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
477
478         socket.onopen = function(event) {
479             currentState = STATE_CONNECTED;
480         };
481
482         socket.onmessage = function(event) {
483
484             var message = event.data;
485
486             var instructions = message.split(";");
487             for (var i=0; i<instructions.length; i++) {
488
489                 var instruction = instructions[i];
490
491                 var opcodeEnd = instruction.indexOf(":");
492                 if (opcodeEnd == -1)
493                     opcodeEnd = instruction.length;
494
495                 var opcode = instruction.substring(0, opcodeEnd);
496                 var parameters = instruction.substring(opcodeEnd+1).split(",");
497
498                 if (tunnel.oninstruction)
499                     tunnel.oninstruction(opcode, parameters);
500
501             }
502
503         };
504
505     };
506
507     this.disconnect = function() {
508         currentState = STATE_DISCONNECTED;
509         socket.close();
510     };
511
512 };
513
514 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();
515