Console access enhancements

This commit is contained in:
nvazquez 2022-07-26 11:45:28 -03:00
parent d177678fd3
commit 15b740d397
No known key found for this signature in database
GPG Key ID: 656E1BCC8CB54F84
36 changed files with 1080 additions and 271 deletions

View File

@ -382,9 +382,10 @@ public class ConsoleProxyResource extends ServerResourceBase implements ServerRe
}
}
public String authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket, Boolean isReauthentication) {
public String authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket,
Boolean isReauthentication, String sessionToken) {
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(host, port, vmId, sid, ticket);
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(host, port, vmId, sid, ticket, sessionToken);
cmd.setReauthenticating(isReauthentication);
ConsoleProxyAuthenticationResult result = new ConsoleProxyAuthenticationResult();

View File

@ -59,6 +59,7 @@ public class VirtualMachineTO {
boolean enableDynamicallyScaleVm;
String vncPassword;
String vncAddr;
String vncPort;
Map<String, String> params;
String uuid;
String bootType;
@ -283,6 +284,14 @@ public class VirtualMachineTO {
this.vncAddr = vncAddr;
}
public String getVncPort() {
return vncPort;
}
public void setVncPort(String vncPort) {
this.vncPort = vncPort;
}
public Map<String, String> getDetails() {
return params;
}

View File

@ -0,0 +1,58 @@
// 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.api.command.user.consoleproxy;
public class ConsoleEndpoint {
private boolean result;
private String details;
private String url;
public ConsoleEndpoint(boolean result, String url) {
this.result = result;
this.url = url;
}
public ConsoleEndpoint(boolean result, String url, String details) {
this(result, url);
this.details = details;
}
public boolean isResult() {
return result;
}
public void setResult(boolean result) {
this.result = result;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
}

View File

@ -0,0 +1,99 @@
// 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.api.command.user.consoleproxy;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.CreateConsoleUrlResponse;
import org.apache.cloudstack.api.response.UserVmResponse;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.log4j.Logger;
import javax.inject.Inject;
import java.util.Map;
@APICommand(name = CreateConsoleEndpointCmd.APINAME, description = "Create a console endpoint to connect to a VM console",
responseObject = CreateConsoleUrlResponse.class, since = "4.18.0",
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class CreateConsoleEndpointCmd extends BaseCmd {
public static final String APINAME = "createConsoleEndpoint";
public static final Logger s_logger = Logger.getLogger(CreateConsoleEndpointCmd.class.getName());
@Inject
private ConsoleAccessManager consoleManager;
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
type = CommandType.UUID,
entityType = UserVmResponse.class,
required = true,
description = "ID of the VM")
private Long vmId;
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
String clientSecurityToken = getClientSecurityToken();
String clientAddress = getClientAddress();
ConsoleEndpoint endpoint = consoleManager.generateConsoleEndpoint(vmId, clientSecurityToken, clientAddress);
if (endpoint != null) {
CreateConsoleUrlResponse response = new CreateConsoleUrlResponse();
response.setResult(endpoint.isResult());
response.setDetails(endpoint.getDetails());
response.setUrl(endpoint.getUrl());
response.setResponseName(getCommandName());
response.setObjectName("consoleendpoint");
setResponseObject(response);
} else {
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Unable to generate console endpoint for vm " + vmId);
}
}
private String getParameterBase(String paramKey) {
Map<String, String> params = getFullUrlParams();
return MapUtils.isNotEmpty(params) && params.containsKey(paramKey) ? params.get(paramKey) : null;
}
private String getClientAddress() {
return getParameterBase(ConsoleAccessUtils.CLIENT_INET_ADDRESS_KEY);
}
private String getClientSecurityToken() {
return getParameterBase(ConsoleAccessUtils.CLIENT_SECURITY_HEADER_PARAM_KEY);
}
@Override
public String getCommandName() {
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
}
@Override
public long getEntityOwnerId() {
return CallContext.current().getCallingAccount().getId();
}
}

View File

@ -0,0 +1,97 @@
// 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.api.response;
import com.cloud.serializer.Param;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseResponse;
public class CreateConsoleUrlResponse extends BaseResponse {
@SerializedName(ApiConstants.RESULT)
@Param(description = "true if the console endpoint is generated properly")
private Boolean result;
@SerializedName(ApiConstants.DETAILS)
@Param(description = "details in case of an error")
private String details;
@SerializedName(ApiConstants.IP_ADDRESS)
@Param(description = "the console ip address")
private String ipAddress;
@SerializedName(ApiConstants.PORT)
@Param(description = "the console port")
private String port;
@SerializedName(ApiConstants.TOKEN)
@Param(description = "the console token")
private String token;
@SerializedName(ApiConstants.URL)
@Param(description = "the console url")
private String url;
public Boolean getResult() {
return result;
}
public void setResult(Boolean result) {
this.result = result;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public String getIpAddress() {
return ipAddress;
}
public void setIpAddress(String ip) {
this.ipAddress = ip;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}

View File

@ -0,0 +1,43 @@
// 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 com.cloud.utils.component.Manager;
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.Configurable;
public interface ConsoleAccessManager extends Manager, Configurable {
ConfigKey<String> ConsoleProxySchema = new ConfigKey<>("Advanced", String.class,
"consoleproxy.schema", "http",
"The http/https schema to be used by the console proxy URLs", true);
ConfigKey<Boolean> ConsoleProxyExtraSecurityHeaderEnabled = new ConfigKey<>("Advanced", Boolean.class,
"consoleproxy.extra.security.header.enabled", "false",
"Enable/disable extra security validation for console proxy using client header", true);
ConfigKey<String> ConsoleProxyExtraSecurityHeaderName = new ConfigKey<>("Advanced", String.class,
"consoleproxy.extra.security.header.name", "SECURITY_TOKEN",
"A client header for extra security validation when using the console proxy", true);
ConsoleEndpoint generateConsoleEndpoint(Long vmId, String clientSecurityToken, String clientAddress);
boolean isSessionAllowed(String sessionUuid);
void removeSessions(String[] sessionUuids);
}

View File

@ -26,6 +26,7 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
private String _vmId;
private String _sid;
private String _ticket;
private String sessionUuid;
private boolean _isReauthenticating;
@ -33,12 +34,14 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
_isReauthenticating = false;
}
public ConsoleAccessAuthenticationCommand(String host, String port, String vmId, String sid, String ticket) {
public ConsoleAccessAuthenticationCommand(String host, String port, String vmId, String sid, String ticket,
String sessiontkn) {
_host = host;
_port = port;
_vmId = vmId;
_sid = sid;
_ticket = ticket;
sessionUuid = sessiontkn;
}
public String getHost() {
@ -68,4 +71,12 @@ public class ConsoleAccessAuthenticationCommand extends AgentControlCommand {
public void setReauthenticating(boolean value) {
_isReauthenticating = value;
}
public String getSessionUuid() {
return sessionUuid;
}
public void setSessionUuid(String sessionUuid) {
this.sessionUuid = sessionUuid;
}
}

View File

@ -26,6 +26,7 @@ public class ConsoleProxyConnectionInfo {
public String tag;
public long createTime;
public long lastUsedTime;
public String sessionUuid;
public ConsoleProxyConnectionInfo() {
}

View File

@ -21,6 +21,7 @@ package com.cloud.info;
public class ConsoleProxyStatus {
private ConsoleProxyConnectionInfo[] connections;
private String[] removedSessions;
public ConsoleProxyStatus() {
}
@ -28,4 +29,8 @@ public class ConsoleProxyStatus {
public ConsoleProxyConnectionInfo[] getConnections() {
return connections;
}
public String[] getRemovedSessions() {
return removedSessions;
}
}

View File

@ -19,6 +19,7 @@
package com.cloud.hypervisor.kvm.resource.wrapper;
import java.io.File;
import java.net.URISyntaxException;
import org.apache.log4j.Logger;
@ -42,11 +43,15 @@ import com.cloud.resource.CommandWrapper;
import com.cloud.resource.ResourceWrapper;
import com.cloud.vm.UserVmManager;
import com.cloud.vm.VirtualMachine;
import com.cloud.utils.ssh.SshHelper;
@ResourceWrapper(handles = StartCommand.class)
public final class LibvirtStartCommandWrapper extends CommandWrapper<StartCommand, Answer, LibvirtComputingResource> {
private static final Logger s_logger = Logger.getLogger(LibvirtStartCommandWrapper.class);
private static final int sshPort = Integer.parseInt(LibvirtComputingResource.DEFAULTDOMRSSHPORT);
private static final File pemFile = new File(LibvirtComputingResource.SSHPRVKEYPATH);
private static final String vncConfFileLocation = "/root/vncport";
@Override
public Answer execute(final StartCommand command, final LibvirtComputingResource libvirtComputingResource) {
@ -107,6 +112,17 @@ public final class LibvirtStartCommandWrapper extends CommandWrapper<StartComman
}
}
if (vmSpec.getType() == VirtualMachine.Type.ConsoleProxy && vmSpec.getVncPort() != null) {
String novncPort = vmSpec.getVncPort();
try {
String addCmd = "echo " + novncPort + " > " + vncConfFileLocation;
SshHelper.sshExecute(controlIp, sshPort, "root",
pemFile, null, addCmd, 20000, 20000, 600000);
} catch (Exception e) {
s_logger.error("Could not set the noVNC port " + novncPort + " to the CPVM", e);
}
}
final VirtualRoutingResource virtRouterResource = libvirtComputingResource.getVirtRouterResource();
// check if the router is up?
for (int count = 0; count < 60; count++) {

View File

@ -32,6 +32,7 @@ import org.apache.cloudstack.api.BaseAsyncCreateCmd;
import org.apache.cloudstack.api.BaseAsyncCustomIdCmd;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.BaseCustomIdCmd;
import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpointCmd;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.jobs.AsyncJob;
import org.apache.cloudstack.framework.jobs.AsyncJobManager;
@ -158,6 +159,12 @@ public class ApiDispatcher {
((BaseAsyncCustomIdCmd)cmd).checkUuid();
} else if (cmd instanceof BaseCustomIdCmd) {
((BaseCustomIdCmd)cmd).checkUuid();
} else if (cmd instanceof CreateConsoleEndpointCmd) {
Map<String, String> fullUrlParams = ((CreateConsoleEndpointCmd) cmd).getFullUrlParams();
s_logger.info("Console URL full params:");
for (String key : fullUrlParams.keySet()) {
s_logger.info(key + " : " + fullUrlParams.get(key));
}
}
cmd.execute();

View File

@ -41,8 +41,11 @@ import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.auth.APIAuthenticationManager;
import org.apache.cloudstack.api.auth.APIAuthenticationType;
import org.apache.cloudstack.api.auth.APIAuthenticator;
import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpointCmd;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
@ -187,8 +190,8 @@ public class ApiServlet extends HttpServlet {
}
final Object[] commandObj = params.get(ApiConstants.COMMAND);
final String command = commandObj == null ? null : (String) commandObj[0];
if (commandObj != null) {
final String command = (String) commandObj[0];
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
if (apiAuthenticator != null) {
@ -283,7 +286,6 @@ public class ApiServlet extends HttpServlet {
// Do a sanity check here to make sure the user hasn't already been deleted
if ((userId != null) && (account != null) && (accountObj != null) && apiServer.verifyUser(userId)) {
final String[] command = (String[])params.get(ApiConstants.COMMAND);
if (command == null) {
s_logger.info("missing command, ignoring request...");
auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + "no command specified");
@ -318,6 +320,16 @@ public class ApiServlet extends HttpServlet {
// Add the HTTP method (GET/POST/PUT/DELETE) as well into the params map.
params.put("httpmethod", new String[]{req.getMethod()});
setProjectContext(params);
if (org.apache.commons.lang3.StringUtils.isNotBlank(command) &&
command.equalsIgnoreCase(CreateConsoleEndpointCmd.APINAME)) {
InetAddress addr = getClientAddress(req);
String clientAddress = addr != null ? addr.getHostAddress() : null;
params.put(ConsoleAccessUtils.CLIENT_INET_ADDRESS_KEY, new String[]{clientAddress});
if (ConsoleAccessManager.ConsoleProxyExtraSecurityHeaderEnabled.value()) {
String clientSecurityToken = req.getHeader(ConsoleAccessManager.ConsoleProxyExtraSecurityHeaderName.value());
params.put(ConsoleAccessUtils.CLIENT_SECURITY_HEADER_PARAM_KEY, new String[]{clientSecurityToken});
}
}
final String response = apiServer.handleRequest(params, responseType, auditTrailSb);
HttpUtils.writeHttpResponse(resp, response != null ? response : "", HttpServletResponse.SC_OK, responseType, ApiServer.JSONcontentType.value());
} else {

View File

@ -21,6 +21,7 @@ import java.util.Map;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.log4j.Logger;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
@ -67,6 +68,8 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
protected ConsoleProxyDao _cpDao;
@Inject
protected KeystoreManager _ksMgr;
@Inject
protected ConsoleAccessManager consoleAccessManager;
@Inject
ConfigurationDao _configDao;
@ -77,8 +80,9 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
public class AgentBasedAgentHook extends AgentHookBase {
public AgentBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) {
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr);
public AgentBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr,
AgentManager agentMgr, KeysManager keysMgr, ConsoleAccessManager consoleAccessManager) {
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr, consoleAccessManager);
}
@Override
@ -121,7 +125,8 @@ public class AgentBasedConsoleProxyManager extends ManagerBase implements Consol
_consoleProxyUrlDomain = configs.get("consoleproxy.url.domain");
_listener = new ConsoleProxyListener(new AgentBasedAgentHook(_instanceDao, _hostDao, _configDao, _ksMgr, _agentMgr, _keysMgr));
_listener = new ConsoleProxyListener(new AgentBasedAgentHook(_instanceDao, _hostDao, _configDao, _ksMgr,
_agentMgr, _keysMgr, consoleAccessManager));
_agentMgr.registerForHostEvents(_listener, true, true, false);
if (s_logger.isInfoEnabled()) {

View File

@ -21,6 +21,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Date;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.cloudstack.framework.security.keystore.KeystoreManager;
@ -68,14 +69,17 @@ public abstract class AgentHookBase implements AgentHook {
AgentManager _agentMgr;
KeystoreManager _ksMgr;
KeysManager _keysMgr;
ConsoleAccessManager consoleAccessManager;
public AgentHookBase(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) {
public AgentHookBase(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr,
AgentManager agentMgr, KeysManager keysMgr, ConsoleAccessManager consoleAccessMgr) {
_instanceDao = instanceDao;
_hostDao = hostDao;
_agentMgr = agentMgr;
_configDao = cfgDao;
_ksMgr = ksMgr;
_keysMgr = keysMgr;
consoleAccessManager = consoleAccessMgr;
}
@Override
@ -83,6 +87,8 @@ public abstract class AgentHookBase implements AgentHook {
Long vmId = null;
String ticketInUrl = cmd.getTicket();
String sessionUuid = cmd.getSessionUuid();
if (ticketInUrl == null) {
s_logger.error("Access ticket could not be found, you could be running an old version of console proxy. vmId: " + cmd.getVmId());
return new ConsoleAccessAuthenticationAnswer(cmd, false);
@ -93,16 +99,20 @@ public abstract class AgentHookBase implements AgentHook {
}
if (!cmd.isReauthenticating()) {
String ticket = ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId());
String ticket = ConsoleAccessManagerImpl.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), sessionUuid);
if (s_logger.isDebugEnabled()) {
s_logger.debug("Console authentication. Ticket in 1 minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticket);
}
if (!consoleAccessManager.isSessionAllowed(sessionUuid)) {
s_logger.error("Invalid session, only one session allowed per token");
return new ConsoleAccessAuthenticationAnswer(cmd, false);
}
if (!ticket.equals(ticketInUrl)) {
Date now = new Date();
// considering of minute round-up
String minuteEarlyTicket =
ConsoleProxyServlet.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000));
String minuteEarlyTicket = ConsoleAccessManagerImpl.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000), sessionUuid);
if (s_logger.isDebugEnabled()) {
s_logger.debug("Console authentication. Ticket in 2-minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " +

View File

@ -0,0 +1,451 @@
// 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 com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetVmVncTicketAnswer;
import com.cloud.agent.api.GetVmVncTicketCommand;
import com.cloud.exception.AgentUnavailableException;
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.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.vm.UserVmDetailVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.dao.UserVmDetailsDao;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.cloudstack.api.command.user.consoleproxy.ConsoleEndpoint;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class ConsoleAccessManagerImpl extends ManagerBase implements ConsoleAccessManager {
@Inject
private AccountManager _accountMgr;
@Inject
private VirtualMachineManager _vmMgr;
@Inject
private ManagementServer _ms;
@Inject
private EntityManager _entityMgr;
@Inject
private UserVmDetailsDao _userVmDetailsDao;
@Inject
private KeysManager _keysMgr;
@Inject
private AgentManager agentManager;
private static KeysManager s_keysMgr;
private final Gson _gson = new GsonBuilder().create();
public static final Logger s_logger = Logger.getLogger(ConsoleAccessManagerImpl.class.getName());
private static Set<String> allowedSessions;
@Override
public boolean configure(String name, Map<String, Object> params) throws ConfigurationException {
s_keysMgr = _keysMgr;
allowedSessions = new HashSet<>();
return super.configure(name, params);
}
@Override
public ConsoleEndpoint generateConsoleEndpoint(Long vmId, String clientSecurityToken, String clientAddress) {
try {
if (_accountMgr == null || _vmMgr == null || _ms == null) {
return new ConsoleEndpoint(false, null,"Console service is not ready");
}
if (_keysMgr.getHashKey() == null) {
String msg = "Console access denied. Ticket service is not ready yet";
s_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) {
s_logger.debug("Invalid user/account, reject console access");
return new ConsoleEndpoint(false, null,"Access denied. Invalid or inconsistent account is found");
}
VirtualMachine vm = _entityMgr.findById(VirtualMachine.class, vmId);
if (vm == null) {
s_logger.info("Invalid console servlet command parameter: " + vmId);
return new ConsoleEndpoint(false, null, "Cannot find VM with ID " + vmId);
}
if (!checkSessionPermision(vm, account)) {
return new ConsoleEndpoint(false, null, "Permission denied");
}
String sessionToken = UUID.randomUUID().toString();
return generateAccessEndpoint(vmId, sessionToken, clientSecurityToken, clientAddress);
} catch (Throwable e) {
s_logger.error("Unexepected exception in ConsoleProxyServlet", e);
return new ConsoleEndpoint(false, null, "Server Internal Error: " + e.getMessage());
}
}
@Override
public boolean isSessionAllowed(String sessionUuid) {
return allowedSessions.contains(sessionUuid);
}
@Override
public void removeSessions(String[] sessionUuids) {
for (String r : sessionUuids) {
allowedSessions.remove(r);
}
}
private boolean checkSessionPermision(VirtualMachine vm, Account account) {
if (_accountMgr.isRootAdmin(account.getId())) {
return true;
}
switch (vm.getType()) {
case User:
try {
_accountMgr.checkAccess(account, null, true, vm);
} catch (PermissionDeniedException ex) {
if (_accountMgr.isNormalUser(account.getId())) {
if (s_logger.isDebugEnabled()) {
s_logger.debug("VM access is denied. VM owner account " + vm.getAccountId() + " does not match the account id in session " +
account.getId() + " and caller is a normal user");
}
} else if (_accountMgr.isDomainAdmin(account.getId())
|| account.getType() == Account.Type.READ_ONLY_ADMIN) {
if(s_logger.isDebugEnabled()) {
s_logger.debug("VM access is denied. VM owner account " + vm.getAccountId()
+ " does not match the account id in session " + account.getId() + " and the domain-admin caller does not manage the target domain");
}
}
return false;
}
break;
case DomainRouter:
case ConsoleProxy:
case SecondaryStorageVm:
return false;
default:
s_logger.warn("Unrecoginized virtual machine type, deny access by default. type: " + vm.getType());
return false;
}
return true;
}
private ConsoleEndpoint generateAccessEndpoint(Long vmId, String sessionToken, String clientSecurityToken, String clientAddress) {
VirtualMachine vm = _vmMgr.findById(vmId);
String msg;
if (vm == null) {
msg = "VM " + vmId + " does not exist, sending blank response for console access request";
s_logger.warn(msg);
throw new CloudRuntimeException(msg);
}
if (vm.getHostId() == null) {
msg = "VM " + vmId + " lost host info, sending blank response for console access request";
s_logger.warn(msg);
throw new CloudRuntimeException(msg);
}
HostVO host = _ms.getHostBy(vm.getHostId());
if (host == null) {
msg = "VM " + vmId + "'s host does not exist, sending blank response for console access request";
s_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 = _ms.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, sessionToken, clientSecurityToken);
s_logger.debug("The console URL is: " + consoleEndpoint.getUrl());
return consoleEndpoint;
}
private ConsoleEndpoint composeConsoleAccessEndpoint(String rootUrl, VirtualMachine vm, HostVO hostVo, String addr,
String sessionUuid, String clientSecurityToken) {
StringBuffer sb = new StringBuffer(rootUrl);
String host = hostVo.getPrivateIpAddress();
Pair<String, Integer> portInfo = null;
if (hostVo.getHypervisorType() == Hypervisor.HypervisorType.KVM &&
(hostVo.getResourceState().equals(ResourceState.ErrorInMaintenance) ||
hostVo.getResourceState().equals(ResourceState.ErrorInPrepareForMaintenance))) {
UserVmDetailVO detailAddress = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_ADDRESS);
UserVmDetailVO detailPort = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_PORT);
if (detailAddress != null && detailPort != null) {
portInfo = new Pair<>(detailAddress.getValue(), Integer.valueOf(detailPort.getValue()));
} else {
s_logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " +
"no VNC Address/Port was available. Falling back to default one from MS.");
}
}
if (portInfo == null) {
portInfo = _ms.getVncPort(vm);
}
if (s_logger.isDebugEnabled())
s_logger.debug("Port info " + portInfo.first());
Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first());
int port = -1;
if (portInfo.second() == -9) {
//for hyperv
port = Integer.parseInt(_ms.findDetail(hostVo.getId(), "rdp.server.port").getValue());
} else {
port = portInfo.second();
}
String sid = vm.getVncPassword();
UserVmDetailVO details = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD);
String tag = vm.getUuid();
String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag, sessionUuid);
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
param.setClientHostAddress(parsedHostInfo.first());
param.setClientHostPort(port);
param.setClientHostPassword(sid);
param.setClientTag(tag);
param.setTicket(ticket);
param.setSessionUuid(sessionUuid);
param.setSourceIP(addr);
if (StringUtils.isNotBlank(clientSecurityToken)) {
param.setClientSecurityHeader(ConsoleAccessManager.ConsoleProxyExtraSecurityHeaderName.value());
param.setClientSecurityToken(clientSecurityToken);
s_logger.debug("Added security token " + clientSecurityToken + " for header " + ConsoleAccessManager.ConsoleProxyExtraSecurityHeaderName.value());
}
if (requiresVncOverWebSocketConnection(vm, hostVo)) {
setWebsocketUrl(vm, param);
}
if (details != null) {
param.setLocale(details.getValue());
}
if (portInfo.second() == -9) {
//For Hyperv Clinet Host Address will send Instance id
param.setHypervHost(host);
param.setUsername(_ms.findDetail(hostVo.getId(), "username").getValue());
param.setPassword(_ms.findDetail(hostVo.getId(), "password").getValue());
}
if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) {
param.setClientTunnelUrl(parsedHostInfo.second());
param.setClientTunnelSession(parsedHostInfo.third());
}
String token = encryptor.encryptObject(ConsoleProxyClientParam.class, param);
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
sb.append("/ajax?token=" + token);
} else {
sb.append("/resource/noVNC/vnc.html")
.append("?autoconnect=true")
.append("&port=" + ConsoleProxyManager.NoVncConsolePort.value())
.append("&token=" + token);
}
// for console access, we need guest OS type to help implement keyboard
long guestOs = vm.getGuestOSId();
GuestOSVO guestOsVo = _ms.getGuestOs(guestOs);
if (guestOsVo.getCategoryId() == 6)
sb.append("&guest=windows");
if (s_logger.isDebugEnabled()) {
s_logger.debug("Compose console url: " + sb);
}
s_logger.debug("Adding allowed session: " + sessionUuid);
allowedSessions.add(sessionUuid);
String url = !sb.toString().startsWith("http") ? ConsoleAccessManager.ConsoleProxySchema.value() + ":" + sb : sb.toString();
return new ConsoleEndpoint(true, url);
}
static public Ternary<String, String, String> parseHostInfo(String hostInfo) {
String host = null;
String tunnelUrl = null;
String tunnelSession = null;
s_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<String, String, String>(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;
}
public static String genAccessTicket(String host, String port, String sid, String tag, String sessionUuid) {
return genAccessTicket(host, port, sid, tag, new Date(), sessionUuid);
}
public static 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("HmacSHA1");
long ts = normalizedHashTime.getTime();
ts = ts / 60000; // round up to 1 minute
String secretKey = s_keysMgr.getHashKey();
SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
mac.init(keySpec);
mac.update(params.getBytes());
mac.update(String.valueOf(ts).getBytes());
byte[] encryptedBytes = mac.doFinal();
return Base64.encodeBase64String(encryptedBytes);
} catch (Exception e) {
s_logger.error("Unexpected exception ", e);
}
return "";
}
private String getEncryptorPassword() {
String key = _keysMgr.getEncryptionKey();
String iv = _keysMgr.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)) {
s_logger.error("Could not obtain VNC ticket for VM " + vm.getInstanceName());
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 {
s_logger.info("Acquiring VNC ticket for VM = " + vm.getHostName());
GetVmVncTicketCommand cmd = new GetVmVncTicketCommand(vm.getInstanceName());
Answer answer = agentManager.send(vm.getHostId(), cmd);
GetVmVncTicketAnswer ans = (GetVmVncTicketAnswer) answer;
if (!ans.getResult()) {
s_logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails());
}
return ans.getTicket();
} catch (AgentUnavailableException | OperationTimedoutException e) {
s_logger.error("Error acquiring ticket", e);
return null;
}
}
@Override
public String getConfigComponentName() {
return ConsoleAccessManagerImpl.class.getSimpleName();
}
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey[] { ConsoleProxySchema, ConsoleProxyExtraSecurityHeaderName,
ConsoleProxyExtraSecurityHeaderEnabled };
}
}

View File

@ -23,39 +23,40 @@ import org.apache.cloudstack.framework.config.ConfigKey;
public interface ConsoleProxyManager extends Manager, ConsoleProxyService {
public static final int DEFAULT_PROXY_CAPACITY = 50;
public static final int DEFAULT_STANDBY_CAPACITY = 10;
public static final int DEFAULT_PROXY_VM_RAMSIZE = 1024; // 1G
public static final int DEFAULT_PROXY_VM_CPUMHZ = 500; // 500 MHz
int DEFAULT_PROXY_CAPACITY = 50;
int DEFAULT_STANDBY_CAPACITY = 10;
int DEFAULT_PROXY_VM_RAMSIZE = 1024; // 1G
int DEFAULT_PROXY_VM_CPUMHZ = 500; // 500 MHz
public static final int DEFAULT_PROXY_CMD_PORT = 8001;
public static final int DEFAULT_PROXY_VNC_PORT = 0;
public static final int DEFAULT_PROXY_URL_PORT = 80;
public static final int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes
int DEFAULT_PROXY_CMD_PORT = 8001;
int DEFAULT_PROXY_VNC_PORT = 0;
int DEFAULT_PROXY_URL_PORT = 80;
int DEFAULT_PROXY_SESSION_TIMEOUT = 300000; // 5 minutes
public static final int DEFAULT_NOVNC_PORT = 8080;
String ALERT_SUBJECT = "proxy-alert";
String CERTIFICATE_NAME = "CPVMCertificate";
public static final String ALERT_SUBJECT = "proxy-alert";
public static final String CERTIFICATE_NAME = "CPVMCertificate";
public static final ConfigKey<Boolean> NoVncConsoleDefault = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.default", "true",
ConfigKey<Boolean> NoVncConsoleDefault = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.default", "true",
"If true, noVNC console will be default console for virtual machines", true);
public static final ConfigKey<Boolean> NoVncConsoleSourceIpCheckEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.sourceip.check.enabled", "false",
ConfigKey<Boolean> NoVncConsoleSourceIpCheckEnabled = new ConfigKey<Boolean>("Advanced", Boolean.class, "novnc.console.sourceip.check.enabled", "false",
"If true, The source IP to access novnc console must be same as the IP in request to management server for console URL. Needs to reconnect CPVM to management server when this changes (via restart CPVM, or management server, or cloud service in CPVM)", false);
public void setManagementState(ConsoleProxyManagementState state);
ConfigKey<Integer> NoVncConsolePort = new ConfigKey<>("Advanced", Integer.class, "novnc.console.port",
"8080", "The listen port for noVNC console", true);
public ConsoleProxyManagementState getManagementState();
void setManagementState(ConsoleProxyManagementState state);
public void resumeLastManagementState();
ConsoleProxyManagementState getManagementState();
public ConsoleProxyVO startProxy(long proxyVmId, boolean ignoreRestartSetting);
void resumeLastManagementState();
public boolean stopProxy(long proxyVmId);
ConsoleProxyVO startProxy(long proxyVmId, boolean ignoreRestartSetting);
public boolean rebootProxy(long proxyVmId);
boolean stopProxy(long proxyVmId);
public boolean destroyProxy(long proxyVmId);
boolean rebootProxy(long proxyVmId);
boolean destroyProxy(long proxyVmId);
}

View File

@ -30,6 +30,7 @@ import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
import org.apache.cloudstack.framework.config.ConfigKey;
@ -256,11 +257,13 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
private KeystoreDao _ksDao;
@Inject
private KeystoreManager _ksMgr;
@Inject
private ConsoleAccessManager consoleAccessManager;
public class VmBasedAgentHook extends AgentHookBase {
public VmBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr) {
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr);
public VmBasedAgentHook(VMInstanceDao instanceDao, HostDao hostDao, ConfigurationDao cfgDao, KeystoreManager ksMgr, AgentManager agentMgr, KeysManager keysMgr, ConsoleAccessManager consoleAccessManager) {
super(instanceDao, hostDao, cfgDao, ksMgr, agentMgr, keysMgr, consoleAccessManager);
}
@Override
@ -1148,7 +1151,8 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
value = agentMgrConfigs.get("port");
managementPort = NumbersUtil.parseInt(value, 8250);
consoleProxyListener = new ConsoleProxyListener(new VmBasedAgentHook(vmInstanceDao, hostDao, configurationDao, _ksMgr, agentManager, keysManager));
consoleProxyListener = new ConsoleProxyListener(new VmBasedAgentHook(vmInstanceDao, hostDao, configurationDao,
_ksMgr, agentManager, keysManager, consoleAccessManager));
agentManager.registerForHostEvents(consoleProxyListener, true, true, false);
virtualMachineManager.registerGuru(VirtualMachine.Type.ConsoleProxy, this);
@ -1582,7 +1586,7 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
@Override
public ConfigKey<?>[] getConfigKeys() {
return new ConfigKey<?>[] { NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled };
return new ConfigKey<?>[] { NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, NoVncConsolePort };
}
protected ConsoleProxyStatus parseJsonToConsoleProxyStatus(String json) throws JsonParseException {
@ -1606,6 +1610,9 @@ public class ConsoleProxyManagerImpl extends ManagerBase implements ConsoleProxy
if (status.getConnections() != null) {
count = status.getConnections().length;
}
if (status.getRemovedSessions() != null) {
consoleAccessManager.removeSessions(status.getRemovedSessions());
}
details = statusInfo.getBytes(Charset.forName("US-ASCII"));
} else {

View File

@ -22,6 +22,7 @@ import java.util.UUID;
import javax.inject.Inject;
import com.cloud.consoleproxy.ConsoleProxyManager;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.backup.Backup;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
@ -277,6 +278,15 @@ public abstract class HypervisorGuruBase extends AdapterBase implements Hypervis
to.setConfigDriveLocation(vmProfile.getConfigDriveLocation());
to.setState(vm.getState());
if (vmInstance.getType() == VirtualMachine.Type.ConsoleProxy) {
try {
String vncPort = String.valueOf(ConsoleProxyManager.NoVncConsolePort.value());
to.setVncPort(vncPort);
} catch (Exception e) {
s_logger.error("Could not parse the noVNC port set on " + ConsoleProxyManager.NoVncConsolePort.key(), e);
}
}
return to;
}

View File

@ -343,6 +343,7 @@ import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScalePolicyCmd
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmGroupCmd;
import org.apache.cloudstack.api.command.user.autoscale.UpdateAutoScaleVmProfileCmd;
import org.apache.cloudstack.api.command.user.config.ListCapabilitiesCmd;
import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpointCmd;
import org.apache.cloudstack.api.command.user.event.ArchiveEventsCmd;
import org.apache.cloudstack.api.command.user.event.DeleteEventsCmd;
import org.apache.cloudstack.api.command.user.event.ListEventTypesCmd;
@ -3503,6 +3504,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
cmdList.add(IssueOutOfBandManagementPowerActionCmd.class);
cmdList.add(ChangeOutOfBandManagementPasswordCmd.class);
cmdList.add(GetUserKeysCmd.class);
cmdList.add(CreateConsoleEndpointCmd.class);
return cmdList;
}

View File

@ -36,6 +36,10 @@ public class ConsoleProxyClientParam {
private String sourceIP;
private String websocketUrl;
private String sessionUuid;
private String clientSecurityHeader;
private String clientSecurityToken;
public ConsoleProxyClientParam() {
clientHostPort = 0;
}
@ -159,4 +163,28 @@ public class ConsoleProxyClientParam {
public void setWebsocketUrl(String websocketUrl) {
this.websocketUrl = websocketUrl;
}
public String getSessionUuid() {
return sessionUuid;
}
public String getClientSecurityHeader() {
return clientSecurityHeader;
}
public void setClientSecurityHeader(String clientSecurityHeader) {
this.clientSecurityHeader = clientSecurityHeader;
}
public void setSessionUuid(String sessionUuid) {
this.sessionUuid = sessionUuid;
}
public String getClientSecurityToken() {
return clientSecurityToken;
}
public void setClientSecurityToken(String clientSecurityToken) {
this.clientSecurityToken = clientSecurityToken;
}
}

View File

@ -17,9 +17,7 @@
package com.cloud.servlet;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
@ -37,13 +35,6 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.cloud.agent.AgentManager;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.GetVmVncTicketAnswer;
import com.cloud.agent.api.GetVmVncTicketCommand;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.exception.OperationTimedoutException;
import com.cloud.utils.StringUtils;
import org.apache.cloudstack.framework.security.keys.KeysManager;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
@ -51,31 +42,22 @@ import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import com.cloud.vm.VmDetailConstants;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.cloud.api.ApiServlet;
import com.cloud.consoleproxy.ConsoleProxyManager;
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.storage.GuestOSVO;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.User;
import com.cloud.uservm.UserVm;
import com.cloud.utils.ConstantTimeComparator;
import com.cloud.utils.Pair;
import com.cloud.utils.Ternary;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.db.TransactionLegacy;
import com.cloud.vm.UserVmDetailVO;
import com.cloud.vm.VirtualMachine;
import com.cloud.vm.VirtualMachineManager;
import com.cloud.vm.dao.UserVmDetailsDao;
/**
* Thumbnail access : /console?cmd=thumbnail&vm=xxx&w=xxx&h=xxx
@ -98,11 +80,7 @@ public class ConsoleProxyServlet extends HttpServlet {
@Inject
EntityManager _entityMgr;
@Inject
UserVmDetailsDao _userVmDetailsDao;
@Inject
KeysManager _keysMgr;
@Inject
AgentManager agentManager;
static KeysManager s_keysMgr;
@ -198,8 +176,6 @@ public class ConsoleProxyServlet extends HttpServlet {
if (cmd.equalsIgnoreCase("thumbnail")) {
handleThumbnailRequest(req, resp, vmId);
} else if (cmd.equalsIgnoreCase("access")) {
handleAccessRequest(req, resp, vmId);
} else {
handleAuthRequest(req, resp, vmId);
}
@ -260,61 +236,6 @@ public class ConsoleProxyServlet extends HttpServlet {
}
}
private void handleAccessRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) {
VirtualMachine vm = _vmMgr.findById(vmId);
if (vm == null) {
s_logger.warn("VM " + vmId + " does not exist, sending blank response for console access request");
sendResponse(resp, "");
return;
}
if (vm.getHostId() == null) {
s_logger.warn("VM " + vmId + " lost host info, sending blank response for console access request");
sendResponse(resp, "");
return;
}
HostVO host = _ms.getHostBy(vm.getHostId());
if (host == null) {
s_logger.warn("VM " + vmId + "'s host does not exist, sending blank response for console access request");
sendResponse(resp, "");
return;
}
if (Hypervisor.HypervisorType.LXC.equals(vm.getHypervisorType())){
sendResponse(resp, "<html><body><p>Console access is not supported for LXC</p></body></html>");
return;
}
String rootUrl = _ms.getConsoleAccessUrlRoot(vmId);
if (rootUrl == null) {
sendResponse(resp, "<html><body><p>Console access will be ready in a few minutes. Please try it again later.</p></body></html>");
return;
}
String vmName = vm.getHostName();
if (vm.getType() == VirtualMachine.Type.User) {
UserVm userVm = _entityMgr.findById(UserVm.class, vmId);
String displayName = userVm.getDisplayName();
if (displayName != null && !displayName.isEmpty() && !displayName.equals(vmName)) {
vmName += "(" + displayName + ")";
}
}
InetAddress remoteAddress = null;
try {
remoteAddress = ApiServlet.getClientAddress(req);
} catch (UnknownHostException e) {
s_logger.warn("UnknownHostException when trying to lookup remote IP-Address. This should never happen. Blocking request.", e);
}
StringBuffer sb = new StringBuffer();
sb.append("<html><title>").append(escapeHTML(vmName)).append("</title><frameset><frame src=\"").append(composeConsoleAccessUrl(rootUrl, vm, host, remoteAddress));
sb.append("\"></frame></frameset></html>");
s_logger.debug("the console url is :: " + sb.toString());
sendResponse(resp, sb.toString());
}
private void handleAuthRequest(HttpServletRequest req, HttpServletResponse resp, long vmId) {
// TODO authentication channel between console proxy VM and management server needs to be secured,
@ -436,145 +357,6 @@ public class ConsoleProxyServlet extends HttpServlet {
return sb.toString();
}
/**
* 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)) {
s_logger.error("Could not obtain VNC ticket for VM " + vm.getInstanceName());
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 {
s_logger.info("Acquiring VNC ticket for VM = " + vm.getHostName());
GetVmVncTicketCommand cmd = new GetVmVncTicketCommand(vm.getInstanceName());
Answer answer = agentManager.send(vm.getHostId(), cmd);
GetVmVncTicketAnswer ans = (GetVmVncTicketAnswer) answer;
if (!ans.getResult()) {
s_logger.info("VNC ticket could not be acquired correctly: " + ans.getDetails());
}
return ans.getTicket();
} catch (AgentUnavailableException | OperationTimedoutException e) {
s_logger.error("Error acquiring ticket", e);
return null;
}
}
private String composeConsoleAccessUrl(String rootUrl, VirtualMachine vm, HostVO hostVo, InetAddress addr) {
StringBuffer sb = new StringBuffer(rootUrl);
String host = hostVo.getPrivateIpAddress();
Pair<String, Integer> portInfo = null;
if (hostVo.getHypervisorType() == Hypervisor.HypervisorType.KVM &&
(hostVo.getResourceState().equals(ResourceState.ErrorInMaintenance) ||
hostVo.getResourceState().equals(ResourceState.ErrorInPrepareForMaintenance))) {
UserVmDetailVO detailAddress = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_ADDRESS);
UserVmDetailVO detailPort = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KVM_VNC_PORT);
if (detailAddress != null && detailPort != null) {
portInfo = new Pair<>(detailAddress.getValue(), Integer.valueOf(detailPort.getValue()));
} else {
s_logger.warn("KVM Host in ErrorInMaintenance/ErrorInPrepareForMaintenance but " +
"no VNC Address/Port was available. Falling back to default one from MS.");
}
}
if (portInfo == null) {
portInfo = _ms.getVncPort(vm);
}
if (s_logger.isDebugEnabled())
s_logger.debug("Port info " + portInfo.first());
Ternary<String, String, String> parsedHostInfo = parseHostInfo(portInfo.first());
int port = -1;
if (portInfo.second() == -9) {
//for hyperv
port = Integer.parseInt(_ms.findDetail(hostVo.getId(), "rdp.server.port").getValue());
} else {
port = portInfo.second();
}
String sid = vm.getVncPassword();
UserVmDetailVO details = _userVmDetailsDao.findDetail(vm.getId(), VmDetailConstants.KEYBOARD);
String tag = vm.getUuid();
String ticket = genAccessTicket(parsedHostInfo.first(), String.valueOf(port), sid, tag);
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(getEncryptorPassword());
ConsoleProxyClientParam param = new ConsoleProxyClientParam();
param.setClientHostAddress(parsedHostInfo.first());
param.setClientHostPort(port);
param.setClientHostPassword(sid);
param.setClientTag(tag);
param.setTicket(ticket);
param.setSourceIP(addr != null ? addr.getHostAddress(): null);
if (requiresVncOverWebSocketConnection(vm, hostVo)) {
setWebsocketUrl(vm, param);
}
if (details != null) {
param.setLocale(details.getValue());
}
if (portInfo.second() == -9) {
//For Hyperv Clinet Host Address will send Instance id
param.setHypervHost(host);
param.setUsername(_ms.findDetail(hostVo.getId(), "username").getValue());
param.setPassword(_ms.findDetail(hostVo.getId(), "password").getValue());
}
if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) {
param.setClientTunnelUrl(parsedHostInfo.second());
param.setClientTunnelSession(parsedHostInfo.third());
}
if (param.getHypervHost() != null || !ConsoleProxyManager.NoVncConsoleDefault.value()) {
sb.append("/ajax?token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
} else {
sb.append("/resource/noVNC/vnc.html")
.append("?autoconnect=true")
.append("&port=" + ConsoleProxyManager.DEFAULT_NOVNC_PORT)
.append("&token=" + encryptor.encryptObject(ConsoleProxyClientParam.class, param));
}
// for console access, we need guest OS type to help implement keyboard
long guestOs = vm.getGuestOSId();
GuestOSVO guestOsVo = _ms.getGuestOs(guestOs);
if (guestOsVo.getCategoryId() == 6)
sb.append("&guest=windows");
if (s_logger.isDebugEnabled()) {
s_logger.debug("Compose console url: " + sb.toString());
}
return sb.toString();
}
/**
* 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;
}
public static String genAccessTicket(String host, String port, String sid, String tag) {
return genAccessTicket(host, port, sid, tag, new Date());
}

View File

@ -107,6 +107,8 @@
value="#{consoleProxyAllocatorsRegistry.registered}" />
</bean>
<bean id="consoleAccessManagerImpl" class="com.cloud.consoleproxy.ConsoleAccessManagerImpl" />
<bean id="securityGroupManagerImpl2" class="com.cloud.network.security.SecurityGroupManagerImpl2" />
<bean id="ipv6AddressManagerImpl" class="com.cloud.network.Ipv6AddressManagerImpl" />

View File

@ -165,7 +165,8 @@ public class ConsoleProxy {
}
}
public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(ConsoleProxyClientParam param, boolean reauthentication) {
public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(ConsoleProxyClientParam param,
boolean reauthentication, Session session) {
ConsoleProxyAuthenticationResult authResult = new ConsoleProxyAuthenticationResult();
authResult.setSuccess(true);
@ -173,6 +174,20 @@ public class ConsoleProxy {
authResult.setHost(param.getClientHostAddress());
authResult.setPort(param.getClientHostPort());
if (session != null && param.getClientSecurityToken() != null) {
String clientSecurityHeader = param.getClientSecurityHeader();
String headerValue = session.getUpgradeRequest().getHeader(clientSecurityHeader);
if (!param.getClientSecurityToken().equals(headerValue)) {
s_logger.error("Security token found but not matching the expected value for this session");
if (s_logger.isDebugEnabled()) {
s_logger.debug(String.format("Expected value for header %s was %s but found %s",
clientSecurityHeader, param.getClientSecurityToken(), headerValue));
}
authResult.setSuccess(false);
return authResult;
}
}
String websocketUrl = param.getWebsocketUrl();
if (StringUtils.isNotBlank(websocketUrl)) {
return authResult;
@ -187,7 +202,7 @@ public class ConsoleProxy {
try {
result =
authMethod.invoke(ConsoleProxy.context, param.getClientHostAddress(), String.valueOf(param.getClientHostPort()), param.getClientTag(),
param.getClientHostPassword(), param.getTicket(), new Boolean(reauthentication));
param.getClientHostPassword(), param.getTicket(), reauthentication, param.getSessionUuid());
} catch (IllegalAccessException e) {
s_logger.error("Unable to invoke authenticateConsoleAccess due to IllegalAccessException" + " for vm: " + param.getClientTag(), e);
authResult.setSuccess(false);
@ -259,7 +274,8 @@ public class ConsoleProxy {
try {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> contextClazz = loader.loadClass("com.cloud.agent.resource.consoleproxy.ConsoleProxyResource");
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class, String.class, String.class, String.class, Boolean.class);
authMethod = contextClazz.getDeclaredMethod("authenticateConsoleAccess", String.class, String.class,
String.class, String.class, String.class, Boolean.class, String.class);
reportMethod = contextClazz.getDeclaredMethod("reportLoadInfo", String.class);
ensureRouteMethod = contextClazz.getDeclaredMethod("ensureRoute", String.class);
} catch (SecurityException e) {
@ -449,7 +465,7 @@ public class ConsoleProxy {
synchronized (connectionMap) {
ConsoleProxyClient viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() == ConsoleProxyNoVncClient.class) {
authenticationExternally(param);
authenticationExternally(param, null);
viewer = getClient(param);
viewer.initClient(param);
@ -470,7 +486,7 @@ public class ConsoleProxy {
if (!viewer.isFrontEndAlive()) {
authenticationExternally(param);
authenticationExternally(param, null);
viewer.initClient(param);
reportLoadChange = true;
}
@ -512,8 +528,8 @@ public class ConsoleProxy {
}
}
public static void authenticationExternally(ConsoleProxyClientParam param) throws AuthenticationException {
ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false);
public static void authenticationExternally(ConsoleProxyClientParam param, Session session) throws AuthenticationException {
ConsoleProxyAuthenticationResult authResult = authenticateConsoleAccess(param, false, session);
if (authResult == null || !authResult.isSuccess()) {
s_logger.warn("External authenticator failed authencation request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword());
@ -523,7 +539,7 @@ public class ConsoleProxy {
}
public static ConsoleProxyAuthenticationResult reAuthenticationExternally(ConsoleProxyClientParam param) {
return authenticateConsoleAccess(param, true);
return authenticateConsoleAccess(param, true, null);
}
public static String getEncryptorPassword() {
@ -552,7 +568,7 @@ public class ConsoleProxy {
synchronized (connectionMap) {
ConsoleProxyClient viewer = connectionMap.get(clientKey);
if (viewer == null || viewer.getClass() != ConsoleProxyNoVncClient.class) {
authenticationExternally(param);
authenticationExternally(param, session);
viewer = new ConsoleProxyNoVncClient(session);
viewer.initClient(param);
@ -564,7 +580,7 @@ public class ConsoleProxy {
throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid");
try {
authenticationExternally(param);
authenticationExternally(param, session);
} catch (Exception e) {
s_logger.error("Authencation failed for param: " + param);
return null;

View File

@ -78,4 +78,6 @@ public interface ConsoleProxyClient {
void initClient(ConsoleProxyClientParam param);
void closeClient();
String getSessionUuid();
}

View File

@ -55,6 +55,7 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons
protected boolean framebufferResized = false;
protected int resizedFramebufferWidth;
protected int resizedFramebufferHeight;
protected String sessionUuid;
public ConsoleProxyClientBase() {
tracker = new TileTracker();
@ -422,4 +423,9 @@ public abstract class ConsoleProxyClientBase implements ConsoleProxyClient, Cons
ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(ConsoleProxy.getEncryptorPassword());
this.clientToken = encryptor.encryptObject(ConsoleProxyClientParam.class, clientParam);
}
@Override
public String getSessionUuid() {
return sessionUuid;
}
}

View File

@ -40,6 +40,10 @@ public class ConsoleProxyClientParam {
private String sourceIP;
private String sessionUuid;
private String clientSecurityHeader;
private String clientSecurityToken;
public ConsoleProxyClientParam() {
clientHostPort = 0;
}
@ -162,4 +166,28 @@ public class ConsoleProxyClientParam {
public void setWebsocketUrl(String websocketUrl) {
this.websocketUrl = websocketUrl;
}
public String getSessionUuid() {
return sessionUuid;
}
public void setSessionUuid(String sessionUuid) {
this.sessionUuid = sessionUuid;
}
public String getClientSecurityHeader() {
return clientSecurityHeader;
}
public void setClientSecurityHeader(String clientSecurityHeader) {
this.clientSecurityHeader = clientSecurityHeader;
}
public String getClientSecurityToken() {
return clientSecurityToken;
}
public void setClientSecurityToken(String clientSecurityToken) {
this.clientSecurityToken = clientSecurityToken;
}
}

View File

@ -20,6 +20,7 @@ import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -31,10 +32,16 @@ import com.google.gson.GsonBuilder;
public class ConsoleProxyClientStatsCollector {
ArrayList<ConsoleProxyConnection> connections;
ArrayList<String> removedSessions;
public ConsoleProxyClientStatsCollector() {
}
public void setRemovedSessions(List<String> removed) {
removedSessions = new ArrayList<>();
removedSessions.addAll(removed);
}
public ConsoleProxyClientStatsCollector(Hashtable<String, ConsoleProxyClient> connMap) {
setConnections(connMap);
}
@ -67,6 +74,7 @@ public class ConsoleProxyClientStatsCollector {
conn.tag = client.getClientTag();
conn.createTime = client.getClientCreateTime();
conn.lastUsedTime = client.getClientLastFrontEndActivityTime();
conn.sessionUuid = client.getSessionUuid();
conns.add(conn);
}
}
@ -81,6 +89,7 @@ public class ConsoleProxyClientStatsCollector {
public String tag;
public long createTime;
public long lastUsedTime;
public String sessionUuid;
public ConsoleProxyConnection() {
}

View File

@ -17,8 +17,10 @@
package com.cloud.consoleproxy;
import java.io.File;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;
import org.apache.log4j.Logger;
@ -67,9 +69,12 @@ public class ConsoleProxyGCThread extends Thread {
boolean bReportLoad = false;
long lastReportTick = System.currentTimeMillis();
List<String> removedSessions = new ArrayList<>();
while (true) {
cleanupLogging();
bReportLoad = false;
removedSessions.clear();
if (s_logger.isDebugEnabled())
s_logger.debug("connMap=" + connMap);
@ -89,6 +94,7 @@ public class ConsoleProxyGCThread extends Thread {
}
synchronized (connMap) {
removedSessions.add(client.getSessionUuid());
connMap.remove(key);
bReportLoad = true;
}
@ -100,7 +106,9 @@ public class ConsoleProxyGCThread extends Thread {
if (bReportLoad || System.currentTimeMillis() - lastReportTick > 5000) {
// report load changes
String loadInfo = new ConsoleProxyClientStatsCollector(connMap).getStatsReport();
ConsoleProxyClientStatsCollector collector = new ConsoleProxyClientStatsCollector(connMap);
collector.setRemovedSessions(removedSessions);
String loadInfo = collector.getStatsReport();
ConsoleProxy.reportLoadInfo(loadInfo);
lastReportTick = System.currentTimeMillis();

View File

@ -96,6 +96,15 @@ public class ConsoleProxyHttpHandlerHelper {
if (param.getWebsocketUrl() != null) {
map.put("websocketUrl", param.getWebsocketUrl());
}
if (param.getSessionUuid() != null) {
map.put("sessionUuid", param.getSessionUuid());
}
if (param.getClientSecurityHeader() != null) {
map.put("clientSecurityHeader", param.getClientSecurityHeader());
}
if (param.getClientSecurityToken() != null) {
map.put("clientSecurityToken", param.getClientSecurityToken());
}
} else {
s_logger.error("Unable to decode token");
}

View File

@ -89,6 +89,9 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
String password = queryMap.get("password");
String sourceIP = queryMap.get("sourceIP");
String websocketUrl = queryMap.get("websocketUrl");
String sessionUuid = queryMap.get("sessionUuid");
String clientSecurityToken = queryMap.get("clientSecurityToken");
String clientSecurityHeader = queryMap.get("clientSecurityHeader");
if (tag == null)
tag = "";
@ -133,6 +136,9 @@ public class ConsoleProxyNoVNCHandler extends WebSocketHandler {
param.setUsername(username);
param.setPassword(password);
param.setWebsocketUrl(websocketUrl);
param.setSessionUuid(sessionUuid);
param.setClientSecurityHeader(clientSecurityHeader);
param.setClientSecurityToken(clientSecurityToken);
viewer = ConsoleProxy.getNoVncViewer(param, ajaxSessionIdStr, session);
} catch (Exception e) {
s_logger.warn("Failed to create viewer due to " + e.getMessage(), e);

View File

@ -17,6 +17,8 @@
package com.cloud.consoleproxy;
import java.io.ByteArrayInputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import com.cloud.consoleproxy.util.Logger;
@ -32,17 +34,30 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
public class ConsoleProxyNoVNCServer {
private static final Logger s_logger = Logger.getLogger(ConsoleProxyNoVNCServer.class);
private static final int wsPort = 8080;
private static int wsPort = 8080;
private static final String vncConfFileLocation = "/root/vncport";
private Server server;
private void init() {
try {
String portStr = Files.readString(Path.of(vncConfFileLocation)).trim();
wsPort = Integer.parseInt(portStr);
s_logger.info("Setting port to: " + wsPort);
} catch (Exception e) {
s_logger.error("Error loading properties from " + vncConfFileLocation, e);
}
}
public ConsoleProxyNoVNCServer() {
init();
this.server = new Server(wsPort);
ConsoleProxyNoVNCHandler handler = new ConsoleProxyNoVNCHandler();
this.server.setHandler(handler);
}
public ConsoleProxyNoVNCServer(byte[] ksBits, String ksPassword) {
init();
this.server = new Server();
ConsoleProxyNoVNCHandler handler = new ConsoleProxyNoVNCHandler();
this.server.setHandler(handler);

View File

@ -46,6 +46,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
private boolean connectionAlive;
private ConsoleProxyClientParam clientParam;
private String sessionUuid;
public ConsoleProxyNoVncClient(Session session) {
this.session = session;
@ -89,6 +90,7 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
setClientParam(param);
client = new NoVncClient();
connectionAlive = true;
this.sessionUuid = param.getSessionUuid();
updateFrontEndActivityTime();
Thread worker = new Thread(new Runnable() {
@ -192,6 +194,11 @@ public class ConsoleProxyNoVncClient implements ConsoleProxyClient {
ConsoleProxy.removeViewer(this);
}
@Override
public String getSessionUuid() {
return sessionUuid;
}
@Override
public int getClientId() {
return this.clientId;

View File

@ -44,6 +44,11 @@ setup_console_proxy() {
setup_sshd $ETH0_IP "eth0"
fi
vncport=`cat /root/vncport`
log_it "vncport read: ${vncport}"
sed -i 's/8080/${vncport}/' /etc/iptables/rules.v4
log_it "vnc port ${vncport} rule applied"
disable_rpfilter
enable_fwding 0
enable_irqbalance 0

View File

@ -201,7 +201,8 @@ known_categories = {
'UnmanagedInstance': 'Virtual Machine',
'Rolling': 'Rolling Maintenance',
'importVsphereStoragePolicies' : 'vSphere storage policies',
'listVsphereStoragePolicies' : 'vSphere storage policies'
'listVsphereStoragePolicies' : 'vSphere storage policies',
'ConsoleEndpoint': 'Console Endpoint'
}

View File

@ -17,9 +17,8 @@
<template>
<a
v-if="['vm', 'systemvm', 'router', 'ilbvm'].includes($route.meta.name) && 'updateVirtualMachine' in $store.getters.apis"
:href="server + '/console?cmd=access&vm=' + resource.id"
target="_blank">
v-if="['vm', 'systemvm', 'router', 'ilbvm'].includes($route.meta.name) && 'updateVirtualMachine' in $store.getters.apis && 'createConsoleEndpoint' in $store.getters.apis"
@click="consoleUrl">
<a-button style="margin-left: 5px" shape="circle" type="dashed" :size="size" :disabled="['Stopped', 'Error', 'Destroyed'].includes(resource.state)" >
<a-icon type="code" />
</a-button>
@ -29,6 +28,7 @@
<script>
import Vue from 'vue'
import { SERVER_MANAGER } from '@/store/mutation-types'
import { api } from '@/api'
export default {
name: 'Console',
@ -42,6 +42,19 @@ export default {
default: 'small'
}
},
data () {
return {
url: ''
}
},
methods: {
consoleUrl () {
api('createConsoleEndpoint', { virtualmachineid: this.resource.id }).then(json => {
this.url = (json && json.createconsoleendpointresponse) ? json.createconsoleendpointresponse.consoleendpoint.url : '#/exception/404'
window.open(this.url, '_blank')
})
}
},
computed: {
server () {
if (!this.$config.multipleServer) {

View File

@ -0,0 +1,27 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.utils.consoleproxy;
import org.apache.log4j.Logger;
public class ConsoleAccessUtils {
public static final Logger s_logger = Logger.getLogger(ConsoleAccessUtils.class.getName());
public static String CLIENT_SECURITY_HEADER_PARAM_KEY = "client-security-token";
public static String CLIENT_INET_ADDRESS_KEY = "client-inet-address";
}