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