mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
Add time stamped ticket to console access URL to make it more secure 2) Fix a problem caused by the inconsistency of using different path seperator between windows platform and linux platform
1385 lines
42 KiB
Java
1385 lines
42 KiB
Java
/**
|
|
*
|
|
* This software is licensed under the GNU General Public License v3 or later.
|
|
*
|
|
* It is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package com.cloud.consoleproxy;
|
|
|
|
|
|
import java.awt.Container;
|
|
import java.awt.Dimension;
|
|
import java.awt.Frame;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Rectangle;
|
|
import java.awt.Toolkit;
|
|
import java.awt.event.InputEvent;
|
|
import java.awt.event.KeyEvent;
|
|
import java.awt.event.MouseEvent;
|
|
import java.awt.image.BufferedImage;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.net.InetSocketAddress;
|
|
import java.net.Socket;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.zip.GZIPOutputStream;
|
|
|
|
import org.apache.log4j.Logger;
|
|
|
|
import com.cloud.console.AuthenticationException;
|
|
import com.cloud.console.ConsoleCanvas;
|
|
import com.cloud.console.ConsoleCanvas2;
|
|
import com.cloud.console.ITileScanListener;
|
|
import com.cloud.console.Region;
|
|
import com.cloud.console.RfbProto;
|
|
import com.cloud.console.RfbProtoAdapter;
|
|
import com.cloud.console.RfbViewer;
|
|
import com.cloud.console.TileInfo;
|
|
import com.cloud.console.TileTracker;
|
|
|
|
public class ConsoleProxyViewer implements java.lang.Runnable, RfbViewer, RfbProtoAdapter, ITileScanListener {
|
|
private static final Logger s_logger = Logger.getLogger(ConsoleProxyViewer.class);
|
|
|
|
public final static int STATUS_ERROR = -1;
|
|
public final static int STATUS_UNINITIALIZED = 0;
|
|
public final static int STATUS_CONNECTING = 1;
|
|
public final static int STATUS_INITIALIZING = 2;
|
|
public final static int STATUS_NORMAL_OPERATION = 3;
|
|
public final static int STATUS_AUTHENTICATION_FAILURE = 100;
|
|
|
|
public final static int SHIFT_KEY_MASK = 64;
|
|
public final static int CTRL_KEY_MASK = 128;
|
|
public final static int META_KEY_MASK = 256;
|
|
public final static int ALT_KEY_MASK = 512;
|
|
|
|
int id = getNextId();
|
|
boolean compressServerMessage = false;
|
|
long createTime = System.currentTimeMillis();
|
|
long lastUsedTime = System.currentTimeMillis();
|
|
int status;
|
|
boolean dropMe = false;
|
|
boolean viewerInReuse = false;
|
|
|
|
String host;
|
|
int port;
|
|
String tag = "";
|
|
|
|
RfbProto rfb;
|
|
Thread rfbThread;
|
|
OutputStream clientStream;
|
|
String clientStreamInfo;
|
|
String passwordParam;
|
|
|
|
ViewerOptions options;
|
|
Frame vncFrame;
|
|
ConsoleCanvas vc;
|
|
Container vncContainer;
|
|
|
|
boolean ajaxViewer = false;
|
|
long ajaxSessionId = 0;
|
|
TileTracker tracker;
|
|
Object tileDirtyEvent;
|
|
boolean dirtyFlag = false;
|
|
boolean justCreated = true;
|
|
AjaxFIFOImageCache ajaxImageCache = new AjaxFIFOImageCache(2);
|
|
|
|
String cursorUpdatesDef;
|
|
String eightBitColorsDef;
|
|
|
|
int deferScreenUpdates;
|
|
int deferCursorUpdates;
|
|
int deferUpdateRequests;
|
|
|
|
int[] encodingsSaved;
|
|
int nEncodingsSaved;
|
|
|
|
boolean framebufferResized = false;
|
|
int resizedFramebufferWidth;
|
|
int resizedFramebufferHeight;
|
|
|
|
boolean cursorMoved = false;
|
|
int lastCursorPosX;
|
|
int lastCursorPosY;
|
|
|
|
boolean cursorShapeChanged = false;
|
|
int lastCursorShapeEncodingType;
|
|
int lastCursorShapeHotX;
|
|
int lastCursorShapeHotY;
|
|
int lastCursorShapeWidth;
|
|
int lastCursorShapeHeight;
|
|
byte[] lastCursorShapeData;
|
|
|
|
static int id_count = 1;
|
|
synchronized static int getNextId() {
|
|
return id_count++;
|
|
}
|
|
|
|
public void init() {
|
|
initProxy();
|
|
}
|
|
|
|
private void initProxy() {
|
|
options = new ViewerOptions();
|
|
options.viewOnly = true;
|
|
|
|
cursorUpdatesDef = null;
|
|
eightBitColorsDef = null;
|
|
|
|
tracker = new TileTracker();
|
|
tracker.initTracking(64, 64, 800, 600);
|
|
|
|
if(rfbThread != null) {
|
|
if(rfbThread.isAlive()) {
|
|
dropMe = true;
|
|
viewerInReuse = true;
|
|
if(rfb != null)
|
|
rfb.close();
|
|
|
|
try {
|
|
rfbThread.join();
|
|
} catch (InterruptedException e) {
|
|
s_logger.warn("InterruptedException while waiting for RFB thread to exit");
|
|
}
|
|
viewerInReuse = false;
|
|
}
|
|
}
|
|
|
|
dropMe = false;
|
|
rfbThread = new Thread(this);
|
|
rfbThread.setName("RFB Thread " + rfbThread.getId() + " >" + host + ":" + port);
|
|
rfbThread.start();
|
|
|
|
tileDirtyEvent = new Object();
|
|
}
|
|
|
|
public synchronized boolean justCreated() {
|
|
if(justCreated) {
|
|
justCreated = false;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public boolean isDropped() {
|
|
return dropMe;
|
|
}
|
|
|
|
public void run() {
|
|
createCanvas(0, 0);
|
|
|
|
int retries = 0;
|
|
while (!dropMe) {
|
|
try {
|
|
s_logger.info("Connecting to VNC server");
|
|
status = STATUS_CONNECTING;
|
|
connectAndAuthenticate();
|
|
retries = 0; // reset the retry count
|
|
status = STATUS_INITIALIZING;
|
|
doProtocolInitialisation();
|
|
vc.rfb = rfb;
|
|
vc.setPixelFormat();
|
|
|
|
// if we have a client current connected, when we have reconnected to the server and
|
|
// received a new ServerInit info (in doProtocolInitialisation()), we will
|
|
// convert it into frame buffer size change to make sure following on updates
|
|
// don't fall out of range
|
|
//
|
|
if(clientStream != null) {
|
|
// 128 bytes will be enough for this single PDU
|
|
s_logger.info("Send init framebuffer size (" + rfb.framebufferWidth + ", " + rfb.framebufferHeight + ")");
|
|
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(128);
|
|
try {
|
|
vc.encodeFramebufferResize(rfb.framebufferWidth, rfb.framebufferHeight, bos);
|
|
} catch(IOException e) {
|
|
}
|
|
writeToClientStream(bos.toByteArray());
|
|
}
|
|
|
|
vc.rfb.writeFramebufferUpdateRequest(0, 0,
|
|
vc.rfb.framebufferWidth, vc.rfb.framebufferHeight,
|
|
true);
|
|
status = STATUS_NORMAL_OPERATION;
|
|
vc.processNormalProtocol();
|
|
} catch (AuthenticationException e) {
|
|
status = STATUS_AUTHENTICATION_FAILURE;
|
|
String msg = e.getMessage();
|
|
s_logger.warn("Authentication exception, msg: " + msg + "sid: " + this.passwordParam);
|
|
} catch (Exception e) {
|
|
status = STATUS_ERROR;
|
|
if(s_logger.isDebugEnabled())
|
|
s_logger.debug("Exception : ", e);
|
|
} finally {
|
|
// String oldName = Thread.currentThread().getName();
|
|
encodingsSaved = null;
|
|
nEncodingsSaved = 0;
|
|
|
|
s_logger.info("Close current RFB");
|
|
synchronized (this) {
|
|
if (rfb != null) {
|
|
rfb.close();
|
|
}
|
|
}
|
|
}
|
|
if (dropMe) {
|
|
break;
|
|
}
|
|
if (status == STATUS_AUTHENTICATION_FAILURE) {
|
|
break;
|
|
} else {
|
|
retries++;
|
|
if(retries > ConsoleProxy.reconnectMaxRetry) {
|
|
s_logger.info("Exception caught, retry has reached to maximum : " + retries + ", will give up and disconnect client");
|
|
break;
|
|
}
|
|
|
|
s_logger.info("Exception caught, retrying in 1 second, current retry:" + retries);
|
|
|
|
try {
|
|
Thread.sleep(1000);
|
|
} catch (InterruptedException e) {
|
|
// ignored
|
|
}
|
|
}
|
|
}
|
|
|
|
// make sure we remove it from the management map upon main thread termination
|
|
dropMe = true;
|
|
|
|
// if we are reusing the viewer object, we shouldn't remove it from the map
|
|
// this can also prevent deadlock in initProxy() while initProxy tries to join
|
|
// the thread, as initProxy() is called with ConsoleProxy.connectionMap being locked
|
|
// while CoonsoleProxy.removeViewer() here will attempt to lock it from another thread
|
|
if(!viewerInReuse)
|
|
ConsoleProxy.removeViewer(this);
|
|
s_logger.info("RFB thread terminating");
|
|
}
|
|
|
|
void connectAndAuthenticate() throws Exception {
|
|
s_logger.info("Initializing...");
|
|
|
|
s_logger.info("Ensure ip route towards host " + host);
|
|
ConsoleProxy.ensureRoute(host);
|
|
|
|
s_logger.info("Connecting to " + host + ", port " + port + "...");
|
|
rfb = new RfbProto(host, port, this);
|
|
s_logger.info("Connected to server");
|
|
|
|
rfb.readVersionMsg();
|
|
s_logger.info("RFB server supports protocol version "
|
|
+ rfb.serverMajor + "." + rfb.serverMinor);
|
|
|
|
rfb.writeVersionMsg();
|
|
s_logger.info("Using RFB protocol version " + rfb.clientMajor
|
|
+ "." + rfb.clientMinor);
|
|
|
|
int secType = rfb.negotiateSecurity();
|
|
int authType;
|
|
if (secType == RfbProto.SecTypeTight) {
|
|
s_logger.info("Enabling TightVNC protocol extensions");
|
|
rfb.initCapabilities();
|
|
rfb.setupTunneling();
|
|
authType = rfb.negotiateAuthenticationTight();
|
|
} else {
|
|
authType = secType;
|
|
}
|
|
|
|
switch (authType) {
|
|
case RfbProto.AuthNone:
|
|
s_logger.info("No authentication needed");
|
|
rfb.authenticateNone();
|
|
break;
|
|
case RfbProto.AuthVNC:
|
|
s_logger.info("Performing standard VNC authentication");
|
|
if (passwordParam != null) {
|
|
rfb.authenticateVNC(passwordParam);
|
|
} else {
|
|
throw new AuthenticationException("Bad password");
|
|
}
|
|
break;
|
|
default:
|
|
throw new Exception("Unknown authentication scheme " + authType);
|
|
}
|
|
}
|
|
|
|
static void authenticationExternally(String host, String port, String tag, String sid, String ticket) throws AuthenticationException {
|
|
/*
|
|
if(ConsoleProxy.management_host != null) {
|
|
try {
|
|
boolean success = false;
|
|
URL url = new URL(ConsoleProxy.management_host + "/console?cmd=auth&vm=" + getTag() + "&sid=" + passwordParam);
|
|
|
|
URLConnection conn = url.openConnection();
|
|
|
|
// setting TIMEOUTs to avoid possible waiting until death situations
|
|
conn.setConnectTimeout(5000);
|
|
conn.setReadTimeout(5000);
|
|
|
|
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
String inputLine;
|
|
if ((inputLine = in.readLine()) != null) {
|
|
if(inputLine.equals("success"))
|
|
success = true;
|
|
}
|
|
in.close();
|
|
|
|
if(!success) {
|
|
if(s_logger.isInfoEnabled())
|
|
s_logger.info("External authenticator failed authencation request for vm " + getTag() + " with sid " + passwordParam);
|
|
|
|
throw new AuthenticationException("Unable to contact external authentication source " + ConsoleProxy.management_host);
|
|
}
|
|
} catch (MalformedURLException e) {
|
|
s_logger.error("Unexpected exception " + e.getMessage(), e);
|
|
} catch(IOException e) {
|
|
s_logger.error("Unable to contact external authentication source due to " + e.getMessage(), e);
|
|
throw new AuthenticationException("Unable to contact external authentication source " + ConsoleProxy.management_host);
|
|
}
|
|
} else {
|
|
s_logger.warn("No external authentication source being setup.");
|
|
}
|
|
*/
|
|
if(!ConsoleProxy.authenticateConsoleAccess(host, port, tag, sid, ticket)) {
|
|
s_logger.warn("External authenticator failed authencation request for vm " + tag + " with sid " + sid);
|
|
|
|
throw new AuthenticationException("External authenticator failed request for vm " + tag + " with sid " + sid);
|
|
}
|
|
}
|
|
|
|
void doProtocolInitialisation() throws IOException {
|
|
rfb.writeClientInit();
|
|
rfb.readServerInit();
|
|
setEncodings();
|
|
}
|
|
|
|
void setEncodings() {
|
|
setEncodings(false);
|
|
}
|
|
|
|
void setEncodings(boolean autoSelectOnly) {
|
|
if (options == null || rfb == null || !rfb.inNormalProtocol)
|
|
return;
|
|
|
|
int preferredEncoding = options.preferredEncoding;
|
|
if (preferredEncoding == -1) {
|
|
long kbitsPerSecond = rfb.kbitsPerSecond();
|
|
if (nEncodingsSaved < 1) {
|
|
// Choose Tight or ZRLE encoding for the very first update.
|
|
// Logger.log(Logger.INFO, "Using Tight/ZRLE encodings");
|
|
preferredEncoding = RfbProto.EncodingTight;
|
|
} else if (kbitsPerSecond > 2000
|
|
&& encodingsSaved[0] != RfbProto.EncodingHextile) {
|
|
// Switch to Hextile if the connection speed is above 2Mbps.
|
|
s_logger.info("Throughput " + kbitsPerSecond
|
|
+ " kbit/s - changing to Hextile encoding");
|
|
preferredEncoding = RfbProto.EncodingHextile;
|
|
} else if (kbitsPerSecond < 1000
|
|
&& encodingsSaved[0] != RfbProto.EncodingTight) {
|
|
// Switch to Tight/ZRLE if the connection speed is below 1Mbps.
|
|
s_logger.info("Throughput " + kbitsPerSecond
|
|
+ " kbit/s - changing to Tight/ZRLE encodings");
|
|
preferredEncoding = RfbProto.EncodingTight;
|
|
} else {
|
|
// Don't change the encoder.
|
|
if (autoSelectOnly)
|
|
return;
|
|
preferredEncoding = encodingsSaved[0];
|
|
}
|
|
} else {
|
|
// Auto encoder selection is not enabled.
|
|
if (autoSelectOnly)
|
|
return;
|
|
}
|
|
|
|
int[] encodings = new int[20];
|
|
int nEncodings = 0;
|
|
|
|
encodings[nEncodings++] = preferredEncoding;
|
|
if (options.useCopyRect) {
|
|
encodings[nEncodings++] = RfbProto.EncodingCopyRect;
|
|
}
|
|
|
|
if (preferredEncoding != RfbProto.EncodingTight) {
|
|
encodings[nEncodings++] = RfbProto.EncodingTight;
|
|
}
|
|
if (preferredEncoding != RfbProto.EncodingZRLE) {
|
|
encodings[nEncodings++] = RfbProto.EncodingZRLE;
|
|
}
|
|
if (preferredEncoding != RfbProto.EncodingHextile) {
|
|
encodings[nEncodings++] = RfbProto.EncodingHextile;
|
|
}
|
|
if (preferredEncoding != RfbProto.EncodingZlib) {
|
|
encodings[nEncodings++] = RfbProto.EncodingZlib;
|
|
}
|
|
if (preferredEncoding != RfbProto.EncodingCoRRE) {
|
|
encodings[nEncodings++] = RfbProto.EncodingCoRRE;
|
|
}
|
|
if (preferredEncoding != RfbProto.EncodingRRE) {
|
|
encodings[nEncodings++] = RfbProto.EncodingRRE;
|
|
}
|
|
|
|
if (options.compressLevel >= 0 && options.compressLevel <= 9) {
|
|
encodings[nEncodings++] = RfbProto.EncodingCompressLevel0
|
|
+ options.compressLevel;
|
|
}
|
|
if (options.jpegQuality >= 0 && options.jpegQuality <= 9) {
|
|
encodings[nEncodings++] = RfbProto.EncodingQualityLevel0
|
|
+ options.jpegQuality;
|
|
}
|
|
|
|
if (options.requestCursorUpdates) {
|
|
encodings[nEncodings++] = RfbProto.EncodingXCursor;
|
|
encodings[nEncodings++] = RfbProto.EncodingRichCursor;
|
|
if (!options.ignoreCursorUpdates)
|
|
encodings[nEncodings++] = RfbProto.EncodingPointerPos;
|
|
}
|
|
|
|
encodings[nEncodings++] = RfbProto.EncodingLastRect;
|
|
encodings[nEncodings++] = RfbProto.EncodingNewFBSize;
|
|
|
|
boolean encodingsWereChanged = false;
|
|
if (nEncodings != nEncodingsSaved) {
|
|
encodingsWereChanged = true;
|
|
} else {
|
|
for (int i = 0; i < nEncodings; i++) {
|
|
if (encodings[i] != encodingsSaved[i]) {
|
|
encodingsWereChanged = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (encodingsWereChanged) {
|
|
try {
|
|
rfb.writeSetEncodings(encodings, nEncodings);
|
|
if (vc != null) {
|
|
vc.softCursorFree();
|
|
}
|
|
} catch (Exception e) {
|
|
s_logger.error(e.toString(), e);
|
|
}
|
|
encodingsSaved = encodings;
|
|
nEncodingsSaved = nEncodings;
|
|
}
|
|
}
|
|
|
|
protected void startRecording() throws IOException {
|
|
}
|
|
|
|
protected void stopRecording() throws IOException {
|
|
}
|
|
|
|
void createCanvas(int maxWidth, int maxHeight) {
|
|
vc = new ConsoleCanvas2(this, maxWidth, maxHeight);
|
|
|
|
if (!options.viewOnly)
|
|
vc.enableInput(true);
|
|
}
|
|
|
|
synchronized void writeToClientStream(byte[] bs) {
|
|
// writeToClientStream swallows exceptions to make sure problems writing
|
|
// to client stream do not impact the main loop
|
|
if (clientStream != null) {
|
|
try {
|
|
lastUsedTime = System.currentTimeMillis();
|
|
synchronized (clientStream) {
|
|
clientStream.write(bs);
|
|
clientStream.flush();
|
|
}
|
|
} catch (IOException e) {
|
|
if(s_logger.isDebugEnabled()) {
|
|
s_logger.debug("Writing to client stream failed, reason: " + e.getMessage());
|
|
}
|
|
try {
|
|
clientStream.close();
|
|
} catch (IOException ioe){
|
|
// ignore
|
|
}
|
|
clientStream = null;
|
|
clientStreamInfo = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Implement RfbViewer interface
|
|
//
|
|
public boolean isProxy() {
|
|
return true;
|
|
}
|
|
|
|
public boolean hasClientConnection() {
|
|
// always return false if the viewer is AJAX viewer
|
|
if(ajaxViewer)
|
|
return false;
|
|
|
|
return clientStream != null;
|
|
}
|
|
|
|
public RfbProto getRfb() {
|
|
return rfb;
|
|
}
|
|
|
|
public Dimension getScreenSize() {
|
|
return (new Frame()).getToolkit().getScreenSize();
|
|
// return vncFrame.getToolkit().getScreenSize();
|
|
}
|
|
|
|
public Dimension getFrameSize() {
|
|
// return vncFrame.getSize();
|
|
return getScreenSize();
|
|
}
|
|
|
|
public int getScalingFactor() {
|
|
return options.scalingFactor;
|
|
}
|
|
|
|
public int getCursorScaleFactor() {
|
|
return options.scaleCursor;
|
|
}
|
|
|
|
public boolean ignoreCursorUpdate() {
|
|
return options.ignoreCursorUpdates;
|
|
}
|
|
|
|
public int getDeferCursorUpdateTimeout() {
|
|
return 0;
|
|
// return deferCursorUpdates;
|
|
}
|
|
|
|
public int getDeferScreenUpdateTimeout() {
|
|
return 0;
|
|
// return deferScreenUpdates;
|
|
}
|
|
|
|
public int getDeferUpdateRequestTimeout() {
|
|
return 0;
|
|
//return deferUpdateRequests;
|
|
}
|
|
|
|
public int setPixelFormat(RfbProto rfb) throws IOException {
|
|
if (options.eightBitColors) {
|
|
rfb.writeSetPixelFormat(8, 8, false, true, 7, 7, 3, 0, 3, 6);
|
|
return 1;
|
|
} else {
|
|
rfb.writeSetPixelFormat(32, 24, false, true, 255, 255, 255, 16, 8, 0);
|
|
return 4;
|
|
}
|
|
}
|
|
|
|
public void onInputEnabled(boolean enable) {
|
|
// do nothing in proxy viewer
|
|
}
|
|
|
|
public void onFramebufferSizeChange(int w, int h) {
|
|
tracker.resize(vc.scaledWidth, vc.scaledHeight);
|
|
|
|
synchronized(this) {
|
|
framebufferResized = true;
|
|
resizedFramebufferWidth = w;
|
|
resizedFramebufferHeight = h;
|
|
}
|
|
|
|
signalTileDirtyEvent();
|
|
}
|
|
|
|
public void onFramebufferUpdate(int x, int y, int w, int h) {
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Frame buffer update {" + x + "," + y + "," + w + "," + h + "}");
|
|
tracker.invalidate(new Rectangle(x, y, w, h));
|
|
|
|
signalTileDirtyEvent();
|
|
}
|
|
|
|
public void onFramebufferCursorMove(int x, int y) {
|
|
synchronized(this) {
|
|
cursorMoved = true;
|
|
lastCursorPosX = x;
|
|
lastCursorPosY = y;
|
|
}
|
|
|
|
signalTileDirtyEvent();
|
|
}
|
|
|
|
public void onFramebufferCursorShapeChange(int encodingType,
|
|
int xhot, int yhot, int width, int height, byte[] cursorData) {
|
|
|
|
synchronized(this) {
|
|
cursorShapeChanged = true;
|
|
|
|
lastCursorShapeEncodingType = encodingType;
|
|
lastCursorShapeHotX = xhot;
|
|
lastCursorShapeHotY = yhot;
|
|
lastCursorShapeWidth = width;
|
|
lastCursorShapeHeight = height;
|
|
lastCursorShapeData = cursorData;
|
|
}
|
|
|
|
signalTileDirtyEvent();
|
|
}
|
|
|
|
public void onDesktopResize() {
|
|
if(vncFrame != null)
|
|
vncFrame.pack();
|
|
}
|
|
|
|
public void onFrameResize(Dimension newSize) {
|
|
if(vncFrame != null)
|
|
vncFrame.setSize(newSize);
|
|
}
|
|
|
|
public void onDisconnectMessage() {
|
|
// do nothing in viewer mode
|
|
}
|
|
|
|
public void onBellMessage() {
|
|
Toolkit.getDefaultToolkit().beep();
|
|
}
|
|
|
|
public void onPreProtocolProcess(byte[] bs) throws IOException {
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Send " + (bs != null ? bs.length : 0) + " bytes (original) to client");
|
|
|
|
if (!ajaxViewer && bs != null && clientStream != null) {
|
|
if(s_logger.isInfoEnabled())
|
|
s_logger.info("getSplit got " + bs.length + " bytes");
|
|
if (compressServerMessage && bs.length > 10000) {
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(256000);
|
|
GZIPOutputStream gos = new GZIPOutputStream(bos, 65536);
|
|
gos.write(bs);
|
|
gos.finish();
|
|
byte[] nbs = bos.toByteArray();
|
|
gos.close();
|
|
int n = nbs.length;
|
|
|
|
if(s_logger.isInfoEnabled())
|
|
s_logger.info("Compressed " + bs.length + "=>" + n);
|
|
|
|
byte[] b = new byte[6];
|
|
b[0] = (byte) 250;
|
|
b[1] = 2;
|
|
b[2] = (byte) ((n >> 24) & 0xff);
|
|
b[3] = (byte) ((n >> 16) & 0xff);
|
|
b[4] = (byte) ((n >> 8) & 0xff);
|
|
b[5] = (byte) (n & 0xff);
|
|
|
|
// make sure two seperated writes completed atomically
|
|
synchronized(clientStream) {
|
|
writeToClientStream(b);
|
|
writeToClientStream(nbs);
|
|
}
|
|
} else {
|
|
if(s_logger.isInfoEnabled())
|
|
s_logger.info("Send uncompressed " + bs.length + " bytes to client");
|
|
|
|
writeToClientStream(bs);
|
|
}
|
|
} else {
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Client is not connected, ignore forwarding " + (bs != null ? bs.length : 0) + " bytes to client");
|
|
}
|
|
|
|
rfb.sis.setSplit();
|
|
}
|
|
|
|
public boolean onPostFrameBufferUpdateProcess(boolean cursorPosReceived) throws IOException {
|
|
boolean fullUpdateNeeded = false;
|
|
|
|
// Defer framebuffer update request if necessary. But wake up
|
|
// immediately on keyboard or mouse event. Also, don't sleep
|
|
// if there is some data to receive, or if the last update
|
|
// included a PointerPos message.
|
|
if (deferUpdateRequests > 0 && rfb.is.available() == 0 && !cursorPosReceived) {
|
|
synchronized(vc.rfb) {
|
|
try {
|
|
vc.rfb.wait(deferUpdateRequests);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
// Before requesting framebuffer update, check if the pixel
|
|
// format should be changed. If it should, request full update
|
|
// instead of an incremental one.
|
|
if (options.eightBitColors != (vc.bytesPixel == 1)) {
|
|
vc.setPixelFormat();
|
|
fullUpdateNeeded = true;
|
|
}
|
|
|
|
return fullUpdateNeeded;
|
|
}
|
|
|
|
public void onProtocolProcessException(IOException e) {
|
|
byte[] bs = new byte[2];
|
|
bs[0] = (byte)250;
|
|
bs[1] = 1;
|
|
writeToClientStream(bs);
|
|
}
|
|
|
|
public Socket createConnection(String host, int port) throws IOException {
|
|
Socket sock = new Socket();
|
|
sock.setSoTimeout(ConsoleProxy.readTimeoutSeconds*1000);
|
|
sock.setKeepAlive(true);
|
|
sock.connect(new InetSocketAddress(host, port), 30000);
|
|
return sock;
|
|
}
|
|
|
|
public void writeInit(OutputStream os) throws IOException {
|
|
if (options.shareDesktop) {
|
|
os.write(1);
|
|
} else {
|
|
os.write(0);
|
|
}
|
|
}
|
|
|
|
public void swapMouseButton(Integer[] masks) {
|
|
if (options.reverseMouseButtons2And3) {
|
|
Integer temp = masks[1];
|
|
masks[1] = masks[0];
|
|
masks[0] = temp;
|
|
}
|
|
}
|
|
|
|
public boolean onTileChange(Rectangle rowMergedRect, int row, int col) {
|
|
// currently we don't do scan-based client update
|
|
return true;
|
|
}
|
|
|
|
public void onRegionChange(List<Region> regionList) {
|
|
// obsolute
|
|
}
|
|
|
|
private void signalTileDirtyEvent() {
|
|
synchronized(tileDirtyEvent) {
|
|
dirtyFlag = true;
|
|
tileDirtyEvent.notifyAll();
|
|
}
|
|
}
|
|
|
|
public String getTag() {
|
|
return tag;
|
|
}
|
|
|
|
public void setTag(String tag) {
|
|
this.tag = tag;
|
|
}
|
|
|
|
//
|
|
// AJAX Image manipulation
|
|
//
|
|
public void copyTile(Graphics2D g, int x, int y, Rectangle rc) {
|
|
if(vc != null && vc.memImage != null) {
|
|
synchronized(vc.memImage) {
|
|
g.drawImage(vc.memImage, x, y, x + rc.width, y + rc.height,
|
|
rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public byte[] getFrameBufferJpeg() {
|
|
int width = 800;
|
|
int height = 600;
|
|
if(vc != null) {
|
|
width = vc.scaledWidth;
|
|
height = vc.scaledHeight;
|
|
}
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("getFrameBufferJpeg, w: " + width + ", h: " + height);
|
|
|
|
BufferedImage bufferedImage = new BufferedImage(width, height,
|
|
BufferedImage.TYPE_3BYTE_BGR);
|
|
if(vc != null && vc.memImage != null) {
|
|
synchronized(vc.memImage) {
|
|
Graphics2D g = bufferedImage.createGraphics();
|
|
g.drawImage(vc.memImage, 0, 0, width, height, 0, 0, width, height, null);
|
|
}
|
|
}
|
|
|
|
byte[] imgBits = null;
|
|
try {
|
|
imgBits = jpegFromImage(bufferedImage);
|
|
} catch (IOException e) {
|
|
}
|
|
return imgBits;
|
|
}
|
|
|
|
public byte[] getTilesMergedJpeg(List<TileInfo> tileList, int tileWidth, int tileHeight) {
|
|
|
|
int width = Math.max(tileWidth, tileWidth*tileList.size());
|
|
BufferedImage bufferedImage = new BufferedImage(width, tileHeight,
|
|
BufferedImage.TYPE_3BYTE_BGR);
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Create merged image, w: " + width + ", h: " + tileHeight);
|
|
|
|
if(vc != null && vc.memImage != null) {
|
|
synchronized(vc.memImage) {
|
|
Graphics2D g = bufferedImage.createGraphics();
|
|
int i = 0;
|
|
for(TileInfo tile : tileList) {
|
|
Rectangle rc = tile.getTileRect();
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Merge tile into jpeg from (" + rc.x + "," + rc.y + "," + (rc.x + rc.width) + "," + (rc.y + rc.height) + ") to (" + i*tileWidth + ",0)" );
|
|
|
|
g.drawImage(vc.memImage, i*tileWidth, 0, i*tileWidth + rc.width, rc.height,
|
|
rc.x, rc.y, rc.x + rc.width, rc.y + rc.height, null);
|
|
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
byte[] imgBits = null;
|
|
try {
|
|
imgBits = jpegFromImage(bufferedImage);
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Merge jpeg image size: " + imgBits.length + ", tiles: " + tileList.size());
|
|
} catch (IOException e) {
|
|
}
|
|
return imgBits;
|
|
}
|
|
|
|
public byte[] jpegFromImage(BufferedImage image) throws IOException {
|
|
ByteArrayOutputStream bos = new ByteArrayOutputStream(128000);
|
|
javax.imageio.ImageIO.write(image, "jpg", bos);
|
|
|
|
byte[] jpegBits = bos.toByteArray();
|
|
bos.close();
|
|
return jpegBits;
|
|
}
|
|
|
|
private String prepareAjaxImage(List<TileInfo> tiles, boolean init) {
|
|
byte[] imgBits;
|
|
if(init)
|
|
imgBits = getFrameBufferJpeg();
|
|
else
|
|
imgBits = getTilesMergedJpeg(tiles, tracker.getTileWidth(), tracker.getTileHeight());
|
|
|
|
if(imgBits == null) {
|
|
s_logger.warn("Unable to generate jpeg image");
|
|
} else {
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Generated jpeg image size: " + imgBits.length);
|
|
}
|
|
|
|
int key = ajaxImageCache.putImage(imgBits);
|
|
StringBuffer sb = new StringBuffer("/ajaximg?host=");
|
|
sb.append(host).append("&port=").append(port).append("&sid=").append(passwordParam);
|
|
sb.append("&key=").append(key).append("&ts=").append(System.currentTimeMillis());
|
|
return sb.toString();
|
|
}
|
|
|
|
private String prepareAjaxSession(boolean init) {
|
|
StringBuffer sb = new StringBuffer();
|
|
|
|
if(init)
|
|
ajaxSessionId++;
|
|
|
|
sb.append("/ajax?host=").append(host).append("&port=").append(port);
|
|
sb.append("&sid=").append(passwordParam).append("&sess=").append(ajaxSessionId);
|
|
return sb.toString();
|
|
}
|
|
|
|
public String onAjaxClientKickoff() {
|
|
return "onKickoff();";
|
|
}
|
|
|
|
private boolean waitForViewerReady() {
|
|
long startTick = System.currentTimeMillis();
|
|
while(System.currentTimeMillis() - startTick < 5000) {
|
|
if(this.status == ConsoleProxyViewer.STATUS_NORMAL_OPERATION)
|
|
return true;
|
|
|
|
try {
|
|
Thread.sleep(100);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private String onAjaxClientConnectFailed() {
|
|
return "<html><head></head><body><div id=\"main_panel\" tabindex=\"1\"><p>" +
|
|
"Unable to start console session as connection is refused by the machine you are accessing" +
|
|
"</p></div></body></html>";
|
|
}
|
|
|
|
public String onAjaxClientStart(String title) {
|
|
if(!waitForViewerReady())
|
|
return onAjaxClientConnectFailed();
|
|
|
|
// make sure we switch to AJAX view on start
|
|
setAjaxViewer(true);
|
|
|
|
int tileWidth = tracker.getTileWidth();
|
|
int tileHeight = tracker.getTileHeight();
|
|
int width = tracker.getTrackWidth();
|
|
int height = tracker.getTrackHeight();
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Ajax client start, frame buffer w: " + width + ", " + height);
|
|
|
|
synchronized(this) {
|
|
if(framebufferResized) {
|
|
framebufferResized = false;
|
|
}
|
|
}
|
|
|
|
int retry = 0;
|
|
if(justCreated()) {
|
|
tracker.initCoverageTest();
|
|
|
|
try {
|
|
rfb.writeFramebufferUpdateRequest(0, 0, tracker.getTrackWidth(), tracker.getTrackHeight(), false);
|
|
|
|
while(!tracker.hasFullCoverage() && retry < 10) {
|
|
try {
|
|
Thread.sleep(1000);
|
|
} catch (InterruptedException e) {
|
|
}
|
|
retry++;
|
|
}
|
|
} catch (IOException e1) {
|
|
s_logger.warn("Connection was broken ");
|
|
}
|
|
}
|
|
|
|
List<TileInfo> tiles = tracker.scan(true);
|
|
String imgUrl = prepareAjaxImage(tiles, true);
|
|
String updateUrl = prepareAjaxSession(true);
|
|
|
|
StringBuffer sbTileSequence = new StringBuffer();
|
|
int i = 0;
|
|
for(TileInfo tile : tiles) {
|
|
sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]");
|
|
if(i < tiles.size() - 1)
|
|
sbTileSequence.append(",");
|
|
|
|
i++;
|
|
}
|
|
|
|
/*
|
|
SimpleHash model = new SimpleHash();
|
|
model.put("tileSequence", sbTileSequence.toString());
|
|
model.put("imgUrl", imgUrl);
|
|
model.put("updateUrl", updateUrl);
|
|
model.put("width", String.valueOf(width));
|
|
model.put("height", String.valueOf(height));
|
|
model.put("tileWidth", String.valueOf(tileWidth));
|
|
model.put("tileHeight", String.valueOf(tileHeight));
|
|
model.put("title", title);
|
|
model.put("rawKeyboard", ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW ? "true" : "false");
|
|
|
|
StringWriter writer = new StringWriter();
|
|
try {
|
|
ConsoleProxy.processTemplate("viewer.ftl", model, writer);
|
|
} catch (IOException e) {
|
|
s_logger.warn("Unexpected exception in processing template.", e);
|
|
} catch (TemplateException e) {
|
|
s_logger.warn("Unexpected exception in processing template.", e);
|
|
}
|
|
StringBuffer sb = writer.getBuffer();
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("onAjaxClientStart response: " + sb.toString());
|
|
return sb.toString();
|
|
*/
|
|
return getAjaxViewerPageContent(sbTileSequence.toString(), imgUrl,
|
|
updateUrl, width, height, tileWidth, tileHeight, title,
|
|
ConsoleProxy.keyboardType == ConsoleProxy.KEYBOARD_RAW);
|
|
}
|
|
|
|
private String getAjaxViewerPageContent(String tileSequence, String imgUrl, String updateUrl, int width,
|
|
int height, int tileWidth, int tileHeight, String title, boolean rawKeyboard) {
|
|
|
|
String[] content = new String[] {
|
|
"<html>",
|
|
"<head>",
|
|
"<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/jquery.js\"></script>",
|
|
"<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/cloud.logger.js\"></script>",
|
|
"<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/ajaxviewer.js\"></script>",
|
|
"<script type=\"text/javascript\" language=\"javascript\" src=\"/resource/js/handler.js\"></script>",
|
|
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/resource/css/ajaxviewer.css\"></link>",
|
|
"<link rel=\"stylesheet\" type=\"text/css\" href=\"/resource/css/logger.css\"></link>",
|
|
"<title>" + title + "</title>",
|
|
"</head>",
|
|
"<body>",
|
|
"<div id=\"toolbar\">",
|
|
"<ul>",
|
|
"<li>",
|
|
"<a href=\"#\" onclick=\"javascript:sendCtrlAltDel();\">",
|
|
"<span><img align=\"left\" src=\"/resource/images/cad.gif\" alt=\"Ctrl-Alt-Del\" />Ctrl-Alt-Del</span>",
|
|
"</a>",
|
|
"</li>",
|
|
"<li>",
|
|
"<a href=\"#\" onclick=\"javascript:sendCtrlEsc();\">",
|
|
"<span><img align=\"left\" src=\"/resource/images/winlog.png\" alt=\"Ctrl-Esc\" style=\"width:16px;height:16px\"/>Ctrl-Esc</span>",
|
|
"</a>",
|
|
"</li>",
|
|
"</ul>",
|
|
"<span id=\"light\" class=\"dark\"></span>",
|
|
"</div>",
|
|
"<div id=\"main_panel\" tabindex=\"1\"></div>",
|
|
"<script language=\"javascript\">",
|
|
|
|
"var tileMap = [ " + tileSequence + " ];",
|
|
"var ajaxViewer = new AjaxViewer('main_panel', '" + imgUrl + "', '" + updateUrl + "', tileMap, ",
|
|
String.valueOf(width) + ", " + String.valueOf(height) + ", " + String.valueOf(tileWidth) + ", " + String.valueOf(tileHeight) + ", " + (rawKeyboard ? "true" : "false") + ");",
|
|
|
|
"$(function() {",
|
|
"ajaxViewer.start();",
|
|
"});",
|
|
|
|
"</script>",
|
|
"</body>",
|
|
"</html>"
|
|
};
|
|
|
|
StringBuffer sb = new StringBuffer();
|
|
for(int i = 0; i < content.length; i++)
|
|
sb.append(content[i]);
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
public String onAjaxClientDisconnected() {
|
|
return "onDisconnect();";
|
|
}
|
|
|
|
public String onAjaxClientUpdate() {
|
|
if(!waitForViewerReady())
|
|
return onAjaxClientDisconnected();
|
|
|
|
synchronized(tileDirtyEvent) {
|
|
if(!dirtyFlag) {
|
|
try {
|
|
tileDirtyEvent.wait(3000);
|
|
} catch(InterruptedException e) {
|
|
}
|
|
}
|
|
}
|
|
|
|
boolean doResize = false;
|
|
synchronized(this) {
|
|
if(framebufferResized) {
|
|
framebufferResized = false;
|
|
doResize = true;
|
|
}
|
|
}
|
|
|
|
List<TileInfo> tiles;
|
|
|
|
if(doResize)
|
|
tiles = tracker.scan(true);
|
|
else
|
|
tiles = tracker.scan(false);
|
|
dirtyFlag = false;
|
|
|
|
String imgUrl = prepareAjaxImage(tiles, false);
|
|
StringBuffer sbTileSequence = new StringBuffer();
|
|
int i = 0;
|
|
for(TileInfo tile : tiles) {
|
|
sbTileSequence.append("[").append(tile.getRow()).append(",").append(tile.getCol()).append("]");
|
|
if(i < tiles.size() - 1)
|
|
sbTileSequence.append(",");
|
|
|
|
i++;
|
|
}
|
|
|
|
/*
|
|
SimpleHash model = new SimpleHash();
|
|
model.put("tileSequence", sbTileSequence.toString());
|
|
model.put("resized", doResize);
|
|
model.put("imgUrl", imgUrl);
|
|
model.put("width", String.valueOf(resizedFramebufferWidth));
|
|
model.put("height", String.valueOf(resizedFramebufferHeight));
|
|
model.put("tileWidth", String.valueOf(tracker.getTileWidth()));
|
|
model.put("tileHeight", String.valueOf(tracker.getTileHeight()));
|
|
|
|
StringWriter writer = new StringWriter();
|
|
try {
|
|
ConsoleProxy.processTemplate("viewer-update.ftl", model, writer);
|
|
} catch (IOException e) {
|
|
s_logger.warn("Unexpected exception in processing template.", e);
|
|
} catch (TemplateException e) {
|
|
s_logger.warn("Unexpected exception in processing template.", e);
|
|
}
|
|
StringBuffer sb = writer.getBuffer();
|
|
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("onAjaxClientUpdate response: " + sb.toString());
|
|
|
|
return sb.toString();
|
|
*/
|
|
return getAjaxViewerUpdatePageContent(sbTileSequence.toString(), imgUrl, doResize, resizedFramebufferWidth,
|
|
resizedFramebufferHeight, tracker.getTileWidth(), tracker.getTileHeight());
|
|
}
|
|
|
|
private String getAjaxViewerUpdatePageContent(String tileSequence, String imgUrl, boolean resized, int width,
|
|
int height, int tileWidth, int tileHeight) {
|
|
|
|
String[] content = new String[] {
|
|
"tileMap = [ " + tileSequence + " ];",
|
|
resized ? "ajaxViewer.resize('main_panel', " + width + ", " + height + " , " + tileWidth + ", " + tileHeight + ");" : "",
|
|
"ajaxViewer.refresh('" + imgUrl + "', tileMap, false);"
|
|
};
|
|
|
|
StringBuffer sb = new StringBuffer();
|
|
for(int i = 0; i < content.length; i++)
|
|
sb.append(content[i]);
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
|
|
public long getAjaxSessionId() {
|
|
return this.ajaxSessionId;
|
|
}
|
|
|
|
public AjaxFIFOImageCache getAjaxImageCache() {
|
|
return ajaxImageCache;
|
|
}
|
|
|
|
public boolean isAjaxViewer() {
|
|
return ajaxViewer;
|
|
}
|
|
|
|
public synchronized void setAjaxViewer(boolean ajaxViewer) {
|
|
if(this.ajaxViewer != ajaxViewer) {
|
|
if(this.ajaxViewer) {
|
|
// previous session was AJAX session
|
|
this.ajaxSessionId++; // increase the session id so that it will disconnect existing AJAX viewer
|
|
} else {
|
|
// close java client session
|
|
if(clientStream != null) {
|
|
byte[] bs = new byte[2];
|
|
bs[0] = (byte)250;
|
|
bs[1] = 1;
|
|
writeToClientStream(bs);
|
|
|
|
try {
|
|
clientStream.close();
|
|
} catch (IOException e) {
|
|
}
|
|
clientStream = null;
|
|
}
|
|
}
|
|
this.ajaxViewer = ajaxViewer;
|
|
}
|
|
}
|
|
|
|
public void writeServer(byte[] b, int off, int len) {
|
|
synchronized (this) {
|
|
if (!rfb.closed()) {
|
|
try {
|
|
// We lock the viewer to avoid race condition when connecting one
|
|
// client forces the current client to disconnect.
|
|
rfb.os.write(b, off, len);
|
|
rfb.os.flush();
|
|
} catch (IOException e) {
|
|
// Swallow the exception because we want the client connection to sustain
|
|
// even when server connection is severed and reestablished.
|
|
s_logger.info("Ignore exception when writing to server: " + e);
|
|
rfb.close();
|
|
}
|
|
} else {
|
|
s_logger.info("Dropping client event because server connection is closed ");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void sendClientMouseEvent(int event, int x, int y, int code, int modifiers) {
|
|
if(code == 2)
|
|
modifiers |= MouseEvent.BUTTON3_MASK;
|
|
else
|
|
modifiers |= MouseEvent.BUTTON1_MASK;
|
|
|
|
int id = 0;
|
|
if(event == 1)
|
|
id = MouseEvent.MOUSE_MOVED;
|
|
else if(event == 2)
|
|
id = MouseEvent.MOUSE_PRESSED;
|
|
else if(event == 3)
|
|
id = MouseEvent.MOUSE_RELEASED;
|
|
else if(event == 8)
|
|
id = MouseEvent.MOUSE_PRESSED;
|
|
|
|
long curTicks = System.currentTimeMillis();
|
|
MouseEvent mouseEvent = new MouseEvent(vc, id,
|
|
curTicks, modifiers, x, y, 1, false);
|
|
|
|
synchronized (this) {
|
|
if (rfb != null && !rfb.closed()) {
|
|
try {
|
|
rfb.writePointerEvent(mouseEvent);
|
|
if(event == 8) {
|
|
if(s_logger.isTraceEnabled())
|
|
s_logger.trace("Replay mouse double click event at " + x + "," + y);
|
|
|
|
mouseEvent = new MouseEvent(vc, MouseEvent.MOUSE_RELEASED,
|
|
curTicks, modifiers, x, y, 1, false);
|
|
rfb.writePointerEvent(mouseEvent);
|
|
|
|
mouseEvent = new MouseEvent(vc, MouseEvent.MOUSE_PRESSED,
|
|
curTicks, modifiers, x, y, 1, false);
|
|
rfb.writePointerEvent(mouseEvent);
|
|
|
|
mouseEvent = new MouseEvent(vc, MouseEvent.MOUSE_RELEASED,
|
|
curTicks, modifiers, x, y, 1, false);
|
|
rfb.writePointerEvent(mouseEvent);
|
|
}
|
|
} catch (IOException e) {
|
|
s_logger.warn("Exception while sending mouse event. ", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void sendClientRawKeyboardEvent(int event, int code, int modifiers) {
|
|
code = ConsoleProxyAjaxKeyMapper.getInstance().getJvmKeyCode(code);
|
|
switch(event) {
|
|
case 4 : // Key press
|
|
//
|
|
// special handling for ' and " (keycode: 222, char code : 39 and 34
|
|
//
|
|
if(code == 39 || code == 34) {
|
|
writeKeyboardEvent(KeyEvent.KEY_PRESSED, 222, (char)code, getAwtModifiers(modifiers));
|
|
}
|
|
break;
|
|
|
|
case 5 : // Key down
|
|
if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0 && (modifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0 && code == KeyEvent.VK_INSERT) {
|
|
code = KeyEvent.VK_DELETE;
|
|
}
|
|
|
|
if(code != 222) {
|
|
writeKeyboardEvent(KeyEvent.KEY_PRESSED, code,
|
|
ConsoleProxyAjaxKeyMapper.getInstance().shiftedKeyCharFromKeyCode(code, (modifiers & ConsoleProxyViewer.SHIFT_KEY_MASK) != 0),
|
|
getAwtModifiers(modifiers));
|
|
}
|
|
break;
|
|
|
|
case 6 : // Key Up
|
|
writeKeyboardEvent(KeyEvent.KEY_RELEASED, code,
|
|
ConsoleProxyAjaxKeyMapper.getInstance().shiftedKeyCharFromKeyCode(code, (modifiers & ConsoleProxyViewer.SHIFT_KEY_MASK) != 0),
|
|
getAwtModifiers(modifiers));
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void sendClientKeyboardEvent(int event, int code, int modifiers) {
|
|
int vkCode;
|
|
switch(event) {
|
|
case 4 : // Key press
|
|
if(code == 0 || (modifiers & (ConsoleProxyViewer.CTRL_KEY_MASK | ConsoleProxyViewer.META_KEY_MASK | ConsoleProxyViewer.ALT_KEY_MASK)) != 0) {
|
|
// if code is extend keys or has ctrl, alt, meta being pressed, ignore javascript key-press event
|
|
return;
|
|
}
|
|
|
|
vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getRegularCharVkCode(code);
|
|
if(vkCode > 0) {
|
|
writeKeyboardEvent(KeyEvent.KEY_PRESSED, vkCode, (char)code, 0);
|
|
writeKeyboardEvent(KeyEvent.KEY_RELEASED, vkCode, (char)code, 0);
|
|
}
|
|
break;
|
|
|
|
case 5 : // Key down
|
|
vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getActionCharVkCode(code);
|
|
if(vkCode >= 0 || (modifiers & (ConsoleProxyViewer.CTRL_KEY_MASK | ConsoleProxyViewer.META_KEY_MASK | ConsoleProxyViewer.ALT_KEY_MASK)) != 0) {
|
|
if(vkCode < 0) {
|
|
vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getRegularCharVkCode(code);
|
|
|
|
if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0) { // if control-key is pressed, always use lower-case char-code
|
|
if(vkCode >= (int)'A' && vkCode <= (int)'Z')
|
|
vkCode = (int)'a' + (vkCode - (int)'A');
|
|
}
|
|
}
|
|
|
|
if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0 && (modifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0 && vkCode == KeyEvent.VK_INSERT) {
|
|
vkCode = KeyEvent.VK_DELETE;
|
|
}
|
|
|
|
writeKeyboardEvent(KeyEvent.KEY_PRESSED, vkCode, (char)vkCode,
|
|
getAwtModifiers(modifiers));
|
|
}
|
|
break;
|
|
|
|
case 6 : // Key Up
|
|
vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getActionCharVkCode(code);
|
|
if(vkCode >= 0 || (modifiers & (ConsoleProxyViewer.CTRL_KEY_MASK | ConsoleProxyViewer.META_KEY_MASK | ConsoleProxyViewer.ALT_KEY_MASK)) != 0) {
|
|
if(vkCode < 0) {
|
|
vkCode = ConsoleProxyAjaxKeyMapper.getInstance().getRegularCharVkCode(code);
|
|
|
|
if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0) { // if control-key is pressed, always use lower-case char-code
|
|
if(vkCode >= (int)'A' && vkCode <= (int)'Z')
|
|
vkCode = (int)'a' + (vkCode - (int)'A');
|
|
}
|
|
|
|
if((modifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0 && (modifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0 && vkCode == KeyEvent.VK_INSERT) {
|
|
vkCode = KeyEvent.VK_DELETE;
|
|
}
|
|
}
|
|
|
|
writeKeyboardEvent(KeyEvent.KEY_RELEASED, vkCode, (char)vkCode,
|
|
getAwtModifiers(modifiers));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
private static int getAwtModifiers(int jsModifiers) {
|
|
int awtModifiers = 0;
|
|
|
|
if((jsModifiers & ConsoleProxyViewer.SHIFT_KEY_MASK) != 0) {
|
|
awtModifiers |= InputEvent.SHIFT_DOWN_MASK;
|
|
}
|
|
|
|
if((jsModifiers & ConsoleProxyViewer.CTRL_KEY_MASK) != 0) {
|
|
awtModifiers |= InputEvent.CTRL_DOWN_MASK;
|
|
}
|
|
|
|
if((jsModifiers & ConsoleProxyViewer.ALT_KEY_MASK) != 0) {
|
|
awtModifiers |= InputEvent.ALT_DOWN_MASK;
|
|
}
|
|
|
|
return awtModifiers;
|
|
}
|
|
|
|
private void writeKeyboardEvent(int keyEventType, int code, char keyChar, int modifiers) {
|
|
KeyEvent keyEvent;
|
|
try {
|
|
keyEvent = new KeyEvent(vc, keyEventType,
|
|
System.currentTimeMillis(), modifiers, code, keyChar);
|
|
} catch(Exception e) {
|
|
s_logger.warn("Unable to construct KeyEvent object, key code: " + code + ", keyChar: " + keyChar + " ", e);
|
|
return;
|
|
}
|
|
|
|
synchronized (this) {
|
|
if (rfb != null && !rfb.closed()) {
|
|
try {
|
|
rfb.writeKeyEvent(keyEvent);
|
|
} catch (IOException e) {
|
|
s_logger.warn("Exception while sending keyboard event. ", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|