| // 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 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; | |
| import org.apache.commons.codec.binary.Base64; | |
| import org.apache.commons.lang3.StringUtils; | |
| import org.apache.logging.log4j.Logger; | |
| import org.apache.logging.log4j.LogManager; | |
| import com.cloud.agent.AgentManager; | |
| import com.cloud.agent.api.AgentControlAnswer; | |
| import com.cloud.agent.api.Answer; | |
| import com.cloud.agent.api.ConsoleAccessAuthenticationAnswer; | |
| import com.cloud.agent.api.ConsoleAccessAuthenticationCommand; | |
| import com.cloud.agent.api.ConsoleProxyLoadReportCommand; | |
| import com.cloud.agent.api.GetVncPortAnswer; | |
| import com.cloud.agent.api.GetVncPortCommand; | |
| import com.cloud.agent.api.StartupCommand; | |
| import com.cloud.agent.api.StartupProxyCommand; | |
| import com.cloud.agent.api.proxy.StartConsoleProxyAgentHttpHandlerCommand; | |
| import com.cloud.configuration.Config; | |
| import com.cloud.exception.AgentUnavailableException; | |
| import com.cloud.exception.OperationTimedoutException; | |
| import com.cloud.host.Host; | |
| import com.cloud.host.HostVO; | |
| import com.cloud.host.Status; | |
| import com.cloud.host.dao.HostDao; | |
| import com.cloud.servlet.ConsoleProxyPasswordBasedEncryptor; | |
| import com.cloud.servlet.ConsoleProxyServlet; | |
| import com.cloud.utils.Ternary; | |
| import com.cloud.vm.VirtualMachine; | |
| import com.cloud.vm.dao.VMInstanceDao; | |
| import com.google.gson.Gson; | |
| import com.google.gson.GsonBuilder; | |
| /** | |
| * Utility class to manage interactions with agent-based console access | |
| * Extracted from ConsoleProxyManagerImpl so that other console proxy managers | |
| * can reuse | |
| */ | |
| public abstract class AgentHookBase implements AgentHook { | |
| protected Logger logger = LogManager.getLogger(getClass()); | |
| VMInstanceDao _instanceDao; | |
| HostDao _hostDao; | |
| ConfigurationDao _configDao; | |
| AgentManager _agentMgr; | |
| KeystoreManager _ksMgr; | |
| KeysManager _keysMgr; | |
| ConsoleAccessManager consoleAccessManager; | |
| protected 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 | |
| public AgentControlAnswer onConsoleAccessAuthentication(ConsoleAccessAuthenticationCommand cmd) { | |
| Long vmId = null; | |
| String ticketInUrl = cmd.getTicket(); | |
| String sessionUuid = cmd.getSessionUuid(); | |
| if (ticketInUrl == null) { | |
| 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); | |
| } | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Console authentication. Ticket in url for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticketInUrl); | |
| } | |
| if (!cmd.isReauthenticating()) { | |
| String ticket = consoleAccessManager.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), sessionUuid); | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Console authentication. Ticket in 1 minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + ticket); | |
| } | |
| if (!consoleAccessManager.isSessionAllowed(sessionUuid)) { | |
| logger.error(String.format("Session [%s] has been already used or does not exist.", sessionUuid)); | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| logger.debug(String.format("Acquiring session [%s] as it was just used.", sessionUuid)); | |
| consoleAccessManager.acquireSession(sessionUuid); | |
| if (!ticket.equals(ticketInUrl)) { | |
| Date now = new Date(); | |
| // considering of minute round-up | |
| String minuteEarlyTicket = consoleAccessManager.genAccessTicket(cmd.getHost(), cmd.getPort(), cmd.getSid(), cmd.getVmId(), new Date(now.getTime() - 60 * 1000), sessionUuid); | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Console authentication. Ticket in 2-minute boundary for " + cmd.getHost() + ":" + cmd.getPort() + "-" + cmd.getVmId() + " is " + | |
| minuteEarlyTicket); | |
| } | |
| if (!minuteEarlyTicket.equals(ticketInUrl)) { | |
| logger.error("Access ticket expired or has been modified. vmId: " + cmd.getVmId() + "ticket in URL: " + ticketInUrl + | |
| ", tickets to check against: " + ticket + "," + minuteEarlyTicket); | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| } | |
| } | |
| if (cmd.getVmId() != null && cmd.getVmId().isEmpty()) { | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Invalid vm id sent from proxy(happens when proxy session has terminated)"); | |
| } | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| VirtualMachine vm = _instanceDao.findByUuid(cmd.getVmId()); | |
| if (vm == null) { | |
| vm = _instanceDao.findById(Long.parseLong(cmd.getVmId())); | |
| } | |
| if (vm == null) { | |
| logger.error("Invalid vm id " + cmd.getVmId() + " sent from console access authentication"); | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| if (vm.getHostId() == null) { | |
| logger.warn("VM " + vmId + " lost host info, failed authentication request"); | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| HostVO host = _hostDao.findById(vm.getHostId()); | |
| if (host == null) { | |
| logger.warn("VM " + vmId + "'s host does not exist, fail authentication request"); | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| String sid = cmd.getSid(); | |
| if (sid == null || !sid.equals(vm.getVncPassword())) { | |
| logger.warn("sid " + sid + " in url does not match stored sid."); | |
| return new ConsoleAccessAuthenticationAnswer(cmd, false); | |
| } | |
| if (cmd.isReauthenticating()) { | |
| ConsoleAccessAuthenticationAnswer authenticationAnswer = new ConsoleAccessAuthenticationAnswer(cmd, true); | |
| authenticationAnswer.setReauthenticating(true); | |
| logger.info("Re-authentication request, ask host " + vm.getHostId() + " for new console info"); | |
| GetVncPortAnswer answer = (GetVncPortAnswer)_agentMgr.easySend(vm.getHostId(), new GetVncPortCommand(vm.getId(), vm.getInstanceName())); | |
| if (answer != null && answer.getResult()) { | |
| Ternary<String, String, String> parsedHostInfo = ConsoleProxyServlet.parseHostInfo(answer.getAddress()); | |
| if (parsedHostInfo.second() != null && parsedHostInfo.third() != null) { | |
| logger.info("Re-authentication result. vm: " + vm.getId() + ", tunnel url: " + parsedHostInfo.second() + ", tunnel session: " + | |
| parsedHostInfo.third()); | |
| authenticationAnswer.setTunnelUrl(parsedHostInfo.second()); | |
| authenticationAnswer.setTunnelSession(parsedHostInfo.third()); | |
| } else { | |
| logger.info("Re-authentication result. vm: " + vm.getId() + ", host address: " + parsedHostInfo.first() + ", port: " + answer.getPort()); | |
| authenticationAnswer.setHost(parsedHostInfo.first()); | |
| authenticationAnswer.setPort(answer.getPort()); | |
| } | |
| } else { | |
| logger.warn("Re-authentication request failed"); | |
| authenticationAnswer.setSuccess(false); | |
| } | |
| return authenticationAnswer; | |
| } | |
| return new ConsoleAccessAuthenticationAnswer(cmd, true); | |
| } | |
| @Override | |
| public void startAgentHttpHandlerInVM(StartupProxyCommand startupCmd) { | |
| StartConsoleProxyAgentHttpHandlerCommand cmd = null; | |
| try { | |
| SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); | |
| byte[] randomBytes = new byte[16]; | |
| random.nextBytes(randomBytes); | |
| String storePassword = Base64.encodeBase64String(randomBytes); | |
| byte[] ksBits = null; | |
| String consoleProxyUrlDomain = _configDao.getValue(Config.ConsoleProxyUrlDomain.key()); | |
| String consoleProxySslEnabled = _configDao.getValue(ConsoleProxyManager.ConsoleProxySslEnabled.key()); | |
| if (!StringUtils.isEmpty(consoleProxyUrlDomain) && !StringUtils.isEmpty(consoleProxySslEnabled) | |
| && consoleProxySslEnabled.equalsIgnoreCase("true")) { | |
| ksBits = _ksMgr.getKeystoreBits(ConsoleProxyManager.CERTIFICATE_NAME, ConsoleProxyManager.CERTIFICATE_NAME, storePassword); | |
| //ks manager raises exception if ksBits are null, hence no need to explicltly handle the condition | |
| } else { | |
| logger.debug("SSL is disabled for console proxy. To enable SSL, please configure consoleproxy.sslEnabled and consoleproxy.url.domain global settings."); | |
| } | |
| cmd = new StartConsoleProxyAgentHttpHandlerCommand(ksBits, storePassword); | |
| cmd.setEncryptorPassword(getEncryptorPassword()); | |
| cmd.setIsSourceIpCheckEnabled(Boolean.parseBoolean(_configDao.getValue(ConsoleProxyManager.NoVncConsoleSourceIpCheckEnabled.key()))); | |
| HostVO consoleProxyHost = findConsoleProxyHost(startupCmd); | |
| assert (consoleProxyHost != null); | |
| if (consoleProxyHost != null) { | |
| Answer answer = _agentMgr.send(consoleProxyHost.getId(), cmd); | |
| if (answer == null || !answer.getResult()) { | |
| logger.error("Console proxy agent reported that it failed to execute http handling startup command"); | |
| } else { | |
| logger.info("Successfully sent out command to start HTTP handling in console proxy agent"); | |
| } | |
| } | |
| }catch (NoSuchAlgorithmException e) { | |
| logger.error("Unexpected exception in SecureRandom Algorithm selection ", e); | |
| } catch (AgentUnavailableException e) { | |
| logger.error("Unable to send http handling startup command to the console proxy resource for proxy:" + startupCmd.getProxyVmId(), e); | |
| } catch (OperationTimedoutException e) { | |
| logger.error("Unable to send http handling startup command(time out) to the console proxy resource for proxy:" + startupCmd.getProxyVmId(), e); | |
| } catch (OutOfMemoryError e) { | |
| logger.error("Unrecoverable OutOfMemory Error, exit and let it be re-launched"); | |
| System.exit(1); | |
| } catch (Exception e) { | |
| logger.error( | |
| "Unexpected exception when sending http handling startup command(time out) to the console proxy resource for proxy:" + startupCmd.getProxyVmId(), e); | |
| } | |
| } | |
| private String getEncryptorPassword() { | |
| String key; | |
| String iv; | |
| ConsoleProxyPasswordBasedEncryptor.KeyIVPair keyIvPair = null; | |
| // if we failed after reset, something is definitely wrong | |
| for (int i = 0; i < 2; i++) { | |
| key = _keysMgr.getEncryptionKey(); | |
| iv = _keysMgr.getEncryptionIV(); | |
| keyIvPair = new ConsoleProxyPasswordBasedEncryptor.KeyIVPair(key, iv); | |
| if (keyIvPair.getIvBytes() == null || keyIvPair.getIvBytes().length != 16 || keyIvPair.getKeyBytes() == null || keyIvPair.getKeyBytes().length != 16) { | |
| logger.warn("Console access AES KeyIV sanity check failed, reset and regenerate"); | |
| _keysMgr.resetEncryptionKeyIV(); | |
| } else { | |
| break; | |
| } | |
| } | |
| Gson gson = new GsonBuilder().create(); | |
| return gson.toJson(keyIvPair); | |
| } | |
| protected abstract HostVO findConsoleProxyHost(StartupProxyCommand cmd); | |
| @Override | |
| public void onLoadReport(ConsoleProxyLoadReportCommand cmd) { | |
| // no-op since we do not auto-scale | |
| } | |
| @Override | |
| public void onAgentConnect(Host host, StartupCommand cmd) { | |
| // no-op | |
| } | |
| @Override | |
| public void onAgentDisconnect(long agentId, Status state) { | |
| // no-op since we do not autoscale | |
| } | |
| } |