diff --git a/agent/bindir/cloud-setup-agent.in b/agent/bindir/cloud-setup-agent.in index cc3fe64ea74..53c6c2f56aa 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 4075b9e8602..0b4820dd7b6 100755 --- a/python/lib/cloudutils/serviceConfig.py +++ b/python/lib/cloudutils/serviceConfig.py @@ -587,6 +587,23 @@ class securityPolicyConfigRedhat(serviceCfgBase): class securityPolicyConfigSUSE(securityPolicyConfigRedhat): pass + +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") + cfo.addEntry("vnc_tls_x509_cert_dir", "\"/etc/pki/libvirt-vnc\"") + else: + cfo.addEntry("vnc_tls", "0") + + if save: + cfo.save() + def configureLibvirtConfig(tls_enabled = True, cfg = None): cfo = configFileOps("/etc/libvirt/libvirtd.conf", cfg) if tls_enabled: @@ -639,6 +656,7 @@ class libvirtConfigRedhat(serviceCfgBase): 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") @@ -676,6 +694,7 @@ class libvirtConfigSUSE(serviceCfgBase): 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") @@ -729,6 +748,7 @@ class libvirtConfigUbuntu(serviceCfgBase): 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"): diff --git a/scripts/util/keystore-cert-import b/scripts/util/keystore-cert-import index 9e4e7f246b3..23956d4f86f 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/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java index 7f0fb0ba7f7..e23778c0b98 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 class ConsoleProxyClientParam { 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 0e3a0f822f6..fb80983415d 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.servlet.ConsoleProxyPasswordBasedEncryptor; 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; @@ -285,11 +286,15 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce 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()); 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(); @@ -353,12 +358,14 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce String sessionUuid, String addr, String extraSecurityToken, VirtualMachine vm, HostVO hostVo, UserVmDetailVO details, - Pair portInfo, String host) { + Pair portInfo, String host, + String displayName) { ConsoleProxyClientParam param = new ConsoleProxyClientParam(); param.setClientHostAddress(parsedHostInfo.first()); param.setClientHostPort(port); param.setClientHostPassword(sid); param.setClientTag(tag); + param.setClientDisplayName(displayName); param.setTicket(ticket); param.setSessionUuid(sessionUuid); param.setSourceIP(addr); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java index e000b4767f5..563843840eb 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxHandler.java @@ -76,6 +76,7 @@ public class ConsoleProxyAjaxHandler implements HttpHandler { String portStr = queryMap.get("port"); String sid = queryMap.get("sid"); String tag = queryMap.get("tag"); + String displayName = queryMap.get("displayname"); String ticket = queryMap.get("ticket"); String ajaxSessionIdStr = queryMap.get("sess"); String eventStr = queryMap.get("event"); @@ -129,6 +130,7 @@ public class ConsoleProxyAjaxHandler implements HttpHandler { param.setClientHostPort(port); param.setClientHostPassword(sid); param.setClientTag(tag); + param.setClientDisplayName(displayName); param.setTicket(ticket); param.setClientTunnelUrl(console_url); param.setClientTunnelSession(console_host_session); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java index 758cac42eb9..ae319ee4006 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyAjaxImageHandler.java @@ -69,6 +69,7 @@ public class ConsoleProxyAjaxImageHandler implements HttpHandler { String portStr = queryMap.get("port"); String sid = queryMap.get("sid"); String tag = queryMap.get("tag"); + String displayName = queryMap.get("displayname"); String ticket = queryMap.get("ticket"); String keyStr = queryMap.get("key"); String console_url = queryMap.get("consoleurl"); @@ -113,6 +114,7 @@ public class ConsoleProxyAjaxImageHandler implements HttpHandler { param.setClientHostPort(port); param.setClientHostPassword(sid); param.setClientTag(tag); + param.setClientDisplayName(displayName); param.setTicket(ticket); param.setClientTunnelUrl(console_url); param.setClientTunnelSession(console_host_session); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java index b27532edd37..aa1f2223a8c 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -26,6 +26,7 @@ public class ConsoleProxyClientParam { private int clientHostPort; private String clientHostPassword; private String clientTag; + private String clientDisplayName; private String ticket; private String clientTunnelUrl; @@ -89,6 +90,10 @@ public class ConsoleProxyClientParam { 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/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 d51a46680e7..ad2d944ef6f 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 @@ -71,6 +71,14 @@ public class ConsoleProxyHttpHandlerHelper { } else { s_logger.error("decode token. tag info is not found!"); } + if (param.getClientDisplayName() != null) { + 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!"); + } 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 72f019bd3d1..849042e7ec4 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.server.Request; 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 class ConsoleProxyNoVNCHandler extends WebSocketHandler { 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 class ConsoleProxyNoVNCHandler extends WebSocketHandler { 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 class ConsoleProxyNoVNCHandler extends WebSocketHandler { 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 f1c19591303..ff777976184 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,8 +24,8 @@ import org.eclipse.jetty.websocket.api.extensions.Frame; 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.List; import com.cloud.consoleproxy.vnc.NoVncClient; @@ -113,6 +113,15 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient { updateFrontEndActivityTime(); } connectionAlive = client.isVncOverWebSocketConnectionAlive(); + } else if (client.isVncOverNioSocket()) { + byte[] bytesArr; + int nextBytes = client.getNextBytes(); + bytesArr = new byte[nextBytes]; + client.readBytes(bytesArr, nextBytes); + if (nextBytes > 0) { + session.getRemote().sendBytes(ByteBuffer.wrap(bytesArr)); + updateFrontEndActivityTime(); + } } else { b = new byte[100]; readBytes = client.read(b); @@ -127,7 +136,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient { } connectionAlive = false; } catch (IOException e) { - e.printStackTrace(); + s_logger.error("Error on VNC client", e); } } @@ -137,18 +146,117 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient { /** * Authenticate to VNC server when not using websockets - * @throws IOException + * + * 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 */ private void authenticateToVNCServer() throws IOException { - if (!client.isVncOverWebSocketConnection()) { + if (client.isVncOverWebSocketConnection()) { + return; + } + + if (!client.isVncOverNioSocket()) { 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 { + authenticateVNCServerThroughNioSocket(); } } + /** + * 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.waitForNoVNCReply(); + } + + protected void handshakeSecurityResult(int secType) { + client.processHandshakeSecurityType(secType, getClientHostPassword(), + getClientHostAddress(), getClientHostPort()); + + client.processSecurityResultMsg(secType); + byte[] securityResultToClient = new byte[] { 0, 0, 0, 0 }; + sendMessageToVNCClient(securityResultToClient, 4); + client.setWaitForNoVnc(true); + } + + 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 + */ + 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 @@ -158,27 +266,23 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient { 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()); } - } 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 7be6421714e..991a56415fa 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,21 +22,37 @@ import java.io.IOException; 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.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; 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.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.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.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.lang3.BooleanUtils; import org.eclipse.jetty.websocket.api.Session; +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; + public class NoVncClient { private static final Logger s_logger = Logger.getLogger(NoVncClient.class); @@ -44,12 +60,18 @@ public class NoVncClient { private DataInputStream is; private DataOutputStream os; + private NioSocketHandler nioSocketConnection; + private WebSocketReverseProxy webSocketReverseProxy; + private boolean flushAfterReceivingNoVNCData = true; + private boolean securityPhaseCompleted = false; + private Integer writerLeft = null; + 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; @@ -59,14 +81,19 @@ public class NoVncClient { RawHTTP tunnel = new RawHTTP("CONNECT", host, port, path, session, useSSL); socket = tunnel.connect(); - setStreams(); + 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 + "..."); - socket = new Socket(host, port); - setStreams(); + s_logger.info(String.format("Connecting to VNC server %s:%s ...", host, port)); + try { + NioSocket nioSocket = new NioSocket(host, port); + this.nioSocketConnection = new NioSocketHandlerImpl(nioSocket); + } catch (Exception e) { + s_logger.error(String.format("Cannot create socket to host: %s and port %s: %s", host, port, + e.getMessage()), e); + } } // VNC over WebSocket connection helpers @@ -75,6 +102,10 @@ public class NoVncClient { webSocketReverseProxy.connect(); } + public boolean isVncOverNioSocket() { + return this.nioSocketConnection != null; + } + public boolean isVncOverWebSocketConnection() { return webSocketReverseProxy != null; } @@ -93,11 +124,18 @@ public class NoVncClient { } } - private void setStreams() throws IOException { + private void setTunnelSocketStreams() throws IOException { 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 { + if (secType == RfbConstants.V_ENCRYPT) { + secType = getVEncryptSecuritySubtype(); + } + return VncSecurity.getSecurityStack(secType, vmPassword, host, port); + } + /** * Handshake with VNC server. */ @@ -110,9 +148,10 @@ public class NoVncClient { // 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 = String.format("Cannot handshake with VNC server. Unsupported protocol version: [%s]", + rfbProtocol); + s_logger.error(msg); + throw new CloudRuntimeException(msg); } // Proxy that we support RFB 3.3 only @@ -122,7 +161,7 @@ public class NoVncClient { /** * VNC authentication. */ - public byte[] authenticate(String password) + public byte[] authenticateTunnel(String password) throws IOException { // Read security type int authType = is.readInt(); @@ -157,7 +196,7 @@ public class NoVncClient { } // 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 }; } /** @@ -184,28 +223,15 @@ public class NoVncClient { // Read security result int authResult = in.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); + Pair pair = processSecurityResultType(authResult); + boolean success = BooleanUtils.toBoolean(pair.first()); + if (!success) { + s_logger.error(pair.second()); + throw new CloudRuntimeException(pair.second()); } } - 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; @@ -214,11 +240,12 @@ public class NoVncClient { 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 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")); @@ -235,8 +262,61 @@ public class NoVncClient { Cipher cipher = Cipher.getInstance("DES/ECB/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); - byte[] response = cipher.doFinal(challenge); - return response; + return cipher.doFinal(challenge); + } + + private void agreeVEncryptVersion() throws IOException { + int majorVEncryptVersion = nioSocketConnection.readUnsignedInteger(8); + int minorVEncryptVersion = nioSocketConnection.readUnsignedInteger(8); + int vEncryptVersion = (majorVEncryptVersion << 8) | minorVEncryptVersion; + if (s_logger.isDebugEnabled()) { + s_logger.debug("VEncrypt version offered by the server: " + vEncryptVersion); + } + nioSocketConnection.writeUnsignedInteger(8, majorVEncryptVersion); + if (vEncryptVersion >= 2) { + nioSocketConnection.writeUnsignedInteger(8, 2); + nioSocketConnection.flushWriteBuffer(); + } else { + nioSocketConnection.writeUnsignedInteger(8, 0); + nioSocketConnection.flushWriteBuffer(); + throw new CloudRuntimeException("Server reported an unsupported VeNCrypt version"); + } + int ack = nioSocketConnection.readUnsignedInteger(8); + if (ack != 0) { + throw new IOException("The VNC server did not agree on the VEncrypt version"); + } + } + + private int selectVEncryptSubtype() { + int numberOfSubtypes = nioSocketConnection.readUnsignedInteger(8); + if (numberOfSubtypes <= 0) { + throw new CloudRuntimeException("The server reported no VeNCrypt sub-types"); + } + for (int i = 0; i < numberOfSubtypes; i++) { + nioSocketConnection.waitForBytesAvailableForReading(4); + int subtype = nioSocketConnection.readUnsignedInteger(32); + if (subtype == RfbConstants.V_ENCRYPT_X509_VNC) { + if (s_logger.isDebugEnabled()) { + s_logger.debug("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(); + + int selectedSubtype = selectVEncryptSubtype(); + nioSocketConnection.writeUnsignedInteger(32, selectedSubtype); + nioSocketConnection.flushWriteBuffer(); + + return selectedSubtype; } public int read(byte[] b) throws IOException { @@ -246,9 +326,208 @@ public class NoVncClient { public void write(byte[] b) throws IOException { if (isVncOverWebSocketConnection()) { proxyMsgOverWebSocketConnection(ByteBuffer.wrap(b)); + } else if (isVncOverNioSocket()) { + writeDataNioSocketConnection(b); } else { 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--; + } + } + + private void writeDataBeforeSecurityPhase(byte[] data) { + nioSocketConnection.writeBytes(data, 0, data.length); + if (flushAfterReceivingNoVNCData) { + nioSocketConnection.flushWriteBuffer(); + flushAfterReceivingNoVNCData = false; + } + } + + protected void writeDataNioSocketConnection(byte[] data) { + if (securityPhaseCompleted) { + writeDataAfterSecurityPhase(data); + } else { + writeDataBeforeSecurityPhase(data); + } + + 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); + + s_logger.debug("Reading RFB protocol version"); + + nioSocketConnection.readBytes(verStr, 12); + + verStr.clear(); + String supportedRfbVersion = RfbConstants.RFB_PROTOCOL_VERSION + "\n"; + verStr.put(supportedRfbVersion.getBytes()).flip(); + + setWaitForNoVnc(true); + 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. + * + * Reference: https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#712security + */ + public int handshakeSecurityType() { + waitForNoVNCReply(); + if (s_logger.isDebugEnabled()) { + s_logger.debug("Processing security types message"); + } + + int selectedSecurityType = RfbConstants.CONNECTION_FAILED; + + List supportedSecurityTypes = Arrays.asList(RfbConstants.NO_AUTH, RfbConstants.VNC_AUTH, + RfbConstants.V_ENCRYPT, RfbConstants.V_ENCRYPT_X509_VNC); + + nioSocketConnection.waitForBytesAvailableForReading(1); + int serverOfferedSecurityTypes = nioSocketConnection.readUnsignedInteger(8); + if (serverOfferedSecurityTypes == 0) { + throw new CloudRuntimeException("No security types provided by the server"); + } + + 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 selectedSecurityType; + } + + 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; + + 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: + 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: + message = String.format("Connection to VNC server failed, reason code: %s", authResult); + } + return new Pair<>(result, message); + } + + 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); + boolean success = BooleanUtils.toBoolean(securityResultType.first()); + if (success) { + 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 nioSocketConnection.readServerInit(); + } + + public int getNextBytes() { + return nioSocketConnection.readNextBytes(); + } + + public boolean isTLSConnectionEstablished() { + return nioSocketConnection.isTLSConnection(); + } + + public void readBytes(byte[] arr, int len) { + nioSocketConnection.readNextByteArray(arr, len); + } + + public void processHandshakeSecurityType(int secType, String vmPassword, String host, int port) { + waitForNoVNCReply(); + + try { + List vncSecurityStack = getVncSecurityStack(secType, vmPassword, host, port); + for (VncSecurity security : vncSecurityStack) { + 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(); + nioSocketConnection.startTLSConnection(sslEngineManager); + } + } + } catch (IOException e) { + s_logger.error("Error processing handshake security type " + secType, e); + } + } } \ 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 7bf4f8dbd7e..3e44ce7d1a6 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 00000000000..d1779042d86 --- /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.Set; + +public class NioSocket { + + private SocketChannel socketChannel; + private Selector writeSelector; + private Selector readSelector; + + private static final int CONNECTION_TIMEOUT_MILLIS = 3000; + 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 waitForSocketSelectorConnected(Selector selector) { + try { + while (selector.select(CONNECTION_TIMEOUT_MILLIS) <= 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); + + 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); + } + } + + 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 00000000000..e1ccd6feb11 --- /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 + void waitForBytesAvailableForReading(int bytes); + void flushWriteBuffer(); + 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 new file mode 100644 index 00000000000..27414aed8ab --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketHandlerImpl.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.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 void waitForBytesAvailableForReading(int bytes) { + while (!inputStream.checkForSizeWithoutWait(bytes)) { + s_logger.trace("Waiting for inStream to be ready"); + } + } + + @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(NioSocketSSLEngineManager 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 00000000000..8747afd85e2 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketInputStream.java @@ -0,0 +1,202 @@ +// 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.nio.charset.StandardCharsets; +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) { + checkItemSizeOnBuffer(itemSize); + + 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); + return new String(str.array(), StandardCharsets.UTF_8); + } + + /** + * 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, (byte) 0, (byte) 0, (byte) 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 00000000000..03ead7a1a36 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketOutputStream.java @@ -0,0 +1,114 @@ +// 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; + + public NioSocketOutputStream(int bufferSize, NioSocket socket) { + super(bufferSize, socket); + this.endPosition = bufferSize; + this.sendPosition = 0; + } + + protected final int checkWriteBufferForSingleItems(int items) { + int window = endPosition - currentPosition; + return window < 1 ? + rearrangeWriteBuffer(1, items) : + Math.min(window, items); + } + + 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) { + checkItemSizeOnBuffer(itemSize); + + 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 = checkWriteBufferForSingleItems(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 = 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/NioSocketSSLEngineManager.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java new file mode 100644 index 00000000000..2b0229b7567 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketSSLEngineManager.java @@ -0,0 +1,191 @@ +// 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.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 NioSocketSSLEngineManager { + + private final SSLEngine engine; + + private final ByteBuffer myNetData; + private final ByteBuffer peerNetData; + + private final Executor executor; + private final NioSocketInputStream inputStream; + private final NioSocketOutputStream outputStream; + + public NioSocketSSLEngineManager(SSLEngine sslEngine, NioSocketHandler socket) { + 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); + } + + 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: + case NEED_UNWRAP_AGAIN: + handshakeNeedUnwrap(peerAppData); + break; + + case NEED_WRAP: + handshakeNeedWrap(myAppData); + break; + + case NEED_TASK: + executeTasks(); + break; + + case FINISHED: + case NOT_HANDSHAKING: + break; + } + } + + public void doHandshake() throws SSLException { + engine.beginHandshake(); + SSLEngineResult.HandshakeStatus handshakeStatus = engine.getHandshakeStatus(); + + int appBufSize = engine.getSession().getApplicationBufferSize(); + ByteBuffer peerAppData = ByteBuffer.allocate(appBufSize); + ByteBuffer myAppData = ByteBuffer.allocate(appBufSize); + + while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && + handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) { + handleHandshakeStatus(handshakeStatus, peerAppData, myAppData); + handshakeStatus = engine.getHandshakeStatus(); + } + } + + private void executeTasks() { + Runnable task; + while ((task = engine.getDelegatedTask()) != null) { + executor.execute(task); + } + } + + public int read(ByteBuffer data) throws IOException { + peerNetData.flip(); + SSLEngineResult result = engine.unwrap(peerNetData, data); + peerNetData.compact(); + + 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; + } + + public int write(ByteBuffer data) throws IOException { + int n = 0; + while (data.hasRemaining()) { + SSLEngineResult result = engine.wrap(data, myNetData); + n += result.bytesConsumed(); + switch (result.getStatus()) { + case OK: + myNetData.flip(); + outputStream.writeBytes(myNetData, myNetData.remaining()); + outputStream.flushWriteBuffer(); + myNetData.compact(); + break; + + case BUFFER_OVERFLOW: + myNetData.flip(); + outputStream.writeBytes(myNetData, myNetData.remaining()); + myNetData.compact(); + break; + + case CLOSED: + engine.closeOutbound(); + break; + + case BUFFER_UNDERFLOW: + break; + } + } + return n; + } + + public SSLSession getSession() { + return engine.getSession(); + } + +} 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 00000000000..66c18f09fd3 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketStream.java @@ -0,0 +1,89 @@ +// 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; + } + } + + protected void checkItemSizeOnBuffer(int itemSize) { + if (itemSize > 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 new file mode 100644 index 00000000000..15a3e15fd05 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/network/NioSocketTLSInputStream.java @@ -0,0 +1,73 @@ +// 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; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class NioSocketTLSInputStream extends NioSocketInputStream { + + private final NioSocketSSLEngineManager sslEngineManager; + + 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 readFromSSLEngineManager(byte[] buffer, int startPos, int length) { + try { + int readBytes = sslEngineManager.read(ByteBuffer.wrap(buffer, startPos, length)); + if (readBytes < 0) { + throw new CloudRuntimeException(String.format("Invalid number of read bytes frm SSL engine manager %s", + readBytes)); + } + return readBytes; + } catch (IOException e) { + s_logger.error(String.format("Error reading from SSL engine manager: %s", e.getMessage()), e); + } + return 0; + } + + @Override + protected int rearrangeBufferToFitSize(int numberItems, int itemSize, boolean wait) { + checkItemSizeOnBuffer(itemSize); + + 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 = readFromSSLEngineManager(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 00000000000..8ee01af059e --- /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 org.apache.log4j.Logger; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class NioSocketTLSOutputStream extends NioSocketOutputStream { + + private final NioSocketSSLEngineManager sslEngineManager; + + private static final Logger s_logger = Logger.getLogger(NioSocketTLSOutputStream.class); + + public NioSocketTLSOutputStream(NioSocketSSLEngineManager sslEngineManager, NioSocket socket) { + super(sslEngineManager.getSession().getApplicationBufferSize(), socket); + this.sslEngineManager = sslEngineManager; + } + + @Override + public void flushWriteBuffer() { + int sentUpTo = start; + while (sentUpTo < currentPosition) { + int n = writeThroughSSLEngineManager(buffer, sentUpTo, currentPosition - sentUpTo); + sentUpTo += n; + offset += n; + } + + currentPosition = start; + } + + protected int writeThroughSSLEngineManager(byte[] data, int startPos, int length) { + try { + return sslEngineManager.write(ByteBuffer.wrap(data, startPos, length)); + } catch (IOException e) { + s_logger.error(String.format("Error writing though SSL engine manager: %s", e.getMessage()), e); + return 0; + } + } + + @Override + protected int rearrangeWriteBuffer(int itemSize, int numberItems) { + 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/NoneVncSecurity.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/NoneVncSecurity.java new file mode 100644 index 00000000000..f9b088d1b9e --- /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 00000000000..3a394eb6339 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncAuthSecurity.java @@ -0,0 +1,59 @@ +// 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 com.cloud.utils.exception.CloudRuntimeException; + +import java.io.IOException; +import java.nio.ByteBuffer; + +public class VncAuthSecurity implements VncSecurity { + + private final String vmPass; + + private static final int VNC_AUTH_CHALLENGE_SIZE = 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(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 CloudRuntimeException("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 00000000000..2423e2be0d3 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncSecurity.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.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 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); + } + } + + 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 00000000000..c11be02a3c2 --- /dev/null +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/vnc/security/VncTLSSecurity.java @@ -0,0 +1,103 @@ +// 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.NioSocketSSLEngineManager; +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 NioSocketSSLEngineManager manager; + + private final String host; + private final int port; + + public VncTLSSecurity(String host, int port) { + this.host = host; + this.port = port; + } + + 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 (String s : supported) { + if (s.matches("TLS.*") || s.matches("X509.*")) { + enabled.add(s); + } + } + engine.setEnabledProtocols(enabled.toArray(new String[0])); + + 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 NioSocketSSLEngineManager(engine, socketHandler); + manager.doHandshake(); + } catch(java.lang.Exception e) { + throw new CloudRuntimeException(e.toString()); + } + } + + public NioSocketSSLEngineManager 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 00000000000..92665945eba --- /dev/null +++ b/services/console-proxy/server/src/test/java/com/cloud/consoleproxy/ConsoleProxyNoVncClientTest.java @@ -0,0 +1,32 @@ +// 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; +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 c7893b3205c..ca236a9e112 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 f503f55c6c9..05026acedb3 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; } @@ -494,9 +506,9 @@ 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 @@ -1101,9 +1113,9 @@ const UI = { let msg; if (UI.getSetting('encrypt')) { - msg = _("Connected"); + msg = _("Connected (encrypted) to ") + UI.desktopName; } else { - msg = _("Connected") + msg = _("Connected (unencrypted) to ") + UI.desktopName; } UI.showStatus(msg); UI.updateVisualState('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 c38e8e5e6f1..056e55f439a 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(); diff --git a/test/integration/smoke/test_vm_life_cycle.py b/test/integration/smoke/test_vm_life_cycle.py index d9878b09765..aef039c98eb 100644 --- a/test/integration/smoke/test_vm_life_cycle.py +++ b/test/integration/smoke/test_vm_life_cycle.py @@ -1009,6 +1009,7 @@ class TestSecuredVmMigration(cloudstackTestCase): 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 ; \