/*
    Copyright (C) 2008-2024, Millistream Market Data <support@millistream.com>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

#include "common.h"
#include "templates.h"
#include "encode.h"
#include "crypt.h"

/* buf must be at least 5 bytes */
static int add_len (uint8_t *buf, uint32_t len)
{
	uint8_t *p = buf;
	uint32_t tmp = len;
	uint32_t offset = 0;
	int d;
	int c = 0;

	do {
		++c;
		offset = offset * 128 + 1;
		tmp = (tmp - 1) / 128;
	} while (tmp != 0);

	len -= offset;
	d = c - 1;

	do {
		p[--c] = (uint8_t) (len % 128);
		len /= 128;
	} while (c != 0);

	p += d;

	*p++ |= 128;

	return p - buf;
}

/* buf must be at least 11 bytes */
static int add_header (uint8_t *buf, uint16_t mref, uint64_t insref)
{
	uint8_t *p = buf;
	uint8_t *attr = buf;

	if (mref <= 256) {
		*p++ = 0;
		*p++ = (uint8_t) mref;
	} else {
		*p++ = 128;
		mref = cpu_to_le16 (mref);
		memcpy (p, &mref, sizeof mref);
		p += 2;
	}

	if (insref <= 8191) {
		*attr |= (uint8_t) (insref >> 8);
		*p++ = (uint8_t) (insref & __UINT64_C (0x00000000000000FF));
	} else if (insref <= 2105343) { 
		insref -= 8192;
		*attr |= 32 | (uint8_t) (insref >> 16);
		uint16_t val = cpu_to_le16 ((uint16_t)(insref & __UINT64_C (0x000000000000FFFF)));
		memcpy (p, &val, sizeof val);
		p += 2;
	} else if (insref <= __UINT64_C (137441058815)) {
		insref -= 2105344;
		*attr |= 64 | (uint8_t) (insref >> 32);
		uint32_t val = cpu_to_le32 ((uint32_t)(insref & __UINT64_C (0x00000000FFFFFFFF)));
		memcpy (p, &val, sizeof val);
		p += 4;
	} else {
		*attr |= 96;
		insref = cpu_to_le64 (insref);
		memcpy (p, &insref, sizeof insref);
		p += 8;
	}

	return p - buf;
}

static inline int calc_header_len (const uint16_t mref, const uint64_t insref)
{
	int len;

	if (mref <= 256)
		len = 2;
	else
		len = 3;

	if (insref <= 8191)
		len++;
	else if (insref <= 2105343)
		len += 2;
	else if (insref <= __UINT64_C (137441058815))
		len += 4;
	else
		len += 8;

	return len;
}

LIBMDF_API mdf_t mdf_create ()
{
	struct mdf_s *handle;
#if defined _WIN32 || defined _WIN64
	WSADATA wsaData;

	if (WSAStartup (MAKEWORD (2,2), &wsaData) != 0)
		return NULL;
#endif
	handle = (struct mdf_s *) calloc (1, sizeof *handle);

	if (handle == NULL) {
#if defined _WIN32 || defined _WIN64
		WSACleanup ();
#endif
		return NULL;
	}

	handle->data_size = 2048;
	handle->data = (uint8_t *) malloc (handle->data_size);
	handle->timediff_ns = 0;
	handle->vlen = 1024;
	handle->value = (uint8_t *) malloc (handle->vlen);
	handle->error = MDF_ERR_NO_ERROR;
	handle->fd = INVALID_SOCKET;
	handle->connect_timeout = 5;
	handle->heartbeat_interval = 30;
	handle->max_missed_heartbeats = 2;
	handle->crypto.cstate = CSTATE_INVALID;

	if (handle->data == NULL || handle->value == NULL) {
#if defined _WIN32 || defined _WIN64
		WSACleanup ();
#endif
		free (handle->data);
		handle->data = NULL;

		free (handle->value);
		handle->value = NULL;

		free (handle);
		return NULL;
	}

	templates_initialize (&handle->templates);

#if !defined (MSG_NOSIGNAL) && !defined (SO_NOSIGPIPE) && defined (SIGPIPE) && defined (HAVE_SIGNAL)
	signal (SIGPIPE, SIG_IGN);
#endif

#if OPENSSL_VERSION_PREREQ(3,0)
	handle->prov_legacy = OSSL_PROVIDER_load(NULL, "legacy");
	handle->prov_default = OSSL_PROVIDER_load(NULL, "default");
	handle->mac = EVP_MAC_fetch (NULL, "HMAC", NULL);
#else
	ENGINE_load_builtin_engines ();
	ENGINE_register_all_complete ();
# if !OPENSSL_VERSION_PREREQ(1,1)
	OpenSSL_add_all_algorithms ();
# endif
#endif
	handle->crypto.digests = mdf_int_get_default_digests ();
	handle->crypto.ciphers = mdf_int_get_default_ciphers ();

	return handle;
}

LIBMDF_API void mdf_destroy (mdf_t handle)
{
	if (handle == NULL)
		return;

	mdf_disconnect (handle);

	free (handle->data);
	handle->data = NULL;

	free (handle->value);
	handle->value = NULL;

	free (handle->connected_host);
	handle->connected_host = NULL;

	free (handle->connected_ip);
	handle->connected_ip = NULL;

	free (handle->bind_address);
	handle->bind_address = NULL;

	templates_free (handle->templates);
	handle->templates = NULL;
	templates_free (handle->templates_old);
	handle->templates_old = NULL;

#if OPENSSL_VERSION_PREREQ(3,0)
	OSSL_PROVIDER_unload (handle->prov_legacy);
	OSSL_PROVIDER_unload (handle->prov_default);
	EVP_MAC_free (handle->mac);
#elif !OPENSSL_VERSION_PREREQ(1,1)
	EVP_cleanup ();
#endif
	free (handle);

#if defined _WIN32 || defined _WIN64
	WSACleanup ();
#endif
}

static void call_status_callback (mdf_t handle, MDF_CONN_STATUS status, const char *host, const char *ip)
{
	if (handle->callbacks.func_status == NULL)
		return;

	handle->callbacks.func_status (handle->callbacks.udata_status, status, host, ip);
}

static void disconnect (mdf_t handle)
{
	int expected = 0;
	while (!atomic_compare_exchange_weak_explicit (&handle->slock, &expected, 1, memory_order_acquire, memory_order_relaxed))
		expected = 0;

	if (handle->crypto.master_secret != NULL) {
		memset (handle->crypto.master_secret, 0x00, handle->crypto.digest_length);
		free (handle->crypto.master_secret);
		handle->crypto.master_secret = NULL;
	}

	if (handle->crypto.cipher_encrypt_ctx != NULL) {
		EVP_CIPHER_CTX_free (handle->crypto.cipher_encrypt_ctx);
		handle->crypto.cipher_encrypt_ctx = NULL;
	}

	if (handle->crypto.cipher_decrypt_ctx != NULL) {
		EVP_CIPHER_CTX_free (handle->crypto.cipher_decrypt_ctx);
		handle->crypto.cipher_decrypt_ctx = NULL;
	}

#if OPENSSL_VERSION_PREREQ(3,0)
	if (handle->crypto.cipher != NULL) {
		EVP_CIPHER_free (handle->crypto.cipher);
		handle->crypto.cipher = NULL;
	}

	if (handle->crypto.digest != NULL) {
		EVP_MD_free (handle->crypto.digest);
		handle->crypto.digest = NULL;
	}

	if (handle->crypto.enc_mctx != NULL) {
		EVP_MAC_CTX_free (handle->crypto.enc_mctx);
		handle->crypto.enc_mctx = NULL;
	}

	if (handle->crypto.dec_mctx != NULL) {
		EVP_MAC_CTX_free (handle->crypto.dec_mctx);
		handle->crypto.dec_mctx = NULL;
	}
#else
	if (handle->crypto.digest_encrypt_ctx != NULL) {
		EVP_MD_CTX_free (handle->crypto.digest_encrypt_ctx);
		handle->crypto.digest_encrypt_ctx = NULL;
	}

	if (handle->crypto.digest_decrypt_ctx != NULL) {
		EVP_MD_CTX_free (handle->crypto.digest_decrypt_ctx);
		handle->crypto.digest_decrypt_ctx = NULL;
	}
#endif

	handle->data_pos = 0;
	handle->data_used = 0;
	handle->message_len = 0;
	handle->message_end = 0;
	handle->error = MDF_ERR_NO_ERROR;

#if defined _WIN32 || defined _WIN64
	closesocket (handle->fd);
#else
	close (handle->fd);
#endif
	handle->fd = INVALID_SOCKET;

#ifdef HAVE_SYS_UN_H
	handle->copt &= ~COPT_ISPIPE;
#endif
	handle->copt &= ~(COPT_SENDDELAY|COPT_RECVDELAY);

	atomic_store_explicit (&handle->slock, 0, memory_order_release);

	call_status_callback (handle, MDF_STATUS_DISCONNECTED, handle->connected_host, handle->connected_ip);
}

static int read_int_msg (mdf_t handle, uint8_t *buf, ssize_t * restrict len, uint32_t * restrict pos, const size_t maxlen)
{
	size_t offset = 0;
	size_t toread = 0;

	do {
		ssize_t ret;
		fd_set fdset;
		struct timeval tv;

		FD_ZERO (&fdset);
		FD_SET (handle->fd, &fdset);

		tv.tv_sec = 10;
		tv.tv_usec = 0;

		ret = select (handle->fd + 1, &fdset, NULL, NULL, &tv);

		if (ret != 1)
			return -1;

		ret = recv (handle->fd, (char *) (buf + offset), maxlen - offset, 0);

		if (ret < 1)
			return -1;

		handle->bytes_read += ret;
		offset += ret;

		if (offset < 5)
			continue;

		if (toread == 0) {
			uint8_t i = 0;

			do {
				toread *= 128;
				toread += (buf[i] & 127) + 1;
			} while ((buf[i++] & 128) == 0 && i < 5);

			*len = toread + i;
			*pos = i;

			toread += i;

			if (toread > maxlen)
				return -1;

			toread -= offset;
		} else
			toread -= ret;
	} while (toread != 0);

	return 0;
}

static int send_greeting (mdf_t handle)
{
	/* As part of setting up the connection with the server, the client
	 * must initiate a crypto handshake with the server. The first
	 * message from the client will contain the mdf version (for book-
	 * keeping), a random nonce (Na), the minimum number of bytes that
	 * the client accepts for the DH p parameter, and a list of the
	 * digests and encryptions that we support. */
	ssize_t len = sizeof (MDF_VERSION) - 1;

	if (len > 255) {
		disconnect (handle);
		return 0;
	}

	ssize_t hostlen = sizeof (MDF_BUILD_HOST) - 1;

	if (hostlen > 255) {
		disconnect (handle);
		return 0;
	}

	uint8_t *msg1 = malloc (5 + 11 + sizeof (uint64_t) + sizeof (uint8_t) + len + sizeof (uint32_t) + EVP_MAX_MD_SIZE + sizeof (uint32_t) + ((NELEMS(DIGESTS) + NELEMS(CIPHERS)) * sizeof (uint32_t)) + sizeof (uint8_t) + hostlen);
	uint8_t *Na = malloc (EVP_MAX_MD_SIZE);

	uint32_t used = add_header (msg1 + 5, 0, 0) + 5;

	uint64_t used64 = cpu_to_le64 (handle->template_version);
	memcpy (msg1 + used, &used64, sizeof used64);
	used += sizeof (uint64_t);

	msg1[used++] = (uint8_t) len;

	memcpy (msg1 + used, MDF_VERSION, len);
	used += len;

	/* we send the largest Na that we can produce */
	if (RAND_bytes (Na, EVP_MAX_MD_SIZE) != 1) {
		free (Na);
		free (msg1);
		disconnect (handle);
		return 0;
	}

	uint32_t used32 = cpu_to_le32 (EVP_MAX_MD_SIZE);
	memcpy (msg1 + used, &used32, sizeof used32);
	used += sizeof (uint32_t);
	memcpy (msg1 + used, Na, EVP_MAX_MD_SIZE);
	used += EVP_MAX_MD_SIZE;

	used32 = cpu_to_le32 (MIN_DH_LENGTH);
	memcpy (msg1 + used, &used32, sizeof used32);
	used += sizeof (uint32_t);

	/* we add the list of our supported digest algorithms */
	uint32_t offset = used;
	used += sizeof (uint32_t);
	uint32_t tmp_u32 = 0;

	for (int i = 0; i < NELEMS (DIGESTS); i++) {
		if (handle->crypto.digests & (1 << DIGESTS[i].id)) {
			used32 = cpu_to_le32 (DIGESTS[i].id);
			memcpy (msg1 + used, &used32, sizeof used32);
			used += sizeof (uint32_t);
			tmp_u32++;
		}
	}

	if (tmp_u32 == 0) {
		free (Na);
		free (msg1);
		disconnect (handle);
		return 0;
	}

	used32 = cpu_to_le32 (tmp_u32);
	memcpy (msg1 + offset, &used32, sizeof used32);

	/* we add the list of supported encryption methods */
	offset = used;
	used += sizeof (uint32_t);
	tmp_u32 = 0;

	for (int i = 0; i < NELEMS (CIPHERS); i++) {
		if (handle->crypto.ciphers & (1 << CIPHERS[i].id)) {
			used32 = cpu_to_le32 (CIPHERS[i].id);
			memcpy (msg1 + used, &used32, sizeof used32);
			used += sizeof (uint32_t);
			tmp_u32++;
		}
	}

	if (tmp_u32 == 0) {
		free (Na);
		free (msg1);
		disconnect (handle);
		return 0;
	}

	used32 = cpu_to_le32 (tmp_u32);
	memcpy (msg1 + offset, &used32, sizeof used32);

	/* add the MDF_BUILD_HOST info */
	msg1[used++] = (uint8_t) hostlen;
	memcpy (msg1 + used, MDF_BUILD_HOST, hostlen);
	used += hostlen;

	int msg1_len = used - 5;

	offset = 5 - add_len (msg1, msg1_len);

	if (offset != 0) {
		memmove (msg1 + offset, msg1, 5 - offset);
		used -= offset;
	}

	len = send (handle->fd, (void *)(msg1 + offset), used, MSG_NOSIGNAL);

	if (len != used) {
		free (Na);
		free (msg1);
		disconnect (handle);
		return 0;
	}

	handle->bytes_written += len;

	/* We should now receive the first message from the server. This
	 * message will contain the public DH parameters (g, p), the
	 * server public DH key (X), a random nonce (Nb), the server
	 * public RSA key (e,n) and a signature from the "master" to
	 * authenticate the public RSA key, the hostname etc.
	 * Last in the message is a RSA signature created with the 
	 * server key of all bytes exchanged so far (by both parties).
	 *
	 * Since the signature covers the random nonce (Na) that the
	 * client generated, the client now knows two things:
	 *	1. The server has seen the first message from the client,
	 *	   so there was no man-in-the-middle fiddling with the
	 *	   message trying to impersonate as the client.
	 *
	 *	2. This message does indeed come from the server, it
	 *	   carries the servers signature and it is not part in
	 *	   any reply attack since Na was randomly chosen.
	*/
	uint8_t *buf = malloc (201728);

	if (read_int_msg (handle, buf, &len, &offset, 201728) == -1) {
		free (Na);
		free (buf);
		free (msg1);
		disconnect (handle);
		return 0;
	}

	int msg2_len = len - offset;

#if OPENSSL_VERSION_PREREQ(3,0)
	BIGNUM *bn1 = NULL;
	EVP_PKEY_CTX *pctx = NULL;
	EVP_PKEY *rsa_master = NULL, *rsa_server = NULL;
	EVP_PKEY *dh_param = NULL, *dh_host = NULL, *dh_peer = NULL;
	unsigned char *rsa_n = NULL, *rsa_e = NULL;
	unsigned char *dh_p = NULL, *dh_g = NULL, *dh_public = NULL;
	size_t dh_p_size, dh_g_size;
	uint32_t choosen_digest;
#else
	RSA *rsa_master = NULL, *rsa_server = NULL;
	DH *dh = DH_new ();
# if OPENSSL_VERSION_PREREQ(1,1)
	BIGNUM *bn1 = NULL, *bn2 = NULL;
# endif
#endif
	BIGNUM *X = NULL;
	EVP_MD_CTX *digest_ctx = NULL;
	EVP_MD_CTX *digest2_ctx = NULL;
	uint8_t *master_key = NULL, *Nb = NULL;

	if (len < 100)
		goto exit_in_error;

	const int msg2_offset = offset;

	if (buf[offset] != 0x00 || buf[offset + 1] != 0x00 || buf[offset + 2] != 0x03)
		goto exit_in_error;

	offset += 3;

	/* do we support the suggested encryption method? */

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

#if OPENSSL_VERSION_PREREQ(3,0)
	EVP_CIPHER *cipher = NULL;
#else
	const EVP_CIPHER *cipher = NULL;
#endif
	for (int i = 0; i < NELEMS (CIPHERS); i++) {
		if (CIPHERS[i].id == (uint32_t)used) {
			if (handle->crypto.ciphers & (1 << CIPHERS[i].id))
#if OPENSSL_VERSION_PREREQ(3,0)
				cipher = EVP_CIPHER_fetch (NULL, CIPHERS[i].name, NULL);
#else
				cipher = EVP_get_cipherbyname (CIPHERS[i].name);
#endif
			break;
		}
	}

#if OPENSSL_VERSION_PREREQ(3,0)
	handle->crypto.cipher = cipher;
#endif

	if (cipher == NULL)
		goto exit_in_error;

	handle->crypto.cipher_encrypt_ctx = EVP_CIPHER_CTX_new ();
	EVP_EncryptInit_ex (handle->crypto.cipher_encrypt_ctx, cipher, NULL, NULL, NULL);

	handle->crypto.cipher_decrypt_ctx = EVP_CIPHER_CTX_new ();
	EVP_DecryptInit_ex (handle->crypto.cipher_decrypt_ctx, cipher, NULL, NULL, NULL);

	/* do we support the suggested digest method? */
	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	handle->crypto.digest = NULL;

	for (int i = 0; i < NELEMS (DIGESTS); i++) {
		if (DIGESTS[i].id == (uint32_t)used) {
			if (handle->crypto.digests & (1 << DIGESTS[i].id)) {
#if OPENSSL_VERSION_PREREQ(3,0)
				handle->crypto.digest = EVP_MD_fetch (NULL, DIGESTS[i].name, NULL);
				choosen_digest = DIGESTS[i].id;
#else
				handle->crypto.digest = EVP_get_digestbyname (DIGESTS[i].name);
#endif
			}

			break;
		}
	}

	if (handle->crypto.digest == NULL)
		goto exit_in_error;

	digest_ctx = EVP_MD_CTX_create ();
	if (EVP_DigestInit_ex (digest_ctx, handle->crypto.digest, NULL) == 0)
		goto exit_in_error;

	handle->crypto.digest_length = EVP_MD_CTX_size (digest_ctx);

	/* since we now know the digest method of choice we can calculate what the
	 * digest of our first message should have been! */
	tmp_u32 = cpu_to_le32 (1); /* message no 1 */
	if (EVP_DigestUpdate (digest_ctx, &tmp_u32, sizeof tmp_u32) == 0)
		goto exit_in_error;

	tmp_u32 = cpu_to_le32 (msg1_len);
	if (EVP_DigestUpdate (digest_ctx, &tmp_u32, sizeof tmp_u32) == 0)
		goto exit_in_error;

	if (EVP_DigestUpdate (digest_ctx, msg1 + 5, msg1_len) == 0)
		goto exit_in_error;

	free (msg1);
	msg1 = NULL;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for g and p-len? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len)
		goto exit_in_error;

#if OPENSSL_VERSION_PREREQ(3,0)
	OSSL_PARAM params[4];
	bn1 = BN_bin2bn (buf + offset, used, NULL);
	dh_g_size = BN_num_bytes (bn1);
	dh_g = malloc (dh_g_size);
	BN_bn2nativepad (bn1, dh_g, dh_g_size);
#elif OPENSSL_VERSION_PREREQ(1,1)
	bn1 = BN_bin2bn (buf + offset, used, NULL);
#else
	dh->g = BN_bin2bn (buf + offset, used, dh->g);
#endif
	offset += used;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for p and X-len? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len)
		goto exit_in_error;

	/* make sure that p is within limits */
	if (used < MIN_DH_LENGTH || used > MAX_DH_LENGTH)
		goto exit_in_error;

#if OPENSSL_VERSION_PREREQ(3,0)
	bn1 = BN_bin2bn (buf + offset, used, bn1);
	dh_p_size = BN_num_bytes (bn1);
	dh_p = malloc (dh_p_size);
	BN_bn2nativepad (bn1, dh_p, dh_p_size);

	params[0] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_FFC_G, dh_g, dh_g_size);
	params[1] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_FFC_P, dh_p, dh_p_size);
	params[2] = OSSL_PARAM_construct_end ();

	if ((pctx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_fromdata_init (pctx) <= 0)
		goto exit_in_error;

	if (EVP_PKEY_fromdata (pctx, &dh_param, OSSL_KEYMGMT_SELECT_ALL, params) <= 0)
		goto exit_in_error;

	EVP_PKEY_CTX_free (pctx);
	pctx = NULL;
#elif OPENSSL_VERSION_PREREQ(1,1)
	bn2 = BN_bin2bn (buf + offset, used, NULL);

	if (DH_set0_pqg (dh, bn2, NULL, bn1) == 0)
		goto exit_in_error;

	/* clear these so that we don't do a double free on exit */
	bn1 = NULL;
	bn2 = NULL;
#else
	dh->p = BN_bin2bn (buf + offset, used, dh->p);
#endif

	offset += used;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for X and Nb-len? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len)
		goto exit_in_error;

	X = BN_bin2bn (buf + offset, used, NULL);
	offset += used;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for Nb and master siglen? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len || used < handle->crypto.digest_length)
		goto exit_in_error;

	Nb = malloc (used);
	memcpy (Nb, buf + offset, used);
	offset += used;

	const int Nb_len = used;

	rsa_master = mdf_int_load_rsapublickey ();

	if (rsa_master == NULL)
		goto exit_in_error;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);

#if OPENSSL_VERSION_PREREQ(3,0)
	const uint32_t rsa_size = EVP_PKEY_get_size (rsa_master);
#else
	const uint32_t rsa_size = RSA_size (rsa_master);
#endif

	/* do we have room for the master signature block? */
	if (offset + used + 1 < offset || offset + used + 1 > len || used <= rsa_size)
		goto exit_in_error;

	/* verify the master signature */
	unsigned char md[EVP_MAX_MD_SIZE];

	digest2_ctx = EVP_MD_CTX_create ();
	if (EVP_DigestInit_ex (digest2_ctx, handle->crypto.digest, NULL) == 0)
		goto exit_in_error;

	if (EVP_DigestUpdate (digest2_ctx, buf + offset, used - rsa_size) == 0)
		goto exit_in_error;

	if (EVP_DigestFinal_ex (digest2_ctx, md, NULL) == 0)
		goto exit_in_error;
#if OPENSSL_VERSION_PREREQ(3,0)
	if ((pctx = EVP_PKEY_CTX_new (rsa_master, NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_verify_init (pctx) <= 0)
		goto exit_in_error;

	/* OpenSSL v3 choose in their infinite wisdom to exclude these from the API */
	if (choosen_digest == 5 || choosen_digest == 10 || choosen_digest == 11 || choosen_digest == 12) {
		if (mdf_int_asn1_verify (pctx, buf + offset + (used - rsa_size), rsa_size, md, handle->crypto.digest_length, choosen_digest) <= 0)
			goto exit_in_error;
	} else {
		if (EVP_PKEY_CTX_set_signature_md (pctx, handle->crypto.digest) <= 0)
			goto exit_in_error;

		if (EVP_PKEY_verify (pctx, buf + offset + (used - rsa_size), rsa_size, md, handle->crypto.digest_length) <= 0)
			goto exit_in_error;
	}

	EVP_PKEY_CTX_free (pctx);
	pctx = NULL;
#else
	if (RSA_verify (EVP_MD_CTX_type (digest2_ctx), md, handle->crypto.digest_length, buf + offset + (used - rsa_size), rsa_size, rsa_master) == 0)
		goto exit_in_error;
#endif

	offset += sizeof (uint32_t);

	/* verify the hostname from the master signature */
	used = 0;
	for (tmp_u32 = buf[offset++]; tmp_u32 != 0; tmp_u32--) {
		if (offset + buf[offset] + 1 > len)
			goto exit_in_error;

		if (buf[offset] == strlen (handle->connected_host) && memcmp (buf + offset + 1, handle->connected_host, buf[offset]) == 0)
			used = 1;
		else if (buf[offset] == strlen (handle->connected_ip) && memcmp (buf + offset + 1, handle->connected_ip, buf[offset]) == 0)
			used = 1;

		offset += buf[offset] + 1;
	}

	if (used == 0)
		goto exit_in_error;

	/* do we have room for the expiry time and e-len? */
	if (offset + (int)(sizeof (int64_t) + sizeof (uint32_t)) < offset || offset + (int)(sizeof (int64_t) + sizeof (uint32_t)) > len)
		goto exit_in_error;

	/* has the signature expired? */
	int64_t expiry;
	memcpy (&expiry, buf + offset, sizeof expiry);
	expiry = le64_to_cpu (expiry);

	if (time (NULL) >= expiry)
		goto exit_in_error;

	offset += sizeof (int64_t);

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for e, n-len and is e of the appropiate size? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len || used > OPENSSL_RSA_MAX_PUBEXP_BITS * 8 || used < 1)
		goto exit_in_error;

#if OPENSSL_VERSION_PREREQ(3,0)
	bn1 = BN_bin2bn (buf + offset, used, bn1);
	rsa_e = malloc (BN_num_bytes (bn1));
	BN_bn2nativepad (bn1, rsa_e, BN_num_bytes (bn1));
	params[0] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_RSA_E, rsa_e, BN_num_bytes (bn1));
#else
	rsa_server = RSA_new ();
# if OPENSSL_VERSION_PREREQ(1,1)
	bn1 = BN_bin2bn (buf + offset, used, NULL);
# else
	rsa_server->e = BN_bin2bn (buf + offset, used, rsa_server->e);
# endif
#endif
	offset += used;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for n, signature-len and is n of the appropiate size? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len || used > OPENSSL_RSA_MAX_MODULUS_BITS * 8 || used < 256)
		goto exit_in_error;

#if OPENSSL_VERSION_PREREQ(3,0)
	bn1 = BN_bin2bn (buf + offset, used, bn1);
	rsa_n = malloc (BN_num_bytes (bn1));
	BN_bn2nativepad (bn1, rsa_n, BN_num_bytes (bn1));

	params[1] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_RSA_N, rsa_n, BN_num_bytes (bn1));
	params[2] = OSSL_PARAM_construct_end ();

	if ((pctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_fromdata_init (pctx) <= 0)
		goto exit_in_error;

	if (EVP_PKEY_fromdata (pctx, &rsa_server, EVP_PKEY_PUBLIC_KEY, params) <= 0)
		goto exit_in_error;

	EVP_PKEY_CTX_free (pctx);
	pctx = NULL;
#elif OPENSSL_VERSION_PREREQ(1,1)
	bn2 = BN_bin2bn (buf + offset, used, NULL);

	if (RSA_set0_key (rsa_server, bn2, bn1, NULL) == 0)
		goto exit_in_error;

	/* clear these so that we don't do a double free on exit */
	bn1 = NULL;
	bn2 = NULL;
#else
	rsa_server->n = BN_bin2bn (buf + offset, used, rsa_server->n);
#endif
	offset += used;

	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	/* do we have room for the signature and the sig-len, and is the signature of the correct size? */
	if (offset + used + (int)sizeof (uint32_t) < offset || offset + used + (int)sizeof (uint32_t) > len || used != rsa_size)
		goto exit_in_error;

	/* ok, come this far we have now verified that we have got a valid RSA key from the server.
	 * We know that it hasn't expired, that it's hostname matches that to which we are connected to,
	 * and that the master signature was valid. */

	offset += used;
#if OPENSSL_VERSION_PREREQ(3,0)
	EVP_PKEY_free (rsa_master);
#else
	RSA_free (rsa_master);
#endif
	rsa_master = NULL;

	/* let's look at the server's signature */
	memcpy (&used32, buf + offset, sizeof used32);
	used = le32_to_cpu (used32);
	offset += sizeof (uint32_t);

	tmp_u32 = cpu_to_le32 (2); /* message no 2 */
	if (EVP_DigestUpdate (digest_ctx, &tmp_u32, sizeof tmp_u32) == 0)
		goto exit_in_error;

	tmp_u32 = cpu_to_le32 (msg2_len);
	if (EVP_DigestUpdate (digest_ctx, &tmp_u32, sizeof tmp_u32) == 0)
		goto exit_in_error;

	if (EVP_DigestUpdate (digest_ctx, buf + msg2_offset, offset - msg2_offset) == 0)
		goto exit_in_error;

	/* do we have room for the signature, and is the signature the correct size? */
	if (offset + used < offset || offset + used > len || used != rsa_size)
		goto exit_in_error;

	/* we make a copy of the CTX since we need to send an AUTH for the
	 * client as well and EVP_DigestFinal_ex() will destroy the context */
	EVP_MD_CTX_copy (digest2_ctx, digest_ctx);
	if (EVP_DigestFinal_ex (digest_ctx, md, NULL) == 0)
		goto exit_in_error;

#if OPENSSL_VERSION_PREREQ(3,0)
	if ((pctx = EVP_PKEY_CTX_new (rsa_server, NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_verify_init (pctx) <= 0)
		goto exit_in_error;

	/* OpenSSL v3 choose in their infinite wisdom to exclude these from the API */
	if (choosen_digest == 5 || choosen_digest == 10 || choosen_digest == 11 || choosen_digest == 12) {
		if (mdf_int_asn1_verify (pctx, buf + offset, used, md, handle->crypto.digest_length, choosen_digest) <= 0)
			goto exit_in_error;
	} else {
		if (EVP_PKEY_CTX_set_signature_md (pctx, handle->crypto.digest) <= 0)
			goto exit_in_error;

		if (EVP_PKEY_verify (pctx, buf + offset, used, md, handle->crypto.digest_length) <= 0)
			goto exit_in_error;
	}

	EVP_PKEY_CTX_free (pctx);
	pctx = NULL;

	EVP_PKEY_free (rsa_server);
	rsa_server = NULL;

	free (rsa_e);
	rsa_e = NULL;

	free (rsa_n);
	rsa_n = NULL;
#else
	if (RSA_verify (EVP_MD_CTX_type (digest_ctx), md, handle->crypto.digest_length, buf + offset, used, rsa_server) == 0)
		goto exit_in_error;

	RSA_free (rsa_server);
	rsa_server = NULL;
#endif
	/* the signature is included in the client AUTH */
	if (EVP_DigestUpdate (digest2_ctx, buf + offset, used) == 0)
		goto exit_in_error;

	/* we check that p is a safe prime and that g is a suitable generator,
	 * since we wouldn't want to be catched with our pants down. And also
	 * we do this _after_ we checked the RSA signatures so we have at least
	 * some guarantees that we haven't received values that will keep us up
	 * all night performing the check, or whatever nasty ugliness that could
	 * have happened. */
#if OPENSSL_VERSION_PREREQ(3,0)
	if ((pctx = EVP_PKEY_CTX_new_from_pkey (NULL, dh_param, NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_param_check (pctx) <= 0)
		goto exit_in_error;

	if (EVP_PKEY_keygen_init (pctx) <= 0)
		goto exit_in_error;

	if (EVP_PKEY_generate (pctx, &dh_host) <= 0)
		goto exit_in_error;

	EVP_PKEY_CTX_free (pctx);

	EVP_PKEY_free (dh_param);
	dh_param = NULL;

	dh_public = malloc (BN_num_bytes (X));
	BN_bn2nativepad (X, dh_public, BN_num_bytes (X));

	params[0] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_FFC_G, dh_g, dh_g_size);
	params[1] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_FFC_P, dh_p, dh_p_size);
	params[2] = OSSL_PARAM_construct_BN (OSSL_PKEY_PARAM_PUB_KEY, dh_public, BN_num_bytes (X));
	params[3] = OSSL_PARAM_construct_end ();

	if ((pctx = EVP_PKEY_CTX_new_from_name (NULL, "DHX", NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_fromdata_init (pctx) <= 0)
		goto exit_in_error;

	if (EVP_PKEY_fromdata (pctx, &dh_peer, OSSL_KEYMGMT_SELECT_ALL, params) <= 0)
		goto exit_in_error;

	EVP_PKEY_CTX_free (pctx);

	if ((pctx = EVP_PKEY_CTX_new_from_pkey (NULL, dh_host, NULL)) == NULL)
		goto exit_in_error;

	if (EVP_PKEY_derive_init (pctx) <= 0)
		goto exit_in_error;

	if (EVP_PKEY_derive_set_peer (pctx, dh_peer) <= 0)
		goto exit_in_error;

	size_t keylen;
	if (EVP_PKEY_derive (pctx, NULL, &keylen) <= 0)
		goto exit_in_error;

	master_key = malloc (keylen);

	if (EVP_PKEY_derive (pctx, master_key, &keylen) <= 0)
		used = 0;

	free (dh_public);
	dh_public = NULL;

	EVP_PKEY_free (dh_peer);
	dh_peer = NULL;

	EVP_PKEY_CTX_free (pctx);
	pctx = NULL;

	free (dh_p);
	dh_p = NULL;

	free (dh_g);
	dh_g = NULL;
#else
	int codes;
	if (DH_check (dh, &codes) == 0)
		goto exit_in_error;

	if (DH_generate_key (dh) == 0)
		goto exit_in_error;

	size_t keylen = DH_size (dh);
	master_key = malloc (keylen);

	if (DH_compute_key (master_key, X, dh) <= 0)
		used = 0;
#endif
	if (used == 0) {
		memset (master_key, 0x00, keylen);
		*(volatile char *)master_key = *(volatile char *)master_key; /* prevent the optimizer from removing the memset call */
		free (master_key);
		goto exit_in_error;
	}

	BN_free (X);
	X = NULL;

	handle->crypto.master_secret = malloc (handle->crypto.digest_length);

	if (EVP_DigestInit_ex (digest_ctx, handle->crypto.digest, NULL) == 0) {
		memset (master_key, 0x00, used);
		free (master_key);
		goto exit_in_error;
	}

	if (EVP_DigestUpdate (digest_ctx, master_key, keylen) == 0) {
		memset (master_key, 0x00, keylen);
		*(volatile char *)master_key = *(volatile char *)master_key; /* prevent the optimizer from removing the memset call */
		free (master_key);
		goto exit_in_error;
	}

	memset (master_key, 0x00, keylen);
	*(volatile char *)master_key = *(volatile char *)master_key; /* prevent the optimizer from removing the memset call */
	free (master_key);

	if (EVP_DigestFinal_ex (digest_ctx, handle->crypto.master_secret, NULL) == 0)
		goto exit_in_error;

	EVP_MD_CTX_free (digest_ctx);
	digest_ctx = NULL;

#if OPENSSL_VERSION_PREREQ(3,0)
	handle->crypto.enc_mctx = EVP_MAC_CTX_new (handle->mac);
	handle->crypto.dec_mctx = EVP_MAC_CTX_new (handle->mac);
#else
	handle->crypto.digest_encrypt_ctx = EVP_MD_CTX_create ();
	handle->crypto.digest_decrypt_ctx = EVP_MD_CTX_create ();
#endif

	if (mdf_int_set_keys (&handle->crypto, ENCRYPT, Na, EVP_MAX_MD_SIZE) == 0)
		goto exit_in_error;

	if (mdf_int_set_keys (&handle->crypto, DECRYPT, Nb, Nb_len) == 0)
		goto exit_in_error;

	free (Na);
	Na = NULL;

	free (Nb);
	Nb = NULL;

	/* Now we construct the final packet in our crypto handshake,
	 * this message will contain the clients public DH key and the
	 * client AUTH encrypted with the client encryption key.
	 *
	 * This AUTH will not authenticate the client as such (the server
	 * does not know which client it is until the client sends the 
	 * username/password credentials, and the client lacks a RSA key.
	 *
	 * No, the reason for the AUTH is to complete the handshake and to
	 * make sure that the server has a secure channel open with "someone"
	 * and that this "someone" was the very same entity who sent the very
	 * first message in the handshake. This is not as bad as it sounds,
	 * this is exactly like secure web browsing (aka SSL/TLS).
	 *
	 * The client on the other had does know that it is communicating
	 * with the server due to the RSA signature, and this is what is
	 * really important since it is the client who will have to send
	 * its password over the wire, and we better make sure it's to no
	 * stranger. */
	used = 5;

	used += add_header (buf + used, 0, 4);

#if OPENSSL_VERSION_PREREQ(3,0)
	if (EVP_PKEY_get_bn_param (dh_host, OSSL_PKEY_PARAM_PUB_KEY, &bn1) <= 0)
		goto exit_in_error;

	len = BN_bn2bin (bn1, buf + used + sizeof (uint32_t));
#elif OPENSSL_VERSION_PREREQ(1,1)
	const BIGNUM *dh_pub, *dh_priv;
	DH_get0_key (dh, &dh_pub, &dh_priv);
	len = BN_bn2bin (dh_pub, buf + used + sizeof (uint32_t));
#else
	len = BN_bn2bin (dh->pub_key, buf + used + sizeof (uint32_t));
#endif

	used32 = cpu_to_le32 (len);
	memcpy (buf + used, &used32, sizeof used32);
	used += sizeof (uint32_t) + len;

#if OPENSSL_VERSION_PREREQ(3,0)
	BN_free (bn1);
	bn1 = NULL;

	EVP_PKEY_free (dh_host);
	dh_host = NULL;
#else
	DH_free (dh);
	dh = NULL;
#endif

	used32 = cpu_to_le32 (handle->crypto.digest_length);
	memcpy (buf + used, &used32, sizeof used32);
	used += sizeof (uint32_t);

	/* len is now as far as we have to calculate AUTH, we have to
	 * perform this little tap dance since we need the message length
	 * attached to be able to calculate the correct message digest */
	len = used;
	used += handle->crypto.digest_length;

	tmp_u32 = cpu_to_le32 (3); /* message no 3 */

	if (EVP_DigestUpdate (digest2_ctx, &tmp_u32, sizeof tmp_u32) == 0)
		goto exit_in_error;

	tmp_u32 = cpu_to_le32 (used - 5);

	if (EVP_DigestUpdate (digest2_ctx, &tmp_u32, sizeof tmp_u32) == 0)
		goto exit_in_error;

	if (EVP_DigestUpdate (digest2_ctx, buf + 5, len - 5) == 0)
		goto exit_in_error;

	offset = 5 - add_len (buf, used - 5);

	if (offset != 0) {
		memmove (buf + offset, buf, 5 - offset);
		used -= offset;
	}

	if (EVP_DigestFinal_ex (digest2_ctx, buf + len, NULL) == 0)
		goto exit_in_error;

	EVP_MD_CTX_free (digest2_ctx);
	digest2_ctx = NULL;

	int outlen;
	if (EVP_EncryptUpdate (handle->crypto.cipher_encrypt_ctx, buf + len, &outlen, buf + len, handle->crypto.digest_length) == 0 || (uint32_t)outlen != handle->crypto.digest_length)
		goto exit_in_error;

	handle->crypto.encrypt_cnt++;
	handle->crypto.encrypt_num += outlen;

	len = send (handle->fd, (void *)(buf + offset), used, MSG_NOSIGNAL);

	if (len != used)
		goto exit_in_error;

	handle->bytes_written += len;
	handle->crypto.cstate = CSTATE_ENCRYPT | CSTATE_DECRYPT;
	free (buf);
	return 1;

exit_in_error:
	disconnect (handle);

#if OPENSSL_VERSION_PREREQ(3,0)
	if (rsa_master != NULL)
 		EVP_PKEY_free (rsa_master);

	if (rsa_server != NULL)
		EVP_PKEY_free (rsa_server);

	if (dh_param != NULL)
		EVP_PKEY_free (dh_param);

	if (dh_host != NULL)
		EVP_PKEY_free (dh_host);

	if (dh_peer != NULL)
		EVP_PKEY_free (dh_peer);

	if (pctx != NULL)
		EVP_PKEY_CTX_free (pctx);

	if (rsa_n != NULL)
		free (rsa_n);

	if (rsa_e != NULL)
		free (rsa_e);

	if (dh_p != NULL)
		free (dh_p);

	if (dh_g != NULL)
		free (dh_g);

	if (dh_public != NULL)
		free (dh_public);
#else
	if (rsa_master != NULL)
		RSA_free (rsa_master);

	if (rsa_server != NULL)
		RSA_free (rsa_server);

	if (dh != NULL)
		DH_free (dh);
#endif

	if (X != NULL)
		BN_free (X);

	if (digest_ctx != NULL)
		EVP_MD_CTX_free (digest_ctx);

	if (digest2_ctx != NULL)
		EVP_MD_CTX_free (digest2_ctx);

#if OPENSSL_VERSION_PREREQ(1,1)
	if (bn1 != NULL)
		BN_free (bn1);

# if !OPENSSL_VERSION_PREREQ(3,0)
	if (bn2 != NULL)
		BN_free (bn2);
# endif
#endif

	free (Na);
	free (Nb);
	free (buf);
	free (msg1);
	return 0;
}

static int should_we_rekey (mdf_t handle)
{
	if ((handle->crypto.cstate & CSTATE_ENCRYPT) != CSTATE_ENCRYPT)
		return 1;

	if (handle->crypto.encrypt_cnt == MAX_ENCRYPTED_MESSAGES_PER_KEY) {
		return mdf_int_send_rekey (handle);
	} else if (handle->crypto.encrypt_num >= MAX_ENCRYPTED_BYTES) {
		handle->crypto.encrypt_num = 0;
		return mdf_int_send_rekey (handle);
	}

	return 1;
}

static int encrypt_and_send (mdf_t handle, uint8_t *buf, size_t len, size_t offset)
{
	if ((handle->copt & COPT_SENDDELAY) == COPT_SENDDELAY)
		buf[len++] = 0x00;

	if ((handle->crypto.cstate & CSTATE_ENCRYPT) == CSTATE_ENCRYPT) {
		const uint32_t tmp_u32 = cpu_to_le32 (len + handle->crypto.digest_length);
		const uint64_t tmp_u64 = cpu_to_le64 (handle->crypto.encrypt_cnt);
		int rc = 1;

#if OPENSSL_VERSION_PREREQ(3,0)
		EVP_MAC_CTX *mctx = EVP_MAC_CTX_dup (handle->crypto.enc_mctx);
		EVP_MAC_update (mctx, (const unsigned char *) &tmp_u64, sizeof tmp_u64);
		EVP_MAC_update (mctx, (const unsigned char *) &tmp_u32, sizeof tmp_u32);
		EVP_MAC_update (mctx, buf, len);

		size_t siglen = EVP_MAX_MD_SIZE;
		EVP_MAC_final (mctx, buf + len, &siglen, siglen);
		EVP_MAC_CTX_free (mctx);
#else
		if (EVP_DigestSignInit (handle->crypto.digest_encrypt_ctx, NULL, handle->crypto.digest, NULL, NULL) == 0)
			rc = 0;

		if (EVP_DigestSignUpdate (handle->crypto.digest_encrypt_ctx, (const unsigned char *) &tmp_u64, sizeof tmp_u64) == 0)
			rc = 0;

		if (EVP_DigestSignUpdate (handle->crypto.digest_encrypt_ctx, (const unsigned char *) &tmp_u32, sizeof tmp_u32) == 0)
			rc = 0;

		if (EVP_DigestSignUpdate (handle->crypto.digest_encrypt_ctx, buf, len) == 0)
			rc = 0;

		size_t siglen = EVP_MAX_MD_SIZE;
		if (EVP_DigestSignFinal (handle->crypto.digest_encrypt_ctx, buf + len, &siglen) == 0)
			rc = 0;
#endif
		len += handle->crypto.digest_length;

		int outlen;
		if (EVP_EncryptUpdate (handle->crypto.cipher_encrypt_ctx, buf + offset, &outlen, buf + offset, len - offset) == 0)
			rc = 0;

		handle->crypto.encrypt_cnt++;
		handle->crypto.encrypt_num += outlen;

		if (rc == 0) {
			disconnect (handle);
			handle->error = MDF_ERR_DISCONNECTED;
			return -1;
		}
	}

	ssize_t written = send (handle->fd, (void *)buf, len, MSG_NOSIGNAL);

	if (written == -1) {
		disconnect (handle);
		handle->error = MDF_ERR_DISCONNECTED;
		return -1;
	}

	handle->bytes_written += written;

	if (should_we_rekey (handle) == 0) {
		disconnect (handle);
		handle->error = MDF_ERR_DISCONNECTED;
		return -1;
	}

	return 0;
}

int mdf_int_send_rekey (mdf_t handle)
{
	/* we don't support rekeyings on non encryption tunnels */
	if ((handle->crypto.cstate & CSTATE_ENCRYPT) != CSTATE_ENCRYPT)
		return 1;

	int len;

	if ((handle->copt & COPT_NOENCRYPTION) == COPT_NOENCRYPTION)
		len = 3 + sizeof (uint32_t);
	else
		len = 3 + sizeof (uint32_t) + EVP_MAX_MD_SIZE;

	uint8_t buf[5 + 11 + sizeof (uint32_t) + EVP_MAX_MD_SIZE + EVP_MAX_MD_SIZE + 1];

	size_t offset = add_len (buf, len);
	uint8_t *p = buf + offset;

	p += add_header (p, 0, 5);

	uint8_t *Na = NULL;

	if ((handle->copt & COPT_NOENCRYPTION) == COPT_NOENCRYPTION) {
		memset (p, 0, sizeof (uint32_t));
		p += sizeof (uint32_t);
	} else {
		uint32_t val = cpu_to_le32 (EVP_MAX_MD_SIZE);
		memcpy (p, &val, sizeof val);
		p += sizeof (uint32_t);

		Na = malloc (EVP_MAX_MD_SIZE);

		if (RAND_bytes (Na, EVP_MAX_MD_SIZE) != 1) {
			free (Na);
			disconnect (handle);
			return 0;
		}

		memcpy (p, Na, EVP_MAX_MD_SIZE);
		p += EVP_MAX_MD_SIZE;
	}

	int ret = encrypt_and_send (handle, buf, p - buf, offset);

	if (ret != 0) {
		free (Na);
		disconnect (handle);
		return 0;
	}

	if ((handle->copt & COPT_NOENCRYPTION) == COPT_NOENCRYPTION) {
		handle->crypto.cstate &= ~CSTATE_ENCRYPT;

		EVP_CIPHER_CTX_free (handle->crypto.cipher_encrypt_ctx);
		handle->crypto.cipher_encrypt_ctx = NULL;

#if OPENSSL_VERSION_PREREQ(3,0)
		EVP_MAC_CTX_free (handle->crypto.enc_mctx);
		handle->crypto.enc_mctx = NULL;
#else
		EVP_MD_CTX_free (handle->crypto.digest_encrypt_ctx);
		handle->crypto.digest_encrypt_ctx = NULL;
#endif
		return 1;
	}

	int rc = 1;

	if (mdf_int_set_keys (&handle->crypto, ENCRYPT, Na, EVP_MAX_MD_SIZE) == 0) {
		disconnect (handle);
		rc = 0;
	}

	free (Na);
	return rc;
}

int mdf_int_send_heartbeat_response (mdf_t handle, const uint64_t timestamp, const uint64_t timestamp_rcv)
{
	uint8_t buf[12 + EVP_MAX_MD_SIZE];

	size_t offset = add_len (buf, 11);
	uint8_t *p = buf + offset;

	p += add_header (p, 0, 2);

	uint64_t val = cpu_to_le64 (timestamp);
	memcpy (p, &val, sizeof val);
	p += 8;

	int ret = encrypt_and_send (handle, buf, p - buf, offset);

	if (ret != 0)
		return ret;

	char timestamp_str[21], timestamp_rcv_str[21];
	sprintf (timestamp_str, "%" PRIu64, timestamp);
	sprintf (timestamp_rcv_str, "%" PRIu64, timestamp_rcv);
	call_status_callback (handle, MDF_STATUS_RCV_HB_REQ, timestamp_str, timestamp_rcv_str);

	return ret;
}

static int request_heartbeat (mdf_t handle)
{
	int expected = 0;
	while (!atomic_compare_exchange_weak_explicit (&handle->slock, &expected, 1, memory_order_acquire, memory_order_relaxed))
		expected = 0;

	uint8_t buf[12 + EVP_MAX_MD_SIZE];

	size_t offset = add_len (buf, 11);
	uint8_t *p = buf + offset;

	p += add_header (p, 0, 1);

	struct timespec ts;
	clock_gettime (CLOCK_REALTIME, &ts);
	uint64_t timestamp = ((uint64_t)ts.tv_sec) * __UINT64_C(1000000000) + (uint64_t) ts.tv_nsec;

	char timestamp_str[21];
	sprintf (timestamp_str, "%" PRIu64, timestamp);

	timestamp = cpu_to_le64 (timestamp);
	memcpy (p, &timestamp, sizeof timestamp);
	p += 8;

	int ret = encrypt_and_send (handle, buf, p - buf, offset);

	atomic_store_explicit (&handle->slock, 0, memory_order_release);

	if (ret != 0)
		return ret;

	call_status_callback (handle, MDF_STATUS_SND_HB_REQ, timestamp_str, NULL);

	return ret;
}

static int mdf_int_set_delaymode (mdf_t handle)
{
	uint8_t buf[12 + EVP_MAX_MD_SIZE];

	size_t offset = add_len (buf, 3);
	uint8_t *p = buf + offset;

	p += add_header (p, 0, 7);

	int expected = 0;
	while (!atomic_compare_exchange_weak_explicit (&handle->slock, &expected, 1, memory_order_acquire, memory_order_relaxed))
		expected = 0;

	int ret = encrypt_and_send (handle, buf, p - buf, offset);
	handle->copt |= COPT_SENDDELAY;

	atomic_store_explicit (&handle->slock, 0, memory_order_release);

	if (ret != 0)
		return ret;

	return ret;
}

#if defined _WIN32 || defined _WIN64
static int is_connected (SOCKET fd, fd_set *rdevents, fd_set *wrevents, fd_set *exevents)
{
	WSASetLastError (0);

	if (!FD_ISSET (fd, rdevents) && !FD_ISSET (fd, wrevents))
		return 0;

	if (FD_ISSET (fd, exevents))
		return 0;

	return 1;
}
#else
static int is_connected (int fd, fd_set *rdevents, fd_set *wrevents, fd_set *exevents)
{
	int err;
	socklen_t len = sizeof err;

	(void)exevents;

	errno = 0;

	if (!FD_ISSET (fd, rdevents) && !FD_ISSET (fd, wrevents))
		return 0;

	if (getsockopt (fd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
		return 0;

	errno = err;

	return err == 0 ? 1 : 0;
}
#endif

static int try_connect (mdf_t handle, struct addrinfo *addr)
{
	fd_set rdevents;
	fd_set wrevents;
	fd_set exevents;
	struct timeval tv;
	int ret;

#if defined _WIN32 || defined _WIN64
	unsigned long val = 1;
#else
	int val;
#endif

	handle->fd = socket (addr->ai_family, addr->ai_socktype | SOCK_CLOEXEC, addr->ai_protocol);

	if (handle->fd == INVALID_SOCKET)
		return 0;

	/* set non-blocking */
#if defined _WIN32 || defined _WIN64
	if (ioctlsocket (handle->fd, FIONBIO, &val) == SOCKET_ERROR) {
		closesocket (handle->fd);
		handle->fd = INVALID_SOCKET;
		return 0;
	}
#else
	val = fcntl (handle->fd, F_GETFL, 0);

	if (val == -1) {
		close (handle->fd);
		handle->fd = INVALID_SOCKET;
		return 0;
	}

	if (fcntl (handle->fd, F_SETFL, val | O_NONBLOCK) == -1) {
		close (handle->fd);
		handle->fd = INVALID_SOCKET;
		return 0;
	}
#endif
	if (handle->bind_address != NULL) {
		struct addrinfo hint;
		struct addrinfo *res;

		memset (&hint, 0, sizeof hint);

		hint.ai_flags    = AI_ADDRCONFIG | AI_NUMERICSERV;
		hint.ai_family   = addr->ai_family;
		hint.ai_socktype = addr->ai_socktype;
		hint.ai_protocol = addr->ai_protocol;

		if (getaddrinfo (handle->bind_address, "0", &hint, &res) != 0) {
#if defined _WIN32 || defined _WIN64
			closesocket (handle->fd);
#else
			close (handle->fd);
#endif
			handle->fd = INVALID_SOCKET;
			return 0;
		}

		if (bind (handle->fd, res->ai_addr, res->ai_addrlen) != 0) {
			freeaddrinfo (res);
#if defined _WIN32 || defined _WIN64
			closesocket (handle->fd);
#else
			close (handle->fd);
#endif
			handle->fd = INVALID_SOCKET;
			return 0;
		}

		freeaddrinfo (res);
	}

	ret = connect (handle->fd, addr->ai_addr, addr->ai_addrlen);

#if defined _WIN32 || defined _WIN64
	if (ret == SOCKET_ERROR) {
		if (WSAGetLastError () != WSAEWOULDBLOCK) {
			closesocket (handle->fd);
			handle->fd = INVALID_SOCKET;
			return 0;
		}
#else
	if (ret == -1) {
		if (errno != EINPROGRESS) {
			close (handle->fd);
			handle->fd = INVALID_SOCKET;
			return 0;
		}
#endif
		FD_ZERO (&rdevents);
		FD_SET (handle->fd, &rdevents);

		wrevents = rdevents;
		exevents = rdevents;

		tv.tv_sec = handle->connect_timeout;
		tv.tv_usec = 0;

		if (select (handle->fd + 1, &rdevents, &wrevents, &exevents, &tv) < 1) {
#if defined _WIN32 || defined _WIN64
			closesocket (handle->fd);
#else
			close (handle->fd);
#endif
			handle->fd = INVALID_SOCKET;
			return 0;
		}

		if (is_connected (handle->fd, &rdevents, &wrevents, &exevents) == 0) {
#if defined _WIN32 || defined _WIN64
			closesocket (handle->fd);
#else
			close (handle->fd);
#endif
			handle->fd = INVALID_SOCKET;
			return 0;
		}
	}

	/* set socket back to blocking */
#if defined _WIN32 || defined _WIN64
	val = 0;
	ioctlsocket (handle->fd, FIONBIO, &val);
#else
	fcntl (handle->fd, F_SETFL, val);
#endif

	return 1;
}

static int try_host (mdf_t handle, const char * restrict host, const char * restrict port)
{
	call_status_callback (handle, MDF_STATUS_LOOKUP, host, NULL);

	struct addrinfo *res;
	struct addrinfo hint = {
		.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV,
		.ai_family = AF_UNSPEC,
		.ai_socktype = SOCK_STREAM,
		.ai_protocol = IPPROTO_TCP
	};

	if (getaddrinfo (host, port, &hint, &res) != 0)
		return 0;

	for (struct addrinfo *r = res; r != NULL; r = r->ai_next) {
		char buf[50];
		char *ip = NULL;

		if (getnameinfo (r->ai_addr, r->ai_addrlen, buf, sizeof buf, NULL, 0, NI_NUMERICHOST) == 0)
			ip = buf;

		call_status_callback (handle, MDF_STATUS_CONNECTING, host, ip);

		if (try_connect (handle, r) == 1) {
			call_status_callback (handle, MDF_STATUS_CONNECTED, host, ip);
			freeaddrinfo (res);

			free (handle->connected_host);
			handle->connected_host = strdup (host);

			free (handle->connected_ip);
			handle->connected_ip = strdup (ip);

/* disable SIGPIPE on macOS */
#ifdef SO_NOSIGPIPE
			int val = 1;
			setsockopt (handle->fd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&val, sizeof val);
#endif
			return 1;
		}
	}

	freeaddrinfo (res);

	return 0;
}

#ifdef HAVE_SYS_UN_H
static int try_unix (mdf_t handle, const char *path)
{
	union { struct sockaddr_un un; struct sockaddr addr; } addr;
	int ret;

	if (strlen (path) + 1 > sizeof addr.un.sun_path)
		return 0;

	handle->fd = socket (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);

	if (handle->fd == INVALID_SOCKET)
		return 0;

	memset (&addr.un, 0, sizeof addr.un);

	addr.un.sun_family = AF_UNIX;
	strcpy (addr.un.sun_path, path);

	ret = connect (handle->fd, &addr.addr, sizeof addr.un);

	if (ret == -1) {
		close (handle->fd);
		handle->fd = INVALID_SOCKET;
		return 0;
	}

	call_status_callback (handle, MDF_STATUS_CONNECTED, path, path);

	free (handle->connected_host);
	handle->connected_host = strdup (path);

	free (handle->connected_ip);
	handle->connected_ip = strdup (path);

	handle->crypto.cstate = 0; /* No need to encrypt data on AF_UNIX */
	return 1;
}
#endif

LIBMDF_API int mdf_connect (mdf_t handle, const char *servers)
{
	if (handle == NULL)
		return 0;

	if (servers == NULL) {
		handle->error = MDF_ERR_ARGUMENT;
		return 0;
	}

	/* if we already are connected we must fail */
	if (handle->fd != INVALID_SOCKET) {
		handle->error = MDF_ERR_CONNECTED;
		return 0;
	}

	handle->crypto.cstate = CSTATE_INVALID;

	char *s = strdup (servers);

	/* parse the servers string */
	char *e, *port;
	for (char *p = s; *p != '\0'; p = e) {
		while (isspace ((unsigned char) *p) != 0 || *p == ',')
			++p;

		/* IPv6 */
		if (*p == '[') {
			++p;

			port = strchr (p, ']');

			if (port == NULL) {
				free (s);
				handle->error = MDF_ERR_ARGUMENT;
				return 0;
			}

			*port++ = '\0';
#ifdef HAVE_SYS_UN_H
		} else if (*p == '/') { /* Unix Domain Sockets */
			e = strchr (p, ',');

			if (e != NULL)
				*e++ = '\0';

			if (try_unix (handle, p) == 1) {
				if ((handle->copt & COPT_HANDLEDELAY) == COPT_HANDLEDELAY) {
					if (mdf_int_set_delaymode (handle) != 0) {
						disconnect (handle);
						continue;
					}
				}

				free (s);
				handle->time_lastupdate = time (NULL);
				handle->heartbeats_sent = 0;
				handle->copt |= COPT_ISPIPE;
				call_status_callback (handle, MDF_STATUS_READYTOLOGON, handle->connected_host, handle->connected_ip);
				return 1;
			}

			if (e == NULL)
				break;

			continue;
#endif
		} else { /* IPv4 or DNS name */
			port = strchr (p, ':');

			if (port == NULL) {
				free (s);
				handle->error = MDF_ERR_ARGUMENT;
				return 0;
			}
		}

		/* the format must be host:port */
		if (*port != ':') {
			free (s);
			handle->error = MDF_ERR_ARGUMENT;
			return 0;
		}

		*port++ = '\0';

		/* scan to the end of the port, yes this code does not perform
		 * all the checks that it could do, i.e we do not check if the port
		 * was written like host:1234newhost, but it is futile to try every
		 * possible error, we should just be happy that we at least do not
		 * crash ;) */
		for (e = port; isdigit ((unsigned char) *e) != 0; e++)
			;

		/* do not increase the end pointer if we are at the last byte in the string */
		if (*e != '\0')
			*e++ = '\0';

		/* we found one, try it! */
		if (try_host (handle, p, port) != 0) {
			int val = handle->tcp_nodelay;
			setsockopt (handle->fd, SOL_TCP, TCP_NODELAY, (char *) &val, sizeof val);

			if (send_greeting (handle) == 1) {
				if ((handle->copt & COPT_HANDLEDELAY) == COPT_HANDLEDELAY) {
					if (mdf_int_set_delaymode (handle) != 0) {
						disconnect (handle);
						continue;
					}
				}

				free (s);
				handle->time_lastupdate = time (NULL);
				handle->heartbeats_sent = 0;
				call_status_callback (handle, MDF_STATUS_READYTOLOGON, handle->connected_host, handle->connected_ip);
				return 1;
			}
		}
	}

	free (s);
	handle->error = MDF_ERR_CONNECT;
	return 0;
}

LIBMDF_API void mdf_disconnect (mdf_t handle)
{
	if (handle == NULL || handle->fd == INVALID_SOCKET)
		return;

	disconnect (handle);
}

LIBMDF_API int mdf_get_property (mdf_t handle, MDF_OPTION option, ...)
{
	va_list ap;

	if (handle == NULL)
		return 0;

	va_start (ap, option);

	switch (option) {
		case MDF_OPT_FD : {
#if defined _WIN32 || defined _WIN64
			SOCKET *value = va_arg (ap, SOCKET *);
#else
			int *value = va_arg (ap, int *);
#endif
			if (value == NULL)
				return 0;

			*value = handle->fd;
			return 1;
		}

		case MDF_OPT_ERROR : {
			MDF_ERROR *value = va_arg (ap, MDF_ERROR *);

			if (value == NULL)
				return 0;

			*value = handle->error;
			return 1;
		}

		case MDF_OPT_RCV_BYTES : {
			uint64_t *value = va_arg (ap, uint64_t *);

			if (value == NULL)
				return 0;

			*value = handle->bytes_read;
			return 1;
		}

		case MDF_OPT_SENT_BYTES : {
			uint64_t *value = va_arg (ap, uint64_t *);

			if (value == NULL)
				return 0;

			*value = handle->bytes_written;
			return 1;
		}

		case MDF_OPT_DATA_CALLBACK_FUNCTION : {
			mdf_data_callback *value = va_arg (ap, mdf_data_callback *);

			if (value == NULL)
				return 0;

			*value = handle->callbacks.func_data;
			return 1;
		}

		case MDF_OPT_DATA_CALLBACK_USERDATA : {
			char **value = va_arg (ap, char **);

			if (value == NULL)
				return 0;

			*value = (char *) handle->callbacks.udata_data;
			return 1;
		}

		case MDF_OPT_STATUS_CALLBACK_FUNCTION : {
			mdf_status_callback *value = va_arg (ap, mdf_status_callback *);

			if (value == NULL)
				return 0;

			*value = handle->callbacks.func_status;
			return 1;
		}

		case MDF_OPT_STATUS_CALLBACK_USERDATA : {
			char **value = va_arg (ap, char **);

			if (value == NULL)
				return 0;

			*value = (char *) handle->callbacks.udata_status;
			return 1;
		}

		case MDF_OPT_CONNECT_TIMEOUT : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			*value = handle->connect_timeout;
			return 1;
		}

		case MDF_OPT_HEARTBEAT_INTERVAL : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			*value = handle->heartbeat_interval;
			return 1;
		}

		case MDF_OPT_HEARTBEAT_MAX_MISSED : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			*value = handle->max_missed_heartbeats;
			return 1;
		}

		case MDF_OPT_TCP_NODELAY : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			*value = handle->tcp_nodelay;
			return 1;
		}

		case MDF_OPT_NO_ENCRYPTION : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			if ((handle->copt & COPT_NOENCRYPTION) == COPT_NOENCRYPTION)
				*value = 1;
			else
				*value = 0;

			return 1;
		}

		case MDF_OPT_TIME_DIFFERENCE : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			*value = handle->timediff_ns / __INT64_C(1000000000);
			return 1;
		}

		case MDF_OPT_BIND_ADDRESS : {
			char **value = va_arg (ap, char **);

			if (value == NULL)
				return 0;

			*value = (char *) handle->bind_address;
			return 1;
		}

		case MDF_OPT_TIME_DIFFERENCE_NS : {
			int64_t *value = va_arg (ap, int64_t *);

			if (value == NULL)
				return 0;

			*value = handle->timediff_ns;
			return 1;
		}

		case MDF_OPT_CRYPT_DIGESTS : {
			char **value = va_arg (ap, char **);

			if (value == NULL)
				return 0;

			int pos = 0;
			for (int i = 0; i < NELEMS (DIGESTS); i++) {
				if (handle->crypto.digests & (1 << DIGESTS[i].id))
					pos += sprintf ((char *)(handle->value + pos), "%s,", DIGESTS[i].name);
			}

			if (pos == 0) {
				handle->value[pos] = '\0';
				*value = (char *)handle->value;
				return 0;
			}

			handle->value[pos - 1] = '\0';
			*value = (char *)handle->value;
			return 1;
		}

		case MDF_OPT_CRYPT_CIPHERS : {
			char **value = va_arg (ap, char **);

			if (value == NULL)
				return 0;

			int pos = 0;
			for (int i = 0; i < NELEMS (CIPHERS); i++) {
				if (handle->crypto.ciphers & (1 << CIPHERS[i].id))
					pos += sprintf ((char *)(handle->value + pos), "%s,", CIPHERS[i].name);
			}

			if (pos == 0) {
				handle->value[pos] = '\0';
				*value = (char *)handle->value;
				return 0;
			}

			handle->value[pos - 1] = '\0';
			*value = (char *)handle->value;
			return 1;
		}

		case MDF_OPT_CRYPT_DIGEST : {
			const char **value = va_arg (ap, const char **);

			if (value == NULL)
				return 0;

			*value = NULL;

			if (handle->crypto.digest == NULL)
				return 0;

			*value = EVP_MD_name (handle->crypto.digest);
			return 1;
		}

		case MDF_OPT_CRYPT_CIPHER : {
			const char **value = va_arg (ap, const char **);

			if (value == NULL)
				return 0;

			*value = NULL;
#if OPENSSL_VERSION_PREREQ(3,0)
			if (handle->crypto.cipher == NULL)
				return 0;

			*value = EVP_CIPHER_get0_name (handle->crypto.cipher);
#else
			const EVP_CIPHER *cipher = NULL;

			if (handle->crypto.cipher_encrypt_ctx != NULL)
				cipher = EVP_CIPHER_CTX_cipher (handle->crypto.cipher_encrypt_ctx);
			else if (handle->crypto.cipher_decrypt_ctx != NULL)
				cipher = EVP_CIPHER_CTX_cipher (handle->crypto.cipher_decrypt_ctx);

			if (cipher == NULL)
				return 0;

			*value = EVP_CIPHER_name (cipher);
#endif
			return 1;
		}

		case MDF_OPT_TIMEOUT : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			if (handle->fd == INVALID_SOCKET) {
				*value = handle->heartbeat_interval;
			} else {
				const int timediff = difftime (time (NULL), handle->time_lastupdate);

				if (timediff >= handle->heartbeat_interval)
					*value = 0;
				else
					*value = handle->heartbeat_interval - timediff;
			}

			return 1;
		}

		case MDF_OPT_HANDLE_DELAY : {
			int *value = va_arg (ap, int *);

			if (value == NULL)
				return 0;

			if ((handle->copt & COPT_RECVDELAY) == COPT_RECVDELAY)
				*value = 1;
			else
				*value = 0;

			return 1;
		}

		case MDF_OPT_RBUF_SIZE : {
			size_t *value = va_arg (ap, size_t *);

			if (value == NULL)
				return 0;

			*value = handle->data_used;
			return 1;
		}

		case MDF_OPT_RBUF_MAXSIZE : {
			size_t *value = va_arg (ap, size_t *);

			if (value == NULL)
				return 0;

			*value = handle->data_size;
			return 1;
		}

		case MDF_OPT_CONNECTED_HOST : {
			const char **value = va_arg (ap, const char **);

			if (value == NULL)
				return 0;

			if (handle->connected_host == NULL) {
				*value = NULL;
				return 0;
			}

			*value = handle->connected_host;
			return 1;
		}

		case MDF_OPT_CONNECTED_IP : {
			const char **value = va_arg (ap, const char **);

			if (value == NULL)
				return 0;

			if (handle->connected_ip == NULL) {
				*value = NULL;
				return 0;
			}

			*value = handle->connected_ip;
			return 1;
		}
	}

	va_end (ap);

	return 0;
}

LIBMDF_API int mdf_set_property (mdf_t handle, MDF_OPTION option, void *value)
{
	if (handle == NULL)
		return 0;

	switch (option) {
		case MDF_OPT_DATA_CALLBACK_FUNCTION :
			handle->callbacks.func_data = (mdf_data_callback) value;
			return 1;

		case MDF_OPT_DATA_CALLBACK_USERDATA :
			handle->callbacks.udata_data = value;
			return 1;

		case MDF_OPT_STATUS_CALLBACK_FUNCTION :
			handle->callbacks.func_status = (mdf_status_callback) value;
			return 1;

		case MDF_OPT_STATUS_CALLBACK_USERDATA :
			handle->callbacks.udata_status = value;
			return 1;

		case MDF_OPT_CONNECT_TIMEOUT : {
			int *val = (int *) value;

			if (*val < 1 || *val > 60)
				return 0;

			handle->connect_timeout = *val;
			return 1;
		}

		case MDF_OPT_RCV_BYTES :
			handle->bytes_read = *((uint64_t *) value);
			return 1;

		case MDF_OPT_SENT_BYTES :
			handle->bytes_written = *((uint64_t *) value);
			return 1;

		case MDF_OPT_ERROR :
			handle->error = *((MDF_ERROR *) value);
			return 1;

		case MDF_OPT_FD :
			return 0;

		case MDF_OPT_HEARTBEAT_INTERVAL : {
			int *val = (int *) value;

			if (*val < 1 || *val > 86400)
				return 0;

			handle->heartbeat_interval = *val;
			return 1;
		}

		case MDF_OPT_HEARTBEAT_MAX_MISSED : {
			int *val = (int *) value;

			if (*val < 1 || *val > 100)
				return 0;

			handle->max_missed_heartbeats = *val;
			return 1;
		}

		case MDF_OPT_TCP_NODELAY : {
			int *val = (int *) value;

			if (*val < 0 || *val > 1)
				return 0;

			handle->tcp_nodelay = *val;

			if (handle->fd != INVALID_SOCKET)
				setsockopt (handle->fd, SOL_TCP, TCP_NODELAY, (char *) val, sizeof *val);

			return 1;
		}

		case MDF_OPT_NO_ENCRYPTION : {
			int *val = (int *) value;

			if (*val < 0 || *val > 1)
				return 0;

			if (*val == 0)
				handle->copt &= ~COPT_NOENCRYPTION;
			else
				handle->copt |= COPT_NOENCRYPTION;

			return 1;
		}

		case MDF_OPT_TIME_DIFFERENCE :
			return 0;

		case MDF_OPT_BIND_ADDRESS : {
			free (handle->bind_address);
			handle->bind_address = value == NULL ? NULL : strdup ((char *)value);
			return 1;
		}

		case MDF_OPT_TIME_DIFFERENCE_NS :
			return 0;

		case MDF_OPT_CRYPT_DIGESTS : {
			if (value == NULL) {
				handle->crypto.digests = mdf_int_get_default_digests ();
				return 1;
			}

			unsigned int v = 0;
			char *p = value;
			do {
				char *e = strchr (p, ',');

				if (e != NULL)
					e++;

				for (int i = 0; NELEMS (DIGESTS); i++) {
					if (strcasecmp (p, DIGESTS[i].name) == 0) {
						v |= mdf_int_check_digest (i);
						break;
					}
				}

				p = e;
			} while (p != NULL);

			if (v == 0)
				return 0;

			handle->crypto.digests = v;
			return 1;
		}

		case MDF_OPT_CRYPT_CIPHERS : {
			if (value == NULL) {
				handle->crypto.ciphers = mdf_int_get_default_ciphers ();
				return 1;
			}

			uint32_t v = 0;
			char *p = value;
			do {
				char *e = strchr (p, ',');

				if (e != NULL)
					e++;

				for (int i = 0; i < NELEMS (CIPHERS); i++) {
					if (strcasecmp (p, CIPHERS[i].name) == 0) {
						v |= mdf_int_check_cipher (i);
						break;
					}
				}

				p = e;
			} while (p != NULL);

			if (v == 0)
				return 0;

			handle->crypto.ciphers = v;
			return 1;
		}

		case MDF_OPT_CRYPT_DIGEST :
			return 0;

		case MDF_OPT_CRYPT_CIPHER :
			return 0;

		case MDF_OPT_TIMEOUT :
			return 0;

		case MDF_OPT_HANDLE_DELAY : {
			int *val = (int *) value;

			if (*val < 0 || *val > 1)
				return 0;

			if (*val == 0)
				handle->copt &= ~COPT_HANDLEDELAY;
			else
				handle->copt |= COPT_HANDLEDELAY;

			return 1;
		}

		case MDF_OPT_RBUF_SIZE :
			return 0;

		case MDF_OPT_RBUF_MAXSIZE : {
			size_t *val = (size_t *) value;

			if (*val < handle->data_used)
				return 0;

			void *tmp = realloc (handle->data, *val);

			if (tmp == NULL)
				return 0;

			handle->data = tmp;
			handle->data_size = *val;
			return 1;
		}

		case MDF_OPT_CONNECTED_HOST :
		case MDF_OPT_CONNECTED_IP :
			return 0;
	}

	return 0;
}

LIBMDF_API int mdf_consume (mdf_t handle, const int timeout)
{
	if (handle == NULL)
		return -1;

	if (handle->fd == INVALID_SOCKET) {
		handle->error = MDF_ERR_NOT_CONNECTED;
		return -1;
	}

	if ((handle->crypto.cstate & CSTATE_INVALID) == CSTATE_INVALID)
		return 0;

	int flags = 0;
#ifdef MSG_DONTWAIT
	flags = MSG_DONTWAIT;

	/* if we are on a real POSIX system and timeout is zero
	 * we can skip the whole select() call saving us a context switch */
	if (timeout != 0) {
#endif
		fd_set rdset;
		FD_ZERO (&rdset);
		FD_SET (handle->fd, &rdset);

		struct timeval tv;
		if (timeout < 0) {
			if (timeout < -999999) {
				tv.tv_sec = 1;
				tv.tv_usec = 0;
			} else {
				tv.tv_sec = 0;
				tv.tv_usec = -timeout;
			}
		} else if (timeout > 60 * 60) {
			tv.tv_sec = 60 * 60;
			tv.tv_usec = 0;
		} else {
			tv.tv_sec = timeout;
			tv.tv_usec = 0;
		}

		const int ret = select (handle->fd + 1, &rdset, NULL, NULL, &tv);

		if (ret == 0) {
			/* is it time to send a heartbeat */
			time_t now = time (NULL);

			if (difftime (now, handle->time_lastupdate) >= handle->heartbeat_interval) {
				if (handle->heartbeats_sent++ == handle->max_missed_heartbeats) {
					disconnect (handle);
					handle->error = MDF_ERR_CONNECTION_IDLE;
					return -1;
				}

				handle->time_lastupdate = now;
				return request_heartbeat (handle);
			}

			return 0;
		}

		if (ret < 0) {
#ifdef HAVE_ERRNO_H
			if (errno == EINTR)
				return 0;
#endif
			disconnect (handle);
			handle->error = MDF_ERR_DISCONNECTED;
			return -1;
		}
#ifdef MSG_DONTWAIT
	}
#endif

#ifdef MSG_DONTWAIT
try_read_again:;
#endif
	const ssize_t len = recv (handle->fd, (char *) (handle->data + handle->data_used), handle->data_size - handle->data_used, flags);

	if (len == 0) {
		disconnect (handle);
		handle->error = MDF_ERR_DISCONNECTED;
		return -1;
	}

	if (len < 0) {
#ifdef MSG_DONTWAIT
		if (errno == EAGAIN) {
			/* is it time to send a heartbeat */
			time_t now = time (NULL);

			if (difftime (now, handle->time_lastupdate) >= handle->heartbeat_interval) {
				if (handle->heartbeats_sent++ == handle->max_missed_heartbeats) {
					disconnect (handle);
					handle->error = MDF_ERR_CONNECTION_IDLE;
					return -1;
				}

				handle->time_lastupdate = now;
				return request_heartbeat (handle);
			}

			return 0;
		}

		if (errno == EINTR)
			goto try_read_again;
#endif
		disconnect (handle);
		handle->error = MDF_ERR_DISCONNECTED;
		return -1;
	}

	handle->time_lastupdate = time (NULL);
	handle->bytes_read += len;
	handle->data_used += len;
	handle->heartbeats_sent = 0;

	if (handle->message_end != 0) { /* handled but lingering message in buffer */
		handle->data_pos += handle->message_len;
		handle->data_used -= handle->data_pos;
		memmove (handle->data, handle->data + handle->data_pos, handle->data_used);
		handle->data_pos = 0;
		handle->message_len = 0;
		handle->message_end = 0;
	} else if (handle->message_len != 0) { /* in case we get called before the client has decoded all the data in the buffer */
		if (handle->callbacks.func_data == NULL)
			return 1;

		do {
			handle->callbacks.func_data (handle->callbacks.udata_data, handle);
		} while (handle->message_len != 0);

		return 0;
	}

	int pos = 0;
	size_t tlen = 0;

	do {
		tlen *= 128;
		tlen += (handle->data[handle->data_pos + pos] & 127) + 1;
	} while ((handle->data[handle->data_pos + pos++] & 128) == 0 && handle->data_pos + pos < handle->data_used);

	if (pos > 5) {
		disconnect (handle);
		handle->error = MDF_ERR_MSG_OOB;
		return -1;
	}

	/* we have not yet received a full header */
	if ((handle->data[handle->data_pos + pos - 1] & 128) == 0) {
		if (handle->data_pos != 0) {
			handle->data_used -= handle->data_pos;
			memmove (handle->data, handle->data + handle->data_pos, handle->data_used);
			handle->data_pos = 0;
		}

		return 0;
	}

	handle->message_len = tlen + pos;
	handle->message_end = 0; /* will be properly set in mdf_get_next_message() */
	handle->pos = pos;
	handle->fpos = handle->data_pos + pos;

	if ((handle->copt & COPT_RECVDELAY) == COPT_RECVDELAY)
		handle->message_len++;

	if ((handle->crypto.cstate & CSTATE_DECRYPT) == CSTATE_DECRYPT)
		handle->message_len += handle->crypto.digest_length;

	size_t max_mayneed_size = handle->data_pos + handle->message_len + 12;

	if (handle->data_used == handle->data_size && handle->data_size * 2 > max_mayneed_size)
		max_mayneed_size = handle->data_size * 2;

	if (max_mayneed_size > handle->data_size) {
		void *tmp = realloc (handle->data, max_mayneed_size);

		if (tmp == NULL) {
			disconnect (handle);
			handle->error = MDF_ERR_NO_MEM;
			return -1;
		}

		handle->data = (uint8_t *) tmp;
		handle->data_size = max_mayneed_size;
	}

	/* we have not yet received the full message */
	if (handle->message_len + handle->data_pos > handle->data_used) {
		if (handle->data_pos != 0) {
			handle->data_used -= handle->data_pos;
			memmove (handle->data, handle->data + handle->data_pos, handle->data_used);
			handle->data_pos = 0;
		}

		handle->message_len = 0;
		return 0;
	}

	if (handle->callbacks.func_data == NULL)
		return 1;

	do {
		handle->callbacks.func_data (handle->callbacks.udata_data, handle);
	} while (handle->message_len != 0);

	return 0;
}

LIBMDF_API void *mdf_extract (mdf_t handle, uint16_t * const restrict mref, uint64_t * const restrict insref, size_t * const restrict len)
{
	int ret = mdf_get_next_message2 (handle, mref, insref);

	if (ret != 1) {
		*len = 0;
		return NULL;
	}

	*len = handle->message_end - handle->data_pos;
	return handle->data + handle->data_pos;
}

LIBMDF_API int mdf_inject (mdf_t handle, const void * const ptr, const size_t len)
{
	if (handle == NULL)
		return 0;

	if (handle->data_used + len + 12 > handle->data_size) {
		void *tmp = realloc (handle->data, handle->data_used + len + 12);

		if (tmp == NULL) {
			handle->error = MDF_ERR_NO_MEM;
			return -1;
		}

		handle->data = (uint8_t *) tmp;
		handle->data_size = handle->data_used + len + 12;
	}

	memcpy (handle->data + handle->data_used, ptr, len);
	handle->data_used += len;

	/* in case we get called before the client has decoded all the data in the buffer */
	if (handle->message_len != 0) {
		if (handle->callbacks.func_data == NULL)
			return 1;

		do {
			handle->callbacks.func_data (handle->callbacks.udata_data, handle);
		} while (handle->message_len != 0);

		return 0;
	}

	int pos = 0;
	size_t tlen = 0;

	do {
		tlen *= 128;
		tlen += (handle->data[handle->data_pos + pos] & 127) + 1;
	} while ((handle->data[handle->data_pos + pos++] & 128) == 0 && handle->data_pos + pos < handle->data_used);

	handle->message_len = tlen + pos;
	handle->message_end = 0; /* will be properly set in mdf_get_next_message() */
	handle->pos = pos;
	handle->fpos = handle->data_pos + pos;

	if (handle->callbacks.func_data == NULL)
		return 1;

	do {
		handle->callbacks.func_data (handle->callbacks.udata_data, handle);
	} while (handle->message_len != 0);

	return 0;
}

LIBMDF_API int mdf_message_send (mdf_t handle, mdf_message_t message)
{
	if (handle == NULL || message == NULL)
		return 0;

	if (handle->fd == INVALID_SOCKET)
		return 0;

	if ((handle->crypto.cstate & CSTATE_INVALID) == CSTATE_INVALID)
		return 0;

	if (message->current_message == -1)
		return 0;

#ifdef __clang__
	const struct templates * const templates = atomic_load_explicit ((struct templates * _Atomic const *)&handle->templates, memory_order_consume);
#else
	const struct templates * const templates = atomic_load_explicit (&handle->templates, memory_order_consume);
#endif

	if (templates == NULL)
		return 0;

	if (templates->max_num_tags > message->sendfields_num) {
		free (message->sendfields);
		message->sendfields = malloc (sizeof **message->sendfields * templates->max_num_tags);
		message->sendfields_num = templates->max_num_tags;
	}

	if (message->sendfields == NULL) {
		handle->error = MDF_ERR_NO_MEM;
		return 0;
	}

	const uint8_t is_delay_mode = (handle->copt & COPT_SENDDELAY) == COPT_SENDDELAY ? 1 : 0;
	const uint8_t do_encrypt = (handle->crypto.cstate & CSTATE_ENCRYPT) == CSTATE_ENCRYPT ? 1 : 0;
	const uint32_t digest_length = handle->crypto.digest_length;

	if (do_encrypt) {
		int expected = 0;
		while (!atomic_compare_exchange_weak_explicit (&handle->slock, &expected, 1, memory_order_acquire, memory_order_relaxed))
			expected = 0;

		if (handle->fd == INVALID_SOCKET) {
			atomic_store_explicit (&handle->slock, 0, memory_order_release);
			return 0;
		}
	}

	struct field *field = message->fields;
	size_t sendbuf_pos = 0;

	for (int i = 0; i <= message->current_message; i++) {
		struct msg_template * restrict template;

		if (message->messages[i].fields == 0 && message->messages[i].mref != MDF_M_INSTRUMENTRESET && message->messages[i].mref != MDF_M_INSTRUMENTDELETE && message->messages[i].mref != MDF_M_ORDERBOOKFLUSH && message->messages[i].mref != MDF_M_LOGOFF)
			continue;

		if (message->messages[i].mref > 0 && message->messages[i].mref < templates->num) {
			template = &templates->templates[message->messages[i].mref];
			memset (message->sendfields, 0, sizeof *message->sendfields * template->tags_num);
		} else {
			template = NULL;
		}

		/* create one list of the tags in positional order and one list of the
		 * tags that we must send as "tagged" */
		int tags_used = 0;
		int maxpos = 0;
		size_t len = 0;
		for (int j = 0; j < message->messages[i].fields; j++) {
			/* perform binary search for the field position */
			int high = template == NULL ? -1 : template->tags_num - 1;
			int pos = -1;
			int low = 0;

			while (high >= low) {
				const int mid = (low + high) / 2;

				if (template->fields[mid].tag == field->tag) {
					pos = template->fields[mid].pos;
					break;
				}

				if (template->fields[mid].tag > field->tag)
					high = mid - 1;
				else
					low = mid + 1;
			}

			if (pos != -1) {
				if (message->sendfields[pos] != NULL)
					len -= message->sendfields[pos]->len;

				message->sendfields[pos] = field;

				if (pos > maxpos)
					maxpos = pos;
			} else {
				if (tags_used >= message->sendtags_num) {
					message->sendtags_num = (tags_used + 1) * 2;
					struct field **tmp = message->sendtags;
					message->sendtags = realloc (message->sendtags, sizeof *message->sendtags * (message->sendtags_num));

					if (message->sendtags == NULL) {
						free (tmp);
						message->sendtags_num = 0;
						handle->error = MDF_ERR_NO_MEM;
						if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);
						return 0;
					}
				}

				message->sendtags[tags_used++] = field;

				if (field->tag <= 255)
					len += 2;
				else if (field->tag <= 65535)
					len += 3;
				else
					len += 5;
			}

			len += field->len;
			field++;
		}

		/* the number of pmap:s that we will use */
		len += (maxpos / 8) + 1;

		if (len + 5 + 11 + digest_length + sendbuf_pos > message->sendbuf_size) {
			message->sendbuf_size = len + 5 + 11 + digest_length + sendbuf_pos + 1;
			message->sendbuf_size += BUFFER_CHUNK_SIZE - (message->sendbuf_size % BUFFER_CHUNK_SIZE);
			void *tmp = realloc (message->sendbuf, message->sendbuf_size);

			if (tmp == NULL) {
				free (message->sendbuf);
				message->sendbuf_size = 0;
				handle->error = MDF_ERR_NO_MEM;
				if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);
				return 0;
			}

			message->sendbuf = tmp;
		}

		const int header_len = calc_header_len (message->messages[i].mref + 1, message->messages[i].insref);
		len += header_len;

		uint8_t *p = message->sendbuf + sendbuf_pos;

		const int len_len = add_len (p, len);
		p += len_len;
	
		add_header (p, message->messages[i].mref + 1, message->messages[i].insref);
		p += header_len;

		uint8_t *pmap_pos;
		if (template != NULL) {
			int pmap = 0;

			for (int j = 0; j <= maxpos; j++) {
				if (pmap == 0) {
					pmap = 128;
					pmap_pos = p;
					*p++ = 0;
				}

				if (message->sendfields[j] != NULL) {
					*pmap_pos |= pmap;

					if (tags_used != 0) {
						int k;
						for (k = 0; k < tags_used; k++) {
							if (message->sendtags[k]->tag <= 255) {
								*p++ = 251;
								*p++ = (uint8_t) message->sendtags[k]->tag;
							} else if (message->sendtags[k]->tag <= 65535) {
								*p++ = 252;
								uint16_t val = cpu_to_le16 (message->sendtags[k]->tag);
								memcpy (p, &val, sizeof val);
								p += 2;
							} else {
								*p++ = 253;
								uint32_t val = cpu_to_le32 (message->sendtags[k]->tag);
								memcpy (p, &val, sizeof val);
								p += 4;
							}

							memcpy (p, message->data + message->sendtags[k]->offset, message->sendtags[k]->len);
							p += message->sendtags[k]->len;
						}

						tags_used = 0;
					}

					memcpy (p, message->data + message->sendfields[j]->offset, message->sendfields[j]->len);
					p += message->sendfields[j]->len;
				}

				pmap /= 2;
			}
		} else if (tags_used == 0) {
			*p++ = 0;
		}

		/* apparently we had only tagged data today... */
		if (tags_used != 0) {
			if (template == NULL)
				*p++ = 128;
			else
				*pmap_pos = 128;

			for (int j = 0; j < tags_used; j++) {
				if (message->sendtags[j]->tag <= 255) {
					*p++ = 251;
					*p++ = (uint8_t) message->sendtags[j]->tag;
				} else if (message->sendtags[j]->tag <= 65535) {
					*p++ = 252;

					uint16_t val = cpu_to_le16 ((uint16_t) message->sendtags[j]->tag);
					memcpy (p, &val, sizeof val);
					p += 2;
				} else {
					*p++ = 253;
					uint32_t val = cpu_to_le32 ((uint32_t) message->sendtags[j]->tag);
					memcpy (p, &val, sizeof val);
					p += 4;
				}

				memcpy (p, message->data + message->sendtags[j]->offset, message->sendtags[j]->len);
				p += message->sendtags[j]->len;
			}
		}

		if (is_delay_mode == 1) {
			*p++ = message->messages[i].delay;
			len++;
		}

		if (do_encrypt == 1) {
			const uint32_t tmp_u32 = cpu_to_le32 (len_len + len + digest_length);
			const uint64_t tmp_u64 = cpu_to_le64 (handle->crypto.encrypt_cnt);
			int rc = 1;

#if OPENSSL_VERSION_PREREQ(3,0)
			EVP_MAC_CTX *mctx = EVP_MAC_CTX_dup (handle->crypto.enc_mctx);
			EVP_MAC_update (mctx, (const unsigned char *) &tmp_u64, sizeof tmp_u64);
			EVP_MAC_update (mctx, (const unsigned char *) &tmp_u32, sizeof tmp_u32);
			EVP_MAC_update (mctx, message->sendbuf + sendbuf_pos, len_len + len);

			size_t siglen = digest_length;
			EVP_MAC_final (mctx, p, &siglen, siglen);
			EVP_MAC_CTX_free (mctx);
#else
			if (EVP_DigestSignInit (handle->crypto.digest_encrypt_ctx, NULL, handle->crypto.digest, NULL, NULL) == 0)
				rc = 0;

			if (EVP_DigestSignUpdate (handle->crypto.digest_encrypt_ctx, (const unsigned char *) &tmp_u64, sizeof tmp_u64) == 0)
				rc = 0;

			if (EVP_DigestSignUpdate (handle->crypto.digest_encrypt_ctx, (const unsigned char *) &tmp_u32, sizeof tmp_u32) == 0)
				rc = 0;

			if (EVP_DigestSignUpdate (handle->crypto.digest_encrypt_ctx, message->sendbuf + sendbuf_pos, len_len + len) == 0)
				rc = 0;

			size_t siglen = digest_length;
			if (EVP_DigestSignFinal (handle->crypto.digest_encrypt_ctx, p, &siglen) == 0)
				rc = 0;
#endif
			len += siglen;

			int outlen;
			if (EVP_EncryptUpdate (handle->crypto.cipher_encrypt_ctx, message->sendbuf + sendbuf_pos + len_len, &outlen, message->sendbuf + sendbuf_pos + len_len, len) == 0 || outlen != (int)len)
				rc = 0;

			handle->crypto.encrypt_cnt++;
			handle->crypto.encrypt_num += outlen;

			if (rc == 0) {
				handle->error = MDF_ERR_AUTHFAIL;
				disconnect (handle);
				if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);
				return 0;
			}
		}

		sendbuf_pos += len + len_len;
	}

	if (sendbuf_pos == 0) {
		if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);
		return 1;
	}

send_again:;
	const ssize_t written = send (handle->fd, (void *)message->sendbuf, sendbuf_pos, MSG_NOSIGNAL);
	if ((size_t)written == sendbuf_pos) {
		handle->bytes_written += written;

		if (do_encrypt == 1 && should_we_rekey (handle) == 0) {
			handle->error = MDF_ERR_AUTHFAIL;
			disconnect (handle);
			if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);
			return 0;
		}

		if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);
		return 1;
	}

#ifdef HAVE_ERRNO_H
	if (written == -1 && errno == EINTR)
		goto send_again;
#endif
	handle->error = MDF_ERR_DISCONNECTED;

	if (do_encrypt) atomic_store_explicit (&handle->slock, 0, memory_order_release);

	disconnect (handle);
	return 0;
}

