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() {
33 this.connect = function(data) {};
35 this.disconnect = function() {};
37 this.sendMessage = function(message) {};
40 this.oninstruction = null;
45 * Guacamole Tunnel implemented over HTTP via XMLHttpRequest.
48 * @augments Guacamole.Tunnel
49 * @param {String} tunnelURL The URL of the HTTP tunneling service.
51 Guacamole.HTTPTunnel = function(tunnelURL) {
57 var TUNNEL_CONNECT = tunnelURL + "?connect";
58 var TUNNEL_READ = tunnelURL + "?read:";
59 var TUNNEL_WRITE = tunnelURL + "?write:";
62 var STATE_CONNECTED = 1;
63 var STATE_DISCONNECTED = 2;
65 var currentState = STATE_IDLE;
67 var POLLING_ENABLED = 1;
68 var POLLING_DISABLED = 0;
70 // Default to polling - will be turned off automatically if not needed
71 var pollingMode = POLLING_ENABLED;
73 var sendingMessages = 0;
74 var outputMessageBuffer = "";
76 function sendMessage(message) {
78 // Do not attempt to send messages if not connected
79 if (currentState != STATE_CONNECTED)
82 // Add event to queue, restart send loop if finished.
83 outputMessageBuffer += message;
84 if (sendingMessages == 0)
85 sendPendingMessages();
89 function sendPendingMessages() {
91 if (outputMessageBuffer.length > 0) {
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");
99 // Once response received, send next queued event.
100 message_xmlhttprequest.onreadystatechange = function() {
101 if (message_xmlhttprequest.readyState == 4)
102 sendPendingMessages();
105 message_xmlhttprequest.send(outputMessageBuffer);
106 outputMessageBuffer = ""; // Clear buffer
115 function handleResponse(xmlhttprequest) {
118 var nextRequest = null;
120 var dataUpdateEvents = 0;
121 var instructionStart = 0;
124 function parseResponse() {
126 // Do not handle responses if not connected
127 if (currentState != STATE_CONNECTED) {
129 // Clean up interval if polling
130 if (interval != null)
131 clearInterval(interval);
136 // Start next request as soon as possible
137 if (xmlhttprequest.readyState >= 2 && nextRequest == null)
138 nextRequest = makeRequest();
140 // Parse stream when data is received and when complete.
141 if (xmlhttprequest.readyState == 3 ||
142 xmlhttprequest.readyState == 4) {
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);
152 // Halt on error during request
153 if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) {
155 // Get error message (if any)
156 var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
158 message = "Internal server error";
160 // Call error handler
161 if (tunnel.onerror) tunnel.onerror(message);
168 var current = xmlhttprequest.responseText;
171 while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
173 // Start next search at next instruction
174 startIndex = instructionEnd+1;
176 var instruction = current.substr(instructionStart,
177 instructionEnd - instructionStart);
179 instructionStart = startIndex;
181 var opcodeEnd = instruction.indexOf(":");
185 if (opcodeEnd == -1) {
186 opcode = instruction;
187 parameters = new Array();
190 opcode = instruction.substr(0, opcodeEnd);
191 parameters = instruction.substr(opcodeEnd+1).split(",");
194 // If we're done parsing, handle the next response.
195 if (opcode.length == 0) {
197 delete xmlhttprequest;
199 handleResponse(nextRequest);
204 // Call instruction handler.
205 if (tunnel.oninstruction != null)
206 tunnel.oninstruction(opcode, parameters);
209 // Start search at end of string.
210 startIndex = current.length;
219 // If response polling enabled, attempt to detect if still
220 // necessary (via wrapping parseResponse())
221 if (pollingMode == POLLING_ENABLED) {
222 xmlhttprequest.onreadystatechange = function() {
224 // If we receive two or more readyState==3 events,
225 // there is no need to poll.
226 if (xmlhttprequest.readyState == 3) {
228 if (dataUpdateEvents >= 2) {
229 pollingMode = POLLING_DISABLED;
230 xmlhttprequest.onreadystatechange = parseResponse;
238 // Otherwise, just parse
240 xmlhttprequest.onreadystatechange = parseResponse;
247 function makeRequest() {
250 var xmlhttprequest = new XMLHttpRequest();
251 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
252 xmlhttprequest.send(null);
254 return xmlhttprequest;
258 function connect(data) {
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);
266 // If failure, throw error
267 if (connect_xmlhttprequest.status != 200) {
269 var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
271 message = "Internal error";
273 throw new Error(message);
277 // Get UUID from response
278 tunnel_uuid = connect_xmlhttprequest.responseText;
280 // Start reading data
281 currentState = STATE_CONNECTED;
282 handleResponse(makeRequest());
286 function disconnect() {
287 currentState = STATE_DISCONNECTED;
291 tunnel.connect = connect;
292 tunnel.disconnect = disconnect;
293 tunnel.sendMessage = sendMessage;
297 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();