d28758909a49eaee1d90c1b057dee9f8ec4560dd
[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     this.connect = function(data) {};
34     
35     this.disconnect = function() {};
36     
37     this.sendMessage = function(message) {};
38     
39     this.onerror = null;
40     this.oninstruction = null;
41
42 };
43
44 /**
45  * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
46  * 
47  * @constructor
48  * @augments Guacamole.Tunnel
49  * @param {String} tunnelURL The URL of the HTTP tunneling service.
50  */
51 Guacamole.HTTPTunnel = function(tunnelURL) {
52
53     var tunnel = this;
54
55     var tunnel_uuid;
56
57     var TUNNEL_CONNECT = tunnelURL + "?connect";
58     var TUNNEL_READ    = tunnelURL + "?read:";
59     var TUNNEL_WRITE   = tunnelURL + "?write:";
60
61     var STATE_IDLE          = 0;
62     var STATE_CONNECTED     = 1;
63     var STATE_DISCONNECTED  = 2;
64
65     var currentState = STATE_IDLE;
66
67     var POLLING_ENABLED     = 1;
68     var POLLING_DISABLED    = 0;
69
70     // Default to polling - will be turned off automatically if not needed
71     var pollingMode = POLLING_ENABLED;
72
73     var sendingMessages = 0;
74     var outputMessageBuffer = "";
75
76     function sendMessage(message) {
77
78         // Do not attempt to send messages if not connected
79         if (currentState != STATE_CONNECTED)
80             return;
81
82         // Add event to queue, restart send loop if finished.
83         outputMessageBuffer += message;
84         if (sendingMessages == 0)
85             sendPendingMessages();
86
87     }
88
89     function sendPendingMessages() {
90
91         if (outputMessageBuffer.length > 0) {
92
93             sendingMessages = 1;
94
95             var message_xmlhttprequest = new XMLHttpRequest();
96             message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
97             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
98
99             // Once response received, send next queued event.
100             message_xmlhttprequest.onreadystatechange = function() {
101                 if (message_xmlhttprequest.readyState == 4)
102                     sendPendingMessages();
103             }
104
105             message_xmlhttprequest.send(outputMessageBuffer);
106             outputMessageBuffer = ""; // Clear buffer
107
108         }
109         else
110             sendingMessages = 0;
111
112     }
113
114
115     function handleResponse(xmlhttprequest) {
116
117         var interval = null;
118         var nextRequest = null;
119
120         var dataUpdateEvents = 0;
121         var instructionStart = 0;
122         var startIndex = 0;
123
124         function parseResponse() {
125
126             // Do not handle responses if not connected
127             if (currentState != STATE_CONNECTED) {
128                 
129                 // Clean up interval if polling
130                 if (interval != null)
131                     clearInterval(interval);
132                 
133                 return;
134             }
135
136             // Start next request as soon as possible
137             if (xmlhttprequest.readyState >= 2 && nextRequest == null)
138                 nextRequest = makeRequest();
139
140             // Parse stream when data is received and when complete.
141             if (xmlhttprequest.readyState == 3 ||
142                 xmlhttprequest.readyState == 4) {
143
144                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
145                 if (pollingMode == POLLING_ENABLED) {
146                     if (xmlhttprequest.readyState == 3 && interval == null)
147                         interval = setInterval(parseResponse, 30);
148                     else if (xmlhttprequest.readyState == 4 && interval != null)
149                         clearInterval(interval);
150                 }
151
152                 // Halt on error during request
153                 if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) {
154
155                     // Get error message (if any)
156                     var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
157                     if (message)
158                         message = "Internal server error";
159
160                     // Call error handler
161                     if (tunnel.onerror) tunnel.onerror(message);
162
163                     // Finish
164                     disconnect();
165                     return;
166                 }
167
168                 var current = xmlhttprequest.responseText;
169                 var instructionEnd;
170
171                 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
172
173                     // Start next search at next instruction
174                     startIndex = instructionEnd+1;
175
176                     var instruction = current.substr(instructionStart,
177                             instructionEnd - instructionStart);
178
179                     instructionStart = startIndex;
180
181                     var opcodeEnd = instruction.indexOf(":");
182
183                     var opcode;
184                     var parameters;
185                     if (opcodeEnd == -1) {
186                         opcode = instruction;
187                         parameters = new Array();
188                     }
189                     else {
190                         opcode = instruction.substr(0, opcodeEnd);
191                         parameters = instruction.substr(opcodeEnd+1).split(",");
192                     }
193
194                     // If we're done parsing, handle the next response.
195                     if (opcode.length == 0) {
196
197                         delete xmlhttprequest;
198                         if (nextRequest)
199                             handleResponse(nextRequest);
200
201                         break;
202                     }
203
204                     // Call instruction handler.
205                     if (tunnel.oninstruction != null)
206                         tunnel.oninstruction(opcode, parameters);
207                 }
208
209                 // Start search at end of string.
210                 startIndex = current.length;
211
212                 delete instruction;
213                 delete parameters;
214
215             }
216
217         }
218
219         // If response polling enabled, attempt to detect if still
220         // necessary (via wrapping parseResponse())
221         if (pollingMode == POLLING_ENABLED) {
222             xmlhttprequest.onreadystatechange = function() {
223
224                 // If we receive two or more readyState==3 events,
225                 // there is no need to poll.
226                 if (xmlhttprequest.readyState == 3) {
227                     dataUpdateEvents++;
228                     if (dataUpdateEvents >= 2) {
229                         pollingMode = POLLING_DISABLED;
230                         xmlhttprequest.onreadystatechange = parseResponse;
231                     }
232                 }
233
234                 parseResponse();
235             }
236         }
237
238         // Otherwise, just parse
239         else
240             xmlhttprequest.onreadystatechange = parseResponse;
241
242         parseResponse();
243
244     }
245
246
247     function makeRequest() {
248
249         // Download self
250         var xmlhttprequest = new XMLHttpRequest();
251         xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
252         xmlhttprequest.send(null);
253
254         return xmlhttprequest;
255
256     }
257
258     function connect(data) {
259
260         // Start tunnel and connect synchronously
261         var connect_xmlhttprequest = new XMLHttpRequest();
262         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
263         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
264         connect_xmlhttprequest.send(data);
265
266         // If failure, throw error
267         if (connect_xmlhttprequest.status != 200) {
268
269             var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
270             if (!message)
271                 message = "Internal error";
272
273             throw new Error(message);
274
275         }
276
277         // Get UUID from response
278         tunnel_uuid = connect_xmlhttprequest.responseText;
279
280         // Start reading data
281         currentState = STATE_CONNECTED;
282         handleResponse(makeRequest());
283
284     }
285
286     function disconnect() {
287         currentState = STATE_DISCONNECTED;
288     }
289
290     // External API
291     tunnel.connect = connect;
292     tunnel.disconnect = disconnect;
293     tunnel.sendMessage = sendMessage;
294
295 };
296
297 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();