From 5d4f27480d0e97a9d56106213733d52f0a6f884f Mon Sep 17 00:00:00 2001 From: nvazquez Date: Tue, 26 Jul 2022 11:45:28 -0300 Subject: [PATCH 01/19] Console access enhancements --- .../consoleproxy/ConsoleProxyResource.java | 5 +- .../cloud/agent/api/to/VirtualMachineTO.java | 9 + .../user/consoleproxy/ConsoleEndpoint.java | 58 +++ .../CreateConsoleEndpointCmd.java | 99 ++++ .../response/CreateConsoleUrlResponse.java | 97 ++++ .../consoleproxy/ConsoleAccessManager.java | 43 ++ .../ConsoleAccessAuthenticationCommand.java | 13 +- .../info/ConsoleProxyConnectionInfo.java | 1 + .../com/cloud/info/ConsoleProxyStatus.java | 5 + .../wrapper/LibvirtStartCommandWrapper.java | 15 + .../java/com/cloud/api/ApiDispatcher.java | 7 + .../main/java/com/cloud/api/ApiServlet.java | 13 + .../AgentBasedConsoleProxyManager.java | 11 +- .../com/cloud/consoleproxy/AgentHookBase.java | 18 +- .../ConsoleAccessManagerImpl.java | 451 ++++++++++++++++++ .../consoleproxy/ConsoleProxyManager.java | 43 +- .../consoleproxy/ConsoleProxyManagerImpl.java | 17 +- .../cloud/hypervisor/HypervisorGuruBase.java | 10 + .../cloud/server/ManagementServerImpl.java | 2 + .../servlet/ConsoleProxyClientParam.java | 28 ++ .../cloud/servlet/ConsoleProxyServlet.java | 218 --------- .../spring-server-core-managers-context.xml | 2 + .../com/cloud/consoleproxy/ConsoleProxy.java | 36 +- .../consoleproxy/ConsoleProxyClient.java | 2 + .../consoleproxy/ConsoleProxyClientBase.java | 6 + .../consoleproxy/ConsoleProxyClientParam.java | 28 ++ .../ConsoleProxyClientStatsCollector.java | 9 + .../consoleproxy/ConsoleProxyGCThread.java | 10 +- .../ConsoleProxyHttpHandlerHelper.java | 9 + .../ConsoleProxyNoVNCHandler.java | 6 + .../consoleproxy/ConsoleProxyNoVNCServer.java | 17 +- .../consoleproxy/ConsoleProxyNoVncClient.java | 7 + .../opt/cloud/bin/setup/consoleproxy.sh | 5 + systemvm/patch-sysvms.sh | 5 +- tools/apidoc/gen_toc.py | 3 +- ui/src/components/widgets/Console.vue | 19 +- .../consoleproxy/ConsoleAccessUtils.java | 27 ++ 37 files changed, 1083 insertions(+), 271 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/ConsoleEndpoint.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/CreateConsoleEndpointCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleUrlResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java create mode 100644 server/src/main/java/com/cloud/consoleproxy/ConsoleAccessManagerImpl.java create mode 100644 utils/src/main/java/org/apache/cloudstack/utils/consoleproxy/ConsoleAccessUtils.java diff --git a/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java b/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java index 257a1fc984a1..3f5372af9aa6 100644 --- a/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java +++ b/agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java @@ -382,9 +382,10 @@ protected void runInContext() { } } - 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(); diff --git a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java index 5fc248343ecc..d612fb624858 100644 --- a/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java +++ b/api/src/main/java/com/cloud/agent/api/to/VirtualMachineTO.java @@ -59,6 +59,7 @@ public class VirtualMachineTO { boolean enableDynamicallyScaleVm; String vncPassword; String vncAddr; + String vncPort; Map params; String uuid; String bootType; @@ -283,6 +284,14 @@ public void setVncAddr(String vncAddr) { this.vncAddr = vncAddr; } + public String getVncPort() { + return vncPort; + } + + public void setVncPort(String vncPort) { + this.vncPort = vncPort; + } + public Map getDetails() { return params; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/ConsoleEndpoint.java b/api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/ConsoleEndpoint.java new file mode 100644 index 000000000000..cd61c223ed16 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/ConsoleEndpoint.java @@ -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; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/CreateConsoleEndpointCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/CreateConsoleEndpointCmd.java new file mode 100644 index 000000000000..b84fa4745106 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/user/consoleproxy/CreateConsoleEndpointCmd.java @@ -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 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(); + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleUrlResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleUrlResponse.java new file mode 100644 index 000000000000..61b05be7ab39 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/CreateConsoleUrlResponse.java @@ -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; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java b/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java new file mode 100644 index 000000000000..55061088c4c6 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/consoleproxy/ConsoleAccessManager.java @@ -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 ConsoleProxySchema = new ConfigKey<>("Advanced", String.class, + "consoleproxy.schema", "http", + "The http/https schema to be used by the console proxy URLs", true); + + ConfigKey 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 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); +} diff --git a/core/src/main/java/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java b/core/src/main/java/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java index dd533d8774da..683d4afd5b2f 100644 --- a/core/src/main/java/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java +++ b/core/src/main/java/com/cloud/agent/api/ConsoleAccessAuthenticationCommand.java @@ -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 ConsoleAccessAuthenticationCommand() { _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 boolean isReauthenticating() { public void setReauthenticating(boolean value) { _isReauthenticating = value; } + + public String getSessionUuid() { + return sessionUuid; + } + + public void setSessionUuid(String sessionUuid) { + this.sessionUuid = sessionUuid; + } } diff --git a/core/src/main/java/com/cloud/info/ConsoleProxyConnectionInfo.java b/core/src/main/java/com/cloud/info/ConsoleProxyConnectionInfo.java index 48819f494750..06a048a21809 100644 --- a/core/src/main/java/com/cloud/info/ConsoleProxyConnectionInfo.java +++ b/core/src/main/java/com/cloud/info/ConsoleProxyConnectionInfo.java @@ -26,6 +26,7 @@ public class ConsoleProxyConnectionInfo { public String tag; public long createTime; public long lastUsedTime; + public String sessionUuid; public ConsoleProxyConnectionInfo() { } diff --git a/core/src/main/java/com/cloud/info/ConsoleProxyStatus.java b/core/src/main/java/com/cloud/info/ConsoleProxyStatus.java index 3d3dda9a508a..e9ef26a63b9e 100644 --- a/core/src/main/java/com/cloud/info/ConsoleProxyStatus.java +++ b/core/src/main/java/com/cloud/info/ConsoleProxyStatus.java @@ -21,6 +21,7 @@ public class ConsoleProxyStatus { private ConsoleProxyConnectionInfo[] connections; + private String[] removedSessions; public ConsoleProxyStatus() { } @@ -28,4 +29,8 @@ public ConsoleProxyStatus() { public ConsoleProxyConnectionInfo[] getConnections() { return connections; } + + public String[] getRemovedSessions() { + return removedSessions; + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java index 7b69993f2e5e..6b5090bf1546 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtStartCommandWrapper.java @@ -24,6 +24,7 @@ import com.cloud.agent.resource.virtualnetwork.VRScripts; import com.cloud.utils.FileUtil; +import com.cloud.utils.ssh.SshHelper; import org.apache.log4j.Logger; import org.libvirt.Connect; import org.libvirt.DomainInfo.DomainState; @@ -50,6 +51,9 @@ public final class LibvirtStartCommandWrapper extends CommandWrapper { 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) { @@ -110,6 +114,17 @@ public Answer execute(final StartCommand command, final LibvirtComputingResource } } + 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++) { diff --git a/server/src/main/java/com/cloud/api/ApiDispatcher.java b/server/src/main/java/com/cloud/api/ApiDispatcher.java index 3880f2aa9d1b..18121497365d 100644 --- a/server/src/main/java/com/cloud/api/ApiDispatcher.java +++ b/server/src/main/java/com/cloud/api/ApiDispatcher.java @@ -32,6 +32,7 @@ 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 void dispatch(final BaseCmd cmd, final Map params, final ((BaseAsyncCustomIdCmd)cmd).checkUuid(); } else if (cmd instanceof BaseCustomIdCmd) { ((BaseCustomIdCmd)cmd).checkUuid(); + } else if (cmd instanceof CreateConsoleEndpointCmd) { + Map 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(); diff --git a/server/src/main/java/com/cloud/api/ApiServlet.java b/server/src/main/java/com/cloud/api/ApiServlet.java index 4bdf31defaf4..f754392c3041 100644 --- a/server/src/main/java/com/cloud/api/ApiServlet.java +++ b/server/src/main/java/com/cloud/api/ApiServlet.java @@ -41,8 +41,11 @@ 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.jetbrains.annotations.Nullable; import org.springframework.stereotype.Component; @@ -324,6 +327,16 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp // 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 { diff --git a/server/src/main/java/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java index 487ec45a4249..f7594e8d0401 100644 --- a/server/src/main/java/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java @@ -21,6 +21,7 @@ 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 boolean configure(String name, Map params) throws Configu _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()) { diff --git a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java index 2bc092e056ba..c461d5b207b7 100644 --- a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java +++ b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java @@ -21,6 +21,7 @@ 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 AgentControlAnswer onConsoleAccessAuthentication(ConsoleAccessAuthenticat 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 AgentControlAnswer onConsoleAccessAuthentication(ConsoleAccessAuthenticat } 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 " + diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleAccessManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleAccessManagerImpl.java new file mode 100644 index 000000000000..1a485625f15d --- /dev/null +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleAccessManagerImpl.java @@ -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 allowedSessions; + + @Override + public boolean configure(String name, Map 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 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 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 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(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://:443/ticket/ + */ + 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 }; + } +} diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java index f7f88b0da66e..2308ffae96a9 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManager.java @@ -23,39 +23,40 @@ 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 NoVncConsoleDefault = new ConfigKey("Advanced", Boolean.class, "novnc.console.default", "true", + ConfigKey NoVncConsoleDefault = new ConfigKey("Advanced", Boolean.class, "novnc.console.default", "true", "If true, noVNC console will be default console for virtual machines", true); - public static final ConfigKey NoVncConsoleSourceIpCheckEnabled = new ConfigKey("Advanced", Boolean.class, "novnc.console.sourceip.check.enabled", "false", + ConfigKey NoVncConsoleSourceIpCheckEnabled = new ConfigKey("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 NoVncConsolePort = new ConfigKey<>("Advanced", Integer.class, "novnc.console.port", + "8080", "The listen port for noVNC console", true); + + void setManagementState(ConsoleProxyManagementState state); - public ConsoleProxyManagementState getManagementState(); + ConsoleProxyManagementState getManagementState(); - public void resumeLastManagementState(); + void resumeLastManagementState(); - public ConsoleProxyVO startProxy(long proxyVmId, boolean ignoreRestartSetting); + ConsoleProxyVO startProxy(long proxyVmId, boolean ignoreRestartSetting); - public boolean stopProxy(long proxyVmId); + boolean stopProxy(long proxyVmId); - public boolean rebootProxy(long proxyVmId); + boolean rebootProxy(long proxyVmId); - public boolean destroyProxy(long proxyVmId); + boolean destroyProxy(long proxyVmId); } diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index f984dab21b75..c230c162cd32 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -32,6 +32,7 @@ import com.cloud.utils.PasswordGenerator; import org.apache.cloudstack.agent.lb.IndirectAgentLB; import org.apache.cloudstack.ca.CAManager; +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.ca.Certificate; @@ -263,11 +264,14 @@ 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 @@ -1156,7 +1160,8 @@ public boolean configure(String name, Map params) throws Configu 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); @@ -1597,7 +1602,7 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] { NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled }; + return new ConfigKey[] { NoVncConsoleDefault, NoVncConsoleSourceIpCheckEnabled, NoVncConsolePort }; } protected ConsoleProxyStatus parseJsonToConsoleProxyStatus(String json) throws JsonParseException { @@ -1621,7 +1626,9 @@ protected void updateConsoleProxyStatus(String statusInfo, Long proxyVmId) { if (status.getConnections() != null) { count = status.getConnections().length; } - + if (status.getRemovedSessions() != null) { + consoleAccessManager.removeSessions(status.getRemovedSessions()); + } details = statusInfo.getBytes(Charset.forName("US-ASCII")); } else { s_logger.debug(String.format("Unable to retrieve load info from proxy {\"vmId\": %s}. Invalid load info [%s].", proxyVmId, statusInfo)); diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index 20b7a2b67d6b..93b37381ffcd 100644 --- a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java +++ b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java @@ -22,6 +22,7 @@ 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; @@ -283,6 +284,15 @@ protected VirtualMachineTO toVirtualMachineTO(VirtualMachineProfile vmProfile) { 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; } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 074fff86be5a..9c7eef4f2bdb 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -365,6 +365,7 @@ 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; @@ -3625,6 +3626,7 @@ public List> getCommands() { cmdList.add(IssueOutOfBandManagementPowerActionCmd.class); cmdList.add(ChangeOutOfBandManagementPasswordCmd.class); cmdList.add(GetUserKeysCmd.class); + cmdList.add(CreateConsoleEndpointCmd.class); return cmdList; } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java index 8f9363df5ba9..51ab3b8c2f0c 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java @@ -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 String getWebsocketUrl() { 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; + } } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index f0b295f4bb25..4389a03ead2e 100644 --- a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java +++ b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java @@ -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,45 +35,29 @@ 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 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 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 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) { 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 @@ private void handleThumbnailRequest(HttpServletRequest req, HttpServletResponse } } - 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, "

Console access is not supported for LXC

"); - return; - } - - String rootUrl = _ms.getConsoleAccessUrlRoot(vmId); - if (rootUrl == null) { - sendResponse(resp, "

Console access will be ready in a few minutes. Please try it again later.

"); - 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("").append(escapeHTML(vmName)).append(""); - 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 @@ private String composeThumbnailUrl(String rootUrl, VirtualMachine vm, HostVO hos 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://:443/ticket/ - */ - 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 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 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()); } diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml index 9c0e9a125f93..e57e94b4d3ad 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-managers-context.xml @@ -109,6 +109,8 @@ value="#{consoleProxyAllocatorsRegistry.registered}" /> + + diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java index 908819abb8b4..770e32f63d96 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxy.java @@ -170,7 +170,8 @@ public static ConsoleProxyServerFactory getHttpServerFactory() { } } - 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); @@ -178,6 +179,20 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console 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; @@ -192,7 +207,7 @@ public static ConsoleProxyAuthenticationResult authenticateConsoleAccess(Console 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); @@ -266,7 +281,8 @@ public static void startWithContext(Properties conf, Object context, byte[] ksBi 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) { @@ -456,7 +472,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, 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); @@ -477,7 +493,7 @@ public static ConsoleProxyClient getAjaxVncViewer(ConsoleProxyClientParam param, if (!viewer.isFrontEndAlive()) { - authenticationExternally(param); + authenticationExternally(param, null); viewer.initClient(param); reportLoadChange = true; } @@ -519,8 +535,8 @@ public static ConsoleProxyClientStatsCollector getStatsCollector() { } } - 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 authentication request for vm " + param.getClientTag() + " with sid " + param.getClientHostPassword()); @@ -530,7 +546,7 @@ public static void authenticationExternally(ConsoleProxyClientParam param) throw } public static ConsoleProxyAuthenticationResult reAuthenticationExternally(ConsoleProxyClientParam param) { - return authenticateConsoleAccess(param, true); + return authenticateConsoleAccess(param, true, null); } public static String getEncryptorPassword() { @@ -559,7 +575,7 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par 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); @@ -571,7 +587,7 @@ public static ConsoleProxyNoVncClient getNoVncViewer(ConsoleProxyClientParam par throw new AuthenticationException("Cannot use the existing viewer " + viewer + ": bad sid"); try { - authenticationExternally(param); + authenticationExternally(param, session); } catch (Exception e) { s_logger.error("Authentication failed for param: " + param); return null; diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClient.java index 6429de4ad2fa..e47837d49e9e 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClient.java @@ -78,4 +78,6 @@ public interface ConsoleProxyClient { void initClient(ConsoleProxyClientParam param); void closeClient(); + + String getSessionUuid(); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientBase.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientBase.java index 1c0f28d0a213..9c24ef619b04 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientBase.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientBase.java @@ -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 void setClientParam(ConsoleProxyClientParam clientParam) { ConsoleProxyPasswordBasedEncryptor encryptor = new ConsoleProxyPasswordBasedEncryptor(ConsoleProxy.getEncryptorPassword()); this.clientToken = encryptor.encryptObject(ConsoleProxyClientParam.class, clientParam); } + + @Override + public String getSessionUuid() { + return sessionUuid; + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java index c071f551da7d..198fd05f60fb 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientParam.java @@ -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 String getWebsocketUrl() { 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; + } } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java index 5251b9386d87..922c659cf6dd 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyClientStatsCollector.java @@ -20,6 +20,7 @@ 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 @@ public class ConsoleProxyClientStatsCollector { ArrayList connections; + ArrayList removedSessions; public ConsoleProxyClientStatsCollector() { } + public void setRemovedSessions(List removed) { + removedSessions = new ArrayList<>(); + removedSessions.addAll(removed); + } + public ConsoleProxyClientStatsCollector(Hashtable connMap) { setConnections(connMap); } @@ -67,6 +74,7 @@ private void setConnections(Hashtable connMap) { conn.tag = client.getClientTag(); conn.createTime = client.getClientCreateTime(); conn.lastUsedTime = client.getClientLastFrontEndActivityTime(); + conn.sessionUuid = client.getSessionUuid(); conns.add(conn); } } @@ -81,6 +89,7 @@ public static class ConsoleProxyConnection { public String tag; public long createTime; public long lastUsedTime; + public String sessionUuid; public ConsoleProxyConnection() { } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java index 2ddb2c7a1d58..7965a2083504 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyGCThread.java @@ -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 void run() { boolean bReportLoad = false; long lastReportTick = System.currentTimeMillis(); + List removedSessions = new ArrayList<>(); + while (true) { cleanupLogging(); bReportLoad = false; + removedSessions.clear(); if (s_logger.isDebugEnabled()) s_logger.debug("connMap=" + connMap); @@ -89,6 +94,7 @@ public void run() { } synchronized (connMap) { + removedSessions.add(client.getSessionUuid()); connMap.remove(key); bReportLoad = true; } @@ -100,7 +106,9 @@ public void run() { 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(); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java index 28d6ec1cab72..2d4a6c1e1a6a 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyHttpHandlerHelper.java @@ -96,6 +96,15 @@ public static Map getQueryMap(String query) { 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"); } diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java index 91d8e192fd9b..1b96b14f6717 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCHandler.java @@ -89,6 +89,9 @@ public void onConnect(final Session session) throws IOException, InterruptedExce 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 void onConnect(final Session session) throws IOException, InterruptedExce 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); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java index 28d179ba6fcd..1ee8e537e2dc 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVNCServer.java @@ -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 @@ 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); diff --git a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java index 596084955fad..f1c195913038 100644 --- a/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java +++ b/services/console-proxy/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyNoVncClient.java @@ -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 void initClient(ConsoleProxyClientParam param) { setClientParam(param); client = new NoVncClient(); connectionAlive = true; + this.sessionUuid = param.getSessionUuid(); updateFrontEndActivityTime(); Thread worker = new Thread(new Runnable() { @@ -192,6 +194,11 @@ public void closeClient() { ConsoleProxy.removeViewer(this); } + @Override + public String getSessionUuid() { + return sessionUuid; + } + @Override public int getClientId() { return this.clientId; diff --git a/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh b/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh index 8006f6bb2445..c0e336b5e7cc 100755 --- a/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh +++ b/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh @@ -32,6 +32,11 @@ setup_console_proxy() { public_ip=`getPublicIp` echo "$public_ip $NAME" >> /etc/hosts + 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 diff --git a/systemvm/patch-sysvms.sh b/systemvm/patch-sysvms.sh index c2083369be7d..ea75784a5bf4 100644 --- a/systemvm/patch-sysvms.sh +++ b/systemvm/patch-sysvms.sh @@ -90,7 +90,10 @@ restart_services() { fi done < "$svcfile" if [ "$TYPE" == "consoleproxy" ]; then - iptables -A INPUT -i eth2 -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT + vncport=`cat /root/vncport` + echo "vncport read: ${vncport}" + sed -i 's/8080/${vncport}/' /etc/iptables/rules.v4 + echo "vnc port ${vncport} rule applied" fi } diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 7d020a515a5b..441b21f1d920 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -201,7 +201,8 @@ 'UnmanagedInstance': 'Virtual Machine', 'Rolling': 'Rolling Maintenance', 'importVsphereStoragePolicies' : 'vSphere storage policies', - 'listVsphereStoragePolicies' : 'vSphere storage policies' + 'listVsphereStoragePolicies' : 'vSphere storage policies', + 'ConsoleEndpoint': 'Console Endpoint' } diff --git a/ui/src/components/widgets/Console.vue b/ui/src/components/widgets/Console.vue index a42843946fe0..81053225bb12 100644 --- a/ui/src/components/widgets/Console.vue +++ b/ui/src/components/widgets/Console.vue @@ -17,9 +17,8 @@