// Copyright 2012 Citrix Systems, Inc. Licensed under the // Apache License, Version 2.0 (the "License"); you may not use this // file except in compliance with the License. Citrix Systems, Inc. // reserves all rights not expressly granted by 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. // // Automatically generated by addcopyright.py at 04/03/2012 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 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) { this.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) { } } if(os != null) { try { os.close(); } catch (Throwable e) { } } if(socket != null) { try { socket.close(); } catch (Throwable e) { } } } 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); this.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 + "..."); this.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(); } this.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() { 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(); } }