/**
 * 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.ambari.server.security;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.apache.ambari.server.configuration.Configuration;
import org.apache.ambari.server.utils.ShellCommandUtil;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Map;

/**
 * Ambari security.
 * Manages server and agent certificates
 */
@Singleton
public class CertificateManager {

  @Inject Configuration configs;

  private static final Log LOG = LogFactory.getLog(CertificateManager.class);


  private static final String GEN_SRVR_KEY = "openssl genrsa -des3 " +
      "-passout pass:{0} -out {1}" + File.separator + "{2} 4096 ";
  private static final String GEN_SRVR_REQ = "openssl req -passin pass:{0} " +
      "-new -key {1}" + File.separator + "{2} -out {1}" + File.separator + "{5} -batch";
  private static final String SIGN_SRVR_CRT = "openssl ca -create_serial " +
    "-out {1}" + File.separator + "{3} -days 365 -keyfile {1}" + File.separator + "{2} -key {0} -selfsign " +
    "-extensions jdk7_ca -config {1}" + File.separator + "ca.config -batch " +
    "-infiles {1}" + File.separator + "{5}";
  private static final String EXPRT_KSTR = "openssl pkcs12 -export" +
      " -in {1}" + File.separator + "{3} -inkey {1}" + File.separator + "{2} -certfile {1}" + File.separator + "{3} -out {1}" + File.separator + "{4} " +
      "-password pass:{0} -passin pass:{0} \n";
  private static final String REVOKE_AGENT_CRT = "openssl ca " +
      "-config {0}" + File.separator + "ca.config -keyfile {0}" + File.separator + "{4} -revoke {0}" + File.separator + "{2} -batch " +
      "-passin pass:{3} -cert {0}" + File.separator + "{5}";
  private static final String SIGN_AGENT_CRT = "openssl ca -config " +
      "{0}" + File.separator + "ca.config -in {0}" + File.separator + "{1} -out {0}" + File.separator + "{2} -batch -passin pass:{3} " +
      "-keyfile {0}" + File.separator + "{4} -cert {0}" + File.separator + "{5}"; /**
       * Verify that root certificate exists, generate it otherwise.
       */
  public void initRootCert() {
    LOG.info("Initialization of root certificate");
    boolean certExists = isCertExists();
    LOG.info("Certificate exists:" + certExists);

    if (!certExists) {
      generateServerCertificate();
    }
  }

  /**
   * Checks root certificate state.
   * @return "true" if certificate exists
   */
  private boolean isCertExists() {

    Map<String, String> configsMap = configs.getConfigsMap();
    String srvrKstrDir = configsMap.get(Configuration.SRVR_KSTR_DIR_KEY);
    String srvrCrtName = configsMap.get(Configuration.SRVR_CRT_NAME_KEY);
    File certFile = new File(srvrKstrDir + File.separator + srvrCrtName);
    LOG.debug("srvrKstrDir = " + srvrKstrDir);
    LOG.debug("srvrCrtName = " + srvrCrtName);
    LOG.debug("certFile = " + certFile.getAbsolutePath());

    return certFile.exists();
  }


  /**
   * Runs os command
   *
   * @return command execution exit code
   */
  private int runCommand(String command) {
    String line = null;
    Process process = null;
    BufferedReader br= null;
    try {
      process = Runtime.getRuntime().exec(command);
      br = new BufferedReader(new InputStreamReader(
          process.getInputStream(), Charset.forName("UTF8")));

      while ((line = br.readLine()) != null) {
        LOG.info(line);
      }

      try {
        process.waitFor();
        ShellCommandUtil.logOpenSslExitCode(command, process.exitValue());
        return process.exitValue(); //command is executed
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    } catch (IOException e) {
      e.printStackTrace();
    } finally {
      if (br != null) {
        try {
          br.close();
        } catch (IOException ioe) {
          ioe.printStackTrace();
        }
      }
    }

    return -1;//some exception occurred

  }

  private void generateServerCertificate() {
    LOG.info("Generation of server certificate");

    Map<String, String> configsMap = configs.getConfigsMap();
    String srvrKstrDir = configsMap.get(Configuration.SRVR_KSTR_DIR_KEY);
    String srvrCrtName = configsMap.get(Configuration.SRVR_CRT_NAME_KEY);
    String srvrCsrName = configsMap.get(Configuration.SRVR_CSR_NAME_KEY);;
    String srvrKeyName = configsMap.get(Configuration.SRVR_KEY_NAME_KEY);
    String kstrName = configsMap.get(Configuration.KSTR_NAME_KEY);
    String srvrCrtPass = configsMap.get(Configuration.SRVR_CRT_PASS_KEY);

    Object[] scriptArgs = {srvrCrtPass, srvrKstrDir, srvrKeyName,
        srvrCrtName, kstrName, srvrCsrName};

    String command = MessageFormat.format(GEN_SRVR_KEY,scriptArgs);
    runCommand(command);

    command = MessageFormat.format(GEN_SRVR_REQ,scriptArgs);
    runCommand(command);

    command = MessageFormat.format(SIGN_SRVR_CRT,scriptArgs);
    runCommand(command);

    command = MessageFormat.format(EXPRT_KSTR,scriptArgs);
    runCommand(command);

  }

  /**
   * Returns server certificate content
   * @return string with server certificate content
   */
  public String getServerCert() {
    Map<String, String> configsMap = configs.getConfigsMap();
    File certFile = new File(configsMap.get(Configuration.SRVR_KSTR_DIR_KEY) +
        File.separator + configsMap.get(Configuration.SRVR_CRT_NAME_KEY));
    String srvrCrtContent = null;
    try {
      srvrCrtContent = FileUtils.readFileToString(certFile);
    } catch (IOException e) {
      LOG.error(e.getMessage());
    }
    return srvrCrtContent;
  }

  /**
   * Signs agent certificate
   * Adds agent certificate to server keystore
   * @return string with agent signed certificate content
   */
  public synchronized SignCertResponse signAgentCrt(String agentHostname, String agentCrtReqContent, String passphraseAgent) {
    SignCertResponse response = new SignCertResponse();
    LOG.info("Signing of agent certificate");
    LOG.info("Verifying passphrase");

    String passphraseSrvr = configs.getConfigsMap().get(Configuration.
        PASSPHRASE_KEY).trim();

    if (!passphraseSrvr.equals(passphraseAgent.trim())) {
      LOG.warn("Incorrect passphrase from the agent");
      response.setResult(SignCertResponse.ERROR_STATUS);
      response.setMessage("Incorrect passphrase from the agent");
      return response;
    }

    Map<String, String> configsMap = configs.getConfigsMap();
    String srvrKstrDir = configsMap.get(Configuration.SRVR_KSTR_DIR_KEY);
    String srvrCrtPass = configsMap.get(Configuration.SRVR_CRT_PASS_KEY);
    String srvrCrtName = configsMap.get(Configuration.SRVR_CRT_NAME_KEY);
    String srvrKeyName = configsMap.get(Configuration.SRVR_KEY_NAME_KEY);
    String agentCrtReqName = agentHostname + ".csr";
    String agentCrtName = agentHostname + ".crt";

    Object[] scriptArgs = {srvrKstrDir, agentCrtReqName, agentCrtName,
        srvrCrtPass, srvrKeyName, srvrCrtName};

    //Revoke previous agent certificate if exists
    File agentCrtFile = new File(srvrKstrDir + File.separator + agentCrtName);

    if (agentCrtFile.exists()) {
      LOG.info("Revoking of " + agentHostname + " certificate.");
      String command = MessageFormat.format(REVOKE_AGENT_CRT, scriptArgs);
      int commandExitCode = runCommand(command);
      if (commandExitCode != 0) {
        response.setResult(SignCertResponse.ERROR_STATUS);
        response.setMessage(ShellCommandUtil.getOpenSslCommandResult(command, commandExitCode));
        return response;
      }
    }

    File agentCrtReqFile = new File(srvrKstrDir + File.separator +
        agentCrtReqName);
    try {
      FileUtils.writeStringToFile(agentCrtReqFile, agentCrtReqContent);
    } catch (IOException e1) {
      // TODO Auto-generated catch block
      e1.printStackTrace();
    }

    String command = MessageFormat.format(SIGN_AGENT_CRT, scriptArgs);

    LOG.debug(ShellCommandUtil.hideOpenSslPassword(command));

    int commandExitCode = runCommand(command); // ssl command execution
    if (commandExitCode != 0) {
      response.setResult(SignCertResponse.ERROR_STATUS);
      response.setMessage(ShellCommandUtil.getOpenSslCommandResult(command, commandExitCode));
      //LOG.warn(ShellCommandUtil.getOpenSslCommandResult(command, commandExitCode));
      return response;
    }

    String agentCrtContent = "";
    try {
      agentCrtContent = FileUtils.readFileToString(agentCrtFile);
    } catch (IOException e) {
      e.printStackTrace();
      LOG.error("Error reading signed agent certificate");
      response.setResult(SignCertResponse.ERROR_STATUS);
      response.setMessage("Error reading signed agent certificate");
      return response;
    }
    response.setResult(SignCertResponse.OK_STATUS);
    response.setSignedCa(agentCrtContent);
    //LOG.info(ShellCommandUtil.getOpenSslCommandResult(command, commandExitCode));
    return response;
  }
}
