mirror of
				https://github.com/gyurix1968/guacamole-client.git
				synced 2025-10-31 17:13:21 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			602 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			602 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 
 | |
| /*
 | |
|  *  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
 | |
|  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | |
|  */
 | |
| 
 | |
| function GuacamoleClient(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;
 | |
|     }
 | |
| 
 | |
|     // 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 += " guac-hide-cursor";
 | |
|             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;
 | |
| 
 | |
|         sendMessage("key:" +  keysym + "," + pressed + ";");
 | |
|     }
 | |
| 
 | |
|     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);
 | |
|         }
 | |
|     );
 | |
| 
 | |
| 
 | |
|     function sendMouseState(mouseState) {
 | |
| 
 | |
|         // Do not send requests if not connected
 | |
|         if (!isConnected())
 | |
|             return;
 | |
| 
 | |
|         // Build mask
 | |
|         var buttonMask = 0;
 | |
|         if (mouseState.getLeft())   buttonMask |= 1;
 | |
|         if (mouseState.getMiddle()) buttonMask |= 2;
 | |
|         if (mouseState.getRight())  buttonMask |= 4;
 | |
|         if (mouseState.getUp())     buttonMask |= 8;
 | |
|         if (mouseState.getDown())   buttonMask |= 16;
 | |
| 
 | |
|         // Send message
 | |
|         sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
 | |
|     }
 | |
| 
 | |
|     var sendingMessages = 0;
 | |
|     var outputMessageBuffer = "";
 | |
| 
 | |
|     function sendMessage(message) {
 | |
| 
 | |
|         // 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", "inbound");
 | |
|             message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
 | |
|             message_xmlhttprequest.setRequestHeader("Content-length", outputMessageBuffer.length);
 | |
| 
 | |
|             // 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;
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
| 	/*****************************************/
 | |
| 	/*** Clipboard                         ***/
 | |
| 	/*****************************************/
 | |
| 
 | |
|     this.setClipboard = function(data) {
 | |
| 
 | |
|         // Do not send requests if not connected
 | |
|         if (!isConnected())
 | |
|             return;
 | |
| 
 | |
|         sendMessage("clipboard:" + escapeGuacamoleString(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 += " guac-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;
 | |
|     var requests = 0;
 | |
| 
 | |
|     this.setClipboardHandler = function(handler) {
 | |
|         clipboardHandler = handler;
 | |
|     };
 | |
| 
 | |
| 
 | |
|     function handleResponse(xmlhttprequest) {
 | |
| 
 | |
|         var nextRequest = null;
 | |
| 
 | |
|         var instructionStart = 0;
 | |
|         var startIndex = 0;
 | |
| 
 | |
|         function parseResponse() {
 | |
| 
 | |
|             // 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) {
 | |
| 
 | |
|                 // 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;
 | |
|                             if (nextRequest)
 | |
|                                 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;
 | |
|         parseResponse();
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     function makeRequest() {
 | |
| 
 | |
|         // Download self
 | |
|         var xmlhttprequest = new XMLHttpRequest();
 | |
|         xmlhttprequest.open("POST", "outbound");
 | |
|         xmlhttprequest.send(null); 
 | |
| 
 | |
|         return xmlhttprequest;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     function escapeGuacamoleString(str) {
 | |
| 
 | |
|         var escapedString = "";
 | |
| 
 | |
|         for (var i=0; i<str.length; i++) {
 | |
| 
 | |
|             var c = str.charAt(i);
 | |
|             if (c == ",")
 | |
|                 escapedString += "\\c";
 | |
|             else if (c == ";")
 | |
|                 escapedString += "\\s";
 | |
|             else if (c == "\\")
 | |
|                 escapedString += "\\\\";
 | |
|             else
 | |
|                 escapedString += c;
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return escapedString;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     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 = 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() {
 | |
| 
 | |
|         setState(STATE_CONNECTING);
 | |
| 
 | |
|         // Start tunnel and connect synchronously
 | |
|         var connect_xmlhttprequest = new XMLHttpRequest();
 | |
|         connect_xmlhttprequest.open("POST", "connect", false);
 | |
|         connect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
 | |
|         connect_xmlhttprequest.setRequestHeader("Content-length", 0);
 | |
|         connect_xmlhttprequest.send(null);
 | |
| 
 | |
|         // Start reading data
 | |
|         setState(STATE_WAITING);
 | |
|         handleResponse(makeRequest());
 | |
| 
 | |
|     };
 | |
| 
 | |
|     
 | |
|     function disconnect() {
 | |
| 
 | |
|         // Only attempt disconnection not disconnected.
 | |
|         if (currentState != STATE_DISCONNECTED
 | |
|                 && currentState != STATE_DISCONNECTING) {
 | |
| 
 | |
|             var message = "disconnect;";
 | |
|             setState(STATE_DISCONNECTING);
 | |
| 
 | |
|             // Send disconnect message (synchronously... as necessary until handoff is implemented)
 | |
|             var disconnect_xmlhttprequest = new XMLHttpRequest();
 | |
|             disconnect_xmlhttprequest.open("POST", "inbound", false);
 | |
|             disconnect_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
 | |
|             disconnect_xmlhttprequest.setRequestHeader("Content-length", message.length);
 | |
|             disconnect_xmlhttprequest.send(message);
 | |
| 
 | |
|             setState(STATE_DISCONNECTED);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     this.disconnect = disconnect;
 | |
| 
 | |
| }
 |