kvm: Secure KVM VNC Console Access Using the CA Framework (#7015)

This PR allows securing the console access through CloudStack to the virtual machines running on KVM. The secure access is achieved through the generated certificates for the CA Framework in CloudStack, that provides mutual TLS connections between agents. These certificates are used to also secure the connection between the console proxies and the VNC ports for VM console access.

This feature is only supported on the KVM hypervisor

Design Document: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Secure+KVM+VNC+connection+using+the+CA+framework
This commit is contained in:
Nicolas Vazquez 2023-01-27 08:52:06 -03:00 committed by GitHub
parent 61a722548f
commit eac357cb77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1823 additions and 71 deletions

View File

@ -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)

View File

@ -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"):

View File

@ -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

View File

@ -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;
}

View File

@ -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<String, Integer> portInfo, String host) {
Pair<String, Integer> 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);

View File

@ -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);

View File

@ -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);

View File

@ -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;
}

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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<VncSecurity> 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<Boolean, String> 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<Integer> 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<Boolean, String> 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<Boolean, String> 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<VncSecurity> 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);
}
}
}

View File

@ -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.

View File

@ -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<SelectionKey> 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;
}
}
}

View File

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

View File

@ -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;
}
}

View File

@ -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<Integer, byte[]> 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<Integer, byte[]> 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<Integer, byte[]> widthPair = readAndCopyUnsignedInteger(16);
bytesRead = ArrayUtils.addAll(bytesRead, widthPair.second());
Pair<Integer, byte[]> heightPair = readAndCopyUnsignedInteger(16);
bytesRead = ArrayUtils.addAll(bytesRead, heightPair.second());
byte[] pixelFormatByteArr = readPixelFormat();
bytesRead = ArrayUtils.addAll(bytesRead, pixelFormatByteArr);
Pair<Integer, byte[]> 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<Integer, byte[]> 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;
}
}
}

View File

@ -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;
}
}
}

View File

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

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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");
}
}

View File

@ -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<VncSecurity> 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;
}

View File

@ -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<String> 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;
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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();

View File

@ -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 ; \