/* * Guacamole - Pure JavaScript/HTML VNC Client * 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 * along with this program. If not, see . */ function VNCClient(display) { var STATE_IDLE = 0; var STATE_CONNECTING = 1; var STATE_WAITING = 2; var STATE_CONNECTED = 3; var STATE_DISCONNECTING = 4; var STATE_DISCONNECTED = 5; var currentState = STATE_IDLE; var stateChangeHandler = null; function setState(state) { if (state != currentState) { currentState = state; if (stateChangeHandler) stateChangeHandler(currentState); } } this.setOnStateChangeHandler = function(handler) { stateChangeHandler = handler; } function isConnected() { return currentState == STATE_CONNECTED || currentState == STATE_WAITING; } var keyIndex = 0; var xmlIndex = 0; // Layers var background = null; var cursor = null; var cursorImage = null; var cursorHotspotX = 0; var cursorHotspotY = 0; // FIXME: Make object. Clean up. var cursorRectX = 0; var cursorRectY = 0; var cursorRectW = 0; var cursorRectH = 0; var cursorHidden = 0; function redrawCursor() { // Hide hardware cursor if (cursorHidden == 0) { display.className += " hideCursor"; cursorHidden = 1; } // Erase old cursor cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH); // Update rect cursorRectX = mouse.getX() - cursorHotspotX; cursorRectY = mouse.getY() - cursorHotspotY; cursorRectW = cursorImage.width; cursorRectH = cursorImage.height; // Draw new cursor cursor.drawImage(cursorRectX, cursorRectY, cursorImage); } /*****************************************/ /*** Keyboard ***/ /*****************************************/ var keyboard = new GuacamoleKeyboard(document); this.disableKeyboard = function() { keyboard.setKeyPressedHandler(null); keyboard.setKeyReleasedHandler(null); }; this.enableKeyboard = function() { keyboard.setKeyPressedHandler( function (keysym) { sendKeyEvent(1, keysym); } ); keyboard.setKeyReleasedHandler( function (keysym) { sendKeyEvent(0, keysym); } ); }; // Enable keyboard by default this.enableKeyboard(); function sendKeyEvent(pressed, keysym) { // Do not send requests if not connected if (!isConnected()) return; var key_xmlhttprequest = new XMLHttpRequest(); key_xmlhttprequest.open("GET", "key?index=" + (keyIndex++) + "&pressed=" + pressed + "&keysym=" + keysym); key_xmlhttprequest.send(null); } this.pressKey = function(keysym) { sendKeyEvent(1, keysym); }; this.releaseKey = function(keysym) { sendKeyEvent(0, keysym); }; /*****************************************/ /*** Mouse ***/ /*****************************************/ var mouse = new GuacamoleMouse(display); mouse.setButtonPressedHandler( function(mouseState) { sendMouseState(mouseState); } ); mouse.setButtonReleasedHandler( function(mouseState) { sendMouseState(mouseState); } ); mouse.setMovementHandler( function(mouseState) { // Draw client-side cursor if (cursorImage != null) { redrawCursor(); } sendMouseState(mouseState); } ); var sendingMouseEvents = 0; var mouseEventBuffer = ""; function sendMouseState(mouseState) { // Do not send requests if not connected if (!isConnected()) return; // Add event to queue, restart send loop if finished. if (mouseEventBuffer.length > 0) mouseEventBuffer += "&"; mouseEventBuffer += "event=" + mouseState.toString(); if (sendingMouseEvents == 0) sendPendingMouseEvents(); } function sendPendingMouseEvents() { // Do not send requests if not connected if (!isConnected()) return; if (mouseEventBuffer.length > 0) { sendingMouseEvents = 1; var mouse_xmlhttprequest = new XMLHttpRequest(); mouse_xmlhttprequest.open("GET", "pointer?" + mouseEventBuffer); mouseEventBuffer = ""; // Clear buffer var eventSendStart = new Date().getTime(); // Once response received, send next queued event. mouse_xmlhttprequest.onreadystatechange = function() { // Update round-trip-time figures if (mouse_xmlhttprequest.readyState == 2) { var eventSendEnd = new Date().getTime(); totalRoundTrip += eventSendEnd - eventSendStart; roundTripSamples++; } if (mouse_xmlhttprequest.readyState == 4) { sendPendingMouseEvents(); } }; mouse_xmlhttprequest.send(null); } else sendingMouseEvents = 0; } /*****************************************/ /*** Clipboard ***/ /*****************************************/ this.setClipboard = function(data) { // Do not send requests if not connected if (!isConnected()) return; var clipboard_xmlhttprequest = new XMLHttpRequest(); clipboard_xmlhttprequest.open("POST", "clipboard"); var sendStart = new Date().getTime(); // Update round trip metrics clipboard_xmlhttprequest.onreadystatechange = function() { // Update round-trip-time figures if (clipboard_xmlhttprequest.readyState == 2) { var sendEnd = new Date().getTime(); totalRoundTrip += sendEnd - sendStart; roundTripSamples++; } }; clipboard_xmlhttprequest.send(data); } function desaturateFilter(data, width, height) { for (var i=0; i= 2) nextRequest = makeRequest(); function parseResponse() { // Make request the moment we receive headers // If the event handler isn't set by the time headers are available, we will // already have made this request. if (xmlhttprequest.readyState == 2) { if (nextRequest == null) nextRequest = makeRequest(); start = new Date().getTime(); startOffset = 0; } // Parse stream when data is received and when complete. if (xmlhttprequest.readyState == 3 || xmlhttprequest.readyState == 4) { // Halt on error during request if (xmlhttprequest.status == 0) { showError("Request canceled by browser."); return; } else if (xmlhttprequest.status != 200) { showError("Error during request (HTTP " + xmlhttprequest.status + "): " + xmlhttprequest.statusText); return; } var current = xmlhttprequest.responseText; var instructionEnd; if (start == null) { start = new Date().getTime(); startOffset = current.length; } 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 = instruction.substr(0, opcodeEnd); var parameters = instruction.substr(opcodeEnd+1).split(","); // Call instruction handler. doInstruction(opcode, parameters); } // Start search at end of string. startIndex = current.length; delete instruction; delete parameters; // If we're done parsing, handle the next response. if (xmlhttprequest.readyState == 4 && isConnected()) { // If we got the start time, do statistics. if (start) { var end = new Date().getTime(); var duration = end - start; var length = current.length; totalTime += duration; totalTransferred += length; } delete xmlhttprequest; handleResponse(nextRequest); } } }; xmlhttprequest.onreadystatechange = parseResponse; // Handle what we have so far. parseResponse(); } function makeRequest() { // Calculate message limit as number of bytes likely to be transferred // in one round trip. var messageLimit; if (totalTime == 0 || roundTripSamples == 0) messageLimit = 10240; // Default to a reasonable 10k else messageLimit = Math.round(totalRoundTrip * totalTransferred / totalTime / roundTripSamples); // Download self var xmlhttprequest = new XMLHttpRequest(); xmlhttprequest.open("GET", "instructions?messageLimit=" + messageLimit); xmlhttprequest.send(null); return xmlhttprequest; } function unescapeGuacamoleString(str) { var unescapedString = ""; for (var i=0; i