blob: 237135b5176b6f02f81c87ed7543f8f529056575 [file]
// 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 org.apache.cloudstack.consoleproxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
import org.apache.cloudstack.api.command.user.consoleproxy.ListConsoleSessionsCmd;
import org.apache.cloudstack.api.response.ConsoleSessionResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.joda.time.DateTime;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetExternalConsoleAnswer;
import com.cloud.agent.api.GetVmVncTicketAnswer;
import com.cloud.agent.api.GetVmVncTicketCommand;
import com.cloud.consoleproxy.ConsoleProxyManager;
import com.cloud.dc.DataCenter;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.resource.ResourceState;
import com.cloud.server.ManagementServer;
import com.cloud.servlet.ConsoleProxyClientParam;
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;
import com.cloud.utils.concurrency.NamedThreadFactory;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.GlobalLock;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.ConsoleSessionVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.ConsoleSessionDao;
import com.cloud.vm.dao.VMInstanceDetailsDao;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import static com.cloud.consoleproxy.ConsoleProxyManager.NoVncConsoleShowDot;
public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAccessManager {
@Inject
private AccountManager accountManager;
@Inject
private DomainDao domainDao;
@Inject
private VirtualMachineManager virtualMachineManager;
@Inject
private ManagementServer managementServer;
@Inject
private EntityManager entityManager;
@Inject
private VMInstanceDetailsDao vmInstanceDetailsDao;
@Inject
private KeysManager keysManager;
@Inject
private AgentManager agentManager;
@Inject
private ConsoleProxyManager consoleProxyManager;
@Inject
DataCenterDao dataCenterDao;
@Inject
private ConsoleSessionDao consoleSessionDao;
@Inject
private ResponseGenerator responseGenerator;
private ScheduledExecutorService executorService = null;
private static KeysManager secretKeysManager;
private final Gson gson = new GsonBuilder().create();
protected Logger logger = LogManager.getLogger(ConsoleAccessManagerImpl.class);
private static final List<VirtualMachine.State> unsupportedConsoleVMState = Arrays.asList(
VirtualMachine.State.Stopped, VirtualMachine.State.Restoring, VirtualMachine.State.Error, VirtualMachine.State.Destroyed
);
protected static final List<ResourceState> MAINTENANCE_RESOURCE_STATES = new ArrayList<>(Arrays.asList(
ResourceState.ErrorInMaintenance, ResourceState.ErrorInPrepareForMaintenance
));
protected static final String WEB_SOCKET_PATH= "websockify";
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
ConsoleAccessManagerImpl.secretKeysManager = keysManager;
executorService = Executors.newScheduledThreadPool(1, new NamedThreadFactory("ConsoleSession-Scavenger"));
return super.configure(name, params);
}
@Override
public boolean start() {
int consoleCleanupInterval = ConsoleAccessManager.ConsoleSessionCleanupInterval.value();
if (consoleCleanupInterval > 0) {
logger.info(String.format("The ConsoleSessionCleanupTask will run every %s hours", consoleCleanupInterval));
executorService.scheduleWithFixedDelay(new ConsoleSessionCleanupTask(), consoleCleanupInterval, consoleCleanupInterval, TimeUnit.HOURS);
}
return true;
}
@Override
public String getConfigComponentName() {
return ConsoleAccessManager.class.getName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[] {
ConsoleAccessManager.ConsoleSessionCleanupInterval,
ConsoleAccessManager.ConsoleSessionCleanupRetentionHours
};
}
public class ConsoleSessionCleanupTask extends ManagedContextRunnable {
@Override
protected void runInContext() {
final GlobalLock gcLock = GlobalLock.getInternLock("ConsoleSession.Cleanup.Lock");
try {
if (gcLock.lock(3)) {
try {
reallyRun();
} finally {
gcLock.unlock();
}
}
} finally {
gcLock.releaseRef();
}
}
private void reallyRun() {
if (logger.isDebugEnabled()) {
logger.debug("Starting ConsoleSessionCleanupTask...");
}
Integer retentionHours = ConsoleAccessManager.ConsoleSessionCleanupRetentionHours.value();
Date dateBefore = DateTime.now().minusHours(retentionHours).toDate();
if (logger.isDebugEnabled()) {
logger.debug(String.format("Retention hours: %s, checking for removed console session " +
"records to expunge older than: %s", retentionHours, dateBefore));
}
int sessionsExpunged = consoleSessionDao.expungeSessionsOlderThanDate(dateBefore);
if (logger.isDebugEnabled()) {
logger.debug(sessionsExpunged > 0 ?
String.format("Expunged %s removed console session records", sessionsExpunged) :
"No removed console session records expunged on this cleanup task run");
}
}
}
@Override
public ListResponse<ConsoleSessionResponse> listConsoleSessions(ListConsoleSessionsCmd cmd) {
Pair<List<ConsoleSessionVO>, Integer> consoleSessions = listConsoleSessionsInternal(cmd);
ListResponse<ConsoleSessionResponse> response = new ListResponse<>();
ResponseObject.ResponseView responseView = ResponseObject.ResponseView.Restricted;
Long callerId = CallContext.current().getCallingAccountId();
if (accountManager.isRootAdmin(callerId)) {
responseView = ResponseObject.ResponseView.Full;
}
List<ConsoleSessionResponse> consoleSessionResponses = new ArrayList<>();
for (ConsoleSessionVO consoleSession : consoleSessions.first()) {
ConsoleSessionResponse consoleSessionResponse = responseGenerator.createConsoleSessionResponse(consoleSession, responseView);
consoleSessionResponses.add(consoleSessionResponse);
}
response.setResponses(consoleSessionResponses, consoleSessions.second());
return response;
}
protected Pair<List<ConsoleSessionVO>, Integer> listConsoleSessionsInternal(ListConsoleSessionsCmd cmd) {
CallContext caller = CallContext.current();
long domainId = getBaseDomainIdToListConsoleSessions(cmd.getDomainId());
Long accountId = cmd.getAccountId();
Long userId = cmd.getUserId();
boolean isRecursive = cmd.isRecursive();
boolean isCallerNormalUser = accountManager.isNormalUser(caller.getCallingAccountId());
if (isCallerNormalUser) {
accountId = caller.getCallingAccountId();
userId = caller.getCallingUserId();
}
List<Long> domainIds = isRecursive ? domainDao.getDomainAndChildrenIds(domainId) : List.of(domainId);
return consoleSessionDao.listConsoleSessions(cmd.getId(), domainIds, accountId, userId,
cmd.getHostId(), cmd.getStartDate(), cmd.getEndDate(), cmd.getVmId(),
cmd.getConsoleEndpointCreatorAddress(), cmd.getClientAddress(), cmd.isActiveOnly(),
cmd.getAcquired(), cmd.getPageSizeVal(), cmd.getStartIndex());
}
/**
* Determines the base domain ID for listing console sessions.
*
* If no domain ID is provided, returns the caller's domain ID. Otherwise,
* checks if the caller has access to that domain and returns the provided domain ID.
*
* @param domainId The domain ID to check, can be null
* @return The base domain ID to use for listing console sessions
* @throws PermissionDeniedException if the caller does not have access to the specified domain
*/
protected long getBaseDomainIdToListConsoleSessions(Long domainId) {
Account caller = CallContext.current().getCallingAccount();
if (domainId == null) {
return caller.getDomainId();
}
Domain domain = domainDao.findById(domainId);
if (domain == null) {
throw new InvalidParameterValueException(String.format("Unable to find domain with ID [%s]. Verify the informed domain and try again.", domainId));
}
accountManager.checkAccess(caller, domain);
return domainId;
}
@Override
public ConsoleSession listConsoleSessionById(long id) {
return consoleSessionDao.findByIdIncludingRemoved(id);
}
@Override
public ConsoleEndpoint generateConsoleEndpoint(Long vmId, String extraSecurityToken, String clientAddress) {
try {
if (ObjectUtils.anyNull(accountManager, virtualMachineManager, managementServer)) {
return new ConsoleEndpoint(false, null, "Console service is not ready");
}
if (keysManager.getHashKey() == null) {
String msg = "Console access denied. Ticket service is not ready yet";
logger.debug(msg);
return new ConsoleEndpoint(false, null, msg);
}
Account account = CallContext.current().getCallingAccount();
// Do a sanity check here to make sure the user hasn't already been deleted
if (account == null) {
logger.debug("Invalid user/account, reject console access");
return new ConsoleEndpoint(false, null,"Access denied. Invalid or inconsistent account is found");
}
VirtualMachine vm = entityManager.findById(VirtualMachine.class, vmId);
if (vm == null) {
logger.info("Invalid console servlet command parameter: " + vmId);
return new ConsoleEndpoint(false, null, "Cannot find VM with ID " + vmId);
}
if (!checkSessionPermission(vm, account)) {
return new ConsoleEndpoint(false, null, "Permission denied");
}
DataCenter zone = dataCenterDao.findById(vm.getDataCenterId());
if (zone != null && DataCenter.Type.Edge.equals(zone.getType())) {
String errorMsg = "Console access is not supported for Edge zones";
logger.error(errorMsg);
return new ConsoleEndpoint(false, null, errorMsg);
}
String sessionUuid = UUID.randomUUID().toString();
return generateAccessEndpoint(vmId, sessionUuid, extraSecurityToken, clientAddress);
} catch (Exception e) {
String errorMsg = String.format("Unexpected exception in ConsoleAccessManager - vmId: %s (%s), clientAddress: %s",
vmId, entityManager.findById(VirtualMachine.class, vmId), clientAddress);
logger.error(errorMsg, e);
return new ConsoleEndpoint(false, null, "Server Internal Error: " + e.getMessage());
}
}
@Override
public boolean isSessionAllowed(String sessionUuid) {
return consoleSessionDao.isSessionAllowed(sessionUuid);
}
@Override
public void removeSessions(String[] sessionUuids) {
if (ArrayUtils.isNotEmpty(sessionUuids)) {
for (String sessionUuid : sessionUuids) {
removeSession(sessionUuid);
}
}
}
protected void removeSession(String sessionUuid) {
consoleSessionDao.removeSession(sessionUuid);
}
@Override
public void acquireSession(String sessionUuid, String clientAddress) {
consoleSessionDao.acquireSession(sessionUuid, clientAddress);
}
protected boolean checkSessionPermission(VirtualMachine vm, Account account) {
if (accountManager.isRootAdmin(account.getId())) {
return true;
}
switch (vm.getType()) {
case User:
try {
accountManager.checkAccess(account, null, true, vm);
} catch (PermissionDeniedException ex) {
if (accountManager.isNormalUser(account.getId())) {
if (logger.isDebugEnabled()) {
logger.debug("VM access is denied for VM {}. VM owner " +
"account {} does not match the account id in session {} and " +
"caller is a normal user", vm,
accountManager.getAccount(vm.getAccountId()), account);
}
} else if ((accountManager.isDomainAdmin(account.getId())
|| account.getType() == Account.Type.READ_ONLY_ADMIN) && logger.isDebugEnabled()) {
logger.debug("VM access is denied for VM {}. VM owner account {}" +
" does not match the account id in session {} and the " +
"domain-admin caller does not manage the target domain",
vm, accountManager.getAccount(vm.getAccountId()), account);
}
return false;
}
break;
case DomainRouter:
case ConsoleProxy:
case SecondaryStorageVm:
return false;
default:
logger.warn("Unrecoginized virtual machine type, deny access by default. type: " + vm.getType());
return false;
}
return true;
}
private ConsoleEndpoint generateAccessEndpoint(Long vmId, String sessionUuid, String extraSecurityToken, String clientAddress) {
VirtualMachine vm = virtualMachineManager.findById(vmId);
String msg;
if (vm == null) {
msg = "VM " + vmId + " does not exist, sending blank response for console access request";
logger.warn(msg);
throw new CloudRuntimeException(msg);
}
if (unsupportedConsoleVMState.contains(vm.getState())) {
msg = String.format("VM %s must be running to connect console, sending blank response for console access request", vm);
logger.warn(msg);
throw new CloudRuntimeException(msg);
}
Long hostId = vm.getState() != VirtualMachine.State.Migrating ? vm.getHostId() : vm.getLastHostId();
if (hostId == null) {
msg = String.format("VM %s lost host info, sending blank response for console access request", vm);
logger.warn(msg);
throw new CloudRuntimeException(msg);
}
HostVO host = managementServer.getHostBy(hostId);
if (host == null) {
msg = String.format("Host for VM %s does not exist, sending blank response for console access request", vm);
logger.warn(msg);
throw new CloudRuntimeException(msg);
}
if (Hypervisor.HypervisorType.LXC.equals(vm.getHypervisorType())) {
throw new CloudRuntimeException("Console access is not supported for LXC");
}
String rootUrl = managementServer.getConsoleAccessUrlRoot(vmId);
if (rootUrl == null) {
throw new CloudRuntimeException("Console access will be ready in a few minutes. Please try it again later.");
}
ConsoleEndpoint consoleEndpoint = composeConsoleAccessEndpoint(rootUrl, vm, host, clientAddress, sessionUuid, extraSecurityToken);
logger.debug("The console URL is: " + consoleEndpoint.getUrl());
return consoleEndpoint;
}
protected ConsoleConnectionDetails getConsoleConnectionDetailsForExternalVm(ConsoleConnectionDetails details,
VirtualMachine vm, HostVO host) {
Answer answer = managementServer.getExternalVmConsole(vm, host);
if (answer == null) {
logger.error("Unable to get console access details for external {} on {}: answer is null.", vm, host);
return null;
}
if (!answer.getResult()) {
logger.error("Unable to get console access details for external {} on {}: answer result is false. Reason: {}", vm, host, answer.getDetails());
return null;
}
if (!(answer instanceof GetExternalConsoleAnswer)) {
logger.error("Unable to get console access details for external {} on {}: answer is not of type GetExternalConsoleAnswer.", vm, host);
return null;
}
GetExternalConsoleAnswer getExternalConsoleAnswer = (GetExternalConsoleAnswer) answer;
details.setModeFromExternalProtocol(getExternalConsoleAnswer.getProtocol());
details.setDirectUrl(getExternalConsoleAnswer.getUrl());
details.setHost(getExternalConsoleAnswer.getHost());
if (getExternalConsoleAnswer.getPort() != null) {
details.setPort(getExternalConsoleAnswer.getPort());
}
if (StringUtils.isNotBlank(getExternalConsoleAnswer.getPassword())) {
details.setSid(getExternalConsoleAnswer.getPassword());
details.setSessionRequiresNewViewer(getExternalConsoleAnswer.isPasswordOneTimeUseOnly());
}
return details;
}
protected Pair<String, Integer> getHostAndPortForKVMMaintenanceHostIfNeeded(HostVO host,
Map<String, String> vmDetails) {
if (!Hypervisor.HypervisorType.KVM.equals(host.getHypervisorType())) {
return null;
}
if(!MAINTENANCE_RESOURCE_STATES.contains(host.getResourceState())) {
return null;
}
String address = vmDetails.get(VmDetailConstants.KVM_VNC_ADDRESS);
String port = vmDetails.get(VmDetailConstants.KVM_VNC_PORT);
if (ObjectUtils.allNotNull(address, port)) {
return new Pair<>(address, Integer.valueOf(port));
}
logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " +
"no VNC Address/Port was available. Falling back to default one from MS.");
return null;
}
protected ConsoleConnectionDetails getConsoleConnectionDetails(VirtualMachine vm, HostVO host) {
String locale = null;
String tag = vm.getUuid();
String displayName = vm.getHostName();
if (vm instanceof UserVm) {
displayName = ((UserVm) vm).getDisplayName();
}
Map<String, String> vmDetails = vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId(),
List.of(VmDetailConstants.KEYBOARD, VmDetailConstants.KVM_VNC_ADDRESS, VmDetailConstants.KVM_VNC_PORT));
if (vmDetails.get(VmDetailConstants.KEYBOARD) != null) {
locale = vmDetails.get(VmDetailConstants.KEYBOARD);
}
ConsoleConnectionDetails details = new ConsoleConnectionDetails(vm.getVncPassword(), locale, tag, displayName);
if (Hypervisor.HypervisorType.External.equals(host.getHypervisorType())) {
return getConsoleConnectionDetailsForExternalVm(details, vm, host);
}
Pair<String, Integer> hostPortInfo = getHostAndPortForKVMMaintenanceHostIfNeeded(host, vmDetails);
if (hostPortInfo == null) {
hostPortInfo = managementServer.getVncPort(vm);
}
logger.debug("Retrieved VNC host and port info :[{}, {}] for {} on {}", hostPortInfo.first(),
hostPortInfo.second(), vm, host);
Ternary<String, String, String> parsedHostInfo = parseHostInfo(hostPortInfo.first());
details.setHost(parsedHostInfo.first());
details.setTunnelUrl(parsedHostInfo.second());
details.setTunnelSession(parsedHostInfo.third());
details.setPort(hostPortInfo.second());
if (hostPortInfo.second() == -9) {
details.setUsingRDP(true);
details.setPort(Integer.parseInt(managementServer.findDetail(host.getId(), "rdp.server.port")
.getValue()));
logger.debug("HyperV RDP port for {} on {} is: {}", vm, host, details.getPort());
}
return details;
}
protected ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMachine vm, HostVO hostVo, String addr,
String sessionUuid, String extraSecurityToken) {
ConsoleConnectionDetails result = getConsoleConnectionDetails(vm, hostVo);
if (result == null) {
return new ConsoleEndpoint(false, null, "Console access to this instance cannot be provided");
}
if (ConsoleConnectionDetails.Mode.Direct.equals(result.getMode())) {
persistConsoleSession(sessionUuid, vm.getId(), hostVo.getId(), addr);
return new ConsoleEndpoint(true, result.getDirectUrl());
}
String ticket = genAccessTicket(result.getHost(), String.valueOf(result.getPort()), result.getSid(),
result.getTag(), sessionUuid);
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
ConsoleProxyClientParam param = generateConsoleProxyClientParam(result, ticket, sessionUuid, addr,
extraSecurityToken, vm, hostVo);
String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param);
int vncPort = consoleProxyManager.getVncPort(vm.getDataCenterId());
String url = generateConsoleAccessUrl(rootUrl, param, token, vncPort, vm, hostVo, result.getLocale());
logger.debug("Adding allowed session: {}", sessionUuid);
persistConsoleSession(sessionUuid, vm.getId(), hostVo.getId(), addr);
managementServer.setConsoleAccessForVm(vm.getId(), sessionUuid);
ConsoleEndpoint consoleEndpoint = new ConsoleEndpoint(true, url);
consoleEndpoint.setWebsocketHost(managementServer.getConsoleAccessAddress(vm.getId()));
consoleEndpoint.setWebsocketPort(String.valueOf(vncPort));
consoleEndpoint.setWebsocketPath(WEB_SOCKET_PATH);
consoleEndpoint.setWebsocketToken(token);
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
consoleEndpoint.setWebsocketExtra(param.getExtraSecurityToken());
}
return consoleEndpoint;
}
protected void persistConsoleSession(String sessionUuid, long instanceId, long hostId, String consoleEndpointCreatorAddress) {
CallContext caller = CallContext.current();
ConsoleSessionVO consoleSessionVo = new ConsoleSessionVO();
consoleSessionVo.setUuid(sessionUuid);
consoleSessionVo.setDomainId(caller.getCallingAccount().getDomainId());
consoleSessionVo.setAccountId(caller.getCallingAccountId());
consoleSessionVo.setUserId(caller.getCallingUserId());
consoleSessionVo.setInstanceId(instanceId);
consoleSessionVo.setHostId(hostId);
consoleSessionVo.setConsoleEndpointCreatorAddress(consoleEndpointCreatorAddress);
consoleSessionDao.persist(consoleSessionVo);
}
protected String generateConsoleAccessUrl(String rootUrl, ConsoleProxyClientParam param, String token, int vncPort,
VirtualMachine vm, HostVO hostVo, String locale) {
StringBuilder sb = new StringBuilder(rootUrl);
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
sb.append("/ajax?token=").append(token);
} else {
String showDot = NoVncConsoleShowDot.valueIn(vm.getDataCenterId()) ? "true" : "false";
sb.append("/resource/noVNC/vnc.html")
.append("?autoconnect=true&show_dot=").append(showDot)
.append("&port=").append(vncPort)
.append("&token=").append(token);
if (requiresVncOverWebSocketConnection(vm, hostVo) && StringUtils.isNotBlank(locale)) {
sb.append("&language=").append(locale);
}
}
if (StringUtils.isNotBlank(param.getExtraSecurityToken())) {
sb.append("&extra=").append(param.getExtraSecurityToken());
}
// for console access, we need guest OS type to help implement keyboard
long guestOs = vm.getGuestOSId();
GuestOSVO guestOsVo = managementServer.getGuestOs(guestOs);
if (guestOsVo.getCategoryId() == 6)
sb.append("&guest=windows");
if (logger.isDebugEnabled()) {
logger.debug("Compose console url: {}", sb);
}
return sb.toString().startsWith("https") ? sb.toString() : "http:" + sb;
}
protected ConsoleProxyClientParam generateConsoleProxyClientParam(ConsoleConnectionDetails details, String ticket,
String sessionUuid, String addr, String extraSecurityToken, VirtualMachine vm, HostVO host) {
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
param.setClientHostAddress(details.getHost());
param.setClientHostPort(details.getPort());
param.setClientHostPassword(details.getSid());
param.setClientTag(details.getTag());
param.setClientDisplayName(details.getDisplayName());
param.setTicket(ticket);
param.setSessionUuid(sessionUuid);
param.setSourceIP(addr);
param.setSessionRequiresNewViewer(details.isSessionRequiresNewViewer());
if (StringUtils.isNotBlank(extraSecurityToken)) {
param.setExtraSecurityToken(extraSecurityToken);
logger.debug("Added security token for client validation");
}
if (requiresVncOverWebSocketConnection(vm, host)) {
setWebsocketUrl(vm, param);
}
if (details.getLocale() != null) {
param.setLocale(details.getLocale());
}
if (details.isUsingRDP()) {
//For Hyperv Client Host Address will send Instance id
param.setHypervHost(host.getPrivateIpAddress());
param.setUsername(managementServer.findDetail(host.getId(), "username").getValue());
param.setPassword(managementServer.findDetail(host.getId(), "password").getValue());
}
if (ObjectUtils.allNotNull(details.getTunnelUrl(), details.getTunnelSession())) {
param.setClientTunnelUrl(details.getTunnelUrl());
param.setClientTunnelSession(details.getTunnelSession());
}
return param;
}
public Ternary<String, String, String> parseHostInfo(String hostInfo) {
String host = null;
String tunnelUrl = null;
String tunnelSession = null;
logger.info("Parse host info returned from executing GetVNCPortCommand. host info: " + hostInfo);
if (hostInfo != null) {
if (hostInfo.startsWith("consoleurl")) {
String[] tokens = hostInfo.split("&");
if (hostInfo.length() > 19 && hostInfo.indexOf('/', 19) > 19) {
host = hostInfo.substring(19, hostInfo.indexOf('/', 19)).trim();
tunnelUrl = tokens[0].substring("consoleurl=".length());
tunnelSession = tokens[1].split("=")[1];
} else {
host = "";
}
} else if (hostInfo.startsWith("instanceId")) {
host = hostInfo.substring(hostInfo.indexOf('=') + 1);
} else {
host = hostInfo;
}
} else {
host = hostInfo;
}
return new Ternary<>(host, tunnelUrl, tunnelSession);
}
/**
* Since VMware 7.0 VNC servers are deprecated, it uses a ticket to create a VNC over websocket connection
* Check: https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
*/
private boolean requiresVncOverWebSocketConnection(VirtualMachine vm, HostVO hostVo) {
return vm.getHypervisorType() == Hypervisor.HypervisorType.VMware && hostVo.getHypervisorVersion().compareTo("7.0") >= 0;
}
@Override
public String genAccessTicket(String host, String port, String sid, String tag, String sessionUuid) {
return genAccessTicket(host, port, sid, tag, new Date(), sessionUuid);
}
@Override
public String genAccessTicket(String host, String port, String sid, String tag, Date normalizedHashTime, String sessionUuid) {
String params = "host=" + host + "&port=" + port + "&sid=" + sid + "&tag=" + tag + "&session=" + sessionUuid;
try {
Mac mac = Mac.getInstance("HmacSHA512");
long ts = normalizedHashTime.getTime();
ts = ts / 60000; // round up to 1 minute
String secretKey = secretKeysManager.getHashKey();
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), mac.getAlgorithm());
mac.init(keySpec);
mac.update(params.getBytes());
mac.update(String.valueOf(ts).getBytes());
byte[] encryptedBytes = mac.doFinal();
return Base64.encodeBase64String(encryptedBytes);
} catch (Exception e) {
logger.error("Unexpected exception ", e);
}
return "";
}
protected String getEncryptorPassword() {
String key = keysManager.getEncryptionKey();
String iv = keysManager.getEncryptionIV();
ConsoleProxyPasswordBasedEncryptor.KeyIVPair keyIvPair = new ConsoleProxyPasswordBasedEncryptor.KeyIVPair(key, iv);
return gson.toJson(keyIvPair);
}
/**
* Sets the URL to establish a VNC over websocket connection
*/
private void setWebsocketUrl(VirtualMachine vm, ConsoleProxyClientParam param) {
String ticket = acquireVncTicketForVmwareVm(vm);
if (StringUtils.isBlank(ticket)) {
logger.error(String.format("Could not obtain VNC ticket for VM %s", vm));
return;
}
String wsUrl = composeWebsocketUrlForVmwareVm(ticket, param);
param.setWebsocketUrl(wsUrl);
}
/**
* Format expected: wss://<ESXi_HOST_IP>:443/ticket/<TICKET_ID>
*/
private String composeWebsocketUrlForVmwareVm(String ticket, ConsoleProxyClientParam param) {
param.setClientHostPort(443);
return String.format("wss://%s:%s/ticket/%s", param.getClientHostAddress(), param.getClientHostPort(), ticket);
}
/**
* Acquires a ticket to be used for console proxy as described in 'Removal of VNC Server from ESXi' on:
* https://docs.vmware.com/en/VMware-vSphere/7.0/rn/vsphere-esxi-vcenter-server-70-release-notes.html
*/
private String acquireVncTicketForVmwareVm(VirtualMachine vm) {
try {
logger.info("Acquiring VNC ticket for VM = {}", vm);
GetVmVncTicketCommand cmd = new GetVmVncTicketCommand(vm.getInstanceName());
Answer answer = agentManager.send(vm.getHostId(), cmd);
GetVmVncTicketAnswer ans = (GetVmVncTicketAnswer) answer;
if (!ans.getResult()) {
logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails());
}
return ans.getTicket();
} catch (AgentUnavailableException | OperationTimedoutException e) {
logger.error("Error acquiring ticket", e);
return null;
}
}
protected static class ConsoleConnectionDetails {
public enum Mode {
ConsoleProxy,
Direct
}
private Mode mode = Mode.ConsoleProxy;
private String host;
private int port = -1;
private String sid;
private String locale;
private String tag;
private String displayName;
private String tunnelUrl = null;
private String tunnelSession = null;
private boolean usingRDP;
private String directUrl;
private boolean sessionRequiresNewViewer = false;
ConsoleConnectionDetails(String sid, String locale, String tag, String displayName) {
this.sid = sid;
this.locale = locale;
this.tag = tag;
this.displayName = displayName;
}
public Mode getMode() {
return mode;
}
public void setModeFromExternalProtocol(String protocol) {
this.mode = Mode.ConsoleProxy;
if (StringUtils.isBlank(protocol)) {
return;
}
if (Mode.Direct.name().toLowerCase().equalsIgnoreCase(protocol)) {
this.mode = Mode.Direct;
}
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getSid() {
return sid;
}
public void setSid(String sid) {
this.sid = sid;
}
public String getLocale() {
return locale;
}
public String getTag() {
return tag;
}
public String getDisplayName() {
return displayName;
}
public String getTunnelUrl() {
return tunnelUrl;
}
public void setTunnelUrl(String tunnelUrl) {
this.tunnelUrl = tunnelUrl;
}
public String getTunnelSession() {
return tunnelSession;
}
public void setTunnelSession(String tunnelSession) {
this.tunnelSession = tunnelSession;
}
public boolean isUsingRDP() {
return usingRDP;
}
public void setUsingRDP(boolean usingRDP) {
this.usingRDP = usingRDP;
}
public String getDirectUrl() {
return directUrl;
}
public void setDirectUrl(String directUrl) {
this.directUrl = directUrl;
}
public boolean isSessionRequiresNewViewer() {
return sessionRequiresNewViewer;
}
public void setSessionRequiresNewViewer(boolean sessionRequiresNewViewer) {
this.sessionRequiresNewViewer = sessionRequiresNewViewer;
}
}
}