mirror of https://github.com/apache/cloudstack.git
459 lines
16 KiB
Java
459 lines
16 KiB
Java
// Licensed to the Apache Software Foundation (ASF) under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
package com.cloud.consoleproxy.vnc;
|
|
|
|
import java.awt.Frame;
|
|
import java.awt.ScrollPane;
|
|
import java.awt.event.WindowAdapter;
|
|
import java.awt.event.WindowEvent;
|
|
import java.io.DataInputStream;
|
|
import java.io.DataOutputStream;
|
|
import java.io.IOException;
|
|
import java.net.Socket;
|
|
import java.net.UnknownHostException;
|
|
import java.security.spec.KeySpec;
|
|
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.SecretKey;
|
|
import javax.crypto.SecretKeyFactory;
|
|
import javax.crypto.spec.DESKeySpec;
|
|
|
|
import com.cloud.consoleproxy.ConsoleProxyClientListener;
|
|
import com.cloud.consoleproxy.util.Logger;
|
|
import com.cloud.consoleproxy.util.RawHTTP;
|
|
import com.cloud.consoleproxy.vnc.packet.client.KeyboardEventPacket;
|
|
import com.cloud.consoleproxy.vnc.packet.client.MouseEventPacket;
|
|
|
|
public class VncClient {
|
|
private static final Logger s_logger = Logger.getLogger(VncClient.class);
|
|
|
|
private Socket socket;
|
|
private DataInputStream is;
|
|
private DataOutputStream os;
|
|
|
|
private final VncScreenDescription screen = new VncScreenDescription();
|
|
|
|
private VncClientPacketSender sender;
|
|
private VncServerPacketReceiver receiver;
|
|
|
|
private boolean noUI = false;
|
|
private ConsoleProxyClientListener clientListener = null;
|
|
|
|
public static void main(String args[]) {
|
|
if (args.length < 3) {
|
|
printHelpMessage();
|
|
System.exit(1);
|
|
}
|
|
|
|
String host = args[0];
|
|
String port = args[1];
|
|
String password = args[2];
|
|
|
|
try {
|
|
new VncClient(host, Integer.parseInt(port), password, false, null);
|
|
} catch (NumberFormatException e) {
|
|
s_logger.error("Incorrect VNC server port number: " + port + ".");
|
|
System.exit(1);
|
|
} catch (UnknownHostException e) {
|
|
s_logger.error("Incorrect VNC server host name: " + host + ".");
|
|
System.exit(1);
|
|
} catch (IOException e) {
|
|
s_logger.error("Cannot communicate with VNC server: " + e.getMessage());
|
|
System.exit(1);
|
|
} catch (Throwable e) {
|
|
s_logger.error("An error happened: " + e.getMessage());
|
|
System.exit(1);
|
|
}
|
|
System.exit(0);
|
|
}
|
|
|
|
private static void printHelpMessage() {
|
|
/* LOG */s_logger.info("Usage: HOST PORT PASSWORD.");
|
|
}
|
|
|
|
public VncClient(ConsoleProxyClientListener clientListener) {
|
|
noUI = true;
|
|
this.clientListener = clientListener;
|
|
}
|
|
|
|
public VncClient(String host, int port, String password, boolean noUI, ConsoleProxyClientListener clientListener) throws UnknownHostException, IOException {
|
|
|
|
this.noUI = noUI;
|
|
this.clientListener = clientListener;
|
|
connectTo(host, port, password);
|
|
}
|
|
|
|
public void shutdown() {
|
|
if (sender != null)
|
|
sender.closeConnection();
|
|
|
|
if (receiver != null)
|
|
receiver.closeConnection();
|
|
|
|
if (is != null) {
|
|
try {
|
|
is.close();
|
|
} catch (Throwable e) {
|
|
s_logger.info("[ignored]"
|
|
+ "failed to close resource for input: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
if (os != null) {
|
|
try {
|
|
os.close();
|
|
} catch (Throwable e) {
|
|
s_logger.info("[ignored]"
|
|
+ "failed to get close resource for output: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
|
|
if (socket != null) {
|
|
try {
|
|
socket.close();
|
|
} catch (Throwable e) {
|
|
s_logger.info("[ignored]"
|
|
+ "failed to get close resource for socket: " + e.getLocalizedMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
public ConsoleProxyClientListener getClientListener() {
|
|
return clientListener;
|
|
}
|
|
|
|
public void connectTo(String host, int port, String path, String session, boolean useSSL, String sid) throws UnknownHostException, IOException {
|
|
if (port < 0) {
|
|
if (useSSL)
|
|
port = 443;
|
|
else
|
|
port = 80;
|
|
}
|
|
|
|
RawHTTP tunnel = new RawHTTP("CONNECT", host, port, path, session, useSSL);
|
|
socket = tunnel.connect();
|
|
doConnect(sid);
|
|
}
|
|
|
|
public void connectTo(String host, int port, String password) throws UnknownHostException, IOException {
|
|
// Connect to server
|
|
s_logger.info("Connecting to VNC server " + host + ":" + port + "...");
|
|
socket = new Socket(host, port);
|
|
doConnect(password);
|
|
}
|
|
|
|
private void doConnect(String password) throws IOException {
|
|
is = new DataInputStream(socket.getInputStream());
|
|
os = new DataOutputStream(socket.getOutputStream());
|
|
|
|
// Initialize connection
|
|
handshake();
|
|
authenticate(password);
|
|
initialize();
|
|
|
|
s_logger.info("Connecting to VNC server succeeded, start session");
|
|
|
|
// Run client-to-server packet sender
|
|
sender = new VncClientPacketSender(os, screen, this);
|
|
|
|
// Create buffered image canvas
|
|
BufferedImageCanvas canvas = new BufferedImageCanvas(sender, screen.getFramebufferWidth(), screen.getFramebufferHeight());
|
|
|
|
// Subscribe packet sender to various events
|
|
canvas.addMouseListener(sender);
|
|
canvas.addMouseMotionListener(sender);
|
|
canvas.addKeyListener(sender);
|
|
|
|
Frame frame = null;
|
|
if (!noUI)
|
|
frame = createVncClientMainWindow(canvas, screen.getDesktopName());
|
|
|
|
new Thread(sender).start();
|
|
|
|
// Run server-to-client packet receiver
|
|
receiver = new VncServerPacketReceiver(is, canvas, screen, this, sender, clientListener);
|
|
try {
|
|
receiver.run();
|
|
} finally {
|
|
if (frame != null) {
|
|
frame.setVisible(false);
|
|
frame.dispose();
|
|
}
|
|
shutdown();
|
|
}
|
|
}
|
|
|
|
private Frame createVncClientMainWindow(BufferedImageCanvas canvas, String title) {
|
|
// Create AWT windows
|
|
final Frame frame = new Frame(title + " - VNCle");
|
|
|
|
// Use scrolling pane to support screens, which are larger than ours
|
|
ScrollPane scroller = new ScrollPane(ScrollPane.SCROLLBARS_AS_NEEDED);
|
|
scroller.add(canvas);
|
|
scroller.setSize(screen.getFramebufferWidth(), screen.getFramebufferHeight());
|
|
|
|
frame.add(scroller);
|
|
frame.pack();
|
|
frame.setVisible(true);
|
|
|
|
frame.addWindowListener(new WindowAdapter() {
|
|
@Override
|
|
public void windowClosing(WindowEvent evt) {
|
|
frame.setVisible(false);
|
|
shutdown();
|
|
}
|
|
});
|
|
|
|
return frame;
|
|
}
|
|
|
|
/**
|
|
* Handshake with VNC server.
|
|
*/
|
|
private void handshake() throws IOException {
|
|
|
|
// Read protocol version
|
|
byte[] buf = new byte[12];
|
|
is.readFully(buf);
|
|
String rfbProtocol = new String(buf);
|
|
|
|
// Server should use RFB protocol 3.x
|
|
if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) {
|
|
s_logger.error("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
|
|
throw new RuntimeException("Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\".");
|
|
}
|
|
|
|
// Send response: we support RFB 3.3 only
|
|
String ourProtocolString = RfbConstants.RFB_PROTOCOL_VERSION + "\n";
|
|
os.write(ourProtocolString.getBytes());
|
|
os.flush();
|
|
}
|
|
|
|
/**
|
|
* VNC authentication.
|
|
*/
|
|
private void authenticate(String password) throws IOException {
|
|
// Read security type
|
|
int authType = is.readInt();
|
|
|
|
switch (authType) {
|
|
case RfbConstants.CONNECTION_FAILED: {
|
|
// Server forbids to connect. Read reason and throw exception
|
|
|
|
int length = is.readInt();
|
|
byte[] buf = new byte[length];
|
|
is.readFully(buf);
|
|
String reason = new String(buf, RfbConstants.CHARSET);
|
|
|
|
s_logger.error("Authentication to VNC server is failed. Reason: " + reason);
|
|
throw new RuntimeException("Authentication to VNC server is failed. Reason: " + reason);
|
|
}
|
|
|
|
case RfbConstants.NO_AUTH: {
|
|
// Client can connect without authorization. Nothing to do.
|
|
break;
|
|
}
|
|
|
|
case RfbConstants.VNC_AUTH: {
|
|
s_logger.info("VNC server requires password authentication");
|
|
doVncAuth(password);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
s_logger.error("Unsupported VNC protocol authorization scheme, scheme code: " + authType + ".");
|
|
throw new RuntimeException("Unsupported VNC protocol authorization scheme, scheme code: " + authType + ".");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode client password and send it to server.
|
|
*/
|
|
private void doVncAuth(String password) throws IOException {
|
|
|
|
// Read challenge
|
|
byte[] challenge = new byte[16];
|
|
is.readFully(challenge);
|
|
|
|
// Encode challenge with password
|
|
byte[] response;
|
|
try {
|
|
response = encodePassword(challenge, password);
|
|
} catch (Exception e) {
|
|
s_logger.error("Cannot encrypt client password to send to server: " + e.getMessage());
|
|
throw new RuntimeException("Cannot encrypt client password to send to server: " + e.getMessage());
|
|
}
|
|
|
|
// Send encoded challenge
|
|
os.write(response);
|
|
os.flush();
|
|
|
|
// Read security result
|
|
int authResult = is.readInt();
|
|
|
|
switch (authResult) {
|
|
case RfbConstants.VNC_AUTH_OK: {
|
|
// Nothing to do
|
|
break;
|
|
}
|
|
|
|
case RfbConstants.VNC_AUTH_TOO_MANY:
|
|
s_logger.error("Connection to VNC server failed: too many wrong attempts.");
|
|
throw new RuntimeException("Connection to VNC server failed: too many wrong attempts.");
|
|
|
|
case RfbConstants.VNC_AUTH_FAILED:
|
|
s_logger.error("Connection to VNC server failed: wrong password.");
|
|
throw new RuntimeException("Connection to VNC server failed: wrong password.");
|
|
|
|
default:
|
|
s_logger.error("Connection to VNC server failed, reason code: " + authResult);
|
|
throw new RuntimeException("Connection to VNC server failed, reason code: " + authResult);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encode password using DES encryption with given challenge.
|
|
*
|
|
* @param challenge
|
|
* a random set of bytes.
|
|
* @param password
|
|
* a password
|
|
* @return DES hash of password and challenge
|
|
*/
|
|
public byte[] encodePassword(byte[] challenge, String password) throws Exception {
|
|
// VNC password consist of up to eight ASCII characters.
|
|
byte[] key = {0, 0, 0, 0, 0, 0, 0, 0}; // Padding
|
|
byte[] passwordAsciiBytes = password.getBytes(RfbConstants.CHARSET);
|
|
System.arraycopy(passwordAsciiBytes, 0, key, 0, Math.min(password.length(), 8));
|
|
|
|
// Flip bytes (reverse bits) in key
|
|
for (int i = 0; i < key.length; i++) {
|
|
key[i] = flipByte(key[i]);
|
|
}
|
|
|
|
KeySpec desKeySpec = new DESKeySpec(key);
|
|
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance("DES");
|
|
SecretKey secretKey = secretKeyFactory.generateSecret(desKeySpec);
|
|
Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding");
|
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
|
|
|
byte[] response = cipher.doFinal(challenge);
|
|
return response;
|
|
}
|
|
|
|
/**
|
|
* Reverse bits in byte, so least significant bit will be most significant
|
|
* bit. E.g. 01001100 will become 00110010.
|
|
*
|
|
* See also: http://www.vidarholen.net/contents/junk/vnc.html ,
|
|
* http://bytecrafter
|
|
* .blogspot.com/2010/09/des-encryption-as-used-in-vnc.html
|
|
*
|
|
* @param b
|
|
* a byte
|
|
* @return byte in reverse order
|
|
*/
|
|
private static byte flipByte(byte b) {
|
|
int b1_8 = (b & 0x1) << 7;
|
|
int b2_7 = (b & 0x2) << 5;
|
|
int b3_6 = (b & 0x4) << 3;
|
|
int b4_5 = (b & 0x8) << 1;
|
|
int b5_4 = (b & 0x10) >>> 1;
|
|
int b6_3 = (b & 0x20) >>> 3;
|
|
int b7_2 = (b & 0x40) >>> 5;
|
|
int b8_1 = (b & 0x80) >>> 7;
|
|
byte c = (byte)(b1_8 | b2_7 | b3_6 | b4_5 | b5_4 | b6_3 | b7_2 | b8_1);
|
|
return c;
|
|
}
|
|
|
|
private void initialize() throws IOException {
|
|
// Send client initialization message
|
|
{
|
|
// Send shared flag
|
|
os.writeByte(RfbConstants.EXCLUSIVE_ACCESS);
|
|
os.flush();
|
|
}
|
|
|
|
// Read server initialization message
|
|
{
|
|
// Read frame buffer size
|
|
int framebufferWidth = is.readUnsignedShort();
|
|
int framebufferHeight = is.readUnsignedShort();
|
|
screen.setFramebufferSize(framebufferWidth, framebufferHeight);
|
|
if (clientListener != null)
|
|
clientListener.onFramebufferSizeChange(framebufferWidth, framebufferHeight);
|
|
}
|
|
|
|
// Read pixel format
|
|
{
|
|
int bitsPerPixel = is.readUnsignedByte();
|
|
int depth = is.readUnsignedByte();
|
|
|
|
int bigEndianFlag = is.readUnsignedByte();
|
|
int trueColorFlag = is.readUnsignedByte();
|
|
|
|
int redMax = is.readUnsignedShort();
|
|
int greenMax = is.readUnsignedShort();
|
|
int blueMax = is.readUnsignedShort();
|
|
|
|
int redShift = is.readUnsignedByte();
|
|
int greenShift = is.readUnsignedByte();
|
|
int blueShift = is.readUnsignedByte();
|
|
|
|
// Skip padding
|
|
is.skipBytes(3);
|
|
|
|
screen.setPixelFormat(bitsPerPixel, depth, bigEndianFlag, trueColorFlag, redMax, greenMax, blueMax, redShift, greenShift, blueShift);
|
|
}
|
|
|
|
// Read desktop name
|
|
{
|
|
int length = is.readInt();
|
|
byte buf[] = new byte[length];
|
|
is.readFully(buf);
|
|
String desktopName = new String(buf, RfbConstants.CHARSET);
|
|
screen.setDesktopName(desktopName);
|
|
}
|
|
}
|
|
|
|
public FrameBufferCanvas getFrameBufferCanvas() {
|
|
if (receiver != null)
|
|
return receiver.getCanvas();
|
|
|
|
return null;
|
|
}
|
|
|
|
public void requestUpdate(boolean fullUpdate) {
|
|
if (fullUpdate)
|
|
sender.requestFullScreenUpdate();
|
|
else
|
|
sender.imagePaintedOnScreen();
|
|
}
|
|
|
|
public void sendClientKeyboardEvent(int event, int code, int modifiers) {
|
|
sender.sendClientPacket(new KeyboardEventPacket(event, code));
|
|
}
|
|
|
|
public void sendClientMouseEvent(int event, int x, int y, int code, int modifiers) {
|
|
sender.sendClientPacket(new MouseEventPacket(event, x, y));
|
|
}
|
|
|
|
public boolean isHostConnected() {
|
|
return receiver != null && receiver.isConnectionAlive();
|
|
}
|
|
}
|