| // 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(); |
| } |
| } |