/*
 * Copyright (C) 2000-2012 Free Software Foundation, Inc.
 *
 * Author: Nikos Mavrogiannopoulos
 *
 * This file is part of GnuTLS.
 *
 * The GnuTLS 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 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser 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 <https://www.gnu.org/licenses/>
 *
 */

/*
 * This file holds all the buffering code used in gnutls.
 * The buffering code works as:
 *
 * RECORD LAYER:
 *  1. uses a buffer to hold data (application/handshake),
 *    we got but they were not requested, yet.
 *  (see gnutls_record_buffer_put(), gnutls_record_buffer_get_size() etc.)
 *
 *  2. uses a buffer to hold data that were incomplete (ie the read/write
 *    was interrupted)
 *  (see _gnutls_io_read_buffered(), _gnutls_io_write_buffered() etc.)
 *
 * HANDSHAKE LAYER:
 *  1. Uses buffer to hold the last received handshake message.
 *  (see _gnutls_handshake_hash_buffer_put() etc.)
 *
 */

#include "gnutls_int.h"
#include "errors.h"
#include "num.h"
#include "record.h"
#include "buffers.h"
#include "mbuffers.h"
#include "state.h"
#include "dtls.h"
#include "system.h"
#include "constate.h" /* gnutls_epoch_get */
#include "handshake.h" /* remaining_time() */
#include <errno.h>
#include "system.h"
#include "debug.h"

#ifndef EAGAIN
#define EAGAIN EWOULDBLOCK
#endif

/* this is the maximum number of messages allowed to queue.
 */
#define MAX_QUEUE 32

/* Buffers received packets of type APPLICATION DATA,
 * HANDSHAKE DATA and HEARTBEAT.
 */
void _gnutls_record_buffer_put(gnutls_session_t session, content_type_t type,
			       uint64_t seq, mbuffer_st *bufel)
{
	bufel->type = type;
	bufel->record_sequence = seq;

	_mbuffer_enqueue(&session->internals.record_buffer, bufel);
	_gnutls_buffers_log("BUF[REC]: Inserted %d bytes of Data(%d)\n",
			    (int)bufel->msg.size, (int)type);

	return;
}

/**
 * gnutls_record_check_pending:
 * @session: is a #gnutls_session_t type.
 *
 * This function checks if there are unread data
 * in the gnutls buffers. If the return value is
 * non-zero the next call to gnutls_record_recv()
 * is guaranteed not to block.
 *
 * Returns: Returns the size of the data or zero.
 **/
size_t gnutls_record_check_pending(gnutls_session_t session)
{
	return _gnutls_record_buffer_get_size(session);
}

/**
 * gnutls_record_check_corked:
 * @session: is a #gnutls_session_t type.
 *
 * This function checks if there pending corked
 * data in the gnutls buffers --see gnutls_record_cork().
 *
 * Returns: Returns the size of the corked data or zero.
 *
 * Since: 3.2.8
 **/
size_t gnutls_record_check_corked(gnutls_session_t session)
{
	return session->internals.record_presend_buffer.length;
}

int _gnutls_record_buffer_get(content_type_t type, gnutls_session_t session,
			      uint8_t *data, size_t length, uint8_t seq[8])
{
	gnutls_datum_t msg;
	mbuffer_st *bufel;

	if (length == 0 || data == NULL) {
		gnutls_assert();
		return GNUTLS_E_INVALID_REQUEST;
	}

	bufel = _mbuffer_head_get_first(&session->internals.record_buffer,
					&msg);
	if (bufel == NULL)
		return gnutls_assert_val(GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE);

	if (type != bufel->type) {
		if (IS_DTLS(session))
			_gnutls_audit_log(
				session,
				"Discarded unexpected %s (%d) packet (expecting: %s (%d))\n",
				_gnutls_packet2str(bufel->type),
				(int)bufel->type, _gnutls_packet2str(type),
				(int)type);
		else
			_gnutls_debug_log(
				"received unexpected packet: %s(%d)\n",
				_gnutls_packet2str(bufel->type),
				(int)bufel->type);

		_mbuffer_head_remove_bytes(&session->internals.record_buffer,
					   msg.size);
		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET);
	}

	if (msg.size <= length)
		length = msg.size;

	if (seq)
		_gnutls_write_uint64(bufel->record_sequence, seq);

	memcpy(data, msg.data, length);
	_mbuffer_head_remove_bytes(&session->internals.record_buffer, length);

	return length;
}

int _gnutls_record_buffer_get_packet(content_type_t type,
				     gnutls_session_t session,
				     gnutls_packet_t *packet)
{
	mbuffer_st *bufel;

	bufel = _mbuffer_head_pop_first(&session->internals.record_buffer);
	if (bufel == NULL)
		return gnutls_assert_val(GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE);

	if (type != bufel->type) {
		if (IS_DTLS(session))
			_gnutls_audit_log(
				session,
				"Discarded unexpected %s (%d) packet (expecting: %s)\n",
				_gnutls_packet2str(bufel->type),
				(int)bufel->type, _gnutls_packet2str(type));
		_mbuffer_head_remove_bytes(&session->internals.record_buffer,
					   bufel->msg.size);
		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET);
	}

	*packet = bufel;

	return bufel->msg.size - bufel->mark;
}

inline static void reset_errno(gnutls_session_t session)
{
	session->internals.errnum = 0;
}

inline static int get_errno(gnutls_session_t session, gnutls_transport_ptr_t fd)
{
	int ret;

	if (session->internals.errnum != 0)
		ret = session->internals.errnum;
	else
		ret = session->internals.errno_func(fd);
	return ret;
}

inline static int errno_to_gerr(int err, unsigned dtls)
{
	switch (err) {
	case EAGAIN:
		return GNUTLS_E_AGAIN;
	case EINTR:
		return GNUTLS_E_INTERRUPTED;
	case EMSGSIZE:
		if (dtls != 0)
			return GNUTLS_E_LARGE_PACKET;
		else
			return GNUTLS_E_PUSH_ERROR;
	case ECONNRESET:
		return GNUTLS_E_PREMATURE_TERMINATION;
	default:
		gnutls_assert();
		return GNUTLS_E_PUSH_ERROR;
	}
}

static ssize_t _gnutls_dgram_read(gnutls_session_t session, mbuffer_st **bufel,
				  gnutls_pull_func pull_func, unsigned int *ms)
{
	ssize_t i, ret;
	uint8_t *ptr;
	struct timespec t1, t2;
	size_t max_size, recv_size;
	gnutls_transport_ptr_t fd = session->internals.transport_recv_ptr;
	unsigned int diff;

	max_size = max_record_recv_size(session);
	recv_size = max_size;

	session->internals.direction = 0;

	if (ms && *ms > 0) {
		ret = _gnutls_io_check_recv(session, *ms);
		if (ret < 0)
			return gnutls_assert_val(ret);
		gnutls_gettime(&t1);
	}

	*bufel = _mbuffer_alloc_align16(max_size, get_total_headers(session));
	if (*bufel == NULL)
		return gnutls_assert_val(GNUTLS_E_MEMORY_ERROR);

	ptr = (*bufel)->msg.data;

	reset_errno(session);
	i = pull_func(fd, ptr, recv_size);

	if (i < 0) {
		int err = get_errno(session, fd);

		_gnutls_read_log("READ: %d returned from %p, errno=%d\n",
				 (int)i, fd, err);

		ret = errno_to_gerr(err, 1);
		goto cleanup;
	} else {
		_gnutls_read_log("READ: Got %d bytes from %p\n", (int)i, fd);
		if (i == 0) {
			/* If we get here, we likely have a stream socket.
			 * That assumption may not work on DCCP. */
			gnutls_assert();
			ret = 0;
			goto cleanup;
		}

		_mbuffer_set_udata_size(*bufel, i);
	}

	if (ms && *ms > 0) {
		gnutls_gettime(&t2);
		diff = timespec_sub_ms(&t2, &t1);
		if (diff < *ms)
			*ms -= diff;
		else {
			ret = gnutls_assert_val(GNUTLS_E_TIMEDOUT);
			goto cleanup;
		}
	}

	_gnutls_read_log("READ: read %d bytes from %p\n", (int)i, fd);

	return i;

cleanup:
	_mbuffer_xfree(bufel);
	return ret;
}

static ssize_t _gnutls_stream_read(gnutls_session_t session, mbuffer_st **bufel,
				   size_t size, gnutls_pull_func pull_func,
				   unsigned int *ms)
{
	size_t left;
	ssize_t i = 0;
	size_t max_size = max_record_recv_size(session);
	uint8_t *ptr;
	gnutls_transport_ptr_t fd = session->internals.transport_recv_ptr;
	int ret;
	struct timespec t1, t2;
	unsigned int diff;

	session->internals.direction = 0;

	*bufel = _mbuffer_alloc_align16(MAX(max_size, size),
					get_total_headers(session));
	if (!*bufel) {
		gnutls_assert();
		return GNUTLS_E_MEMORY_ERROR;
	}
	ptr = (*bufel)->msg.data;

	left = size;
	while (left > 0) {
		if (ms && *ms > 0) {
			ret = _gnutls_io_check_recv(session, *ms);
			if (ret < 0) {
				gnutls_assert();
				goto cleanup;
			}

			gnutls_gettime(&t1);
		}

		reset_errno(session);

		i = pull_func(fd, &ptr[size - left], left);

		if (i < 0) {
			int err = get_errno(session, fd);

			_gnutls_read_log(
				"READ: %d returned from %p, errno=%d gerrno=%d\n",
				(int)i, fd, errno, session->internals.errnum);

			if (err == EAGAIN || err == EINTR) {
				if (size - left > 0) {
					_gnutls_read_log(
						"READ: returning %d bytes from %p\n",
						(int)(size - left), fd);

					goto finish;
				}

				ret = errno_to_gerr(err, 0);
				goto cleanup;
			} else {
				gnutls_assert();
				ret = GNUTLS_E_PULL_ERROR;
				goto cleanup;
			}
		} else {
			_gnutls_read_log("READ: Got %d bytes from %p\n", (int)i,
					 fd);

			if (i == 0)
				break; /* EOF */
		}

		left -= i;
		(*bufel)->msg.size += i;

		if (ms && *ms > 0 && *ms != GNUTLS_INDEFINITE_TIMEOUT) {
			gnutls_gettime(&t2);
			diff = timespec_sub_ms(&t2, &t1);
			if (diff < *ms)
				*ms -= diff;
			else {
				ret = gnutls_assert_val(GNUTLS_E_TIMEDOUT);
				goto cleanup;
			}
		}
	}

finish:

	_gnutls_read_log("READ: read %d bytes from %p\n", (int)(size - left),
			 fd);

	if (size - left == 0)
		_mbuffer_xfree(bufel);

	return (size - left);

cleanup:
	_mbuffer_xfree(bufel);
	return ret;
}

/* This function is like read. But it does not return -1 on error.
 * It does return gnutls_errno instead.
 *
 * Flags are only used if the default recv() function is being used.
 */
static ssize_t _gnutls_read(gnutls_session_t session, mbuffer_st **bufel,
			    size_t size, gnutls_pull_func pull_func,
			    unsigned int *ms)
{
	if (IS_DTLS(session))
		/* Size is not passed, since a whole datagram will be read. */
		return _gnutls_dgram_read(session, bufel, pull_func, ms);
	else
		return _gnutls_stream_read(session, bufel, size, pull_func, ms);
}

/* @vec: if non-zero then the vector function will be used to
 *       push the data.
 */
static ssize_t _gnutls_writev_emu(gnutls_session_t session,
				  gnutls_transport_ptr_t fd,
				  const giovec_t *giovec,
				  unsigned int giovec_cnt, unsigned vec)
{
	unsigned int j = 0;
	size_t total = 0;
	ssize_t ret = 0;

	for (j = 0; j < giovec_cnt; j++) {
		if (vec) {
			ret = session->internals.vec_push_func(fd, &giovec[j],
							       1);
		} else {
			size_t sent = 0;
			ssize_t left = giovec[j].iov_len;
			char *p = giovec[j].iov_base;
			do {
				ret = session->internals.push_func(fd, p, left);
				if (ret > 0) {
					sent += ret;
					left -= ret;
					p += ret;
				}
			} while (ret > 0 && left > 0);

			if (sent > 0)
				ret = sent;
		}

		if (ret == -1) {
			gnutls_assert();
			break;
		}

		total += ret;

		if ((size_t)ret != giovec[j].iov_len)
			break;
	}

	if (total > 0)
		return total;

	return ret;
}

/* @total: The sum of the data in giovec
 */
static ssize_t _gnutls_writev(gnutls_session_t session, const giovec_t *giovec,
			      unsigned giovec_cnt, unsigned total)
{
	int i;
	bool is_dtls = IS_DTLS(session);
	unsigned no_writev = 0;
	gnutls_transport_ptr_t fd = session->internals.transport_send_ptr;

	reset_errno(session);

	if (session->internals.vec_push_func != NULL) {
		if (is_dtls && giovec_cnt > 1) {
			if (total > session->internals.dtls.mtu) {
				no_writev = 1;
			}
		}

		if (no_writev == 0) {
			i = session->internals.vec_push_func(fd, giovec,
							     giovec_cnt);
		} else {
			i = _gnutls_writev_emu(session, fd, giovec, giovec_cnt,
					       1);
		}
	} else if (session->internals.push_func != NULL) {
		i = _gnutls_writev_emu(session, fd, giovec, giovec_cnt, 0);
	} else
		return gnutls_assert_val(GNUTLS_E_PUSH_ERROR);

	if (i == -1) {
		int err = get_errno(session, fd);
		_gnutls_debug_log("WRITE: %d returned from %p, errno: %d\n", i,
				  fd, err);

		return errno_to_gerr(err, is_dtls);
	}
	return i;
}

/*
 * @ms: a pointer to the number of milliseconds to wait for data. Use zero or NULL for indefinite.
 *
 * This function is like recv(with MSG_PEEK). But it does not return -1 on error.
 * It does return gnutls_errno instead.
 * This function reads data from the socket and keeps them in a buffer, of up to
 * max_record_recv_size.
 *
 * This is not a general purpose function. It returns EXACTLY the data requested,
 * which are stored in a local (in the session) buffer.
 *
 * If the @ms parameter is non zero then this function will return before
 * the given amount of milliseconds or return GNUTLS_E_TIMEDOUT.
 *
 */
ssize_t _gnutls_io_read_buffered(gnutls_session_t session, size_t total,
				 content_type_t recv_type, unsigned int *ms)
{
	ssize_t ret;
	size_t min;
	mbuffer_st *bufel = NULL;
	size_t recvdata, readsize;

	if (total > max_record_recv_size(session) || total == 0) {
		gnutls_assert();
		return GNUTLS_E_RECORD_OVERFLOW;
	}

	/* calculate the actual size, ie. get the minimum of the
	 * buffered data and the requested data.
	 */
	min = MIN(session->internals.record_recv_buffer.byte_length, total);
	if (min > 0) {
		/* if we have enough buffered data
		 * then just return them.
		 */
		if (min == total) {
			return min;
		}
	}

	/* min is over zero. recvdata is the data we must
	 * receive in order to return the requested data.
	 */
	recvdata = total - min;
	readsize = recvdata;

	/* Check if the previously read data plus the new data to
	 * receive are longer than the maximum receive buffer size.
	 */
	if ((session->internals.record_recv_buffer.byte_length + recvdata) >
	    max_record_recv_size(session)) {
		gnutls_assert(); /* internal error */
		return GNUTLS_E_INVALID_REQUEST;
	}

	/* READ DATA
	 */
	if (readsize > 0) {
		ret = _gnutls_read(session, &bufel, readsize,
				   session->internals.pull_func, ms);

		/* return immediately if we got an interrupt or eagain
		 * error.
		 */
		if (ret < 0) {
			return gnutls_assert_val(ret);
		}

		if (ret == 0) /* EOF */
			return gnutls_assert_val(0);

		/* copy fresh data to our buffer.
		 */
		_gnutls_read_log(
			"RB: Have %d bytes into buffer. Adding %d bytes.\n",
			(int)session->internals.record_recv_buffer.byte_length,
			(int)ret);
		_gnutls_read_log("RB: Requested %d bytes\n", (int)total);

		_mbuffer_enqueue(&session->internals.record_recv_buffer, bufel);

		if (IS_DTLS(session))
			ret = MIN(total, session->internals.record_recv_buffer
						 .byte_length);
		else
			ret = session->internals.record_recv_buffer.byte_length;

		if ((ret > 0) && ((size_t)ret < total)) /* Short Read */
			return gnutls_assert_val(GNUTLS_E_AGAIN);
		else
			return ret;
	} else
		return gnutls_assert_val(0);
}

/* This function is like write. But it does not return -1 on error.
 * It does return gnutls_errno instead.
 *
 * This function takes full responsibility of freeing msg->data.
 *
 * In case of E_AGAIN and E_INTERRUPTED errors, you must call
 * gnutls_write_flush(), until it returns ok (0).
 *
 * We need to push exactly the data in msg->size, since we cannot send
 * less data. In TLS the peer must receive the whole packet in order
 * to decrypt and verify the integrity.
 *
 */
ssize_t _gnutls_io_write_buffered(gnutls_session_t session, mbuffer_st *bufel,
				  unsigned int mflag)
{
	mbuffer_head_st *const send_buffer =
		&session->internals.record_send_buffer;

	/* to know where the procedure was interrupted.
	 */
	session->internals.direction = 1;

	_mbuffer_enqueue(send_buffer, bufel);

	_gnutls_write_log("WRITE: enqueued %d bytes for %p. Total %d bytes.\n",
			  (int)bufel->msg.size,
			  session->internals.transport_recv_ptr,
			  (int)send_buffer->byte_length);

	if (mflag == MBUFFER_FLUSH)
		return _gnutls_io_write_flush(session);
	else
		return bufel->msg.size;
}

typedef ssize_t (*send_func)(gnutls_session_t, const giovec_t *, int);

/* This function writes the data that are left in the
 * TLS write buffer (ie. because the previous write was
 * interrupted.
 */
ssize_t _gnutls_io_write_flush(gnutls_session_t session)
{
	gnutls_datum_t msg;
	mbuffer_head_st *send_buffer = &session->internals.record_send_buffer;
	int ret;
	ssize_t sent = 0, tosend = 0;
	giovec_t iovec[MAX_QUEUE];
	int i = 0;
	mbuffer_st *cur;

	session->internals.direction = 1;
	_gnutls_write_log("WRITE FLUSH: %d bytes in buffer.\n",
			  (int)send_buffer->byte_length);

	for (cur = _mbuffer_head_get_first(send_buffer, &msg); cur != NULL;
	     cur = _mbuffer_head_get_next(cur, &msg)) {
		iovec[i].iov_base = msg.data;
		iovec[i++].iov_len = msg.size;
		tosend += msg.size;

		/* we buffer up to MAX_QUEUE messages */
		if (i >= MAX_QUEUE) {
			gnutls_assert();
			return GNUTLS_E_INTERNAL_ERROR;
		}
	}

	if (tosend == 0) {
		gnutls_assert();
		return 0;
	}

	ret = _gnutls_writev(session, iovec, i, tosend);
	if (ret >= 0) {
		_mbuffer_head_remove_bytes(send_buffer, ret);
		_gnutls_write_log("WRITE: wrote %d bytes, %d bytes left.\n",
				  ret, (int)send_buffer->byte_length);

		sent += ret;
	} else if (ret == GNUTLS_E_INTERRUPTED || ret == GNUTLS_E_AGAIN) {
		_gnutls_write_log("WRITE interrupted: %d bytes left.\n",
				  (int)send_buffer->byte_length);
		return ret;
	} else if (ret == GNUTLS_E_LARGE_PACKET) {
		_mbuffer_head_remove_bytes(send_buffer, tosend);
		_gnutls_write_log(
			"WRITE cannot send large packet (%u bytes).\n",
			(unsigned int)tosend);
		return ret;
	} else {
		_gnutls_write_log("WRITE error: code %d, %d bytes left.\n", ret,
				  (int)send_buffer->byte_length);

		gnutls_assert();
		return ret;
	}

	if (sent < tosend) {
		return gnutls_assert_val(GNUTLS_E_AGAIN);
	}

	return sent;
}

/* Checks whether there are received data within
 * a timeframe.
 *
 * Returns 0 if data were received, GNUTLS_E_TIMEDOUT
 * on timeout and a negative error code on error.
 */
int _gnutls_io_check_recv(gnutls_session_t session, unsigned int ms)
{
	gnutls_transport_ptr_t fd = session->internals.transport_recv_ptr;
	int ret = 0, err;

	if (NO_TIMEOUT_FUNC_SET(session)) {
		_gnutls_debug_log(
			"The pull function has been replaced but not the pull timeout.\n");
		return gnutls_assert_val(GNUTLS_E_PULL_ERROR);
	}

	reset_errno(session);

	ret = session->internals.pull_timeout_func(fd, ms);
	if (ret == -1) {
		err = get_errno(session, fd);
		_gnutls_read_log(
			"READ_TIMEOUT: %d returned from %p, errno=%d (timeout: %u)\n",
			(int)ret, fd, err, ms);
		return errno_to_gerr(err, IS_DTLS(session));
	}

	if (ret > 0)
		return 0;
	else
		return GNUTLS_E_TIMEDOUT;
}

/* HANDSHAKE buffers part
 */

/* This function writes the data that are left in the
 * Handshake write buffer (ie. because the previous write was
 * interrupted.
 *
 */
ssize_t _gnutls_handshake_io_write_flush(gnutls_session_t session)
{
	mbuffer_head_st *const send_buffer =
		&session->internals.handshake_send_buffer;
	gnutls_datum_t msg;
	int ret;
	uint16_t epoch;
	mbuffer_st *cur;

	_gnutls_write_log("HWRITE FLUSH: %d bytes in buffer.\n",
			  (int)send_buffer->byte_length);

	if (IS_DTLS(session))
		return _dtls_transmit(session);

	for (cur = _mbuffer_head_get_first(send_buffer, &msg); cur != NULL;
	     cur = _mbuffer_head_get_first(send_buffer, &msg)) {
		epoch = cur->epoch;

		if (session->internals.h_read_func) {
			record_parameters_st *params;

			ret = _gnutls_epoch_get(session, epoch, &params);
			if (ret < 0)
				return gnutls_assert_val(ret);
			ret = session->internals.h_read_func(
				session, params->write.level, cur->htype,
				msg.data, msg.size);
			if (ret < 0)
				return gnutls_assert_val(ret);

			ret = msg.size;
		} else {
			ret = _gnutls_send_int(session, cur->type, cur->htype,
					       epoch, msg.data, msg.size, 0);
		}

		if (ret >= 0) {
			ret = _mbuffer_head_remove_bytes(send_buffer, ret);
			/* for each queued message we send, ensure that
			 * we drop the epoch refcount set in _gnutls_handshake_io_cache_int(). */
			if (ret == 1)
				_gnutls_epoch_refcount_dec(session, epoch);

			_gnutls_write_log(
				"HWRITE: wrote %d bytes, %d bytes left.\n", ret,
				(int)send_buffer->byte_length);

		} else {
			_gnutls_write_log(
				"HWRITE error: code %d, %d bytes left.\n", ret,
				(int)send_buffer->byte_length);

			gnutls_assert();
			return ret;
		}
	}

	return _gnutls_io_write_flush(session);
}

/* This is a send function for the gnutls handshake
 * protocol. Just makes sure that all data have been sent.
 *
 */
int _gnutls_handshake_io_cache_int(gnutls_session_t session,
				   gnutls_handshake_description_t htype,
				   mbuffer_st *bufel)
{
	mbuffer_head_st *send_buffer;

	if (IS_DTLS(session)) {
		bufel->handshake_sequence =
			session->internals.dtls.hsk_write_seq - 1;
	}

	send_buffer = &session->internals.handshake_send_buffer;

	/* ensure that our epoch does not get garbage collected
	 * before we send all queued messages with it */
	bufel->epoch = (uint16_t)_gnutls_epoch_refcount_inc(
		session, EPOCH_WRITE_CURRENT);
	bufel->htype = htype;
	if (bufel->htype == GNUTLS_HANDSHAKE_CHANGE_CIPHER_SPEC)
		bufel->type = GNUTLS_CHANGE_CIPHER_SPEC;
	else
		bufel->type = GNUTLS_HANDSHAKE;

	_mbuffer_enqueue(send_buffer, bufel);

	_gnutls_write_log("HWRITE: enqueued [%s] %d. Total %d bytes.\n",
			  _gnutls_handshake2str(bufel->htype),
			  (int)bufel->msg.size, (int)send_buffer->byte_length);

	return 0;
}

static int handshake_compare(const void *_e1, const void *_e2)
{
	const handshake_buffer_st *e1 = _e1;
	const handshake_buffer_st *e2 = _e2;

	if (e1->sequence <= e2->sequence)
		return 1;
	else
		return -1;
}

#define SSL2_HEADERS 1
static int parse_handshake_header(gnutls_session_t session, mbuffer_st *bufel,
				  handshake_buffer_st *hsk)
{
	uint8_t *dataptr = NULL; /* for realloc */
	size_t handshake_header_size = HANDSHAKE_HEADER_SIZE(session),
	       data_size, frag_size;

	/* Note: SSL2_HEADERS == 1 */
	if (_mbuffer_get_udata_size(bufel) < handshake_header_size)
		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);

	dataptr = _mbuffer_get_udata_ptr(bufel);

	/* if reading a client hello of SSLv2 */
#ifdef ENABLE_SSL2
	if (unlikely(!IS_DTLS(session) &&
		     bufel->htype == GNUTLS_HANDSHAKE_CLIENT_HELLO_V2)) {
		handshake_header_size =
			SSL2_HEADERS; /* we've already read one byte */

		frag_size =
			_mbuffer_get_udata_size(bufel) -
			handshake_header_size; /* we've read the first byte */

		if (dataptr[0] != GNUTLS_HANDSHAKE_CLIENT_HELLO)
			return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET);

		hsk->rtype = hsk->htype = GNUTLS_HANDSHAKE_CLIENT_HELLO_V2;

		hsk->sequence = 0;
		hsk->start_offset = 0;
		hsk->length = frag_size;
	} else
#endif
	{ /* TLS or DTLS handshake headers */

		hsk->rtype = hsk->htype = dataptr[0];

		/* we do not use DECR_LEN because we know
		 * that the packet has enough data.
		 */
		hsk->length = _gnutls_read_uint24(&dataptr[1]);

		if (IS_DTLS(session)) {
			hsk->sequence = _gnutls_read_uint16(&dataptr[4]);
			hsk->start_offset = _gnutls_read_uint24(&dataptr[6]);
			frag_size = _gnutls_read_uint24(&dataptr[9]);
		} else {
			hsk->sequence = 0;
			hsk->start_offset = 0;
			frag_size = MIN((_mbuffer_get_udata_size(bufel) -
					 handshake_header_size),
					hsk->length);
		}

		/* TLS1.3: distinguish server hello versus hello retry request.
		 * The epitome of slick protocol design. */
		if (hsk->htype == GNUTLS_HANDSHAKE_SERVER_HELLO &&
		    hsk->start_offset == 0 && !IS_DTLS(session)) {
			if (_mbuffer_get_udata_size(bufel) >
				    handshake_header_size + 2 +
					    GNUTLS_RANDOM_SIZE &&
			    memcmp(dataptr + handshake_header_size + 2,
				   HRR_RANDOM, GNUTLS_RANDOM_SIZE) == 0) {
				hsk->htype =
					GNUTLS_HANDSHAKE_HELLO_RETRY_REQUEST;
			}
		}
	}
	data_size = _mbuffer_get_udata_size(bufel) - handshake_header_size;

	if (frag_size > 0)
		hsk->end_offset = hsk->start_offset + frag_size - 1;
	else
		hsk->end_offset = 0;

	_gnutls_handshake_log(
		"HSK[%p]: %s (%u) was received. Length %d[%d], frag offset %d, frag length: %d, sequence: %d\n",
		session, _gnutls_handshake2str(hsk->htype),
		(unsigned)hsk->htype, (int)hsk->length, (int)data_size,
		hsk->start_offset, (int)frag_size, (int)hsk->sequence);

	hsk->header_size = handshake_header_size;
	memcpy(hsk->header, _mbuffer_get_udata_ptr(bufel),
	       handshake_header_size);

	if (hsk->length > 0 &&
	    (frag_size > data_size ||
	     (frag_size > 0 && hsk->end_offset >= hsk->length))) {
		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);
	} else if (hsk->length == 0 && hsk->end_offset != 0 &&
		   hsk->start_offset != 0)
		return gnutls_assert_val(GNUTLS_E_UNEXPECTED_PACKET_LENGTH);

	return handshake_header_size;
}

static void _gnutls_handshake_buffer_move(handshake_buffer_st *dst,
					  handshake_buffer_st *src)
{
	memcpy(dst, src, sizeof(*dst));
	memset(src, 0, sizeof(*src));
	src->htype = -1;
}

/* will merge the given handshake_buffer_st to the handshake_recv_buffer
 * list. The given hsk packet will be released in any case (success or failure).
 * Only used in DTLS.
 */
static int merge_handshake_packet(gnutls_session_t session,
				  handshake_buffer_st *hsk)
{
	int exists = 0, i, pos = 0;
	int ret;

	for (i = 0; i < session->internals.handshake_recv_buffer_size; i++) {
		if (session->internals.handshake_recv_buffer[i].htype ==
		    hsk->htype) {
			exists = 1;
			pos = i;
			break;
		}
	}

	if (!exists)
		pos = session->internals.handshake_recv_buffer_size;

	if (pos >= MAX_HANDSHAKE_MSGS)
		return gnutls_assert_val(GNUTLS_E_TOO_MANY_HANDSHAKE_PACKETS);

	if (!exists) {
		if (hsk->length != hsk->data.length) {
			ret = _gnutls_buffer_resize(&hsk->data, hsk->length);
			if (ret < 0)
				return gnutls_assert_val(ret);

			hsk->data.length = hsk->length;
		}

		if (hsk->length > 0 && hsk->end_offset > 0 &&
		    hsk->end_offset - hsk->start_offset + 1 != hsk->length) {
			memmove(&hsk->data.data[hsk->start_offset],
				hsk->data.data,
				hsk->end_offset - hsk->start_offset + 1);
		}

		session->internals.handshake_recv_buffer_size++;

		/* rewrite headers to make them look as each packet came as a single fragment */
		_gnutls_write_uint24(hsk->length, &hsk->header[1]);
		_gnutls_write_uint24(0, &hsk->header[6]);
		_gnutls_write_uint24(hsk->length, &hsk->header[9]);

		_gnutls_handshake_buffer_move(
			&session->internals.handshake_recv_buffer[pos], hsk);

	} else {
		if (hsk->start_offset <
			    session->internals.handshake_recv_buffer[pos]
				    .start_offset &&
		    hsk->end_offset + 1 >=
			    session->internals.handshake_recv_buffer[pos]
				    .start_offset) {
			memcpy(&session->internals.handshake_recv_buffer[pos]
					.data.data[hsk->start_offset],
			       hsk->data.data, hsk->data.length);
			session->internals.handshake_recv_buffer[pos]
				.start_offset = hsk->start_offset;
			session->internals.handshake_recv_buffer[pos]
				.end_offset = MIN(
				hsk->end_offset,
				session->internals.handshake_recv_buffer[pos]
					.end_offset);
		} else if (hsk->end_offset >
				   session->internals.handshake_recv_buffer[pos]
					   .end_offset &&
			   hsk->start_offset <=
				   session->internals.handshake_recv_buffer[pos]
						   .end_offset +
					   1) {
			memcpy(&session->internals.handshake_recv_buffer[pos]
					.data.data[hsk->start_offset],
			       hsk->data.data, hsk->data.length);

			session->internals.handshake_recv_buffer[pos]
				.end_offset = hsk->end_offset;
			session->internals.handshake_recv_buffer[pos]
				.start_offset = MIN(
				hsk->start_offset,
				session->internals.handshake_recv_buffer[pos]
					.start_offset);
		}
		_gnutls_handshake_buffer_clear(hsk);
	}

	return 0;
}

/* returns non-zero on match and zero on mismatch
 */
inline static int cmp_hsk_types(gnutls_handshake_description_t expected,
				gnutls_handshake_description_t recvd)
{
	if (expected == GNUTLS_HANDSHAKE_ANY)
		return 1;

#ifdef ENABLE_SSL2
	if (expected == GNUTLS_HANDSHAKE_CLIENT_HELLO &&
	    recvd == GNUTLS_HANDSHAKE_CLIENT_HELLO_V2)
		return 1;
#endif
	if (expected != recvd)
		return 0;

	return 1;
}

#define LAST_ELEMENT (session->internals.handshake_recv_buffer_size - 1)

/* returns the last stored handshake packet.
 */
static int get_last_packet(gnutls_session_t session,
			   gnutls_handshake_description_t htype,
			   handshake_buffer_st *hsk, unsigned int optional)
{
	handshake_buffer_st *recv_buf =
		session->internals.handshake_recv_buffer;

	if (IS_DTLS(session)) {
		if (session->internals.handshake_recv_buffer_size == 0 ||
		    (session->internals.dtls.hsk_read_seq !=
		     recv_buf[LAST_ELEMENT].sequence))
			goto timeout;

		if (htype != recv_buf[LAST_ELEMENT].htype) {
			if (optional == 0)
				_gnutls_audit_log(
					session,
					"Received unexpected handshake message '%s' (%d). Expected '%s' (%d)\n",
					_gnutls_handshake2str(
						recv_buf[0].htype),
					(int)recv_buf[0].htype,
					_gnutls_handshake2str(htype),
					(int)htype);

			return gnutls_assert_val(
				GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET);
		}

		else if ((recv_buf[LAST_ELEMENT].start_offset == 0 &&
			  recv_buf[LAST_ELEMENT].end_offset ==
				  recv_buf[LAST_ELEMENT].length - 1) ||
			 recv_buf[LAST_ELEMENT].length == 0) {
			session->internals.dtls.hsk_read_seq++;
			_gnutls_handshake_buffer_move(hsk,
						      &recv_buf[LAST_ELEMENT]);
			session->internals.handshake_recv_buffer_size--;
			return 0;
		} else {
			/* if we don't have a complete handshake message, but we
			 * have queued data waiting, try again to reconstruct the
			 * handshake packet, using the queued */
			if (recv_buf[LAST_ELEMENT].end_offset !=
				    recv_buf[LAST_ELEMENT].length - 1 &&
			    record_check_unprocessed(session) > 0)
				return gnutls_assert_val(
					GNUTLS_E_INT_CHECK_AGAIN);
			else
				goto timeout;
		}
	} else { /* TLS */

		if (session->internals.handshake_recv_buffer_size > 0 &&
		    recv_buf[0].length == recv_buf[0].data.length) {
			if (cmp_hsk_types(htype, recv_buf[0].htype) == 0) {
				return gnutls_assert_val(
					GNUTLS_E_UNEXPECTED_HANDSHAKE_PACKET);
			}

			_gnutls_handshake_buffer_move(hsk, &recv_buf[0]);
			session->internals.handshake_recv_buffer_size--;
			return 0;
		} else
			return gnutls_assert_val(
				GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE);
	}

timeout:
	RETURN_DTLS_EAGAIN_OR_TIMEOUT(session, 0);
}

/* This is a receive function for the gnutls handshake
 * protocol. Makes sure that we have received all data.
 *
 * htype is the next handshake packet expected.
 */
int _gnutls_parse_record_buffered_msgs(gnutls_session_t session)
{
	gnutls_datum_t msg;
	mbuffer_st *bufel = NULL, *prev = NULL;
	int ret;
	size_t data_size;
	handshake_buffer_st *recv_buf =
		session->internals.handshake_recv_buffer;

	bufel = _mbuffer_head_get_first(&session->internals.record_buffer,
					&msg);
	if (bufel == NULL)
		return GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE;

	if (!IS_DTLS(session)) {
		ssize_t append, header_size;

		do {
			if (bufel->type != GNUTLS_HANDSHAKE)
				return gnutls_assert_val(
					GNUTLS_E_UNEXPECTED_PACKET);

			if (unlikely(
				    session->internals
						    .handshake_recv_buffer_size ==
					    0 &&
				    msg.size < HANDSHAKE_HEADER_SIZE(session) &&
				    session->internals
						    .handshake_header_recv_buffer
						    .byte_length <
					    HANDSHAKE_HEADER_SIZE(session) -
						    msg.size)) {
				bufel = _mbuffer_head_pop_first(
					&session->internals.record_buffer);
				_mbuffer_enqueue(
					&session->internals
						 .handshake_header_recv_buffer,
					bufel);
				break;
			} else if (session->internals.handshake_recv_buffer_size >
					   0 &&
				   recv_buf[0].length >
					   recv_buf[0].data.length) {
				/* this is the rest of a previous message */
				append = MIN(msg.size,
					     recv_buf[0].length -
						     recv_buf[0].data.length);

				ret = _gnutls_buffer_append_data(
					&recv_buf[0].data, msg.data, append);
				if (ret < 0)
					return gnutls_assert_val(ret);

				_mbuffer_head_remove_bytes(
					&session->internals.record_buffer,
					append);
			} else { /* received new message */
				if (unlikely(
					    session->internals
						    .handshake_header_recv_buffer
						    .length > 0)) {
					bufel = _mbuffer_head_pop_first(
						&session->internals
							 .record_buffer);
					_mbuffer_enqueue(
						&session->internals
							 .handshake_header_recv_buffer,
						bufel);
					ret = _mbuffer_linearize_align16(
						&session->internals
							 .handshake_header_recv_buffer,
						get_total_headers(session));
					if (ret < 0)
						return gnutls_assert_val(ret);
					bufel = _mbuffer_head_pop_first(
						&session->internals
							 .handshake_header_recv_buffer);
					_mbuffer_head_push_first(
						&session->internals
							 .record_buffer,
						bufel);
				}

				ret = parse_handshake_header(session, bufel,
							     &recv_buf[0]);
				if (ret < 0)
					return gnutls_assert_val(ret);

				header_size = ret;
				session->internals.handshake_recv_buffer_size =
					1;

				_mbuffer_set_uhead_size(bufel, header_size);

				data_size = MIN(recv_buf[0].length,
						_mbuffer_get_udata_size(bufel));
				ret = _gnutls_buffer_append_data(
					&recv_buf[0].data,
					_mbuffer_get_udata_ptr(bufel),
					data_size);
				if (ret < 0)
					return gnutls_assert_val(ret);
				_mbuffer_set_uhead_size(bufel, 0);
				_mbuffer_head_remove_bytes(
					&session->internals.record_buffer,
					data_size + header_size);
			}

			/* if packet is complete then return it
			 */
			if (recv_buf[0].length == recv_buf[0].data.length) {
				return 0;
			}
			bufel = _mbuffer_head_get_first(
				&session->internals.record_buffer, &msg);
		} while (bufel != NULL);

		/* if we are here it means that the received packets were not
		 * enough to complete the handshake packet.
		 */
		return gnutls_assert_val(GNUTLS_E_AGAIN);
	} else { /* DTLS */

		handshake_buffer_st tmp;

		do {
			/* we now
			 * 0. parse headers
			 * 1. insert to handshake_recv_buffer
			 * 2. sort handshake_recv_buffer on sequence numbers
			 * 3. return first packet if completed or GNUTLS_E_AGAIN.
			 */
			do {
				if (bufel->type != GNUTLS_HANDSHAKE) {
					gnutls_assert();
					goto next; /* ignore packet */
				}

				_gnutls_handshake_buffer_init(&tmp);

				ret = parse_handshake_header(session, bufel,
							     &tmp);
				if (ret < 0) {
					gnutls_assert();
					_gnutls_audit_log(
						session,
						"Invalid handshake packet headers. Discarding.\n");
					break;
				}

				_mbuffer_consume(
					&session->internals.record_buffer,
					bufel, ret);

				data_size = MIN(tmp.length,
						tmp.end_offset -
							tmp.start_offset + 1);

				ret = _gnutls_buffer_append_data(
					&tmp.data,
					_mbuffer_get_udata_ptr(bufel),
					data_size);
				if (ret < 0)
					return gnutls_assert_val(ret);

				_mbuffer_consume(
					&session->internals.record_buffer,
					bufel, data_size);

				ret = merge_handshake_packet(session, &tmp);
				if (ret < 0)
					return gnutls_assert_val(ret);

			} while (_mbuffer_get_udata_size(bufel) > 0);

			prev = bufel;
			bufel = _mbuffer_dequeue(
				&session->internals.record_buffer, bufel);

			_mbuffer_xfree(&prev);
			continue;

		next:
			bufel = _mbuffer_head_get_next(bufel, NULL);
		} while (bufel != NULL);

		/* sort in descending order */
		if (session->internals.handshake_recv_buffer_size > 1)
			qsort(recv_buf,
			      session->internals.handshake_recv_buffer_size,
			      sizeof(recv_buf[0]), handshake_compare);

		while (session->internals.handshake_recv_buffer_size > 0 &&
		       recv_buf[LAST_ELEMENT].sequence <
			       session->internals.dtls.hsk_read_seq) {
			_gnutls_audit_log(
				session,
				"Discarded replayed handshake packet with sequence %d\n",
				recv_buf[LAST_ELEMENT].sequence);
			_gnutls_handshake_buffer_clear(&recv_buf[LAST_ELEMENT]);
			session->internals.handshake_recv_buffer_size--;
		}

		return 0;
	}
}

/* This is a receive function for the gnutls handshake
 * protocol. Makes sure that we have received all data.
 */
ssize_t _gnutls_handshake_io_recv_int(gnutls_session_t session,
				      gnutls_handshake_description_t htype,
				      handshake_buffer_st *hsk,
				      unsigned int optional)
{
	int ret;
	unsigned int tleft = 0;
	int retries = 7;

	ret = get_last_packet(session, htype, hsk, optional);
	if (ret != GNUTLS_E_AGAIN && ret != GNUTLS_E_INTERRUPTED &&
	    ret != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE &&
	    ret != GNUTLS_E_INT_CHECK_AGAIN) {
		return gnutls_assert_val(ret);
	}

	/* try using the already existing records before
	 * trying to receive.
	 */
	ret = _gnutls_parse_record_buffered_msgs(session);

	if (ret == 0) {
		ret = get_last_packet(session, htype, hsk, optional);
	}

	if (IS_DTLS(session)) {
		if (ret >= 0)
			return ret;
	} else {
		if ((ret != GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE && ret < 0) ||
		    ret >= 0)
			return gnutls_assert_val(ret);
	}

	/* If handshake is handled manually, don't receive records from I/O */
	if (session->internals.h_read_func)
		return GNUTLS_E_AGAIN;

	if (htype != (gnutls_handshake_description_t)-1) {
		ret = handshake_remaining_time(session);
		if (ret < 0)
			return gnutls_assert_val(ret);
		tleft = ret;
	}

	do {
		/* if we don't have a complete message waiting for us, try
		 * receiving more */
		ret = _gnutls_recv_in_buffers(session, GNUTLS_HANDSHAKE, htype,
					      tleft);
		if (ret < 0)
			return gnutls_assert_val_fatal(ret);

		ret = _gnutls_parse_record_buffered_msgs(session);
		if (ret == 0) {
			ret = get_last_packet(session, htype, hsk, optional);
		}
		/* we put an upper limit (retries) to the number of partial handshake
		 * messages in a record packet. */
	} while (IS_DTLS(session) && ret == GNUTLS_E_INT_CHECK_AGAIN &&
		 retries-- > 0);

	if (unlikely(IS_DTLS(session) && ret == GNUTLS_E_INT_CHECK_AGAIN)) {
		ret = gnutls_assert_val(GNUTLS_E_TOO_MANY_HANDSHAKE_PACKETS);
	}

	return ret;
}
