mirror of https://github.com/apache/cloudstack.git
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:
parent
11f38ad102
commit
b0c47f4a97
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ; \
|
||||
|
|
|
|||
Loading…
Reference in New Issue