/**
 * @file ssl-gnutls.c GNUTLS SSL plugin.
 *
 * purple
 *
 * Copyright (C) 2003 Christian Hammond <chipx86@gnupdate.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 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 Street, Fifth Floor, Boston, MA  02111-1301  USA
 */
#include "internal.h"
#include "debug.h"
#include "certificate.h"
#include "plugin.h"
#include "sslconn.h"
#include "version.h"
#include "util.h"

#define SSL_GNUTLS_PLUGIN_ID "ssl-gnutls"

#ifdef HAVE_GNUTLS

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

typedef struct
{
	gnutls_session session;
	guint handshake_handler;
} PurpleSslGnutlsData;

#define PURPLE_SSL_GNUTLS_DATA(gsc) ((PurpleSslGnutlsData *)gsc->private_data)

static gnutls_certificate_client_credentials xcred;

static void
ssl_gnutls_init_gnutls(void)
{
	/* Configure GnuTLS to use glib memory management */
	/* I expect that this isn't really necessary, but it may prevent
	   some bugs */
	/* TODO: It may be necessary to wrap this allocators for GnuTLS.
	   If there are strange bugs, perhaps look here (yes, I am a
	   hypocrite) */
	gnutls_global_set_mem_functions(
		(gnutls_alloc_function)   g_malloc, /* malloc */
		(gnutls_alloc_function)   g_malloc, /* secure malloc */
		NULL,      /* mem_is_secure */
		(gnutls_realloc_function) g_realloc, /* realloc */
		(gnutls_free_function)    g_free     /* free */
		);

	gnutls_global_init();

	gnutls_certificate_allocate_credentials(&xcred);

	/* TODO: I can likely remove this */
	gnutls_certificate_set_x509_trust_file(xcred, "ca.pem",
		GNUTLS_X509_FMT_PEM);
}

static gboolean
ssl_gnutls_init(void)
{
	return TRUE;
}

static void
ssl_gnutls_uninit(void)
{
	gnutls_global_deinit();

	gnutls_certificate_free_credentials(xcred);
}

static void
ssl_gnutls_verified_cb(PurpleCertificateVerificationStatus st,
		       gpointer userdata)
{
	PurpleSslConnection *gsc = (PurpleSslConnection *) userdata;

	if (st == PURPLE_CERTIFICATE_VALID) {
		/* Certificate valid? Good! Do the connection! */
		gsc->connect_cb(gsc->connect_cb_data, gsc, PURPLE_INPUT_READ);
	} else {
		/* Otherwise, signal an error */
		if(gsc->error_cb != NULL)
			gsc->error_cb(gsc, PURPLE_SSL_CERTIFICATE_INVALID,
				      gsc->connect_cb_data);
		purple_ssl_close(gsc);
	}
}



static void ssl_gnutls_handshake_cb(gpointer data, gint source,
		PurpleInputCondition cond)
{
	PurpleSslConnection *gsc = data;
	PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc);
	ssize_t ret;

	/*purple_debug_info("gnutls", "Handshaking with %s\n", gsc->host);*/
	ret = gnutls_handshake(gnutls_data->session);

	if(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED)
		return;

	purple_input_remove(gnutls_data->handshake_handler);
	gnutls_data->handshake_handler = 0;

	if(ret != 0) {
		purple_debug_error("gnutls", "Handshake failed. Error %s\n",
			gnutls_strerror(ret));

		if(gsc->error_cb != NULL)
			gsc->error_cb(gsc, PURPLE_SSL_HANDSHAKE_FAILED,
				gsc->connect_cb_data);

		purple_ssl_close(gsc);
	} else {
		/* Now we are cooking with gas! */
		PurpleSslOps *ops = purple_ssl_get_ops();
		GList * peers = ops->get_peer_certificates(gsc);

		PurpleCertificateScheme *x509 =
			purple_certificate_find_scheme("x509");

		GList * l;

		/* TODO: Remove all this debugging babble */
		purple_debug_info("gnutls", "Handshake complete\n");

		for (l=peers; l; l = l->next) {
			PurpleCertificate *crt = l->data;
			GByteArray *z =
				x509->get_fingerprint_sha1(crt);
			gchar * fpr =
				purple_base16_encode_chunked(z->data,
							     z->len);

			purple_debug_info("gnutls/x509",
					  "Key print: %s\n",
					  fpr);

			/* Kill the cert! */
			x509->destroy_certificate(crt);

			g_free(fpr);
			g_byte_array_free(z, TRUE);
		}
		g_list_free(peers);

		{
			const gnutls_datum *cert_list;
			unsigned int cert_list_size = 0;
			gnutls_session session=gnutls_data->session;
			int i;

			cert_list =
				gnutls_certificate_get_peers(session, &cert_list_size);

			purple_debug_info("gnutls",
					    "Peer provided %d certs\n",
					    cert_list_size);
			for (i=0; i<cert_list_size; i++)
			{
				gchar fpr_bin[256];
				gsize fpr_bin_sz = sizeof(fpr_bin);
				gchar * fpr_asc = NULL;
				gchar tbuf[256];
				gsize tsz=sizeof(tbuf);
				gchar * tasc = NULL;
				gnutls_x509_crt cert;

				gnutls_x509_crt_init(&cert);
				gnutls_x509_crt_import (cert, &cert_list[i],
						GNUTLS_X509_FMT_DER);

				gnutls_x509_crt_get_fingerprint(cert, GNUTLS_MAC_SHA,
						fpr_bin, &fpr_bin_sz);

				fpr_asc =
						purple_base16_encode_chunked((const guchar *)fpr_bin, fpr_bin_sz);

				purple_debug_info("gnutls",
						"Lvl %d SHA1 fingerprint: %s\n",
						i, fpr_asc);

				tsz=sizeof(tbuf);
				gnutls_x509_crt_get_serial(cert,tbuf,&tsz);
				tasc=purple_base16_encode_chunked((const guchar *)tbuf, tsz);
				purple_debug_info("gnutls",
						"Serial: %s\n",
						tasc);
				g_free(tasc);

				tsz=sizeof(tbuf);
				gnutls_x509_crt_get_dn (cert, tbuf, &tsz);
				purple_debug_info("gnutls",
						"Cert DN: %s\n",
						tbuf);
				tsz=sizeof(tbuf);
				gnutls_x509_crt_get_issuer_dn (cert, tbuf, &tsz);
				purple_debug_info("gnutls",
						"Cert Issuer DN: %s\n",
						tbuf);

				g_free(fpr_asc);
				fpr_asc = NULL;
				gnutls_x509_crt_deinit(cert);
			}
		}

		/* TODO: The following logic should really be in libpurple */
		/* If a Verifier was given, hand control over to it */
		if (gsc->verifier) {
			GList *peers;
			/* First, get the peer cert chain */
			peers = purple_ssl_get_peer_certificates(gsc);

			/* Now kick off the verification process */
			purple_certificate_verify(gsc->verifier,
						  gsc->host,
						  peers,
						  ssl_gnutls_verified_cb,
						  gsc);

			purple_certificate_destroy_list(peers);
		} else {
			/* Otherwise, just call the "connection complete"
			   callback */
			gsc->connect_cb(gsc->connect_cb_data, gsc, cond);
		}
	}

}


static void
ssl_gnutls_connect(PurpleSslConnection *gsc)
{
	PurpleSslGnutlsData *gnutls_data;
	static const int cert_type_priority[2] = { GNUTLS_CRT_X509, 0 };

	gnutls_data = g_new0(PurpleSslGnutlsData, 1);
	gsc->private_data = gnutls_data;

	gnutls_init(&gnutls_data->session, GNUTLS_CLIENT);
	gnutls_set_default_priority(gnutls_data->session);

	gnutls_certificate_type_set_priority(gnutls_data->session,
		cert_type_priority);

	gnutls_credentials_set(gnutls_data->session, GNUTLS_CRD_CERTIFICATE,
		xcred);

	gnutls_transport_set_ptr(gnutls_data->session, GINT_TO_POINTER(gsc->fd));

	gnutls_data->handshake_handler = purple_input_add(gsc->fd,
		PURPLE_INPUT_READ, ssl_gnutls_handshake_cb, gsc);

	purple_debug_info("gnutls", "Starting handshake with %s\n", gsc->host);

	/* Orborde asks: Why are we configuring a callback, then
	   immediately calling it?

	   Answer: gnutls_handshake (up in handshake_cb) needs to be called
	   once in order to get the ball rolling on the SSL connection.
	   Once it has done so, only then will the server reply, triggering
	   the callback.

	   Since the logic driving gnutls_handshake is the same with the first
	   and subsequent calls, we'll just fire the callback immediately to
	   accomplish this.
	*/
	ssl_gnutls_handshake_cb(gsc, gsc->fd, PURPLE_INPUT_READ);
}

static void
ssl_gnutls_close(PurpleSslConnection *gsc)
{
	PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc);

	if(!gnutls_data)
		return;

	if(gnutls_data->handshake_handler)
		purple_input_remove(gnutls_data->handshake_handler);

	gnutls_bye(gnutls_data->session, GNUTLS_SHUT_RDWR);

	gnutls_deinit(gnutls_data->session);

	g_free(gnutls_data);
	gsc->private_data = NULL;
}

static size_t
ssl_gnutls_read(PurpleSslConnection *gsc, void *data, size_t len)
{
	PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc);
	ssize_t s;

	s = gnutls_record_recv(gnutls_data->session, data, len);

	if(s == GNUTLS_E_AGAIN || s == GNUTLS_E_INTERRUPTED) {
		s = -1;
		errno = EAGAIN;
	} else if(s < 0) {
		purple_debug_error("gnutls", "receive failed: %s\n",
				gnutls_strerror(s));
		s = -1;
		/*
		 * TODO: Set errno to something more appropriate.  Or even
		 *       better: allow ssl plugins to keep track of their
		 *       own error message, then add a new ssl_ops function
		 *       that returns the error message.
		 */
		errno = EIO;
	}

	return s;
}

static size_t
ssl_gnutls_write(PurpleSslConnection *gsc, const void *data, size_t len)
{
	PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc);
	ssize_t s = 0;

	/* XXX: when will gnutls_data be NULL? */
	if(gnutls_data)
		s = gnutls_record_send(gnutls_data->session, data, len);

	if(s == GNUTLS_E_AGAIN || s == GNUTLS_E_INTERRUPTED) {
		s = -1;
		errno = EAGAIN;
	} else if(s < 0) {
		purple_debug_error("gnutls", "send failed: %s\n",
				gnutls_strerror(s));
		s = -1;
		/*
		 * TODO: Set errno to something more appropriate.  Or even
		 *       better: allow ssl plugins to keep track of their
		 *       own error message, then add a new ssl_ops function
		 *       that returns the error message.
		 */
		errno = EIO;
	}

	return s;
}

/* Forward declarations are fun! */
static PurpleCertificate *
x509_import_from_datum(const gnutls_datum dt, gnutls_x509_crt_fmt mode);

static GList *
ssl_gnutls_get_peer_certificates(PurpleSslConnection * gsc)
{
	PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc);

	/* List of Certificate instances to return */
	GList * peer_certs = NULL;

	/* List of raw certificates as given by GnuTLS */
	const gnutls_datum *cert_list;
	unsigned int cert_list_size = 0;

	unsigned int i;

	/* This should never, ever happen. */
	g_return_val_if_fail( gnutls_certificate_type_get (gnutls_data->session) == GNUTLS_CRT_X509, NULL);

	/* Get the certificate list from GnuTLS */
	/* TODO: I am _pretty sure_ this doesn't block or do other exciting things */
	cert_list = gnutls_certificate_get_peers(gnutls_data->session,
						 &cert_list_size);

	/* Convert each certificate to a Certificate and append it to the list */
	for (i = 0; i < cert_list_size; i++) {
		PurpleCertificate * newcrt = x509_import_from_datum(cert_list[i],
							      GNUTLS_X509_FMT_DER);
		/* Append is somewhat inefficient on linked lists, but is easy
		   to read. If someone complains, I'll change it.
		   TODO: Is anyone complaining? (Maybe elb?) */
		peer_certs = g_list_append(peer_certs, newcrt);
	}

	/* cert_list doesn't need free()-ing */

	return peer_certs;
}

/************************************************************************/
/* X.509 functionality                                                  */
/************************************************************************/
const gchar * SCHEME_NAME = "x509";

static PurpleCertificateScheme x509_gnutls;

/** Refcounted GnuTLS certificate data instance */
typedef struct {
	gint refcount;
	gnutls_x509_crt crt;
} x509_crtdata_t;

/** Helper functions for reference counting */
static x509_crtdata_t *
x509_crtdata_addref(x509_crtdata_t *cd)
{
	(cd->refcount)++;
	return cd;
}

static void
x509_crtdata_delref(x509_crtdata_t *cd)
{
	(cd->refcount)--;

	if (cd->refcount < 0)
		g_critical("Refcount of x509_crtdata_t is %d, which is less "
				"than zero!\n", cd->refcount);

	/* If the refcount reaches zero, kill the structure */
	if (cd->refcount <= 0) {
		/* Kill the internal data */
		gnutls_x509_crt_deinit( cd->crt );
		/* And kill the struct */
		g_free( cd );
	}
}

/** Helper macro to retrieve the GnuTLS crt_t from a PurpleCertificate */
#define X509_GET_GNUTLS_DATA(pcrt) ( ((x509_crtdata_t *) (pcrt->data))->crt)

/** Transforms a gnutls_datum containing an X.509 certificate into a Certificate instance under the x509_gnutls scheme
 *
 * @param dt   Datum to transform
 * @param mode GnuTLS certificate format specifier (GNUTLS_X509_FMT_PEM for
 *             reading from files, and GNUTLS_X509_FMT_DER for converting
 *             "over the wire" certs for SSL)
 *
 * @return A newly allocated Certificate structure of the x509_gnutls scheme
 */
static PurpleCertificate *
x509_import_from_datum(const gnutls_datum dt, gnutls_x509_crt_fmt mode)
{
	/* Internal certificate data structure */
	x509_crtdata_t *certdat;
	/* New certificate to return */
	PurpleCertificate * crt;

	/* Allocate and prepare the internal certificate data */
	certdat = g_new0(x509_crtdata_t, 1);
	gnutls_x509_crt_init(&(certdat->crt));
	certdat->refcount = 0;

	/* Perform the actual certificate parse */
	/* Yes, certdat->crt should be passed as-is */
	gnutls_x509_crt_import(certdat->crt, &dt, mode);

	/* Allocate the certificate and load it with data */
	crt = g_new0(PurpleCertificate, 1);
	crt->scheme = &x509_gnutls;
	crt->data = x509_crtdata_addref(certdat);

	return crt;
}

/** Imports a PEM-formatted X.509 certificate from the specified file.
 * @param filename Filename to import from. Format is PEM
 *
 * @return A newly allocated Certificate structure of the x509_gnutls scheme
 */
static PurpleCertificate *
x509_import_from_file(const gchar * filename)
{
	PurpleCertificate *crt;  /* Certificate being constructed */
	gchar *buf;        /* Used to load the raw file data */
	gsize buf_sz;      /* Size of the above */
	gnutls_datum dt; /* Struct to pass down to GnuTLS */

	purple_debug_info("gnutls",
			  "Attempting to load X.509 certificate from %s\n",
			  filename);

	/* Next, we'll simply yank the entire contents of the file
	   into memory */
	/* TODO: Should I worry about very large files here? */
	g_return_val_if_fail(
		g_file_get_contents(filename,
			    &buf,
			    &buf_sz,
			    NULL      /* No error checking for now */
		),
		NULL);

	/* Load the datum struct */
	dt.data = (unsigned char *) buf;
	dt.size = buf_sz;

	/* Perform the conversion; files should be in PEM format */
	crt = x509_import_from_datum(dt, GNUTLS_X509_FMT_PEM);

	/* Cleanup */
	g_free(buf);

	return crt;
}

/**
 * Exports a PEM-formatted X.509 certificate to the specified file.
 * @param filename Filename to export to. Format will be PEM
 * @param crt      Certificate to export
 *
 * @return TRUE if success, otherwise FALSE
 */
static gboolean
x509_export_certificate(const gchar *filename, PurpleCertificate *crt)
{
	gnutls_x509_crt crt_dat; /* GnuTLS cert struct */
	int ret;
	gchar * out_buf; /* Data to output */
	size_t out_size; /* Output size */
	gboolean success = FALSE;

	/* Paranoia paranoia paranoia! */
	g_return_val_if_fail(filename, FALSE);
	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, FALSE);
	g_return_val_if_fail(crt->data, FALSE);

	crt_dat = X509_GET_GNUTLS_DATA(crt);

	/* Obtain the output size required */
	out_size = 0;
	ret = gnutls_x509_crt_export(crt_dat, GNUTLS_X509_FMT_PEM,
				     NULL, /* Provide no buffer yet */
				     &out_size /* Put size here */
		);
	g_return_val_if_fail(ret == GNUTLS_E_SHORT_MEMORY_BUFFER, FALSE);

	/* Now allocate a buffer and *really* export it */
	out_buf = g_new0(gchar, out_size);
	ret = gnutls_x509_crt_export(crt_dat, GNUTLS_X509_FMT_PEM,
				     out_buf, /* Export to our new buffer */
				     &out_size /* Put size here */
		);
	if (ret != 0) {
		purple_debug_error("gnutls/x509",
				   "Failed to export cert to buffer with code %d\n",
				   ret);
		g_free(out_buf);
		return FALSE;
	}

	/* Write it out to an actual file */
	success = purple_util_write_data_to_file_absolute(filename,
							  out_buf, out_size);

	g_free(out_buf);
	g_return_val_if_fail(success, FALSE);
	return success;
}

static PurpleCertificate *
x509_copy_certificate(PurpleCertificate *crt)
{
	x509_crtdata_t *crtdat;
	PurpleCertificate *newcrt;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, NULL);

	crtdat = (x509_crtdata_t *) crt->data;

	newcrt = g_new0(PurpleCertificate, 1);
	newcrt->scheme = &x509_gnutls;
	newcrt->data = x509_crtdata_addref(crtdat);

	return newcrt;
}
/** Frees a Certificate
 *
 * Destroys a Certificate's internal data structures and frees the pointer
 * given.
 * @param crt Certificate instance to be destroyed. It WILL NOT be destroyed
 *            if it is not of the correct CertificateScheme. Can be NULL
 *
 */
static void
x509_destroy_certificate(PurpleCertificate * crt)
{
	if (NULL == crt) return;

	/* Check that the scheme is x509_gnutls */
	if ( crt->scheme != &x509_gnutls ) {
		purple_debug_error("gnutls",
				   "destroy_certificate attempted on certificate of wrong scheme (scheme was %s, expected %s)\n",
				   crt->scheme->name,
				   SCHEME_NAME);
		return;
	}

	g_return_if_fail(crt->data != NULL);
	g_return_if_fail(crt->scheme != NULL);

	/* Use the reference counting system to free (or not) the
	   underlying data */
	x509_crtdata_delref((x509_crtdata_t *)crt->data);

	/* Kill the structure itself */
	g_free(crt);
}

/** Determines whether one certificate has been issued and signed by another
 *
 * @param crt       Certificate to check the signature of
 * @param issuer    Issuer's certificate
 *
 * @return TRUE if crt was signed and issued by issuer, otherwise FALSE
 * @TODO  Modify this function to return a reason for invalidity?
 */
static gboolean
x509_certificate_signed_by(PurpleCertificate * crt,
			   PurpleCertificate * issuer)
{
	gnutls_x509_crt crt_dat;
	gnutls_x509_crt issuer_dat;
	unsigned int verify; /* used to store result from GnuTLS verifier */
	int ret;

	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(issuer, FALSE);

	/* Verify that both certs are the correct scheme */
	g_return_val_if_fail(crt->scheme == &x509_gnutls, FALSE);
	g_return_val_if_fail(issuer->scheme == &x509_gnutls, FALSE);

	/* TODO: check for more nullness? */

	crt_dat = X509_GET_GNUTLS_DATA(crt);
	issuer_dat = X509_GET_GNUTLS_DATA(issuer);

	/* First, let's check that crt.issuer is actually issuer */
	ret = gnutls_x509_crt_check_issuer(crt_dat, issuer_dat);
	if (ret <= 0) {

		if (ret < 0) {
			purple_debug_error("gnutls/x509",
					   "GnuTLS error %d while checking certificate issuer match.",
					   ret);
		} else {
			gchar *crt_id, *issuer_id, *crt_issuer_id;
			crt_id = purple_certificate_get_unique_id(crt);
			issuer_id = purple_certificate_get_unique_id(issuer);
			crt_issuer_id =
				purple_certificate_get_issuer_unique_id(crt);
			purple_debug_info("gnutls/x509",
					  "Certificate for %s claims to be "
					  "issued by %s, but the certificate "
					  "for %s does not match.\n",
					  crt_id ? crt_id : "(null)",
					  crt_issuer_id ? crt_issuer_id : "(null)",
					  issuer_id ? issuer_id : "(null)");
			g_free(crt_id);
			g_free(issuer_id);
			g_free(crt_issuer_id);
		}

		/* The issuer is not correct, or there were errors */
		return FALSE;
	}

	/* Now, check the signature */
	/* The second argument is a ptr to an array of "trusted" issuer certs,
	   but we're only using one trusted one */
	ret = gnutls_x509_crt_verify(crt_dat, &issuer_dat, 1,
				     /* Permit signings by X.509v1 certs
					(Verisign and possibly others have
					root certificates that predate the
					current standard) */
				     GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT,
				     &verify);

	if (ret != 0) {
		purple_debug_error("gnutls/x509",
				   "Attempted certificate verification caused a GnuTLS error code %d. I will just say the signature is bad, but you should look into this.\n", ret);
		return FALSE;
	}

	if (verify & GNUTLS_CERT_INVALID) {
		/* Signature didn't check out, but at least
		   there were no errors*/
		gchar *crt_id = purple_certificate_get_unique_id(crt);
		gchar *issuer_id = purple_certificate_get_issuer_unique_id(crt);
		purple_debug_info("gnutls/x509",
				  "Bad signature for %s on %s\n",
				  issuer_id, crt_id);
		g_free(crt_id);
		g_free(issuer_id);

		return FALSE;
	} /* if (ret, etc.) */

	/* If we got here, the signature is good */
	return TRUE;
}

static GByteArray *
x509_sha1sum(PurpleCertificate *crt)
{
	size_t hashlen = 20; /* SHA1 hashes are 20 bytes */
	size_t tmpsz = hashlen; /* Throw-away variable for GnuTLS to stomp on*/
	gnutls_x509_crt crt_dat;
	GByteArray *hash; /**< Final hash container */
	guchar hashbuf[hashlen]; /**< Temporary buffer to contain hash */

	g_return_val_if_fail(crt, NULL);

	crt_dat = X509_GET_GNUTLS_DATA(crt);

	/* Extract the fingerprint */
	g_return_val_if_fail(
		0 == gnutls_x509_crt_get_fingerprint(crt_dat, GNUTLS_MAC_SHA,
						     hashbuf, &tmpsz),
		NULL);

	/* This shouldn't happen */
	g_return_val_if_fail(tmpsz == hashlen, NULL);

	/* Okay, now create and fill hash array */
	hash = g_byte_array_new();
	g_byte_array_append(hash, hashbuf, hashlen);

	return hash;
}

static gchar *
x509_cert_dn (PurpleCertificate *crt)
{
	gnutls_x509_crt cert_dat;
	gchar *dn = NULL;
	size_t dn_size;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, NULL);

	cert_dat = X509_GET_GNUTLS_DATA(crt);

	/* Figure out the length of the Distinguished Name */
	/* Claim that the buffer is size 0 so GnuTLS just tells us how much
	   space it needs */
	dn_size = 0;
	gnutls_x509_crt_get_dn(cert_dat, dn, &dn_size);

	/* Now allocate and get the Distinguished Name */
	/* Old versions of GnuTLS have an off-by-one error in reporting
	   the size of the needed buffer in some functions, so allocate
	   an extra byte */
	dn = g_new0(gchar, ++dn_size);
	if (0 != gnutls_x509_crt_get_dn(cert_dat, dn, &dn_size)) {
		purple_debug_error("gnutls/x509",
				   "Failed to get Distinguished Name\n");
		g_free(dn);
		return NULL;
	}

	return dn;
}

static gchar *
x509_issuer_dn (PurpleCertificate *crt)
{
	gnutls_x509_crt cert_dat;
	gchar *dn = NULL;
	size_t dn_size;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, NULL);

	cert_dat = X509_GET_GNUTLS_DATA(crt);

	/* Figure out the length of the Distinguished Name */
	/* Claim that the buffer is size 0 so GnuTLS just tells us how much
	   space it needs */
	dn_size = 0;
	gnutls_x509_crt_get_issuer_dn(cert_dat, dn, &dn_size);

	/* Now allocate and get the Distinguished Name */
	/* Old versions of GnuTLS have an off-by-one error in reporting
	   the size of the needed buffer in some functions, so allocate
	   an extra byte */
	dn = g_new0(gchar, ++dn_size);
	if (0 != gnutls_x509_crt_get_issuer_dn(cert_dat, dn, &dn_size)) {
		purple_debug_error("gnutls/x509",
				   "Failed to get issuer's Distinguished "
				   "Name\n");
		g_free(dn);
		return NULL;
	}

	return dn;
}

static gchar *
x509_common_name (PurpleCertificate *crt)
{
	gnutls_x509_crt cert_dat;
	gchar *cn = NULL;
	size_t cn_size;
	int ret;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, NULL);

	cert_dat = X509_GET_GNUTLS_DATA(crt);

	/* Figure out the length of the Common Name */
	/* Claim that the buffer is size 0 so GnuTLS just tells us how much
	   space it needs */
	cn_size = 0;
	gnutls_x509_crt_get_dn_by_oid(cert_dat,
				      GNUTLS_OID_X520_COMMON_NAME,
				      0, /* First CN found, please */
				      0, /* Not in raw mode */
				      cn, &cn_size);

	/* Now allocate and get the Common Name */
	/* Old versions of GnuTLS have an off-by-one error in reporting
	   the size of the needed buffer in some functions, so allocate
	   an extra byte */
	cn = g_new0(gchar, ++cn_size);
	ret = gnutls_x509_crt_get_dn_by_oid(cert_dat,
					    GNUTLS_OID_X520_COMMON_NAME,
					    0, /* First CN found, please */
					    0, /* Not in raw mode */
					    cn, &cn_size);
	if (ret != 0) {
		purple_debug_error("gnutls/x509",
				   "Failed to get Common Name\n");
		g_free(cn);
		return NULL;
	}

	return cn;
}

static gboolean
x509_check_name (PurpleCertificate *crt, const gchar *name)
{
	gnutls_x509_crt crt_dat;

	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, FALSE);
	g_return_val_if_fail(name, FALSE);

	crt_dat = X509_GET_GNUTLS_DATA(crt);

	if (gnutls_x509_crt_check_hostname(crt_dat, name)) {
		return TRUE;
	} else {
		return FALSE;
	}
}

static gboolean
x509_times (PurpleCertificate *crt, time_t *activation, time_t *expiration)
{
	gnutls_x509_crt crt_dat;
	/* GnuTLS time functions return this on error */
	const time_t errval = (time_t) (-1);
	gboolean success = TRUE;

	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_gnutls, FALSE);

	crt_dat = X509_GET_GNUTLS_DATA(crt);

	if (activation) {
		*activation = gnutls_x509_crt_get_activation_time(crt_dat);
		if (*activation == errval)
			success = FALSE;
	}
	if (expiration) {
		*expiration = gnutls_x509_crt_get_expiration_time(crt_dat);
		if (*expiration == errval)
			success = FALSE;
	}

	return success;
}

/* X.509 certificate operations provided by this plugin */
static PurpleCertificateScheme x509_gnutls = {
	"x509",                          /* Scheme name */
	N_("X.509 Certificates"),        /* User-visible scheme name */
	x509_import_from_file,           /* Certificate import function */
	x509_export_certificate,         /* Certificate export function */
	x509_copy_certificate,           /* Copy */
	x509_destroy_certificate,        /* Destroy cert */
	x509_certificate_signed_by,      /* Signature checker */
	x509_sha1sum,                    /* SHA1 fingerprint */
	x509_cert_dn,                    /* Unique ID */
	x509_issuer_dn,                  /* Issuer Unique ID */
	x509_common_name,                /* Subject name */
	x509_check_name,                 /* Check subject name */
	x509_times,                      /* Activation/Expiration time */

	NULL,
	NULL,
	NULL,
	NULL

};

static PurpleSslOps ssl_ops =
{
	ssl_gnutls_init,
	ssl_gnutls_uninit,
	ssl_gnutls_connect,
	ssl_gnutls_close,
	ssl_gnutls_read,
	ssl_gnutls_write,
	ssl_gnutls_get_peer_certificates,

	/* padding */
	NULL,
	NULL,
	NULL
};

#endif /* HAVE_GNUTLS */

static gboolean
plugin_load(PurplePlugin *plugin)
{
#ifdef HAVE_GNUTLS
	if(!purple_ssl_get_ops()) {
		purple_ssl_set_ops(&ssl_ops);
	}

	/* Init GNUTLS now so others can use it even if sslconn never does */
	ssl_gnutls_init_gnutls();

	/* Register that we're providing an X.509 CertScheme */
	purple_certificate_register_scheme( &x509_gnutls );

	return TRUE;
#else
	return FALSE;
#endif
}

static gboolean
plugin_unload(PurplePlugin *plugin)
{
#ifdef HAVE_GNUTLS
	if(purple_ssl_get_ops() == &ssl_ops) {
		purple_ssl_set_ops(NULL);
	}

	purple_certificate_unregister_scheme( &x509_gnutls );
#endif

	return TRUE;
}

static PurplePluginInfo info =
{
	PURPLE_PLUGIN_MAGIC,
	PURPLE_MAJOR_VERSION,
	PURPLE_MINOR_VERSION,
	PURPLE_PLUGIN_STANDARD,                             /**< type           */
	NULL,                                             /**< ui_requirement */
	PURPLE_PLUGIN_FLAG_INVISIBLE,                       /**< flags          */
	NULL,                                             /**< dependencies   */
	PURPLE_PRIORITY_DEFAULT,                            /**< priority       */

	SSL_GNUTLS_PLUGIN_ID,                             /**< id             */
	N_("GNUTLS"),                                     /**< name           */
	DISPLAY_VERSION,                                  /**< version        */
	                                                  /**  summary        */
	N_("Provides SSL support through GNUTLS."),
	                                                  /**  description    */
	N_("Provides SSL support through GNUTLS."),
	"Christian Hammond <chipx86@gnupdate.org>",
	PURPLE_WEBSITE,                                     /**< homepage       */

	plugin_load,                                      /**< load           */
	plugin_unload,                                    /**< unload         */
	NULL,                                             /**< destroy        */

	NULL,                                             /**< ui_info        */
	NULL,                                             /**< extra_info     */
	NULL,                                             /**< prefs_info     */
	NULL,                                             /**< actions        */

	/* padding */
	NULL,
	NULL,
	NULL,
	NULL
};

static void
init_plugin(PurplePlugin *plugin)
{
}

PURPLE_INIT_PLUGIN(ssl_gnutls, init_plugin, info)
