Added missing semicolons, improved state handling
[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 function GuacamoleHTTPTunnel(tunnelURL) {
20
21     var TUNNEL_CONNECT = tunnelURL + "?connect";
22     var TUNNEL_READ    = tunnelURL + "?read";
23     var TUNNEL_WRITE   = tunnelURL + "?write";
24
25     var STATE_IDLE          = 0;
26     var STATE_CONNECTED     = 1;
27     var STATE_DISCONNECTED  = 2;
28
29     var currentState = STATE_IDLE;
30
31     var POLLING_ENABLED     = 1;
32     var POLLING_DISABLED    = 0;
33
34     // Default to polling - will be turned off automatically if not needed
35     var pollingMode = POLLING_ENABLED;
36
37     var instructionHandler = null;
38
39     var sendingMessages = 0;
40     var outputMessageBuffer = "";
41
42     function sendMessage(message) {
43
44         // Do not attempt to send messages if not connected
45         if (currentState != STATE_CONNECTED)
46             return;
47
48         // Add event to queue, restart send loop if finished.
49         outputMessageBuffer += message;
50         if (sendingMessages == 0)
51             sendPendingMessages();
52
53     }
54
55     function sendPendingMessages() {
56
57         if (outputMessageBuffer.length > 0) {
58
59             sendingMessages = 1;
60
61             var message_xmlhttprequest = new XMLHttpRequest();
62             message_xmlhttprequest.open("POST", TUNNEL_WRITE);
63             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
64             message_xmlhttprequest.setRequestHeader("Content-length", outputMessageBuffer.length);
65
66             // Once response received, send next queued event.
67             message_xmlhttprequest.onreadystatechange = function() {
68                 if (message_xmlhttprequest.readyState == 4)
69                     sendPendingMessages();
70             }
71
72             message_xmlhttprequest.send(outputMessageBuffer);
73             outputMessageBuffer = ""; // Clear buffer
74
75         }
76         else
77             sendingMessages = 0;
78
79     }
80
81
82     function handleResponse(xmlhttprequest) {
83
84         var interval = null;
85         var nextRequest = null;
86
87         var dataUpdateEvents = 0;
88         var instructionStart = 0;
89         var startIndex = 0;
90
91         function parseResponse() {
92
93             // Do not handle responses if not connected
94             if (currentState != STATE_CONNECTED) {
95                 
96                 // Clean up interval if polling
97                 if (interval != null)
98                     clearInterval(interval);
99                 
100                 return;
101             }
102
103             // Start next request as soon as possible
104             if (xmlhttprequest.readyState >= 2 && nextRequest == null)
105                 nextRequest = makeRequest();
106
107             // Parse stream when data is received and when complete.
108             if (xmlhttprequest.readyState == 3 ||
109                 xmlhttprequest.readyState == 4) {
110
111                 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
112                 if (pollingMode == POLLING_ENABLED) {
113                     if (xmlhttprequest.readyState == 3 && interval == null)
114                         interval = setInterval(parseResponse, 30);
115                     else if (xmlhttprequest.readyState == 4 && interval != null)
116                         clearInterval(interval);
117                 }
118
119                 // Halt on error during request
120                 if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) {
121                     disconnect();
122                     return;
123                 }
124
125                 var current = xmlhttprequest.responseText;
126                 var instructionEnd;
127
128                 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
129
130                     // Start next search at next instruction
131                     startIndex = instructionEnd+1;
132
133                     var instruction = current.substr(instructionStart,
134                             instructionEnd - instructionStart);
135
136                     instructionStart = startIndex;
137
138                     var opcodeEnd = instruction.indexOf(":");
139
140                     var opcode;
141                     var parameters;
142                     if (opcodeEnd == -1) {
143                         opcode = instruction;
144                         parameters = new Array();
145                     }
146                     else {
147                         opcode = instruction.substr(0, opcodeEnd);
148                         parameters = instruction.substr(opcodeEnd+1).split(",");
149                     }
150
151                     // If we're done parsing, handle the next response.
152                     if (opcode.length == 0) {
153
154                         delete xmlhttprequest;
155                         if (nextRequest)
156                             handleResponse(nextRequest);
157
158                         break;
159                     }
160
161                     // Call instruction handler.
162                     if (instructionHandler != null)
163                         instructionHandler(opcode, parameters);
164                 }
165
166                 // Start search at end of string.
167                 startIndex = current.length;
168
169                 delete instruction;
170                 delete parameters;
171
172             }
173
174         }
175
176         // If response polling enabled, attempt to detect if still
177         // necessary (via wrapping parseResponse())
178         if (pollingMode == POLLING_ENABLED) {
179             xmlhttprequest.onreadystatechange = function() {
180
181                 // If we receive two or more readyState==3 events,
182                 // there is no need to poll.
183                 if (xmlhttprequest.readyState == 3) {
184                     dataUpdateEvents++;
185                     if (dataUpdateEvents >= 2) {
186                         pollingMode = POLLING_DISABLED;
187                         xmlhttprequest.onreadystatechange = parseResponse;
188                     }
189                 }
190
191                 parseResponse();
192             }
193         }
194
195         // Otherwise, just parse
196         else
197             xmlhttprequest.onreadystatechange = parseResponse;
198
199         parseResponse();
200
201     }
202
203
204     function makeRequest() {
205
206         // Download self
207         var xmlhttprequest = new XMLHttpRequest();
208         xmlhttprequest.open("POST", TUNNEL_READ);
209         xmlhttprequest.send(null);
210
211         return xmlhttprequest;
212
213     }
214
215     function connect() {
216
217         // Start tunnel and connect synchronously
218         var connect_xmlhttprequest = new XMLHttpRequest();
219         connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
220         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
221         connect_xmlhttprequest.setRequestHeader("Content-length", 0);
222         connect_xmlhttprequest.send(null);
223
224         // Start reading data
225         currentState = STATE_CONNECTED;
226         handleResponse(makeRequest());
227
228     }
229
230     function disconnect() {
231         currentState = STATE_DISCONNECTED;
232     }
233
234     // External API
235     this.connect = connect;
236     this.disconnect = disconnect;
237     this.sendMessage = sendMessage;
238     this.setInstructionHandler = function(handler) {
239         instructionHandler = handler;
240     };
241
242 }