/*
 *   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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * $Id: a34f01ab7d1045baa8c09d7e8d9fb2809ecd1e5b $
 *
 * @file radlock.c
 * @brief Utility to examine semaphores used to provide exclusive running rights for a process
 *
 * @copyright 2021 Arran Cudbard-Bell (a.cudbardb@freeradius.org)
 */
RCSID("$Id: a34f01ab7d1045baa8c09d7e8d9fb2809ecd1e5b $")

#include <freeradius-devel/autoconf.h>
#include <freeradius-devel/util/perm.h>
#include <freeradius-devel/util/sem.h>
#include <freeradius-devel/util/value.h>
#include <freeradius-devel/util/syserror.h>
#include <freeradius-devel/util/strerror.h>

#include <ctype.h>
#include <stdarg.h>
#include <stdbool.h>
#include <sys/ipc.h>
#include <sys/sem.h>

#ifdef HAVE_GETOPT_H
#  include <getopt.h>
#endif
#include <assert.h>

DIAG_OFF(unused-macros)
#define INFO(fmt, ...)		fprintf(stdout, fmt "\n", ## __VA_ARGS__)
DIAG_ON(unused-macros)

typedef enum {
	RADLOCK_INVALID = 0,
	RADLOCK_LOCK,		//!< Acquire the semaphore if it's at 0.
	RADLOCK_TRYLOCK,	//!< Try and lock the semaphore and return if we can't.
	RADLOCK_UNLOCK,		//!< Unlock the semaphore.
	RADLOCK_REMOVE,		//!< Remove the semaphore.
	RADLOCK_INFO,		//!< Information about the semaphore.
	RADLOCK_PERM		//!< Modify permissions for a given semaphore.
} fr_radlock_action_t;

static fr_table_num_sorted_t const radlock_action_table[] = {
	{ L("info"),		RADLOCK_INFO	},
	{ L("lock"),		RADLOCK_LOCK	},
	{ L("perm"),		RADLOCK_PERM	},
	{ L("remove"),		RADLOCK_REMOVE	},
	{ L("trylock"),		RADLOCK_TRYLOCK	},
	{ L("unlock"),		RADLOCK_UNLOCK	}
};
static size_t radlock_action_table_len = NUM_ELEMENTS(radlock_action_table);

static NEVER_RETURNS void usage(int ret)
{
	fprintf(stderr, "usage: radlock <file> [lock|trylock|unlock|remove|info|perm]\n");
	fprintf(stderr, "  -u <uid>         Desired user.\n");
	fprintf(stderr, "  -g <gid>         Desired group.\n");
	fprintf(stderr, "  -m <perm>        Octal permissions string.\n");
	fprintf(stderr, "  -h               This help text.\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "Simple utility to query the locking state of a config file\n");
	fr_exit_now(ret);
}

#define EXIT_WITH_FAILURE exit(EXIT_FAILURE)
#define EXIT_WITH_SUCCESS exit(EXIT_SUCCESS)

/**
 *
 * @hidecallgraph
 */
int main(int argc, char *argv[])
{
	char			c;
	fr_radlock_action_t	action;
	char const		*file;
	uid_t			uid = geteuid();
	bool			uid_set = false;
	gid_t			gid = getegid();
	bool			gid_set = false;
	long			mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH;
	bool			mode_set = false;
	int			sem_id;

	TALLOC_CTX		*autofree;

	autofree = talloc_autofree_context();

#ifndef NDEBUG
	if (fr_fault_setup(autofree, getenv("PANIC_ACTION"), argv[0]) < 0) {
		fr_perror("radict");
		fr_exit(EXIT_FAILURE);
	}
#endif

	talloc_set_log_stderr();

	while ((c = getopt(argc, argv, "u:g:m:h")) != -1) switch (c) {
		case 'u':
			if (fr_perm_uid_from_str(autofree, &uid, optarg) < 0) {
				fr_perror("radlock");
				EXIT_WITH_FAILURE;
			}
			uid_set = true;
			break;

		case 'g':
			if (fr_perm_uid_from_str(autofree, &gid, optarg) < 0) {
				fr_perror("radlock");
				EXIT_WITH_FAILURE;
			}
			gid_set = true;
			break;

		case 'm':
			mode = strtol(optarg, NULL, 0);	/* 0 base plus 0 prefix = octal */
			if (errno == EINVAL) {
				fr_perror("radlock - Bad mode value");
				EXIT_WITH_FAILURE;
			}
			mode_set = true;
			break;

		case 'h':
		default:
			usage(EXIT_SUCCESS);
	}
	argc -= optind;
	argv += optind;

	if (argc == 0) {
		fr_perror("radlock - Need file to operate on");
		usage(64);
	}

	if (argc == 1) {
		fr_perror("radlock - Need action, must be one of (lock|trylock|unlock|remove|info|perm)");
		usage(64);
	}

	file = argv[0];
	action = fr_table_value_by_str(radlock_action_table, argv[1], RADLOCK_INVALID);
	if (action == RADLOCK_INVALID) {
		fr_perror("radlock - Action must be one of (lock|trylock|unlock|remove|info|perm), got %s", argv[1]);
		usage(64);
	}

	if (action == RADLOCK_PERM) {
		fr_perror("radlock - At least one of -u, -g, -m must be specified");
		usage(64);
	}

	/*
	 *	Mismatch between the binary and the libraries it depends on
	 */
	if (fr_check_lib_magic(RADIUSD_MAGIC_NUMBER) < 0) {
		fr_perror("radlock");
		EXIT_WITH_FAILURE;
	}

	switch (action) {
	case RADLOCK_LOCK:
	case RADLOCK_TRYLOCK:
		sem_id = fr_sem_get(file, 0, uid, gid, false, false);
		if (sem_id < 0) {
			fr_perror("radlock");
			EXIT_WITH_FAILURE;
		}
		switch (fr_sem_wait(sem_id, file, false, action == RADLOCK_TRYLOCK)) {
		case 1:	/* Already locked */
		{
			pid_t pid;

			fr_sem_pid(&pid, sem_id);
			fr_perror("radlock - Can't lock \"%s\" already held by PID %u", file, pid);
			EXIT_WITH_FAILURE;
		}

		case 0:
			EXIT_WITH_SUCCESS;

		default:
			break;
		}
		fr_perror("radlock");
		EXIT_WITH_FAILURE;

	case RADLOCK_UNLOCK:
		sem_id = fr_sem_get(file, 0, uid, gid, false, true);
		if (sem_id == -4) EXIT_WITH_SUCCESS;
		if (sem_id < 0) {
			fr_perror("radlock");
			EXIT_WITH_FAILURE;
		}

	again:
		switch (fr_sem_post(sem_id, file, false)) {
		case 1:	/* already unlocked */
			EXIT_WITH_SUCCESS;

		case 0:
			goto again;

		default:
			break;
		}
		fr_perror("radlock");
		EXIT_WITH_FAILURE;

	case RADLOCK_REMOVE:
		sem_id = fr_sem_get(file, 0, uid, gid, false, true);
		if (sem_id == -4) EXIT_WITH_SUCCESS;

		if (fr_sem_close(sem_id, file) < 0) {
			fr_perror("radlock");
			EXIT_WITH_FAILURE;
		}
		EXIT_WITH_SUCCESS;

	case RADLOCK_INFO:
	{
		struct semid_ds	info;
		char		buff[10];
		unsigned int	perm = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
		bool		dead = false;
		pid_t		pid;
		int		ret;
		int		value;
		char const	*uid_str, *gid_str, *cuid_str, *cgid_str;

		sem_id = fr_sem_get(file, 0, uid, gid, false, true);
		if (sem_id == -4) EXIT_WITH_FAILURE;
		if (sem_id < 0) {
			fr_perror("radlock");
			EXIT_WITH_FAILURE;
		}

		if (semctl(sem_id, 0, IPC_STAT, &info) < 0) {
			fr_perror("radlock - Failed getting lock info for \"%s\": %s", file, fr_syserror(errno));
			EXIT_WITH_FAILURE;
		}

		if (fr_sem_pid(&pid, sem_id) < 0) {
			fr_perror("radlock");
			EXIT_WITH_FAILURE;
		}

		ret = kill(sem_id, 0);
		if ((ret < 0) && (errno == ESRCH)) dead = true;

		uid_str = fr_perm_uid_to_str(autofree, info.sem_perm.uid);
		if (!uid_str) uid_str = "";

		gid_str = fr_perm_gid_to_str(autofree, info.sem_perm.gid);
		if (!gid_str) gid_str = "";

		cuid_str = fr_perm_uid_to_str(autofree, info.sem_perm.cuid);
		if (!cuid_str) cuid_str = "";

		cgid_str = fr_perm_gid_to_str(autofree, info.sem_perm.cgid);
		if (!cgid_str) cgid_str = "";

		value = semctl(sem_id, 0, GETVAL);

		INFO("Locking information for \"%s\"", file);
		INFO("\tsemid         : %u", sem_id);
		INFO("\tPermissions   : %s", fr_perm_mode_to_str(buff, info.sem_perm.mode & perm));
		INFO("\tValue         : %u (%s)", value, value > 0 ? "locked" : "unlocked");
		INFO("Last Modified:");
		INFO("\tPID           : %u (%s)", pid, dead ? "dead" : "alive");
		INFO("\tUser          : %s (%u)", uid_str, info.sem_perm.uid);
		INFO("\tGroup         : %s (%u)", gid_str, info.sem_perm.gid);
		INFO("\tTime          : %s",
		     fr_asprintf(autofree, "%pV", fr_box_time(fr_time_from_sec(info.sem_otime))));
		INFO("Created:");
		INFO("\tUser          : %s (%u)", cuid_str, info.sem_perm.cuid);
		INFO("\tGroup         : %s (%u)", cgid_str, info.sem_perm.cgid);
		INFO("\tTime          : %s",
		     fr_asprintf(autofree, "%pV", fr_box_time(fr_time_from_sec(info.sem_ctime))));
	}
		EXIT_WITH_SUCCESS;

	case RADLOCK_PERM:
	{
		struct semid_ds	info;

		sem_id = fr_sem_get(file, 0, uid, gid, false, false);	/* Will create if does not already exist */
		if (sem_id < 0) {
			fr_perror("radlock");
			EXIT_WITH_FAILURE;
		}

		if (semctl(sem_id, 0, IPC_STAT, &info) < 0) {
			fr_perror("radlock - Failed getting lock info for \"%s\": %s",
				  fr_syserror(errno), file);
			EXIT_WITH_FAILURE;
		}

		if (uid_set) info.sem_perm.uid = uid;
		if (gid_set) info.sem_perm.gid = gid;
		if (mode_set) info.sem_perm.mode = mode;

		if (semctl(sem_id, 0, IPC_SET, &info) < 0) {
			fr_perror("radlock - Failed setting lock permissions for \"%s\": %s",
				  fr_syserror(errno), file);
			EXIT_WITH_FAILURE;
		}
	}
		EXIT_WITH_SUCCESS;

	case RADLOCK_INVALID:
		usage(64);
	}

	return 0;
}
