/*
 * restorecond
 *
 * Copyright (C) 2006-2009 Red Hat
 * Copyright (C) 2020 Nicolas Iooss
 * see file 'COPYING' for use and warranty information
 *
 * 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.
 *
 * 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., 59 Temple Place, Suite 330, Boston, MA
 * 02111-1307  USA
 *
 * Authors:
 *   Dan Walsh <dwalsh@redhat.com>
 *   Nicolas Iooss <nicolas.iooss@m4x.org>
*/

#define _GNU_SOURCE
#include <sys/inotify.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <syslog.h>
#include <limits.h>
#include <fcntl.h>

#include <selinux/selinux.h>

#include "restorecond.h"
#include "stringslist.h"
#include <glib.h>
#include <glib-unix.h>

static int local_lock_fd = -1;

#ifdef HAVE_DBUS
#include <gio/gio.h>

static const char *DBUS_NAME = "org.selinux.Restorecond";

static void on_name_acquired(GDBusConnection *connection G_GNUC_UNUSED,
			     const gchar *name,
			     gpointer user_data G_GNUC_UNUSED)
{
	if (debug_mode)
		g_print("D-Bus name acquired: %s\n", name);
}

static void on_name_lost(GDBusConnection *connection G_GNUC_UNUSED,
			 const gchar *name, gpointer user_data)
{
	/* Exit when the D-Bus connection closes */
	GMainLoop *loop = user_data;

	if (debug_mode)
		g_print("D-Bus name lost (%s), exiting\n", name);
	g_main_loop_quit(loop);
}

/**
 * Try starting a D-Bus server on the session bus.
 * Returns -1 if the connection failed, so that a local server can be launched
 */
static int dbus_server(GMainLoop *loop)
{
	GDBusConnection *bus;
	guint client_id;

	bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, NULL);
	if (!bus)
		return -1;

	client_id = g_bus_own_name_on_connection(bus, DBUS_NAME,
						 G_BUS_NAME_OWNER_FLAGS_NONE,
						 on_name_acquired, on_name_lost,
						 loop, NULL);
	g_object_unref(bus);
	if (client_id == 0)
		return -1;

	return 0;
}

#endif

/* size of the event structure, not counting name */
#define EVENT_SIZE (sizeof(struct inotify_event))
/* reasonable guess as to size of 1024 events */
#define BUF_LEN (1024 * (EVENT_SIZE + 16))

static gboolean io_channel_callback(GIOChannel *source, GIOCondition condition,
				    gpointer data __attribute__((__unused__)))
{
	char buffer[BUF_LEN + 1];
	gsize bytes_read;
	unsigned int i = 0;

	if (condition & G_IO_IN) {
		/* Data is available. */
		g_io_channel_read_chars(source, buffer, sizeof(buffer),
					&bytes_read, NULL);

		if (!bytes_read) {
			/* Session/Terminal Ended */
			exit(0);
		}

		while (i < bytes_read) {
			struct inotify_event *event;
			event = (struct inotify_event *)&buffer[i];
			if (debug_mode)
				printf("wd=%d mask=%u cookie=%u len=%u\n",
				       event->wd, event->mask, event->cookie,
				       event->len);
			if (event->len)
				watch_list_find(event->wd, event->name);

			i += EVENT_SIZE + event->len;
		}
	}

	/* An error happened while reading
     the file. */

	if (condition & G_IO_NVAL)
		return FALSE;

	/* We have reached the end of the
     file. */

	if (condition & G_IO_HUP) {
		g_io_channel_shutdown(source, 0, NULL);
		exit(0);
		return FALSE;
	}

	/* Returning TRUE will make sure
     the callback remains associated
     to the channel. */

	return TRUE;
}

static gboolean stdin_callback(GIOChannel *source, GIOCondition condition,
			       gpointer data __attribute__((__unused__)))
{
	char buffer[256];
	gsize bytes_read;

	if (condition & G_IO_IN) {
		/* Data is available. */
		while (g_io_channel_read_chars(source, buffer, sizeof(buffer),
					       &bytes_read,
					       NULL) == G_IO_STATUS_NORMAL &&
		       bytes_read > 0)
			;
		if (bytes_read == 0)
			exit(0);
	}

	if (condition & (G_IO_HUP | G_IO_ERR | G_IO_NVAL))
		exit(0);

	return TRUE;
}

int start(void)
{
#ifdef HAVE_DBUS
	GDBusConnection *bus;
	GError *err = NULL;
	GVariant *result;

	/* Get a connection to the session bus */
	bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);
	if (!bus) {
		if (debug_mode)
			g_warning("Failed to connect to the D-BUS daemon: %s",
				  err->message);
		g_error_free(err);
		return 1;
	}

	/* Start restorecond D-Bus service by pinging its bus name
	 *
	 * https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-peer
	 */
	result = g_dbus_connection_call_sync(
		bus, DBUS_NAME, /* bus name */
		"/", /* object path */
		"org.freedesktop.DBus.Peer", /* interface */
		"Ping", /* method */
		NULL, /* parameters */
		NULL, /* reply_type */
		G_DBUS_CALL_FLAGS_NONE, -1, /* timeout_msec */
		NULL, &err);
	if (!result) {
		g_object_unref(bus);
		if (debug_mode)
			g_warning("Failed to start %s: %s", DBUS_NAME,
				  err->message);
		g_error_free(err);
		return 1;
	}
	g_object_unref(bus);
#endif /* HAVE_DBUS */
	return 0;
}

static int local_server(void)
{
	// ! dbus, run as local service
	char *ptr = NULL;
	if (asprintf(&ptr, "%s/.restorecond", homedir) < 0) {
		if (debug_mode)
			perror("asprintf");
		return -1;
	}
	local_lock_fd = open(ptr, O_CREAT | O_WRONLY | O_NOFOLLOW | O_CLOEXEC,
			     S_IRUSR | S_IWUSR);
	if (debug_mode)
		g_warning("Lock file: %s", ptr);

	free(ptr);
	if (local_lock_fd < 0) {
		if (debug_mode)
			perror("open");
		return -1;
	}
	if (flock(local_lock_fd, LOCK_EX | LOCK_NB) < 0) {
		if (debug_mode)
			perror("flock");
		close(local_lock_fd);
		local_lock_fd = -1;
		return -1;
	}
	/* watch for stdin/terminal going away */
	GIOChannel *in = g_io_channel_unix_new(0);
	g_io_channel_set_encoding(in, NULL, NULL);
	g_io_channel_set_flags(
		in, g_io_channel_get_flags(in) | G_IO_FLAG_NONBLOCK, NULL);
	g_io_add_watch_full(in, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
			    stdin_callback, NULL, NULL);

	return 0;
}

static void end_local_server(void)
{
	if (local_lock_fd >= 0)
		close(local_lock_fd);
	local_lock_fd = -1;
}

static int sigterm_handler(gpointer user_data)
{
	GMainLoop *loop = user_data;

	if (debug_mode)
		g_print("Received SIGTERM, exiting\n");
	g_main_loop_quit(loop);
	return FALSE;
}

int server(int master_fd, const char *watch_file)
{
	GMainLoop *loop;

	loop = g_main_loop_new(NULL, FALSE);

#ifdef HAVE_DBUS
	if (dbus_server(loop) != 0)
#endif /* HAVE_DBUS */
		if (local_server())
			goto end;

	read_config(master_fd, watch_file);

	if (watch_list_isempty())
		goto end;

	set_matchpathcon_flags(MATCHPATHCON_NOTRANS);

	GIOChannel *c = g_io_channel_unix_new(master_fd);
	g_io_channel_set_encoding(c, NULL, NULL);
	g_io_channel_set_flags(
		c, g_io_channel_get_flags(c) | G_IO_FLAG_NONBLOCK, NULL);

	g_io_add_watch_full(c, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
			    io_channel_callback, NULL, NULL);

	/* Handle SIGTERM */
	g_unix_signal_add_full(G_PRIORITY_DEFAULT, SIGTERM, sigterm_handler,
			       loop, NULL);

	g_main_loop_run(loop);

end:
	end_local_server();
	g_main_loop_unref(loop);
	return 0;
}
