3 * Guacamole - Clientless Remote Desktop
4 * Copyright (C) 2010 Michael Jumper
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.
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.
16 * You should have received a copy of the GNU Affero General Public License
19 // Guacamole namespace
20 var Guacamole = Guacamole || {};
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
29 * @see Guacamole.HTTPTunnel
31 Guacamole.Tunnel = function() {
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.
38 * @param {String} data The data to send to the tunnel when connecting.
40 this.connect = function(data) {};
43 * Disconnect from the tunnel.
45 this.disconnect = function() {};
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.
51 * @param {String} message The message to send to the service on the other
54 this.sendMessage = function(message) {};
57 * Fired whenever an error is encountered by the tunnel.
60 * @param {String} message A human-readable description of the error that
66 * Fired once for every complete Guacamole instruction received, in order.
69 * @param {String} opcode The Guacamole instruction opcode.
70 * @param {Array} parameters The parameters provided for the instruction,
73 this.oninstruction = null;
78 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
81 * @augments Guacamole.Tunnel
82 * @param {String} tunnelURL The URL of the HTTP tunneling service.
84 Guacamole.HTTPTunnel = function(tunnelURL) {
87 * Reference to this HTTP tunnel.
93 var TUNNEL_CONNECT = tunnelURL + "?connect";
94 var TUNNEL_READ = tunnelURL + "?read:";
95 var TUNNEL_WRITE = tunnelURL + "?write:";
98 var STATE_CONNECTED = 1;
99 var STATE_DISCONNECTED = 2;
101 var currentState = STATE_IDLE;
103 var POLLING_ENABLED = 1;
104 var POLLING_DISABLED = 0;
106 // Default to polling - will be turned off automatically if not needed
107 var pollingMode = POLLING_ENABLED;
109 var sendingMessages = false;
110 var outputMessageBuffer = "";
112 this.sendMessage = function(message) {
114 // Do not attempt to send messages if not connected
115 if (currentState != STATE_CONNECTED)
118 // Add event to queue, restart send loop if finished.
119 outputMessageBuffer += message;
120 if (!sendingMessages)
121 sendPendingMessages();
125 function sendPendingMessages() {
127 if (outputMessageBuffer.length > 0) {
129 sendingMessages = true;
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");
135 // Once response received, send next queued event.
136 message_xmlhttprequest.onreadystatechange = function() {
137 if (message_xmlhttprequest.readyState == 4)
138 sendPendingMessages();
141 message_xmlhttprequest.send(outputMessageBuffer);
142 outputMessageBuffer = ""; // Clear buffer
146 sendingMessages = false;
151 function handleResponse(xmlhttprequest) {
154 var nextRequest = null;
156 var dataUpdateEvents = 0;
157 var instructionStart = 0;
160 function parseResponse() {
162 // Do not handle responses if not connected
163 if (currentState != STATE_CONNECTED) {
165 // Clean up interval if polling
166 if (interval != null)
167 clearInterval(interval);
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();
176 // Parse stream when data is received and when complete.
177 if (xmlhttprequest.readyState == 3 ||
178 xmlhttprequest.readyState == 4) {
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);
188 // If canceled, stop transfer
189 if (xmlhttprequest.status == 0) {
194 // Halt on error during request
195 else if (xmlhttprequest.status != 200) {
197 // Get error message (if any)
198 var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
200 message = "Internal server error";
202 // Call error handler
203 if (tunnel.onerror) tunnel.onerror(message);
210 var current = xmlhttprequest.responseText;
213 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
215 // Start next search at next instruction
216 startIndex = instructionEnd+1;
218 var instruction = current.substr(instructionStart,
219 instructionEnd - instructionStart);
221 instructionStart = startIndex;
223 var opcodeEnd = instruction.indexOf(":");
227 if (opcodeEnd == -1) {
228 opcode = instruction;
229 parameters = new Array();
232 opcode = instruction.substr(0, opcodeEnd);
233 parameters = instruction.substr(opcodeEnd+1).split(",");
236 // If we're done parsing, handle the next response.
237 if (opcode.length == 0) {
239 delete xmlhttprequest;
241 handleResponse(nextRequest);
246 // Call instruction handler.
247 if (tunnel.oninstruction != null)
248 tunnel.oninstruction(opcode, parameters);
251 // Start search at end of string.
252 startIndex = current.length;
261 // If response polling enabled, attempt to detect if still
262 // necessary (via wrapping parseResponse())
263 if (pollingMode == POLLING_ENABLED) {
264 xmlhttprequest.onreadystatechange = function() {
266 // If we receive two or more readyState==3 events,
267 // there is no need to poll.
268 if (xmlhttprequest.readyState == 3) {
270 if (dataUpdateEvents >= 2) {
271 pollingMode = POLLING_DISABLED;
272 xmlhttprequest.onreadystatechange = parseResponse;
280 // Otherwise, just parse
282 xmlhttprequest.onreadystatechange = parseResponse;
289 function makeRequest() {
292 var xmlhttprequest = new XMLHttpRequest();
293 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
294 xmlhttprequest.send(null);
296 return xmlhttprequest;
300 this.connect = function(data) {
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);
308 // If failure, throw error
309 if (connect_xmlhttprequest.status != 200) {
311 var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
313 message = "Internal error";
315 throw new Error(message);
319 // Get UUID from response
320 tunnel_uuid = connect_xmlhttprequest.responseText;
322 // Start reading data
323 currentState = STATE_CONNECTED;
324 handleResponse(makeRequest());
328 this.disconnect = function() {
329 currentState = STATE_DISCONNECTED;
334 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();