/*****************************************************************************************************
 * Software License Agreement (BSD License)
 * Author : Souheil Ben Ayed <souheil@tera.ics.keio.ac.jp>
 *
 * Copyright (c) 2009-2010, Souheil Ben Ayed, Teraoka Laboratory of Keio University, and the WIDE Project
 * All rights reserved.
 *
 * Redistribution and use of this software in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *    This product includes software developed by Souheil Ben Ayed <souheil@tera.ics.keio.ac.jp>.
 *
 * 4. Neither the name of Souheil Ben Ayed, Teraoka Laboratory of Keio University or the WIDE Project nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *****************************************************************************************************/


#include "diameap_tls.h"
#include "libdiameap.h"

//GCRY_THREAD_OPTION_PTHREAD_IMPL;

int diameap_tls_init(struct tls_config * tls_conf)
{
	CHECK_GNUTLS_DO( gnutls_global_init(), return EINVAL );

	gnutls_global_set_log_function(diameap_tls_log);
	//gnutls_global_set_log_level(9);


	if(tls_conf->cafile ==NULL){
		fd_log_error("%s[EAP-TLS] Missing certification authority (CA) certificates. Please provide CA configuration directive.", DIAMEAP_EXTENSION);
		return EINVAL;
	}
	if( !tls_conf->certfile || !tls_conf->keyfile){
		fd_log_error("%s[EAP-TLS] Missing private Key. Please provide Cred configuration directive.", DIAMEAP_EXTENSION);
		return EINVAL;
	}

	CHECK_GNUTLS_DO( gnutls_certificate_allocate_credentials(&tls_conf->cert_cred), return EINVAL );

	CHECK_GNUTLS_DO( gnutls_certificate_set_x509_trust_file(tls_conf->cert_cred,
			tls_conf->cafile, GNUTLS_X509_FMT_PEM), return EINVAL );

	if (tls_conf->crlfile)
	{
		CHECK_GNUTLS_DO( gnutls_certificate_set_x509_crl_file(tls_conf->cert_cred,
				tls_conf->crlfile, GNUTLS_X509_FMT_PEM), return EINVAL );
	}

	CHECK_GNUTLS_DO( gnutls_certificate_set_x509_key_file(tls_conf->cert_cred,
			tls_conf->certfile, tls_conf->keyfile, GNUTLS_X509_FMT_PEM), return EINVAL );

	return 0;
}

void diameap_tls_log(int lev, const char * text)
{
	u8 * msg;
	if (text == NULL)
		return;
	msg = (u8 *) strdup(text);
	int i;
	for (i = 0; (G8(text+i) != '\n') && (G8(text+i) != '\0'); i++)
	{
	}
	P8((msg+i),'\0');
	fd_log_debug("%s[EAP-TLS] GNUTLS log[%d] : %s", DIAMEAP_EXTENSION, lev, msg);
	free(msg);
}

int diameap_tls_init_session(struct tls_config * tls_conf,
		struct tls_data * data)
{
	CHECK_GNUTLS_DO( gnutls_init(&data->session, GNUTLS_SERVER), return EINVAL );

	CHECK_GNUTLS_DO( gnutls_set_default_priority(data->session), return EINVAL );

	CHECK_GNUTLS_DO( gnutls_credentials_set(data->session, GNUTLS_CRD_CERTIFICATE,
			tls_conf->cert_cred), return EINVAL);

	/* request client certificate if any.
	 */
	gnutls_certificate_server_set_request(data->session, GNUTLS_CERT_REQUIRE); //GNUTLS_CERT_REQUEST);

	gnutls_transport_set_pull_function(data->session, diameap_tls_receive);
	gnutls_transport_set_push_function(data->session, diameap_tls_send);
	gnutls_transport_set_ptr(data->session, (gnutls_transport_ptr_t) data);

	/* starting version 2.12, this call is not needed */
	//gnutls_transport_set_lowat(data->session, 0);
	
	return 0;
}

ssize_t diameap_tls_receive(gnutls_transport_ptr_t ptr, void *buffer,
		size_t length)
{
	struct tls_data * data = (struct tls_data *) ptr;
	if (data->p_length == 0)
	{
		errno = EWOULDBLOCK;
		return -1;
	}
	if (length > data->p_length)
	{
		length = data->p_length;
	}
	memcpy(buffer, data->tlsResp.data + (data->tlsResp.datalength
			- data->p_length), length);
	data->p_length -= length;

	return length;
}

ssize_t diameap_tls_send(gnutls_transport_ptr_t ptr, const void *buffer,
		size_t length)
{
	struct tls_data * data = (struct tls_data *) ptr;
	data->tlsReq.data = realloc(data->tlsReq.data, data->tlsReq.datalength
			+ length);
	U8COPY(data->tlsReq.data,data->tlsReq.datalength,length,(u8*)buffer);
	data->tlsReq.datalength += length;

	return length;
}

int diameap_tls_new(struct tls_msg * tlsmsg)
{
	if (!tlsmsg)
	{
		return EINVAL;
	}
	tlsmsg->flags = 0x00;
	tlsmsg->length = 0;
	tlsmsg->data = NULL;
	tlsmsg->datalength = 0;
	return 0;
}

int diameap_tls_get_flags(struct tls_msg tlsmsg, u8 * flags)
{
	*flags = tlsmsg.flags;
	return 0;
}

int diameap_tls_set_flags(struct tls_msg * tlsmsg, u8 flags)
{
	if (!tlsmsg)
	{
		return EINVAL;
	}
	if ((flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
	{
		flags = flags ^ TLS_FLAG_LENGTH;
	}
	tlsmsg->flags = tlsmsg->flags | flags;
	return 0;
}

int diameap_tls_get_message_length(struct tls_msg tlsmsg, u32 * length)
{
	if ((tlsmsg.flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
	{
		*length = tlsmsg.length;
	}
	else
	{
		*length = 0;
	}
	return 0;
}

int diameap_tls_set_message_length(struct tls_msg * tlsmsg, u32 length)
{
	if (!tlsmsg)
	{
		return EINVAL;
	}
	if (length > 0)
	{
		tlsmsg->length = length;
		tlsmsg->flags = tlsmsg->flags | TLS_FLAG_LENGTH;
	}
	else
	{
		tlsmsg->length = 0;
		if ((tlsmsg->flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
		{
			tlsmsg->flags = tlsmsg->flags ^ TLS_FLAG_LENGTH;
		}
	}
	return 0;
}

int diameap_tls_get_data(struct tls_msg tlsmsg, u8** tls_data,
		u32 * data_length)
{
	if (tlsmsg.datalength > 0)
	{
		*tls_data = malloc(sizeof(u8) * tlsmsg.datalength);
		U8COPY(*tls_data,0,tlsmsg.datalength,tlsmsg.data);
		*data_length = tlsmsg.datalength;
	}
	else
	{
		*tls_data = NULL;
		*data_length = 0;
	}
	return 0;
}

int diameap_tls_set_data(struct tls_msg * tlsmsg, u8* tls_data, int data_length)
{
	if (!tlsmsg)
	{
		return EINVAL;
	}
	tlsmsg->data = malloc(sizeof(u8) * data_length);
	U8COPY(tlsmsg->data,0,data_length,tls_data);
	tlsmsg->datalength = data_length;
	return 0;
}

/*
 * data : returned data
 * eaptls_data : the TLS_Data field
 * length : the length of eaptls_data
 * flags : combination of flags et set
 */
int diameap_tls_new_tls_packet(u8** data, int * len, struct tls_msg tlsmsg)
{
	int buflen, pos = 0;

	buflen = 1;

	if ((tlsmsg.flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
	{
		buflen += 4;
	}

	if (tlsmsg.datalength > 0)
	{
		buflen += tlsmsg.datalength;
	}
	*data = malloc(sizeof(u8) * buflen);
	memset(*data, 0, sizeof(u8) * buflen);
	P8(*data,tlsmsg.flags);
	pos++;

	if ((tlsmsg.flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
	{
		P32BIGE(*data+pos,tlsmsg.length);
		pos += 4;
	}
	if (tlsmsg.data)
	{
		U8COPY(*data,pos,tlsmsg.datalength,tlsmsg.data);
	}
	*len = buflen;
	return 0;
}

int diameap_set_tls(struct tls_msg * tlsmsg, u8 flags, u32 length,
		u8 *tls_data, int data_length)
{
	diameap_tls_new(tlsmsg);
	diameap_tls_set_flags(tlsmsg, flags);
	diameap_tls_set_message_length(tlsmsg, length);
	diameap_tls_set_data(tlsmsg, tls_data, data_length);
	return 0;
}

int diameap_tls_parse(u8* data, int len, struct tls_msg * tlsmsg)
{

	if (data == NULL)
		return EINVAL;
	int pos = 0;
	diameap_tls_new(tlsmsg);
	tlsmsg->flags = G8(data);
	pos++;
	if ((tlsmsg->flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
	{
		tlsmsg->length = G32BIGE(data+1);
		pos = pos + 4;
	}

	if (len > pos)
	{
		tlsmsg->data = malloc(sizeof(u8) * (len - pos));
		U8COPY(tlsmsg->data,0,(len-pos),data+pos);
		tlsmsg->datalength = len - pos;
	}
	else
	{
		tlsmsg->data = NULL;
		tlsmsg->datalength = 0;
	}
	return 0;
}

void diameap_tls_dump(struct tls_msg tlsmsg)
{
	u8 * data = NULL;
	u32 len;
	diameap_tls_get_data(tlsmsg, &data, &len);

	char * buf = NULL;
	size_t buflen = 0, bufoff = 0;

	CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "-------------Dump EAP-TLS msg-------------\n"), );

	u8 flags;
	diameap_tls_get_flags(tlsmsg, &flags);
	CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "\t -flags       		: %02x ", flags), );
	if (flags & TLS_FLAG_LENGTH)
		CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, " TLS_FLAG_LENGTH "), );
	if (flags & TLS_FLAG_MORE)
		CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, " TLS_FLAG_MORE "), );
	if (flags & TLS_FLAG_START)
		CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, " TLS_FLAG_START "), );
	CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "\n"), );
	if ((tlsmsg.flags & TLS_FLAG_LENGTH) == TLS_FLAG_LENGTH)
	{
		u32 length;
		diameap_tls_get_message_length(tlsmsg, &length);
		CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "\t -TLS msg length	: %u (0x%02x%02x%02x%02x)\n",
				length, (length >> 24) & 0xffU, (length >> 16) & 0xffU, (length
						>> 8) & 0xffU, length & 0xffU), );
	}
	CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "\t -data length		: %d \n", len), );
	/*
	if (len > 0)
	{
		int i;
		CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "\t -Data			: "), );
		for (i = 0; i < len; i++)
		{
			CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "%02x ", G8(data + i)), );
		}
		CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "\n"), );
	}
	*/
	CHECK_MALLOC_DO( fd_dump_extend( &buf, &buflen, &bufoff, "-------------End Dump EAP-TLS msg-------------\n"), );

	fd_log_debug("%s", buf ?: "Error dumping EAP-TLS msg");
	
	free(data);
	free(buf);
}

int diameap_tls_initialize(struct tls_data * data)
{
	if (!data)
	{
		return EINVAL;
	}
	data->state = START;
	data->more_tosend_length = 0;
	data->more_toreceive = FALSE;
	data->handshake = FALSE;
	return 0;
}

int diameap_tls_reassemble(struct tls_msg * to, struct tls_msg from)
{
	u8 from_flag;
	diameap_tls_get_flags(from, &from_flag);

	if (from_flag & TLS_FLAG_LENGTH)
	{
		diameap_tls_new(to);
		u32 length;
		diameap_tls_get_message_length(from, &length);
		diameap_tls_set_message_length(to, length);
	}
	diameap_tls_set_flags(to, from_flag);

	u8 * tlsRespData;
	u32 tlsRespDataLength;
	diameap_tls_get_data(from, &tlsRespData, &tlsRespDataLength);
	to->data = realloc(to->data, to->datalength + tlsRespDataLength);
	U8COPY(to->data,to->datalength,tlsRespDataLength,tlsRespData);
	to->datalength += tlsRespDataLength;
	free(tlsRespData);
	return 0;
}

int diameap_tls_copy(struct tls_msg * to, struct tls_msg from)
{
	u8 flag;
	u32 length;
	u8 * data;
	diameap_tls_new(to);
	diameap_tls_get_flags(from, &flag);
	diameap_tls_set_flags(to, flag);
	diameap_tls_get_message_length(from, &length);
	diameap_tls_set_message_length(to, length);
	length = 0;
	diameap_tls_get_data(from, &data, &length);
	diameap_tls_set_data(to, data, length);
	return 0;
}

int diameap_tls_process_receive(struct tls_data * data)
{
	int ret;

	data->p_length = data->tlsResp.datalength;

	ret = gnutls_handshake(data->session);

	if (ret < 0)
	{
		switch (ret)
		{
		case GNUTLS_E_AGAIN:
			break;
		case GNUTLS_E_INTERRUPTED:
			fd_log_debug("%s[EAP-TLS] gnutls handshake : GNUTLS_E_INTERRUPTED: %s",
				DIAMEAP_EXTENSION, gnutls_strerror(ret));
			break;
		case GNUTLS_E_GOT_APPLICATION_DATA:
			fd_log_debug("%s[EAP-TLS] gnutls handshake : GNUTLS_E_GOT_APPLICATION_DATA: %s",
				DIAMEAP_EXTENSION, gnutls_strerror(ret));
			break;
		case GNUTLS_E_WARNING_ALERT_RECEIVED:
			fd_log_debug("%sEAP-TLS] gnutls handshake : GNUTLS_E_WARNING_ALERT_RECEIVED: %s",
				DIAMEAP_EXTENSION, gnutls_strerror(ret));
			break;
		}
	}
	if (ret == GNUTLS_E_SUCCESS)
	{
		data->handshake = TRUE;
	}
	return 0;
}

