FR65: Secure KVM VNC connection using CA framework (#217)

* Support RFB 3.8 and VNC auth working

* IN PROGRESS: Add TigerVNC classes and authenticate, work left on the messaging

* Fix console display

* Cleanup

* Last in-progress work

* Don't block reads in case stream empty, use Link to init client certs (#215)

Co-authored-by: Marcus Sorensen <mls@apple.com>

* Unused files and methods cleanup

* Rewrite finished

* More cleanup

* Rename console server session to VM display name (#216)

* Rename console server session to VM display name

* Fix after rebasing

* Add encryption bar when available

Co-authored-by: Marcus Sorensen <mls@apple.com>
Co-authored-by: Nicolas Vazquez <nicovazquez90@gmail.com>

* Add missing license header

* Remove unused variable from the TLS security

* Address review comments and sonar cloud reports

* Address more review comments

* Last sonarcloud code smell fixes

* Automate VNC TLS provisioning and improve UI

* Add missing cases for sonarcloud report

* Fix certs renewal issue on configuring TLS

* Address review comments

* Refactor serviceConfig script

* Fix certs propagation TLS conf

* Fix unsecure host tests

Co-authored-by: Marcus Sorensen <marcus_sorensen@apple.com>
Co-authored-by: Marcus Sorensen <mls@apple.com>
This commit is contained in:
Nicolas Vazquez 2023-01-19 08:35:16 -03:00 committed by GitHub
parent 11f38ad102
commit b0c47f4a97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1820 additions and 70 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

@ -584,6 +584,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:
@ -627,6 +644,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")
@ -663,6 +681,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")
@ -707,6 +726,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

@ -88,6 +88,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;
@ -81,6 +82,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;
@ -274,6 +275,10 @@ 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());
@ -282,6 +287,7 @@ public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAcce
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;
@ -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

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

@ -16,7 +16,7 @@
// under the License.
package com.cloud.consoleproxy;
import com.cloud.utils.StringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.extensions.Frame;
@ -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,14 @@ 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));
}
} else {
b = new byte[100];
readBytes = client.read(b);
@ -127,7 +135,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
}
connectionAlive = false;
} catch (IOException e) {
e.printStackTrace();
s_logger.error("Error on VNC client", e);
}
}
@ -137,18 +145,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 +265,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

@ -1012,6 +1012,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 ; \