#include "plugin.h"
#include "netinc.h"

static plugfsfuncs_t *fsfuncs;
static plugnetfuncs_t *netfuncs;

static cvar_t *pdtls_psk_hint, *pdtls_psk_user, *pdtls_psk_key;

#undef SHA1
#undef HMAC
#include "openssl/bio.h"
#include "openssl/ssl.h"
#include "openssl/err.h"
#include "openssl/conf.h"

#define assert(c) do{if (!(c)) Con_Printf("assert failed: "STRINGIFY(c)"\n");}while(0)

static qboolean OSSL_Init(void);
static int ossl_fte_certctx;
struct fte_certctx_s
{
	const char *peername;
	qboolean dtls;

	hashfunc_t *hash;	//if set peer's cert MUST match the specified digest (with this hash function)
	qbyte digest[DIGEST_MAXSIZE];
};

static struct
{
	X509 *servercert;
	EVP_PKEY *privatekey;
} vhost;

static BIO_METHOD *biometh_vfs;

static int OSSL_Bio_FWrite(BIO *h, const char *buf, int size)
{
	vfsfile_t *f = BIO_get_data(h);
	int r = VFS_WRITE(f, buf, size);
	BIO_clear_retry_flags(h);
	if (r == 0)
	{
		BIO_set_retry_write(h);
		r = -1; //paranoia
	}
	return r;
}
static int OSSL_Bio_FRead(BIO *h, char *buf, int size)
{
	vfsfile_t *f = BIO_get_data(h);
	int r = VFS_READ(f, buf, size);
	BIO_clear_retry_flags(h);
	if (r == 0)	//no data yet.
	{
		BIO_set_retry_read(h);
		r = -1;	//shouldn't be needed, but I'm paranoid
	}
	return r;
}
static int OSSL_Bio_FPuts(BIO *h, const char *buf)
{
	return OSSL_Bio_FWrite(h, buf, strlen(buf));
}
static long OSSL_Bio_FCtrl(BIO *h, int cmd, long arg1, void *arg2)
{
	vfsfile_t *f = BIO_get_data(h);
	switch(cmd)
	{
	case BIO_CTRL_FLUSH:
		VFS_FLUSH(f);
		return 1;
	default:
		Con_Printf("OSSL_Bio_FCtrl: unknown cmd %i\n", cmd);
	case BIO_CTRL_PUSH:
	case BIO_CTRL_POP:
		return 0;
	}
	return 0;	//failure
}
static long OSSL_Bio_FOtherCtrl(BIO *h, int cmd, BIO_info_cb *cb)
{
	switch(cmd)
	{
	default:
		Con_Printf("OSSL_Bio_FOtherCtrl unknown cmd %i\n", cmd);
		return 0;
	}
	return 0;	//failure
}
static int OSSL_Bio_FCreate(BIO *h)
{	//we'll have to fill this in after we create the bio.
	BIO_set_data(h, NULL);
	return 1;
}
static int OSSL_Bio_FDestroy(BIO *h)
{
	vfsfile_t *f = BIO_get_data(h);
	VFS_CLOSE(f);
	BIO_set_data(h, NULL);
	return 1;
}

static int OSSL_PrintError_CB (const char *str, size_t len, void *u)
{
	Con_Printf("%s\n", str);
	return 1;
}

typedef struct
{
	vfsfile_t funcs;
	struct fte_certctx_s cert;
	SSL_CTX *ctx;
	BIO *bio;
	SSL *ssl;
} osslvfs_t;
static int QDECL OSSL_FRead (struct vfsfile_s *file, void *buffer, int bytestoread)
{
	osslvfs_t *o = (osslvfs_t*)file;
	int r = BIO_read(o->bio, buffer, bytestoread);
	if (r <= 0)
	{
		if (BIO_should_io_special(o->bio))
		{
			switch(BIO_get_retry_reason(o->bio))
			{
			//these are temporary errors, try again later.
			case BIO_RR_SSL_X509_LOOKUP:
				return -1;	//certificate failure.
			case BIO_RR_ACCEPT:
			case BIO_RR_CONNECT:
				return -1;	//should never happen
			}
		}
		if (BIO_should_retry(o->bio))
			return 0;
		return -1;	//eof or something
	}
	return r;
}
static int QDECL OSSL_FWrite (struct vfsfile_s *file, const void *buffer, int bytestowrite)
{
	osslvfs_t *o = (osslvfs_t*)file;
	int r = BIO_write(o->bio, buffer, bytestowrite);
	if (r <= 0)
	{
		if (BIO_should_io_special(o->bio))
		{
			switch(BIO_get_retry_reason(o->bio))
			{
			//these are temporary errors, try again later.
			case BIO_RR_SSL_X509_LOOKUP:
				return -1;	//certificate failure.
			case BIO_RR_ACCEPT:
			case BIO_RR_CONNECT:
				return -1;	//should never happen
			}
		}
		if (BIO_should_retry(o->bio))
			return 0;
		return -1;	//eof or something
	}
	return r;
}
//static qboolean QDECL OSSL_Seek (struct vfsfile_s *file, qofs_t pos);	//returns false for error
//static qofs_t QDECL OSSL_Tell (struct vfsfile_s *file);
//static qofs_t QDECL OSSL_GetLen (struct vfsfile_s *file);	//could give some lag
static qboolean QDECL OSSL_Close (struct vfsfile_s *file)
{
	osslvfs_t *o = (osslvfs_t*)file;
	BIO_free(o->bio);
	SSL_CTX_free(o->ctx);
	free(o);
	return true;	//success, I guess
}
//static void QDECL OSSL_Flush (struct vfsfile_s *file);



static qboolean print_cn_name(X509_NAME* const name, const char *utf8match, const char *prefix)
{
    int idx = -1;
	qboolean success = 0;
    unsigned char *utf8 = NULL;
	X509_NAME_ENTRY* entry;
	ASN1_STRING* data;
	int length;

    do
    {
        if(!name) break; /* failed */

        idx = X509_NAME_get_index_by_NID(name, NID_commonName, -1);
        if(!(idx > -1))  break; /* failed */

        entry = X509_NAME_get_entry(name, idx);
        if(!entry) break; /* failed */

        data = X509_NAME_ENTRY_get_data(entry);
        if(!data) break; /* failed */

        length = ASN1_STRING_to_UTF8(&utf8, data);
        if(!utf8 || !(length > 0))  break; /* failed */

		if (utf8match)
			success = !strcmp(utf8, utf8match);
		else
		{
			Con_Printf("%s%s", prefix, utf8);
			success = 1;
		}

    } while (0);

    if(utf8)
        OPENSSL_free(utf8);

	return success;
}

static int OSSL_Verify_Peer(int preverify_ok, X509_STORE_CTX *x509_ctx)
{
	SSL *ssl = X509_STORE_CTX_get_ex_data(x509_ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
	struct fte_certctx_s *uctx = SSL_get_ex_data(ssl, ossl_fte_certctx);

	if (uctx->hash)
	{	//our special 'must-match-digest' mode without any other kind of trust.
		X509* cert = X509_STORE_CTX_get_current_cert(x509_ctx);
		size_t blobsize;
		qbyte *blob;
		qbyte *end;
		qbyte digest[DIGEST_MAXSIZE];
		void *hctx = alloca(uctx->hash->contextsize);
		blobsize = i2d_X509(cert, NULL);
		blob = alloca(blobsize);
		end = blob;
		i2d_X509(cert, &end);

		uctx->hash->init(hctx);
		uctx->hash->process(hctx, blob, blobsize);
		uctx->hash->terminate(digest, hctx);

		//return 1 for success
		return !memcmp(digest, uctx->digest, uctx->hash->digestsize);
	}

	if(preverify_ok == 0)
	{
		int depth = X509_STORE_CTX_get_error_depth(x509_ctx);
		int err = X509_STORE_CTX_get_error(x509_ctx);

		X509* cert = X509_STORE_CTX_get_current_cert(x509_ctx);
		X509_NAME* iname = cert ? X509_get_issuer_name(cert) : NULL;
		//X509_NAME* sname = cert ? X509_get_subject_name(cert) : NULL;

		if (err == X509_V_ERR_CERT_HAS_EXPIRED || err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)
		{
			size_t knownsize;
			qbyte *knowndata = netfuncs->TLS_GetKnownCertificate(uctx->peername, &knownsize);
			if (knowndata)
			{	//check
				size_t blobsize;
				qbyte *blob;
				qbyte *end;
				blobsize = i2d_X509(cert, NULL);
				if (blobsize == knownsize)
				{	//sizes must match.
					blob = alloca(blobsize);
					end = blob;
					i2d_X509(cert, &end);
					if (!memcmp(blob, knowndata, blobsize))
						preverify_ok = 1;	//exact match to a known cert. yay. allow it.
				}
				plugfuncs->Free(knowndata);
				return preverify_ok;
			}

#ifdef HAVE_CLIENT
			if (uctx->dtls)
			{
				unsigned int probs = 0;
				size_t blobsize;
				qbyte *blob;
				qbyte *end;
				blobsize = i2d_X509(cert, NULL);
				blob = alloca(blobsize);
				end = blob;
				i2d_X509(cert, &end);

				switch(err)
				{
				case 0:
					probs |= CERTLOG_WRONGHOST;
					break;
				case X509_V_ERR_CERT_HAS_EXPIRED:
					probs |= CERTLOG_EXPIRED;
					break;
				case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
					probs |= CERTLOG_MISSINGCA;
					break;
				default:
					probs |= CERTLOG_UNKNOWN;
					break;
				}
				if (netfuncs->CertLog_ConnectOkay && netfuncs->CertLog_ConnectOkay(uctx->peername, blob, blobsize, probs))
					return 1; //ignore the errors...
				return 0;	//allow it.
			}
#endif
		}

		Con_Printf(CON_ERROR"%s ", uctx->peername);
		//FIXME: this is probably on a worker thread. expect munged prints.
		//print_cn_name(sname, NULL, CON_ERROR);
		Con_Printf(" (issued by ");
		print_cn_name(iname, NULL, S_COLOR_YELLOW);
		Con_Printf(")");
		if(depth == 0) {
			/* If depth is 0, its the server's certificate. Print the SANs too */
	//		print_san_name("Subject (san)", cert);
		}

		if(err == X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY)
			Con_Printf(":  Error = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY\n");
		else if (err == X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE)
			Con_Printf(":  Error = X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE\n");
		else if(err == X509_V_ERR_CERT_UNTRUSTED)
			Con_Printf(":  Error = X509_V_ERR_CERT_UNTRUSTED\n");
		else if(err == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN)
			Con_Printf(":  Error = X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN\n");
		else if(err == X509_V_ERR_CERT_NOT_YET_VALID)
			Con_Printf(":  Error = X509_V_ERR_CERT_NOT_YET_VALID\n");
		else if(err == X509_V_ERR_CERT_HAS_EXPIRED)
			Con_Printf(":  Error = X509_V_ERR_CERT_HAS_EXPIRED\n");
		else if(err == X509_V_OK)
			Con_Printf(":  Error = X509_V_OK\n");
		else if(err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)
			Con_Printf(":  Error = X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT\n");
		else
			Con_Printf(":  Error = %d\n", err);
	}

	if (!preverify_ok && cvarfuncs->GetFloat("tls_ignorecertificateerrors"))
	{
		Con_Printf(CON_ERROR "%s: Ignoring certificate errors (tls_ignorecertificateerrors is set)\n", uctx->peername);
		return 1;
	}

	return preverify_ok;
}


static vfsfile_t *OSSL_OpenPrivKeyFile(char *nativename, size_t nativesize)
{
#define privname "privkey.pem"
	vfsfile_t *privf;
	const char *mode = nativename?"wb":"rb";
	/*int i = COM_CheckParm("-privkey");
	if (i++)
	{
		if (nativename)
			Q_strncpyz(nativename, com_argv[i], nativesize);
		privf = FS_OpenVFS(com_argv[i], mode, FS_SYSTEM);
	}
	else*/
	{
		if (nativename)
			if (!fsfuncs->NativePath(privname, FS_ROOT, nativename, nativesize))
				return NULL;

		privf = fsfuncs->OpenVFS(privname, mode, FS_ROOT);
	}
	return privf;
#undef privname
}
static vfsfile_t *OSSL_OpenPubKeyFile(char *nativename, size_t nativesize)
{
#define fullchainname "fullchain.pem"
#define pubname "cert.pem"
	vfsfile_t *pubf = NULL;
	const char *mode = nativename?"wb":"rb";
	/*int i = COM_CheckParm("-pubkey");
	if (i++)
	{
		if (nativename)
			Q_strncpyz(nativename, com_argv[i], nativesize);
		pubf = FS_OpenVFS(com_argv[i], mode, FS_SYSTEM);
	}
	else*/
	{
		if (!pubf && (!nativename || fsfuncs->NativePath(fullchainname, FS_ROOT, nativename, nativesize)))
			pubf = fsfuncs->OpenVFS(fullchainname, mode, FS_ROOT);
		if (!pubf && (!nativename || fsfuncs->NativePath(pubname, FS_ROOT, nativename, nativesize)))
			pubf = fsfuncs->OpenVFS(pubname, mode, FS_ROOT);
	}
	return pubf;
#undef pubname
}
static BIO *OSSL_BioFromFile(vfsfile_t *f)
{
	qbyte buf[4096];
	int r;
	BIO *b = BIO_new(BIO_s_mem());
	if (f)
	{
		for(;;)
		{
			r = VFS_READ(f, buf, sizeof(buf));
			if (r <= 0)
				break;
			BIO_write(b, buf, r);
		}
		VFS_CLOSE(f);
	}

	return b;
}
static void OSSL_OpenPrivKey(void)
{
	BIO *bio = OSSL_BioFromFile(OSSL_OpenPrivKeyFile(NULL,0));
	vhost.privatekey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
	BIO_free(bio);
}
static void OSSL_OpenPubKey(void)
{
	BIO *bio = OSSL_BioFromFile(OSSL_OpenPubKeyFile(NULL,0));
	vhost.servercert = PEM_read_bio_X509(bio, NULL, NULL, NULL);
	BIO_free(bio);
}

static char *OSSL_SetCertificateName(char *out, const char *hostname)
{	//glorified strcpy...
	int i;
	if (hostname)
	{
		const char *host = strstr(hostname, "://");
		if (host)
			hostname = host+3;
		//any dtls:// prefix will have been stripped now.
		if (*hostname == '[')
		{	//eg: [::1]:foo - skip the lead [ and strip the ] and any trailing data (hopefully just a :port or nothing)
			hostname++;
			host = strchr(hostname, ']');
			if (host)
			{
				memcpy(out, hostname, host-hostname);
				out[host-hostname] = 0;
				hostname = out;
			}
		}
		else
		{	//eg: 127.0.0.1:port - strip the port number if specified.
			host = strchr(hostname, ':');
			if (host)
			{
				memcpy(out, hostname, host-hostname);
				out[host-hostname] = 0;
				hostname = out;
			}
		}
		for (i = 0; hostname[i]; i++)
		{
			if (hostname[i] >= 'a' && hostname[i] <= 'z')
				;
			else if (hostname[i] >= 'A' && hostname[i] <= 'Z')
				;
			else if (hostname[i] >= '0' && hostname[i] <= '9')
				;
			else if (hostname[i] == '-' || hostname[i] == '.')
				;
			else
			{
				hostname = NULL;	//something invalid. bum.
				break;
			}
		}
		//we should have a cleaned up host name now, ready for (ab)use in certificates.
	}

	if (!hostname)
		*out = 0;
	else if (hostname == out)
		;
	else
		memcpy(out, hostname, strlen(hostname)+1);

	return out;
}

static vfsfile_t *OSSL_OpenVFS(const char *hostname, vfsfile_t *source, qboolean isserver)
{
	BIO *sink;
	osslvfs_t *n;
	if (!OSSL_Init())
		return NULL;	//FAIL!

	if (!hostname)
		hostname = "";

	n = calloc(sizeof(*n) + strlen(hostname)+1, 1);

	n->funcs.ReadBytes = OSSL_FRead;
	n->funcs.WriteBytes = OSSL_FWrite;
	n->funcs.Seek = NULL;
	n->funcs.Tell = NULL;
	n->funcs.GetLen = NULL;
	n->funcs.Close = OSSL_Close;
	n->funcs.Flush = NULL;
	n->funcs.seekstyle = SS_UNSEEKABLE;

	n->cert.peername = OSSL_SetCertificateName((char*)(n+1), hostname);
	n->cert.dtls = false;

	ERR_print_errors_cb(OSSL_PrintError_CB, NULL);

	sink = BIO_new(biometh_vfs);
	if (sink)
	{
		n->ctx = SSL_CTX_new(isserver?TLS_server_method():TLS_client_method());
		if (n->ctx)
		{
			assert(1==SSL_CTX_set_cipher_list(n->ctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"));

			SSL_CTX_set_session_cache_mode(n->ctx, SSL_SESS_CACHE_OFF);

			SSL_CTX_set_default_verify_paths(n->ctx);
			SSL_CTX_set_verify(n->ctx, SSL_VERIFY_PEER, OSSL_Verify_Peer);
			SSL_CTX_set_verify_depth(n->ctx, 5);
			SSL_CTX_set_options(n->ctx, SSL_OP_NO_COMPRESSION);	//compression allows guessing the contents of the stream somehow.

			if (isserver)
			{
				if (vhost.servercert && vhost.privatekey)
				{
					SSL_CTX_use_certificate(n->ctx, vhost.servercert);
					SSL_CTX_use_PrivateKey(n->ctx, vhost.privatekey);
					assert(1==SSL_CTX_check_private_key(n->ctx));
				}
			}

			n->bio = BIO_new_ssl(n->ctx, !isserver);

			//set up the source/sink
			BIO_set_data(sink, source);	//source now belongs to the bio
			BIO_set_init(sink, true);	//our sink is now ready...
			n->bio = BIO_push(n->bio, sink);
			BIO_free(sink);
			sink = NULL;

			BIO_get_ssl(n->bio, &n->ssl);
			SSL_set_ex_data(n->ssl, ossl_fte_certctx, &n->cert);
			SSL_set_mode(n->ssl, SSL_MODE_ENABLE_PARTIAL_WRITE|SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
			SSL_set_tlsext_host_name(n->ssl, n->cert.peername);	//let the server know which cert to send
			BIO_do_connect(n->bio);
			return &n->funcs;
		}
		BIO_free(sink);
	}
	return NULL;
}
/*static int OSSL_GetChannelBinding(vfsfile_t *vf, qbyte *binddata, size_t *bindsize)
{
	//FIXME: not yet supported. tbh I've no idea how to get that data. probably something convoluted.
	return -1;
}*/



static BIO_METHOD *biometh_dtls;
typedef struct {
	struct fte_certctx_s cert;

	void *cbctx;
	neterr_t(*push)(void *cbctx, const qbyte *data, size_t datasize);

	SSL_CTX *ctx;
	BIO *bio;
	SSL *ssl;

//	BIO *sink;
	qbyte *pending;
	size_t pendingsize;

	void	*peeraddr;
	size_t	peeraddrsize;
} ossldtls_t;
static int OSSL_Bio_DWrite(BIO *h, const char *buf, int size)
{
	ossldtls_t *f = BIO_get_data(h);
	neterr_t r = f->push(f->cbctx, buf, size);

	BIO_clear_retry_flags(h);
	switch(r)
	{
	case NETERR_SENT:
		return size;
	case NETERR_NOROUTE:
	case NETERR_DISCONNECTED:
		return -1;
	case NETERR_MTU:
		return -1;
	case NETERR_CLOGGED:
		BIO_set_retry_write(h);
		return -1;
	}
	return r;
}
static int OSSL_Bio_DRead(BIO *h, char *buf, int size)
{
	ossldtls_t *f = BIO_get_data(h);

	BIO_clear_retry_flags(h);
	if (f->pending)
	{
		size = min(size, f->pendingsize);
		memcpy(buf, f->pending, f->pendingsize);

		//we've read it now, don't read it again.
		f->pending = 0;
		f->pendingsize = 0;
		return size;
	}
	//nothing available.
	BIO_set_retry_read(h);
	return -1;
}
static long OSSL_Bio_DCtrl(BIO *h, int cmd, long arg1, void *arg2)
{
//	ossldtls_t *f = BIO_get_data(h);
	switch(cmd)
	{
	case BIO_CTRL_FLUSH:
		return 1;


	case BIO_CTRL_DGRAM_GET_PEER:
		return 0;

	case BIO_CTRL_DGRAM_SET_NEXT_TIMEOUT:	//we're non-blocking, so this doesn't affect us.
	case BIO_CTRL_DGRAM_GET_MTU_OVERHEAD:
	case BIO_CTRL_WPENDING:
	case BIO_CTRL_DGRAM_QUERY_MTU:
	case BIO_CTRL_DGRAM_SET_MTU:
	case BIO_CTRL_DGRAM_GET_FALLBACK_MTU:
		return 0;


	default:
		Con_Printf("OSSL_Bio_DCtrl: unknown cmd %i\n", cmd);
	case BIO_CTRL_PUSH:
	case BIO_CTRL_POP:
		return 0;
	}
	return 0;	//failure
}
static long OSSL_Bio_DOtherCtrl(BIO *h, int cmd, BIO_info_cb *cb)
{
	switch(cmd)
	{
	default:
		Con_Printf("OSSL_Bio_DOtherCtrl unknown cmd %i\n", cmd);
		return 0;
	}
	return 0;	//failure
}
static int OSSL_Bio_DCreate(BIO *h)
{	//we'll have to fill this in after we create the bio.
	BIO_set_data(h, NULL);
	return 1;
}
static int OSSL_Bio_DDestroy(BIO *h)
{
	BIO_set_data(h, NULL);
	return 1;
}

static int dehex(int i)
{
	if      (i >= '0' && i <= '9')
		return (i-'0');
	else if (i >= 'A' && i <= 'F')
		return (i-'A'+10);
	else
		return (i-'a'+10);
}
static size_t Base16_DecodeBlock_(const char *in, qbyte *out, size_t outsize)
{
	qbyte *start = out;
	if (!out)
		return ((strlen(in)+1)/2) + 1;

	for (; ishexcode(in[0]) && ishexcode(in[1]) && outsize > 0; outsize--, in+=2)
		*out++ = (dehex(in[0])<<4) | dehex(in[1]);
	return out-start;
}

static unsigned int OSSL_SV_Validate_PSK(SSL *ssl, const char *identity, unsigned char *psk, unsigned int max_psk_len)
{
	if (!strcmp(identity, pdtls_psk_user->string))
	{	//Yay! We know this one!
		return Base16_DecodeBlock_(pdtls_psk_key->string, psk, max_psk_len);
	}
	return 0;	//0 for error, or something.
}
unsigned int OSSL_CL_Validate_PSK(SSL *ssl, const char *hint, char *identity, unsigned int max_identity_len, unsigned char *psk, unsigned int max_psk_len)
{	//if our hint cvar matches, then report our user+key cvars to the server
	if ((!*hint && *pdtls_psk_user->string && !*pdtls_psk_hint->string) || (*hint && !strcmp(hint, pdtls_psk_hint->string)))
	{
#ifndef NOLEGACY
		if (*hint)
		{
			//Try to avoid crashing QE servers by recognising its hint and blocking it when the hashes of the user+key are wrong.
			quint32_t digest[SHA_DIGEST_LENGTH/4];

			SHA1(hint, strlen(hint), (qbyte*)digest);
			if ((digest[0]^digest[1]^digest[2]^digest[3]^digest[4]) == 0xb6c27b61)
			{
				SHA1(pdtls_psk_key->string, strlen(pdtls_psk_key->string), (qbyte*)digest);
				if (strcmp(hint, pdtls_psk_user->string) || (digest[0]^digest[1]^digest[2]^digest[3]^digest[4]) != 0x3dd348e4)
				{
					Con_Printf(CON_WARNING	"Possible QEx Server, please set your ^[%s\\type\\%s^] and ^[%s\\type\\%s^] cvars correctly, their current values are likely to crash the server.\n", pdtls_psk_user->name,pdtls_psk_user->name, pdtls_psk_key->name,pdtls_psk_key->name);
					return 0; //don't report anything.
				}
			}
		}
#endif

		Q_strlcpy(identity, pdtls_psk_user->string, max_identity_len);
		return Base16_DecodeBlock_(pdtls_psk_key->string, psk, max_psk_len);
	}
	else if (*hint)
		Con_Printf(CON_WARNING	"Unable to supply PSK response to server (hint is \"%s\").\n"
								"Please set ^[%s\\type\\%s^], ^[%s\\type\\%s^], and ^[%s\\type\\%s^] cvars to match the server.\n", hint, pdtls_psk_hint->name,pdtls_psk_hint->name, pdtls_psk_user->name,pdtls_psk_user->name, pdtls_psk_key->name,pdtls_psk_key->name);
	return 0;	//we don't know what to report.
}

static void *OSSL_CreateContext(const dtlscred_t *cred, void *cbctx, neterr_t(*push)(void *cbctx, const qbyte *data, size_t datasize), qboolean isserver)
{	//if remotehost is null then their certificate will not be validated.
	ossldtls_t *n;
	BIO *sink;
	const char *remotehost = cred?cred->peer.name:NULL;

	if (!remotehost)
		remotehost = "";
	n = calloc(sizeof(*n) + strlen(remotehost)+1, 1);

	n->cbctx = cbctx;
	n->push = push;

	n->ctx = SSL_CTX_new(isserver?DTLS_server_method():DTLS_client_method());

	n->cert.peername = OSSL_SetCertificateName((char*)(n+1), remotehost);
	n->cert.dtls = true;
	n->cert.hash = cred->peer.hash;
	memcpy(n->cert.digest, cred->peer.digest, sizeof(cred->peer.digest));

	if (n->ctx)
	{
		assert(1==SSL_CTX_set_cipher_list(n->ctx, "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"));

		SSL_CTX_set_session_cache_mode(n->ctx, SSL_SESS_CACHE_OFF);

		SSL_CTX_set_verify(n->ctx, SSL_VERIFY_PEER|(cred->peer.hash?SSL_VERIFY_FAIL_IF_NO_PEER_CERT:0), OSSL_Verify_Peer);
		SSL_CTX_set_verify_depth(n->ctx, 5);
		SSL_CTX_set_options(n->ctx, SSL_OP_NO_COMPRESSION|	//compression allows guessing the contents of the stream somehow.
									SSL_OP_NO_RENEGOTIATION);

		if (cred->local.certsize||cred->local.keysize)
		{
			X509 *cert = NULL;
			EVP_PKEY *key = NULL;
			const unsigned char *ffs;
			ffs = cred->local.cert;
			d2i_X509(&cert, &ffs, cred->local.certsize);
			SSL_CTX_use_certificate(n->ctx, cert);

			ffs = cred->local.key;
			d2i_PrivateKey(EVP_PKEY_RSA, &key, &ffs, cred->local.keysize);
			SSL_CTX_use_PrivateKey(n->ctx, key);
		}
		else if (isserver)
		{
			if (*pdtls_psk_user->string)
			{
				if (*pdtls_psk_user->string)
					SSL_CTX_use_psk_identity_hint(n->ctx, pdtls_psk_hint->string);
				SSL_CTX_set_psk_server_callback(n->ctx, OSSL_SV_Validate_PSK);
			}

			if (vhost.servercert && vhost.privatekey)
			{
				SSL_CTX_use_certificate(n->ctx, vhost.servercert);
				SSL_CTX_use_PrivateKey(n->ctx, vhost.privatekey);
				assert(1==SSL_CTX_check_private_key(n->ctx));
			}
		}
		else
		{
//			if (*pdtls_psk_user->string)
				SSL_CTX_set_psk_client_callback(n->ctx, OSSL_CL_Validate_PSK);
		}

		//SSL_CTX_use_certificate_file
		//FIXME: SSL_CTX_use_certificate_file aka SSL_CTX_use_certificate(PEM_read_bio_X509)
		//FIXME: SSL_CTX_use_PrivateKey_file aka SSL_CTX_use_PrivateKey(PEM_read_bio_PrivateKey)
		//assert(1==SSL_CTX_check_private_key(n->ctx));

		{
			n->bio = BIO_new_ssl(n->ctx, !isserver);

			//set up the source/sink
			sink = BIO_new(biometh_dtls);
			if (sink)
			{
				BIO_set_data(sink, n);
				BIO_set_init(sink, true);	//our sink is now ready...
				n->bio = BIO_push(n->bio, sink);
				BIO_free(sink);
				sink = NULL;
			}

			BIO_get_ssl(n->bio, &n->ssl);
			SSL_set_app_data(n->ssl, n);
			SSL_set_ex_data(n->ssl, ossl_fte_certctx, &n->cert);
			if (*n->cert.peername)
				SSL_set_tlsext_host_name(n->ssl, n->cert.peername);	//let the server know which cert to send
			BIO_do_connect(n->bio);
			ERR_print_errors_cb(OSSL_PrintError_CB, NULL);
			return n;
		}
	}

	return NULL;
}

static qbyte dtlscookiekey[16];
static int OSSL_GenCookie(SSL *ssl, unsigned char *cookie, unsigned int *cookie_len)
{
	ossldtls_t *f = SSL_get_app_data(ssl);
	qbyte *blurgh = alloca(sizeof(dtlscookiekey) + f->peeraddrsize);

	memcpy(blurgh, dtlscookiekey, sizeof(dtlscookiekey));
	memcpy(blurgh+sizeof(dtlscookiekey), f->peeraddr, f->peeraddrsize);

	SHA1(blurgh, sizeof(dtlscookiekey) + f->peeraddrsize, cookie);
	*cookie_len = SHA_DIGEST_LENGTH;

	return 1;
}
static int OSSL_VerifyCookie(SSL *ssl, const unsigned char *cookie, unsigned int cookie_len)
{
	unsigned char match[DTLS1_COOKIE_LENGTH];
	unsigned int matchsize;
	if (OSSL_GenCookie(ssl, match, &matchsize))
		if (cookie_len == matchsize && !memcmp(cookie, match, matchsize))
			return 1;
	return 0;	//not valid.
}
qboolean OSSL_CheckConnection(void *cbctx, void *peeraddr, size_t peeraddrsize, void *indata, size_t insize, neterr_t(*push)(void *cbctx, const qbyte *data, size_t datasize), void (*EstablishTrueContext)(void **cbctx, void *state))
{
	int ret;
	static ossldtls_t *pending;
	BIO_ADDR *bioaddr = BIO_ADDR_new();

	if (!pending)
	{
		pending = OSSL_CreateContext(NULL, cbctx, push, true);

		SSL_CTX_set_cookie_generate_cb(pending->ctx, OSSL_GenCookie);
		SSL_CTX_set_cookie_verify_cb(pending->ctx, OSSL_VerifyCookie);
	}

	SSL_set_app_data(pending->ssl, pending);

	//make sure its kept current...
	pending->cbctx = cbctx;
	pending->push = push;

	pending->pending = indata;
	pending->pendingsize = insize;
	ret = DTLSv1_listen(pending->ssl, bioaddr);

	BIO_ADDR_free(bioaddr);

	if (ret >= 1)
	{
		pending->pending = NULL;
		pending->pendingsize = 0;

		EstablishTrueContext(&pending->cbctx, pending);
		pending = NULL;	//returned to called. next request gets a new one.
		return true;
	}
	//0 = nonfatal
	//-1 = fatal
	return false;
}
static void OSSL_DestroyContext(void *ctx)
{
	ossldtls_t *o = (ossldtls_t*)ctx;
	BIO_free(o->bio);
	SSL_CTX_free(o->ctx);
	free(o);
}
static neterr_t OSSL_Transmit(void *ctx, const qbyte *data, size_t datasize)
{	//we're sending data
	ossldtls_t *o = (ossldtls_t*)ctx;
	int r = BIO_write(o->bio, data, datasize);
	if (r <= 0)
	{
		if (BIO_should_io_special(o->bio))
		{
			switch(BIO_get_retry_reason(o->bio))
			{
			//these are temporary errors, try again later.
			case BIO_RR_SSL_X509_LOOKUP:
				return NETERR_NOROUTE;	//certificate failure.
			case BIO_RR_ACCEPT:
			case BIO_RR_CONNECT:
				return NETERR_NOROUTE;	//should never happen
			}
		}
		if (BIO_should_retry(o->bio))
			return 0;
		return NETERR_DISCONNECTED;	//eof or something
	}
	return NETERR_SENT;
}
static neterr_t OSSL_Received(void *ctx, sizebuf_t *message)
{	//we have received some encrypted data...
	ossldtls_t *o = (ossldtls_t*)ctx;
	int r;

	if (!message)
		r = 0;
	else
	{
		o->pending = message->data;
		o->pendingsize = message->cursize;
		r = BIO_read(o->bio, message->data, message->maxsize);
		o->pending = NULL;
		o->pendingsize = 0;
	}

	if (r > 0)
	{
		message->cursize = r;
		return NETERR_SENT;
	}
	else
	{
		if (BIO_should_io_special(o->bio))
		{
			switch(BIO_get_retry_reason(o->bio))
			{
			//these are temporary errors, try again later.
			case BIO_RR_SSL_X509_LOOKUP:
				return NETERR_NOROUTE;	//certificate failure.
			case BIO_RR_ACCEPT:
			case BIO_RR_CONNECT:
				return NETERR_NOROUTE;	//should never happen
			}
		}
		if (BIO_should_retry(o->bio))
			return 0;
		return NETERR_DISCONNECTED;	//eof or something
	}
	return NETERR_NOROUTE;
}
static neterr_t OSSL_Timeouts(void *ctx)
{	//keep it ticking over, or something.
	return OSSL_Received(ctx, NULL);
}

qboolean OSSL_GenTempCertificate(const char *subject, struct dtlslocalcred_s *cred)
{
	EVP_PKEY*pkey = EVP_PKEY_new();
	RSA		*rsa = RSA_new();
	BIGNUM	*pkexponent = BN_new();
	qbyte *ffs;
	//The pseudo-random number generator must be seeded prior to calling RSA_generate_key_ex().
	BN_set_word(pkexponent, RSA_F4);
	RSA_generate_key_ex(rsa, 2048, pkexponent, NULL);
	BN_free(pkexponent);

	EVP_PKEY_assign_RSA(pkey, rsa);

	cred->keysize = i2d_PrivateKey(pkey, NULL);
	cred->key = ffs = plugfuncs->Malloc(cred->keysize);
	cred->keysize = i2d_PrivateKey(pkey, &ffs);

	{
		X509 *x509 = X509_new();
		ASN1_INTEGER_set(X509_get_serialNumber(x509), 1);
		X509_gmtime_adj(X509_get_notBefore(x509), 0);
		X509_gmtime_adj(X509_get_notAfter(x509), 365*24*60*60);	//lots of validity
		X509_set_pubkey(x509, pkey);

		{
			X509_NAME	*name = X509_get_subject_name(x509);
			X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (subject?subject:"localhost"), -1, -1, 0);
			X509_set_issuer_name(x509, name);
		}

		X509_sign(x509, pkey, EVP_sha1());

		cred->certsize = i2d_X509(x509, NULL);
		cred->cert = ffs = plugfuncs->Malloc(cred->certsize);
		cred->certsize = i2d_X509(x509, &ffs);

		X509_free(x509);
	}

	EVP_PKEY_free(pkey);	//also frees the rsa pointer.

	return true;
}

static dtlsfuncs_t ossl_dtlsfuncs =
{
	OSSL_CreateContext,
	OSSL_CheckConnection,
	OSSL_DestroyContext,
	OSSL_Transmit,
	OSSL_Received,
	OSSL_Timeouts,
	NULL,
	OSSL_GenTempCertificate,
};
static const dtlsfuncs_t *OSSL_InitClient(void)
{
	if (OSSL_Init())
		return &ossl_dtlsfuncs;
	return NULL;
}
static const dtlsfuncs_t *OSSL_InitServer(void)
{
	if (OSSL_Init())
		return &ossl_dtlsfuncs;
	return NULL;
}










static struct
{
	qboolean inited;
	qboolean init_success;
} ossl;
static qboolean OSSL_Init(void)
{
	if (ossl.inited)
		return ossl.init_success;
	ossl.inited = true;
#if 0//def LOADERTHREAD
	Sys_LockMutex(com_resourcemutex);
	if (inited)	//now check again, just in case
	{
		Sys_UnlockMutex(com_resourcemutex);
		return init_success;
	}
#endif

	SSL_library_init();
    SSL_load_error_strings();
//	OPENSSL_config(NULL);
	ERR_print_errors_cb(OSSL_PrintError_CB, NULL);

	OSSL_OpenPubKey();
	OSSL_OpenPrivKey();

	biometh_vfs = BIO_meth_new(BIO_get_new_index()|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR, "fte_vfs");
	if (biometh_vfs)
	{
		BIO_meth_set_write(biometh_vfs, OSSL_Bio_FWrite);
		BIO_meth_set_read(biometh_vfs, OSSL_Bio_FRead);
		BIO_meth_set_puts(biometh_vfs, OSSL_Bio_FPuts);	//I cannot see how gets/puts can work with dtls...
//		BIO_meth_set_gets(biometh_vfs, OSSL_Bio_FGets);
		BIO_meth_set_ctrl(biometh_vfs, OSSL_Bio_FCtrl);
		BIO_meth_set_create(biometh_vfs, OSSL_Bio_FCreate);
		BIO_meth_set_destroy(biometh_vfs, OSSL_Bio_FDestroy);
		BIO_meth_set_callback_ctrl(biometh_vfs, OSSL_Bio_FOtherCtrl);
		ossl.init_success |= 1;
	}

	biometh_dtls = BIO_meth_new(BIO_get_new_index()|BIO_TYPE_SOURCE_SINK|BIO_TYPE_DESCRIPTOR, "fte_dtls");
	if (biometh_dtls)
	{
		BIO_meth_set_write(biometh_dtls, OSSL_Bio_DWrite);
		BIO_meth_set_read(biometh_dtls, OSSL_Bio_DRead);
//		BIO_meth_set_puts(biometh_dtls, OSSL_Bio_DPuts);	//I cannot see how gets/puts can work with dtls...
//		BIO_meth_set_gets(biometh_dtls, OSSL_Bio_DGets);
		BIO_meth_set_ctrl(biometh_dtls, OSSL_Bio_DCtrl);
		BIO_meth_set_create(biometh_dtls, OSSL_Bio_DCreate);
		BIO_meth_set_destroy(biometh_dtls, OSSL_Bio_DDestroy);
		BIO_meth_set_callback_ctrl(biometh_dtls, OSSL_Bio_DOtherCtrl);
		ossl.init_success |= 2;
	}

	ossl_fte_certctx = SSL_get_ex_new_index(0, "ossl_fte_certctx", NULL, NULL, NULL);

#if 0//def LOADERTHREAD
	Sys_UnlockMutex(com_resourcemutex);
#endif
	return ossl.init_success;
}

static enum hashvalidation_e OSSL_VerifyHash(const qbyte *hashdata, size_t hashsize, const qbyte *pubkeydata, size_t pubkeysize, const qbyte *signdata, size_t signsize)
{
	int result = VH_UNSUPPORTED;
	BIO *bio_pubkey = BIO_new_mem_buf(pubkeydata, pubkeysize);
	if (bio_pubkey)
	{
		X509 *x509_pubkey = PEM_read_bio_X509(bio_pubkey, NULL, NULL, NULL);
		if (x509_pubkey)
		{
			EVP_PKEY *evp_pubkey = X509_get_pubkey(x509_pubkey);
			if (evp_pubkey)
			{
				RSA *rsa_pubkey = EVP_PKEY_get1_RSA(evp_pubkey);
				if (rsa_pubkey)
				{
					if (1 == RSA_verify(NID_sha512, hashdata, hashsize,	signdata, signsize, rsa_pubkey))
						result = VH_CORRECT;
					else
						result = VH_INCORRECT;
					RSA_free(rsa_pubkey);
				}
				EVP_PKEY_free(evp_pubkey);
			}
			X509_free(x509_pubkey);
		}
		BIO_free(bio_pubkey);
	}

	return result;
}

static ftecrypto_t crypto_openssl =
{
	"OpenSSL",
	OSSL_OpenVFS,
	NULL,//OSSL_GetChannelBinding,
	OSSL_InitClient,
	OSSL_InitServer,
	OSSL_VerifyHash,
	NULL,
};

static void OSSL_PluginShutdown(void)
{
	ossl.inited = false;
	ossl.init_success = false;

	X509_free(vhost.servercert);
	EVP_PKEY_free(vhost.privatekey);
	BIO_meth_free(biometh_vfs);
	BIO_meth_free(biometh_dtls);
}
static qboolean OSSL_PluginMayShutdown(void)
{
	//the engine has a habit of holding on to handles without any refcounts, so don't allow it to die early.
	return false;
}

qboolean Plug_Init(void)
{
	fsfuncs = plugfuncs->GetEngineInterface(plugfsfuncs_name, sizeof(*fsfuncs));
	netfuncs = plugfuncs->GetEngineInterface(plugnetfuncs_name, sizeof(*netfuncs));
	if (!fsfuncs || !netfuncs)
		return false;

	plugfuncs->ExportFunction("Shutdown", OSSL_PluginShutdown);
	plugfuncs->ExportFunction("MayUnload", OSSL_PluginMayShutdown);

	pdtls_psk_hint = cvarfuncs->GetNVFDG("dtls_psk_hint", "", 0, NULL, "DTLS stuff");
	pdtls_psk_user = cvarfuncs->GetNVFDG("dtls_psk_user", "", 0, NULL, "DTLS stuff");
	pdtls_psk_key  = cvarfuncs->GetNVFDG("dtls_psk_key",  "", 0, NULL, "DTLS stuff");
	netfuncs->RandomBytes(dtlscookiekey, sizeof(dtlscookiekey));	//something random so people can't guess cookies for arbitrary victim IPs.

	OSSL_Init();	//shoving this here solves threading issues (eg two loader threads racing to open an https image url)

	return plugfuncs->ExportInterface("Crypto", &crypto_openssl, sizeof(crypto_openssl)); //export a named interface struct to the engine
}