/*
 * Copyright (C) 2018 Tobias Brunner
 * Copyright (C) 2008-2009 Martin Willi
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#include "ha_socket.h"
#include "ha_plugin.h"

#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <unistd.h>

#include <daemon.h>
#include <networking/host.h>
#include <threading/thread.h>
#include <processing/jobs/callback_job.h>

/**
 * Default receive buffer size
 */
#ifndef HA_SOCKET_RCVBUF_DEFAULT
#define HA_SOCKET_RCVBUF_DEFAULT (8 * 1024 * 1024)
#endif

typedef struct private_ha_socket_t private_ha_socket_t;

/**
 * Private data of an ha_socket_t object.
 */
struct private_ha_socket_t {

	/**
	 * Public ha_socket_t interface.
	 */
	ha_socket_t public;

	/**
	 * UDP communication socket fd
	 */
	int fd;

	/**
	 * local host to receive/send from
	 */
	host_t *local;

	/**
	 * remote host to receive/send to
	 */
	host_t *remote;

	/**
	 * Receive buffer size
	 */
	u_int buflen;
};

/**
 * Data to pass to the send_message() callback job
 */
typedef struct {
	chunk_t chunk;
	int fd;
} job_data_t;

/**
 * Cleanup job data
 */
static void job_data_destroy(job_data_t *this)
{
	free(this->chunk.ptr);
	free(this);
}

/**
 * Callback to asynchronously send messages
 */
static job_requeue_t send_message(job_data_t *data)
{
	if (send(data->fd, data->chunk.ptr, data->chunk.len, 0) < data->chunk.len)
	{
		DBG1(DBG_CFG, "pushing HA message failed: %s", strerror(errno));
	}
	return JOB_REQUEUE_NONE;
}

METHOD(ha_socket_t, push, void,
	private_ha_socket_t *this, ha_message_t *message)
{
	chunk_t chunk;

	/* Try to send synchronously, but non-blocking. */
	chunk = message->get_encoding(message);
	if (send(this->fd, chunk.ptr, chunk.len, MSG_DONTWAIT) < chunk.len)
	{
		if (errno == EAGAIN)
		{
			callback_job_t *job;
			job_data_t *data;

			/* Fallback to asynchronous transmission. This is required, as sendto()
			 * is a blocking call if it acquires a policy. We could end up in a
			 * deadlock, as we own an IKE_SA. */
			INIT(data,
				.chunk = chunk_clone(chunk),
				.fd = this->fd,
			);

			job = callback_job_create_with_prio((callback_job_cb_t)send_message,
							data, (void*)job_data_destroy, NULL, JOB_PRIO_HIGH);
			lib->processor->queue_job(lib->processor, (job_t*)job);
			return;
		}
		DBG1(DBG_CFG, "pushing HA message failed: %s", strerror(errno));
	}
}

METHOD(ha_socket_t, pull, ha_message_t*,
	private_ha_socket_t *this)
{
	while (TRUE)
	{
		ha_message_t *message;
		char buf[this->buflen];
		struct iovec iov = {
			.iov_base = buf,
			.iov_len = this->buflen,
		};
		struct msghdr msg = {
			.msg_iov = &iov,
			.msg_iovlen = 1,
		};
		bool oldstate;
		ssize_t len;

		oldstate = thread_cancelability(TRUE);
		len = recvmsg(this->fd, &msg, 0);
		thread_cancelability(oldstate);
		if (msg.msg_flags & MSG_TRUNC)
		{
			DBG1(DBG_CFG, "HA message exceeds receive buffer");
			continue;
		}
		if (len <= 0)
		{
			switch (errno)
			{
				case ECONNREFUSED:
				case EINTR:
					continue;
				default:
					DBG1(DBG_CFG, "pulling HA message failed: %s",
						 strerror(errno));
					sleep(1);
					continue;
			}
		}
		message = ha_message_parse(chunk_create(buf, len));
		if (message)
		{
			return message;
		}
	}
}

/**
 * Open and connect the HA socket
 */
static bool open_socket(private_ha_socket_t *this)
{
	int rcvbuf_size = 0;

	this->fd = socket(this->local->get_family(this->local), SOCK_DGRAM, 0);
	if (this->fd == -1)
	{
		DBG1(DBG_CFG, "opening HA socket failed: %s", strerror(errno));
		return FALSE;
	}

	if (bind(this->fd, this->local->get_sockaddr(this->local),
			 *this->local->get_sockaddr_len(this->local)) == -1)
	{
		DBG1(DBG_CFG, "binding HA socket failed: %s", strerror(errno));
		close(this->fd);
		this->fd = -1;
		return FALSE;
	}

	rcvbuf_size = lib->settings->get_int(lib->settings,
										 "%s.plugins.ha.receive_buffer_size",
										 HA_SOCKET_RCVBUF_DEFAULT, lib->ns);
	if (rcvbuf_size)
	{
#ifdef SO_RCVBUFFORCE
		if (setsockopt(this->fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf_size,
					   sizeof(rcvbuf_size)) == -1)
#endif
		{
			if (setsockopt(this->fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf_size,
						   sizeof(rcvbuf_size)) == -1)
			{
				DBG1(DBG_CFG, "failed to set receive buffer size to %d: %s",
					 rcvbuf_size, strerror(errno));
			}
		}
	}

	if (connect(this->fd, this->remote->get_sockaddr(this->remote),
				*this->remote->get_sockaddr_len(this->remote)) == -1)
	{
		DBG1(DBG_CFG, "connecting HA socket failed: %s", strerror(errno));
		close(this->fd);
		this->fd = -1;
		return FALSE;
	}

	return TRUE;
}

METHOD(ha_socket_t, destroy, void,
	private_ha_socket_t *this)
{
	if (this->fd != -1)
	{
		close(this->fd);
	}
	DESTROY_IF(this->local);
	DESTROY_IF(this->remote);
	free(this);
}

/**
 * See header
 */
ha_socket_t *ha_socket_create(char *local, char *remote)
{
	private_ha_socket_t *this;

	INIT(this,
		.public = {
			.push = _push,
			.pull = _pull,
			.destroy = _destroy,
		},
		.local = host_create_from_dns(local, 0, HA_PORT),
		.remote = host_create_from_dns(remote, 0, HA_PORT),
		.buflen = lib->settings->get_int(lib->settings,
										 "%s.plugins.ha.buflen", 2048, lib->ns),
		.fd = -1,
	);

	if (!this->local || !this->remote)
	{
		DBG1(DBG_CFG, "invalid local/remote HA address");
		destroy(this);
		return NULL;
	}
	if (!open_socket(this))
	{
		destroy(this);
		return NULL;
	}
	return &this->public;
}

