//
// VMime library (http://www.vmime.org)
// Copyright (C) 2002 Vincent Richard <vincent@vmime.org>
//
// 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 3 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
//
// Linking this library statically or dynamically with other modules is making
// a combined work based on this library.  Thus, the terms and conditions of
// the GNU General Public License cover the whole combination.
//

#include "vmime/config.hpp"


#if VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_TLS_SUPPORT && VMIME_TLS_SUPPORT_LIB_IS_GNUTLS


#include <gnutls/gnutls.h>
#include <gnutls/x509.h>

#include <ctime>

#include "vmime/security/cert/gnutls/X509Certificate_GnuTLS.hpp"

#include "vmime/utility/outputStreamByteArrayAdapter.hpp"


namespace vmime {
namespace security {
namespace cert {


#ifndef VMIME_BUILDING_DOC

struct GnuTLSX509CertificateInternalData {

	GnuTLSX509CertificateInternalData() {

		gnutls_x509_crt_init(&cert);
	}

	~GnuTLSX509CertificateInternalData() {

		gnutls_x509_crt_deinit(cert);
	}

	void swap(gnutls_x509_crt_t to) {
		gnutls_x509_crt_deinit(cert);
		cert = to;
	}


	gnutls_x509_crt_t cert;
};

#endif // VMIME_BUILDING_DOC


X509Certificate_GnuTLS::X509Certificate_GnuTLS()
	: m_data(new GnuTLSX509CertificateInternalData) {

}


X509Certificate_GnuTLS::X509Certificate_GnuTLS(const X509Certificate&)
	: X509Certificate(), m_data(NULL) {

	// Not used
}


X509Certificate_GnuTLS::~X509Certificate_GnuTLS() {

	delete m_data;
}


void* X509Certificate_GnuTLS::getInternalData() {

	return &m_data->cert;
}


// static
shared_ptr <X509Certificate> X509Certificate::import(
	utility::inputStream& is
) {

	byteArray bytes;
	byte_t chunk[4096];

	while (!is.eof()) {
		const size_t len = is.read(chunk, sizeof(chunk));
		bytes.insert(bytes.end(), chunk, chunk + len);
	}

	return import(&bytes[0], bytes.size());
}


// static
shared_ptr <X509Certificate> X509Certificate::import(
	const byte_t* data,
	const size_t length
) {

	gnutls_datum_t buffer;
	buffer.data = const_cast <byte_t*>(data);
	buffer.size = static_cast <unsigned int>(length);

	// Try DER format
	shared_ptr <X509Certificate_GnuTLS> derCert = make_shared <X509Certificate_GnuTLS>();

	if (gnutls_x509_crt_import(derCert->m_data->cert, &buffer, GNUTLS_X509_FMT_DER) >= 0) {
		return derCert;
	}

	// Try PEM format
	shared_ptr <X509Certificate_GnuTLS> pemCert = make_shared <X509Certificate_GnuTLS>();

	if (gnutls_x509_crt_import(pemCert->m_data->cert, &buffer, GNUTLS_X509_FMT_PEM) >= 0) {
		return pemCert;
	}

	return null;
}


// static
void X509Certificate::import(
	utility::inputStream& is,
	std::vector <shared_ptr <X509Certificate> >& certs
) {

	byteArray bytes;
	byte_t chunk[4096];

	while (!is.eof()) {
		const size_t len = is.read(chunk, sizeof(chunk));
		bytes.insert(bytes.end(), chunk, chunk + len);
	}

	import(&bytes[0], bytes.size(), certs);
}


// static
void X509Certificate::import(
	const byte_t* data,
	const size_t length,
	std::vector <shared_ptr <X509Certificate> >& certs
) {

	gnutls_datum_t buffer;
	buffer.data = const_cast <byte_t*>(data);
	buffer.size = static_cast <unsigned int>(length);

	unsigned int size = 1024;
	gnutls_x509_crt_t x509[1024];

	// Try DER format
	if (gnutls_x509_crt_list_import(x509, &size, &buffer, GNUTLS_X509_FMT_DER, 0) < 0) {

		// Try PEM format
		if (gnutls_x509_crt_list_import(x509, &size, &buffer, GNUTLS_X509_FMT_PEM, 0) < 0) {
			return;
		}
	}

	for (unsigned int i = 0 ; i < size ; i += 1) {

		auto c = make_shared <X509Certificate_GnuTLS>();
		c->m_data->swap(x509[i]);
		certs.push_back(c);
	}
}


void X509Certificate_GnuTLS::write(
	utility::outputStream& os,
	const Format format
) const {

	size_t dataSize = 0;
	gnutls_x509_crt_fmt_t fmt = GNUTLS_X509_FMT_DER;

	switch (format) {
		case FORMAT_DER: fmt = GNUTLS_X509_FMT_DER; break;
		case FORMAT_PEM: fmt = GNUTLS_X509_FMT_PEM; break;
	}

	gnutls_x509_crt_export(m_data->cert, fmt, NULL, &dataSize);

	std::vector <byte_t> data(dataSize);

	gnutls_x509_crt_export(m_data->cert, fmt, &data[0], &dataSize);

	os.write(reinterpret_cast <byte_t*>(&data[0]), dataSize);
}


const byteArray X509Certificate_GnuTLS::getSerialNumber() const {

	char serial[64];
	size_t serialSize = sizeof(serial);

	gnutls_x509_crt_get_serial(m_data->cert, serial, &serialSize);

	return byteArray(serial, serial + serialSize);
}


bool X509Certificate_GnuTLS::checkIssuer(const shared_ptr <const X509Certificate>& issuer_) const {

	shared_ptr <const X509Certificate_GnuTLS> issuer =
		dynamicCast <const X509Certificate_GnuTLS>(issuer_);

	return gnutls_x509_crt_check_issuer(m_data->cert, issuer->m_data->cert) >= 1;
}


bool X509Certificate_GnuTLS::verify(const shared_ptr <const X509Certificate>& caCert_) const {

	shared_ptr <const X509Certificate_GnuTLS> caCert =
		dynamicCast <const X509Certificate_GnuTLS>(caCert_);

	unsigned int verify = 0;

	const int res = gnutls_x509_crt_verify(
		m_data->cert, &(caCert->m_data->cert), 1,
		GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT,
		&verify
	);

	return res == 0 && verify == 0;
}


bool X509Certificate_GnuTLS::verifyHostName(
	const string& hostname,
	std::vector <std::string>* nonMatchingNames
) const {

	if (gnutls_x509_crt_check_hostname(m_data->cert, hostname.c_str()) != 0) {
		return true;
	}

	if (nonMatchingNames) {

		const int MAX_CN = 256;
		const char* OID_X520_COMMON_NAME = "2.5.4.3";

		char dnsName[MAX_CN];
		size_t dnsNameLength;

		dnsNameLength = sizeof(dnsName);

		if (gnutls_x509_crt_get_dn_by_oid(m_data->cert, OID_X520_COMMON_NAME, 0, 0, dnsName, &dnsNameLength) >= 0) {
			nonMatchingNames->push_back(dnsName);
		}

		for (int i = 0, ret = 0 ; ret >= 0 ; ++i) {

			dnsNameLength = sizeof(dnsName);
			ret = gnutls_x509_crt_get_subject_alt_name(m_data->cert, i, dnsName, &dnsNameLength, NULL);

			if (ret == GNUTLS_SAN_DNSNAME) {
				nonMatchingNames->push_back(dnsName);
			}
		}
	}

	return false;
}


const datetime X509Certificate_GnuTLS::getActivationDate() const {

	const time_t t = gnutls_x509_crt_get_activation_time(m_data->cert);
	return datetime(t);
}


const datetime X509Certificate_GnuTLS::getExpirationDate() const {

	const time_t t = gnutls_x509_crt_get_expiration_time(m_data->cert);
	return datetime(t);
}


const byteArray X509Certificate_GnuTLS::getFingerprint(const DigestAlgorithm algo) const {

	gnutls_digest_algorithm_t galgo;

	switch (algo) {

		case DIGEST_MD5:

			galgo = GNUTLS_DIG_MD5;
			break;

		default:
		case DIGEST_SHA1:

			galgo = GNUTLS_DIG_SHA;
			break;
	}

	size_t bufferSize = 0;
	gnutls_x509_crt_get_fingerprint(m_data->cert, galgo, NULL, &bufferSize);

	std::vector <byte_t> buffer(bufferSize);

	if (gnutls_x509_crt_get_fingerprint(m_data->cert, galgo, &buffer[0], &bufferSize) == 0) {

		byteArray res;
		res.insert(res.end(), &buffer[0], &buffer[0] + bufferSize);

		return res;
	}

	return byteArray();
}


const byteArray X509Certificate_GnuTLS::getEncoded() const {

	byteArray bytes;
	utility::outputStreamByteArrayAdapter os(bytes);

	write(os, FORMAT_DER);

	return bytes;
}


const string X509Certificate_GnuTLS::getIssuerString() const {

	char buffer[4096];
	size_t bufferSize = sizeof(buffer);

	if (gnutls_x509_crt_get_issuer_dn(m_data->cert, buffer, &bufferSize) != GNUTLS_E_SUCCESS) {
		return "";
	}

	return buffer;
}


const string X509Certificate_GnuTLS::getType() const {

	return "X.509";
}


int X509Certificate_GnuTLS::getVersion() const {

	return gnutls_x509_crt_get_version(m_data->cert);
}


bool X509Certificate_GnuTLS::equals(const shared_ptr <const certificate>& other) const {

	shared_ptr <const X509Certificate_GnuTLS> otherX509 =
		dynamicCast <const X509Certificate_GnuTLS>(other);

	if (!otherX509) {
		return false;
	}

	const byteArray fp1 = getFingerprint(DIGEST_MD5);
	const byteArray fp2 = otherX509->getFingerprint(DIGEST_MD5);

	return fp1 == fp2;
}


} // cert
} // security
} // vmime


#endif // VMIME_HAVE_MESSAGING_FEATURES && VMIME_HAVE_TLS_SUPPORT && VMIME_TLS_SUPPORT_LIB_IS_GNUTLS
