mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 00:53:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			590 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			590 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
 | |
| /*
 | |
|  *  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 <http://www.gnu.org/licenses/>.
 | |
|  */
 | |
| 
 | |
| 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
 | |
| 
 | |
|             // Once response received, send next queued event.
 | |
|             setTimeout(sendPendingMouseEvents, 10);
 | |
| 
 | |
|             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");
 | |
|         clipboard_xmlhttprequest.send(data);
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     function desaturateFilter(data, width, height) {
 | |
| 
 | |
|         for (var i=0; i<data.length; i+=4) {
 | |
| 
 | |
|             // Get RGB values
 | |
|             var r = data[i];
 | |
|             var g = data[i+1];
 | |
|             var b = data[i+2];
 | |
| 
 | |
|             // Desaturate
 | |
|             var v = Math.max(r, g, b) / 2;
 | |
|             data[i]   = v;
 | |
|             data[i+1] = v;
 | |
|             data[i+2] = v;
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     var errorHandler = null;
 | |
|     this.setErrorHandler = function(handler) {
 | |
|         errorHandler = handler;
 | |
|     };
 | |
| 
 | |
|     var errorEncountered = 0;
 | |
|     function showError(error) {
 | |
|         // Only display first error (avoid infinite error loops)
 | |
|         if (errorEncountered == 0) {
 | |
|             errorEncountered = 1;
 | |
| 
 | |
|             disconnect();
 | |
| 
 | |
|             // In case nothing has been rendered yet, use error style
 | |
|             display.className += " error";
 | |
| 
 | |
|             // Show error by desaturating display
 | |
|             if (background)
 | |
|                 background.filter(desaturateFilter);
 | |
| 
 | |
|             if (errorHandler)
 | |
|                 errorHandler(error);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     function handleErrors(message) {
 | |
|         var errors = message.getErrors();
 | |
|         for (var errorIndex=0; errorIndex<errors.length; errorIndex++)
 | |
|             showError(errors[errorIndex].getMessage());
 | |
|     }
 | |
| 
 | |
|     var clipboardHandler = null;
 | |
| 
 | |
|     this.setClipboardHandler = function(handler) {
 | |
|         clipboardHandler = handler;
 | |
|     };
 | |
| 
 | |
| 
 | |
|     function handleResponse(xmlhttprequest) {
 | |
| 
 | |
|         var startOffset = null;
 | |
| 
 | |
|         var instructionStart = 0;
 | |
|         var startIndex = 0;
 | |
| 
 | |
|         // Make request for next before it's too late
 | |
|         var nextRequest = null;
 | |
|         if (xmlhttprequest.readyState >= 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();
 | |
| 
 | |
|                 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;
 | |
|                 
 | |
|                 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) {
 | |
| 
 | |
|                         if (isConnected()) {
 | |
|                             delete xmlhttprequest;
 | |
|                             handleResponse(nextRequest);
 | |
|                         }
 | |
| 
 | |
|                         break;
 | |
|                     }
 | |
| 
 | |
|                     // Call instruction handler.
 | |
|                     doInstruction(opcode, parameters);
 | |
|                 }
 | |
| 
 | |
|                 // Start search at end of string.
 | |
|                 startIndex = current.length;
 | |
| 
 | |
|                 delete instruction;
 | |
|                 delete parameters;
 | |
| 
 | |
|             }
 | |
| 
 | |
|         };
 | |
| 
 | |
|         xmlhttprequest.onreadystatechange = parseResponse;
 | |
| 
 | |
|         // Handle what we have so far.
 | |
|         parseResponse();
 | |
|     }
 | |
| 
 | |
|     function makeRequest() {
 | |
| 
 | |
|         // Download self
 | |
|         var xmlhttprequest = new XMLHttpRequest();
 | |
|         xmlhttprequest.open("GET", "instructions");
 | |
|         xmlhttprequest.send(null); 
 | |
| 
 | |
|         return xmlhttprequest;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     function unescapeGuacamoleString(str) {
 | |
| 
 | |
|         var unescapedString = "";
 | |
| 
 | |
|         for (var i=0; i<str.length; i++) {
 | |
| 
 | |
|             var c = str.charAt(i);
 | |
|             if (c == "\\" && i<str.length-1) {
 | |
| 
 | |
|                 var escapeChar = str.charAt(++i);
 | |
|                 if (escapeChar == "c")
 | |
|                     unescapedString += ",";
 | |
|                 else if (escapeChar == "s")
 | |
|                     unescapedString += ";";
 | |
|                 else if (escapeChar == "\\")
 | |
|                     unescapedString += "\\";
 | |
|                 else
 | |
|                     unescapedString += "\\" + escapeChar;
 | |
| 
 | |
|             }
 | |
|             else
 | |
|                 unescapedString += c;
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return unescapedString;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     var instructionHandlers = {
 | |
| 
 | |
|         "error": function(parameters) {
 | |
|             showError(unescapeGuacamoleString(parameters[0]));
 | |
|         },
 | |
| 
 | |
|         "name": function(parameters) {
 | |
|             document.title = "Guacamole (" + unescapeGuacamoleString(parameters[0]) + ")";
 | |
|         },
 | |
| 
 | |
|         "clipboard": function(parameters) {
 | |
|             clipboardHandler(unescapeGuacamoleString(parameters[0]));
 | |
|         },
 | |
| 
 | |
|         "size": function(parameters) {
 | |
| 
 | |
|             var width = parseInt(parameters[0]);
 | |
|             var height = parseInt(parameters[1]);
 | |
| 
 | |
|             // Update (set) display size
 | |
|             if (display && (background == null || cursor == null)) {
 | |
|                 display.style.width = width + "px";
 | |
|                 display.style.height = height + "px";
 | |
| 
 | |
|                 background = new Layer(width, height);
 | |
|                 cursor = new Layer(width, height);
 | |
| 
 | |
|                 display.appendChild(background);
 | |
|                 display.appendChild(cursor);
 | |
|             }
 | |
| 
 | |
|         },
 | |
| 
 | |
|         "rect": function(parameters) {
 | |
| 
 | |
|             var x = parseInt(parameters[0]);
 | |
|             var y = parseInt(parameters[1]);
 | |
|             var w = parseInt(parameters[2]);
 | |
|             var h = parseInt(parameters[3]);
 | |
|             var color = parameters[4];
 | |
| 
 | |
|             background.drawRect(
 | |
|                 x,
 | |
|                 y,
 | |
|                 w,
 | |
|                 h,
 | |
|                 color
 | |
|             );
 | |
| 
 | |
|         },
 | |
| 
 | |
|         "png": function(parameters) {
 | |
| 
 | |
|             var x = parseInt(parameters[0]);
 | |
|             var y = parseInt(parameters[1]);
 | |
|             var data = parameters[2];
 | |
| 
 | |
|             background.draw(
 | |
|                 x,
 | |
|                 y,
 | |
|                 "data:image/png;base64," + data
 | |
|             );
 | |
| 
 | |
|             // If received first update, no longer waiting.
 | |
|             if (currentState == STATE_WAITING)
 | |
|                 setState(STATE_CONNECTED);
 | |
| 
 | |
|         },
 | |
| 
 | |
|         "copy": function(parameters) {
 | |
| 
 | |
|             var srcX = parseInt(parameters[0]);
 | |
|             var srcY = parseInt(parameters[1]);
 | |
|             var srcWidth = parseInt(parameters[2]);
 | |
|             var srcHeight = parseInt(parameters[3]);
 | |
|             var dstX = parseInt(parameters[4]);
 | |
|             var dstY = parseInt(parameters[5]);
 | |
| 
 | |
|             background.copyRect(
 | |
|                 srcX,
 | |
|                 srcY,
 | |
|                 srcWidth, 
 | |
|                 srcHeight, 
 | |
|                 dstX,
 | |
|                 dstY 
 | |
|             );
 | |
| 
 | |
|         },
 | |
| 
 | |
|         "cursor": function(parameters) {
 | |
| 
 | |
|             var x = parseInt(parameters[0]);
 | |
|             var y = parseInt(parameters[1]);
 | |
|             var data = parameters[2];
 | |
| 
 | |
|             // Start cursor image load
 | |
|             var image = new Image();
 | |
|             image.onload = function() {
 | |
|                 cursorImage = image;
 | |
|                 cursorHotspotX = x;
 | |
|                 cursorHotspotY = y;
 | |
|                 redrawCursor();
 | |
|             };
 | |
|             image.src = "data:image/png;base64," + data
 | |
| 
 | |
|         }
 | |
|       
 | |
|     };
 | |
| 
 | |
| 
 | |
|     function doInstruction(opcode, parameters) {
 | |
| 
 | |
|         var handler = instructionHandlers[opcode];
 | |
|         if (handler)
 | |
|             handler(parameters);
 | |
| 
 | |
|     }
 | |
|         
 | |
| 
 | |
|     this.connect = function() {
 | |
| 
 | |
|         // Attempt connection
 | |
|         var connect_xmlhttprequest = new XMLHttpRequest();
 | |
|         connect_xmlhttprequest.open("GET", "connect", false);
 | |
| 
 | |
|         setState(STATE_CONNECTING);
 | |
|         connect_xmlhttprequest.send(null);
 | |
| 
 | |
|         // Handle result (and check for errors) 
 | |
|         var message = new GuacamoleMessage(connect_xmlhttprequest.responseXML);
 | |
|         if (!message.hasErrors()) {
 | |
|             setState(STATE_WAITING);
 | |
|             handleResponse(makeRequest()); // Start stream if connection successful
 | |
|         }
 | |
|         else
 | |
|             handleErrors(message);
 | |
| 
 | |
|     };
 | |
| 
 | |
|     function disconnect() {
 | |
| 
 | |
|         // Only attempt disconnection not disconnected.
 | |
|         if (currentState != STATE_DISCONNECTED
 | |
|                 && currentState != STATE_DISCONNECTING) {
 | |
| 
 | |
|             setState(STATE_DISCONNECTING);
 | |
| 
 | |
|             // Attempt disdisconnection
 | |
|             var disconnect_xmlhttprequest = new XMLHttpRequest();
 | |
|             disconnect_xmlhttprequest.open("GET", "disconnect", false);
 | |
|             disconnect_xmlhttprequest.send(null);
 | |
| 
 | |
|             // Handle result (and check for errors) 
 | |
|             var message = new GuacamoleMessage(disconnect_xmlhttprequest.responseXML);
 | |
|             handleErrors(message);
 | |
| 
 | |
|             setState(STATE_DISCONNECTED);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     this.disconnect = disconnect;
 | |
| 
 | |
| }
 |