/******************************************************************************
 * Icinga 2                                                                   *
 * Copyright (C) 2012-2017 Icinga Development Team (https://www.icinga.com/)  *
 *                                                                            *
 * This program is free software; you can redistribute it and/or              *
 * modify it under the terms of the GNU General Public License                *
 * as published by the Free Software Foundation; either version 2             *
 * of the License, or (at your option) any later version.                     *
 *                                                                            *
 * This program is distributed in the hope that it will be useful,            *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of             *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              *
 * GNU General Public License for more details.                               *
 *                                                                            *
 * You should have received a copy of the GNU General Public License          *
 * along with this program; if not, write to the Free Software Foundation     *
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.             *
 ******************************************************************************/

#include "remote/jsonrpcconnection.hpp"
#include "remote/apilistener.hpp"
#include "remote/apifunction.hpp"
#include "remote/jsonrpc.hpp"
#include "base/configtype.hpp"
#include "base/objectlock.hpp"
#include "base/utility.hpp"
#include "base/logger.hpp"
#include "base/exception.hpp"
#include "base/convert.hpp"
#include <boost/thread/once.hpp>
#include <boost/regex.hpp>
#include <fstream>

using namespace icinga;

static Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(RequestCertificate, pki, &RequestCertificateHandler);
static Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params);
REGISTER_APIFUNCTION(UpdateCertificate, pki, &UpdateCertificateHandler);

Value RequestCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
	if (!params)
		return Empty;

	String certText = params->Get("cert_request");

	boost::shared_ptr<X509> cert;

	Dictionary::Ptr result = new Dictionary();

	/* Use the presented client certificate if not provided. */
	if (certText.IsEmpty())
		cert = origin->FromClient->GetStream()->GetPeerCertificate();
	else
		cert = StringToCertificate(certText);

	ApiListener::Ptr listener = ApiListener::GetInstance();
	boost::shared_ptr<X509> cacert = GetX509Certificate(listener->GetDefaultCaPath());

	String cn = GetCertificateCN(cert);

	bool signedByCA = VerifyCertificate(cacert, cert);

	Log(LogInformation, "JsonRpcConnection")
	    << "Received certificate request for CN '" << cn << "'"
	    << (signedByCA ? "" : " not") << " signed by our CA.";

	if (signedByCA) {
		time_t now;
		time(&now);

		/* auto-renew all certificates which were created before 2017 to force an update of the CA,
		 * because Icinga versions older than 2.4 sometimes create certificates with an invalid
		 * serial number. */
		time_t forceRenewalEnd = 1483228800; /* January 1st, 2017 */
		time_t renewalStart = now + 30 * 24 * 60 * 60;

		if (X509_cmp_time(X509_get_notBefore(cert.get()), &forceRenewalEnd) != -1 && X509_cmp_time(X509_get_notAfter(cert.get()), &renewalStart) != -1) {

			Log(LogInformation, "JsonRpcConnection")
			    << "The certificate for CN '" << cn << "' cannot be renewed yet.";
			result->Set("status_code", 1);
			result->Set("error", "The certificate for CN '" + cn + "' cannot be renewed yet.");
			return result;
		}
	}

	unsigned int n;
	unsigned char digest[EVP_MAX_MD_SIZE];

	if (!X509_digest(cert.get(), EVP_sha256(), digest, &n)) {
		result->Set("status_code", 1);
		result->Set("error", "Could not calculate fingerprint for the X509 certificate for CN '" + cn + "'.");

		Log(LogWarning, "JsonRpcConnection")
		    << "Could not calculate fingerprint for the X509 certificate requested for CN '"
		    << cn << "'.";

		return result;
	}

	char certFingerprint[EVP_MAX_MD_SIZE*2+1];
	for (unsigned int i = 0; i < n; i++)
		sprintf(certFingerprint + 2 * i, "%02x", digest[i]);

	result->Set("fingerprint_request", certFingerprint);

	String requestDir = ApiListener::GetCertificateRequestsDir();
	String requestPath = requestDir + "/" + certFingerprint + ".json";

	result->Set("ca", CertificateToString(cacert));

	JsonRpcConnection::Ptr client = origin->FromClient;

	/* If we already have a signed certificate request, send it to the client. */
	if (Utility::PathExists(requestPath)) {
		Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);

		String certResponse = request->Get("cert_response");

		if (!certResponse.IsEmpty()) {
			Log(LogInformation, "JsonRpcConnection")
			    << "Sending certificate response for CN '" << cn
			    << "' to endpoint '" << client->GetIdentity() << "'.";

			result->Set("cert", certResponse);
			result->Set("status_code", 0);

			Dictionary::Ptr message = new Dictionary();
			message->Set("jsonrpc", "2.0");
			message->Set("method", "pki::UpdateCertificate");
			message->Set("params", result);
			JsonRpc::SendMessage(client->GetStream(), message);

			return result;
		}
	}

	boost::shared_ptr<X509> newcert;
	boost::shared_ptr<EVP_PKEY> pubkey;
	X509_NAME *subject;
	Dictionary::Ptr message;
	String ticket;

	/* Check whether we are a signing instance or we
	 * must delay the signing request.
	 */
	if (!Utility::PathExists(GetIcingaCADir() + "/ca.key"))
		goto delayed_request;

	if (!signedByCA) {
		String salt = listener->GetTicketSalt();

		ticket = params->Get("ticket");

		/* Auto-signing is disabled by either a) no TicketSalt
		 * or b) the client did not include a ticket in its request.
		 */
		if (salt.IsEmpty() || ticket.IsEmpty())
			goto delayed_request;

		String realTicket = PBKDF2_SHA1(cn, salt, 50000);

		if (ticket != realTicket) {
			Log(LogWarning, "JsonRpcConnection")
			    << "Ticket for CN '" << cn << "' is invalid.";

			result->Set("status_code", 1);
			result->Set("error", "Invalid ticket for CN '" + cn + "'.");
			return result;
		}
	}

	pubkey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(cert.get()), EVP_PKEY_free);
	subject = X509_get_subject_name(cert.get());

	newcert = CreateCertIcingaCA(pubkey.get(), subject);

	/* verify that the new cert matches the CA we're using for the ApiListener;
	 * this ensures that the CA we have in /var/lib/icinga2/ca matches the one
	 * we're using for cluster connections (there's no point in sending a client
	 * a certificate it wouldn't be able to use to connect to us anyway) */
	if (!VerifyCertificate(cacert, newcert)) {
		Log(LogWarning, "JsonRpcConnection")
		    << "The CA in '" << listener->GetDefaultCaPath() << "' does not match the CA which Icinga uses "
		    << "for its own cluster connections. This is most likely a configuration problem.";
		goto delayed_request;
	}

	/* Send the signed certificate update. */
	Log(LogInformation, "JsonRpcConnection")
	    << "Sending certificate response for CN '" << cn << "' to endpoint '"
	    << client->GetIdentity() << "'" << (!ticket.IsEmpty() ? " (auto-signing ticket)" : "" ) << ".";

	result->Set("cert", CertificateToString(newcert));

	result->Set("status_code", 0);

	message = new Dictionary();
	message->Set("jsonrpc", "2.0");
	message->Set("method", "pki::UpdateCertificate");
	message->Set("params", result);
	JsonRpc::SendMessage(client->GetStream(), message);

	return result;

delayed_request:
	/* Send a delayed certificate signing request. */
	Utility::MkDirP(requestDir, 0700);

	Dictionary::Ptr request = new Dictionary();
	request->Set("cert_request", CertificateToString(cert));
	request->Set("ticket", params->Get("ticket"));

	Utility::SaveJsonFile(requestPath, 0600, request);

	JsonRpcConnection::SendCertificateRequest(JsonRpcConnection::Ptr(), origin, requestPath);

	result->Set("status_code", 2);
	result->Set("error", "Certificate request for CN '" + cn + "' is pending. Waiting for approval from the parent Icinga instance.");

	Log(LogInformation, "JsonRpcConnection")
	    << "Certificate request for CN '" << cn << "' is pending. Waiting for approval.";

	return result;
}

void JsonRpcConnection::SendCertificateRequest(const JsonRpcConnection::Ptr& aclient, const MessageOrigin::Ptr& origin, const String& path)
{
	Dictionary::Ptr message = new Dictionary();
	message->Set("jsonrpc", "2.0");
	message->Set("method", "pki::RequestCertificate");

	ApiListener::Ptr listener = ApiListener::GetInstance();

	if (!listener)
		return;

	Dictionary::Ptr params = new Dictionary();
	message->Set("params", params);

	/* Path is empty if this is our own request. */
	if (path.IsEmpty()) {
		String ticketPath = ApiListener::GetCertsDir() + "/ticket";

		std::ifstream fp(ticketPath.CStr());
		String ticket((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
		fp.close();

		params->Set("ticket", ticket);
	} else {
		Dictionary::Ptr request = Utility::LoadJsonFile(path);

		if (request->Contains("cert_response"))
			return;

		params->Set("cert_request", request->Get("cert_request"));
		params->Set("ticket", request->Get("ticket"));
	}

	/* Send the request to a) the connected client
	 * or b) the local zone and all parents.
	 */
	if (aclient)
		JsonRpc::SendMessage(aclient->GetStream(), message);
	else
		listener->RelayMessage(origin, Zone::GetLocalZone(), message, false);
}

Value UpdateCertificateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
	if (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)) {
		Log(LogWarning, "ClusterEvents")
		    << "Discarding 'update certificate' message from '" << origin->FromClient->GetIdentity() << "': Invalid endpoint origin (client not allowed).";

		return Empty;
	}

	String ca = params->Get("ca");
	String cert = params->Get("cert");

	ApiListener::Ptr listener = ApiListener::GetInstance();

	if (!listener)
		return Empty;

	boost::shared_ptr<X509> oldCert = GetX509Certificate(listener->GetDefaultCertPath());
	boost::shared_ptr<X509> newCert = StringToCertificate(cert);

	String cn = GetCertificateCN(newCert);

	Log(LogInformation, "JsonRpcConnection")
	    << "Received certificate update message for CN '" << cn << "'";

	/* Check if this is a certificate update for a subordinate instance. */
	boost::shared_ptr<EVP_PKEY> oldKey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(oldCert.get()), EVP_PKEY_free);
	boost::shared_ptr<EVP_PKEY> newKey = boost::shared_ptr<EVP_PKEY>(X509_get_pubkey(newCert.get()), EVP_PKEY_free);

	if (X509_NAME_cmp(X509_get_subject_name(oldCert.get()), X509_get_subject_name(newCert.get())) != 0 ||
	    EVP_PKEY_cmp(oldKey.get(), newKey.get()) != 1) {
		String certFingerprint = params->Get("fingerprint_request");

		/* Validate the fingerprint format. */
		boost::regex expr("^[0-9a-f]+$");

		if (!boost::regex_match(certFingerprint.GetData(), expr)) {
			Log(LogWarning, "JsonRpcConnection")
			    << "Endpoint '" << origin->FromClient->GetIdentity() << "' sent an invalid certificate fingerprint: '"
			    << certFingerprint << "' for CN '" << cn << "'.";
			return Empty;
		}

		String requestDir = ApiListener::GetCertificateRequestsDir();
		String requestPath = requestDir + "/" + certFingerprint + ".json";

		/* Save the received signed certificate request to disk. */
		if (Utility::PathExists(requestPath)) {
			Log(LogInformation, "JsonRpcConnection")
			    << "Saved certificate update for CN '" << cn << "'";

			Dictionary::Ptr request = Utility::LoadJsonFile(requestPath);
			request->Set("cert_response", cert);
			Utility::SaveJsonFile(requestPath, 0644, request);
		}

		return Empty;
	}

	/* Update CA certificate. */
	String caPath = listener->GetDefaultCaPath();

	Log(LogInformation, "JsonRpcConnection")
	    << "Updating CA certificate in '" << caPath << "'.";

	std::fstream cafp;
	String tempCaPath = Utility::CreateTempFile(caPath + ".XXXXXX", 0644, cafp);
	cafp << ca;
	cafp.close();

#ifdef _WIN32
	_unlink(caPath.CStr());
#endif /* _WIN32 */

	if (rename(tempCaPath.CStr(), caPath.CStr()) < 0) {
		BOOST_THROW_EXCEPTION(posix_error()
		    << boost::errinfo_api_function("rename")
		    << boost::errinfo_errno(errno)
		    << boost::errinfo_file_name(tempCaPath));
	}

	/* Update signed certificate. */
	String certPath = listener->GetDefaultCertPath();

	Log(LogInformation, "JsonRpcConnection")
	    << "Updating client certificate for CN '" << cn << "' in '" << certPath << "'.";

	std::fstream certfp;
	String tempCertPath = Utility::CreateTempFile(certPath + ".XXXXXX", 0644, certfp);
	certfp << cert;
	certfp.close();

#ifdef _WIN32
	_unlink(certPath.CStr());
#endif /* _WIN32 */

	if (rename(tempCertPath.CStr(), certPath.CStr()) < 0) {
		BOOST_THROW_EXCEPTION(posix_error()
		    << boost::errinfo_api_function("rename")
		    << boost::errinfo_errno(errno)
		    << boost::errinfo_file_name(tempCertPath));
	}

	/* Remove ticket for successful signing request. */
	String ticketPath = ApiListener::GetCertsDir() + "/ticket";

	if (unlink(ticketPath.CStr()) < 0 && errno != ENOENT) {
		BOOST_THROW_EXCEPTION(posix_error()
		    << boost::errinfo_api_function("unlink")
		    << boost::errinfo_errno(errno)
		    << boost::errinfo_file_name(ticketPath));
	}

	/* Update the certificates at runtime and reconnect all endpoints. */
	Log(LogInformation, "JsonRpcConnection")
	    << "Updating the client certificate for CN '" << cn << "' at runtime and reconnecting the endpoints.";

	listener->UpdateSSLContext();

	return Empty;
}
