/*
** Copyright 2007-2019 S. Varshavchik.
** See COPYING for distribution information.
*/
#include	"config.h"
#include	"argparse.h"
#include	"spipe.h"
#include	"libcouriertls.h"
#include	"tlscache.h"
#include	"soxwrap/soxwrap.h"
#include	<gnutls/gnutls.h>
#ifndef HAVE_GNUTLS3
#include	<gnutls/extra.h>
#endif
#include	<gnutls/x509.h>
#include	<stdio.h>
#include	<string.h>
#include	<stdlib.h>
#include	<ctype.h>
#include	<netdb.h>
#include	<idn2.h>
#if HAVE_DIRENT_H
#include <dirent.h>
#define NAMLEN(dirent) strlen((dirent)->d_name)
#else
#define dirent direct
#define NAMLEN(dirent) (dirent)->d_namlen
#if HAVE_SYS_NDIR_H
#include <sys/ndir.h>
#endif
#if HAVE_SYS_DIR_H
#include <sys/dir.h>
#endif
#if HAVE_NDIR_H
#include <ndir.h>
#endif
#endif
#if	HAVE_UNISTD_H
#include	<unistd.h>
#endif
#if	HAVE_FCNTL_H
#include	<fcntl.h>
#endif
#include	<errno.h>
#if	HAVE_SYS_TYPES_H
#include	<sys/types.h>
#endif
#if	HAVE_SYS_STAT_H
#include	<sys/stat.h>
#endif
#include	<sys/socket.h>
#include	<arpa/inet.h>

#include	<sys/time.h>

struct oid_name {
	const char *oid;
	const char *name;
};

static struct oid_name oid_name_list[]={
	{"2.5.4.0","objectClass"},
	{"2.5.4.2","knowledgeInformation"},
	{"2.5.4.3","cn"},
	{"2.5.4.4","sn"},
	{"2.5.4.5","serialNumber"},
	{"2.5.4.6","c"},
	{"2.5.4.7","l"},
	{"2.5.4.8","st"},
	{"2.5.4.9","street"},
	{"2.5.4.10","o"},
	{"2.5.4.11","ou"},
	{"2.5.4.12","title"},
	{"2.5.4.13","description"},
	{"2.5.4.14","searchGuide"},
	{"2.5.4.15","businessCategory"},
	{"2.5.4.16","postalAddress"},
	{"2.5.4.17","postalCode"},
	{"2.5.4.18","postOfficeBox"},
	{"2.5.4.19","physicalDeliveryOfficeName"},
	{"2.5.4.20","telephoneNumber"},
	{"2.5.4.21","telexNumber"},
	{"2.5.4.22","teletexTerminalIdentifier"},
	{"2.5.4.23","facsimileTelephoneNumber"},
	{"2.5.4.24","x121Address"},
	{"2.5.4.25","internationaliSDNNumber"},
	{"2.5.4.26","registeredAddress"},
	{"2.5.4.27","destinationIndicator"},
	{"2.5.4.28","preferredDeliveryMethod"},
	{"2.5.4.29","presentationAddress"},
	{"2.5.4.30","supportedApplicationContext"},
	{"2.5.4.31","member"},
	{"2.5.4.32","owner"},
	{"2.5.4.33","roleOccupant"},
	{"2.5.4.35","userPassword"},
	{"2.5.4.36","userCertificate"},
	{"2.5.4.37","cACertificate"},
	{"2.5.4.38","authorityRevocationList"},
	{"2.5.4.39","certificateRevocationList"},
	{"2.5.4.40","crossCertificatePair"},
	{"2.5.4.41","name"},
	{"2.5.4.42","givenName"},
	{"2.5.4.43","initials"},
	{"2.5.4.44","generationQualifier"},
	{"2.5.4.45","x500UniqueIdentifier"},
	{"2.5.4.46","dnQualifier"},
	{"2.5.4.47","enhancedSearchGuide"},
	{"2.5.4.48","protocolInformation"},
	{"2.5.4.49","distinguishedName"},
	{"2.5.4.50","uniqueMember"},
	{"2.5.4.51","houseIdentifier"},
	{"2.5.4.52","supportedAlgorithms"},
	{"2.5.4.53","deltaRevocationList"},
	{"2.5.4.54","dmdName"},
	{"2.5.4.65","pseudonym"},
	{"0.9.2342.19200300.100.1.3","mail"},
	{"0.9.2342.19200300.100.1.25","dc"},
	{"0.9.2342.19200300.100.1.1","uid"},
	{"1.3.6.1.1.3.1","uidObject"},
	{"1.2.840.113549.1.9.1","emailaddress"},
};


struct ssl_context_t {
	int isserver;
	struct tls_info info_cpy;
	const char *priority_list;

	char *certfile;
	char *keyfile;
	char *dhfile;

	char *trustcerts;

	int verify_cert;
	int fail_if_no_cert;
};

struct ssl_handle_t {
	struct tls_info info_cpy;
	ssl_context ctx;
	gnutls_anon_client_credentials_t anonclientcred;
	gnutls_anon_server_credentials_t anonservercred;
	gnutls_certificate_credentials_t xcred;
	gnutls_dh_params_t dhparams;
	int dhparams_initialized;
	gnutls_session_t session;

	gnutls_x509_privkey_t x509_key;
};

static void nonsslerror(struct tls_info *info, const char *pfix)
{
        char errmsg[256];

        strcpy(errmsg, "couriertls: ");
        strncat(errmsg, pfix, 200);
        strcat(errmsg, ": ");
        strncat(errmsg, strerror(errno), 255 - strlen(errmsg));

        (*info->tls_err_msg)(errmsg, info->app_data);
}

#if HAVE_GNUTLS_ALPN

static void sslerror(ssl_context ctx, int errcode, const char *pfix)
{
	char errmsg[512];
	const char *err;

	strcat(strcpy(errmsg, pfix), ": ");

	err=gnutls_strerror(errcode);

	strncat(errmsg, err, sizeof(errmsg)-1-strlen(errmsg));
	errmsg[sizeof(errmsg)-1]=0;
	(*ctx->info_cpy.tls_err_msg)(errmsg, ctx->info_cpy.app_data);
}
#endif

static const char *safe_getenv(ssl_context context, const char *n,
			       const char *def)
{
	const char *v=(*context->info_cpy.getconfigvar)
		(n, context->info_cpy.app_data);

	if (!v)	v="";

	if (!*v)
		v=def;
	return (v);
}

static void log_2stderr( int level, const char *s)
{
	fprintf(stderr, "%s", s);
}

ssl_context tls_create(int isserver, const struct tls_info *info)
{
	static int first=1;

	ssl_context p=malloc(sizeof(struct ssl_context_t));
	char *certfile=NULL;
	char debug_flag;

	if (!p)
		return NULL;

	memset(p, 0, sizeof(*p));

	p->isserver=isserver;
	p->info_cpy=*info;
	p->info_cpy.certificate_verified=0;

	debug_flag=*safe_getenv(p, "TLS_DEBUG", "");

	if (first)
	{
		if (gnutls_check_version(LIBGNUTLS_VERSION) == NULL)
		{
			fprintf(stderr, "GnuTLS version mismatch\n");
			free(p);
			errno=EINVAL;
			return (NULL);
		}

		first=0;

		if (debug_flag)
		{
			gnutls_global_set_log_function(log_2stderr);
			gnutls_global_set_log_level(9);
		}

		if (gnutls_global_init() < 0)
		{
			fprintf(stderr, "gnutls_global_init() failed\n");
			free(p);
			errno=EINVAL;
			return (NULL);
		}

#ifndef HAVE_GNUTLS3
		if (gnutls_global_init_extra() < 0)
		{
			gnutls_global_deinit();
			fprintf(stderr, "gnutls_global_init() failed\n");
			free(p);
			errno=EINVAL;
			return (NULL);
		}
#endif
	}

	p->priority_list=safe_getenv(p, "TLS_PRIORITY",
				     "NORMAL:-CTYPE-OPENPGP");

	if ((certfile=strdup(safe_getenv(p, "TLS_CERTFILE", ""))) == NULL ||
	    (p->trustcerts=strdup(safe_getenv(p, "TLS_TRUSTCERTS", "")))
	    == NULL)
	{
		if (certfile)
			free(certfile);
		tls_destroy(p);
		return NULL;
	}

	if (*certfile)
	{
		p->certfile=certfile;
		certfile=NULL;
	}

	if (certfile)
		free(certfile);

	if ((certfile=strdup(safe_getenv(p, "TLS_DHPARAMS", ""))) != NULL &&
	    *certfile)
	{
		p->dhfile=certfile;
	}
	else
	{
		if (certfile)
			free(certfile);
	}

	if ((certfile=strdup(safe_getenv(p, "TLS_PRIVATE_KEYFILE", ""))) != NULL &&
	    *certfile)
	{
		p->keyfile=certfile;
	}
	else
	{
		if (certfile)
			free(certfile);
	}

	switch (*safe_getenv(p, "TLS_VERIFYPEER", "P")) {
	case 'n':
	case 'N':
		p->verify_cert=0;
		p->fail_if_no_cert=0;
		break;
	case 'p':
	case 'P':		/* PEER */
		p->verify_cert=1;
		p->fail_if_no_cert=0;
		break;
	case 'r':
	case 'R':		/* REQUIREPEER */
		p->verify_cert=1;
		p->fail_if_no_cert=1;
		break;
	}

	if (info->peer_verify_domain)
		p->verify_cert=p->fail_if_no_cert=1;

	{
		const char *filename=safe_getenv(p, "TLS_CACHEFILE", "");
		const char *cachesize=safe_getenv(p, "TLS_CACHESIZE", "");
		off_t cachesize_l;

		if (filename && *filename)
		{
			cachesize_l= cachesize ? (off_t)atol(cachesize):0;

			if (cachesize_l <= 0)
				cachesize_l=512L * 1024;
			if ((p->info_cpy.tlscache=tls_cache_open(filename,
								 cachesize_l))
			    == NULL)
			{
				nonsslerror(&p->info_cpy, filename);
				tls_destroy(p);
				return NULL;
			}
		}
	}

#if 0
	int session_timeout=atoi(safe_getenv(ctx, "TLS_TIMEOUT"));
#endif
	return p;
}

void tls_destroy(ssl_context p)
{
	if (p->certfile)
		free(p->certfile);
	if (p->keyfile)
		free(p->keyfile);
	if (p->dhfile)
		free(p->dhfile);
	if (p->trustcerts)
		free(p->trustcerts);

	if (p->info_cpy.tlscache)
		tls_cache_close(p->info_cpy.tlscache);
	free(p);
}

int tls_certificate_verified(ssl_handle ssl)
{
	return ssl->info_cpy.certificate_verified;
}

static int read_cert_dir(const char *cert_dir,
			 int (*cb_func)(const char *filename,
					struct stat *stat_buf,
					void *arg),
			 void *arg)
{
	DIR *dirp;
	struct dirent *de;
	int rc=0;

	if ((dirp=opendir(cert_dir)) == NULL)
		return 0;

	while ((de=readdir(dirp)) != NULL)
	{
		char *buf;
		struct stat stat_buf;

		if (de->d_name[0] == '.')
			continue;

		buf=malloc(strlen(cert_dir)+strlen(de->d_name)+2);

		if (!buf)
			continue;

		strcat(strcat(strcpy(buf, cert_dir), "/"), de->d_name);

		if (stat(buf, &stat_buf) < 0 || !S_ISREG(stat_buf.st_mode))
		{
			free(buf);
			continue;
		}

		rc=(*cb_func)(buf, &stat_buf, arg);
		free(buf);
		if (rc)
			break;
	}
	closedir(dirp);
	return rc;
}

static int cnt_cert_size(const char *filename,
			 struct stat *stat_buf,
			 void *arg)
{
	*(size_t *)arg += stat_buf->st_size;
	return 0;
}

struct cert_buf_ptr {
	char *ptr;
	size_t cnt;
};

static int save_cert_to_buf(const char *filename,
			    struct stat *stat_buf,
			    void *arg)
{
	struct cert_buf_ptr *p=(struct cert_buf_ptr *)arg;
	FILE *fp;

	if (p->cnt < stat_buf->st_size)
		return 1;

	fp=fopen(filename, "r");

	if (fp)
	{
		if (stat_buf->st_size &&
		    fread(p->ptr, stat_buf->st_size, 1, fp) != 1)
		{
			fclose(fp);
			return 1;
		}
		fclose(fp);
	}
	p->ptr += stat_buf->st_size;
	p->cnt -= stat_buf->st_size;
	return 0;
}


static int add_certificates(gnutls_certificate_credentials_t xcred,
			    const char *certfile)
{
	struct stat stat_buf;
	struct cert_buf_ptr ptr;
	gnutls_datum_t datum_ptr;

	if (!certfile || !*certfile || stat(certfile, &stat_buf) < 0)
		return 0;

	if (S_ISREG(stat_buf.st_mode))
	{
		return gnutls_certificate_set_x509_trust_file(xcred, certfile,
							      GNUTLS_X509_FMT_PEM);
	}

	if (!S_ISDIR(stat_buf.st_mode))
		return 0;

	ptr.cnt=0;

	if (read_cert_dir(certfile, cnt_cert_size, &ptr.cnt))
		return 0;

	datum_ptr.data=malloc(ptr.cnt+1);
	datum_ptr.size=ptr.cnt;

	if (!datum_ptr.data)
		return 0;

	ptr.ptr=(char *)datum_ptr.data;

	if (read_cert_dir(certfile, save_cert_to_buf, &ptr) ||
	    ptr.cnt)
	{
		free(datum_ptr.data);
		return 0;
	}
	*ptr.ptr=0;

	gnutls_certificate_set_x509_trust_mem(xcred, &datum_ptr,
					      GNUTLS_X509_FMT_PEM);
	free(datum_ptr.data);

	return 0;
}

static void tls_free_session_keys(ssl_handle ssl)
{
	if (ssl->x509_key)
		gnutls_x509_privkey_deinit(ssl->x509_key);

	ssl->x509_key=NULL;
}

static void tls_free_session(ssl_handle ssl)
{
	gnutls_deinit(ssl->session);
	gnutls_certificate_free_credentials(ssl->xcred);
	gnutls_anon_free_client_credentials(ssl->anonclientcred);
	gnutls_anon_free_server_credentials(ssl->anonservercred);
	gnutls_dh_params_deinit(ssl->dhparams);
	tls_free_session_keys(ssl);
	free(ssl);
}

static int chk_error(int rc, ssl_handle ssl, int fd, fd_set *r, fd_set *w,
		     int *result_rc)
{
	if (rc == GNUTLS_E_SUCCESS)
	{
		if (result_rc)
			*result_rc=0;
		return 0;
	}

	if (rc == GNUTLS_E_WARNING_ALERT_RECEIVED)
		return 1;

	if (rc == GNUTLS_E_FATAL_ALERT_RECEIVED)
	{
		const char *alert=
			gnutls_alert_get_name(gnutls_alert_get(ssl->session));
		(*ssl->info_cpy.tls_err_msg)(alert, ssl->info_cpy.app_data);

		if (result_rc)
			*result_rc= -1;
		return 0;
	}

	if (rc == GNUTLS_E_AGAIN || rc == GNUTLS_E_INTERRUPTED)
	{
		fd_set *p=gnutls_record_get_direction(ssl->session)
			? w:r;

		if (p)
			FD_SET(fd, p);

		if (result_rc)
			*result_rc=1;
		return 0;
	}

	if (result_rc)
	{
		const char *errmsg=gnutls_strerror(rc);

#ifdef GNUTLS_E_PREMATURE_TERMINATION
		if (rc == GNUTLS_E_PREMATURE_TERMINATION)
		{
			errmsg="DEBUG: Unexpected SSL connection shutdown.";
		}
#endif
		(*ssl->info_cpy.tls_err_msg)(errmsg,
					     ssl->info_cpy.app_data);
		*result_rc= -1;
	}
	return 0;
}

static int name_check(ssl_handle ssl,
		      gnutls_x509_crt_t cert)
{
	char *idn_domain;
	const char *p;
	int rc;

	if (idn2_to_unicode_8z8z(ssl->info_cpy.peer_verify_domain,
				 &idn_domain, 0) != IDNA_SUCCESS)
		idn_domain=0;

	p=idn_domain ? idn_domain:ssl->info_cpy.peer_verify_domain;

	rc=gnutls_x509_crt_check_hostname(cert, p);

	if (idn_domain)
	{
		free(idn_domain);

		/* Now try the ACE-encoded hostname for a filename. */

		if (rc == 0)
		{
			p=ssl->info_cpy.peer_verify_domain;
			rc=gnutls_x509_crt_check_hostname(cert, p);
		}
	}
	return rc;
}


static int verify_client(ssl_handle ssl, int fd)
{
	unsigned int status;
	int rc;
	const gnutls_datum_t *cert_list;
	unsigned int cert_list_size;

	if (!ssl->ctx->verify_cert)
		return 0;

	cert_list = gnutls_certificate_get_peers(ssl->session, &cert_list_size);
	if (cert_list == NULL || cert_list_size == 0)
	{
		if (ssl->ctx->fail_if_no_cert)
		{
			(*ssl->info_cpy.tls_err_msg)
				("No certificate supplied by peer",
				 ssl->info_cpy.app_data);
			return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
		}
		return 0;
	}

	status=0;
	rc=gnutls_certificate_verify_peers2(ssl->session, &status);

	if (rc)
	{
		(*ssl->info_cpy.tls_err_msg)
			("Peer certificate verification failed",
			 ssl->info_cpy.app_data);
		return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
	}

	if (status)
	{
		(*ssl->info_cpy.tls_err_msg)
			(status & GNUTLS_CERT_REVOKED ?
			 "Peer's certificate is revoked":
			 status & GNUTLS_CERT_SIGNER_NOT_FOUND ?
			 "Peer's certificate not signed by a trusted authority":
			 status & GNUTLS_CERT_SIGNER_NOT_CA ?
			 "Invalid peer certificate authority":
			 status & GNUTLS_CERT_INSECURE_ALGORITHM ?
			 "Peer's certificate does not use a secure checksum":
			 "Invalid peer certificate",
			 ssl->info_cpy.app_data);
		return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
	}

	if (gnutls_certificate_type_get(ssl->session) == GNUTLS_CRT_X509)
	{
		gnutls_x509_crt_t cert;

		if (gnutls_x509_crt_init(&cert) < 0)
		{
			(*ssl->info_cpy.tls_err_msg)
				("Error initializing certificate",
				 ssl->info_cpy.app_data);
			return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
		}

		if (gnutls_x509_crt_import(cert, &cert_list[0],
					   GNUTLS_X509_FMT_DER) < 0)
		{
			(*ssl->info_cpy.tls_err_msg)
				("Error parsing certificate",
				 ssl->info_cpy.app_data);
			gnutls_x509_crt_deinit (cert);
			return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
		}


		if (gnutls_x509_crt_get_expiration_time(cert) < time(NULL))
		{
			(*ssl->info_cpy.tls_err_msg)
				("Expired certificate",
				 ssl->info_cpy.app_data);
			gnutls_x509_crt_deinit (cert);
			return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
		}

		if (gnutls_x509_crt_get_activation_time(cert) > time(NULL))
		{
			(*ssl->info_cpy.tls_err_msg)
				("Certificate not activated",
				 ssl->info_cpy.app_data);
			gnutls_x509_crt_deinit (cert);
			return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
		}

		if (ssl->info_cpy.peer_verify_domain &&
		    *ssl->info_cpy.peer_verify_domain &&
		    !name_check(ssl, cert))
		{
			char hostname[256];
			size_t hostname_size=sizeof(hostname);
			const char *errmsg_txt="Certificate owner mismatch: ";
			char *errmsg_buf;

			if (gnutls_x509_crt_get_dn_by_oid(cert,
							  "2.5.4.3", 0,
							  0, hostname,
							  &hostname_size) < 0)
				strcpy(hostname,"(unknown)");

			errmsg_buf=malloc(strlen(errmsg_txt)+
					  strlen(hostname)+10);

			if (errmsg_buf)
				strcat(strcpy(errmsg_buf, errmsg_txt),
				       hostname);

			(*ssl->info_cpy.tls_err_msg)
				(errmsg_buf ? errmsg_buf: strerror(errno),
				 ssl->info_cpy.app_data);
			gnutls_x509_crt_deinit (cert);

			if (errmsg_buf)
				free(errmsg_buf);
			return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
		}

		gnutls_x509_crt_deinit (cert);
	}
	else
	{
		(*ssl->info_cpy.tls_err_msg)
			("No certificate supplied by peer",
			 ssl->info_cpy.app_data);
		return GNUTLS_E_INSUFFICIENT_CREDENTIALS;
	}

	ssl->info_cpy.certificate_verified=1;
	return 0;
}

#if HAVE_GNUTLS_ALPN
/*
** Parse the TLS_ALPN setting, repeatedly invoking a callback with:
** 1) size of the protocol string
** 2) the protocol string
** 3) pass-through parameter.
*/

static gnutls_alpn_flags_t alpn_parse(const char *alpn,
				      void (*cb)(unsigned char l,
						 const char *p,
						 void *arg),
				      void *arg)
{
	gnutls_alpn_flags_t flags=GNUTLS_ALPN_MANDATORY;

	while (alpn && *alpn)
	{
		const char *p;
		unsigned char l;

		/* Commas and spaces are delimiters here */

		if (*alpn==',' || isspace(*alpn))
		{
			++alpn;
			continue;
		}

		/* Look for the next comma, spaces, or end of string */
		p=alpn;

		while (*alpn && *alpn != ',' && !isspace(*alpn))
			++alpn;

		/*
		** We now have the character count and the string.
		** Defend against a bad setting by checking for overflow.
		*/
		l=alpn - p;

		if (l != alpn - p)
			continue;

		if (*p != '@')
			(*cb)(l, p, arg);
		else
		{
			if (l > 1)
				switch (p[1]) {
				case 'o':
				case 'O':
					flags &= GNUTLS_ALPN_MANDATORY;
					break;
				case 's':
				case 'S':
					flags |= GNUTLS_ALPN_SERVER_PRECEDENCE;
					break;
				}
		}
	}

	return flags;
}

static void alpn_count(unsigned char l,
		       const char *p,
		       void *arg)
{
	++*(unsigned *)arg;
}

static void alpn_save(unsigned char l,
		      const char *name,
		      void *arg)
{
	gnutls_datum_t **p=(gnutls_datum_t **)arg;

	(*p)->data=(unsigned char *)name;
	(*p)->size=l;
	++*p;
}

static int alpn_set(ssl_context ctx,
		    gnutls_session_t session)
{
	const char *p=safe_getenv(ctx, "TLS_ALPN", "");
	unsigned n=0;
	gnutls_datum_t *datums;
	gnutls_datum_t *datum_ptr;
	gnutls_alpn_flags_t flags;
	int ret;

	alpn_parse(p, alpn_count, &n);

	if (n == 0)
		return GNUTLS_E_SUCCESS;

	if ((datums=(gnutls_datum_t *)malloc(n * sizeof(gnutls_datum_t)))
	    == NULL)
	{
		return GNUTLS_E_SUCCESS;
	}

	datum_ptr=datums;

	flags=alpn_parse(p, alpn_save, &datum_ptr);

	ret=gnutls_alpn_set_protocols(session, datums, n, flags);
	free((char *)datums);

	return ret;
}

#if 0
static int verify_alpn(ssl_handle ssl)
{
	const char *p=safe_getenv(ssl->ctx, "TLS_ALPN", "");
	unsigned n=0;

	gnutls_datum_t protocol;
	int rc;

	alpn_parse(p, alpn_count, &n);

	rc=gnutls_alpn_get_selected_protocol(ssl->session, &protocol);
	if (rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
	{
		fprintf(stderr, "Not negotiated\n");

		return 0;
	}
	if (rc != GNUTLS_E_SUCCESS)
	{
		fprintf(stderr, "ERROR: %d %s\n",
			rc, gnutls_strerror(rc));
		return 0;
	}

	fprintf(stderr, "Negotiated ");
	fwrite(protocol.data, protocol.size, 1, stderr);
	fprintf(stderr, "\n");

	return 0;
}
#endif
#endif

static int dohandshake(ssl_handle ssl, int fd, fd_set *r, fd_set *w)
{
	int rc;

	while (chk_error(gnutls_handshake(ssl->session),
			 ssl, fd, r, w, &rc))
		;

	if (rc == 0)
	{
		ssl->info_cpy.connect_interrupted=0;

		if (verify_client(ssl, fd))
			return -1;

#if 0
		if (verify_alpn(ssl))
			return -1;
#endif
		if (ssl->info_cpy.connect_callback != NULL &&
		    !(*ssl->info_cpy.connect_callback)(ssl,
						       ssl->info_cpy.app_data))
			return (-1);
	}
	return rc;
}

static char *check_cert_unicode(const char *filename,
				gnutls_certificate_type_t cert_type,
				const char *req_dn,
				int isvirtual)
{
	if (!filename || !*filename)
		return NULL;

	while (*req_dn)
	{
		char *p=malloc(strlen(filename)+strlen(req_dn)+10);

		if (!p)
			return NULL;

		strcat(strcat(strcpy(p, filename), "."), req_dn);

		if (access(p, R_OK) == 0)
			return p;

		free(p);

		if (!isvirtual)
			break;

		if ((req_dn=strchr(req_dn, '.')) == NULL)
			break;
		++req_dn;
	}

	/*
	** We're called with a hostname first. Don't check the defualt
	** filename, we'll be called again with an IP address
	*/

	if (!isvirtual)
	{
		char *p=malloc(strlen(filename)+10);

		if (!p)
			return NULL;

		strcpy(p, filename);

		if (access(p, R_OK) == 0)
			return p;

		free(p);
	}
	return NULL;
}

static char *check_cert(const char *filename,
			gnutls_certificate_type_t cert_type,
			const char *req_dn,
			int isvirtual)
{
	if (isvirtual)
	{
		char *p;
		char *retfile;

		if (idn2_to_ascii_8z(req_dn, &p, 0) != IDNA_SUCCESS)
			p=0;

		if (p)
		{
			retfile=check_cert_unicode(filename, cert_type, p,
						    isvirtual);
			free(p);

			if (retfile)
				return retfile;
		}
	}

	return check_cert_unicode(filename, cert_type, req_dn, isvirtual);
}

static char *check_key(const char *filename,
			gnutls_certificate_type_t cert_type,
			const char *req_dn,
			int isvirtual)
{
	return check_cert(filename, cert_type, req_dn, isvirtual);
}

static int read_file(const char *file,
		     gnutls_datum_t *filebuf)
{
	FILE *fp;
	struct stat stat_buf;

	filebuf->data=NULL;

	if ((fp=fopen(file, "r")) == NULL ||
	    fstat(fileno(fp), &stat_buf) < 0)
	{
		if (fp)
			fclose(fp);
		return GNUTLS_E_FILE_ERROR;
	}

	if ((filebuf->data=malloc(stat_buf.st_size)) == NULL)
	{
		fclose(fp);
		return GNUTLS_E_MEMORY_ERROR;
	}

	if (fread(filebuf->data, filebuf->size=stat_buf.st_size, 1, fp) != 1)
	{
		if (fp)
			fclose(fp);
		return GNUTLS_E_FILE_ERROR;
	}
	return 0;
}

static void release_file(gnutls_datum_t *filebuf)
{
	if (filebuf->data)
		free(filebuf->data);
	filebuf->data=NULL;
}

static int set_cert(ssl_handle ssl,
		    gnutls_session_t session,
		    gnutls_retr2_st *st,
		    const char *certfilename,
		    const char *keyfilename)
{
	int rc;
	gnutls_datum_t filebuf;
	gnutls_datum_t keyfilebuf;
	unsigned int cert_cnt;

	st->ncerts=0;
	st->deinit_all=0;
	tls_free_session_keys(ssl);

	if ((rc=read_file(certfilename, &filebuf)) < 0)
		return rc;

	if (keyfilename)
	{
		if ((rc=read_file(keyfilename, &keyfilebuf)) < 0)
		{
			release_file(&filebuf);
			return rc;
		}
	}

	switch (st->cert_type) {
	case GNUTLS_CRT_X509:

		cert_cnt=0;

		if (keyfilename)
		{
			if ((rc=gnutls_x509_privkey_init(&ssl->x509_key)) < 0 ||
			    (rc=gnutls_x509_privkey_import(ssl->x509_key, &keyfilebuf,
							   GNUTLS_X509_FMT_PEM)) < 0)
				break;
		}
		else
		{
			if ((rc=gnutls_x509_privkey_init(&ssl->x509_key)) < 0 ||
			    (rc=gnutls_x509_privkey_import(ssl->x509_key, &filebuf,
							   GNUTLS_X509_FMT_PEM)) < 0)
				break;
		}

		rc=gnutls_x509_crt_list_import(NULL, &cert_cnt,
					       &filebuf,
					       GNUTLS_X509_FMT_PEM,
					       GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED);

		if (rc != GNUTLS_E_SHORT_MEMORY_BUFFER)
			break;

		st->ncerts=cert_cnt+1;
		st->cert.x509=gnutls_malloc(st->ncerts*sizeof(*st->cert.x509));

		rc=gnutls_x509_crt_list_import(st->cert.x509, &st->ncerts,
					       &filebuf,
					       GNUTLS_X509_FMT_PEM, 0);

		if (rc < 0)
		{
			st->ncerts=0;
			gnutls_free(st->cert.x509);
			st->cert.x509=0;
			break;
		}
		st->ncerts=rc;
		st->key.x509=ssl->x509_key;
		ssl->x509_key=0;
		st->deinit_all=1;

		break;
	default:
		break;
	}

	release_file(&filebuf);
	if (keyfilename)
		release_file(&keyfilebuf);
	return 0;
}

static int get_server_cert(gnutls_session_t session,
			   const gnutls_datum_t * req_ca_rdn, int nreqs,
			   const gnutls_pk_algorithm_t * sign_algos,
			   int sign_algos_length, gnutls_retr2_st *st)

{
	ssl_handle ssl=(ssl_handle)gnutls_session_get_ptr(session);
	int vhost_idx;
	char *vhost_buf;
	size_t vhost_max_size=0;
	size_t vhost_size;
	unsigned int type=GNUTLS_NAME_DNS;
	char *certfilename=NULL;
	char *keyfilename=NULL;
	int rc;

	st->cert_type=gnutls_certificate_type_get(session);

	for (vhost_idx=0; vhost_size=0,
		     gnutls_server_name_get(session, NULL, &vhost_size, &type,
					    vhost_idx) ==
		     GNUTLS_E_SHORT_MEMORY_BUFFER; ++vhost_idx)
	{
		if (++vhost_size > vhost_max_size)
			vhost_max_size=vhost_size;
	}

	vhost_buf=malloc(vhost_max_size);

	if (!vhost_buf)
		return GNUTLS_E_MEMORY_ERROR;

	for (vhost_idx=0; vhost_size=vhost_max_size,
		     gnutls_server_name_get(session, vhost_buf, &vhost_size,
					    &type,
					    vhost_idx) == GNUTLS_E_SUCCESS;
	     ++vhost_idx)
	{
		char *p;
		char *utf8;
		char *namebuf;

		/* Convert to UTF8 */
		if (idn2_to_unicode_8z8z(vhost_buf, &utf8, 0)
		    != IDNA_SUCCESS)
			utf8=0;

		namebuf=utf8 ? utf8:vhost_buf;

		for (p=namebuf; *p; p++)
			if (*p == '/')
				*p='.'; /* Script kiddie check */

		if (ssl->ctx->certfile)
			certfilename=check_cert(ssl->ctx->certfile,
						st->cert_type,
						namebuf, 1);

		if (ssl->ctx->keyfile)
			keyfilename=check_key(ssl->ctx->keyfile,
						st->cert_type,
						namebuf, 1);

		if (utf8)
			free(utf8);
		if (certfilename)
			break;
	}

	if (!certfilename)
	{
		const char *ip=
			safe_getenv(ssl->ctx,
				    "TCPLOCALIP", "");

		if (strncmp(ip, "::ffff:", 7) == 0 && strchr(ip, '.'))
			ip += 7;

		if (ssl->ctx->certfile)
			certfilename=check_cert(ssl->ctx->certfile,
						st->cert_type, ip, 0);
		if (ssl->ctx->keyfile)
			keyfilename=check_key(ssl->ctx->keyfile,
					      st->cert_type, ip, 0);
	}

	if (!certfilename)
	{
		if (ssl->ctx->certfile)
			nonsslerror(&ssl->info_cpy, ssl->ctx->certfile);
		free(vhost_buf);
		return 0;
	}

	rc=set_cert(ssl, session, st, certfilename, keyfilename);
	free(certfilename);
	if (keyfilename)
		free(keyfilename);
	free(vhost_buf);
	return rc;
}


static int pick_client_cert(gnutls_session_t session,
			    const gnutls_datum_t * req_ca_rdn, int nreqs,
			    const gnutls_pk_algorithm_t * sign_algos,
			    int sign_algos_length, gnutls_retr2_st *st)
{
	ssl_handle ssl=(ssl_handle)gnutls_session_get_ptr(session);
	int i, j;
	const char *cert_array;
	size_t cert_array_size;
	int rc=0;

	if (ssl->info_cpy.getpemclientcert4ca == NULL)
		return 0;

	if (st->cert_type != GNUTLS_CRT_X509)
		return 0;

	if (ssl->info_cpy.loadpemclientcert4ca)
		(*ssl->info_cpy.loadpemclientcert4ca)(ssl->info_cpy.app_data);

	for (j=0; (*ssl->info_cpy.getpemclientcert4ca)(j, &cert_array,
						       &cert_array_size,
						       ssl->info_cpy.app_data);
	     ++j)
	{
		gnutls_datum_t data;
		unsigned int cert_cnt=0;
		gnutls_x509_crt_t *certbuf;
		size_t issuer_buf_size=0;
		char *issuer_rdn;
		gnutls_x509_privkey_t pk;

		data.data=(unsigned char *)cert_array;
		data.size=cert_array_size;
		gnutls_x509_privkey_init(&pk);
		if (gnutls_x509_privkey_import(pk, &data,
					       GNUTLS_X509_FMT_PEM)
		    != GNUTLS_E_SUCCESS)
		{
			gnutls_x509_privkey_deinit(pk);
			continue;
		}

		data.data=(void *)cert_array;
		data.size=cert_array_size;;

		gnutls_x509_crt_list_import(NULL, &cert_cnt, &data,
					    GNUTLS_X509_FMT_PEM,
					    GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED);
		if (cert_cnt == 0)
		{
			gnutls_x509_privkey_deinit(pk);
			continue;
		}

		certbuf=gnutls_malloc(sizeof(*certbuf)*cert_cnt);

		if (!certbuf)
		{
			gnutls_x509_privkey_deinit(pk);
			continue;
		}

		if (gnutls_x509_crt_list_import(certbuf, &cert_cnt, &data,
						GNUTLS_X509_FMT_PEM,
						GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED) < 0)
		{
			free(certbuf);
			gnutls_x509_privkey_deinit(pk);
			continue;
		}


		gnutls_x509_crt_get_issuer_dn(certbuf[0], NULL,
					      &issuer_buf_size);

		++issuer_buf_size;

		issuer_rdn=gnutls_malloc(issuer_buf_size+1);

		if (gnutls_x509_crt_get_issuer_dn(certbuf[0], issuer_rdn,
						  &issuer_buf_size)
		    != GNUTLS_E_SUCCESS)
		{
			gnutls_free(issuer_rdn);
			issuer_rdn=0;
		}
		else
			issuer_rdn[issuer_buf_size]=0;

		for (i=0; issuer_rdn && i<nreqs; i++)
		{
			size_t buf_size=0;
			char *ca_rdn;

			gnutls_x509_rdn_get(&req_ca_rdn[i], NULL, &buf_size);

			++buf_size;

			ca_rdn=gnutls_malloc(buf_size+1);

			if (gnutls_x509_rdn_get(&req_ca_rdn[i], ca_rdn, &buf_size) !=
			    GNUTLS_E_SUCCESS)
			{
				gnutls_free(ca_rdn);
				continue;
			}

			ca_rdn[buf_size]=0;

			if (strcmp(ca_rdn, issuer_rdn) == 0)
				break;
			gnutls_free(ca_rdn);
		}

		st->ncerts=0;
		if (issuer_rdn && i < nreqs)
		{
			st->cert.x509=certbuf;
			st->ncerts=cert_cnt;
			st->deinit_all=1;
			st->key.x509=pk;
			cert_cnt=0;
			rc=1;
		}
		else
		{
			gnutls_x509_privkey_deinit(pk);
			while (cert_cnt)
				gnutls_x509_crt_deinit(certbuf[--cert_cnt]);
			gnutls_free(certbuf);
		}
		gnutls_free(issuer_rdn);
		if (rc)
			break;
	}

	return rc;
}

static int get_client_cert(gnutls_session_t session,
			   const gnutls_datum_t * req_ca_rdn, int nreqs,
			   const gnutls_pk_algorithm_t * sign_algos,
			   int sign_algos_length, gnutls_retr2_st *st)
{
	ssl_handle ssl=(ssl_handle)gnutls_session_get_ptr(session);
	int rc;
	char *certfilename=NULL;
	char *keyfilename=NULL;

	rc= 0;
	st->cert_type=gnutls_certificate_type_get(session);

	if (ssl->ctx->certfile)
		certfilename=check_cert(ssl->ctx->certfile,
					st->cert_type, "", 0);

	if (ssl->ctx->keyfile)
		keyfilename=check_key(ssl->ctx->keyfile,
					st->cert_type, "", 0);

	st->ncerts=0;
	st->deinit_all=0;

	if (certfilename)
	{
		rc=set_cert(ssl, session, st, certfilename, keyfilename);
		free(certfilename);
	}
	else
	{
		rc=pick_client_cert(session, req_ca_rdn, nreqs, sign_algos,
				    sign_algos_length, st);
		if (rc > 0)
			rc=0;
	}

	if (keyfilename)
		free(keyfilename);

	return rc;
}

static int read_dh_params(gnutls_dh_params_t dhparams,
			  const char *filename,
			  int *dhparams_initialized)
{
	int rc;
	gnutls_datum_t filebuf;

	if (*dhparams_initialized)
		return 0;

	if (!filename)
		return 0;

	rc=read_file(filename, &filebuf);

	if (rc == 0)
	{
		if (gnutls_dh_params_import_pkcs3(dhparams, &filebuf,
						  GNUTLS_X509_FMT_PEM) == 0)
			*dhparams_initialized=1;
		release_file(&filebuf);
	}
	return 0;
}

static int db_store_func(void *dummy, gnutls_datum_t key,
			 gnutls_datum_t data)
{
	char *p=malloc(key.size + data.size + sizeof(int));

	if (!p)
		return -1;

	memcpy(p, &key.size, sizeof(key.size));
	memcpy(p+sizeof(key.size), key.data, key.size);
	memcpy(p+sizeof(key.size)+key.size, data.data, data.size);

	tls_cache_add(((ssl_handle)dummy)->info_cpy.tlscache, p,
		      key.size + data.size + sizeof(int));
	free(p);
	return 0;
}

static int do_cache_remove(void *rec, size_t recsize, int *doupdate, void *arg)
{
	char *recptr=rec;
	gnutls_datum_t *key=(gnutls_datum_t *)arg;

	if (recsize >= key->size + sizeof(key->size))
	{
		gnutls_datum_t dummy;

		memcpy(&dummy.size, recptr, sizeof(dummy.size));

		if (dummy.size == key->size &&
		    memcmp(recptr + sizeof(key->size),
			   key->data, key->size) == 0)
		{
			dummy.size= -1;
			memcpy(recptr, &dummy.size, sizeof(dummy.size));
			*doupdate=1;
			return 1;
		}
	}
	return 0;
}

static int db_remove_func(void *dummy, gnutls_datum_t key)
{
	tls_cache_walk(((ssl_handle)dummy)->info_cpy.tlscache,
		       do_cache_remove, &key);
	return 0;
}

struct db_retrieve_s {
	gnutls_datum_t ret;
	gnutls_datum_t *key;
};

static int do_cache_retrieve(void *rec, size_t recsize, int *doupdate,
			     void *arg)
{
	char *recptr=rec;
	struct db_retrieve_s *ret=(struct db_retrieve_s *)arg;

	if (recsize >= ret->key->size + sizeof(ret->key->size))
	{
		gnutls_datum_t dummy;

		memcpy(&dummy.size, recptr, sizeof(dummy.size));

		if (dummy.size == ret->key->size &&
		    memcmp(recptr+sizeof(dummy.size),
			   ret->key->data,
			   ret->key->size) == 0)
		{
			ret->ret.size=recsize-sizeof(dummy.size)-ret->key->size;

			ret->ret.data=gnutls_malloc(ret->ret.size);

			if (ret->ret.data)
				memcpy(ret->ret.data,
				       (void *)(recptr+sizeof(dummy.size)
						+ret->key->size),
				       ret->ret.size);
			else
				ret->ret.size=0;

			return 1;
		}
	}
	return 0;
}

static gnutls_datum_t db_retrieve_func(void *dummy, gnutls_datum_t key)
{
	struct db_retrieve_s drs;

	drs.ret.data=NULL;
	drs.ret.size=0;
	drs.key= &key;

	tls_cache_walk(((ssl_handle)dummy)->info_cpy.tlscache,
		       do_cache_retrieve, &drs);
	return drs.ret;
}

static int name_set(ssl_handle ssl, ssl_context ctx)
{
	char *idn_domain;
	const char *p;
	int rc;

	if (idn2_to_unicode_8z8z(ctx->info_cpy.peer_verify_domain,
				 &idn_domain, 0) != IDNA_SUCCESS)
		idn_domain=0;

	p=idn_domain ? idn_domain:ctx->info_cpy.peer_verify_domain;

	rc=gnutls_server_name_set(ssl->session, GNUTLS_NAME_DNS,
				  p, strlen(p));

	if (idn_domain)
		free(idn_domain);
	return rc;
}

ssl_handle tls_connect(ssl_context ctx, int fd)
{
	ssl_handle ssl=malloc(sizeof(struct ssl_handle_t));

	if (!ssl)
		return NULL;

	memset(ssl, 0, sizeof(*ssl));

	ssl->info_cpy=ctx->info_cpy;
	ssl->ctx=ctx;

	if (ctx->info_cpy.peer_verify_domain && !*ctx->trustcerts)
	{
		errno=ENOENT;
		(*ctx->info_cpy.tls_err_msg)( "TLS_TRUSTCERTS not set",
					      ctx->info_cpy.app_data);
		free(ssl);
		return NULL;
	}

        if (fcntl(fd, F_SETFL, O_NONBLOCK))
        {
                nonsslerror(&ctx->info_cpy, "fcntl");
                return (NULL);
        }

#ifdef  SO_KEEPALIVE

        {
        int     dummy;

                dummy=1;

                if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE,
                        (const char *)&dummy, sizeof(dummy)) < 0)
                {
                        nonsslerror(&ctx->info_cpy, "setsockopt");
                        return (NULL);
                }
        }
#endif

#ifdef  SO_LINGER
        {
        struct linger l;

                l.l_onoff=0;
                l.l_linger=0;

                if (setsockopt(fd, SOL_SOCKET, SO_LINGER,
                        (const char *)&l, sizeof(l)) < 0)
                {
                        nonsslerror(&ctx->info_cpy, "setsockopt");
                        return (NULL);
                }
        }
#endif

	if (gnutls_anon_allocate_client_credentials(&ssl->anonclientcred) < 0)
	{
		free(ssl);
		return NULL;
	}

	if (gnutls_anon_allocate_server_credentials(&ssl->anonservercred) < 0)
	{
		gnutls_anon_free_client_credentials(ssl->anonclientcred);
		free(ssl);
		return NULL;
	}

	if (gnutls_certificate_allocate_credentials(&ssl->xcred) < 0)
	{
		gnutls_anon_free_server_credentials(ssl->anonservercred);
		gnutls_anon_free_client_credentials(ssl->anonclientcred);
		free(ssl);
		return NULL;
	}

	if (gnutls_dh_params_init(&ssl->dhparams) < 0)
	{
		gnutls_certificate_free_credentials(ssl->xcred);
		gnutls_anon_free_server_credentials(ssl->anonservercred);
		gnutls_anon_free_client_credentials(ssl->anonclientcred);
		free(ssl);
		return NULL;
	}

	if (gnutls_init (&ssl->session,
			 ctx->isserver ? GNUTLS_SERVER:GNUTLS_CLIENT) < 0)
	{
		gnutls_certificate_free_credentials(ssl->xcred);
		gnutls_anon_free_server_credentials(ssl->anonservercred);
		gnutls_anon_free_client_credentials(ssl->anonclientcred);
		free(ssl);
		return NULL;
	}

	{
		const char *p=getenv("TLS_MIN_DH_BITS");
		unsigned int n=atoi(p ? p:"0");

		if (n)
			gnutls_dh_set_prime_bits(ssl->session, n);
	}

	gnutls_session_set_ptr(ssl->session, ssl);

        gnutls_handshake_set_private_extensions(ssl->session, 1);
        gnutls_certificate_set_verify_flags(ssl->xcred,
                                            GNUTLS_VERIFY_ALLOW_X509_V1_CA_CRT |

                                            /*
                                            GNUTLS_VERIFY_DO_NOT_ALLOW_SAME |
                                            GNUTLS_VERIFY_ALLOW_ANY_X509_V1_CA_C
RT |

                                            GNUTLS_VERIFY_ALLOW_SIGN_RSA_MD2 |
                                            GNUTLS_VERIFY_ALLOW_SIGN_RSA_MD5 |*/
                                            0);

        gnutls_certificate_set_verify_limits(ssl->xcred, 16384, 10);
	ssl->dhparams_initialized=0;

	if (gnutls_priority_set_direct(ssl->session, ctx->priority_list,
				       NULL) < 0 ||
	    read_dh_params(ssl->dhparams, ctx->dhfile,
			   &ssl->dhparams_initialized) < 0 ||
	    read_dh_params(ssl->dhparams, ctx->certfile,
			   &ssl->dhparams_initialized) < 0 ||
	    add_certificates(ssl->xcred, ctx->trustcerts) < 0 ||
	    gnutls_credentials_set(ssl->session, GNUTLS_CRD_ANON,
				   ctx->isserver
				   ? (void *)ssl->anonservercred
				   : (void *)ssl->anonclientcred)
	    < 0 ||
	    gnutls_credentials_set(ssl->session, GNUTLS_CRD_CERTIFICATE,
				   ssl->xcred) < 0 ||

	    (ctx->info_cpy.peer_verify_domain &&
	     name_set(ssl, ctx) < 0)
	    )
	{
		tls_free_session(ssl);
		return NULL;
	}

#if HAVE_GNUTLS_ALPN
	{
		int result_rc;

		result_rc=alpn_set(ctx, ssl->session);

		if (result_rc != GNUTLS_E_SUCCESS)
		{
			sslerror(ctx, result_rc, "gnutls_alpn_set_protocols");
			tls_free_session(ssl);
			return (NULL);
		}
	}
#endif
	if (ssl->dhparams_initialized)
	{
		gnutls_certificate_set_dh_params(ssl->xcred, ssl->dhparams);

		gnutls_anon_set_server_dh_params(ssl->anonservercred,
						 ssl->dhparams);
	}

	if (ctx->isserver)
	{
		if (ctx->verify_cert)
			gnutls_certificate_server_set_request(ssl->session,
							      ctx->fail_if_no_cert ?
							      GNUTLS_CERT_REQUIRE:
							      GNUTLS_CERT_REQUEST);
		gnutls_certificate_set_retrieve_function(ssl->xcred,
							 get_server_cert);
	}
	else gnutls_certificate_set_retrieve_function(ssl->xcred,
						      get_client_cert);

	gnutls_transport_set_ptr(ssl->session,(gnutls_transport_ptr_t)
				 GNUTLS_CAST_PTR_T fd);

	if (ssl->ctx->info_cpy.tlscache)
	{
		gnutls_db_set_ptr(ssl->session, ssl);

		gnutls_db_set_cache_expiration(ssl->session, 3600);

		gnutls_db_set_remove_function(ssl->session,
					      db_remove_func);
		gnutls_db_set_retrieve_function(ssl->session,
						db_retrieve_func);
		gnutls_db_set_store_function(ssl->session,
					     db_store_func);
	}

	ssl->info_cpy.connect_interrupted=1;

	if (dohandshake(ssl, fd, NULL, NULL) < 0)
	{
		tls_disconnect(ssl, fd);
		return NULL;
	}

	return ssl;
}

void tls_disconnect(ssl_handle ssl, int fd)
{
	fcntl(fd, F_SETFL, 0);
	gnutls_bye(ssl->session, GNUTLS_SHUT_RDWR);
	tls_free_session(ssl);
}

int	tls_transfer(struct tls_transfer_info *t, ssl_handle ssl, int fd,
		     fd_set *r, fd_set *w)
{
	if (ssl->info_cpy.connect_interrupted)
	{
		if (dohandshake(ssl, fd, r, w) < 0)
			return -1;

		return 0;
	}

	if (t->shutdown)
		return -1;

	if (t->shutdown_interrupted)
	{
		while (chk_error(gnutls_bye(ssl->session, GNUTLS_SHUT_RDWR),
				 ssl, fd, r, w, NULL))
			;

		if ((r && FD_ISSET(fd, r)) ||
		    (w && FD_ISSET(fd, w)))
		{
			return 1;
		}


		t->shutdown_interrupted=0;
		t->shutdown= -1;
		return -1;
	}

	if(0) printf("readleft=%d writeleft=%d\nread_interrupted=%d read_interrupted=%d\n",
		    (int)t->readleft,(int)t->writeleft,
		    t->read_interrupted,t->write_interrupted);

	if (!t->write_interrupted && t->readleft > 0 && t->writeleft == 0)
	{
		int rc;
		ssize_t n;

		do
		{
			n=gnutls_record_recv(ssl->session, t->readptr,
					     t->readleft);

			if (n >= 0)
			{
				if (n == 0)
				{
					t->shutdown=1;
					return -1;
				}

				t->readptr += n;
				t->readleft -= n;
				return 0;
			}

			if ((int)n == GNUTLS_E_REHANDSHAKE)
			{
				ssl->info_cpy.connect_interrupted=1;

				return tls_transfer(t, ssl, fd, r, w);
			}

		} while (chk_error((int)n, ssl, fd, r, w, &rc));

		if (rc < 0)
		{
			t->shutdown_interrupted=1;
			return tls_transfer(t, ssl, fd, r, w);
		}
	} else if (t->writeleft > 0)
	{
		int rc;
		ssize_t n;

		t->write_interrupted=0;

		do
		{
			n=gnutls_record_send(ssl->session, (void *)t->writeptr,
					     t->writeleft);

			if (n >= 0)
			{
				if (n == 0)
				{
					t->shutdown=1;
					return -1;
				}

				t->writeptr += n;
				t->writeleft -= n;
				return 0;
			}

			if ((int)n == GNUTLS_E_REHANDSHAKE)
			{
				t->write_interrupted=1;
				ssl->info_cpy.connect_interrupted=1;

				return tls_transfer(t, ssl, fd, r, w);
			}

		} while (chk_error((int)n, ssl, fd, r, w, &rc));

		if (rc < 0)
		{
			t->shutdown=1;
			return -1;
		}
		t->write_interrupted=1;
	}
	else
	{
		FD_SET(fd, r);
		FD_SET(fd, w);
	}
	return (1);
}

int tls_connecting(ssl_handle ssl)
{
	return ssl->info_cpy.connect_interrupted;
}

static const char *dump_dn(gnutls_x509_crt_t cert,
			   int (*get_dn_func)(gnutls_x509_crt_t cert,
					      unsigned indx,
					      void *oid, size_t * sizeof_oid),
			   int (*get_dnval_func)(gnutls_x509_crt_t cert,
						 const char *oid,
						 unsigned indx,
						 unsigned int raw_flag,
						 void *buf, size_t *sizeof_buf),
			   void (*dump_func)(const char *, int cnt, void *),
			   void *dump_arg)
{
	int idx;
	size_t bufsiz;
	size_t maxnamesize;
	size_t maxvalsize;
	char *oidname;
	char *oidval;
	int oidcnt;

	maxnamesize=0;
	maxvalsize=0;

	oidcnt=0;

	while (bufsiz=0, (*get_dn_func)(cert, oidcnt, NULL, &bufsiz)
	       == GNUTLS_E_SHORT_MEMORY_BUFFER)
	{
		if (bufsiz > maxnamesize)
			maxnamesize=bufsiz;
		++oidcnt;
	}

	oidname=malloc(maxnamesize);

	if (!oidname)
		return strerror(errno);

	for (idx=0; idx<oidcnt; ++idx)
	{
		int vidx;
		int rc;

		bufsiz=maxnamesize;

		if ((rc=(*get_dn_func)(cert, idx, oidname, &bufsiz)) < 0)
		{
			free(oidname);
			return gnutls_strerror(rc);
		}

		vidx=0;

		while (bufsiz=0,
		       (*get_dnval_func)(cert, oidname, vidx, 0,
					 NULL, &bufsiz)
		       == GNUTLS_E_SHORT_MEMORY_BUFFER)
		{
			if (bufsiz > maxvalsize)
				maxvalsize=bufsiz;
			++vidx;
		}
	}

	oidval=malloc(maxvalsize);

	if (!oidval)
	{
		free(oidname);
		return strerror(errno);
	}

	for (idx=0; idx<oidcnt; ++idx)
	{
		int vidx;
		int rc;
		size_t i;
		const char *oidname_str;

		bufsiz=maxnamesize;

		if ((rc=(*get_dn_func)(cert, idx, oidname, &bufsiz)) < 0)
		{
			free(oidval);
			free(oidname);
			return gnutls_strerror(rc);
		}

		oidname_str=oidname;

		for (i=0; i<sizeof(oid_name_list)/sizeof(oid_name_list[0]);
		     ++i)
		{
			if (strcmp(oid_name_list[i].oid, oidname) == 0)
			{
				oidname_str=oid_name_list[i].name;
				break;
			}
		}

		vidx=0;

		while (bufsiz=maxvalsize,
		       (*get_dnval_func)(cert, oidname, vidx, 0,
					 oidval, &bufsiz) >= 0)
		{
			(*dump_func)("   ", -1, dump_arg);
			(*dump_func)(oidname_str, -1, dump_arg);
			(*dump_func)("=", -1, dump_arg);
			(*dump_func)(oidval, -1, dump_arg);
			(*dump_func)("\n", -1, dump_arg);
			++vidx;
		}
	}

	free(oidval);
	free(oidname);
	return NULL;
}

static void print_time(const char *name, time_t t,
		       void (*dump_func)(const char *, int cnt, void *),
		       void *dump_arg)
{
	struct tm *tmptr=gmtime(&t);
	char buf[256];

	strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", tmptr);

	(*dump_func)(name, -1, dump_arg);
	(*dump_func)(": ", 2, dump_arg);
	(*dump_func)(buf, -1, dump_arg);
	(*dump_func)("\n", 1, dump_arg);
}

static void tls_dump_connection_info_x509(ssl_handle ssl,
					  int server,
					  void (*dump_func)(const char *,
							    int cnt, void *),
					  void *dump_arg);

static void dump_cipher_name(gnutls_session_t session,
			     void (*dump_func)(const char *,
					       int cnt, void *),
			     void *dump_arg);

void tls_dump_connection_info(ssl_handle ssl,
			      int server,
			      void (*dump_func)(const char *, int cnt, void *),
			      void *dump_arg)
{
	if (gnutls_certificate_type_get (ssl->session) == GNUTLS_CRT_X509)
		tls_dump_connection_info_x509(ssl, server, dump_func,
					      dump_arg);

	(*dump_func)("Version: ", -1, dump_arg);
	(*dump_func)
		(gnutls_protocol_get_name(gnutls_protocol_get_version(ssl->session)),
		 -1, dump_arg);
	(*dump_func)("\n", 1, dump_arg);

	{
		char buf[20];

		(*dump_func)("Bits: ", -1, dump_arg);

		snprintf(buf, sizeof(buf), "%d", (int)
			 gnutls_cipher_get_key_size(gnutls_cipher_get(ssl->session))
			 *8);
		buf[sizeof(buf)-1]=0;

		(*dump_func)(buf, -1, dump_arg);
		(*dump_func)("\n", 1, dump_arg);
	}

	(*dump_func)("Cipher: ", -1, dump_arg);
	dump_cipher_name(ssl->session, dump_func, dump_arg);
	(*dump_func)("\n", 1, dump_arg);
}

static void dump_cipher_name(gnutls_session_t session,
			     void (*dump_func)(const char *,
					       int cnt, void *),
			     void *dump_arg)
{
	gnutls_kx_algorithm_t kx_algo;
	gnutls_cipher_algorithm_t cipher_algo;
	gnutls_mac_algorithm_t mac_algo;
	const char *cipher_name;

	kx_algo=gnutls_kx_get(session);
	cipher_algo=gnutls_cipher_get(session);
	mac_algo=gnutls_mac_get(session);
	cipher_name=gnutls_cipher_suite_get_name(kx_algo, cipher_algo,
						 mac_algo);

	if (cipher_name)
		(*dump_func)(cipher_name, -1, dump_arg);
	else
	{
		(*dump_func)(gnutls_kx_get_name(kx_algo), -1, dump_arg);

		(*dump_func)("-", 1, dump_arg);
		(*dump_func)(gnutls_certificate_type_get_name(gnutls_certificate_type_get(session)),
			     -1, dump_arg);

		(*dump_func)("-", 1, dump_arg);
		(*dump_func)(gnutls_cipher_get_name(cipher_algo), -1,
			     dump_arg);

		(*dump_func)("-", 1, dump_arg);
		(*dump_func)(gnutls_mac_get_name(gnutls_mac_get(session)),
			     -1, dump_arg);
	}
}

static void tls_dump_connection_info_x509(ssl_handle ssl,
					  int server,
					  void (*dump_func)(const char *,
							    int cnt, void *),
					  void *dump_arg)
{
	const gnutls_datum_t *cert_list;
	unsigned int cert_list_size;
	gnutls_x509_crt_t *cert;

	cert_list=gnutls_certificate_get_peers(ssl->session, &cert_list_size);

	if (cert_list)
	{
		unsigned int i;

		cert=malloc(sizeof (*cert) * cert_list_size);

		for (i = 0; i<cert_list_size; i++)
		{
			gnutls_x509_crt_init(&cert[i]);
			gnutls_x509_crt_import(cert[i],
					       &cert_list[i],
					       GNUTLS_X509_FMT_DER);
		}

		for (i = 0; i < cert_list_size; i++)
		{
			time_t notbefore;
			time_t notafter;
			char buffer[256];
			unsigned j;

			(*dump_func)("Subject:\n", -1, dump_arg);

			dump_dn(cert[i],
				gnutls_x509_crt_get_dn_oid,
				gnutls_x509_crt_get_dn_by_oid,
				dump_func, dump_arg);
			(*dump_func)("\n", 1, dump_arg);

			for (j=0; ; ++j)
			{
				size_t s=sizeof(buffer);
				enum gnutls_x509_subject_alt_name_t t;

				t=gnutls_x509_crt_get_subject_alt_name
					(cert[i], j, buffer, &s, 0);

				if (t == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE)
					break;

				if (t != GNUTLS_SAN_DNSNAME)
					continue;

				if (s == sizeof(buffer))
					--s; /* The API is not clear */

				buffer[s]=0;
				(*dump_func)("Subject-Alt-Name-DNS: ", -1,
					     dump_arg);
				(*dump_func)(buffer, -1, dump_arg);
				(*dump_func)("\n", -1, dump_arg);
			}

#if 0
			(*dump_func)("Issuer:\n", -1, dump_arg);

			dump_dn(cert[i],
				gnutls_x509_crt_get_issuer_dn_oid,
				gnutls_x509_crt_get_issuer_dn_by_oid,
				dump_func, dump_arg);
			(*dump_func)("\n", 1, dump_arg);
#endif

			notbefore=gnutls_x509_crt_get_activation_time(cert[i]);
			notafter=gnutls_x509_crt_get_expiration_time(cert[i]);
			print_time("Not-Before", notbefore,
				   dump_func, dump_arg);
			print_time("Not-After",  notafter,
				   dump_func, dump_arg);
		}

		for (i = 0; i < cert_list_size; i++)
			gnutls_x509_crt_deinit(cert[i]);
		free(cert);
	}
}

static void gen_encryption_desc(gnutls_session_t session,
				void (*dump_func)(const char *,
						  int cnt, void *),
				void *dump_arg);

static void cnt_desc_size(const char *str, int s, void *ptr)
{
	if (s < 0)
		s=strlen(str);

	*(size_t *)ptr += s;
}

static void save_desc(const char *str, int s, void *ptr)
{
	if (s < 0)
		s=strlen(str);

	memcpy(*(char **)ptr, str, s);
	*(char **)ptr += s;
}

char *tls_get_encryption_desc(ssl_handle ssl)
{
	size_t n=1;
	char *buf;

	gen_encryption_desc(ssl->session, cnt_desc_size, &n);

	buf=malloc(n);

	if (buf)
	{
		char *ptr=buf;
		gen_encryption_desc(ssl->session, save_desc, &ptr);
		*ptr=0;
	}
	return buf;
}

static void gen_encryption_desc(gnutls_session_t session,
				void (*dump_func)(const char *,
						  int cnt, void *),
				void *dump_arg)
{
	char buf[20];

	(*dump_func)(gnutls_protocol_get_name(gnutls_protocol_get_version(session)),
		     -1, dump_arg);
	(*dump_func)(",", 1, dump_arg);
	snprintf(buf, sizeof(buf), "%d",
		 (int)gnutls_cipher_get_key_size(gnutls_cipher_get(session))
		 *8);
	buf[sizeof(buf)-1]=0;
	(*dump_func)(buf, -1, dump_arg);
	(*dump_func)("bits,", -1, dump_arg);
	dump_cipher_name(session, dump_func, dump_arg);
}


/* ------------------- */

int tls_validate_pem_cert(const char *buf, size_t buf_size)
{
	gnutls_datum_t dat;
	unsigned int cert_cnt=0;
 	gnutls_x509_crt_t *certbuf;

	dat.data=(void *)buf;
	dat.size=buf_size;

	gnutls_x509_crt_list_import(NULL, &cert_cnt, &dat,
				    GNUTLS_X509_FMT_PEM,
				    GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED);

	if (cert_cnt == 0)
		return 0;
	certbuf=malloc(sizeof(*certbuf)*cert_cnt);

	if (!certbuf)
		return 0;

	if (gnutls_x509_crt_list_import(certbuf, &cert_cnt, &dat,
					GNUTLS_X509_FMT_PEM,
					GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED) < 0)
		return 0;

	while (cert_cnt)
		gnutls_x509_crt_deinit(certbuf[--cert_cnt]);
	free(certbuf);
	return (1);
}

char *tls_cert_name(const char *buf, size_t buf_size)
{
	gnutls_datum_t dat;
	unsigned int cert_cnt=0;
 	gnutls_x509_crt_t *certbuf;
	char *p=0;
	size_t p_size;


	dat.data=(void *)buf;
	dat.size=buf_size;

	gnutls_x509_crt_list_import(NULL, &cert_cnt, &dat,
				    GNUTLS_X509_FMT_PEM,
				    GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED);

	if (cert_cnt == 0)
		return 0;
	certbuf=malloc(sizeof(*certbuf)*cert_cnt);

	if (!certbuf)
		return 0;

	if (gnutls_x509_crt_list_import(certbuf, &cert_cnt, &dat,
					GNUTLS_X509_FMT_PEM,
					GNUTLS_X509_CRT_LIST_IMPORT_FAIL_IF_EXCEED) < 0)
		return 0;

	p_size=0;
	gnutls_x509_crt_get_dn(certbuf[0], NULL, &p_size);
	++p_size;
	p=malloc(p_size+1);

	if (p)
	{
		if (gnutls_x509_crt_get_dn(certbuf[0], p, &p_size)
		    != GNUTLS_E_SUCCESS)
		{
			free(p);
			p=0;
		}
		else p[p_size]=0;
	}

	while (cert_cnt)
		gnutls_x509_crt_deinit(certbuf[--cert_cnt]);
	free(certbuf);
	return p;
}
