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 {...} elements The elements of the message to send to the
52 * service on the other side of the tunnel.
54 this.sendMessage = function(elements) {};
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() {
114 // Do not attempt to send messages if not connected
115 if (currentState != STATE_CONNECTED)
118 // Do not attempt to send empty messages
119 if (arguments.length == 0)
122 function getElement(value) {
123 var string = new String(value);
124 return string.length + "." + string;
127 // Initialized message with first element
128 var message = getElement(arguments[0]);
130 // Append remaining elements
131 for (var i=1; i<arguments.length; i++)
132 message += "," + getElement(arguments[i]);
137 // Add message to buffer, restart send loop if finished.
138 outputMessageBuffer += message;
139 if (!sendingMessages)
140 sendPendingMessages();
144 function sendPendingMessages() {
146 if (outputMessageBuffer.length > 0) {
148 sendingMessages = true;
150 var message_xmlhttprequest = new XMLHttpRequest();
151 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
152 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
154 // Once response received, send next queued event.
155 message_xmlhttprequest.onreadystatechange = function() {
156 if (message_xmlhttprequest.readyState == 4)
157 sendPendingMessages();
160 message_xmlhttprequest.send(outputMessageBuffer);
161 outputMessageBuffer = ""; // Clear buffer
165 sendingMessages = false;
170 function handleResponse(xmlhttprequest) {
173 var nextRequest = null;
175 var dataUpdateEvents = 0;
177 // The location of the last element's terminator
180 // Where to start the next length search or the next element
184 var elements = new Array();
186 function parseResponse() {
188 // Do not handle responses if not connected
189 if (currentState != STATE_CONNECTED) {
191 // Clean up interval if polling
192 if (interval != null)
193 clearInterval(interval);
198 // Start next request as soon as possible IF request was successful
199 if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
200 nextRequest = makeRequest();
202 // Parse stream when data is received and when complete.
203 if (xmlhttprequest.readyState == 3 ||
204 xmlhttprequest.readyState == 4) {
206 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
207 if (pollingMode == POLLING_ENABLED) {
208 if (xmlhttprequest.readyState == 3 && interval == null)
209 interval = setInterval(parseResponse, 30);
210 else if (xmlhttprequest.readyState == 4 && interval != null)
211 clearInterval(interval);
214 // If canceled, stop transfer
215 if (xmlhttprequest.status == 0) {
220 // Halt on error during request
221 else if (xmlhttprequest.status != 200) {
223 // Get error message (if any)
224 var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
226 message = "Internal server error";
228 // Call error handler
229 if (tunnel.onerror) tunnel.onerror(message);
236 var current = xmlhttprequest.responseText;
238 // While search is within currently received data
239 while (elementEnd < current.length) {
241 // If we are waiting for element data
242 if (elementEnd >= startIndex) {
244 // We now have enough data for the element. Parse.
245 var element = current.substring(startIndex, elementEnd);
246 var terminator = current.substring(elementEnd, elementEnd+1);
248 // Add element to array
249 elements.push(element);
251 // If last element, handle instruction
252 if (terminator == ";") {
255 var opcode = elements.shift();
257 // Call instruction handler.
258 if (tunnel.oninstruction != null)
259 tunnel.oninstruction(opcode, elements);
266 // Start searching for length at character after
267 // element terminator
268 startIndex = elementEnd + 1;
272 // Search for end of length
273 var lengthEnd = current.indexOf(".", startIndex);
274 if (lengthEnd != -1) {
277 var length = parseInt(current.substring(elementEnd+1, lengthEnd));
279 // If we're done parsing, handle the next response.
282 // Clean up interval if polling
283 if (interval != null)
284 clearInterval(interval);
287 xmlhttprequest.onreadystatechange = null;
288 xmlhttprequest.abort();
290 // Start handling next request
292 handleResponse(nextRequest);
299 // Calculate start of element
300 startIndex = lengthEnd + 1;
302 // Calculate location of element terminator
303 elementEnd = startIndex + length;
307 // If no period yet, continue search when more data
310 startIndex = current.length;
320 // If response polling enabled, attempt to detect if still
321 // necessary (via wrapping parseResponse())
322 if (pollingMode == POLLING_ENABLED) {
323 xmlhttprequest.onreadystatechange = function() {
325 // If we receive two or more readyState==3 events,
326 // there is no need to poll.
327 if (xmlhttprequest.readyState == 3) {
329 if (dataUpdateEvents >= 2) {
330 pollingMode = POLLING_DISABLED;
331 xmlhttprequest.onreadystatechange = parseResponse;
339 // Otherwise, just parse
341 xmlhttprequest.onreadystatechange = parseResponse;
348 function makeRequest() {
351 var xmlhttprequest = new XMLHttpRequest();
352 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
353 xmlhttprequest.send(null);
355 return xmlhttprequest;
359 this.connect = function(data) {
361 // Start tunnel and connect synchronously
362 var connect_xmlhttprequest = new XMLHttpRequest();
363 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
364 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
365 connect_xmlhttprequest.send(data);
367 // If failure, throw error
368 if (connect_xmlhttprequest.status != 200) {
370 var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
372 message = "Internal error";
374 throw new Error(message);
378 // Get UUID from response
379 tunnel_uuid = connect_xmlhttprequest.responseText;
381 // Start reading data
382 currentState = STATE_CONNECTED;
383 handleResponse(makeRequest());
387 this.disconnect = function() {
388 currentState = STATE_DISCONNECTED;
393 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
397 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
400 * @augments Guacamole.Tunnel
401 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
403 Guacamole.WebSocketTunnel = function(tunnelURL) {
406 * Reference to this WebSocket tunnel.
411 * The WebSocket used by this tunnel.
416 * The WebSocket protocol corresponding to the protocol used for the current
425 var STATE_CONNECTED = 1;
426 var STATE_DISCONNECTED = 2;
428 var currentState = STATE_IDLE;
430 // Transform current URL to WebSocket URL
432 // If not already a websocket URL
433 if ( tunnelURL.substring(0, 3) != "ws:"
434 && tunnelURL.substring(0, 4) != "wss:") {
436 var protocol = ws_protocol[window.location.protocol];
438 // If absolute URL, convert to absolute WS URL
439 if (tunnelURL.substring(0, 1) == "/")
442 + "//" + window.location.host
445 // Otherwise, construct absolute from relative URL
448 // Get path from pathname
449 var slash = window.location.pathname.lastIndexOf("/");
450 var path = window.location.pathname.substring(0, slash + 1);
452 // Construct absolute URL
455 + "//" + window.location.host
463 this.sendMessage = function(message) {
465 // Do not attempt to send messages if not connected
466 if (currentState != STATE_CONNECTED)
469 socket.send(message);
473 this.connect = function(data) {
476 socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
478 socket.onopen = function(event) {
479 currentState = STATE_CONNECTED;
482 socket.onmessage = function(event) {
484 var message = event.data;
486 var instructions = message.split(";");
487 for (var i=0; i<instructions.length; i++) {
489 var instruction = instructions[i];
491 var opcodeEnd = instruction.indexOf(":");
493 opcodeEnd = instruction.length;
495 var opcode = instruction.substring(0, opcodeEnd);
496 var parameters = instruction.substring(opcodeEnd+1).split(",");
498 if (tunnel.oninstruction)
499 tunnel.oninstruction(opcode, parameters);
507 this.disconnect = function() {
508 currentState = STATE_DISCONNECTED;
514 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();