/*
 * gnome-keyring
 *
 * Copyright (C) 2009 Stefan Walter
 *
 * 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 2.1 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
 * 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
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "gkd-control.h"
#include "gkd-control-codes.h"

#include "daemon/gkd-main.h"
#include "daemon/gkd-util.h"

#include "egg/egg-buffer.h"
#include "egg/egg-cleanup.h"
#include "egg/egg-secure-memory.h"
#include "egg/egg-unix-credentials.h"

#include "daemon/login/gkd-login.h"

#include <errno.h>
#include <fcntl.h>
#include <string.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>

#ifdef WITH_SYSTEMD
#include <systemd/sd-daemon.h>
#endif

typedef struct _ControlData {
	EggBuffer buffer;
	gsize position;
} ControlData;

EGG_SECURE_DECLARE (control_server);

static gchar *control_path;

/* -----------------------------------------------------------------------------------
 * CONTROL SERVER
 */

static ControlData*
control_data_new (void)
{
	ControlData *cdata = g_slice_new0 (ControlData);
	egg_buffer_init_full (&cdata->buffer, 128, egg_secure_realloc);
	cdata->position = 0;
	return cdata;
}

static void
control_data_free (gpointer data)
{
	ControlData *cdata = data;
	egg_buffer_uninit (&cdata->buffer);
	g_slice_free (ControlData, cdata);
}

static guint32
control_unlock_login (EggBuffer *buffer)
{
	gchar *master;
	gsize offset = 8;
	guint32 res;

	if (!egg_buffer_get_string (buffer, offset, &offset, &master, egg_secure_realloc))
		return GKD_CONTROL_RESULT_FAILED;

	if (gkd_login_unlock (master))
		res = GKD_CONTROL_RESULT_OK;
	else
		res = GKD_CONTROL_RESULT_DENIED;

	egg_secure_strfree (master);
	return res;
}

static guint32
control_change_login (EggBuffer *buffer)
{
	gsize offset = 8;
	guint32 res;
	gchar *master;
	gchar *original;

	if (!egg_buffer_get_string (buffer, offset, &offset, &original, egg_secure_realloc)) {
		return GKD_CONTROL_RESULT_FAILED;
	}

	if (!egg_buffer_get_string (buffer, offset, &offset, &master, egg_secure_realloc)) {
		egg_secure_strfree (original);
		return GKD_CONTROL_RESULT_FAILED;
	}

	if (gkd_login_change_lock (original, master))
		res = GKD_CONTROL_RESULT_OK;
	else
		res = GKD_CONTROL_RESULT_DENIED;

	egg_secure_strfree (master);
	egg_secure_strfree (original);
	return res;
}

static guint32
control_quit (EggBuffer *buffer)
{
	gkd_main_quit ();
	return GKD_CONTROL_RESULT_OK;
}

static guint32
control_initialize_components (EggBuffer *buffer)
{
	gchar *components;
	gchar **environment, **e;
	gsize offset = 8;
	gchar *x;
	int i;

	if (!egg_buffer_get_string (buffer, offset, &offset, &components, g_realloc))
		return GKD_CONTROL_RESULT_FAILED;

	if (!egg_buffer_get_stringv (buffer, offset, &offset, &environment, g_realloc)) {
		g_free (components);
		return GKD_CONTROL_RESULT_FAILED;
	}

	/* Accept environment from outside */
	for (e = environment; *e; ++e) {
		x = strchr (*e, '=');
		if (x) {
			*(x++) = 0;
			/* We're only interested in these environment variables */
			for (i = 0; GKD_UTIL_IN_ENVIRONMENT[i] != NULL; ++i) {
				if (g_str_equal (*e, GKD_UTIL_IN_ENVIRONMENT[i])) {
					g_setenv (*e, x, FALSE);
					break;
				}
			}
		}
	}

	g_strfreev (environment);

	/*
	 * We've now definitely received everything we need to run. Ask
	 * the daemon to complete the initialization.
	 */
	gkd_main_complete_initialization (components);
	g_free (components);

	return GKD_CONTROL_RESULT_OK;
}

static gboolean
control_output (GIOChannel *channel, GIOCondition cond, gpointer user_data)
{
	ControlData *cdata = user_data;
	EggBuffer *buffer = &cdata->buffer;
	int fd, res;

	fd = g_io_channel_unix_get_fd (channel);
	g_assert (cdata->position < buffer->len);

	if (cond & G_IO_OUT) {
		res = write (fd, buffer->buf + cdata->position, buffer->len - cdata->position);
		if (res < 0) {
			if (errno != EAGAIN && errno != EINTR)
				cdata->position = buffer->len;
		} else if (res == 0) {
			cdata->position = buffer->len;
		} else {
			cdata->position += res;
			g_assert (cdata->position <= buffer->len);
		}
	}

	if (cdata->position == buffer->len)
		cond |= G_IO_HUP;

	return (cond & G_IO_HUP) == 0;
}

static void
control_process (EggBuffer *req, GIOChannel *channel)
{
	ControlData *cdata = NULL;
	guint32 res;
	guint32 op;

	if (!egg_buffer_get_uint32 (req, 4, NULL, &op)) {
		g_message ("invalid operation sent to control socket");
		return;
	}

	switch (op) {
	case GKD_CONTROL_OP_UNLOCK:
		res = control_unlock_login (req);
		cdata = control_data_new ();
		egg_buffer_add_uint32 (&cdata->buffer, 0);
		egg_buffer_add_uint32 (&cdata->buffer, res);
		break;
	case GKD_CONTROL_OP_CHANGE:
		res = control_change_login (req);
		cdata = control_data_new ();
		egg_buffer_add_uint32 (&cdata->buffer, 0);
		egg_buffer_add_uint32 (&cdata->buffer, res);
		break;
	case GKD_CONTROL_OP_INITIALIZE:
		res = control_initialize_components (req);
		cdata = control_data_new ();
		egg_buffer_add_uint32 (&cdata->buffer, 0);
		egg_buffer_add_uint32 (&cdata->buffer, res);
		egg_buffer_add_stringv (&cdata->buffer, gkd_util_get_environment ());
		break;
	case GKD_CONTROL_OP_QUIT:
		res = control_quit (req);
		cdata = control_data_new ();
		egg_buffer_add_uint32 (&cdata->buffer, 0);
		egg_buffer_add_uint32 (&cdata->buffer, res);
		break;
	default:
		g_message ("received unsupported request operation on control socket: %d", (int)op);
		break;
	}

	if (cdata) {
		g_return_if_fail (!egg_buffer_has_error (&cdata->buffer));
		egg_buffer_set_uint32 (&cdata->buffer, 0, cdata->buffer.len);

		/*
		 * Quit messages are a bit strange because the daemon will quit.
		 * There are three syncronization issues here.
		 * 1. We need the response to be written right away, because if
		 *    we wait for the main loop it might not be written.
		 * 2. Callers may want to wait for the daemon to exit, so keep the
		 *    socket open until we do.
		 * 3. Prevent additional connections on the control socket (done via
		 *    gkd_control_stop()).
		 */
		if (op == GKD_CONTROL_OP_QUIT) {
			if (write (g_io_channel_unix_get_fd (channel),
			           cdata->buffer.buf, cdata->buffer.len) != cdata->buffer.len)
				g_message ("couldn't write response to close control request");
			if (res == GKD_CONTROL_RESULT_OK) {
				egg_cleanup_register ((GDestroyNotify)g_io_channel_unref,
				                      g_io_channel_ref (channel));
			}
			control_data_free (cdata);

		/* Any other response, send in the main loop */
		} else {
			g_io_add_watch_full (channel, G_PRIORITY_DEFAULT, G_IO_OUT | G_IO_HUP,
			                     control_output, cdata, control_data_free);
		}
	}
}

static gboolean
control_input (GIOChannel *channel, GIOCondition cond, gpointer user_data)
{
	ControlData *cdata = user_data;
	EggBuffer *buffer = &cdata->buffer;
	guint32 packet_size = 0;
	gboolean finished = FALSE;
	int fd, res;
	pid_t pid;
	uid_t uid;

	fd = g_io_channel_unix_get_fd (channel);

	if (cond & G_IO_IN) {

		/* Time for reading credentials */
		if (cdata->position == 0) {
			if (egg_unix_credentials_read (fd, &pid, &uid) < 0) {
				if (errno != EAGAIN && errno != EINTR)
					finished = TRUE;
			} else if (getuid () != uid) {
				g_message ("control request from bad uid: %u, should be %u\n", uid, getuid ());
				finished = TRUE;
			} else {
				cdata->position = 1;
			}

		/* Time for reading a packet size */
		} else if (egg_buffer_length (buffer) < 4) {
			egg_buffer_reserve (buffer, 4);
			res = read (fd, buffer->buf + buffer->len, 4 - buffer->len);
			if (res < 0) {
				if (errno != EAGAIN && errno != EINTR)
					finished = TRUE;
			} else if (res == 0) {
				finished = TRUE;
			} else {
				buffer->len += res;
			}

		/* Time for reading the packet */
		} else {
			if (!egg_buffer_get_uint32 (buffer, 0, NULL, &packet_size) || packet_size < 4) {
				g_message ("invalid packet size in control request");
				finished = TRUE;
			} else {
				g_assert (buffer->len < packet_size);
				egg_buffer_reserve (buffer, packet_size);
				res = read (fd, buffer->buf + buffer->len, packet_size - buffer->len);
				if (res < 0) {
					if (errno != EAGAIN && errno != EINTR)
						finished = TRUE;
				} else if (res == 0) {
					finished = TRUE;
				} else {
					buffer->len += res;
					g_assert (buffer->len <= packet_size);
				}
			}
		}

		/* Received a full packet, process */
		if (packet_size && buffer->len == packet_size) {
			control_process (buffer, channel);
			finished = TRUE;
		}
	}

	if (finished)
		cond |= G_IO_HUP;

	return (cond & G_IO_HUP) == 0;
}

static gboolean
control_accept (GIOChannel *channel, GIOCondition cond, gpointer callback_data)
{
	struct sockaddr_un addr;
	socklen_t addrlen;
	ControlData *cdata;
	GIOChannel *new_channel;
	int fd, new_fd;
	int val;

	fd = g_io_channel_unix_get_fd (channel);

	addrlen = sizeof (addr);
	new_fd = accept (fd, (struct sockaddr *) &addr, &addrlen);
	if (new_fd < 0) {
		g_warning ("couldn't accept new control request: %s", g_strerror (errno));
		return TRUE;
	}

	val = fcntl (new_fd, F_GETFL, 0);
	if (val < 0) {
		g_warning ("can't get control request fd flags: %s", g_strerror (errno));
		close (new_fd);
		return TRUE;
	}

	if (fcntl (new_fd, F_SETFL, val | O_NONBLOCK) < 0) {
		g_warning ("can't set control request to non-blocking io: %s", g_strerror (errno));
		close (new_fd);
		return TRUE;
	}

	cdata = control_data_new ();
	new_channel = g_io_channel_unix_new (new_fd);
	g_io_channel_set_close_on_unref (new_channel, TRUE);
	g_io_add_watch_full (new_channel, G_PRIORITY_DEFAULT, G_IO_IN | G_IO_HUP,
	                     control_input, cdata, control_data_free);
	g_io_channel_unref (new_channel);

	return TRUE;
}

void
gkd_control_stop (void)
{
	if (control_path) {
		unlink (control_path);
		g_free (control_path);
		control_path = NULL;
	}
}

gboolean
gkd_control_listen (void)
{
	GIOChannel *channel;
	int sock;

#ifdef WITH_SYSTEMD
	int res;

	res = sd_listen_fds (0);
	if (res > 1) {
		g_message ("too many file descriptors received");
		return FALSE;
	} else if (res == 1) {
		sock = SD_LISTEN_FDS_START + 0;
	} else
#endif
	{
		struct sockaddr_un addr;

		control_path = g_strdup_printf ("%s/control", gkd_util_get_master_directory ());
		egg_cleanup_register ((GDestroyNotify)gkd_control_stop, NULL);

		unlink (control_path);

		sock = socket (AF_UNIX, SOCK_STREAM, 0);
		if (sock < 0) {
			g_warning ("couldn't open socket: %s", g_strerror (errno));
			return FALSE;
		}

		memset (&addr, 0, sizeof (addr));
		addr.sun_family = AF_UNIX;
		g_strlcpy (addr.sun_path, control_path, sizeof (addr.sun_path));
		if (bind (sock, (struct sockaddr*) &addr, sizeof (addr)) < 0) {
			g_warning ("couldn't bind to control socket: %s: %s", control_path, g_strerror (errno));
			close (sock);
			return FALSE;
		}

		if (listen (sock, 128) < 0) {
			g_warning ("couldn't listen on control socket: %s: %s", control_path, g_strerror (errno));
			close (sock);
			return FALSE;
		}
	}

	if (egg_unix_credentials_setup (sock) < 0) {
		close (sock);
		return FALSE;
	}

	channel = g_io_channel_unix_new (sock);
	g_io_add_watch (channel, G_IO_IN | G_IO_HUP, control_accept, NULL);
	g_io_channel_set_close_on_unref (channel, TRUE);
	egg_cleanup_register ((GDestroyNotify)g_io_channel_unref, channel);

	return TRUE;
}
