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)
123 * Converts the given value to a length/string pair for use as an
124 * element in a Guacamole instruction.
126 * @param value The value to convert.
127 * @return {String} The converted value.
129 function getElement(value) {
130 var string = new String(value);
131 return string.length + "." + string;
134 // Initialized message with first element
135 var message = getElement(arguments[0]);
137 // Append remaining elements
138 for (var i=1; i<arguments.length; i++)
139 message += "," + getElement(arguments[i]);
144 // Add message to buffer
145 outputMessageBuffer += message;
147 // Send if not currently sending
148 if (!sendingMessages)
149 sendPendingMessages();
153 function sendPendingMessages() {
155 if (outputMessageBuffer.length > 0) {
157 sendingMessages = true;
159 var message_xmlhttprequest = new XMLHttpRequest();
160 message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid);
161 message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
163 // Once response received, send next queued event.
164 message_xmlhttprequest.onreadystatechange = function() {
165 if (message_xmlhttprequest.readyState == 4)
166 sendPendingMessages();
169 message_xmlhttprequest.send(outputMessageBuffer);
170 outputMessageBuffer = ""; // Clear buffer
174 sendingMessages = false;
179 function handleResponse(xmlhttprequest) {
182 var nextRequest = null;
184 var dataUpdateEvents = 0;
186 // The location of the last element's terminator
189 // Where to start the next length search or the next element
193 var elements = new Array();
195 function parseResponse() {
197 // Do not handle responses if not connected
198 if (currentState != STATE_CONNECTED) {
200 // Clean up interval if polling
201 if (interval != null)
202 clearInterval(interval);
207 // Start next request as soon as possible IF request was successful
208 if (xmlhttprequest.readyState >= 2 && nextRequest == null && xmlhttprequest.status == 200)
209 nextRequest = makeRequest();
211 // Parse stream when data is received and when complete.
212 if (xmlhttprequest.readyState == 3 ||
213 xmlhttprequest.readyState == 4) {
215 // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data)
216 if (pollingMode == POLLING_ENABLED) {
217 if (xmlhttprequest.readyState == 3 && interval == null)
218 interval = setInterval(parseResponse, 30);
219 else if (xmlhttprequest.readyState == 4 && interval != null)
220 clearInterval(interval);
223 // If canceled, stop transfer
224 if (xmlhttprequest.status == 0) {
229 // Halt on error during request
230 else if (xmlhttprequest.status != 200) {
232 // Get error message (if any)
233 var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
235 message = "Internal server error";
237 // Call error handler
238 if (tunnel.onerror) tunnel.onerror(message);
245 var current = xmlhttprequest.responseText;
247 // While search is within currently received data
248 while (elementEnd < current.length) {
250 // If we are waiting for element data
251 if (elementEnd >= startIndex) {
253 // We now have enough data for the element. Parse.
254 var element = current.substring(startIndex, elementEnd);
255 var terminator = current.substring(elementEnd, elementEnd+1);
257 // Add element to array
258 elements.push(element);
260 // If last element, handle instruction
261 if (terminator == ";") {
264 var opcode = elements.shift();
266 // Call instruction handler.
267 if (tunnel.oninstruction != null)
268 tunnel.oninstruction(opcode, elements);
275 // Start searching for length at character after
276 // element terminator
277 startIndex = elementEnd + 1;
281 // Search for end of length
282 var lengthEnd = current.indexOf(".", startIndex);
283 if (lengthEnd != -1) {
286 var length = parseInt(current.substring(elementEnd+1, lengthEnd));
288 // If we're done parsing, handle the next response.
291 // Clean up interval if polling
292 if (interval != null)
293 clearInterval(interval);
296 xmlhttprequest.onreadystatechange = null;
297 xmlhttprequest.abort();
299 // Start handling next request
301 handleResponse(nextRequest);
308 // Calculate start of element
309 startIndex = lengthEnd + 1;
311 // Calculate location of element terminator
312 elementEnd = startIndex + length;
316 // If no period yet, continue search when more data
319 startIndex = current.length;
329 // If response polling enabled, attempt to detect if still
330 // necessary (via wrapping parseResponse())
331 if (pollingMode == POLLING_ENABLED) {
332 xmlhttprequest.onreadystatechange = function() {
334 // If we receive two or more readyState==3 events,
335 // there is no need to poll.
336 if (xmlhttprequest.readyState == 3) {
338 if (dataUpdateEvents >= 2) {
339 pollingMode = POLLING_DISABLED;
340 xmlhttprequest.onreadystatechange = parseResponse;
348 // Otherwise, just parse
350 xmlhttprequest.onreadystatechange = parseResponse;
357 function makeRequest() {
360 var xmlhttprequest = new XMLHttpRequest();
361 xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid);
362 xmlhttprequest.send(null);
364 return xmlhttprequest;
368 this.connect = function(data) {
370 // Start tunnel and connect synchronously
371 var connect_xmlhttprequest = new XMLHttpRequest();
372 connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false);
373 connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
374 connect_xmlhttprequest.send(data);
376 // If failure, throw error
377 if (connect_xmlhttprequest.status != 200) {
379 var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message");
381 message = "Internal error";
383 throw new Error(message);
387 // Get UUID from response
388 tunnel_uuid = connect_xmlhttprequest.responseText;
390 // Start reading data
391 currentState = STATE_CONNECTED;
392 handleResponse(makeRequest());
396 this.disconnect = function() {
397 currentState = STATE_DISCONNECTED;
402 Guacamole.HTTPTunnel.prototype = new Guacamole.Tunnel();
406 * Guacamole Tunnel implemented over WebSocket via XMLHttpRequest.
409 * @augments Guacamole.Tunnel
410 * @param {String} tunnelURL The URL of the WebSocket tunneling service.
412 Guacamole.WebSocketTunnel = function(tunnelURL) {
415 * Reference to this WebSocket tunnel.
420 * The WebSocket used by this tunnel.
425 * The WebSocket protocol corresponding to the protocol used for the current
434 var STATE_CONNECTED = 1;
435 var STATE_DISCONNECTED = 2;
437 var currentState = STATE_IDLE;
439 // Transform current URL to WebSocket URL
441 // If not already a websocket URL
442 if ( tunnelURL.substring(0, 3) != "ws:"
443 && tunnelURL.substring(0, 4) != "wss:") {
445 var protocol = ws_protocol[window.location.protocol];
447 // If absolute URL, convert to absolute WS URL
448 if (tunnelURL.substring(0, 1) == "/")
451 + "//" + window.location.host
454 // Otherwise, construct absolute from relative URL
457 // Get path from pathname
458 var slash = window.location.pathname.lastIndexOf("/");
459 var path = window.location.pathname.substring(0, slash + 1);
461 // Construct absolute URL
464 + "//" + window.location.host
472 this.sendMessage = function(message) {
474 // Do not attempt to send messages if not connected
475 if (currentState != STATE_CONNECTED)
478 socket.send(message);
482 this.connect = function(data) {
485 socket = new WebSocket(tunnelURL + "?" + data, "guacamole");
487 socket.onopen = function(event) {
488 currentState = STATE_CONNECTED;
491 socket.onmessage = function(event) {
493 var message = event.data;
495 var instructions = message.split(";");
496 for (var i=0; i<instructions.length; i++) {
498 var instruction = instructions[i];
500 var opcodeEnd = instruction.indexOf(":");
502 opcodeEnd = instruction.length;
504 var opcode = instruction.substring(0, opcodeEnd);
505 var parameters = instruction.substring(opcodeEnd+1).split(",");
507 if (tunnel.oninstruction)
508 tunnel.oninstruction(opcode, parameters);
516 this.disconnect = function() {
517 currentState = STATE_DISCONNECTED;
523 Guacamole.WebSocketTunnel.prototype = new Guacamole.Tunnel();