/*
 *
 *  Web service library with GLib integration
 *
 *  Copyright (C) 2009-2012  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#include <gnutls/gnutls.h>

#include "giognutls.h"

//#define DBG(fmt, arg...)  printf("%s: " fmt "\n" , __func__ , ## arg)
#define DBG(fmt, arg...)

typedef struct _GIOGnuTLSChannel GIOGnuTLSChannel;
typedef struct _GIOGnuTLSWatch GIOGnuTLSWatch;

struct _GIOGnuTLSChannel {
	GIOChannel channel;
	gint fd;
	gnutls_certificate_credentials_t cred;
	gnutls_session_t session;
	bool established;
	bool again;
};

struct _GIOGnuTLSWatch {
	GSource source;
	GPollFD pollfd;
	GIOChannel *channel;
	GIOCondition condition;
};

static volatile int global_init_done = 0;

static inline void g_io_gnutls_global_init(void)
{
	if (__sync_bool_compare_and_swap(&global_init_done, 0, 1))
		gnutls_global_init();
}

static GIOStatus check_handshake(GIOChannel *channel, GError **err)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;
	int result;

	DBG("channel %p", channel);

	if (gnutls_channel->established)
		return G_IO_STATUS_NORMAL;

again:
	result = gnutls_handshake(gnutls_channel->session);

	if (result == GNUTLS_E_INTERRUPTED || result == GNUTLS_E_AGAIN) {
		GIOFlags flags = g_io_channel_get_flags(channel);

		if (gnutls_channel->again)
			return G_IO_STATUS_AGAIN;

		if (flags & G_IO_FLAG_NONBLOCK)
			return G_IO_STATUS_AGAIN;

		goto again;
	}

	if (result < 0) {
		g_set_error(err, G_IO_CHANNEL_ERROR,
				G_IO_CHANNEL_ERROR_FAILED, "Handshake failed");
		return G_IO_STATUS_ERROR;
	}

	gnutls_channel->established = true;

	DBG("handshake done");

	return G_IO_STATUS_NORMAL;
}

static GIOStatus g_io_gnutls_read(GIOChannel *channel, gchar *buf,
				gsize count, gsize *bytes_read, GError **err)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;
	GIOStatus status;
	ssize_t result;

	DBG("channel %p count %zu", channel, count);

	*bytes_read = 0;

again:
	status = check_handshake(channel, err);
	if (status != G_IO_STATUS_NORMAL)
		return status;

	result = gnutls_record_recv(gnutls_channel->session, buf, count);

	DBG("result %zd", result);

	if (result == GNUTLS_E_REHANDSHAKE) {
		gnutls_channel->established = false;
		goto again;
	}

	if (result == GNUTLS_E_INTERRUPTED || result == GNUTLS_E_AGAIN) {
		GIOFlags flags = g_io_channel_get_flags(channel);

		if (gnutls_channel->again)
			return G_IO_STATUS_AGAIN;

		if (flags & G_IO_FLAG_NONBLOCK)
			return G_IO_STATUS_AGAIN;

		goto again;
	}

	if (result == GNUTLS_E_UNEXPECTED_PACKET_LENGTH)
		return G_IO_STATUS_EOF;

	if (result < 0) {
		g_set_error(err, G_IO_CHANNEL_ERROR,
				G_IO_CHANNEL_ERROR_FAILED, "Stream corrupted");
		return G_IO_STATUS_ERROR;
	}

	*bytes_read = result;

	return (result > 0) ? G_IO_STATUS_NORMAL : G_IO_STATUS_EOF;
}

static GIOStatus g_io_gnutls_write(GIOChannel *channel, const gchar *buf,
				gsize count, gsize *bytes_written, GError **err)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;
	GIOStatus status;
	ssize_t result;

	DBG("channel %p count %zu", channel, count);

	*bytes_written = 0;

again:
	status = check_handshake(channel, err);
	if (status != G_IO_STATUS_NORMAL)
		return status;

	result = gnutls_record_send(gnutls_channel->session, buf, count);

	DBG("result %zd", result);

	if (result == GNUTLS_E_REHANDSHAKE) {
		gnutls_channel->established = false;
		goto again;
	}

	if (result == GNUTLS_E_INTERRUPTED || result == GNUTLS_E_AGAIN) {
		GIOFlags flags = g_io_channel_get_flags(channel);

		if (gnutls_channel->again)
			return G_IO_STATUS_AGAIN;

		if (flags & G_IO_FLAG_NONBLOCK)
			return G_IO_STATUS_AGAIN;

		goto again;
	}

	if (result < 0) {
		g_set_error(err, G_IO_CHANNEL_ERROR,
				G_IO_CHANNEL_ERROR_FAILED, "Stream corrupted");
		return G_IO_STATUS_ERROR;
        }

	*bytes_written = result;

	return (result > 0) ? G_IO_STATUS_NORMAL : G_IO_STATUS_EOF;
}

static GIOStatus g_io_gnutls_seek(GIOChannel *channel, gint64 offset,
						GSeekType type, GError **err)
{
	DBG("channel %p", channel);

	g_set_error_literal(err, G_IO_CHANNEL_ERROR,
				G_IO_CHANNEL_ERROR_FAILED, "Not supported");
	return G_IO_STATUS_ERROR;
}

static GIOStatus g_io_gnutls_close(GIOChannel *channel, GError **err)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;

	DBG("channel %p", channel);

	if (gnutls_channel->established)
		gnutls_bye(gnutls_channel->session, GNUTLS_SHUT_RDWR);

	if (close(gnutls_channel->fd) < 0) {
		g_set_error_literal(err, G_IO_CHANNEL_ERROR,
				G_IO_CHANNEL_ERROR_FAILED, "Closing failed");
		return G_IO_STATUS_ERROR;
	}

	return G_IO_STATUS_NORMAL;
}

static void g_io_gnutls_free(GIOChannel *channel)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;

	DBG("channel %p", channel);

	gnutls_deinit(gnutls_channel->session);

	gnutls_certificate_free_credentials(gnutls_channel->cred);

	g_free(gnutls_channel);
}

static GIOStatus g_io_gnutls_set_flags(GIOChannel *channel,
						GIOFlags flags, GError **err)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;
	glong fcntl_flags = 0;

	DBG("channel %p flags %u", channel, flags);

	if (flags & G_IO_FLAG_NONBLOCK)
		fcntl_flags |= O_NONBLOCK;

	if (fcntl(gnutls_channel->fd, F_SETFL, fcntl_flags) < 0) {
		g_set_error_literal(err, G_IO_CHANNEL_ERROR,
			G_IO_CHANNEL_ERROR_FAILED, "Setting flags failed");
		return G_IO_STATUS_ERROR;
	}

	return G_IO_STATUS_NORMAL;
}

static GIOFlags g_io_gnutls_get_flags(GIOChannel *channel)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;
	GIOFlags flags = 0;
	glong fcntl_flags;

	DBG("channel %p", channel);

	fcntl_flags = fcntl(gnutls_channel->fd, F_GETFL);
	if (fcntl_flags < 0)
		return 0;

	if (fcntl_flags & O_NONBLOCK)
		flags |= G_IO_FLAG_NONBLOCK;

	return flags;
}

static gboolean g_io_gnutls_prepare(GSource *source, gint *timeout)
{
	DBG("source %p", source);

	*timeout = -1;

	return FALSE;
}

static gboolean g_io_gnutls_check(GSource *source)
{
	GIOGnuTLSWatch *watch = (GIOGnuTLSWatch *) source;
	GIOCondition condition = watch->pollfd.revents;

	DBG("source %p condition %u", source, condition);

	if (condition & watch->condition)
		return TRUE;

	return FALSE;
}

static gboolean g_io_gnutls_dispatch(GSource *source, GSourceFunc callback,
							gpointer user_data)
{
	GIOGnuTLSWatch *watch = (GIOGnuTLSWatch *) source;
	GIOFunc func = (GIOFunc) (void (*) (void)) callback;
	GIOCondition condition = watch->pollfd.revents;

	DBG("source %p condition %u", source, condition);

	if (!func)
		return FALSE;

	return func(watch->channel, condition & watch->condition, user_data);
}

static void g_io_gnutls_finalize(GSource *source)
{
	GIOGnuTLSWatch *watch = (GIOGnuTLSWatch *) source;

	DBG("source %p", source);

	g_io_channel_unref(watch->channel);
}

static GSourceFuncs gnutls_watch_funcs = {
	g_io_gnutls_prepare,
	g_io_gnutls_check,
	g_io_gnutls_dispatch,
	g_io_gnutls_finalize,
};

static GSource *g_io_gnutls_create_watch(GIOChannel *channel,
						GIOCondition condition)
{
	GIOGnuTLSChannel *gnutls_channel = (GIOGnuTLSChannel *) channel;
	GIOGnuTLSWatch *watch;
	GSource *source;

	DBG("channel %p condition %u", channel, condition);

	source = g_source_new(&gnutls_watch_funcs, sizeof(GIOGnuTLSWatch));

	watch = (GIOGnuTLSWatch *) source;

	watch->channel = channel;
	g_io_channel_ref(channel);

	watch->condition = condition;

	watch->pollfd.fd = gnutls_channel->fd;
	watch->pollfd.events = condition;

	g_source_add_poll(source, &watch->pollfd);

	return source;
}

static GIOFuncs gnutls_channel_funcs = {
	g_io_gnutls_read,
	g_io_gnutls_write,
	g_io_gnutls_seek,
	g_io_gnutls_close,
	g_io_gnutls_create_watch,
	g_io_gnutls_free,
	g_io_gnutls_set_flags,
	g_io_gnutls_get_flags,
};

static ssize_t g_io_gnutls_push_func(gnutls_transport_ptr_t transport_data,
						const void *buf, size_t count)
{
	GIOGnuTLSChannel *gnutls_channel = transport_data;
	ssize_t result;

	DBG("count %zu", count);

	result = write(gnutls_channel->fd, buf, count);

	if (result < 0 && errno == EAGAIN)
		gnutls_channel->again = true;
	else
		gnutls_channel->again = false;

	DBG("result %zd", result);

	return result;
}

static ssize_t g_io_gnutls_pull_func(gnutls_transport_ptr_t transport_data,
						void *buf, size_t count)
{
	GIOGnuTLSChannel *gnutls_channel = transport_data;
	ssize_t result;

	DBG("count %zu", count);

	result = read(gnutls_channel->fd, buf, count);

	if (result < 0 && errno == EAGAIN)
		gnutls_channel->again = true;
	else
		gnutls_channel->again = false;

	DBG("result %zd", result);

	return result;
}

bool g_io_channel_supports_tls(void)
{
	return true;
}

GIOChannel *g_io_channel_gnutls_new(int fd)
{
	GIOGnuTLSChannel *gnutls_channel;
	GIOChannel *channel;
	int err;

	DBG("");

	gnutls_channel = g_new0(GIOGnuTLSChannel, 1);

	channel = (GIOChannel *) gnutls_channel;

	g_io_channel_init(channel);
	channel->funcs = &gnutls_channel_funcs;

	gnutls_channel->fd = fd;

	channel->is_seekable = FALSE;
	channel->is_readable = TRUE;
	channel->is_writeable = TRUE;

	channel->do_encode = FALSE;

	g_io_gnutls_global_init();

        err = gnutls_init(&gnutls_channel->session, GNUTLS_CLIENT);
	if (err < 0) {
		g_free(gnutls_channel);
		return NULL;
	}

	gnutls_transport_set_ptr(gnutls_channel->session, gnutls_channel);
        gnutls_transport_set_push_function(gnutls_channel->session,
						g_io_gnutls_push_func);
        gnutls_transport_set_pull_function(gnutls_channel->session,
						g_io_gnutls_pull_func);
#if GNUTLS_VERSION_NUMBER < 0x020c00
	gnutls_transport_set_lowat(gnutls_channel->session, 0);

	gnutls_priority_set_direct(gnutls_channel->session,
						"NORMAL:%COMPAT", NULL);
#else
	gnutls_priority_set_direct(gnutls_channel->session,
		"NORMAL:-VERS-TLS-ALL:+VERS-TLS1.0:+VERS-SSL3.0:%COMPAT", NULL);
#endif

	gnutls_certificate_allocate_credentials(&gnutls_channel->cred);
	gnutls_credentials_set(gnutls_channel->session,
				GNUTLS_CRD_CERTIFICATE, gnutls_channel->cred);

	DBG("channel %p", channel);

	return channel;
}
