/* * Guacamole - Clientless Remote Desktop * Copyright (C) 2010 Michael Jumper * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License */ // Guacamole namespace var Guacamole = Guacamole || {}; Guacamole.HTTPTunnel = function(tunnelURL) { var tunnel = this; var tunnel_uuid; var TUNNEL_CONNECT = tunnelURL + "?connect"; var TUNNEL_READ = tunnelURL + "?read:"; var TUNNEL_WRITE = tunnelURL + "?write:"; var STATE_IDLE = 0; var STATE_CONNECTED = 1; var STATE_DISCONNECTED = 2; var currentState = STATE_IDLE; var POLLING_ENABLED = 1; var POLLING_DISABLED = 0; // Default to polling - will be turned off automatically if not needed var pollingMode = POLLING_ENABLED; var sendingMessages = 0; var outputMessageBuffer = ""; // Handlers tunnel.onerror = null; tunnel.oninstruction = null; function sendMessage(message) { // Do not attempt to send messages if not connected if (currentState != STATE_CONNECTED) return; // Add event to queue, restart send loop if finished. outputMessageBuffer += message; if (sendingMessages == 0) sendPendingMessages(); } function sendPendingMessages() { if (outputMessageBuffer.length > 0) { sendingMessages = 1; var message_xmlhttprequest = new XMLHttpRequest(); message_xmlhttprequest.open("POST", TUNNEL_WRITE + tunnel_uuid); message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); // Once response received, send next queued event. message_xmlhttprequest.onreadystatechange = function() { if (message_xmlhttprequest.readyState == 4) sendPendingMessages(); } message_xmlhttprequest.send(outputMessageBuffer); outputMessageBuffer = ""; // Clear buffer } else sendingMessages = 0; } function handleResponse(xmlhttprequest) { var interval = null; var nextRequest = null; var dataUpdateEvents = 0; var instructionStart = 0; var startIndex = 0; function parseResponse() { // Do not handle responses if not connected if (currentState != STATE_CONNECTED) { // Clean up interval if polling if (interval != null) clearInterval(interval); return; } // Start next request as soon as possible if (xmlhttprequest.readyState >= 2 && nextRequest == null) nextRequest = makeRequest(); // Parse stream when data is received and when complete. if (xmlhttprequest.readyState == 3 || xmlhttprequest.readyState == 4) { // Also poll every 30ms (some browsers don't repeatedly call onreadystatechange for new data) if (pollingMode == POLLING_ENABLED) { if (xmlhttprequest.readyState == 3 && interval == null) interval = setInterval(parseResponse, 30); else if (xmlhttprequest.readyState == 4 && interval != null) clearInterval(interval); } // Halt on error during request if (xmlhttprequest.status == 0 || xmlhttprequest.status != 200) { // Get error message (if any) var message = xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message"); if (message) message = "Internal server error"; // Call error handler if (tunnel.onerror) tunnel.onerror(message); // Finish disconnect(); return; } var current = xmlhttprequest.responseText; var instructionEnd; while ((instructionEnd = current.indexOf(";", startIndex)) != -1) { // Start next search at next instruction startIndex = instructionEnd+1; var instruction = current.substr(instructionStart, instructionEnd - instructionStart); instructionStart = startIndex; var opcodeEnd = instruction.indexOf(":"); var opcode; var parameters; if (opcodeEnd == -1) { opcode = instruction; parameters = new Array(); } else { opcode = instruction.substr(0, opcodeEnd); parameters = instruction.substr(opcodeEnd+1).split(","); } // If we're done parsing, handle the next response. if (opcode.length == 0) { delete xmlhttprequest; if (nextRequest) handleResponse(nextRequest); break; } // Call instruction handler. if (tunnel.oninstruction != null) tunnel.oninstruction(opcode, parameters); } // Start search at end of string. startIndex = current.length; delete instruction; delete parameters; } } // If response polling enabled, attempt to detect if still // necessary (via wrapping parseResponse()) if (pollingMode == POLLING_ENABLED) { xmlhttprequest.onreadystatechange = function() { // If we receive two or more readyState==3 events, // there is no need to poll. if (xmlhttprequest.readyState == 3) { dataUpdateEvents++; if (dataUpdateEvents >= 2) { pollingMode = POLLING_DISABLED; xmlhttprequest.onreadystatechange = parseResponse; } } parseResponse(); } } // Otherwise, just parse else xmlhttprequest.onreadystatechange = parseResponse; parseResponse(); } function makeRequest() { // Download self var xmlhttprequest = new XMLHttpRequest(); xmlhttprequest.open("POST", TUNNEL_READ + tunnel_uuid); xmlhttprequest.send(null); return xmlhttprequest; } function connect(data) { // Start tunnel and connect synchronously var connect_xmlhttprequest = new XMLHttpRequest(); connect_xmlhttprequest.open("POST", TUNNEL_CONNECT, false); connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); connect_xmlhttprequest.send(data); // If failure, throw error if (connect_xmlhttprequest.status != 200) { var message = connect_xmlhttprequest.getResponseHeader("X-Guacamole-Error-Message"); if (!message) message = "Internal error"; throw new Error(message); } // Get UUID from response tunnel_uuid = connect_xmlhttprequest.responseText; // Start reading data currentState = STATE_CONNECTED; handleResponse(makeRequest()); } function disconnect() { currentState = STATE_DISCONNECTED; } // External API tunnel.connect = connect; tunnel.disconnect = disconnect; tunnel.sendMessage = sendMessage; }