From 0081dc288b18b09bc1f4b35f4df7863e16adc5b9 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Wed, 21 Sep 2022 02:24:24 -0300 Subject: [PATCH 01/15] Secure KVM VNC Console Access Using the CA Framework --- .../servlet/ConsoleProxyClientParam.java | 5 + .../ConsoleAccessManagerImpl.java | 6 + .../consoleproxy/ConsoleProxyAjaxHandler.java | 2 + .../ConsoleProxyAjaxImageHandler.java | 2 + .../consoleproxy/ConsoleProxyClientParam.java | 5 + .../ConsoleProxyHttpHandlerHelper.java | 6 + .../ConsoleProxyNoVNCHandler.java | 8 + .../consoleproxy/ConsoleProxyNoVncClient.java | 82 +++- .../cloud/consoleproxy/vnc/NoVncClient.java | 419 ++++++++++++++++-- .../cloud/consoleproxy/vnc/RfbConstants.java | 5 +- .../consoleproxy/vnc/network/NioSocket.java | 123 +++++ .../vnc/network/NioSocketHandler.java | 45 ++ .../vnc/network/NioSocketHandlerImpl.java | 113 +++++ .../vnc/network/NioSocketInputStream.java | 210 +++++++++ .../vnc/network/NioSocketOutputStream.java | 121 +++++ .../vnc/network/NioSocketStream.java | 81 ++++ .../vnc/network/NioSocketTLSInputStream.java | 68 +++ .../vnc/network/NioSocketTLSOutputStream.java | 65 +++ .../vnc/network/SSLEngineManager.java | 180 ++++++++ .../vnc/security/NoneVncSecurity.java | 27 ++ .../vnc/security/VncAuthSecurity.java | 58 +++ .../vnc/security/VncSecurity.java | 46 ++ .../vnc/security/VncTLSSecurity.java | 116 +++++ .../ConsoleProxyNoVncClientTest.java | 16 + systemvm/agent/noVNC/app/styles/base.css | 6 + systemvm/agent/noVNC/app/ui.js | 20 +- systemvm/agent/noVNC/core/rfb.js | 9 +- 27 files changed, 1802 insertions(+), 42 deletions(-) create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/NoneVncSecurity.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java create mode 100644 services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java create mode 100644 services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java index 7f0fb0ba7f7f..e23778c0b984 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java @@ -22,6 +22,7 @@ public class ConsoleProxyClientParam { private int clientHostPort; private String clientHostPassword; private String clientTag; + private String clientDisplayName; private String ticket; private String locale; private String clientTunnelUrl; @@ -85,6 +86,10 @@ public void setClientTag(String clientTag) { this.clientTag = clientTag; } + public String getClientDisplayName() { return this.clientDisplayName; } + + public void setClientDisplayName(String clientDisplayName) { this.clientDisplayName = clientDisplayName; } + public String getTicket() { return ticket; } diff --git a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java index 559ceb43e053..17e79af224b6 100644 --- a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java @@ -33,6 +33,7 @@ import com.cloud.storage.GuestOSVO; import com.cloud.user.Account; import com.cloud.user.AccountManager; +import com.cloud.uservm.UserVm; import com.cloud.utils.Pair; import com.cloud.utils.Ternary; import com.cloud.utils.component.ManagerBase; @@ -278,6 +279,10 @@ private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMach UserVmDetailVO details = userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD); String tag = vm.getUuid(); + String displayName = vm.getHostName(); + if (vm instanceof UserVm) { + displayName = ((UserVm) vm).getDisplayName(); + } String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid); ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword()); @@ -342,6 +347,7 @@ private ConsoleProxyClientParam generateConsoleProxyClientParam(Ternary getQueryMap(String query) { } else { s_logger.error("decode token. tag info is not found!"); } + if (param.getClientDisplayName() != null) { + s_logger.debug("decode token. displayname: " + param.getClientDisplayName()); + map.put("displayname", param.getClientDisplayName()); + } else { + s_logger.error("decode token. displayname info is not found!"); + } if (param.getClientHostPassword() != null) { map.put("sid", param.getClientHostPassword()); } else { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java index 72f019bd3d12..849042e7ec45 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java @@ -29,6 +29,7 @@ import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError; import org.eclipse.jetty.websocket.api.annotations.OnWebSocketFrame; import org.eclipse.jetty.websocket.api.annotations.WebSocket; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -79,6 +80,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce String sid = queryMap.get("sid"); String tag = queryMap.get("tag"); String ticket = queryMap.get("ticket"); + String displayName = queryMap.get("displayname"); String ajaxSessionIdStr = queryMap.get("sess"); String console_url = queryMap.get("consoleurl"); String console_host_session = queryMap.get("sessionref"); @@ -126,6 +128,7 @@ public void onConnect(final Session session) throws IOException, InterruptedExce param.setClientHostPassword(sid); param.setClientTag(tag); param.setTicket(ticket); + param.setClientDisplayName(displayName); param.setClientTunnelUrl(console_url); param.setClientTunnelSession(console_host_session); param.setLocale(vm_locale); @@ -174,4 +177,9 @@ public void onClose(Session session, int statusCode, String reason) throws IOExc public void onFrame(Frame f) throws IOException { viewer.sendClientFrame(f); } + + @OnWebSocketError + public void onError(Throwable cause) { + s_logger.error("Error on websocket", cause); + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index f1c195913038..46ec7053e1fe 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -26,6 +26,8 @@ import java.net.URI; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.List; import com.cloud.consoleproxy.vnc.NoVncClient; @@ -80,9 +82,7 @@ public boolean isFrontEndAlive() { } public void sendClientFrame(Frame f) throws IOException { - byte[] data = new byte[f.getPayloadLength()]; - f.getPayload().get(data); - client.write(data); + client.writeFrame(f); } @Override @@ -113,6 +113,14 @@ public void run() { updateFrontEndActivityTime(); } connectionAlive = client.isVncOverWebSocketConnectionAlive(); + } else if (!client.isVncOverTunnel()) { + byte[] bytesArr; + int nextBytes = client.getNextBytes(); + bytesArr = new byte[nextBytes]; + client.readBytes(bytesArr, nextBytes); + if (nextBytes > 0) { + session.getRemote().sendBytes(ByteBuffer.wrap(bytesArr)); + } } else { b = new byte[100]; readBytes = client.read(b); @@ -127,7 +135,7 @@ public void run() { } connectionAlive = false; } catch (IOException e) { - e.printStackTrace(); + s_logger.error("Error on VNC client", e); } } @@ -137,18 +145,80 @@ public void run() { /** * Authenticate to VNC server when not using websockets + * + * Since we are supporting the 3.8 version of the RFB protocol, there are changes on the stages: + * 1. Handshake: + * 1.a. Protocol version + * 1.b. Security types + * 2. Security types + * 3. Initialisation + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#7protocol-messages * @throws IOException */ private void authenticateToVNCServer() throws IOException { - if (!client.isVncOverWebSocketConnection()) { + if (client.isVncOverWebSocketConnection()) { + return; + } + + if (client.isVncOverTunnel()) { String ver = client.handshake(); session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length())); - byte[] b = client.authenticate(getClientHostPassword()); + byte[] b = client.authenticateTunnel(getClientHostPassword()); session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4)); + } else { + ByteBuffer verStr = client.handshakeProtocolVersion(); + sendMessageToVNCClient(verStr.array(), 12); + + int secType = client.handshakeSecurityType(); + byte[] numberTypesToClient = new byte[] { 1, (byte) secType }; + sendMessageToVNCClient(numberTypesToClient, 2); + + client.processHandshakeSecurityType(secType, getClientHostPassword(), + getClientHostAddress(), getClientHostPort()); + + byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 }; + sendMessageToVNCClient(securityResultToClient, 4); + client.setWaitForNoVnc(true); + + while (client.isWaitForNoVnc()) { + s_logger.debug("Waiting"); + } + + String serverName = String.format("%s %s", clientParam.getClientDisplayName(), + client.isTLSConnectionEstablished() ? "(TLS backend)" : ""); + byte[] bytesServerInit = rewriteServerNameInServerInit(client.readServerInit(), serverName); + s_logger.info(String.format("Server init message is %s (%s)", Arrays.toString(bytesServerInit), new String(bytesServerInit))); + session.getRemote().sendBytes(ByteBuffer.wrap(bytesServerInit)); + client.setWaitForNoVnc(true); + while (client.isWaitForNoVnc()) { + s_logger.debug("Waiting"); + } + s_logger.info("Authenticated successfully"); } } + /** + * Send a message to the noVNC client + */ + private void sendMessageToVNCClient(byte[] arr, int length) { + try { + session.getRemote().sendBytes(ByteBuffer.wrap(arr, 0, length)); + } catch (IOException e) { + s_logger.error("Error sending a message to the noVNC client", e); + } + } + + protected static byte[] rewriteServerNameInServerInit(byte[] serverInitBytes, String serverName) { + byte[] serverNameBytes = serverName.getBytes(StandardCharsets.UTF_8); + ByteBuffer serverInitBuffer = ByteBuffer.allocate(24 + serverNameBytes.length); + serverInitBuffer.put(serverInitBytes, 0, 20); + serverInitBuffer.putInt(serverNameBytes.length); + serverInitBuffer.put(serverNameBytes); + return serverInitBuffer.array(); + } + /** * Connect to a VNC server in one of three possible ways: * - When tunnelUrl and tunnelSession are not empty -> via tunnel diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index 7be6421714e7..1aa0dc2b86a0 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -26,26 +26,44 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.spec.KeySpec; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.DESKeySpec; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; import com.cloud.consoleproxy.util.Logger; import com.cloud.consoleproxy.util.RawHTTP; +import com.cloud.consoleproxy.vnc.network.NioSocket; +import com.cloud.consoleproxy.vnc.network.NioSocketHandler; +import com.cloud.consoleproxy.vnc.network.NioSocketHandlerImpl; +import com.cloud.consoleproxy.vnc.network.SSLEngineManager; +import com.cloud.consoleproxy.vnc.security.VncSecurity; +import com.cloud.consoleproxy.vnc.security.VncTLSSecurity; import com.cloud.consoleproxy.websocket.WebSocketReverseProxy; +import com.cloud.utils.exception.CloudRuntimeException; +import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.extensions.Frame; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; public class NoVncClient { private static final Logger s_logger = Logger.getLogger(NoVncClient.class); - private Socket socket; - private DataInputStream is; - private DataOutputStream os; + private Socket tunnelSocket; + private DataInputStream tunnelInputStream; + private DataOutputStream tunnelOutputStream; + + private NioSocketHandler socketConnection; private WebSocketReverseProxy webSocketReverseProxy; + private boolean flushAfterReceivingNoVNCData = true; + private boolean securityPhaseCompleted = false; + private Integer writerLeft = null; + public NoVncClient() { } @@ -58,15 +76,19 @@ public void connectTo(String host, int port, String path, String session, boolea } RawHTTP tunnel = new RawHTTP("CONNECT", host, port, path, session, useSSL); - socket = tunnel.connect(); - setStreams(); + tunnelSocket = tunnel.connect(); + setTunnelSocketStreams(); } public void connectTo(String host, int port) throws UnknownHostException, IOException { // Connect to server s_logger.info("Connecting to VNC server " + host + ":" + port + "..."); - socket = new Socket(host, port); - setStreams(); + try { + NioSocket nioSocket = new NioSocket(host, port); + this.socketConnection = new NioSocketHandlerImpl(nioSocket); + } catch (Exception e) { + s_logger.error("Cannot create socket to host: " + host + " and port " + port, e); + } } // VNC over WebSocket connection helpers @@ -75,6 +97,10 @@ public void connectToWebSocket(String websocketUrl, Session session) throws URIS webSocketReverseProxy.connect(); } + public boolean isVncOverTunnel() { + return this.tunnelSocket != null; + } + public boolean isVncOverWebSocketConnection() { return webSocketReverseProxy != null; } @@ -93,9 +119,16 @@ public void proxyMsgOverWebSocketConnection(ByteBuffer msg) { } } - private void setStreams() throws IOException { - this.is = new DataInputStream(this.socket.getInputStream()); - this.os = new DataOutputStream(this.socket.getOutputStream()); + private void setTunnelSocketStreams() throws IOException { + this.tunnelInputStream = new DataInputStream(this.tunnelSocket.getInputStream()); + this.tunnelOutputStream = new DataOutputStream(this.tunnelSocket.getOutputStream()); + } + + public List getVncSecurityStack(int secType, String vmPassword, String host, int port) throws IOException { + if (secType == RfbConstants.V_ENCRYPT) { + secType = getVEncryptSecuritySubtype(); + } + return VncSecurity.getSecurityStack(secType, vmPassword, host, port); } /** @@ -105,7 +138,7 @@ public String handshake() throws IOException { // Read protocol version byte[] buf = new byte[12]; - is.readFully(buf); + tunnelInputStream.readFully(buf); String rfbProtocol = new String(buf); // Server should use RFB protocol 3.x @@ -119,20 +152,21 @@ public String handshake() throws IOException { return RfbConstants.RFB_PROTOCOL_VERSION + "\n"; } + /** * VNC authentication. */ - public byte[] authenticate(String password) + public byte[] authenticateTunnel(String password) throws IOException { // Read security type - int authType = is.readInt(); + int authType = tunnelInputStream.readInt(); switch (authType) { case RfbConstants.CONNECTION_FAILED: { // Server forbids to connect. Read reason and throw exception - int length = is.readInt(); + int length = tunnelInputStream.readInt(); byte[] buf = new byte[length]; - is.readFully(buf); + tunnelInputStream.readFully(buf); String reason = new String(buf, RfbConstants.CHARSET); s_logger.error("Authentication to VNC server is failed. Reason: " + reason); @@ -146,7 +180,7 @@ public byte[] authenticate(String password) case RfbConstants.VNC_AUTH: { s_logger.info("VNC server requires password authentication"); - doVncAuth(is, os, password); + doVncAuth(tunnelInputStream, tunnelOutputStream, password); break; } @@ -157,7 +191,7 @@ public byte[] authenticate(String password) } // Since we've taken care of the auth, we tell the client that there's no auth // going on - return new byte[] { 0, 0, 0, 1 }; + return new byte[] { 0, 0, 0, 1 }; } /** @@ -205,7 +239,7 @@ private void doVncAuth(DataInputStream in, DataOutputStream out, String password } } - private byte flipByte(byte b) { + public static byte flipByte(byte b) { int b1_8 = (b & 0x1) << 7; int b2_7 = (b & 0x2) << 5; int b3_6 = (b & 0x4) << 3; @@ -218,7 +252,7 @@ private byte flipByte(byte b) { return c; } - public byte[] encodePassword(byte[] challenge, String password) throws Exception { + public static 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(Charset.availableCharsets().get("US-ASCII")); @@ -239,16 +273,349 @@ public byte[] encodePassword(byte[] challenge, String password) throws Exception return response; } + /** + * Decide the RFB protocol version with the VNC server + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion + */ + protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException { + // Read protocol version + byte[] buf = new byte[12]; + tunnelInputStream.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 + "\"."); + } + tunnelOutputStream.write(buf); + return RfbConstants.RFB_PROTOCOL_VERSION + "\n"; + } + + /** + * Agree on the security type with the VNC server + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security + * @return list of the security types to be processed + */ + protected List handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword, + String host, int port) throws IOException { + int securityType = selectFromTheServerOfferedSecurityTypes(); + + // Inform the server about our decision + this.tunnelOutputStream.writeByte(securityType); + + byte[] numberTypesToClient = new byte[] { 1, (byte) securityType }; + clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2)); + + if (securityType == RfbConstants.V_ENCRYPT) { + securityType = getVEncryptSecuritySubtype(); + } + return VncSecurity.getSecurityStack(securityType, vmPassword, host, port); + } + + /** + * Obtain the VEncrypt subtype from the VNC server + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt + */ + protected int getVEncryptSecuritySubtype() throws IOException { + int majorVEncryptVersion = socketConnection.readUnsignedInteger(8); + int minorVEncryptVersion = socketConnection.readUnsignedInteger(8); + int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion; + s_logger.debug("VEncrypt version: " + vEncryptVersion); + socketConnection.writeUnsignedInteger(8, majorVEncryptVersion); + if (vEncryptVersion >= 0x0002) { + socketConnection.writeUnsignedInteger(8, 2); + socketConnection.flushWriteBuffer(); + } else { + socketConnection.writeUnsignedInteger(8, 0); + socketConnection.flushWriteBuffer(); + throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version"); + } + int ack = socketConnection.readUnsignedInteger(8); + if (ack != 0) { + throw new IOException("The VNC server did not agree on the VEncrypt version"); + } + + int numberOfSubtypes = socketConnection.readUnsignedInteger(8); + if (numberOfSubtypes <= 0) { + throw new CloudRuntimeException("The server reported no VeNCrypt sub-types"); + } + int selectedSubtype = 0; + for (int i = 0; i < numberOfSubtypes; i++) { + while (!socketConnection.checkIfBytesAreAvailableForReading(4)) { + s_logger.trace("Waiting for vEncrypt subtype"); + } + int subtype = socketConnection.readUnsignedInteger(32); + if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) { + selectedSubtype = subtype; + break; + } + } + + s_logger.info("Selected VEncrypt subtype " + selectedSubtype); + socketConnection.writeUnsignedInteger(32, selectedSubtype); + socketConnection.flushWriteBuffer(); + + return selectedSubtype; + } + + private int selectFromTheServerOfferedSecurityTypes() throws IOException { + int numberOfSecurityTypes = tunnelInputStream.readByte(); + if (numberOfSecurityTypes == 0) { + int reasonLength = tunnelInputStream.readInt(); + byte[] reasonBuffer = new byte[reasonLength]; + tunnelInputStream.readFully(reasonBuffer); + String reason = new String(reasonBuffer); + String errMsg = "No security type provided by the VNC server, reason: " + reason; + s_logger.error(errMsg); + throw new IOException(errMsg); + } + + for (int i = 0; i < numberOfSecurityTypes; i++) { + int securityType = tunnelInputStream.readByte(); + if (securityType != 0 && VncSecurity.supportedSecurityTypes.contains(securityType)) { + s_logger.info("Selected the security type: " + securityType); + return securityType; + } + } + throw new IOException("Could not select a supported or valid security type from the offered by the server"); + } + + /** + * VNC authentication. + */ + public void processSecurityResult(String password) + throws IOException { + // Read security result + int authResult = this.tunnelInputStream.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); + } + } + public int read(byte[] b) throws IOException { - return is.read(b); + return tunnelInputStream.read(b); } public void write(byte[] b) throws IOException { if (isVncOverWebSocketConnection()) { proxyMsgOverWebSocketConnection(ByteBuffer.wrap(b)); + } else if (!isVncOverTunnel()) { + this.socketConnection.writeBytes(b, 0, b.length); + } else { + tunnelOutputStream.write(b); + } + } + + public void writeFrame(Frame frame) { + byte[] data = new byte[frame.getPayloadLength()]; + frame.getPayload().get(data); + + if (securityPhaseCompleted) { + socketConnection.writeBytes(ByteBuffer.wrap(data), data.length); + socketConnection.flushWriteBuffer(); + if (writerLeft == null) { + writerLeft = 3; + setWaitForNoVnc(false); + } else if (writerLeft > 0) { + writerLeft--; + } + } else { + socketConnection.writeBytes(data, 0, data.length); + if (flushAfterReceivingNoVNCData) { + socketConnection.flushWriteBuffer(); + flushAfterReceivingNoVNCData = false; + } + } + + if (!securityPhaseCompleted || (writerLeft != null && writerLeft == 0)) { + setWaitForNoVnc(false); + } + } + + /** + * Starts the handshake with the VNC server - ProtocolVersion + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion + */ + public ByteBuffer handshakeProtocolVersion() { + ByteBuffer verStr = ByteBuffer.allocate(12); + int majorVersion; + int minorVersion; + + s_logger.debug("Reading RFB protocol version"); + + socketConnection.readBytes(verStr, 12); + + if ((new String(verStr.array())).matches("RFB \\d{3}\\.\\d{3}\\n")) { + majorVersion = Integer.parseInt((new String(verStr.array())).substring(4,7)); + minorVersion = Integer.parseInt((new String(verStr.array())).substring(8,11)); + } else { + throw new CloudRuntimeException("Reading version failed: not an RFB server?"); + } + + s_logger.info("Server supports RFB protocol version " + majorVersion + "." + minorVersion); + + verStr.clear(); + verStr.put(String.format("RFB %03d.%03d\n", majorVersion, minorVersion).getBytes()).flip(); + + s_logger.info("Using RFB protocol version " + majorVersion + "." + minorVersion); + setWaitForNoVnc(true); + return verStr; + } + + /** + * Once the protocol version has been decided, the server and client must agree on the type + * of security to be used on the connection. + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security + */ + public int handshakeSecurityType() { + while (isWaitForNoVnc()) { + s_logger.trace("Waiting for noVNC msg received"); + } + s_logger.debug("Processing security types message"); + + int secType = RfbConstants.CONNECTION_FAILED; + + List secTypes = Arrays.asList(1, 2, 19, 261); + + while (!socketConnection.checkIfBytesAreAvailableForReading(1)) { + s_logger.trace("Waiting for inStream to be ready"); + } + int nServerSecTypes = socketConnection.readUnsignedInteger(8); + if (nServerSecTypes == 0) { + throw new CloudRuntimeException("No security types provided by the server"); + } + + Iterator j; + for (int i = 0; i < nServerSecTypes; i++) { + int serverSecType = socketConnection.readUnsignedInteger(8); + s_logger.debug("Server offers security type " + serverSecType); + + /* + * Use the first type sent by server which matches client's type. + * It means server's order specifies priority. + */ + if (secType == RfbConstants.CONNECTION_FAILED) { + for (j = secTypes.iterator(); j.hasNext(); ) { + int refType = (Integer) j.next(); + if (refType == serverSecType) { + secType = refType; + break; + } + } + } + } + this.flushAfterReceivingNoVNCData = true; + setWaitForNoVnc(true); + return secType; + } + + private final Object lock = new Object(); + public void setWaitForNoVnc(boolean val) { + synchronized (lock) { + this.waitForNoVnc = val; + } + } + + public boolean isWaitForNoVnc() { + synchronized (lock) { + return this.waitForNoVnc; + } + } + + private boolean waitForNoVnc = false; + + public void processSecurityResultMsg(int secType) { + s_logger.info("Processing security result message"); + int result; + + if (secType == RfbConstants.NO_AUTH) { + result = RfbConstants.VNC_AUTH_OK; } else { - os.write(b); + while (!socketConnection.checkIfBytesAreAvailableForReading(1)) { + s_logger.trace("Waiting for inStream"); + } + result = socketConnection.readUnsignedInteger(32); + } + + switch (result) { + case RfbConstants.VNC_AUTH_OK: + s_logger.info("Security completed"); + handleSecurityCompleted(); + return; + case RfbConstants.VNC_AUTH_FAILED: + s_logger.debug("auth failed"); + break; + case RfbConstants.VNC_AUTH_TOO_MANY: + s_logger.debug("auth failed - too many tries"); + break; + default: + throw new CloudRuntimeException("Unknown security result from server"); } + String reason = socketConnection.readString(); + throw new CloudRuntimeException(reason); + } + + private void handleSecurityCompleted() { + s_logger.info("Security completed"); + this.securityPhaseCompleted = true; + } + + public byte[] readServerInit() { + return socketConnection.readServerInit(); + } + + public int getNextBytes() { + return socketConnection.readNextBytes(); + } + + public boolean isTLSConnectionEstablished() { + return socketConnection.isTLSConnection(); + } + + public void readBytes(byte[] arr, int len) { + socketConnection.readNextByteArray(arr, len); } + public void processHandshakeSecurityType(int secType, String vmPassword, String host, int port) { + while (isWaitForNoVnc()) { + s_logger.trace("Waiting for noVNC msg received"); + } + + try { + List vncSecurityStack = getVncSecurityStack(secType, vmPassword, host, port); + for (VncSecurity security : vncSecurityStack) { + security.process(this.socketConnection); + if (security instanceof VncTLSSecurity) { + s_logger.debug("Setting new streams with SSLEngineManger after TLS security has passed"); + SSLEngineManager sslEngineManager = ((VncTLSSecurity) security).getSSLEngineManager(); + socketConnection.startTLSConnection(sslEngineManager); + } + } + } catch (IOException e) { + s_logger.error("Error processing handshake security type " + secType, e); + } + + processSecurityResultMsg(secType); + } } \ No newline at end of file diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/RfbConstants.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/RfbConstants.java index 7bf4f8dbd7e1..3e44ce7d1a64 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/RfbConstants.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/RfbConstants.java @@ -22,7 +22,7 @@ public interface RfbConstants { public static final String RFB_PROTOCOL_VERSION_MAJOR = "RFB 003."; // public static final String VNC_PROTOCOL_VERSION_MINOR = "003"; - public static final String VNC_PROTOCOL_VERSION_MINOR = "003"; + public static final String VNC_PROTOCOL_VERSION_MINOR = "008"; public static final String RFB_PROTOCOL_VERSION = RFB_PROTOCOL_VERSION_MAJOR + VNC_PROTOCOL_VERSION_MINOR; /** @@ -39,7 +39,8 @@ public interface RfbConstants { /** * Server authorization type */ - public final static int CONNECTION_FAILED = 0, NO_AUTH = 1, VNC_AUTH = 2; + public final static int CONNECTION_FAILED = 0, NO_AUTH = 1, VNC_AUTH = 2, + V_ENCRYPT = 19, V_ENCRYPT_X509_VNC = 261; /** * Server authorization reply. diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java new file mode 100644 index 000000000000..59a4d988def0 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java @@ -0,0 +1,123 @@ +// 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.network; + +import org.apache.log4j.Logger; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Set; + +public class NioSocket { + + private SocketChannel socketChannel; + private Selector writeSelector; + private Selector readSelector; + + private static final Logger s_logger = Logger.getLogger(NioSocket.class); + + private void initializeSocket() { + try { + socketChannel = SocketChannel.open(); + socketChannel.configureBlocking(false); + socketChannel.socket().setSoTimeout(5000); + writeSelector = Selector.open(); + readSelector = Selector.open(); + socketChannel.register(writeSelector, SelectionKey.OP_WRITE); + socketChannel.register(readSelector, SelectionKey.OP_READ); + } catch (IOException e) { + s_logger.error("Could not initialize NioSocket: " + e.getMessage(), e); + } + } + + private void connectSocket(String host, int port) { + try { + socketChannel.connect(new InetSocketAddress(host, port)); + Selector selector = Selector.open(); + socketChannel.register(selector, SelectionKey.OP_CONNECT); + boolean connected = false; + while (selector.select(3000) > 0) { + while (!connected) { + Set keys = selector.selectedKeys(); + Iterator i = keys.iterator(); + + while (i.hasNext()) { + SelectionKey key = (SelectionKey)i.next(); + + // Remove the current key + i.remove(); + + // Attempt a connection + if (key.isConnectable()) { + if (socketChannel.isConnectionPending()) { + socketChannel.finishConnect(); + } + connected = true; + } + } + } + } + socketChannel.socket().setTcpNoDelay(false); + } catch (IOException e) { + s_logger.error(String.format("Error creating NioSocket to %s:%s: %s", host, port, e.getMessage()), e); + } + } + + public NioSocket(String host, int port) { + initializeSocket(); + connectSocket(host, port); + } + + protected int select(boolean read, Integer timeout) { + try { + Selector selector = read ? readSelector : writeSelector; + selector.selectedKeys().clear(); + return timeout == null ? selector.select() : selector.selectNow(); + } catch (IOException e) { + s_logger.error(String.format("Error obtaining %s select: %s", read ? "read" : "write", e.getMessage()), e); + return -1; + } + } + + protected int readFromSocketChannel(ByteBuffer readBuffer, int len) { + try { + int readBytes = socketChannel.read(readBuffer.slice().limit(len)); + int position = readBuffer.position(); + readBuffer.position(position + readBytes); + return Math.max(readBytes, 0); + } catch (Exception e) { + s_logger.error("Error reading from socket channel: " + e.getMessage(), e); + return 0; + } + } + + protected int writeToSocketChannel(ByteBuffer buf, int len) { + try { + int writtenBytes = socketChannel.write(buf.slice().limit(len)); + buf.position(buf.position() + writtenBytes); + return writtenBytes; + } catch (java.io.IOException e) { + s_logger.error("Error writing bytes to socket channel: " + e.getMessage(), e); + return 0; + } + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java new file mode 100644 index 000000000000..306120f9da05 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java @@ -0,0 +1,45 @@ +// 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.network; + +import java.nio.ByteBuffer; + +public interface NioSocketHandler { + + // Getters + NioSocketInputStream getInputStream(); + NioSocketOutputStream getOutputStream(); + + // Read operations + int readUnsignedInteger(int sizeInBits); + void readBytes(ByteBuffer data, int length); + String readString(); + byte[] readServerInit(); + int readNextBytes(); + void readNextByteArray(byte[] arr, int len); + + // Write operations + void writeUnsignedInteger(int sizeInBits, int value); + void writeBytes(byte[] data, int dataPtr, int length); + void writeBytes(ByteBuffer data, int length); + + // Additional operations + boolean checkIfBytesAreAvailableForReading(int bytes); + void flushWriteBuffer(); + void startTLSConnection(SSLEngineManager sslEngineManager); + boolean isTLSConnection(); +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java new file mode 100644 index 000000000000..44b16e53142a --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java @@ -0,0 +1,113 @@ +// 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.network; + +import org.apache.log4j.Logger; + +import java.nio.ByteBuffer; + +public class NioSocketHandlerImpl implements NioSocketHandler { + + private NioSocketInputStream inputStream; + private NioSocketOutputStream outputStream; + private boolean isTLS = false; + + private static final int DEFAULT_BUF_SIZE = 16384; + private static final Logger s_logger = Logger.getLogger(NioSocketHandlerImpl.class); + + public NioSocketHandlerImpl(NioSocket socket) { + this.inputStream = new NioSocketInputStream(DEFAULT_BUF_SIZE, socket); + this.outputStream = new NioSocketOutputStream(DEFAULT_BUF_SIZE, socket); + } + + @Override + public int readUnsignedInteger(int sizeInBits) { + return inputStream.readUnsignedInteger(sizeInBits); + } + + @Override + public void writeUnsignedInteger(int sizeInBits, int value) { + outputStream.writeUnsignedInteger(sizeInBits, value); + } + + @Override + public void readBytes(ByteBuffer data, int length) { + inputStream.readBytes(data, length); + } + + @Override + public boolean checkIfBytesAreAvailableForReading(int bytes) { + return inputStream.checkForSizeWithoutWait(bytes); + } + + @Override + public void writeBytes(byte[] data, int dataPtr, int length) { + outputStream.writeBytes(data, dataPtr, length); + } + + @Override + public void writeBytes(ByteBuffer data, int length) { + outputStream.writeBytes(data, length); + } + + @Override + public void flushWriteBuffer() { + outputStream.flushWriteBuffer(); + } + + @Override + public void startTLSConnection(SSLEngineManager sslEngineManager) { + this.inputStream = new NioSocketTLSInputStream(sslEngineManager, this.inputStream.socket); + this.outputStream = new NioSocketTLSOutputStream(sslEngineManager, this.outputStream.socket); + this.isTLS = true; + } + + @Override + public boolean isTLSConnection() { + return this.isTLS; + } + + @Override + public String readString() { + return inputStream.readString(); + } + + @Override + public byte[] readServerInit() { + return inputStream.readServerInit(); + } + + @Override + public int readNextBytes() { + return inputStream.getNextBytes(); + } + + @Override + public void readNextByteArray(byte[] arr, int len) { + inputStream.readNextByteArrayFromReadBuffer(arr, len); + } + + @Override + public NioSocketInputStream getInputStream() { + return inputStream; + } + + @Override + public NioSocketOutputStream getOutputStream() { + return outputStream; + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java new file mode 100644 index 000000000000..d6151a8b2ae3 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java @@ -0,0 +1,210 @@ +// 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.network; + +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.ArrayUtils; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class NioSocketInputStream extends NioSocketStream { + + public NioSocketInputStream(int bufferSize, NioSocket socket) { + super(bufferSize, socket); + } + + public int getReadBytesAvailableToFitSize(int itemSize, int numberItems, boolean wait) { + int window = endPosition - currentPosition; + if (itemSize > window) { + return rearrangeBufferToFitSize(numberItems, itemSize, wait); + } + return Math.min(window / itemSize, numberItems); + } + + protected void moveDataToBufferStart() { + if (endPosition - currentPosition != 0) { + System.arraycopy(buffer, currentPosition, buffer, 0, endPosition - currentPosition); + } + offset += currentPosition; + endPosition -= currentPosition; + currentPosition = 0; + } + + protected boolean canUseReadSelector(boolean wait) { + int n = -1; + Integer timeout = !wait ? 0 : null; + while (n < 0) { + n = socket.select(true, timeout); + } + return n > 0 || wait; + } + + protected int readBytesToBuffer(ByteBuffer buf, int bytesToRead, boolean wait) { + if (!canUseReadSelector(wait)) { + return 0; + } + int readBytes = socket.readFromSocketChannel(buf, bytesToRead); + if (readBytes == 0) { + throw new CloudRuntimeException("End of stream exception"); + } + return readBytes; + } + + protected int rearrangeBufferToFitSize(int numberItems, int itemSize, boolean wait) { + if (itemSize > buffer.length) { + throw new CloudRuntimeException(String.format("The requested item size %s exceeds the buffer size %s", + itemSize, buffer.length)); + } + + moveDataToBufferStart(); + + while (endPosition < itemSize) { + int remainingBufferSize = buffer.length - endPosition; + int desiredCapacity = itemSize * numberItems; + int bytesToRead = Math.min(remainingBufferSize, Math.max(desiredCapacity, 8)); + + ByteBuffer buf = ByteBuffer.wrap(buffer).position(endPosition); + int n = readBytesToBuffer(buf, bytesToRead, wait); + if (n == 0) { + return 0; + } + endPosition += n; + } + + int window = endPosition - currentPosition; + return Math.min(window / itemSize, numberItems); + } + + protected Pair readAndCopyUnsignedInteger(int sizeInBits) { + checkUnsignedIntegerSize(sizeInBits); + int bytes = sizeInBits / 8; + getReadBytesAvailableToFitSize(bytes, 1, true); + byte[] unsignedIntegerArray = Arrays.copyOfRange(buffer, currentPosition, currentPosition + bytes); + currentPosition += bytes; + return new Pair<>(convertByteArrayToUnsignedInteger(unsignedIntegerArray), unsignedIntegerArray); + } + + protected int readUnsignedInteger(int sizeInBits) { + Pair pair = readAndCopyUnsignedInteger(sizeInBits); + return pair.first(); + } + + protected void readBytes(ByteBuffer data, int length) { + while (length > 0) { + int n = getReadBytesAvailableToFitSize(1, length, true); + data.put(buffer, currentPosition, n); + currentPosition += n; + length -= n; + } + } + + public boolean checkForSizeWithoutWait(int size) { + return getReadBytesAvailableToFitSize(size, 1, false) != 0; + } + + protected final String readString() { + int len = readUnsignedInteger(32); + + ByteBuffer str = ByteBuffer.allocate(len); + readBytes(str, len); + String utf8string = new String(); + try { + utf8string = new String(str.array(),"UTF8"); + } catch(java.io.UnsupportedEncodingException e) { + e.printStackTrace(); + } + return utf8string; + } + + /** + * Read ServerInit message and return it as a byte[] for noVNC + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#732serverinit + */ + public byte[] readServerInit() { + // Read width, height, pixel format and VM name + byte[] bytesRead = new byte[] {}; + Pair widthPair = readAndCopyUnsignedInteger(16); + bytesRead = ArrayUtils.addAll(bytesRead, widthPair.second()); + Pair heightPair = readAndCopyUnsignedInteger(16); + bytesRead = ArrayUtils.addAll(bytesRead, heightPair.second()); + + byte[] pixelFormatByteArr = readPixelFormat(); + bytesRead = ArrayUtils.addAll(bytesRead, pixelFormatByteArr); + + Pair pair = readAndCopyUnsignedInteger(32); + int len = pair.first(); + bytesRead = ArrayUtils.addAll(bytesRead, pair.second()); + + ByteBuffer str = ByteBuffer.allocate(len); + readBytes(str, len); + return ArrayUtils.addAll(bytesRead, str.array()); + } + + protected final void skipReadBytes(int bytes) { + while (bytes > 0) { + int n = getReadBytesAvailableToFitSize(1, bytes, true); + currentPosition += n; + bytes -= n; + } + } + + /** + * Read PixelFormat and return it as byte[] + */ + private byte[] readPixelFormat() { + Pair bppPair = readAndCopyUnsignedInteger(8); + byte[] ret = bppPair.second(); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(16).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(16).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(16).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); + ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); + skipReadBytes(3); + return ArrayUtils.addAll(ret, new byte[] {0, 0, 0}); + } + + protected int getNextBytes() { + int size = 200; + while (size > 0) { + if (checkForSizeWithoutWait(size)) { + break; + } + size--; + } + return size; + } + + protected void readNextByteArrayFromReadBuffer(byte[] arr, int len) { + copyBytesFromReadBuffer(len, arr); + } + + protected void copyBytesFromReadBuffer(int length, byte[] arr) { + int ptr = 0; + while (length > 0) { + int n = getReadBytesAvailableToFitSize(1, length, true); + readBytes(ByteBuffer.wrap(arr, ptr, n), n); + ptr += n; + length -= n; + } + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java new file mode 100644 index 000000000000..4410d6172934 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java @@ -0,0 +1,121 @@ +// 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.network; + +import com.cloud.utils.exception.CloudRuntimeException; + +import java.nio.ByteBuffer; + +public class NioSocketOutputStream extends NioSocketStream { + + private int sendPosition; + private boolean block; + + public NioSocketOutputStream(int bufferSize, NioSocket socket) { + super(bufferSize, socket); + this.endPosition = bufferSize; + this.sendPosition = 0; + this.block = true; + } + + protected final int checkWriteBufferForSizeAndItemsNumber(int itemSize, int nItems) { + if (itemSize > (endPosition - currentPosition)) { + return rearrangeWriteBuffer(itemSize, nItems); + } + + int window = endPosition - currentPosition; + return Math.min(window / itemSize, nItems); + + } + + public final void checkWriteBufferForSize(int itemSize) { + if (itemSize > endPosition - currentPosition) { + rearrangeWriteBuffer(itemSize, 1); + } + } + + public void flushWriteBuffer() { + while (sendPosition < currentPosition) { + int writtenBytes = writeFromWriteBuffer(buffer, sendPosition, currentPosition - sendPosition); + + if (writtenBytes == 0) { + throw new CloudRuntimeException("Timeout exception"); + } + + sendPosition += writtenBytes; + offset += writtenBytes; + } + + if (sendPosition == currentPosition) { + sendPosition = start; + currentPosition = start; + } + } + + protected boolean canUseWriteSelector() { + int n = -1; + while (n < 0) { + n = socket.select(false, null); + } + return n > 0; + } + + private int writeFromWriteBuffer(byte[] data, int dataPtr, int length) { + if (!canUseWriteSelector()) { + return 0; + } + return socket.writeToSocketChannel(ByteBuffer.wrap(data, dataPtr, length), length); + } + + protected int rearrangeWriteBuffer(int itemSize, int numberItems) { + if (itemSize > this.buffer.length) { + throw new CloudRuntimeException("Cannot write item longer than the buffer size"); + } + + flushWriteBuffer(); + + int window = endPosition - currentPosition; + return Math.min(window / itemSize, numberItems); + + } + + protected void writeUnsignedInteger(int sizeInBits, int value) { + checkUnsignedIntegerSize(sizeInBits); + int bytes = sizeInBits / 8; + checkWriteBufferForSize(bytes); + placeUnsignedIntegerToBuffer(bytes, value); + } + + protected void writeBytes(byte[] data, int dataPtr, int length) { + int dataEnd = dataPtr + length; + while (dataPtr < dataEnd) { + int n = checkWriteBufferForSizeAndItemsNumber(1, dataEnd - dataPtr); + System.arraycopy(data, dataPtr, buffer, currentPosition, n); + currentPosition += n; + dataPtr += n; + } + } + + protected void writeBytes(ByteBuffer data, int length) { + while (length > 0) { + int n = checkWriteBufferForSizeAndItemsNumber(1, length); + data.get(buffer, currentPosition, n); + currentPosition += n; + length -= n; + } + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java new file mode 100644 index 000000000000..f02e2c23a66d --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java @@ -0,0 +1,81 @@ +// 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.network; + +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.log4j.Logger; + +public class NioSocketStream { + + protected byte[] buffer; + protected int currentPosition; + protected int offset; + protected int endPosition; + protected int start; + protected NioSocket socket; + + private static final Logger s_logger = Logger.getLogger(NioSocketStream.class); + + public NioSocketStream(int bufferSize, NioSocket socket) { + this.buffer = new byte[bufferSize]; + this.currentPosition = 0; + this.offset = 0; + this.endPosition = 0; + this.start = 0; + this.socket = socket; + } + + protected boolean isUnsignedIntegerSizeAllowed(int sizeInBits) { + return sizeInBits % 8 == 0 && sizeInBits > 0 && sizeInBits <= 32; + } + + protected void checkUnsignedIntegerSize(int sizeInBits) { + if (!isUnsignedIntegerSizeAllowed(sizeInBits)) { + String msg = "Unsupported size in bits for unsigned integer reading " + sizeInBits; + s_logger.error(msg); + throw new CloudRuntimeException(msg); + } + } + + protected int convertByteArrayToUnsignedInteger(byte[] readBytes) { + if (readBytes.length == 1) { + return readBytes[0] & 0xff; + } else if (readBytes.length == 2) { + int signed = readBytes[0] << 8 | readBytes[1] & 0xff; + return signed & 0xffff; + } else if (readBytes.length == 4) { + return (readBytes[0] << 24) | (readBytes[1] & 0xff) << 16 | + (readBytes[2] & 0xff) << 8 | (readBytes[3] & 0xff); + } else { + throw new CloudRuntimeException("Error reading unsigned integer from socket stream"); + } + } + + protected void placeUnsignedIntegerToBuffer(int bytes, int value) { + if (bytes == 1) { + buffer[currentPosition++] = (byte) value; + } else if (bytes == 2) { + buffer[currentPosition++] = (byte) (value >> 8); + buffer[currentPosition++] = (byte) value; + } else if (bytes == 4) { + buffer[currentPosition++] = (byte) (value >> 24); + buffer[currentPosition++] = (byte) (value >> 16); + buffer[currentPosition++] = (byte) (value >> 8); + buffer[currentPosition++] = (byte) value; + } + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java new file mode 100644 index 000000000000..0739ab016456 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java @@ -0,0 +1,68 @@ +// 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.network; + +import com.cloud.utils.exception.CloudRuntimeException; + +import java.nio.ByteBuffer; + +public class NioSocketTLSInputStream extends NioSocketInputStream { + + private final SSLEngineManager sslEngineManager; + + public NioSocketTLSInputStream(SSLEngineManager sslEngineManager, NioSocket socket) { + super(sslEngineManager.getSession().getApplicationBufferSize(), socket); + this.sslEngineManager = sslEngineManager; + } + + protected int readTLS(byte[] buf, int bufPtr, int len) { + int n = -1; + try { + n = sslEngineManager.read(ByteBuffer.wrap(buf, bufPtr, len), len); + } catch (java.io.IOException e) { + e.printStackTrace(); + } + if (n < 0) throw new CloudRuntimeException("readTLS" + n); + return n; + } + + @Override + protected int rearrangeBufferToFitSize(int numberItems, int itemSize, boolean wait) { + if (itemSize > buffer.length) { + throw new CloudRuntimeException("Cannot read item longer than the buffer size"); + } + + if (endPosition - currentPosition != 0) { + System.arraycopy(buffer, currentPosition, buffer, 0, endPosition - currentPosition); + } + + offset += currentPosition - start; + endPosition -= currentPosition - start; + currentPosition = start; + + while ((endPosition - start) < itemSize) { + int n = readTLS(buffer, endPosition, start + buffer.length - endPosition); + if (!wait && n == 0) { + return 0; + } + endPosition += n; + } + + int window = endPosition - currentPosition; + return Math.min(window / itemSize, numberItems); + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java new file mode 100644 index 000000000000..45eac7c7a684 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java @@ -0,0 +1,65 @@ +// 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.network; + +import com.cloud.utils.exception.CloudRuntimeException; + +import java.nio.ByteBuffer; + +public class NioSocketTLSOutputStream extends NioSocketOutputStream { + + private final SSLEngineManager sslEngineManager; + + public NioSocketTLSOutputStream(SSLEngineManager sslEngineManager, NioSocket socket) { + super(sslEngineManager.getSession().getApplicationBufferSize(), socket); + this.sslEngineManager = sslEngineManager; + } + + @Override + public void flushWriteBuffer() { + int sentUpTo = start; + while (sentUpTo < currentPosition) { + int n = writeTLS(buffer, sentUpTo, currentPosition - sentUpTo); + sentUpTo += n; + offset += n; + } + + currentPosition = start; + } + + protected int writeTLS(byte[] data, int dataPtr, int length) { + int n; + try { + n = sslEngineManager.write(ByteBuffer.wrap(data, dataPtr, length), length); + } catch (java.io.IOException e) { + throw new CloudRuntimeException(e.getMessage()); + } + return n; + } + + @Override + protected int rearrangeWriteBuffer(int itemSize, int numberItems) { + if (itemSize > buffer.length) + throw new CloudRuntimeException("TLSOutStream overrun: max itemSize exceeded"); + + flushWriteBuffer(); + + int window = endPosition - currentPosition; + return Math.min(window / itemSize, numberItems); + + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java new file mode 100644 index 000000000000..9326c882cbf7 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java @@ -0,0 +1,180 @@ +// 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.network; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class SSLEngineManager { + + private SSLEngine engine = null; + + private ByteBuffer myNetData; + private ByteBuffer peerNetData; + + private Executor executor; + private NioSocketInputStream inputStream; + private NioSocketOutputStream outputStream; + + public SSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) throws IOException { + this.inputStream = socket.getInputStream(); + this.outputStream = socket.getOutputStream(); + engine = sslEngine; + + executor = Executors.newSingleThreadExecutor(); + + int pktBufSize = engine.getSession().getPacketBufferSize(); + myNetData = ByteBuffer.allocate(pktBufSize); + peerNetData = ByteBuffer.allocate(pktBufSize); + } + + public void doHandshake() throws Exception { + + // Begin handshake + engine.beginHandshake(); + SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus(); + + // Process handshaking message + SSLEngineResult res = null; + int appBufSize = engine.getSession().getApplicationBufferSize(); + ByteBuffer peerAppData = ByteBuffer.allocate(appBufSize); + ByteBuffer myAppData = ByteBuffer.allocate(appBufSize); + while (hs != SSLEngineResult.HandshakeStatus.FINISHED && + hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + + switch (hs) { + + case NEED_UNWRAP: + // Receive handshaking data from peer + peerNetData.flip(); + res = engine.unwrap(peerNetData, peerAppData); + peerNetData.compact(); + hs = res.getHandshakeStatus(); + + // Check status + switch (res.getStatus()) { + case BUFFER_UNDERFLOW: + int avail = inputStream.getReadBytesAvailableToFitSize(1, peerNetData.remaining(), + false); + inputStream.readBytes(peerNetData, avail); + break; + case OK: + // Process incoming handshaking data + break; + case CLOSED: + engine.closeInbound(); + break; + } + break; + + case NEED_WRAP: + // Generate handshaking data + res = engine.wrap(myAppData, myNetData); + hs = res.getHandshakeStatus(); + + // Check status + switch (res.getStatus()) { + case OK: + myNetData.flip(); + outputStream.writeBytes(myNetData, myNetData.remaining()); + outputStream.flushWriteBuffer(); + myNetData.compact(); + break; + case CLOSED: + engine.closeOutbound(); + break; + } + break; + + case NEED_TASK: + // Handle blocking tasks + executeTasks(); + break; + } + hs = engine.getHandshakeStatus(); + } + } + + private void executeTasks() { + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + executor.execute(task); + } + } + + public int read(ByteBuffer data, int length) throws IOException { + // Read SSL/TLS encoded data from peer + peerNetData.flip(); + SSLEngineResult res = engine.unwrap(peerNetData, data); + peerNetData.compact(); + switch (res.getStatus()) { + case OK : + return res.bytesProduced(); + + case BUFFER_UNDERFLOW: + // attempt to drain the underlying buffer first + int need = peerNetData.remaining(); + int available = inputStream.getReadBytesAvailableToFitSize(1, need, false); + inputStream.readBytes(peerNetData, available); + break; + + case CLOSED: + engine.closeInbound(); + break; + + } + return 0; + } + + public int write(ByteBuffer data, int length) throws IOException { + int n = 0; + while (data.hasRemaining()) { + SSLEngineResult res = engine.wrap(data, myNetData); + n += res.bytesConsumed(); + switch (res.getStatus()) { + case OK: + myNetData.flip(); + outputStream.writeBytes(myNetData, myNetData.remaining()); + outputStream.flushWriteBuffer(); + myNetData.compact(); + break; + + case BUFFER_OVERFLOW: + // Make room in the buffer by flushing the outstream + myNetData.flip(); + outputStream.writeBytes(myNetData, myNetData.remaining()); + myNetData.compact(); + break; + + case CLOSED: + engine.closeOutbound(); + break; + } + } + return n; + } + + public SSLSession getSession() { + return engine.getSession(); + } + +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/NoneVncSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/NoneVncSecurity.java new file mode 100644 index 000000000000..f9b088d1b9ec --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/NoneVncSecurity.java @@ -0,0 +1,27 @@ +// 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.security; + +import com.cloud.consoleproxy.vnc.network.NioSocketHandler; + +public class NoneVncSecurity implements VncSecurity { + + @Override + public void process(NioSocketHandler socketHandler) { + // No auth required + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java new file mode 100644 index 000000000000..5272e5660f7c --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java @@ -0,0 +1,58 @@ +// 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.security; + +import com.cloud.consoleproxy.util.Logger; +import com.cloud.consoleproxy.vnc.NoVncClient; +import com.cloud.consoleproxy.vnc.network.NioSocketHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class VncAuthSecurity implements VncSecurity { + + private final String vmPass; + + private static final int vncAuthChallengeSize = 16; + private static final Logger s_logger = Logger.getLogger(VncAuthSecurity.class); + + public VncAuthSecurity(String vmPass) { + this.vmPass = vmPass; + } + + @Override + public void process(NioSocketHandler socketHandler) throws IOException { + s_logger.info("VNC server requires password authentication"); + + // Read the challenge & obtain the user's password + ByteBuffer challenge = ByteBuffer.allocate(vncAuthChallengeSize); + socketHandler.readBytes(challenge, vncAuthChallengeSize); + + byte[] encodedPassword; + try { + encodedPassword = NoVncClient.encodePassword(challenge.array(), vmPass); + } 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()); + } + + // Return the response to the server + socketHandler.writeBytes(ByteBuffer.wrap(encodedPassword), encodedPassword.length); + socketHandler.flushWriteBuffer(); + s_logger.info("Finished VNCAuth security"); + } +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java new file mode 100644 index 000000000000..7154aa52f3fc --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java @@ -0,0 +1,46 @@ +// 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.security; + +import com.cloud.consoleproxy.vnc.RfbConstants; +import com.cloud.consoleproxy.vnc.network.NioSocketHandler; +import com.cloud.utils.exception.CloudRuntimeException; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public interface VncSecurity { + + static List getSecurityStack(int securityType, String vmPassword, String host, int port) { + switch (securityType) { + case RfbConstants.NO_AUTH: return Collections.singletonList(new NoneVncSecurity()); + case RfbConstants.VNC_AUTH: return Collections.singletonList(new VncAuthSecurity(vmPassword)); + + // Do not add VCentrypt type = 19 but its supported subtypes + case RfbConstants.V_ENCRYPT_X509_VNC: + return Arrays.asList(new VncTLSSecurity(host, port), new VncAuthSecurity(vmPassword)); + default: throw new CloudRuntimeException("Unsupported security type " + securityType); + } + } + + List supportedSecurityTypes = Arrays.asList(RfbConstants.NO_AUTH, RfbConstants.VNC_AUTH, + RfbConstants.V_ENCRYPT, RfbConstants.V_ENCRYPT_X509_VNC); + + void process(NioSocketHandler socketHandler) throws IOException; +} diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java new file mode 100644 index 000000000000..ea5d14d33204 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java @@ -0,0 +1,116 @@ +// 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.security; + +import com.cloud.consoleproxy.util.Logger; +import com.cloud.consoleproxy.vnc.RfbConstants; +import com.cloud.consoleproxy.vnc.network.NioSocketHandler; +import com.cloud.consoleproxy.vnc.network.SSLEngineManager; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.nio.Link; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.ArrayList; + +public class VncTLSSecurity implements VncSecurity { + + private static final Logger s_logger = Logger.getLogger(VncTLSSecurity.class); + + private SSLContext ctx; + private SSLEngine engine; + private SSLEngineManager manager; + + private boolean anon; + private final String host; + private final int port; + + public VncTLSSecurity(String host, int port) { + this.host = host; + this.port = port; + this.anon = false; + } + + private void initGlobal() { + try { + ctx = Link.initClientSSLContext(); + } catch (GeneralSecurityException | IOException e) { + throw new CloudRuntimeException("Unable to initialize SSL context", e); + } + } + + private void setParam() { + engine = ctx.createSSLEngine(this.host, this.port); + engine.setUseClientMode(true); + + String[] supported = engine.getSupportedProtocols(); + ArrayList enabled = new ArrayList(); + for (int i = 0; i < supported.length; i++) + if (supported[i].matches("TLS.*")) + enabled.add(supported[i]); + engine.setEnabledProtocols(enabled.toArray(new String[0])); + + if (anon) { + supported = engine.getSupportedCipherSuites(); + enabled = new ArrayList(); + // prefer ECDH over DHE + for (int i = 0; i < supported.length; i++) + if (supported[i].matches("TLS_ECDH_anon.*")) + enabled.add(supported[i]); + for (int i = 0; i < supported.length; i++) + if (supported[i].matches("TLS_DH_anon.*")) + enabled.add(supported[i]); + engine.setEnabledCipherSuites(enabled.toArray(new String[0])); + } else { + engine.setEnabledCipherSuites(engine.getSupportedCipherSuites()); + } + } + + @Override + public void process(NioSocketHandler socketHandler) { + s_logger.info("Processing VNC TLS security"); + + initGlobal(); + + if (manager == null) { + if (socketHandler.readUnsignedInteger(8) == 0) { + int result = socketHandler.readUnsignedInteger(32); + String reason; + if (result == RfbConstants.VNC_AUTH_FAILED || result == RfbConstants.VNC_AUTH_TOO_MANY) { + reason = socketHandler.readString(); + } else { + reason = "Authentication failure (protocol error)"; + } + throw new CloudRuntimeException(reason); + } + setParam(); + } + + try { + manager = new SSLEngineManager(engine, socketHandler); + manager.doHandshake(); + } catch(java.lang.Exception e) { + throw new CloudRuntimeException(e.toString()); + } + } + + public SSLEngineManager getSSLEngineManager() { + return manager; + } +} diff --git a/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java new file mode 100644 index 000000000000..ee4199b4f5d9 --- /dev/null +++ b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java @@ -0,0 +1,16 @@ +package com.cloud.consoleproxy; + +import org.junit.Assert; +import org.junit.Test; + +public class ConsoleProxyNoVncClientTest { + @Test + public void rewriteServerNameInServerInitTest() { + String serverName = "server123, backend:TLS"; + byte[] serverInitTestBytes = new byte[]{ 4, 0, 3, 0, 32, 24, 0, 1, 0, -1, 0, -1, 0, -1, 16, 8, 0, 0, 0, 0, 0, 0, 0, 15, 81, 69, 77, 85, 32, 40, 105, 45, 50, 45, 56, 45, 86, 77, 41}; + byte[] newServerInit = ConsoleProxyNoVncClient.rewriteServerNameInServerInit(serverInitTestBytes, serverName); + + byte[] expectedBytes = new byte[]{4, 0, 3, 0, 32, 24, 0, 1, 0, -1, 0, -1, 0, -1, 16, 8, 0, 0, 0, 0, 0, 0, 0, 22, 115, 101, 114, 118, 101, 114, 49, 50, 51, 44, 32, 98, 97, 99, 107, 101, 110, 100, 58, 84, 76, 83}; + Assert.assertArrayEquals(newServerInit, expectedBytes); + } +} \ No newline at end of file diff --git a/systemvm/agent/noVNC/app/styles/base.css b/systemvm/agent/noVNC/app/styles/base.css index c7893b3205c5..ca236a9e1126 100644 --- a/systemvm/agent/noVNC/app/styles/base.css +++ b/systemvm/agent/noVNC/app/styles/base.css @@ -771,6 +771,12 @@ select:active { #noVNC_status.noVNC_status_warn::before { content: url("../images/warning.svg") " "; } +#noVNC_status.noVNC_status_tls_success { + background: rgba(6, 199, 38, 0.9); +} +#noVNC_status.noVNC_status_tls_success::before { + content: url("../images/connect.svg") " "; +} /* ---------------------------------------- * Connect Dialog diff --git a/systemvm/agent/noVNC/app/ui.js b/systemvm/agent/noVNC/app/ui.js index f503f55c6c9f..2ebc9bd8b212 100644 --- a/systemvm/agent/noVNC/app/ui.js +++ b/systemvm/agent/noVNC/app/ui.js @@ -455,6 +455,9 @@ const UI = { if (typeof statusType === 'undefined') { statusType = 'normal'; } + if (UI.getSetting('encrypt')) { + statusType = 'encrypted'; + } // Don't overwrite more severe visible statuses and never // errors. Only shows the first error. @@ -471,15 +474,23 @@ const UI = { clearTimeout(UI.statusTimeout); switch (statusType) { + case 'encrypted': + statusElem.classList.remove("noVNC_status_warn"); + statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.remove("noVNC_status_error"); + statusElem.classList.add("noVNC_status_tls_success"); + break; case 'error': statusElem.classList.remove("noVNC_status_warn"); statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.remove("noVNC_status_tls_success"); statusElem.classList.add("noVNC_status_error"); break; case 'warning': case 'warn': statusElem.classList.remove("noVNC_status_error"); statusElem.classList.remove("noVNC_status_normal"); + statusElem.classList.remove("noVNC_status_tls_success"); statusElem.classList.add("noVNC_status_warn"); break; case 'normal': @@ -487,6 +498,7 @@ const UI = { default: statusElem.classList.remove("noVNC_status_error"); statusElem.classList.remove("noVNC_status_warn"); + statusElem.classList.remove("noVNC_status_tls_success"); statusElem.classList.add("noVNC_status_normal"); break; } @@ -500,7 +512,7 @@ const UI = { } // Error messages do not timeout - if (statusType !== 'error') { + if (statusType !== 'error' && UI.getSetting('encrypt') === false) { UI.statusTimeout = window.setTimeout(UI.hideStatus, time); } }, @@ -1101,7 +1113,7 @@ const UI = { let msg; if (UI.getSetting('encrypt')) { - msg = _("Connected"); + msg = _("Connected (encrypted) to ") + UI.desktopName; } else { msg = _("Connected") } @@ -1662,6 +1674,10 @@ const UI = { UI.desktopName = e.detail.name; // Display the desktop name in the document title document.title = e.detail.name + " - " + PAGE_TITLE; + if (e.detail.name.includes('(TLS backend)')) { + UI.forceSetting('encrypt', true); + UI.enableSetting('encrypt'); + } }, bell(e) { diff --git a/systemvm/agent/noVNC/core/rfb.js b/systemvm/agent/noVNC/core/rfb.js index c38e8e5e6f11..056e55f439aa 100644 --- a/systemvm/agent/noVNC/core/rfb.js +++ b/systemvm/agent/noVNC/core/rfb.js @@ -1634,7 +1634,10 @@ export default class RFB extends EventTargetMixin { _negotiateAuthentication() { switch (this._rfbAuthScheme) { + // Let CloudStack handle the authentication (RFB 3.8 requires the client to select the auth scheme) case 1: // no auth + case 2: // VNC authentication + case 19: // VeNCrypt Security Type if (this._rfbVersion >= 3.8) { this._rfbInitState = 'SecurityResult'; return true; @@ -1645,15 +1648,9 @@ export default class RFB extends EventTargetMixin { case 22: // XVP auth return this._negotiateXvpAuth(); - case 2: // VNC authentication - return this._negotiateStdVNCAuth(); - case 16: // TightVNC Security Type return this._negotiateTightAuth(); - case 19: // VeNCrypt Security Type - return this._negotiateVeNCryptAuth(); - case 129: // TightVNC UNIX Security Type return this._negotiateTightUnixAuth(); From 697fd5abdcef82ddc50a776699931e9e06c7ba0f Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 22 Dec 2022 01:32:45 -0300 Subject: [PATCH 02/15] Add missing license header --- .../ConsoleProxyNoVncClientTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java index ee4199b4f5d9..92665945eba1 100644 --- a/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java +++ b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java @@ -1,3 +1,19 @@ +// 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; import org.junit.Assert; From 9d43897cff337ccf47175e7b02ea4f1d28d10a44 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 22 Dec 2022 01:57:17 -0300 Subject: [PATCH 03/15] Fix missing parameter --- .../cloudstack/consoleproxy/ConsoleAccessManagerImpl.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java index 17e79af224b6..e13e16a1bcec 100644 --- a/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManagerImpl.java @@ -287,7 +287,7 @@ private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMach String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid); ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword()); ConsoleProxyClientParam param = generateConsoleProxyClientParam(parsedHostInfo, port, sid, tag, ticket, - sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host); + sessionUuid, addr, extraSecurityToken, vm, hostVo, details, portInfo, host, displayName); String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param); int vncPort = consoleProxyManager.getVncPort(); @@ -341,7 +341,8 @@ private ConsoleProxyClientParam generateConsoleProxyClientParam(Ternary portInfo, String host) { + Pair portInfo, String host, + String displayName) { ConsoleProxyClientParam param = new ConsoleProxyClientParam(); param.setClientHostAddress(parsedHostInfo.first()); param.setClientHostPort(port); From 9e90aa3a794237ae8ed0b962390e9067c4cbd76f Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 22 Dec 2022 07:55:06 -0300 Subject: [PATCH 04/15] Remove unused variable from the TLS security --- .../consoleproxy/ConsoleProxyNoVncClient.java | 2 +- .../vnc/security/VncTLSSecurity.java | 27 +++++-------------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index 46ec7053e1fe..e9eccaeff0c3 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -189,7 +189,7 @@ private void authenticateToVNCServer() throws IOException { String serverName = String.format("%s %s", clientParam.getClientDisplayName(), client.isTLSConnectionEstablished() ? "(TLS backend)" : ""); byte[] bytesServerInit = rewriteServerNameInServerInit(client.readServerInit(), serverName); - s_logger.info(String.format("Server init message is %s (%s)", Arrays.toString(bytesServerInit), new String(bytesServerInit))); + s_logger.debug(String.format("Server init message is %s (%s)", Arrays.toString(bytesServerInit), new String(bytesServerInit))); session.getRemote().sendBytes(ByteBuffer.wrap(bytesServerInit)); client.setWaitForNoVnc(true); while (client.isWaitForNoVnc()) { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java index ea5d14d33204..ce5969de0ae6 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java @@ -37,14 +37,12 @@ public class VncTLSSecurity implements VncSecurity { private SSLEngine engine; private SSLEngineManager manager; - private boolean anon; private final String host; private final int port; public VncTLSSecurity(String host, int port) { this.host = host; this.port = port; - this.anon = false; } private void initGlobal() { @@ -60,26 +58,15 @@ private void setParam() { engine.setUseClientMode(true); String[] supported = engine.getSupportedProtocols(); - ArrayList enabled = new ArrayList(); - for (int i = 0; i < supported.length; i++) - if (supported[i].matches("TLS.*")) - enabled.add(supported[i]); + ArrayList enabled = new ArrayList<>(); + for (String s : supported) { + if (s.matches("TLS.*") || s.matches("X509.*")) { + enabled.add(s); + } + } engine.setEnabledProtocols(enabled.toArray(new String[0])); - if (anon) { - supported = engine.getSupportedCipherSuites(); - enabled = new ArrayList(); - // prefer ECDH over DHE - for (int i = 0; i < supported.length; i++) - if (supported[i].matches("TLS_ECDH_anon.*")) - enabled.add(supported[i]); - for (int i = 0; i < supported.length; i++) - if (supported[i].matches("TLS_DH_anon.*")) - enabled.add(supported[i]); - engine.setEnabledCipherSuites(enabled.toArray(new String[0])); - } else { - engine.setEnabledCipherSuites(engine.getSupportedCipherSuites()); - } + engine.setEnabledCipherSuites(engine.getSupportedCipherSuites()); } @Override From eda2a833cbe3fb05e0286fd648c2cd78c03c04f9 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 22 Dec 2022 11:18:54 -0300 Subject: [PATCH 05/15] Address review comments and sonar cloud reports --- .../consoleproxy/ConsoleProxyNoVncClient.java | 7 +- .../cloud/consoleproxy/vnc/NoVncClient.java | 159 +++--------------- .../consoleproxy/vnc/network/NioSocket.java | 44 ++--- .../vnc/network/NioSocketHandler.java | 4 +- .../vnc/network/NioSocketHandlerImpl.java | 9 +- .../vnc/network/NioSocketInputStream.java | 14 +- .../vnc/network/NioSocketOutputStream.java | 21 +-- ...er.java => NioSocketSSLEngineManager.java} | 31 ++-- .../vnc/network/NioSocketStream.java | 8 + .../vnc/network/NioSocketTLSInputStream.java | 30 ++-- .../vnc/network/NioSocketTLSOutputStream.java | 26 +-- .../vnc/security/VncSecurity.java | 5 +- .../vnc/security/VncTLSSecurity.java | 8 +- 13 files changed, 123 insertions(+), 243 deletions(-) rename services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/{SSLEngineManager.java => NioSocketSSLEngineManager.java} (88%) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index e9eccaeff0c3..686e8f35cecb 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -24,7 +24,6 @@ import java.awt.Image; import java.io.IOException; import java.net.URI; -import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; @@ -81,7 +80,7 @@ public boolean isFrontEndAlive() { return true; } - public void sendClientFrame(Frame f) throws IOException { + public void sendClientFrame(Frame f) { client.writeFrame(f); } @@ -245,10 +244,6 @@ private void connectClientToVNCServer(String tunnelUrl, String tunnelSession, St ConsoleProxy.ensureRoute(getClientHostAddress()); client.connectTo(getClientHostAddress(), getClientHostPort()); } - } catch (UnknownHostException e) { - s_logger.error("Unexpected exception", e); - } catch (IOException e) { - s_logger.error("Unexpected exception", e); } catch (Throwable e) { s_logger.error("Unexpected exception", e); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index 1aa0dc2b86a0..9c760c898c4e 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -22,7 +22,6 @@ import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; -import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.spec.KeySpec; @@ -35,12 +34,11 @@ import com.cloud.consoleproxy.vnc.network.NioSocket; import com.cloud.consoleproxy.vnc.network.NioSocketHandler; import com.cloud.consoleproxy.vnc.network.NioSocketHandlerImpl; -import com.cloud.consoleproxy.vnc.network.SSLEngineManager; +import com.cloud.consoleproxy.vnc.network.NioSocketSSLEngineManager; import com.cloud.consoleproxy.vnc.security.VncSecurity; import com.cloud.consoleproxy.vnc.security.VncTLSSecurity; import com.cloud.consoleproxy.websocket.WebSocketReverseProxy; import com.cloud.utils.exception.CloudRuntimeException; -import org.eclipse.jetty.websocket.api.RemoteEndpoint; import org.eclipse.jetty.websocket.api.Session; import org.eclipse.jetty.websocket.api.extensions.Frame; @@ -67,7 +65,7 @@ public class NoVncClient { public NoVncClient() { } - public void connectTo(String host, int port, String path, String session, boolean useSSL) throws UnknownHostException, IOException { + public void connectTo(String host, int port, String path, String session, boolean useSSL) throws IOException { if (port < 0) { if (useSSL) port = 443; @@ -80,7 +78,7 @@ public void connectTo(String host, int port, String path, String session, boolea setTunnelSocketStreams(); } - public void connectTo(String host, int port) throws UnknownHostException, IOException { + public void connectTo(String host, int port) { // Connect to server s_logger.info("Connecting to VNC server " + host + ":" + port + "..."); try { @@ -143,16 +141,15 @@ public String handshake() throws IOException { // 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 + "\"."); + String msg = "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\"."; + s_logger.error(msg); + throw new RuntimeException(msg); } // Proxy that we support RFB 3.3 only return RfbConstants.RFB_PROTOCOL_VERSION + "\n"; } - /** * VNC authentication. */ @@ -219,6 +216,7 @@ private void doVncAuth(DataInputStream in, DataOutputStream out, String password // Read security result int authResult = in.readInt(); + String msg; switch (authResult) { case RfbConstants.VNC_AUTH_OK: { // Nothing to do @@ -226,16 +224,19 @@ private void doVncAuth(DataInputStream in, DataOutputStream out, String password } 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."); + msg = "Connection to VNC server failed: too many wrong attempts."; + s_logger.error(msg); + throw new RuntimeException(msg); 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."); + msg = "Connection to VNC server failed: wrong password."; + s_logger.error(msg); + throw new RuntimeException(msg); default: - s_logger.error("Connection to VNC server failed, reason code: " + authResult); - throw new RuntimeException("Connection to VNC server failed, reason code: " + authResult); + msg = "Connection to VNC server failed, reason code: " + authResult; + s_logger.error(msg); + throw new RuntimeException(msg); } } @@ -248,8 +249,7 @@ public static byte flipByte(byte b) { 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; + return (byte) (b1_8 | b2_7 | b3_6 | b4_5 | b5_4 | b6_3 | b7_2 | b8_1); } public static byte[] encodePassword(byte[] challenge, String password) throws Exception { @@ -269,49 +269,7 @@ public static byte[] encodePassword(byte[] challenge, String password) throws Ex Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] response = cipher.doFinal(challenge); - return response; - } - - /** - * Decide the RFB protocol version with the VNC server - * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#711protocolversion - */ - protected String handshakeProtocolVersion(RemoteEndpoint clientRemote) throws IOException { - // Read protocol version - byte[] buf = new byte[12]; - tunnelInputStream.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 + "\"."); - } - tunnelOutputStream.write(buf); - return RfbConstants.RFB_PROTOCOL_VERSION + "\n"; - } - - /** - * Agree on the security type with the VNC server - * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security - * @return list of the security types to be processed - */ - protected List handshakeSecurityTypes(RemoteEndpoint clientRemote, String vmPassword, - String host, int port) throws IOException { - int securityType = selectFromTheServerOfferedSecurityTypes(); - - // Inform the server about our decision - this.tunnelOutputStream.writeByte(securityType); - - byte[] numberTypesToClient = new byte[] { 1, (byte) securityType }; - clientRemote.sendBytes(ByteBuffer.wrap(numberTypesToClient, 0, 2)); - - if (securityType == RfbConstants.V_ENCRYPT) { - securityType = getVEncryptSecuritySubtype(); - } - return VncSecurity.getSecurityStack(securityType, vmPassword, host, port); + return cipher.doFinal(challenge); } /** @@ -344,9 +302,7 @@ protected int getVEncryptSecuritySubtype() throws IOException { } int selectedSubtype = 0; for (int i = 0; i < numberOfSubtypes; i++) { - while (!socketConnection.checkIfBytesAreAvailableForReading(4)) { - s_logger.trace("Waiting for vEncrypt subtype"); - } + socketConnection.waitForBytesAvailableForReading(4); int subtype = socketConnection.readUnsignedInteger(32); if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) { selectedSubtype = subtype; @@ -361,56 +317,6 @@ protected int getVEncryptSecuritySubtype() throws IOException { return selectedSubtype; } - private int selectFromTheServerOfferedSecurityTypes() throws IOException { - int numberOfSecurityTypes = tunnelInputStream.readByte(); - if (numberOfSecurityTypes == 0) { - int reasonLength = tunnelInputStream.readInt(); - byte[] reasonBuffer = new byte[reasonLength]; - tunnelInputStream.readFully(reasonBuffer); - String reason = new String(reasonBuffer); - String errMsg = "No security type provided by the VNC server, reason: " + reason; - s_logger.error(errMsg); - throw new IOException(errMsg); - } - - for (int i = 0; i < numberOfSecurityTypes; i++) { - int securityType = tunnelInputStream.readByte(); - if (securityType != 0 && VncSecurity.supportedSecurityTypes.contains(securityType)) { - s_logger.info("Selected the security type: " + securityType); - return securityType; - } - } - throw new IOException("Could not select a supported or valid security type from the offered by the server"); - } - - /** - * VNC authentication. - */ - public void processSecurityResult(String password) - throws IOException { - // Read security result - int authResult = this.tunnelInputStream.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); - } - } - public int read(byte[] b) throws IOException { return tunnelInputStream.read(b); } @@ -458,26 +364,15 @@ public void writeFrame(Frame frame) { */ public ByteBuffer handshakeProtocolVersion() { ByteBuffer verStr = ByteBuffer.allocate(12); - int majorVersion; - int minorVersion; s_logger.debug("Reading RFB protocol version"); socketConnection.readBytes(verStr, 12); - if ((new String(verStr.array())).matches("RFB \\d{3}\\.\\d{3}\\n")) { - majorVersion = Integer.parseInt((new String(verStr.array())).substring(4,7)); - minorVersion = Integer.parseInt((new String(verStr.array())).substring(8,11)); - } else { - throw new CloudRuntimeException("Reading version failed: not an RFB server?"); - } - - s_logger.info("Server supports RFB protocol version " + majorVersion + "." + minorVersion); - verStr.clear(); - verStr.put(String.format("RFB %03d.%03d\n", majorVersion, minorVersion).getBytes()).flip(); + String supportedRfbVersion = RfbConstants.RFB_PROTOCOL_VERSION + "\n"; + verStr.put(supportedRfbVersion.getBytes()).flip(); - s_logger.info("Using RFB protocol version " + majorVersion + "." + minorVersion); setWaitForNoVnc(true); return verStr; } @@ -498,9 +393,7 @@ public int handshakeSecurityType() { List secTypes = Arrays.asList(1, 2, 19, 261); - while (!socketConnection.checkIfBytesAreAvailableForReading(1)) { - s_logger.trace("Waiting for inStream to be ready"); - } + socketConnection.waitForBytesAvailableForReading(1); int nServerSecTypes = socketConnection.readUnsignedInteger(8); if (nServerSecTypes == 0) { throw new CloudRuntimeException("No security types provided by the server"); @@ -517,7 +410,7 @@ public int handshakeSecurityType() { */ if (secType == RfbConstants.CONNECTION_FAILED) { for (j = secTypes.iterator(); j.hasNext(); ) { - int refType = (Integer) j.next(); + int refType = j.next(); if (refType == serverSecType) { secType = refType; break; @@ -552,9 +445,7 @@ public void processSecurityResultMsg(int secType) { if (secType == RfbConstants.NO_AUTH) { result = RfbConstants.VNC_AUTH_OK; } else { - while (!socketConnection.checkIfBytesAreAvailableForReading(1)) { - s_logger.trace("Waiting for inStream"); - } + socketConnection.waitForBytesAvailableForReading(1); result = socketConnection.readUnsignedInteger(32); } @@ -608,7 +499,7 @@ public void processHandshakeSecurityType(int secType, String vmPassword, String security.process(this.socketConnection); if (security instanceof VncTLSSecurity) { s_logger.debug("Setting new streams with SSLEngineManger after TLS security has passed"); - SSLEngineManager sslEngineManager = ((VncTLSSecurity) security).getSSLEngineManager(); + NioSocketSSLEngineManager sslEngineManager = ((VncTLSSecurity) security).getSSLEngineManager(); socketConnection.startTLSConnection(sslEngineManager); } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java index 59a4d988def0..cfd9c079f005 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java @@ -24,7 +24,6 @@ import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; -import java.util.Iterator; import java.util.Set; public class NioSocket { @@ -33,6 +32,7 @@ public class NioSocket { private Selector writeSelector; private Selector readSelector; + private static final int connectionTimeoutMillis = 3000; private static final Logger s_logger = Logger.getLogger(NioSocket.class); private void initializeSocket() { @@ -49,33 +49,33 @@ private void initializeSocket() { } } + private void waitForSocketSelectorConnected(Selector selector) { + try { + while (selector.select(connectionTimeoutMillis) <= 0) { + s_logger.debug("Waiting for ready operations to connect to the socket"); + } + Set keys = selector.selectedKeys(); + for (SelectionKey selectionKey: keys) { + if (selectionKey.isConnectable()) { + if (socketChannel.isConnectionPending()) { + socketChannel.finishConnect(); + } + s_logger.debug("Connected to the socket"); + break; + } + } + } catch (IOException e) { + s_logger.error(String.format("Error waiting for socket selector ready: %s", e.getMessage()), e); + } + } + private void connectSocket(String host, int port) { try { socketChannel.connect(new InetSocketAddress(host, port)); Selector selector = Selector.open(); socketChannel.register(selector, SelectionKey.OP_CONNECT); - boolean connected = false; - while (selector.select(3000) > 0) { - while (!connected) { - Set keys = selector.selectedKeys(); - Iterator i = keys.iterator(); - - while (i.hasNext()) { - SelectionKey key = (SelectionKey)i.next(); - - // Remove the current key - i.remove(); - // Attempt a connection - if (key.isConnectable()) { - if (socketChannel.isConnectionPending()) { - socketChannel.finishConnect(); - } - connected = true; - } - } - } - } + waitForSocketSelectorConnected(selector); socketChannel.socket().setTcpNoDelay(false); } catch (IOException e) { s_logger.error(String.format("Error creating NioSocket to %s:%s: %s", host, port, e.getMessage()), e); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java index 306120f9da05..e1ccd6feb11c 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandler.java @@ -38,8 +38,8 @@ public interface NioSocketHandler { void writeBytes(ByteBuffer data, int length); // Additional operations - boolean checkIfBytesAreAvailableForReading(int bytes); + void waitForBytesAvailableForReading(int bytes); void flushWriteBuffer(); - void startTLSConnection(SSLEngineManager sslEngineManager); + void startTLSConnection(NioSocketSSLEngineManager sslEngineManager); boolean isTLSConnection(); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java index 44b16e53142a..27414aed8aba 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.java @@ -27,6 +27,7 @@ public class NioSocketHandlerImpl implements NioSocketHandler { private boolean isTLS = false; private static final int DEFAULT_BUF_SIZE = 16384; + private static final Logger s_logger = Logger.getLogger(NioSocketHandlerImpl.class); public NioSocketHandlerImpl(NioSocket socket) { @@ -50,8 +51,10 @@ public void readBytes(ByteBuffer data, int length) { } @Override - public boolean checkIfBytesAreAvailableForReading(int bytes) { - return inputStream.checkForSizeWithoutWait(bytes); + public void waitForBytesAvailableForReading(int bytes) { + while (!inputStream.checkForSizeWithoutWait(bytes)) { + s_logger.trace("Waiting for inStream to be ready"); + } } @Override @@ -70,7 +73,7 @@ public void flushWriteBuffer() { } @Override - public void startTLSConnection(SSLEngineManager sslEngineManager) { + public void startTLSConnection(NioSocketSSLEngineManager sslEngineManager) { this.inputStream = new NioSocketTLSInputStream(sslEngineManager, this.inputStream.socket); this.outputStream = new NioSocketTLSOutputStream(sslEngineManager, this.outputStream.socket); this.isTLS = true; diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java index d6151a8b2ae3..5ac1bee9e54c 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java @@ -21,6 +21,7 @@ import org.apache.commons.lang3.ArrayUtils; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; public class NioSocketInputStream extends NioSocketStream { @@ -67,10 +68,7 @@ protected int readBytesToBuffer(ByteBuffer buf, int bytesToRead, boolean wait) { } protected int rearrangeBufferToFitSize(int numberItems, int itemSize, boolean wait) { - if (itemSize > buffer.length) { - throw new CloudRuntimeException(String.format("The requested item size %s exceeds the buffer size %s", - itemSize, buffer.length)); - } + checkItemSizeOnBuffer(itemSize); moveDataToBufferStart(); @@ -123,13 +121,7 @@ protected final String readString() { ByteBuffer str = ByteBuffer.allocate(len); readBytes(str, len); - String utf8string = new String(); - try { - utf8string = new String(str.array(),"UTF8"); - } catch(java.io.UnsupportedEncodingException e) { - e.printStackTrace(); - } - return utf8string; + return new String(str.array(), StandardCharsets.UTF_8); } /** diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java index 4410d6172934..03ead7a1a36e 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java @@ -23,23 +23,18 @@ public class NioSocketOutputStream extends NioSocketStream { private int sendPosition; - private boolean block; public NioSocketOutputStream(int bufferSize, NioSocket socket) { super(bufferSize, socket); this.endPosition = bufferSize; this.sendPosition = 0; - this.block = true; } - protected final int checkWriteBufferForSizeAndItemsNumber(int itemSize, int nItems) { - if (itemSize > (endPosition - currentPosition)) { - return rearrangeWriteBuffer(itemSize, nItems); - } - + protected final int checkWriteBufferForSingleItems(int items) { int window = endPosition - currentPosition; - return Math.min(window / itemSize, nItems); - + return window < 1 ? + rearrangeWriteBuffer(1, items) : + Math.min(window, items); } public final void checkWriteBufferForSize(int itemSize) { @@ -82,9 +77,7 @@ private int writeFromWriteBuffer(byte[] data, int dataPtr, int length) { } protected int rearrangeWriteBuffer(int itemSize, int numberItems) { - if (itemSize > this.buffer.length) { - throw new CloudRuntimeException("Cannot write item longer than the buffer size"); - } + checkItemSizeOnBuffer(itemSize); flushWriteBuffer(); @@ -103,7 +96,7 @@ protected void writeUnsignedInteger(int sizeInBits, int value) { protected void writeBytes(byte[] data, int dataPtr, int length) { int dataEnd = dataPtr + length; while (dataPtr < dataEnd) { - int n = checkWriteBufferForSizeAndItemsNumber(1, dataEnd - dataPtr); + int n = checkWriteBufferForSingleItems(dataEnd - dataPtr); System.arraycopy(data, dataPtr, buffer, currentPosition, n); currentPosition += n; dataPtr += n; @@ -112,7 +105,7 @@ protected void writeBytes(byte[] data, int dataPtr, int length) { protected void writeBytes(ByteBuffer data, int length) { while (length > 0) { - int n = checkWriteBufferForSizeAndItemsNumber(1, length); + int n = checkWriteBufferForSingleItems(length); data.get(buffer, currentPosition, n); currentPosition += n; length -= n; diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java similarity index 88% rename from services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java rename to services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java index 9326c882cbf7..4fc69634b826 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/SSLEngineManager.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java @@ -18,24 +18,25 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLException; import javax.net.ssl.SSLSession; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.Executor; import java.util.concurrent.Executors; -public class SSLEngineManager { +public class NioSocketSSLEngineManager { - private SSLEngine engine = null; + private final SSLEngine engine; - private ByteBuffer myNetData; - private ByteBuffer peerNetData; + private final ByteBuffer myNetData; + private final ByteBuffer peerNetData; - private Executor executor; - private NioSocketInputStream inputStream; - private NioSocketOutputStream outputStream; + private final Executor executor; + private final NioSocketInputStream inputStream; + private final NioSocketOutputStream outputStream; - public SSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) throws IOException { + public NioSocketSSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) { this.inputStream = socket.getInputStream(); this.outputStream = socket.getOutputStream(); engine = sslEngine; @@ -47,17 +48,15 @@ public SSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) throws IOE peerNetData = ByteBuffer.allocate(pktBufSize); } - public void doHandshake() throws Exception { - - // Begin handshake + public void doHandshake() throws SSLException { engine.beginHandshake(); SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus(); - // Process handshaking message - SSLEngineResult res = null; + SSLEngineResult res; int appBufSize = engine.getSession().getApplicationBufferSize(); ByteBuffer peerAppData = ByteBuffer.allocate(appBufSize); ByteBuffer myAppData = ByteBuffer.allocate(appBufSize); + while (hs != SSLEngineResult.HandshakeStatus.FINISHED && hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { @@ -68,7 +67,6 @@ public void doHandshake() throws Exception { peerNetData.flip(); res = engine.unwrap(peerNetData, peerAppData); peerNetData.compact(); - hs = res.getHandshakeStatus(); // Check status switch (res.getStatus()) { @@ -89,7 +87,6 @@ public void doHandshake() throws Exception { case NEED_WRAP: // Generate handshaking data res = engine.wrap(myAppData, myNetData); - hs = res.getHandshakeStatus(); // Check status switch (res.getStatus()) { @@ -121,7 +118,7 @@ private void executeTasks() { } } - public int read(ByteBuffer data, int length) throws IOException { + public int read(ByteBuffer data) throws IOException { // Read SSL/TLS encoded data from peer peerNetData.flip(); SSLEngineResult res = engine.unwrap(peerNetData, data); @@ -145,7 +142,7 @@ public int read(ByteBuffer data, int length) throws IOException { return 0; } - public int write(ByteBuffer data, int length) throws IOException { + public int write(ByteBuffer data) throws IOException { int n = 0; while (data.hasRemaining()) { SSLEngineResult res = engine.wrap(data, myNetData); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java index f02e2c23a66d..795f753b6344 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java @@ -78,4 +78,12 @@ protected void placeUnsignedIntegerToBuffer(int bytes, int value) { buffer[currentPosition++] = (byte) value; } } + + protected void checkItemSizeOnBuffer(int itemSize) { + if (itemSize > buffer.length) { + String msg = "Item size " + itemSize + " exceeds the buffer size " + buffer.length; + s_logger.error(msg); + throw new CloudRuntimeException(msg); + } + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java index 0739ab016456..7b738b867d51 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java @@ -17,34 +17,38 @@ package com.cloud.consoleproxy.vnc.network; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.log4j.Logger; +import java.io.IOException; import java.nio.ByteBuffer; public class NioSocketTLSInputStream extends NioSocketInputStream { - private final SSLEngineManager sslEngineManager; + private final NioSocketSSLEngineManager sslEngineManager; - public NioSocketTLSInputStream(SSLEngineManager sslEngineManager, NioSocket socket) { + private static final Logger s_logger = Logger.getLogger(NioSocketTLSInputStream.class); + + public NioSocketTLSInputStream(NioSocketSSLEngineManager sslEngineManager, NioSocket socket) { super(sslEngineManager.getSession().getApplicationBufferSize(), socket); this.sslEngineManager = sslEngineManager; } - protected int readTLS(byte[] buf, int bufPtr, int len) { - int n = -1; + protected int readFromSSLEngineManager(byte[] buffer, int startPos, int length) { try { - n = sslEngineManager.read(ByteBuffer.wrap(buf, bufPtr, len), len); - } catch (java.io.IOException e) { - e.printStackTrace(); + int readBytes = sslEngineManager.read(ByteBuffer.wrap(buffer, startPos, length)); + if (readBytes < 0) { + throw new CloudRuntimeException("Invalid number of read bytes frm SSL engine manager " + readBytes); + } + return readBytes; + } catch (IOException e) { + s_logger.error("Error reading from SSL engine manager: " + e.getMessage(), e); } - if (n < 0) throw new CloudRuntimeException("readTLS" + n); - return n; + return 0; } @Override protected int rearrangeBufferToFitSize(int numberItems, int itemSize, boolean wait) { - if (itemSize > buffer.length) { - throw new CloudRuntimeException("Cannot read item longer than the buffer size"); - } + checkItemSizeOnBuffer(itemSize); if (endPosition - currentPosition != 0) { System.arraycopy(buffer, currentPosition, buffer, 0, endPosition - currentPosition); @@ -55,7 +59,7 @@ protected int rearrangeBufferToFitSize(int numberItems, int itemSize, boolean wa currentPosition = start; while ((endPosition - start) < itemSize) { - int n = readTLS(buffer, endPosition, start + buffer.length - endPosition); + int n = readFromSSLEngineManager(buffer, endPosition, start + buffer.length - endPosition); if (!wait && n == 0) { return 0; } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java index 45eac7c7a684..a346bd2310e8 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java @@ -16,15 +16,18 @@ // under the License. package com.cloud.consoleproxy.vnc.network; -import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.log4j.Logger; +import java.io.IOException; import java.nio.ByteBuffer; public class NioSocketTLSOutputStream extends NioSocketOutputStream { - private final SSLEngineManager sslEngineManager; + private final NioSocketSSLEngineManager sslEngineManager; - public NioSocketTLSOutputStream(SSLEngineManager sslEngineManager, NioSocket socket) { + private static final Logger s_logger = Logger.getLogger(NioSocketTLSOutputStream.class); + + public NioSocketTLSOutputStream(NioSocketSSLEngineManager sslEngineManager, NioSocket socket) { super(sslEngineManager.getSession().getApplicationBufferSize(), socket); this.sslEngineManager = sslEngineManager; } @@ -33,7 +36,7 @@ public NioSocketTLSOutputStream(SSLEngineManager sslEngineManager, NioSocket soc public void flushWriteBuffer() { int sentUpTo = start; while (sentUpTo < currentPosition) { - int n = writeTLS(buffer, sentUpTo, currentPosition - sentUpTo); + int n = writeThroughSSLEngineManager(buffer, sentUpTo, currentPosition - sentUpTo); sentUpTo += n; offset += n; } @@ -41,25 +44,22 @@ public void flushWriteBuffer() { currentPosition = start; } - protected int writeTLS(byte[] data, int dataPtr, int length) { - int n; + protected int writeThroughSSLEngineManager(byte[] data, int startPos, int length) { try { - n = sslEngineManager.write(ByteBuffer.wrap(data, dataPtr, length), length); - } catch (java.io.IOException e) { - throw new CloudRuntimeException(e.getMessage()); + return sslEngineManager.write(ByteBuffer.wrap(data, startPos, length)); + } catch (IOException e) { + s_logger.error("Error writing though SSL engine manager: " + e.getMessage(), e); + return 0; } - return n; } @Override protected int rearrangeWriteBuffer(int itemSize, int numberItems) { - if (itemSize > buffer.length) - throw new CloudRuntimeException("TLSOutStream overrun: max itemSize exceeded"); + checkItemSizeOnBuffer(itemSize); flushWriteBuffer(); int window = endPosition - currentPosition; return Math.min(window / itemSize, numberItems); - } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java index 7154aa52f3fc..3906d7af3e6f 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java @@ -32,15 +32,12 @@ static List getSecurityStack(int securityType, String vmPassword, S case RfbConstants.NO_AUTH: return Collections.singletonList(new NoneVncSecurity()); case RfbConstants.VNC_AUTH: return Collections.singletonList(new VncAuthSecurity(vmPassword)); - // Do not add VCentrypt type = 19 but its supported subtypes + // Do not add VEncrypt type = 19 but its supported subtypes case RfbConstants.V_ENCRYPT_X509_VNC: return Arrays.asList(new VncTLSSecurity(host, port), new VncAuthSecurity(vmPassword)); default: throw new CloudRuntimeException("Unsupported security type " + securityType); } } - List supportedSecurityTypes = Arrays.asList(RfbConstants.NO_AUTH, RfbConstants.VNC_AUTH, - RfbConstants.V_ENCRYPT, RfbConstants.V_ENCRYPT_X509_VNC); - void process(NioSocketHandler socketHandler) throws IOException; } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java index ce5969de0ae6..c11be02a3c20 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java @@ -19,7 +19,7 @@ import com.cloud.consoleproxy.util.Logger; import com.cloud.consoleproxy.vnc.RfbConstants; import com.cloud.consoleproxy.vnc.network.NioSocketHandler; -import com.cloud.consoleproxy.vnc.network.SSLEngineManager; +import com.cloud.consoleproxy.vnc.network.NioSocketSSLEngineManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.nio.Link; @@ -35,7 +35,7 @@ public class VncTLSSecurity implements VncSecurity { private SSLContext ctx; private SSLEngine engine; - private SSLEngineManager manager; + private NioSocketSSLEngineManager manager; private final String host; private final int port; @@ -90,14 +90,14 @@ public void process(NioSocketHandler socketHandler) { } try { - manager = new SSLEngineManager(engine, socketHandler); + manager = new NioSocketSSLEngineManager(engine, socketHandler); manager.doHandshake(); } catch(java.lang.Exception e) { throw new CloudRuntimeException(e.toString()); } } - public SSLEngineManager getSSLEngineManager() { + public NioSocketSSLEngineManager getSSLEngineManager() { return manager; } } From 7254e87076133c3ac76aaa7c9e371b00ca118390 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 22 Dec 2022 20:45:33 -0300 Subject: [PATCH 06/15] Address more review comments --- .../ConsoleProxyHttpHandlerHelper.java | 4 +- .../consoleproxy/ConsoleProxyNoVncClient.java | 106 ++++-- .../cloud/consoleproxy/vnc/NoVncClient.java | 318 +++++++++--------- .../consoleproxy/vnc/network/NioSocket.java | 4 +- .../vnc/network/NioSocketStream.java | 2 +- .../vnc/network/NioSocketTLSInputStream.java | 5 +- .../vnc/network/NioSocketTLSOutputStream.java | 2 +- .../vnc/security/VncAuthSecurity.java | 9 +- 8 files changed, 255 insertions(+), 195 deletions(-) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java index c09083b37465..ad2d944ef6ff 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java @@ -72,7 +72,9 @@ public static Map getQueryMap(String query) { s_logger.error("decode token. tag info is not found!"); } if (param.getClientDisplayName() != null) { - s_logger.debug("decode token. displayname: " + param.getClientDisplayName()); + if (s_logger.isDebugEnabled()) { + s_logger.debug("decode token. displayname: " + param.getClientDisplayName()); + } map.put("displayname", param.getClientDisplayName()); } else { s_logger.error("decode token. displayname info is not found!"); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index 686e8f35cecb..b95bd4e13969 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -26,7 +26,6 @@ import java.net.URI; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.util.Arrays; import java.util.List; import com.cloud.consoleproxy.vnc.NoVncClient; @@ -80,8 +79,10 @@ public boolean isFrontEndAlive() { return true; } - public void sendClientFrame(Frame f) { - client.writeFrame(f); + public void sendClientFrame(Frame f) throws IOException { + byte[] data = new byte[f.getPayloadLength()]; + f.getPayload().get(data); + client.write(data); } @Override @@ -112,7 +113,7 @@ public void run() { updateFrontEndActivityTime(); } connectionAlive = client.isVncOverWebSocketConnectionAlive(); - } else if (!client.isVncOverTunnel()) { + } else if (client.isVncOverNioSocket()) { byte[] bytesArr; int nextBytes = client.getNextBytes(); bytesArr = new byte[nextBytes]; @@ -153,51 +154,88 @@ public void run() { * 3. Initialisation * * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#7protocol-messages - * @throws IOException */ private void authenticateToVNCServer() throws IOException { if (client.isVncOverWebSocketConnection()) { return; } - if (client.isVncOverTunnel()) { + if (!client.isVncOverNioSocket()) { String ver = client.handshake(); session.getRemote().sendBytes(ByteBuffer.wrap(ver.getBytes(), 0, ver.length())); byte[] b = client.authenticateTunnel(getClientHostPassword()); session.getRemote().sendBytes(ByteBuffer.wrap(b, 0, 4)); } else { - ByteBuffer verStr = client.handshakeProtocolVersion(); - sendMessageToVNCClient(verStr.array(), 12); + authenticateVNCServerThroughNioSocket(); + } + } - int secType = client.handshakeSecurityType(); - byte[] numberTypesToClient = new byte[] { 1, (byte) secType }; - sendMessageToVNCClient(numberTypesToClient, 2); + /** + * Handshaking messages consist on 3 phases: + * - ProtocolVersion + * - Security + * - SecurityResult + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#71handshaking-messages + */ + protected void handshakePhase() { + handshakeProtocolVersion(); + int securityType = handshakeSecurity(); + handshakeSecurityResult(securityType); - client.processHandshakeSecurityType(secType, getClientHostPassword(), - getClientHostAddress(), getClientHostPort()); + client.waitForNoVNCReply(); + } - byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 }; - sendMessageToVNCClient(securityResultToClient, 4); - client.setWaitForNoVnc(true); + protected void handshakeSecurityResult(int secType) { + client.processHandshakeSecurityType(secType, getClientHostPassword(), + getClientHostAddress(), getClientHostPort()); - while (client.isWaitForNoVnc()) { - s_logger.debug("Waiting"); - } + client.processSecurityResultMsg(secType); + byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 }; + sendMessageToVNCClient(securityResultToClient, 4); + client.setWaitForNoVnc(true); + } - String serverName = String.format("%s %s", clientParam.getClientDisplayName(), - client.isTLSConnectionEstablished() ? "(TLS backend)" : ""); - byte[] bytesServerInit = rewriteServerNameInServerInit(client.readServerInit(), serverName); - s_logger.debug(String.format("Server init message is %s (%s)", Arrays.toString(bytesServerInit), new String(bytesServerInit))); - session.getRemote().sendBytes(ByteBuffer.wrap(bytesServerInit)); - client.setWaitForNoVnc(true); - while (client.isWaitForNoVnc()) { - s_logger.debug("Waiting"); - } - s_logger.info("Authenticated successfully"); + protected int handshakeSecurity() { + int secType = client.handshakeSecurityType(); + byte[] numberTypesToClient = new byte[] { 1, (byte) secType }; + sendMessageToVNCClient(numberTypesToClient, 2); + return secType; + } + + protected void handshakeProtocolVersion() { + ByteBuffer verStr = client.handshakeProtocolVersion(); + sendMessageToVNCClient(verStr.array(), 12); + } + + protected void authenticateVNCServerThroughNioSocket() { + handshakePhase(); + initialisationPhase(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Authenticated successfully"); } } + /** + * Initialisation messages consist on: + * - ClientInit + * - ServerInit + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#73initialisation-messages + */ + private void initialisationPhase() { + byte[] serverInitByteArray = client.readServerInit(); + + String displayNameForVM = String.format("%s %s", clientParam.getClientDisplayName(), + client.isTLSConnectionEstablished() ? "(TLS backend)" : ""); + byte[] bytesServerInit = rewriteServerNameInServerInit(serverInitByteArray, displayNameForVM); + + sendMessageToVNCClient(bytesServerInit, bytesServerInit.length); + client.setWaitForNoVnc(true); + client.waitForNoVNCReply(); + } + /** * Send a message to the noVNC client */ @@ -227,20 +265,20 @@ protected static byte[] rewriteServerNameInServerInit(byte[] serverInitBytes, St private void connectClientToVNCServer(String tunnelUrl, String tunnelSession, String websocketUrl) { try { if (StringUtils.isNotBlank(websocketUrl)) { - s_logger.info("Connect to VNC over websocket URL: " + websocketUrl); + s_logger.info(String.format("Connect to VNC over websocket URL: %s", websocketUrl)); client.connectToWebSocket(websocketUrl, session); } else if (tunnelUrl != null && !tunnelUrl.isEmpty() && tunnelSession != null && !tunnelSession.isEmpty()) { URI uri = new URI(tunnelUrl); - s_logger.info("Connect to VNC server via tunnel. url: " + tunnelUrl + ", session: " - + tunnelSession); + s_logger.info(String.format("Connect to VNC server via tunnel. url: %s, session: %s", + tunnelUrl, tunnelSession)); ConsoleProxy.ensureRoute(uri.getHost()); client.connectTo(uri.getHost(), uri.getPort(), uri.getPath() + "?" + uri.getQuery(), tunnelSession, "https".equalsIgnoreCase(uri.getScheme())); } else { - s_logger.info("Connect to VNC server directly. host: " + getClientHostAddress() + ", port: " - + getClientHostPort()); + s_logger.info(String.format("Connect to VNC server directly. host: %s, port: %s", + getClientHostAddress(), getClientHostPort())); ConsoleProxy.ensureRoute(getClientHostAddress()); client.connectTo(getClientHostAddress(), getClientHostPort()); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index 9c760c898c4e..d1e104480535 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -24,9 +24,11 @@ import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; import java.util.Arrays; -import java.util.Iterator; import java.util.List; import com.cloud.consoleproxy.util.Logger; @@ -38,11 +40,14 @@ import com.cloud.consoleproxy.vnc.security.VncSecurity; import com.cloud.consoleproxy.vnc.security.VncTLSSecurity; import com.cloud.consoleproxy.websocket.WebSocketReverseProxy; +import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.eclipse.jetty.websocket.api.Session; -import org.eclipse.jetty.websocket.api.extensions.Frame; +import javax.crypto.BadPaddingException; import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; @@ -50,11 +55,11 @@ public class NoVncClient { private static final Logger s_logger = Logger.getLogger(NoVncClient.class); - private Socket tunnelSocket; - private DataInputStream tunnelInputStream; - private DataOutputStream tunnelOutputStream; + private Socket socket; + private DataInputStream is; + private DataOutputStream os; - private NioSocketHandler socketConnection; + private NioSocketHandler nioSocketConnection; private WebSocketReverseProxy webSocketReverseProxy; @@ -74,18 +79,19 @@ public void connectTo(String host, int port, String path, String session, boolea } RawHTTP tunnel = new RawHTTP("CONNECT", host, port, path, session, useSSL); - tunnelSocket = tunnel.connect(); + socket = tunnel.connect(); setTunnelSocketStreams(); } public void connectTo(String host, int port) { // Connect to server - s_logger.info("Connecting to VNC server " + host + ":" + port + "..."); + s_logger.info(String.format("Connecting to VNC server %s:%s ...", host, port)); try { NioSocket nioSocket = new NioSocket(host, port); - this.socketConnection = new NioSocketHandlerImpl(nioSocket); + this.nioSocketConnection = new NioSocketHandlerImpl(nioSocket); } catch (Exception e) { - s_logger.error("Cannot create socket to host: " + host + " and port " + port, e); + s_logger.error(String.format("Cannot create socket to host: %s and port %s: %s", host, port, + e.getMessage()), e); } } @@ -95,8 +101,8 @@ public void connectToWebSocket(String websocketUrl, Session session) throws URIS webSocketReverseProxy.connect(); } - public boolean isVncOverTunnel() { - return this.tunnelSocket != null; + public boolean isVncOverNioSocket() { + return this.nioSocketConnection != null; } public boolean isVncOverWebSocketConnection() { @@ -118,8 +124,8 @@ public void proxyMsgOverWebSocketConnection(ByteBuffer msg) { } private void setTunnelSocketStreams() throws IOException { - this.tunnelInputStream = new DataInputStream(this.tunnelSocket.getInputStream()); - this.tunnelOutputStream = new DataOutputStream(this.tunnelSocket.getOutputStream()); + this.is = new DataInputStream(this.socket.getInputStream()); + this.os = new DataOutputStream(this.socket.getOutputStream()); } public List getVncSecurityStack(int secType, String vmPassword, String host, int port) throws IOException { @@ -136,14 +142,15 @@ public String handshake() throws IOException { // Read protocol version byte[] buf = new byte[12]; - tunnelInputStream.readFully(buf); + is.readFully(buf); String rfbProtocol = new String(buf); // Server should use RFB protocol 3.x if (!rfbProtocol.contains(RfbConstants.RFB_PROTOCOL_VERSION_MAJOR)) { - String msg = "Cannot handshake with VNC server. Unsupported protocol version: \"" + rfbProtocol + "\"."; + String msg = String.format("Cannot handshake with VNC server. Unsupported protocol version: [%s]", + rfbProtocol); s_logger.error(msg); - throw new RuntimeException(msg); + throw new CloudRuntimeException(msg); } // Proxy that we support RFB 3.3 only @@ -156,14 +163,14 @@ public String handshake() throws IOException { public byte[] authenticateTunnel(String password) throws IOException { // Read security type - int authType = tunnelInputStream.readInt(); + int authType = is.readInt(); switch (authType) { case RfbConstants.CONNECTION_FAILED: { // Server forbids to connect. Read reason and throw exception - int length = tunnelInputStream.readInt(); + int length = is.readInt(); byte[] buf = new byte[length]; - tunnelInputStream.readFully(buf); + is.readFully(buf); String reason = new String(buf, RfbConstants.CHARSET); s_logger.error("Authentication to VNC server is failed. Reason: " + reason); @@ -177,7 +184,7 @@ public byte[] authenticateTunnel(String password) case RfbConstants.VNC_AUTH: { s_logger.info("VNC server requires password authentication"); - doVncAuth(tunnelInputStream, tunnelOutputStream, password); + doVncAuth(is, os, password); break; } @@ -215,28 +222,10 @@ private void doVncAuth(DataInputStream in, DataOutputStream out, String password // Read security result int authResult = in.readInt(); - - String msg; - switch (authResult) { - case RfbConstants.VNC_AUTH_OK: { - // Nothing to do - break; - } - - case RfbConstants.VNC_AUTH_TOO_MANY: - msg = "Connection to VNC server failed: too many wrong attempts."; - s_logger.error(msg); - throw new RuntimeException(msg); - - case RfbConstants.VNC_AUTH_FAILED: - msg = "Connection to VNC server failed: wrong password."; - s_logger.error(msg); - throw new RuntimeException(msg); - - default: - msg = "Connection to VNC server failed, reason code: " + authResult; - s_logger.error(msg); - throw new RuntimeException(msg); + Pair pair = processSecurityResultType(authResult); + if (!pair.first()) { + s_logger.error(pair.second()); + throw new CloudRuntimeException(pair.second()); } } @@ -252,7 +241,9 @@ public static byte flipByte(byte b) { return (byte) (b1_8 | b2_7 | b3_6 | b4_5 | b5_4 | b6_3 | b7_2 | b8_1); } - public static byte[] encodePassword(byte[] challenge, String password) throws Exception { + public static byte[] encodePassword(byte[] challenge, String password) throws InvalidKeyException, + InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, + IllegalBlockSizeException, BadPaddingException { // VNC password consist of up to eight ASCII characters. byte[] key = { 0, 0, 0, 0, 0, 0, 0, 0 }; // Padding byte[] passwordAsciiBytes = password.getBytes(Charset.availableCharsets().get("US-ASCII")); @@ -272,84 +263,98 @@ public static byte[] encodePassword(byte[] challenge, String password) throws Ex return cipher.doFinal(challenge); } - /** - * Obtain the VEncrypt subtype from the VNC server - * - * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt - */ - protected int getVEncryptSecuritySubtype() throws IOException { - int majorVEncryptVersion = socketConnection.readUnsignedInteger(8); - int minorVEncryptVersion = socketConnection.readUnsignedInteger(8); + private void agreeVEncryptVersion() throws IOException { + int majorVEncryptVersion = nioSocketConnection.readUnsignedInteger(8); + int minorVEncryptVersion = nioSocketConnection.readUnsignedInteger(8); int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion; - s_logger.debug("VEncrypt version: " + vEncryptVersion); - socketConnection.writeUnsignedInteger(8, majorVEncryptVersion); + if (s_logger.isDebugEnabled()) { + s_logger.debug("VEncrypt version offered by the server: " + vEncryptVersion); + } + nioSocketConnection.writeUnsignedInteger(8, majorVEncryptVersion); if (vEncryptVersion >= 0x0002) { - socketConnection.writeUnsignedInteger(8, 2); - socketConnection.flushWriteBuffer(); + nioSocketConnection.writeUnsignedInteger(8, 2); + nioSocketConnection.flushWriteBuffer(); } else { - socketConnection.writeUnsignedInteger(8, 0); - socketConnection.flushWriteBuffer(); + nioSocketConnection.writeUnsignedInteger(8, 0); + nioSocketConnection.flushWriteBuffer(); throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version"); } - int ack = socketConnection.readUnsignedInteger(8); + int ack = nioSocketConnection.readUnsignedInteger(8); if (ack != 0) { throw new IOException("The VNC server did not agree on the VEncrypt version"); } + } - int numberOfSubtypes = socketConnection.readUnsignedInteger(8); + private int selectVEncryptSubtype() { + int numberOfSubtypes = nioSocketConnection.readUnsignedInteger(8); if (numberOfSubtypes <= 0) { throw new CloudRuntimeException("The server reported no VeNCrypt sub-types"); } - int selectedSubtype = 0; for (int i = 0; i < numberOfSubtypes; i++) { - socketConnection.waitForBytesAvailableForReading(4); - int subtype = socketConnection.readUnsignedInteger(32); + nioSocketConnection.waitForBytesAvailableForReading(4); + int subtype = nioSocketConnection.readUnsignedInteger(32); if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) { - selectedSubtype = subtype; - break; + if (s_logger.isDebugEnabled()) { + s_logger.info("Selected VEncrypt subtype " + subtype); + } + return subtype; } } + throw new CloudRuntimeException("Could not select a VEncrypt subtype"); + } + /** + * Obtain the VEncrypt subtype from the VNC server + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#724vencrypt + */ + protected int getVEncryptSecuritySubtype() throws IOException { + agreeVEncryptVersion(); - s_logger.info("Selected VEncrypt subtype " + selectedSubtype); - socketConnection.writeUnsignedInteger(32, selectedSubtype); - socketConnection.flushWriteBuffer(); + int selectedSubtype = selectVEncryptSubtype(); + nioSocketConnection.writeUnsignedInteger(32, selectedSubtype); + nioSocketConnection.flushWriteBuffer(); return selectedSubtype; } public int read(byte[] b) throws IOException { - return tunnelInputStream.read(b); + return is.read(b); } public void write(byte[] b) throws IOException { if (isVncOverWebSocketConnection()) { proxyMsgOverWebSocketConnection(ByteBuffer.wrap(b)); - } else if (!isVncOverTunnel()) { - this.socketConnection.writeBytes(b, 0, b.length); + } else if (isVncOverNioSocket()) { + writeDataNioSocketConnection(b); } else { - tunnelOutputStream.write(b); + os.write(b); + } + } + + private void writeDataAfterSecurityPhase(byte[] data) { + nioSocketConnection.writeBytes(ByteBuffer.wrap(data), data.length); + nioSocketConnection.flushWriteBuffer(); + if (writerLeft == null) { + writerLeft = 3; + setWaitForNoVnc(false); + } else if (writerLeft > 0) { + writerLeft--; } } - public void writeFrame(Frame frame) { - byte[] data = new byte[frame.getPayloadLength()]; - frame.getPayload().get(data); + private void writeDataBeforeSecurityPhase(byte[] data) { + nioSocketConnection.writeBytes(data, 0, data.length); + if (flushAfterReceivingNoVNCData) { + nioSocketConnection.flushWriteBuffer(); + flushAfterReceivingNoVNCData = false; + } + } + protected void writeDataNioSocketConnection(byte[] data) { if (securityPhaseCompleted) { - socketConnection.writeBytes(ByteBuffer.wrap(data), data.length); - socketConnection.flushWriteBuffer(); - if (writerLeft == null) { - writerLeft = 3; - setWaitForNoVnc(false); - } else if (writerLeft > 0) { - writerLeft--; - } + writeDataAfterSecurityPhase(data); } else { - socketConnection.writeBytes(data, 0, data.length); - if (flushAfterReceivingNoVNCData) { - socketConnection.flushWriteBuffer(); - flushAfterReceivingNoVNCData = false; - } + writeDataBeforeSecurityPhase(data); } if (!securityPhaseCompleted || (writerLeft != null && writerLeft == 0)) { @@ -367,7 +372,7 @@ public ByteBuffer handshakeProtocolVersion() { s_logger.debug("Reading RFB protocol version"); - socketConnection.readBytes(verStr, 12); + nioSocketConnection.readBytes(verStr, 12); verStr.clear(); String supportedRfbVersion = RfbConstants.RFB_PROTOCOL_VERSION + "\n"; @@ -377,6 +382,16 @@ public ByteBuffer handshakeProtocolVersion() { return verStr; } + public void waitForNoVNCReply() { + int cycles = 0; + while (isWaitForNoVnc()) { + cycles++; + } + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("Waited %d cycles for NoVnc", cycles)); + } + } + /** * Once the protocol version has been decided, the server and client must agree on the type * of security to be used on the connection. @@ -384,43 +399,38 @@ public ByteBuffer handshakeProtocolVersion() { * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security */ public int handshakeSecurityType() { - while (isWaitForNoVnc()) { - s_logger.trace("Waiting for noVNC msg received"); + waitForNoVNCReply(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Processing security types message"); } - s_logger.debug("Processing security types message"); - int secType = RfbConstants.CONNECTION_FAILED; + int selectedSecurityType = RfbConstants.CONNECTION_FAILED; - List secTypes = Arrays.asList(1, 2, 19, 261); + List supportedSecurityTypes = Arrays.asList(RfbConstants.NO_AUTH, RfbConstants.VNC_AUTH, + RfbConstants.V_ENCRYPT, RfbConstants.V_ENCRYPT_X509_VNC); - socketConnection.waitForBytesAvailableForReading(1); - int nServerSecTypes = socketConnection.readUnsignedInteger(8); - if (nServerSecTypes == 0) { + nioSocketConnection.waitForBytesAvailableForReading(1); + int serverOfferedSecurityTypes = nioSocketConnection.readUnsignedInteger(8); + if (serverOfferedSecurityTypes == 0) { throw new CloudRuntimeException("No security types provided by the server"); } - Iterator j; - for (int i = 0; i < nServerSecTypes; i++) { - int serverSecType = socketConnection.readUnsignedInteger(8); - s_logger.debug("Server offers security type " + serverSecType); - - /* - * Use the first type sent by server which matches client's type. - * It means server's order specifies priority. - */ - if (secType == RfbConstants.CONNECTION_FAILED) { - for (j = secTypes.iterator(); j.hasNext(); ) { - int refType = j.next(); - if (refType == serverSecType) { - secType = refType; - break; - } + for (int i = 0; i < serverOfferedSecurityTypes; i++) { + int serverSecurityType = nioSocketConnection.readUnsignedInteger(8); + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("Server offers security type: %s", serverSecurityType)); + } + if (supportedSecurityTypes.contains(serverSecurityType)) { + selectedSecurityType = serverSecurityType; + if (s_logger.isDebugEnabled()) { + s_logger.debug(String.format("Selected supported security type: %s", selectedSecurityType)); } + break; } } this.flushAfterReceivingNoVNCData = true; setWaitForNoVnc(true); - return secType; + return selectedSecurityType; } private final Object lock = new Object(); @@ -438,75 +448,83 @@ public boolean isWaitForNoVnc() { private boolean waitForNoVnc = false; - public void processSecurityResultMsg(int secType) { - s_logger.info("Processing security result message"); - int result; - - if (secType == RfbConstants.NO_AUTH) { - result = RfbConstants.VNC_AUTH_OK; - } else { - socketConnection.waitForBytesAvailableForReading(1); - result = socketConnection.readUnsignedInteger(32); - } - - switch (result) { - case RfbConstants.VNC_AUTH_OK: - s_logger.info("Security completed"); - handleSecurityCompleted(); - return; - case RfbConstants.VNC_AUTH_FAILED: - s_logger.debug("auth failed"); + private Pair processSecurityResultType(int authResult) { + boolean result = false; + String message; + switch (authResult) { + case RfbConstants.VNC_AUTH_OK: { + result = true; + message = "Security completed"; break; + } case RfbConstants.VNC_AUTH_TOO_MANY: - s_logger.debug("auth failed - too many tries"); + message = "Connection to VNC server failed: too many wrong attempts."; + break; + case RfbConstants.VNC_AUTH_FAILED: + message = "Connection to VNC server failed: wrong password."; break; default: - throw new CloudRuntimeException("Unknown security result from server"); + message = String.format("Connection to VNC server failed, reason code: %s", authResult); } - String reason = socketConnection.readString(); - throw new CloudRuntimeException(reason); + return new Pair<>(result, message); } - private void handleSecurityCompleted() { - s_logger.info("Security completed"); - this.securityPhaseCompleted = true; + public void processSecurityResultMsg(int securityType) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("Processing security result message"); + } + + int result; + if (securityType == RfbConstants.NO_AUTH) { + result = RfbConstants.VNC_AUTH_OK; + } else { + nioSocketConnection.waitForBytesAvailableForReading(1); + result = nioSocketConnection.readUnsignedInteger(32); + } + + Pair securityResultType = processSecurityResultType(result); + if (securityResultType.first()) { + securityPhaseCompleted = true; + } else { + s_logger.error(securityResultType.second()); + String reason = nioSocketConnection.readString(); + String msg = String.format("%s - Reason: %s", securityResultType.second(), reason); + s_logger.error(msg); + throw new CloudRuntimeException(msg); + } } public byte[] readServerInit() { - return socketConnection.readServerInit(); + return nioSocketConnection.readServerInit(); } public int getNextBytes() { - return socketConnection.readNextBytes(); + return nioSocketConnection.readNextBytes(); } public boolean isTLSConnectionEstablished() { - return socketConnection.isTLSConnection(); + return nioSocketConnection.isTLSConnection(); } public void readBytes(byte[] arr, int len) { - socketConnection.readNextByteArray(arr, len); + nioSocketConnection.readNextByteArray(arr, len); } public void processHandshakeSecurityType(int secType, String vmPassword, String host, int port) { - while (isWaitForNoVnc()) { - s_logger.trace("Waiting for noVNC msg received"); - } + waitForNoVNCReply(); try { List vncSecurityStack = getVncSecurityStack(secType, vmPassword, host, port); for (VncSecurity security : vncSecurityStack) { - security.process(this.socketConnection); + security.process(this.nioSocketConnection); if (security instanceof VncTLSSecurity) { s_logger.debug("Setting new streams with SSLEngineManger after TLS security has passed"); NioSocketSSLEngineManager sslEngineManager = ((VncTLSSecurity) security).getSSLEngineManager(); - socketConnection.startTLSConnection(sslEngineManager); + nioSocketConnection.startTLSConnection(sslEngineManager); } } } catch (IOException e) { s_logger.error("Error processing handshake security type " + secType, e); } - - processSecurityResultMsg(secType); } } \ No newline at end of file diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java index cfd9c079f005..d1779042d86f 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocket.java @@ -32,7 +32,7 @@ public class NioSocket { private Selector writeSelector; private Selector readSelector; - private static final int connectionTimeoutMillis = 3000; + private static final int CONNECTION_TIMEOUT_MILLIS = 3000; private static final Logger s_logger = Logger.getLogger(NioSocket.class); private void initializeSocket() { @@ -51,7 +51,7 @@ private void initializeSocket() { private void waitForSocketSelectorConnected(Selector selector) { try { - while (selector.select(connectionTimeoutMillis) <= 0) { + while (selector.select(CONNECTION_TIMEOUT_MILLIS) <= 0) { s_logger.debug("Waiting for ready operations to connect to the socket"); } Set keys = selector.selectedKeys(); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java index 795f753b6344..66c18f09fd39 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java @@ -81,7 +81,7 @@ protected void placeUnsignedIntegerToBuffer(int bytes, int value) { protected void checkItemSizeOnBuffer(int itemSize) { if (itemSize > buffer.length) { - String msg = "Item size " + itemSize + " exceeds the buffer size " + buffer.length; + String msg = String.format("Item size: %s exceeds the buffer size: %s", itemSize, buffer.length); s_logger.error(msg); throw new CloudRuntimeException(msg); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java index 7b738b867d51..15a3e15fd05a 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java @@ -37,11 +37,12 @@ protected int readFromSSLEngineManager(byte[] buffer, int startPos, int length) try { int readBytes = sslEngineManager.read(ByteBuffer.wrap(buffer, startPos, length)); if (readBytes < 0) { - throw new CloudRuntimeException("Invalid number of read bytes frm SSL engine manager " + readBytes); + throw new CloudRuntimeException(String.format("Invalid number of read bytes frm SSL engine manager %s", + readBytes)); } return readBytes; } catch (IOException e) { - s_logger.error("Error reading from SSL engine manager: " + e.getMessage(), e); + s_logger.error(String.format("Error reading from SSL engine manager: %s", e.getMessage()), e); } return 0; } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java index a346bd2310e8..8ee01af059e2 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSOutputStream.java @@ -48,7 +48,7 @@ protected int writeThroughSSLEngineManager(byte[] data, int startPos, int length try { return sslEngineManager.write(ByteBuffer.wrap(data, startPos, length)); } catch (IOException e) { - s_logger.error("Error writing though SSL engine manager: " + e.getMessage(), e); + s_logger.error(String.format("Error writing though SSL engine manager: %s", e.getMessage()), e); return 0; } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java index 5272e5660f7c..3a394eb63399 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java @@ -19,6 +19,7 @@ import com.cloud.consoleproxy.util.Logger; import com.cloud.consoleproxy.vnc.NoVncClient; import com.cloud.consoleproxy.vnc.network.NioSocketHandler; +import com.cloud.utils.exception.CloudRuntimeException; import java.io.IOException; import java.nio.ByteBuffer; @@ -27,7 +28,7 @@ public class VncAuthSecurity implements VncSecurity { private final String vmPass; - private static final int vncAuthChallengeSize = 16; + private static final int VNC_AUTH_CHALLENGE_SIZE = 16; private static final Logger s_logger = Logger.getLogger(VncAuthSecurity.class); public VncAuthSecurity(String vmPass) { @@ -39,15 +40,15 @@ public void process(NioSocketHandler socketHandler) throws IOException { s_logger.info("VNC server requires password authentication"); // Read the challenge & obtain the user's password - ByteBuffer challenge = ByteBuffer.allocate(vncAuthChallengeSize); - socketHandler.readBytes(challenge, vncAuthChallengeSize); + ByteBuffer challenge = ByteBuffer.allocate(VNC_AUTH_CHALLENGE_SIZE); + socketHandler.readBytes(challenge, VNC_AUTH_CHALLENGE_SIZE); byte[] encodedPassword; try { encodedPassword = NoVncClient.encodePassword(challenge.array(), vmPass); } 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()); + throw new CloudRuntimeException("Cannot encrypt client password to send to server: " + e.getMessage()); } // Return the response to the server From b90f15f3907812beeffff1bedee8c88d8aa23c12 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 22 Dec 2022 23:17:13 -0300 Subject: [PATCH 07/15] Last sonarcloud code smell fixes --- .../cloud/consoleproxy/vnc/NoVncClient.java | 7 +- .../vnc/network/NioSocketInputStream.java | 2 +- .../network/NioSocketSSLEngineManager.java | 137 ++++++++++-------- 3 files changed, 79 insertions(+), 67 deletions(-) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index d1e104480535..4731f170c531 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -42,6 +42,7 @@ import com.cloud.consoleproxy.websocket.WebSocketReverseProxy; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.BooleanUtils; import org.eclipse.jetty.websocket.api.Session; import javax.crypto.BadPaddingException; @@ -223,7 +224,8 @@ private void doVncAuth(DataInputStream in, DataOutputStream out, String password // Read security result int authResult = in.readInt(); Pair pair = processSecurityResultType(authResult); - if (!pair.first()) { + boolean success = BooleanUtils.toBoolean(pair.first()); + if (!success) { s_logger.error(pair.second()); throw new CloudRuntimeException(pair.second()); } @@ -483,7 +485,8 @@ public void processSecurityResultMsg(int securityType) { } Pair securityResultType = processSecurityResultType(result); - if (securityResultType.first()) { + boolean success = BooleanUtils.toBoolean(securityResultType.first()); + if (success) { securityPhaseCompleted = true; } else { s_logger.error(securityResultType.second()); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java index 5ac1bee9e54c..8747afd85e20 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java @@ -172,7 +172,7 @@ private byte[] readPixelFormat() { ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); ret = ArrayUtils.addAll(ret, readAndCopyUnsignedInteger(8).second()); skipReadBytes(3); - return ArrayUtils.addAll(ret, new byte[] {0, 0, 0}); + return ArrayUtils.addAll(ret, (byte) 0, (byte) 0, (byte) 0); } protected int getNextBytes() { diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java index 4fc69634b826..c3d37bd8052f 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java @@ -48,66 +48,74 @@ public NioSocketSSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) { peerNetData = ByteBuffer.allocate(pktBufSize); } + private void handshakeNeedUnwrap(ByteBuffer peerAppData) throws SSLException { + peerNetData.flip(); + SSLEngineResult result = engine.unwrap(peerNetData, peerAppData); + peerNetData.compact(); + + switch (result.getStatus()) { + case BUFFER_UNDERFLOW: + int avail = inputStream.getReadBytesAvailableToFitSize(1, peerNetData.remaining(), + false); + inputStream.readBytes(peerNetData, avail); + break; + case OK: + case BUFFER_OVERFLOW: + break; + case CLOSED: + engine.closeInbound(); + break; + } + } + + private void handshakeNeedWrap(ByteBuffer myAppData) throws SSLException { + SSLEngineResult result = engine.wrap(myAppData, myNetData); + + switch (result.getStatus()) { + case OK: + myNetData.flip(); + outputStream.writeBytes(myNetData, myNetData.remaining()); + outputStream.flushWriteBuffer(); + myNetData.compact(); + break; + case CLOSED: + engine.closeOutbound(); + break; + case BUFFER_OVERFLOW: + case BUFFER_UNDERFLOW: + break; + } + } + + private void handleHandshakeStatus(SSLEngineResult.HandshakeStatus handshakeStatus, + ByteBuffer peerAppData, ByteBuffer myAppData) throws SSLException { + switch (handshakeStatus) { + case NEED_UNWRAP: + handshakeNeedUnwrap(peerAppData); + break; + + case NEED_WRAP: + handshakeNeedWrap(myAppData); + break; + + case NEED_TASK: + executeTasks(); + break; + } + } + public void doHandshake() throws SSLException { engine.beginHandshake(); - SSLEngineResult.HandshakeStatus hs = engine.getHandshakeStatus(); + SSLEngineResult.HandshakeStatus handshakeStatus = engine.getHandshakeStatus(); - SSLEngineResult res; int appBufSize = engine.getSession().getApplicationBufferSize(); ByteBuffer peerAppData = ByteBuffer.allocate(appBufSize); ByteBuffer myAppData = ByteBuffer.allocate(appBufSize); - while (hs != SSLEngineResult.HandshakeStatus.FINISHED && - hs != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { - - switch (hs) { - - case NEED_UNWRAP: - // Receive handshaking data from peer - peerNetData.flip(); - res = engine.unwrap(peerNetData, peerAppData); - peerNetData.compact(); - - // Check status - switch (res.getStatus()) { - case BUFFER_UNDERFLOW: - int avail = inputStream.getReadBytesAvailableToFitSize(1, peerNetData.remaining(), - false); - inputStream.readBytes(peerNetData, avail); - break; - case OK: - // Process incoming handshaking data - break; - case CLOSED: - engine.closeInbound(); - break; - } - break; - - case NEED_WRAP: - // Generate handshaking data - res = engine.wrap(myAppData, myNetData); - - // Check status - switch (res.getStatus()) { - case OK: - myNetData.flip(); - outputStream.writeBytes(myNetData, myNetData.remaining()); - outputStream.flushWriteBuffer(); - myNetData.compact(); - break; - case CLOSED: - engine.closeOutbound(); - break; - } - break; - - case NEED_TASK: - // Handle blocking tasks - executeTasks(); - break; - } - hs = engine.getHandshakeStatus(); + while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && + handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + handleHandshakeStatus(handshakeStatus, peerAppData, myAppData); + handshakeStatus = engine.getHandshakeStatus(); } } @@ -119,25 +127,24 @@ private void executeTasks() { } public int read(ByteBuffer data) throws IOException { - // Read SSL/TLS encoded data from peer peerNetData.flip(); - SSLEngineResult res = engine.unwrap(peerNetData, data); + SSLEngineResult result = engine.unwrap(peerNetData, data); peerNetData.compact(); - switch (res.getStatus()) { - case OK : - return res.bytesProduced(); + switch (result.getStatus()) { + case OK : + return result.bytesProduced(); case BUFFER_UNDERFLOW: // attempt to drain the underlying buffer first int need = peerNetData.remaining(); int available = inputStream.getReadBytesAvailableToFitSize(1, need, false); inputStream.readBytes(peerNetData, available); break; - case CLOSED: engine.closeInbound(); break; - + case BUFFER_OVERFLOW: + break; } return 0; } @@ -145,9 +152,9 @@ public int read(ByteBuffer data) throws IOException { public int write(ByteBuffer data) throws IOException { int n = 0; while (data.hasRemaining()) { - SSLEngineResult res = engine.wrap(data, myNetData); - n += res.bytesConsumed(); - switch (res.getStatus()) { + SSLEngineResult result = engine.wrap(data, myNetData); + n += result.bytesConsumed(); + switch (result.getStatus()) { case OK: myNetData.flip(); outputStream.writeBytes(myNetData, myNetData.remaining()); @@ -156,7 +163,6 @@ public int write(ByteBuffer data) throws IOException { break; case BUFFER_OVERFLOW: - // Make room in the buffer by flushing the outstream myNetData.flip(); outputStream.writeBytes(myNetData, myNetData.remaining()); myNetData.compact(); @@ -165,6 +171,9 @@ public int write(ByteBuffer data) throws IOException { case CLOSED: engine.closeOutbound(); break; + + case BUFFER_UNDERFLOW: + break; } } return n; From ae305a141d8cf8986745c80afc0ce6c6a25bc3de Mon Sep 17 00:00:00 2001 From: nvazquez Date: Fri, 23 Dec 2022 14:08:09 -0300 Subject: [PATCH 08/15] Automate VNC TLS provisioning and improve UI --- python/lib/cloudutils/serviceConfig.py | 18 ++++++++++++++++++ scripts/util/keystore-cert-import | 6 ++++++ systemvm/agent/noVNC/app/ui.js | 8 ++++---- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/python/lib/cloudutils/serviceConfig.py b/python/lib/cloudutils/serviceConfig.py index c6e9dca923a5..5ce14861d725 100755 --- a/python/lib/cloudutils/serviceConfig.py +++ b/python/lib/cloudutils/serviceConfig.py @@ -630,6 +630,12 @@ def config(self): cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") cfo.addEntry("vnc_listen", "\"0.0.0.0\"") + if self.syscfg.env.secure: + cfo.addEntry("vnc_tls", "1") + cfo.addEntry("vnc_tls_x509_verify", "1") + cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") + else: + cfo.addEntry("vnc_tls", "0") cfo.save() self.syscfg.svo.stopService("libvirtd") @@ -666,6 +672,12 @@ def config(self): cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") cfo.addEntry("vnc_listen", "\"0.0.0.0\"") + if self.syscfg.env.secure: + cfo.addEntry("vnc_tls", "1") + cfo.addEntry("vnc_tls_x509_verify", "1") + cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") + else: + cfo.addEntry("vnc_tls", "0") cfo.save() self.syscfg.svo.stopService("libvirtd") @@ -710,6 +722,12 @@ def config(self): cfo.addEntry("security_driver", "\"none\"") cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") + if self.syscfg.env.secure: + cfo.addEntry("vnc_tls", "1") + cfo.addEntry("vnc_tls_x509_verify", "1") + cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") + else: + cfo.addEntry("vnc_tls", "0") cfo.save() if os.path.exists("/lib/systemd/system/libvirtd.service"): diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import index 9e4e7f246b31..23956d4f86f0 100755 --- a/scripts/util/keystore-cert-import +++ b/scripts/util/keystore-cert-import @@ -114,6 +114,12 @@ if [ -f "$LIBVIRTD_FILE" ]; then ln -sf /etc/cloudstack/agent/cloud.crt /etc/pki/libvirt/servercert.pem ln -sf /etc/cloudstack/agent/cloud.key /etc/pki/libvirt/private/clientkey.pem ln -sf /etc/cloudstack/agent/cloud.key /etc/pki/libvirt/private/serverkey.pem + + # VNC TLS directory and certificates + mkdir -p /etc/pki/libvirt-vnc + ln -sf /etc/pki/CA/cacert.pem /etc/pki/libvirt-vnc/ca-cert.pem + ln -sf /etc/pki/libvirt/servercert.pem /etc/pki/libvirt-vnc/server-cert.pem + ln -sf /etc/pki/libvirt/private/serverkey.pem /etc/pki/libvirt-vnc/server-key.pem cloudstack-setup-agent -s > /dev/null fi diff --git a/systemvm/agent/noVNC/app/ui.js b/systemvm/agent/noVNC/app/ui.js index 2ebc9bd8b212..05026acedb31 100644 --- a/systemvm/agent/noVNC/app/ui.js +++ b/systemvm/agent/noVNC/app/ui.js @@ -506,13 +506,13 @@ const UI = { statusElem.textContent = text; statusElem.classList.add("noVNC_open"); - // If no time was specified, show the status for 1.5 seconds + // If no time was specified, show the status for 4 seconds if (typeof time === 'undefined') { - time = 1500; + time = 4000; } // Error messages do not timeout - if (statusType !== 'error' && UI.getSetting('encrypt') === false) { + if (statusType !== 'error') { UI.statusTimeout = window.setTimeout(UI.hideStatus, time); } }, @@ -1115,7 +1115,7 @@ const UI = { if (UI.getSetting('encrypt')) { msg = _("Connected (encrypted) to ") + UI.desktopName; } else { - msg = _("Connected") + msg = _("Connected (unencrypted) to ") + UI.desktopName; } UI.showStatus(msg); UI.updateVisualState('connected'); From 51f15a99ec2d85c6cceb5022f40240d802acb51a Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 12 Jan 2023 12:52:46 -0300 Subject: [PATCH 09/15] Add missing cases for sonarcloud report --- .../consoleproxy/vnc/network/NioSocketSSLEngineManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java index c3d37bd8052f..2b0229b75676 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java @@ -91,6 +91,7 @@ private void handleHandshakeStatus(SSLEngineResult.HandshakeStatus handshakeStat ByteBuffer peerAppData, ByteBuffer myAppData) throws SSLException { switch (handshakeStatus) { case NEED_UNWRAP: + case NEED_UNWRAP_AGAIN: handshakeNeedUnwrap(peerAppData); break; @@ -101,6 +102,10 @@ private void handleHandshakeStatus(SSLEngineResult.HandshakeStatus handshakeStat case NEED_TASK: executeTasks(); break; + + case FINISHED: + case NOT_HANDSHAKING: + break; } } From 83085ca055b55783d0d0c5d13761330fee546a14 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 12 Jan 2023 15:27:24 -0300 Subject: [PATCH 10/15] Fix certs renewal issue on configuring TLS --- python/lib/cloudutils/serviceConfig.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/python/lib/cloudutils/serviceConfig.py b/python/lib/cloudutils/serviceConfig.py index 5ce14861d725..97a2b6d8ab34 100755 --- a/python/lib/cloudutils/serviceConfig.py +++ b/python/lib/cloudutils/serviceConfig.py @@ -604,6 +604,15 @@ def configureLibvirtConfig(tls_enabled = True, cfg = None): cfo.addEntry("auth_tls", "\"none\"") cfo.save() + cfo = configFileOps("/etc/libvirt/qemu.conf", cfg) + if tls_enabled: + cfo.addEntry("vnc_tls", "1") + cfo.addEntry("vnc_tls_x509_verify", "1") + cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") + else: + cfo.addEntry("vnc_tls", "0") + cfo.save() + class libvirtConfigRedhat(serviceCfgBase): def __init__(self, syscfg): super(libvirtConfigRedhat, self).__init__(syscfg) From ba1ef3ed86e2ffcc8d5e628524e1c353b2284f8e Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 12 Jan 2023 16:38:42 -0300 Subject: [PATCH 11/15] Address review comments --- python/lib/cloudutils/serviceConfig.py | 39 +++++++------------ .../cloud/consoleproxy/vnc/NoVncClient.java | 4 +- .../vnc/security/VncSecurity.java | 10 +++-- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/python/lib/cloudutils/serviceConfig.py b/python/lib/cloudutils/serviceConfig.py index 97a2b6d8ab34..0e94a0aa5b4b 100755 --- a/python/lib/cloudutils/serviceConfig.py +++ b/python/lib/cloudutils/serviceConfig.py @@ -587,6 +587,18 @@ def restore(self): class securityPolicyConfigSUSE(securityPolicyConfigRedhat): pass + +def configure_libvirt_tls(tls_enabled=True, cfg=None): + cfo = configFileOps("/etc/libvirt/qemu.conf", cfg) + if tls_enabled: + cfo.addEntry("vnc_tls", "1") + cfo.addEntry("vnc_tls_x509_verify", "1") + cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") + else: + cfo.addEntry("vnc_tls", "0") + cfo.save() + + def configureLibvirtConfig(tls_enabled = True, cfg = None): cfo = configFileOps("/etc/libvirt/libvirtd.conf", cfg) if tls_enabled: @@ -604,14 +616,7 @@ def configureLibvirtConfig(tls_enabled = True, cfg = None): cfo.addEntry("auth_tls", "\"none\"") cfo.save() - cfo = configFileOps("/etc/libvirt/qemu.conf", cfg) - if tls_enabled: - cfo.addEntry("vnc_tls", "1") - cfo.addEntry("vnc_tls_x509_verify", "1") - cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") - else: - cfo.addEntry("vnc_tls", "0") - cfo.save() + configure_libvirt_tls(tls_enabled, cfg) class libvirtConfigRedhat(serviceCfgBase): def __init__(self, syscfg): @@ -639,12 +644,6 @@ def config(self): cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") cfo.addEntry("vnc_listen", "\"0.0.0.0\"") - if self.syscfg.env.secure: - cfo.addEntry("vnc_tls", "1") - cfo.addEntry("vnc_tls_x509_verify", "1") - cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") - else: - cfo.addEntry("vnc_tls", "0") cfo.save() self.syscfg.svo.stopService("libvirtd") @@ -681,12 +680,6 @@ def config(self): cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") cfo.addEntry("vnc_listen", "\"0.0.0.0\"") - if self.syscfg.env.secure: - cfo.addEntry("vnc_tls", "1") - cfo.addEntry("vnc_tls_x509_verify", "1") - cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") - else: - cfo.addEntry("vnc_tls", "0") cfo.save() self.syscfg.svo.stopService("libvirtd") @@ -731,12 +724,6 @@ def config(self): cfo.addEntry("security_driver", "\"none\"") cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") - if self.syscfg.env.secure: - cfo.addEntry("vnc_tls", "1") - cfo.addEntry("vnc_tls_x509_verify", "1") - cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") - else: - cfo.addEntry("vnc_tls", "0") cfo.save() if os.path.exists("/lib/systemd/system/libvirtd.service"): diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java index 4731f170c531..991a56415fa5 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/NoVncClient.java @@ -273,7 +273,7 @@ private void agreeVEncryptVersion() throws IOException { s_logger.debug("VEncrypt version offered by the server: " + vEncryptVersion); } nioSocketConnection.writeUnsignedInteger(8, majorVEncryptVersion); - if (vEncryptVersion >= 0x0002) { + if (vEncryptVersion >= 2) { nioSocketConnection.writeUnsignedInteger(8, 2); nioSocketConnection.flushWriteBuffer(); } else { @@ -297,7 +297,7 @@ private int selectVEncryptSubtype() { int subtype = nioSocketConnection.readUnsignedInteger(32); if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) { if (s_logger.isDebugEnabled()) { - s_logger.info("Selected VEncrypt subtype " + subtype); + s_logger.debug("Selected VEncrypt subtype " + subtype); } return subtype; } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java index 3906d7af3e6f..2423e2be0d36 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.java @@ -29,13 +29,15 @@ public interface VncSecurity { static List getSecurityStack(int securityType, String vmPassword, String host, int port) { switch (securityType) { - case RfbConstants.NO_AUTH: return Collections.singletonList(new NoneVncSecurity()); - case RfbConstants.VNC_AUTH: return Collections.singletonList(new VncAuthSecurity(vmPassword)); - + case RfbConstants.NO_AUTH: + return Collections.singletonList(new NoneVncSecurity()); + case RfbConstants.VNC_AUTH: + return Collections.singletonList(new VncAuthSecurity(vmPassword)); // Do not add VEncrypt type = 19 but its supported subtypes case RfbConstants.V_ENCRYPT_X509_VNC: return Arrays.asList(new VncTLSSecurity(host, port), new VncAuthSecurity(vmPassword)); - default: throw new CloudRuntimeException("Unsupported security type " + securityType); + default: + throw new CloudRuntimeException("Unsupported security type " + securityType); } } From 42b9603aafba85fdb30aaa0f53342ec3aec1e8ab Mon Sep 17 00:00:00 2001 From: nvazquez Date: Thu, 12 Jan 2023 19:51:02 -0300 Subject: [PATCH 12/15] Refactor serviceConfig script --- python/lib/cloudutils/serviceConfig.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/python/lib/cloudutils/serviceConfig.py b/python/lib/cloudutils/serviceConfig.py index 0e94a0aa5b4b..b033b00fb617 100755 --- a/python/lib/cloudutils/serviceConfig.py +++ b/python/lib/cloudutils/serviceConfig.py @@ -588,15 +588,13 @@ class securityPolicyConfigSUSE(securityPolicyConfigRedhat): pass -def configure_libvirt_tls(tls_enabled=True, cfg=None): - cfo = configFileOps("/etc/libvirt/qemu.conf", cfg) +def configure_libvirt_tls(tls_enabled=False, cfo=None): if tls_enabled: cfo.addEntry("vnc_tls", "1") cfo.addEntry("vnc_tls_x509_verify", "1") cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") else: cfo.addEntry("vnc_tls", "0") - cfo.save() def configureLibvirtConfig(tls_enabled = True, cfg = None): @@ -616,8 +614,6 @@ def configureLibvirtConfig(tls_enabled = True, cfg = None): cfo.addEntry("auth_tls", "\"none\"") cfo.save() - configure_libvirt_tls(tls_enabled, cfg) - class libvirtConfigRedhat(serviceCfgBase): def __init__(self, syscfg): super(libvirtConfigRedhat, self).__init__(syscfg) @@ -644,6 +640,7 @@ def config(self): cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") cfo.addEntry("vnc_listen", "\"0.0.0.0\"") + configure_libvirt_tls(self.syscfg.env.secure, cfo) cfo.save() self.syscfg.svo.stopService("libvirtd") @@ -680,6 +677,7 @@ def config(self): cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") cfo.addEntry("vnc_listen", "\"0.0.0.0\"") + configure_libvirt_tls(self.syscfg.env.secure, cfo) cfo.save() self.syscfg.svo.stopService("libvirtd") @@ -724,6 +722,7 @@ def config(self): cfo.addEntry("security_driver", "\"none\"") cfo.addEntry("user", "\"root\"") cfo.addEntry("group", "\"root\"") + configure_libvirt_tls(self.syscfg.env.secure, cfo) cfo.save() if os.path.exists("/lib/systemd/system/libvirtd.service"): From 2d8764637e970c14a70b8380598faef147c65851 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Fri, 13 Jan 2023 11:13:18 -0300 Subject: [PATCH 13/15] Fix certs propagation TLS conf --- agent/bindir/cloud-setup-agent.in | 3 ++- python/lib/cloudutils/serviceConfig.py | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/agent/bindir/cloud-setup-agent.in b/agent/bindir/cloud-setup-agent.in index cc3fe64ea746..53c6c2f56aa4 100755 --- a/agent/bindir/cloud-setup-agent.in +++ b/agent/bindir/cloud-setup-agent.in @@ -26,7 +26,7 @@ from cloudutils.configFileOps import configFileOps from cloudutils.globalEnv import globalEnv from cloudutils.networkConfig import networkConfig from cloudutils.syscfg import sysConfigFactory -from cloudutils.serviceConfig import configureLibvirtConfig +from cloudutils.serviceConfig import configureLibvirtConfig, configure_libvirt_tls from optparse import OptionParser @@ -115,6 +115,7 @@ if __name__ == '__main__': if not options.auto and options.secure: configureLibvirtConfig(True) + configure_libvirt_tls(True) print("Libvirtd with TLS configured") sys.exit(0) diff --git a/python/lib/cloudutils/serviceConfig.py b/python/lib/cloudutils/serviceConfig.py index b033b00fb617..8bb408c8a9f5 100755 --- a/python/lib/cloudutils/serviceConfig.py +++ b/python/lib/cloudutils/serviceConfig.py @@ -589,6 +589,11 @@ class securityPolicyConfigSUSE(securityPolicyConfigRedhat): def configure_libvirt_tls(tls_enabled=False, cfo=None): + save = False + if not cfo: + cfo = configFileOps("/etc/libvirt/qemu.conf") + save = True + if tls_enabled: cfo.addEntry("vnc_tls", "1") cfo.addEntry("vnc_tls_x509_verify", "1") @@ -596,6 +601,8 @@ def configure_libvirt_tls(tls_enabled=False, cfo=None): else: cfo.addEntry("vnc_tls", "0") + if save: + cfo.save() def configureLibvirtConfig(tls_enabled = True, cfg = None): cfo = configFileOps("/etc/libvirt/libvirtd.conf", cfg) From 7894de7db2ae7700901c3956f3b546ff710c2a48 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Wed, 18 Jan 2023 11:05:45 -0300 Subject: [PATCH 14/15] Fix unsecure host tests --- test/integration/smoke/test_vm_life_cycle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index d9878b097659..aef039c98ebe 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -1009,6 +1009,7 @@ def unsecure_host(self, host): sed -i 's/listen_tls.*/listen_tls=0/g' /etc/libvirt/libvirtd.conf && \ sed -i 's/listen_tcp.*/listen_tcp=1/g' /etc/libvirt/libvirtd.conf && \ sed -i '/.*_file=.*/d' /etc/libvirt/libvirtd.conf && \ + sed -i 's/vnc_tls.*/vnc_tls=0/g' /etc/libvirt/qemu.conf && \ service libvirtd start ; \ service libvirt-bin start ; \ sleep 30 ; \ From e75a69ab645f32e6936a9696f02185a914509d7a Mon Sep 17 00:00:00 2001 From: nvazquez Date: Fri, 20 Jan 2023 07:25:26 -0300 Subject: [PATCH 15/15] Update activity time when reading from socket --- .../java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java | 1 + 1 file changed, 1 insertion(+) diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index b95bd4e13969..ff777976184f 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -120,6 +120,7 @@ public void run() { client.readBytes(bytesArr, nextBytes); if (nextBytes > 0) { session.getRemote().sendBytes(ByteBuffer.wrap(bytesArr)); + updateFrontEndActivityTime(); } } else { b = new byte[100];