content: // 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 java.util.Objects;
  
  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.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.hypervisor.Hypervisor;
  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();
          String clientAddress = cmd.getClientAddress();
  
          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, clientAddress);
  
              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())) {
              if (Hypervisor.HypervisorType.External.equals(vm.getHypervisorType())) {
                  logger.debug("{} is on External hypervisor, skip checking sid", vm.getHypervisorType());
              } else {
                  logger.warn("sid {} in url does not match stored sid.", 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;
  
              HostVO consoleProxyHost = findConsoleProxyHost(startupCmd);
  
              if (Objects.isNull(consoleProxyHost)) {
                  throw new IllegalStateException("Console proxy host is null");
              }
  
              Long datacenterId = consoleProxyHost.getDataCenterId();
              String consoleProxyUrlDomain = ConsoleProxyManager.ConsoleProxyUrlDomain.valueIn(datacenterId);
              Boolean consoleProxySslEnabled = ConsoleProxyManager.ConsoleProxySslEnabled.valueIn(datacenterId);
              if (!StringUtils.isEmpty(consoleProxyUrlDomain) && Boolean.TRUE.equals(consoleProxySslEnabled)) {
                  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())));
  
              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
      }
  }
  