From 15b740d39704568802598eaca88a30cf0ee55a20 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Tue, 26 Jul 2022 11:45:28 -0300 Subject: [PATCH] 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 | 16 + .../java/com/cloud/api/ApiDispatcher.java | 7 + .../main/java/com/cloud/api/ApiServlet.java | 16 +- .../AgentBasedConsoleProxyManager.java | 11 +- .../com/cloud/consoleproxy/AgentHookBase.java | 18 +- .../ConsoleAccessManagerImpl.java | 451 ++++++++++++++++++ .../consoleproxy/ConsoleProxyManager.java | 43 +- .../consoleproxy/ConsoleProxyManagerImpl.java | 15 +- .../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 + tools/apidoc/gen_toc.py | 3 +- ui/src/components/widgets/Console.vue | 19 +- .../consoleproxy/ConsoleAccessUtils.java | 27 ++ 36 files changed, 1080 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 257a1fc984a..3f5372af9aa 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 @@ 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(); 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 5fc248343ec..d612fb62485 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 class VirtualMachineTO { 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 00000000000..cd61c223ed1 --- /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 00000000000..b84fa474510 --- /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 00000000000..61b05be7ab3 --- /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 00000000000..55061088c4c --- /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 dd533d8774d..683d4afd5b2 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 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; + } } diff --git a/core/src/main/java/com/cloud/info/ConsoleProxyConnectionInfo.java b/core/src/main/java/com/cloud/info/ConsoleProxyConnectionInfo.java index 48819f49475..06a048a2180 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 3d3dda9a508..e9ef26a63b9 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 @@ 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; + } } 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 f151255d5cd..1eb988738e0 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 @@ -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 { 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 " + 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 3880f2aa9d1..18121497365 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.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 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 3d4167eaf4c..adcc5d74702 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.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 { diff --git a/server/src/main/java/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java b/server/src/main/java/com/cloud/consoleproxy/AgentBasedConsoleProxyManager.java index 487ec45a424..f7594e8d040 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 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()) { diff --git a/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java b/server/src/main/java/com/cloud/consoleproxy/AgentHookBase.java index 2bc092e056b..c461d5b207b 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.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 " + 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 00000000000..1a485625f15 --- /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 f7f88b0da66..2308ffae96a 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 @@ 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 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); - 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); } diff --git a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java index a3177fa7705..7b621fa13d9 100644 --- a/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java +++ b/server/src/main/java/com/cloud/consoleproxy/ConsoleProxyManagerImpl.java @@ -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 { diff --git a/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java b/server/src/main/java/com/cloud/hypervisor/HypervisorGuruBase.java index 6a0b575396c..6a20990c404 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 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; } diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index c31b583780c..5f599492339 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -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; } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyClientParam.java index 8f9363df5ba..51ab3b8c2f0 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 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; + } } diff --git a/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java b/server/src/main/java/com/cloud/servlet/ConsoleProxyServlet.java index b755a84887d..49fb7c89cd9 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,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, "

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 @@ 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://: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 30248d73a36..e34ea66da44 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 @@ -107,6 +107,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 702e9a855d1..9dcc156ca2a 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 @@ -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; 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 6429de4ad2f..e47837d49e9 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 bc2230b7aa9..d789ca9d096 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 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; + } } 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 c071f551da7..198fd05f60f 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 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; + } } 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 5251b9386d8..922c659cf6d 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.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 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 @@ 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() { } 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 2e987d707e5..de9577408c9 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 class ConsoleProxyGCThread extends Thread { 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 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(); 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 b7f969a1e57..6b89178cc0f 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 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"); } 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 91d8e192fd9..1b96b14f671 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 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); 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 28d179ba6fc..1ee8e537e2d 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 @@ 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); 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 cf0a05de622..04a5c5df3a4 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 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; diff --git a/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh b/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh index 3f00f3da43a..9d3aea01de7 100755 --- a/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh +++ b/systemvm/debian/opt/cloud/bin/setup/consoleproxy.sh @@ -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 diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 18996028182..832514031cc 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -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' } diff --git a/ui/src/components/widgets/Console.vue b/ui/src/components/widgets/Console.vue index 0c65600ea45..188189a66b0 100644 --- a/ui/src/components/widgets/Console.vue +++ b/ui/src/components/widgets/Console.vue @@ -17,9 +17,8 @@